Complete overhaul
This commit is contained in:
parent
83e2b16af0
commit
8eeea0e700
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
layout-card.js binary
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules/
|
425
README.md
425
README.md
@ -1,237 +1,266 @@
|
||||
layout-card
|
||||
===========
|
||||
|
||||
[](https://github.com/custom-components/hacs)
|
||||
|
||||
Get more control over the placement of lovelace cards
|
||||
|
||||
# Installation instructions
|
||||
|
||||
This card requires [card-tools](https://github.com/thomasloven/lovelace-card-tools) to be installed.
|
||||
|
||||
For installation instructions [see this guide](https://github.com/thomasloven/hass-config/wiki/Lovelace-Plugins).
|
||||
|
||||
The recommended type of this plugin is: `js`
|
||||
Install `layout-card.js` as a `module`.
|
||||
|
||||
### If you are using [custom\_updater](https://github.com/custom-components/custom_updater):
|
||||
```yaml
|
||||
resources:
|
||||
- url: /customcards/github/thomasloven/card-tools.js?track=true
|
||||
type: js
|
||||
- url: /customcards/github/thomasloven/layout-card.js?track=true
|
||||
type: js
|
||||
- url: /local/layout-card.js
|
||||
type: module
|
||||
```
|
||||
|
||||
# Usage instructions
|
||||
## Usage
|
||||
|
||||
This card takes other cards and place them in different layouts.
|
||||
|
||||
This card works best if used in [panel mode](https://www.home-assistant.io/lovelace/views/#panel-mode).
|
||||
|
||||
Each layout consists of columns, the number of which is determined by your screen width and the settings of layout-card.
|
||||
|
||||
There are three different layouts:
|
||||
|
||||
### `auto`
|
||||
|
||||
This layout works the same way as the default lovelace layout. Cards are automatically added to stacks depending on their height.
|
||||
That may seem pointless, but trust me, this has it's uses.
|
||||
|
||||

|
||||
```yaml
|
||||
- title: Default
|
||||
panel: true
|
||||
cards:
|
||||
- type: custom:layout-card
|
||||
cards:
|
||||
- type: entities
|
||||
title: 1
|
||||
entities:
|
||||
- light.bed_light
|
||||
- type: entities
|
||||
title: 2
|
||||
entities:
|
||||
- light.bed_light
|
||||
- type: entities
|
||||
title: 3
|
||||
entities:
|
||||
- light.bed_light
|
||||
- type: entities
|
||||
title: 4
|
||||
entities:
|
||||
- light.bed_light
|
||||
- light.bed_light
|
||||
- light.bed_light
|
||||
- light.bed_light
|
||||
- type: entities
|
||||
title: 5
|
||||
entities:
|
||||
- light.bed_light
|
||||
- type: entities
|
||||
title: 6
|
||||
entities:
|
||||
- light.bed_light
|
||||
- type: entities
|
||||
title: 7
|
||||
entities:
|
||||
- light.bed_light
|
||||
- type: entities
|
||||
title: 8
|
||||
entities:
|
||||
- light.bed_light
|
||||
```
|
||||
|
||||
### horizontal
|
||||
|
||||
This layout will place cards in one column at a time, and then move on to the next - horizontally.
|
||||
|
||||

|
||||
```yaml
|
||||
- title: Horizontal
|
||||
panel: true
|
||||
cards:
|
||||
- type: custom:layout-card
|
||||
layout: horizontal
|
||||
cards:
|
||||
- type: entities
|
||||
```
|
||||
|
||||
### vertical
|
||||
|
||||
This layout will place cards vertically in one column
|
||||
|
||||

|
||||
```yaml
|
||||
- title: Vertical
|
||||
panel: true
|
||||
cards:
|
||||
- type: custom:layout-card
|
||||
layout: vertical
|
||||
cards:
|
||||
- type: entities
|
||||
```
|
||||
|
||||
It's OK to think I'm out of my mind at this point, but hang on; let me introduce the `break`.
|
||||
|
||||
Add a `- break` to the list of cards to break the column right there, and move on to the next one.
|
||||
|
||||

|
||||
```yaml
|
||||
- title: Manual breaks
|
||||
panel: true
|
||||
cards:
|
||||
- type: custom:layout-card
|
||||
layout: vertical
|
||||
cards:
|
||||
- type: entities
|
||||
title: 1
|
||||
...
|
||||
- type: entities
|
||||
title: 2
|
||||
...
|
||||
- break
|
||||
- type: entities
|
||||
title: 3
|
||||
...
|
||||
- break
|
||||
- type: entities
|
||||
title: 4
|
||||
...
|
||||
- type: entities
|
||||
title: 5
|
||||
...
|
||||
- type: entities
|
||||
title: 6
|
||||
...
|
||||
- break
|
||||
- type: entities
|
||||
title: 7
|
||||
...
|
||||
- type: entities
|
||||
title: 8
|
||||
...
|
||||
```
|
||||
|
||||
Breaks also work with the `horizontal` layout, and even `auto`. Experiment a bit.
|
||||
|
||||
## Configuration
|
||||
```yaml
|
||||
title: My view
|
||||
panel: true
|
||||
cards:
|
||||
- type: custom:layout-card
|
||||
type: custom:layout-card
|
||||
layout: <layout>
|
||||
column_num: <column num>
|
||||
max_columns: <max columns>
|
||||
column_width: <column width>
|
||||
max_width: <max width>
|
||||
min_height: <min height>
|
||||
min_height: <min_height>
|
||||
min_columns: <min_columns>
|
||||
max_columns: <max_columns>
|
||||
column_width: <column_width>
|
||||
max_width: <max_width>
|
||||
min_width: <min_width>
|
||||
flex_grow: <flex_grow>
|
||||
justify_content: <justify_content>
|
||||
ltr: <ltr>
|
||||
rebuild: <rebuild>
|
||||
cards:
|
||||
<cards>
|
||||
```
|
||||
|
||||
### `<layout>`
|
||||
Optional. Default: `auto`
|
||||
## Options
|
||||
- `<cards>` **Required** A list of lovelace cards to display.
|
||||
- `<layout>` The layout method to use. `auto`, `vertical` or `horizontal`. See below. Default: `auto`.
|
||||
- `<min_height>` The minimum length of a column in `auto` layout.
|
||||
- `<min_columns>` The minimum number of columns to use. Default: `1`.
|
||||
- `<max_columns>` The maximum number of columns to use. Default: `100`.
|
||||
- `<column_width>` Width of columns. Default: `300px`.
|
||||
- `<max_width>`, `<min_width>`, `<flex_grow>` Set the `max-width`, `min-width` and `flex-grow` CSS properties of the columns manually. Default: `column_width or 500px`, `undefined`, `undefined`.
|
||||
- `<justify_content>` Set the `justify-content` CSS property of the column container. Default: `center`.
|
||||
|
||||
Either `auto`, `vertical` or `horizontal`.
|
||||
## Layouts
|
||||
|
||||
### `<column num>`
|
||||
Optional. Default: 1
|
||||
The basic concept of this card is that it takes a number of other cards, and places them in the browser window, just like lovelace does normally, but allowing you a bit more control.
|
||||
|
||||
The minimum number of columns to make. Note that if a column has no cards, it will be removed regardless.
|
||||
Since `layout-card` is a card in it self its area of effect will be limited to the width of a card, and thus you will (almost) always want to use it in [panel mode](https://www.home-assistant.io/lovelace/views/#panel-mode):
|
||||
|
||||
### `<max columns>`
|
||||
Optional. Default: 100
|
||||
```yaml
|
||||
views:
|
||||
- title: My view
|
||||
panel: true
|
||||
cards:
|
||||
- type: custom:layout-card
|
||||
...
|
||||
```
|
||||
|
||||
The maximum number of columns to make.
|
||||
### `auto` layout
|
||||
|
||||
### `<column width>`
|
||||
Optional. Default: 300
|
||||
The auto layout works in the same way as the default lovelace layout.
|
||||
|
||||
The minimum width of a column in pixels.
|
||||
It follows a simple process.
|
||||
- A number of columns are prepared based on the screen width and `<column_widt>`.
|
||||
- The number of columns is clamped between `<min_columns>` and `<max_columns>`
|
||||
- Cards have a `cardHeight`, which is calculated from their content. One unit is roughly 50 pixels tall.
|
||||
- Each new card is added to the first row which is less than `<min_height>` units tall.
|
||||
- If all columns are taller than `<min_height>`, the card is added to the shortes column.
|
||||
- Once all cards have been placed, any remaining empty columns are removed.
|
||||
|
||||
```yaml
|
||||
type: custom:layout-card
|
||||
cards:
|
||||
- type: entities
|
||||
title: 1
|
||||
entities:
|
||||
- light.bed_light
|
||||
- type: entities
|
||||
title: 2
|
||||
entities:
|
||||
- light.bed_light
|
||||
- type: entities
|
||||
title: 3
|
||||
entities:
|
||||
- light.bed_light
|
||||
- type: entities
|
||||
title: 4
|
||||
entities:
|
||||
- light.bed_light
|
||||
- light.bed_light
|
||||
- light.bed_light
|
||||
- light.bed_light
|
||||
- type: entities
|
||||
title: 5
|
||||
entities:
|
||||
- light.bed_light
|
||||
- type: entities
|
||||
title: 6
|
||||
entities:
|
||||
- light.bed_light
|
||||
- type: entities
|
||||
title: 7
|
||||
entities:
|
||||
- light.bed_light
|
||||
- type: entities
|
||||
title: 8
|
||||
entities:
|
||||
- light.bed_light
|
||||
```
|
||||

|
||||
|
||||
> Note: The same 8 cards will be used in the following examples and will be omitted for clarity.
|
||||
|
||||
### `horizontal` layout
|
||||
|
||||
The horizontal layout calculates the number of columns just like the `auto` layout.
|
||||
It then places the first card in the first column, the second card in the second columns, and so on. Once it reaches the last column, it starts over from the first.
|
||||
|
||||
```yaml
|
||||
type: custom:layout-card
|
||||
layout: horizontal
|
||||
cards:
|
||||
- ...
|
||||
```
|
||||

|
||||
|
||||
### `vertical` layout
|
||||
|
||||
The vertical layout calculates the number of columns just like the `auto` layout.
|
||||
It then places every card in the first column.
|
||||
|
||||
```yaml
|
||||
type: custom:layout-card
|
||||
layout: vertical
|
||||
cards:
|
||||
- ...
|
||||
```
|
||||

|
||||
|
||||
It's OK to think I'm out of my mind at this point. And if you don't, you probably will once I claim that this is probably the most useful layout.
|
||||
|
||||
Still here? OK. Let me tell you about the `break`.
|
||||
|
||||
### `- break`
|
||||
|
||||
Just add `- break` to the list of `<cards>` to make card placer move on to the next column for the next card.
|
||||
|
||||
This is most useful in the `vertical` layout, but will work in the `horizontal` and `auto` layouts too.
|
||||
|
||||
```yaml
|
||||
type: custom:layout-card
|
||||
layout: vertical
|
||||
cards:
|
||||
- type: entities
|
||||
title: 1
|
||||
...
|
||||
- type: entities
|
||||
title: 2
|
||||
...
|
||||
- break
|
||||
- type: entities
|
||||
title: 3
|
||||
...
|
||||
- break
|
||||
- type: entities
|
||||
title: 4
|
||||
...
|
||||
- type: entities
|
||||
title: 6
|
||||
...
|
||||
- break
|
||||
- type: entities
|
||||
title: 7
|
||||
...
|
||||
```
|
||||

|
||||
|
||||
## Tweaking layouts
|
||||
|
||||
- First of all `<min_columns>` and `<max_columns>` options, which can be used to force the number of columns displayed:
|
||||
|
||||
```yaml
|
||||
type: custom:layout-card
|
||||
layout: vertical
|
||||
min_columns: 7
|
||||
cards:
|
||||
- ...
|
||||
```
|
||||

|
||||
|
||||
> Note: See how squeezing cards too tight will make them look weird? Keep this in mind, and don't send me bug reports about it.
|
||||
> Your toggles would pop out too if someone forced you into a 100 pixel box.
|
||||
|
||||
- The width of columns can be specified either all together...:
|
||||
|
||||
```yaml
|
||||
type: custom:layout-card
|
||||
column_width: 200
|
||||
cards:
|
||||
- ...
|
||||
```
|
||||

|
||||
|
||||
- ...or as a list of column widths:
|
||||
|
||||
```yaml
|
||||
type: custom:layout-card
|
||||
column_width: [200, 300, 150]
|
||||
cards:
|
||||
- ...
|
||||
```
|
||||

|
||||
|
||||
If there are more column than values in the list, the last value in the list will be used for the remaining columns.
|
||||
|
||||
- Values can be specified either in pixels or in percentages:
|
||||
|
||||
```yaml
|
||||
type: custom:layout-card
|
||||
column_width: 30%
|
||||
cards:
|
||||
- ...
|
||||
```
|
||||

|
||||
|
||||
|
||||
### `<max width>`
|
||||
Optional. Default: 500
|
||||
- Further tweaks can be made in the same way using `<max_width>` and `<min_width>`, but most of the time `<column_width>` should be enough.
|
||||
|
||||
The maximum width of a column in pixels.
|
||||
`<max width>` can also be an array of values ending with `%` or `px`. In that case the values will specify the width of each column. This works best when combined with `<column count>` and `<max columns>`.
|
||||
- `<flex_grow>` (single value or list) and `<justify_content>` (single value) can be used to tweak the CSS flexbox settings of the layout. See [this excelent guide](https://css-tricks.com/snippets/css/a-guide-to-flexbox/) for more info.
|
||||
|
||||
Ex: `max_width: [70%, 300px, 30%]` will result in three columns, where the center one is 300 pixels wide, and the left and right divide the remaining space in a 70/30 ratio.
|
||||
- `<rtl>` will make the columns display from right to left instead of left to right.
|
||||
|
||||

|
||||
|
||||
This allows for some really interesting layout options when combined with stacks. Play around!
|
||||
## A few tips
|
||||
- Layout-cards can be placed inside other layout-cards or in vertical-stack cards:
|
||||

|
||||
|
||||

|
||||
- [gap-card](https://github.com/thomasloven/lovelace-gap-card) can be used to leave a gap in the layout:
|
||||

|
||||
|
||||
### `<min height>`
|
||||
Optional. Default: 5
|
||||
- The card list can be populated automatically using [auto-entities](https://github.com/thomasloven/lovelace-auto-entities)
|
||||
|
||||
The number of units needed before a column is considered not empty.
|
||||
|
||||
### `<rtl>`
|
||||
Optional. Default: false
|
||||
|
||||
If set to true, columns will be placed right-to-left.
|
||||
|
||||
### `<rebuild>`
|
||||
Optional. Default: false
|
||||
|
||||
If set, a rebuild of the layout will be triggered after this many milliseconds. May be useful if your layout looks different after a page reload or when you return to the view, but it will cause your screen to flash once.
|
||||
|
||||
### `<cards>`
|
||||
Required.
|
||||
|
||||
A list of cards to put in the layout.
|
||||
|
||||
The list can also contain `- break` - see description of layouts above.
|
||||
|
||||
# FAQ
|
||||
|
||||
### Can I leave a gap in the layout?
|
||||
|
||||
Yes. By using the compainon card: [gap-card](https://github.com/thomasloven/lovelace-gap-card).
|
||||
```yaml
|
||||
type: custom:auto-entities
|
||||
filter:
|
||||
include:
|
||||
- domain: light
|
||||
options:
|
||||
type: light # Make sure to specify a card type for every filter
|
||||
- domain: climate
|
||||
options:
|
||||
type: thermostat
|
||||
exclude:
|
||||
- state: unavailable
|
||||
sort:
|
||||
method: name
|
||||
ignore_case: true
|
||||
card:
|
||||
type: custom:layout-card
|
||||
```
|
||||

|
||||
|
||||
---
|
||||
<a href="https://www.buymeacoffee.com/uqD6KHCdJ" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/white_img.png" alt="Buy Me A Coffee" style="height: auto !important;width: auto !important;" ></a>
|
204
layout-card.js
204
layout-card.js
@ -1,62 +1,35 @@
|
||||
customElements.whenDefined('card-tools').then(() => {
|
||||
let cardTools = customElements.get('card-tools');
|
||||
class LayoutCard extends cardTools.LitElement {
|
||||
|
||||
async setConfig(config) {
|
||||
this.config = config;
|
||||
this.layout = config.layout || 'auto';
|
||||
this.minCols = config.column_num || 1;
|
||||
this.maxCols = config.max_columns || 100;
|
||||
this.colWidth = config.column_width || 400;
|
||||
this.maxWidth = config.max_width || 500;
|
||||
this.minHeight = config.min_height || 5;
|
||||
this.rtl = config.rtl || false;
|
||||
this.cardSize = 1;
|
||||
|
||||
this.make_cards();
|
||||
|
||||
window.addEventListener('resize', () => this.build());
|
||||
window.addEventListener('hass-open-menu', () => setTimeout(() => this.build(), 100));
|
||||
window.addEventListener('hass-close-menu', () => setTimeout(() => this.build(), 100));
|
||||
window.addEventListener('location-changed', () => {
|
||||
if(location.hash === "") setTimeout(() =>
|
||||
this.build(), 100)
|
||||
});
|
||||
if(config.rebuild)
|
||||
window.setTimeout(() => this.build(), config.rebuild);
|
||||
}
|
||||
|
||||
render() {
|
||||
return cardTools.LitHtml`
|
||||
<div id="columns"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
if(this.parentElement && this.parentElement.id !== "view")
|
||||
{
|
||||
this.style.padding = "0";
|
||||
}
|
||||
if(this.rtl)
|
||||
this.shadowRoot.querySelector("#columns").style.flexDirection = 'row-reverse';
|
||||
this.build();
|
||||
this._cardModder = {
|
||||
target: this,
|
||||
styles: this.shadowRoot.querySelector("style")
|
||||
};
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return cardTools.LitCSS`
|
||||
!function(t){var e={};function n(i){if(e[i])return e[i].exports;var s=e[i]={i:i,l:!1,exports:{}};return t[i].call(s.exports,s,s.exports,n),s.l=!0,s.exports}n.m=t,n.c=e,n.d=function(t,e,i){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:i})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var s in t)n.d(i,s,function(e){return t[e]}.bind(null,s));return i},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=0)}([function(t,e,n){"use strict";n.r(e);const i=customElements.get("home-assistant-main")?Object.getPrototypeOf(customElements.get("home-assistant-main")):Object.getPrototypeOf(customElements.get("hui-view")),s=i.prototype.html,o=i.prototype.css;function r(){return document.querySelector("home-assistant").hass}function a(t,e,n){t.forEach(t=>{if(!t)return;const i=e[function(){let t=0;for(let i=0;i<e.length;i++){if(e[i].length<n.min_height)return i;e[i].length<e[t].length&&(t=i)}return t}()];i.appendChild(t),i.length+=t.getCardSize?t.getCardSize():1})}customElements.define("layout-card",class extends i{static get properties(){return{hass:{},_config:{}}}async setConfig(t){this._config={min_height:5,column_width:300,max_width:t.column_width||"500px",min_columns:t.column_num||1,max_columns:100,...t},this.cards=[],this.columns=[]}async firstUpdated(){window.addEventListener("resize",()=>this.place_cards()),window.addEventListener("hass-open-menu",()=>setTimeout(()=>this.place_cards(),100)),window.addEventListener("hass-close-menu",()=>setTimeout(()=>this.place_cards(),100)),window.addEventListener("location-changed",()=>{""===location.hash&&setTimeout(()=>this.place_cards(),100)})}async updated(t){this.cards.length||(this.cards=await this.build_cards(),this.place_cards()),t.has("hass")&&this.hass&&this.cards&&this.cards.forEach(t=>{t&&(t.hass=this.hass)})}async build_card(t){if("break"===t)return null;const e=document.createElement("card-maker");return e.config=t,e.hass=r(),this.shadowRoot.querySelector("#staging").appendChild(e),new Promise((t,n)=>e.updateComplete.then(()=>t(e)))}async build_cards(){const t=this.shadowRoot.querySelector("#staging");for(;t.lastChild;)t.removeChild(t.lastChild);return Promise.all((this._config.entities||this._config.cards).map(t=>this.build_card(t)))}place_cards(){const t=this.shadowRoot.querySelector("#columns").clientWidth;this.columns=function(t,e,n){const i=t=>"string"==typeof t&&t.endsWith("%")?Math.floor(e*parseInt(t)/100):parseInt(t);let s=0;if("object"==typeof n.column_width){let t=e;for(;t>0;){let e=n.column_width[s];void 0===e&&(e=n.column_width.slice(-1)[0]),t-=i(e),s+=1}s=Math.max(s-1,1)}else s=Math.floor(e/i(n.column_width));s=Math.max(s,n.min_columns),s=Math.min(s,n.max_columns);let o=[];for(let t=0;t<s;t++){const t=document.createElement("div");t.classList.add("column"),t.length=0,o.push(t)}switch(n.layout){case"horizontal":!function(t,e,n){let i=0;t.forEach(t=>{if(i+=1,!t)return;const n=e[(i-1)%e.length];n.appendChild(t),n.length+=t.getCardSize?t.getCardSize():1})}(t,o);break;case"vertical":!function(t,e,n){let i=0;t.forEach(t=>{if(!t)return void(i+=1);const n=e[i%e.length];n.appendChild(t),n.length+=t.getCardSize?t.getCardSize():1})}(t,o);break;case"auto":default:a(t,o,n)}return o=o.filter(t=>t.childElementCount>0)}(this.cards,t,this._config),this.format_columns(),this.requestUpdate()}format_columns(){const t=(t,e,n,i="px")=>{if(void 0===this._config[e])return"";let s=`${t}: `;const o=this._config[e];return"object"==typeof o?o.length>n?s+=`${o[n]}`:s+=`${o.slice(-1)}`:s+=`${o}`,s.endsWith("px")||s.endsWith("%")||(s+=i),s+";"};for(const[e,n]of this.columns.entries()){const i=[t("max-width","max_width",e),t("min-width","min_width",e),t("width","column_width",e),t("flex-grow","flex_grow",e,"")];n.style.cssText="".concat(...i)}}getCardSize(){if(this.columns)return Math.max.apply(Math,this.columns.map(t=>t.length))}isPanel(){let t=this.parentElement,e=10;for(;e--;){if("hui-panel-view"===t.localName)return!0;if("div"===t.localName)return!1;t=t.parentElement}return!1}render(){return s`
|
||||
<div id="columns"
|
||||
class="
|
||||
${this._config.rtl?"rtl":" "}
|
||||
${this.isPanel()?"panel":" "}
|
||||
"
|
||||
style="
|
||||
${this._config.justify_content?`justify-content: ${this._config.justify_content};`:""}
|
||||
">
|
||||
${this.columns.map(t=>s`
|
||||
${t}
|
||||
`)}
|
||||
</div>
|
||||
<div id="staging"></div>
|
||||
`}static get styles(){return o`
|
||||
:host {
|
||||
padding: 8px 4px 0;
|
||||
padding: 0 4px;
|
||||
display: block;
|
||||
margin-bottom: 0!important;
|
||||
}
|
||||
|
||||
#columns {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
margin-top: -8px;
|
||||
}
|
||||
#columns.rtl {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
#columns.panel {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.column {
|
||||
@ -65,130 +38,19 @@ class LayoutCard extends cardTools.LitElement {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.column > * {
|
||||
card-maker>* {
|
||||
display: block;
|
||||
margin: 4px 4px 8px;
|
||||
}
|
||||
|
||||
.column > *:first-child {
|
||||
margin-top: 0;
|
||||
card-maker:first-child>* {
|
||||
margin-top: 8px;
|
||||
}
|
||||
`;
|
||||
card-maker:last-child>* {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
make_cards() {
|
||||
this._cards = this.config.cards.map((c) => {
|
||||
if (typeof c === 'string') return c;
|
||||
const card = cardTools.createCard(c);
|
||||
if(this._hass) card.hass = this._hass;
|
||||
this.appendChild(card); // Place card in DOM to get size
|
||||
return card;
|
||||
});
|
||||
#staging {
|
||||
visibility: hidden;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
update_columns() {
|
||||
const width = (this.shadowRoot && this.shadowRoot.querySelector("#columns").clientWidth) || (this.parentElement && this.parentElement.clientWidth);
|
||||
this.colNum = Math.floor(width / this.colWidth) || 1;
|
||||
this.colNum = Math.max(this.colNum, this.minCols);
|
||||
this.colNum = Math.min(this.colNum, this.maxCols);
|
||||
}
|
||||
|
||||
build() {
|
||||
if (this.offsetParent === null) return;
|
||||
const root = this.shadowRoot.querySelector("#columns");
|
||||
while(root.lastChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
|
||||
this.update_columns();
|
||||
|
||||
if(!this._cards) this.make_cards();
|
||||
|
||||
let cols = [];
|
||||
let colSize = [];
|
||||
for(let i = 0; i < this.colNum; i++) {
|
||||
cols.push([]);
|
||||
colSize.push(0);
|
||||
}
|
||||
|
||||
const shortestCol = () => {
|
||||
let i = 0;
|
||||
for(let j = 0; j < this.colNum; j++) {
|
||||
if(colSize[j] < this.min_height)
|
||||
return j;
|
||||
if(colSize[j] < colSize[i])
|
||||
i = j;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
this._cards.forEach((c) => {
|
||||
const isBreak = (typeof(c) === 'string');
|
||||
const sz = c.getCardSize ? c.getCardSize() : 1;
|
||||
|
||||
switch(this.layout) {
|
||||
case 'horizontal':
|
||||
if(i >= this.colNum) i = 0;
|
||||
i += 1;
|
||||
if(isBreak) break;
|
||||
cols[i-1].push(c);
|
||||
colSize[i-1] += sz;
|
||||
break;
|
||||
case 'vertical':
|
||||
if(isBreak){
|
||||
i += 1;
|
||||
if(i >= this.colNum)
|
||||
i = 0;
|
||||
break;
|
||||
}
|
||||
cols[i].push(c);
|
||||
colSize[i] += sz;
|
||||
break;
|
||||
case 'auto':
|
||||
default:
|
||||
if(isBreak) break;
|
||||
cols[shortestCol()].push(c);
|
||||
colSize[shortestCol()] += sz;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
cols = cols.filter((c) => c.length > 0);
|
||||
cols.forEach((c, i) => {
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('column');
|
||||
c.forEach((e) => div.appendChild(e));
|
||||
root.appendChild(div);
|
||||
if(cols.length > 1 && typeof(this.maxWidth) === 'object') {
|
||||
div.style.setProperty('max-width', this.maxWidth[i]);
|
||||
} else {
|
||||
div.style.setProperty('max-width', this.maxWidth+'px');
|
||||
}
|
||||
});
|
||||
|
||||
this.cardSize = Math.max.apply(null, colSize);
|
||||
}
|
||||
|
||||
set hass(hass) {
|
||||
this._hass = hass;
|
||||
if(this._cards)
|
||||
this._cards
|
||||
.filter((c) => typeof(c) !== 'string')
|
||||
.forEach((c) => c.hass = hass);
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
return this.cardSize;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
customElements.define('layout-card', LayoutCard);
|
||||
});
|
||||
window.setTimeout(() => {
|
||||
if(customElements.get('card-tools')) return;
|
||||
customElements.define('layout-card', class extends HTMLElement{
|
||||
setConfig() { throw new Error("Can't find card-tools. See https://github.com/thomasloven/lovelace-card-tools");}
|
||||
});
|
||||
}, 2000);
|
||||
`}})}]);
|
4051
package-lock.json
generated
Normal file
4051
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
package.json
Normal file
19
package.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "layout-card",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "webpack",
|
||||
"watch": "webpack --watch --mode=development"
|
||||
},
|
||||
"author": "Thomas Lovén",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"webpack": "^4.41.2",
|
||||
"webpack-cli": "^3.3.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"card-tools": "github:thomasloven/lovelace-card-tools"
|
||||
}
|
||||
}
|
102
src/layout.js
Normal file
102
src/layout.js
Normal file
@ -0,0 +1,102 @@
|
||||
function defaultLayout(cards, cols, config) {
|
||||
function shortestCol() {
|
||||
// Find the shortest column or the first one less than min_height
|
||||
let i = 0;
|
||||
for(let j=0; j<cols.length; j++){
|
||||
if(cols[j].length < config.min_height)
|
||||
return j;
|
||||
if(cols[j].length < cols[i].length)
|
||||
i = j;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
cards.forEach((c) => {
|
||||
if(!c) return; // Gap. Skip
|
||||
const col = cols[shortestCol()];
|
||||
col.appendChild(c);
|
||||
col.length += c.getCardSize ? c.getCardSize() : 1;
|
||||
});
|
||||
}
|
||||
|
||||
function horizontalLayout(cards, cols, config) {
|
||||
let i = 0;
|
||||
cards.forEach((c) => {
|
||||
i += 1;
|
||||
if(!c) return; // Gap. Skip
|
||||
const col = cols[(i-1)%cols.length];
|
||||
col.appendChild(c);
|
||||
col.length += c.getCardSize ? c.getCardSize() : 1;
|
||||
})
|
||||
}
|
||||
|
||||
function verticalLayout(cards, cols, config) {
|
||||
let i = 0;
|
||||
cards.forEach((c) => {
|
||||
if(!c) { // Gap. Skip to next column
|
||||
i += 1;
|
||||
return;
|
||||
}
|
||||
const col = cols[(i)%cols.length];
|
||||
col.appendChild(c);
|
||||
col.length += c.getCardSize ? c.getCardSize() : 1;
|
||||
})
|
||||
}
|
||||
|
||||
export function buildLayout(cards, width, config) {
|
||||
|
||||
const parseValue = (val) => {
|
||||
// Accepts e.g. `5` , `5px` or `50%`.
|
||||
if(typeof(val) === "string")
|
||||
if(val.endsWith("%"))
|
||||
return Math.floor(width*parseInt(val)/100);
|
||||
return parseInt(val)
|
||||
}
|
||||
|
||||
let colnum = 0;
|
||||
if(typeof(config.column_width) === "object") {
|
||||
// If many widths are given, keep adding collumns as long as there is any
|
||||
// horizontal space left
|
||||
let calcWidth = width;
|
||||
while(calcWidth > 0) {
|
||||
// If there are not enough values, use the last one
|
||||
let w = config.column_width[colnum];
|
||||
if(w === undefined) w = config.column_width.slice(-1)[0];
|
||||
|
||||
calcWidth -= parseValue(w);
|
||||
colnum += 1;
|
||||
}
|
||||
|
||||
colnum = Math.max(colnum-1, 1);
|
||||
|
||||
} else {
|
||||
colnum = Math.floor(width / parseValue(config.column_width));
|
||||
}
|
||||
colnum = Math.max(colnum, config.min_columns);
|
||||
colnum = Math.min(colnum, config.max_columns);
|
||||
|
||||
let cols = [];
|
||||
for (let i = 0; i < colnum; i++) {
|
||||
const newCol = document.createElement("div");
|
||||
newCol.classList.add("column");
|
||||
newCol.length = 0;
|
||||
cols.push(newCol);
|
||||
}
|
||||
|
||||
switch(config.layout) {
|
||||
case 'horizontal':
|
||||
horizontalLayout(cards, cols, config);
|
||||
break;
|
||||
case 'vertical':
|
||||
verticalLayout(cards, cols, config);
|
||||
break;
|
||||
case 'auto':
|
||||
default:
|
||||
defaultLayout(cards, cols, config);
|
||||
}
|
||||
|
||||
// Remove empty columns
|
||||
cols = cols.filter((c) => c.childElementCount > 0);
|
||||
|
||||
return cols;
|
||||
}
|
211
src/main.js
Normal file
211
src/main.js
Normal file
@ -0,0 +1,211 @@
|
||||
import { LitElement, html, css } from "card-tools/src/lit-element";
|
||||
// import "card-tools/src/card-maker";
|
||||
import { hass } from "card-tools/src/hass";
|
||||
|
||||
import {buildLayout} from "./layout";
|
||||
|
||||
class LayoutCard extends LitElement {
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {},
|
||||
_config: {},
|
||||
};
|
||||
}
|
||||
|
||||
async setConfig(config) {
|
||||
this._config = {
|
||||
min_height: 5,
|
||||
|
||||
column_width: 300,
|
||||
max_width: config.column_width || "500px",
|
||||
|
||||
min_columns: config.column_num || 1,
|
||||
max_columns: 100,
|
||||
|
||||
...config,
|
||||
}
|
||||
|
||||
this.cards = [];
|
||||
this.columns = [];
|
||||
}
|
||||
|
||||
async firstUpdated() {
|
||||
window.addEventListener('resize', () => this.place_cards());
|
||||
window.addEventListener('hass-open-menu', () => setTimeout(() => this.place_cards(), 100));
|
||||
window.addEventListener('hass-close-menu', () => setTimeout(() => this.place_cards(), 100));
|
||||
window.addEventListener('location-changed', () => {
|
||||
if(location.hash === "")
|
||||
setTimeout(() => this.place_cards(), 100)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
async updated(changedproperties) {
|
||||
if(!this.cards.length) {
|
||||
// Build cards and layout
|
||||
this.cards = await this.build_cards();
|
||||
this.place_cards();
|
||||
}
|
||||
|
||||
if(changedproperties.has("hass") && this.hass && this.cards) {
|
||||
// Update the hass object of every card
|
||||
this.cards.forEach((c) => {
|
||||
if(!c) return;
|
||||
c.hass = this.hass;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async build_card(c) {
|
||||
if(c === "break")
|
||||
return null;
|
||||
const el = document.createElement("card-maker");
|
||||
el.config = c;
|
||||
el.hass = hass();
|
||||
// Cards are initially placed in the staging area
|
||||
// That places them in the DOM and lets us read their getCardSize() function
|
||||
this.shadowRoot.querySelector("#staging").appendChild(el);
|
||||
return new Promise((resolve, reject) => el.updateComplete.then(() => resolve(el)));
|
||||
}
|
||||
|
||||
async build_cards() {
|
||||
// Clear out any cards in the staging area which might have been built but not placed
|
||||
const staging = this.shadowRoot.querySelector("#staging");
|
||||
while(staging.lastChild)
|
||||
staging.removeChild(staging.lastChild);
|
||||
return Promise.all(
|
||||
(this._config.entities || this._config.cards)
|
||||
.map((c) => this.build_card(c))
|
||||
);
|
||||
}
|
||||
|
||||
place_cards() {
|
||||
const width = this.shadowRoot.querySelector("#columns").clientWidth;
|
||||
|
||||
this.columns = buildLayout(
|
||||
this.cards,
|
||||
width,
|
||||
this._config
|
||||
);
|
||||
|
||||
this.format_columns();
|
||||
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
format_columns() {
|
||||
const renderProp = (name, property, index, unit="px") => {
|
||||
// Check if the config option is specified
|
||||
if (this._config[property] === undefined) return "";
|
||||
|
||||
let retval = `${name}: `;
|
||||
const prop = this._config[property];
|
||||
if (typeof(prop) === "object")
|
||||
// Get the last value if there are not enough
|
||||
if(prop.length > index)
|
||||
retval += `${prop[index]}`;
|
||||
else
|
||||
retval += `${prop.slice(-1)}`;
|
||||
else
|
||||
retval += `${prop}`;
|
||||
|
||||
// Add unit (px) if necessary
|
||||
if(!retval.endsWith("px") && !retval.endsWith("%")) retval += unit;
|
||||
return retval + ";"
|
||||
}
|
||||
|
||||
// Set element style for each column
|
||||
for(const [i, c] of this.columns.entries()) {
|
||||
const styles = [
|
||||
renderProp("max-width", "max_width", i),
|
||||
renderProp("min-width", "min_width", i),
|
||||
renderProp("width", "column_width", i),
|
||||
renderProp("flex-grow", "flex_grow", i, ""),
|
||||
]
|
||||
c.style.cssText = ''.concat(...styles);
|
||||
}
|
||||
}
|
||||
|
||||
getCardSize() {
|
||||
if(this.columns)
|
||||
return Math.max.apply(Math, this.columns.map((c) => c.length));
|
||||
}
|
||||
|
||||
isPanel() {
|
||||
let el = this.parentElement;
|
||||
let steps = 10;
|
||||
while(steps--) {
|
||||
if(el.localName === "hui-panel-view") return true;
|
||||
if(el.localName === "div") return false;
|
||||
el = el.parentElement;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div id="columns"
|
||||
class="
|
||||
${this._config.rtl ? "rtl": " "}
|
||||
${this.isPanel() ? "panel": " "}
|
||||
"
|
||||
style="
|
||||
${this._config.justify_content ? `justify-content: ${this._config.justify_content};` : ''}
|
||||
">
|
||||
${this.columns.map((col) => html`
|
||||
${col}
|
||||
`)}
|
||||
</div>
|
||||
<div id="staging"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
:host {
|
||||
padding: 0 4px;
|
||||
display: block;
|
||||
margin-bottom: 0!important;
|
||||
}
|
||||
|
||||
#columns {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
margin-top: -8px;
|
||||
}
|
||||
#columns.rtl {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
#columns.panel {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.column {
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
card-maker>* {
|
||||
display: block;
|
||||
margin: 4px 4px 8px;
|
||||
}
|
||||
card-maker:first-child>* {
|
||||
margin-top: 8px;
|
||||
}
|
||||
card-maker:last-child>* {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
#staging {
|
||||
visibility: hidden;
|
||||
height: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
customElements.define("layout-card", LayoutCard);
|
10
webpack.config.js
Normal file
10
webpack.config.js
Normal file
@ -0,0 +1,10 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
entry: './src/main.js',
|
||||
mode: 'production',
|
||||
output: {
|
||||
filename: 'layout-card.js',
|
||||
path: path.resolve(__dirname)
|
||||
}
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user