diff --git a/README.md b/README.md index ffa818a..b837d8d 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ column_width: max_width: min_width: flex_grow: +gridcols: +gridrows: justify_content: rtl: cards: @@ -38,12 +40,13 @@ card_options: ## Options - `` **Required** A list of lovelace cards to display. - `` are options that are applied to all cards. -- `` The layout method to use. `auto`, `vertical` or `horizontal`. See below. Default: `auto`. +- `` The layout method to use. `auto`, `vertical`, `horizontal` or `grid`. See below. Default: `auto`. - `` The minimum length of a column in `auto` layout. Default: `5`. - `` The minimum number of columns to use. Default: `1`. - `` The maximum number of columns to use. Default: `100`. - `` Width of columns. Default: `300px`. - ``, ``, `` Set the `max-width`, `min-width` and `flex-grow` CSS properties of the columns manually. Default: `column_width or 500px`, `undefined`, `undefined`. +- ``, `` Set the `grid-template-rows` and `grid-template-columns` CSS properties when using `layout: grid`. - `` Set the `justify-content` CSS property of the column container. Default: `center`. ## Layouts @@ -180,6 +183,52 @@ cards: ``` ![layout-card 4 - manual breaks](https://user-images.githubusercontent.com/1299821/48088461-62312500-e202-11e8-96ab-e4f560f8d4fc.png) +### `grid` layout (experimental) + +For maximum controll, you can place every card manually in a [CSS grid](https://css-tricks.com/snippets/css/complete-guide-grid/) by using the `grid` layout. + +To do this, you need to specify `gridrows` and `gridcols` with the settings for `grid-template-rows` and `grid-template-columns` repectively **and** also add `gridcol:` and `gridrow:` for *each card* with the settings for `grid-column` and `grid-row` respectively. + +> Hint: This may look better if you also have [card-mod](https://github.com/thomasloven/lovelace-card-mod) and set the card heights to `100 %`. + +```yaml +type: custom:layout-card +layout: vertical +column_width: 100% +cards: + - type: markdown + content: "# Grid" + - type: custom:layout-card + layout: grid + gridrows: 180px 200px auto + gridcols: 180px auto 180px + cards: + - type: glance + entities: + - sun.sun + gridrow: 1 / 2 + gridcol: 1 / 2 + style: "ha-card { height: 100%; }" + - type: entities + entities: &ents + - light.bed_light + - light.kitchen_lights + - light.ceiling_lights + gridrow: 1 / 3 + gridcol: 2 / 4 + style: "ha-card { height: 100%; }" + - type: markdown + content: test + gridrow: 2 / 4 + gridcol: 1 / 2 + style: "ha-card { height: 100%; }" + - type: entities + entities: *ents + gridrow: 3 / 4 + gridcol: 2 / 3 +``` +![layout-card - Grid](https://user-images.githubusercontent.com/1299821/71694902-e3f1f380-2db0-11ea-82f1-8f880a2fbb24.png) + ## Tweaking layouts - First of all `` and `` options, which can be used to force the number of columns displayed: diff --git a/layout-card.js b/layout-card.js index ea64eb0..b2f178a 100644 --- a/layout-card.js +++ b/layout-card.js @@ -1,4 +1,11 @@ -!function(t){var e={};function n(s){if(e[s])return e[s].exports;var o=e[s]={i:s,l:!1,exports:{}};return t[s].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=t,n.c=e,n.d=function(t,e,s){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:s})},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 s=Object.create(null);if(n.r(s),Object.defineProperty(s,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(s,o,function(e){return t[e]}.bind(null,o));return s},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 s=customElements.get("home-assistant-main")?Object.getPrototypeOf(customElements.get("home-assistant-main")):Object.getPrototypeOf(customElements.get("hui-view")),o=s.prototype.html,i=s.prototype.css;const r="custom:";function c(t,e){const n=document.createElement("hui-error-card");return n.setConfig({type:"error",error:t,origConfig:e}),n}function a(t,e){if(!e||"object"!=typeof e||!e.type)return c(`No ${t} type configured`,e);let n=e.type;if(n=n.startsWith(r)?n.substr(r.length):`hui-${n}-${t}`,customElements.get(n))return function(t,e){const n=document.createElement(t);try{n.setConfig(e)}catch(t){return c(t,e)}return n}(n,e);const s=c(`Custom element doesn't exist: ${n}.`,e);s.style.display="None";const o=setTimeout(()=>{s.style.display=""},2e3);return customElements.whenDefined(n).then(()=>{clearTimeout(o),function(t,e,n=null){if((t=new Event(t,{bubbles:!0,cancelable:!1,composed:!0})).detail=e||{},n)n.dispatchEvent(t);else{var s=document.querySelector("home-assistant");(s=(s=(s=(s=(s=(s=(s=(s=(s=(s=(s=s&&s.shadowRoot)&&s.querySelector("home-assistant-main"))&&s.shadowRoot)&&s.querySelector("app-drawer-layout partial-panel-resolver"))&&s.shadowRoot||s)&&s.querySelector("ha-panel-lovelace"))&&s.shadowRoot)&&s.querySelector("hui-root"))&&s.shadowRoot)&&s.querySelector("ha-app-layout #view"))&&s.firstElementChild)&&s.dispatchEvent(t)}}("ll-rebuild",{},s)}),s}function l(){return document.querySelector("home-assistant").hass}const u=2;class d extends s{static get version(){return u}static get properties(){return{noHass:{type:Boolean}}}setConfig(t){var e;this._config=t,this.el?this.el.setConfig(t):(this.el=this.create(t),this._hass&&(this.el.hass=this._hass),this.noHass&&(e=this,document.querySelector("home-assistant").provideHass(e)))}set config(t){this.setConfig(t)}set hass(t){this._hass=t,this.el&&(this.el.hass=t)}createRenderRoot(){return this}render(){return o`${this.el}`}}const h=function(t,e){const n=Object.getOwnPropertyDescriptors(e.prototype);for(const[e,s]of Object.entries(n))"constructor"!==e&&Object.defineProperty(t.prototype,e,s);const s=Object.getOwnPropertyDescriptors(e);for(const[e,n]of Object.entries(s))"prototype"!==e&&Object.defineProperty(t,e,n);const o=Object.getPrototypeOf(e),i=Object.getOwnPropertyDescriptors(o.prototype);for(const[e,n]of Object.entries(i))"constructor"!==e&&Object.defineProperty(Object.getPrototypeOf(t).prototype,e,n);const r=Object.getOwnPropertyDescriptors(o);for(const[e,n]of Object.entries(r))"prototype"!==e&&Object.defineProperty(Object.getPrototypeOf(t),e,n)},m=customElements.get("card-maker");if(!m||!m.version||m.version{if(!t)return;const s=e[function(){let t=0;for(let s=0;sthis.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._config.entities&&this._config.entities.length||this._config.cards&&this._config.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,...this._config.card_options},e.hass=l(),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 s=t=>"string"==typeof t&&t.endsWith("%")?Math.floor(e*parseInt(t)/100):parseInt(t);let o=0;if("object"==typeof n.column_width){let t=e;for(;t>0;){let e=n.column_width[o];void 0===e&&(e=n.column_width.slice(-1)[0]),t-=s(e),o+=1}o=Math.max(o-1,1)}else o=Math.floor(e/s(n.column_width));o=Math.max(o,n.min_columns),o=Math.min(o,n.max_columns);let i=[];for(let t=0;t{if(s+=1,!t)return;const n=e[(s-1)%e.length];n.appendChild(t),n.length+=t.getCardSize?t.getCardSize():1})}(t,i);break;case"vertical":!function(t,e,n){let s=0;t.forEach(t=>{if(!t)return void(s+=1);const n=e[s%e.length];n.appendChild(t),n.length+=t.getCardSize?t.getCardSize():1})}(t,i);break;case"auto":default:g(t,i,n)}return i=i.filter(t=>t.childElementCount>0)}(this.cards,t,this._config),this._config.rtl&&this.columns.reverse(),this.format_columns(),this.requestUpdate()}format_columns(){const t=(t,e,n,s="px")=>{if(void 0===this._config[e])return"";let o=`${t}: `;const i=this._config[e];return"object"==typeof i?i.length>n?o+=`${i[n]}`:o+=`${i.slice(-1)}`:o+=`${i}`,o.endsWith("px")||o.endsWith("%")||(o+=s),o+";"};for(const[e,n]of this.columns.entries()){const s=[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(...s)}}getCardSize(){if(this.columns)return Math.max.apply(Math,this.columns.map(t=>t.length))}_isPanel(){if(this.isPanel)return!0;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 o` +!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;const r="custom:";function c(t,e){const n=document.createElement("hui-error-card");return n.setConfig({type:"error",error:t,origConfig:e}),n}function a(t,e){if(!e||"object"!=typeof e||!e.type)return c(`No ${t} type configured`,e);let n=e.type;if(n=n.startsWith(r)?n.substr(r.length):`hui-${n}-${t}`,customElements.get(n))return function(t,e){const n=document.createElement(t);try{n.setConfig(e)}catch(t){return c(t,e)}return n}(n,e);const i=c(`Custom element doesn't exist: ${n}.`,e);i.style.display="None";const s=setTimeout(()=>{i.style.display=""},2e3);return customElements.whenDefined(n).then(()=>{clearTimeout(s),function(t,e,n=null){if((t=new Event(t,{bubbles:!0,cancelable:!1,composed:!0})).detail=e||{},n)n.dispatchEvent(t);else{var i=document.querySelector("home-assistant");(i=(i=(i=(i=(i=(i=(i=(i=(i=(i=(i=i&&i.shadowRoot)&&i.querySelector("home-assistant-main"))&&i.shadowRoot)&&i.querySelector("app-drawer-layout partial-panel-resolver"))&&i.shadowRoot||i)&&i.querySelector("ha-panel-lovelace"))&&i.shadowRoot)&&i.querySelector("hui-root"))&&i.shadowRoot)&&i.querySelector("ha-app-layout #view"))&&i.firstElementChild)&&i.dispatchEvent(t)}}("ll-rebuild",{},i)}),i}function l(t){return a("card",t)}function u(){return document.querySelector("home-assistant").hass}const d=2;class h extends i{static get version(){return d}static get properties(){return{noHass:{type:Boolean}}}setConfig(t){var e;this._config=t,this.el?this.el.setConfig(t):(this.el=this.create(t),this._hass&&(this.el.hass=this._hass),this.noHass&&(e=this,document.querySelector("home-assistant").provideHass(e)))}set config(t){this.setConfig(t)}set hass(t){this._hass=t,this.el&&(this.el.hass=t)}createRenderRoot(){return this}render(){return s`${this.el}`}}const m=function(t,e){const n=Object.getOwnPropertyDescriptors(e.prototype);for(const[e,i]of Object.entries(n))"constructor"!==e&&Object.defineProperty(t.prototype,e,i);const i=Object.getOwnPropertyDescriptors(e);for(const[e,n]of Object.entries(i))"prototype"!==e&&Object.defineProperty(t,e,n);const s=Object.getPrototypeOf(e),o=Object.getOwnPropertyDescriptors(s.prototype);for(const[e,n]of Object.entries(o))"constructor"!==e&&Object.defineProperty(Object.getPrototypeOf(t).prototype,e,n);const r=Object.getOwnPropertyDescriptors(s);for(const[e,n]of Object.entries(r))"prototype"!==e&&Object.defineProperty(Object.getPrototypeOf(t),e,n)},f=customElements.get("card-maker");if(!f||!f.version||f.version{if(!t)return;const i=e[function(){let t=0;for(let i=0;ithis.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._config.entities&&this._config.entities.length||this._config.cards&&this._config.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={...t,...this._config.card_options},n=l(e);return n.hass=u(),n.style.gridColumn=e.gridcol,n.style.gridRow=e.gridrow,this.shadowRoot.querySelector("#staging").appendChild(n),new Promise((t,e)=>n.updateComplete?n.updateComplete.then(()=>t(n)):t(n))}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(){if("grid"===this._config.layout)return;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{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:y(t,o,n)}return o=o.filter(t=>t.childElementCount>0)}(this.cards,t,this._config),this._config.rtl&&this.columns.reverse(),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(){if(this.isPanel)return!0;let t=this.parentElement,e=10;for(;e--&&t;){if("hui-panel-view"===t.localName)return!0;if("div"===t.localName)return!1;t=t.parentElement}return!1}render(){return"grid"===this._config.layout?s` +
+ `:s`
- ${this.columns.map(t=>o` + ${this.columns.map(t=>s` ${t} `)}
- `}static get styles(){return i` + `}static get styles(){return o` :host { padding: 0 4px; display: block; @@ -34,18 +41,19 @@ overflow-x: hidden; } - card-maker>* { + + .cards>* { display: block; margin: 4px 4px 8px; } - card-maker:first-child>* { + .cards>*:first-child { margin-top: 8px; } - card-maker:last-child>* { + .cards>*:last-child { margin-bottom: 4px; } - #staging { + #staging:not(.grid) { visibility: hidden; height: 0; } diff --git a/src/layout.js b/src/layout.js index fc40475..0e4f7b3 100644 --- a/src/layout.js +++ b/src/layout.js @@ -79,6 +79,7 @@ export function buildLayout(cards, width, config) { for (let i = 0; i < colnum; i++) { const newCol = document.createElement("div"); newCol.classList.add("column"); + newCol.classList.add("cards"); newCol.length = 0; cols.push(newCol); } diff --git a/src/main.js b/src/main.js index 4b57585..c98193b 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,6 @@ import { LitElement, html, css } from "card-tools/src/lit-element"; import "card-tools/src/card-maker"; +import { createCard } from "card-tools/src/lovelace-element"; import { hass } from "card-tools/src/hass"; import {buildLayout} from "./layout"; @@ -62,13 +63,20 @@ class LayoutCard extends LitElement { async build_card(c) { if(c === "break") return null; - const el = document.createElement("card-maker"); - el.config = {...c, ...this._config.card_options}; + const config = {...c, ...this._config.card_options}; + const el = createCard(config); el.hass = hass(); + + el.style.gridColumn = config.gridcol; + el.style.gridRow = config.gridrow; // 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))); + return new Promise((resolve, reject) => + el.updateComplete + ? el.updateComplete.then(() => resolve(el)) + : resolve(el) + ); } async build_cards() { @@ -83,6 +91,8 @@ class LayoutCard extends LitElement { } place_cards() { + if(this._config.layout === "grid") + return; const width = this.shadowRoot.querySelector("#columns").clientWidth; this.columns = buildLayout( @@ -141,7 +151,7 @@ class LayoutCard extends LitElement { if(this.isPanel) return true; let el = this.parentElement; let steps = 10; - while(steps--) { + while(steps-- && el) { if(el.localName === "hui-panel-view") return true; if(el.localName === "div") return false; el = el.parentElement; @@ -150,6 +160,15 @@ class LayoutCard extends LitElement { } render() { + if(this._config.layout === "grid") + return html` +
+ `; return html`