diff --git a/README.md b/README.md index f4d01e9..f55cd80 100644 --- a/README.md +++ b/README.md @@ -115,14 +115,17 @@ The first three are column based and work similarly: All column based layouts accept the following `layout` options: -| Option | Values | Description | Default | -| ----------- | -------------- | ------------------------------------ | ------------------------------------------------- | -| `width` | number | Size in pixels of each column | 300 | -| `max_width` | number | Maximum width of a card | 1.5 \* `width` if specified
otherwise 500 | -| `max_cols` | number | Maximum number of columns to show | 4 if sidebar is hidden
3 if sidebar is shown | -| `rtl` | `true`/`false` | Place columns in right-to-left order | `false` | - -> NOTE: If you're migrating from layout-card "1.0" (v16 - sorry about the version number confusion), this is significantly fewer options than you are used to. \ +| Option | Values | Description | Default | +| --------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | +| `width` | number | Size in pixels of each column | 300 | +| `max_width` | number | Maximum width of a card | 1.5 \* `width` if specified
otherwise 500 | +| `max_cols` | number | Maximum number of columns to show | 4 if sidebar is hidden
3 if sidebar is shown | +| `rtl` | `true`/`false` | Place columns in right-to-left order | `false` | +| `column_widths` | special | A [`grid-template-columns`](https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns) specification of column widths | `none` | + +> NOTE: Even if `column_widths` is specified, the number of columns displayed will only depend on the available space, `width` and `max_cols`. If you get weird results, consider the Grid Layout. + +> NOTE 2: If you're migrating from layout-card "1.0" (v16 - sorry about the version number confusion), this is significantly fewer options than you are used to. \ > The reason for this is twofold. \ > First: Maintainability. As Home Assistant and Lovelace evolves, it grows increasingly more difficult to keep up the more options you want to keep alive. \ > Second: Usability. I want to focus on fewer options and doing the right thing out of the box instead. And the three remaining options actually have much more impact than you'd think. @@ -176,13 +179,13 @@ The vertical layout accepts the following **card** `view_layout` options: The grid layout will give you full controll of your cards by leveraging [CSS Grid](https://css-tricks.com/snippets/css/complete-guide-grid/). -The grid layout accepts any option starting with `grid-` that works for a Grid Container. +The grid layout accepts any option starting with `grid-` that works for a Grid Container as well as `grid` and `place-items`. Furthermore, the special option `mediaquery` can be used to set grid options depending on currently matched [@media rules](https://www.w3schools.com/cssref/css3_pr_mediaquery.asp). This helps immensely in creating fully responsive layouts. \ Please see the example code accompanying the screen recording below. \ Note that only the first matching rule will be applied. -For the card `view_layout` options. the grid layout accepts any css grid property starting with `grid-` that works for a Grid Item. +For the card `view_layout` options. the grid layout accepts any css grid property starting with `grid-` that works for a Grid Item as well as `place-self`. There's no point in me trying to list all `grid-` options here. Instead see the excellent guide linked above. diff --git a/layout-card.js b/layout-card.js index 5f0abb5..fbdc025 100644 --- a/layout-card.js +++ b/layout-card.js @@ -10,7 +10,7 @@ function t(t,e,i,s){var o,n=arguments.length,r=n<3?e:null===s?s=Object.getOwnPro bottom: calc(16px + env(safe-area-inset-bottom)); z-index: 1; } - `}}t([J()],Et.prototype,"cards",void 0),t([J()],Et.prototype,"index",void 0),t([J()],Et.prototype,"narrow",void 0),t([J()],Et.prototype,"hass",void 0),t([J()],Et.prototype,"lovelace",void 0),t([J()],Et.prototype,"_editMode",void 0),t([J()],Et.prototype,"_config",void 0);class $t extends Et{constructor(){super(...arguments),this._mediaQueries=[]}async setConfig(t){var e,i,s;await super.setConfig(t);for(const t of this._config.cards)if("string"!=typeof(null===(e=t.view_layout)||void 0===e?void 0:e.show)&&(null===(s=null===(i=t.view_layout)||void 0===i?void 0:i.show)||void 0===s?void 0:s.mediaquery)){const e=window.matchMedia(`${t.view_layout.show.mediaquery}`);this._mediaQueries.push(e),e.addEventListener("change",(()=>this._makeLayout()))}else this._mediaQueries.push(null);this._observer&&this._observer.disconnect(),this._observer=new ut((()=>{this._updateSize()}))}async updated(t){var e;await super.updated(t),(t.has("_columns")||t.has("cards"))&&this._makeLayout(),t.has("_editMode")&&this._makeLayout(),t.has("narrow")&&this._updateSize(),t.has("hass")&&(null===(e=t.get("hass"))||void 0===e?void 0:e.dockedSidebar)!=this.hass.dockedSidebar&&this._updateSize()}async firstUpdated(){var t,e,i,s;this._updateSize();const o=(null===(t=this._config.layout)||void 0===t?void 0:t.max_width)||((null===(e=this._config.layout)||void 0===e?void 0:e.width)?Math.ceil(1.5*(null===(i=this._config.layout)||void 0===i?void 0:i.width)):500),n=(null===(s=this._config.layout)||void 0===s?void 0:s.width)?2*this._config.layout.width:600,r=document.createElement("style");r.innerHTML=`\n :host {\n --column-max-width: ${o}px;\n }\n @media (max-width: ${o}px) {\n .column:first-child > * {\n margin-left: 0;\n }\n .column:last-child > * {\n margin-right: 0;\n }\n }\n @media (max-width: ${n-1}px) {\n .column {\n --column-max-width: ${n}px;\n }\n }\n `,this.shadowRoot.appendChild(r)}connectedCallback(){super.connectedCallback(),this._updateSize()}disconnectedCallback(){super.disconnectedCallback(),this._observer.disconnect()}async _updateSize(){var t,e,i;let s=this.getBoundingClientRect().width,o=0;o=Math.floor(s/((null===(t=this._config.layout)||void 0===t?void 0:t.width)||300)),o=Math.min(o,(null===(e=this._config.layout)||void 0===e?void 0:e.max_cols)||("docked"===(null===(i=this.hass)||void 0===i?void 0:i.dockedSidebar)?3:4)),o=Math.max(o,1),o!==this._columns&&(this._columns=o)}_shouldShow(t,e,i){if(!super._shouldShow(t,e,i))return!1;const s=this._mediaQueries[i];return!s||!!s.matches}isBreak(t){return"layout-break"===t.localName}async _makeLayout(){this._makeColumnLayout()}async _makeColumnLayout(){var t;if(this._observer.disconnect(),!this._columns)return;let e=[];for(let t=0;t{const i=this._config.cards[e];return{card:t,config:i,index:e,show:this._shouldShow(t,i,e)}}));await this._placeColumnCards(e,i.filter((t=>{var e;return(null===(e=this.lovelace)||void 0===e?void 0:e.editMode)||t.show}))),e=e.filter((t=>t.childElementCount>0)),(null===(t=this._config.layout)||void 0===t?void 0:t.rtl)&&e.reverse();const s=this.shadowRoot.querySelector("#columns");for(;s.firstChild;)s.removeChild(s.firstChild);for(const t of e)s.appendChild(t);this.requestUpdate().then((()=>this._observer.observe(this)))}async _placeColumnCards(t,e){}render(){return j` + `}}t([J()],Et.prototype,"cards",void 0),t([J()],Et.prototype,"index",void 0),t([J()],Et.prototype,"narrow",void 0),t([J()],Et.prototype,"hass",void 0),t([J()],Et.prototype,"lovelace",void 0),t([J()],Et.prototype,"_editMode",void 0),t([J()],Et.prototype,"_config",void 0);class $t extends Et{constructor(){super(...arguments),this._mediaQueries=[]}async setConfig(t){var e,i,s;await super.setConfig(t);for(const t of this._config.cards)if("string"!=typeof(null===(e=t.view_layout)||void 0===e?void 0:e.show)&&(null===(s=null===(i=t.view_layout)||void 0===i?void 0:i.show)||void 0===s?void 0:s.mediaquery)){const e=window.matchMedia(`${t.view_layout.show.mediaquery}`);this._mediaQueries.push(e),e.addEventListener("change",(()=>this._makeLayout()))}else this._mediaQueries.push(null);this._observer&&this._observer.disconnect(),this._observer=new ut((()=>{this._updateSize()}))}async updated(t){var e;await super.updated(t),(t.has("_columns")||t.has("cards"))&&this._makeLayout(),t.has("_editMode")&&this._makeLayout(),t.has("narrow")&&this._updateSize(),t.has("hass")&&(null===(e=t.get("hass"))||void 0===e?void 0:e.dockedSidebar)!=this.hass.dockedSidebar&&this._updateSize()}async firstUpdated(){var t,e,i,s,o,n,r;this._updateSize();const a=(null===(t=this._config.layout)||void 0===t?void 0:t.width)||300,d=(null===(e=this._config.layout)||void 0===e?void 0:e.max_width)||((null===(i=this._config.layout)||void 0===i?void 0:i.width)?Math.ceil(1.5*(null===(s=this._config.layout)||void 0===s?void 0:s.width)):500),l=(null===(o=this._config.layout)||void 0===o?void 0:o.width)?2*this._config.layout.width:600,c=document.createElement("style");c.innerHTML=`\n :host {\n --column-max-width: ${d}px;\n --column-width: ${a}px;\n --column-widths: ${null!==(r=null===(n=this._config.layout)||void 0===n?void 0:n.column_widths)&&void 0!==r?r:"none"};\n }\n @media (max-width: ${d}px) {\n .column:first-child > * {\n margin-left: 0;\n }\n .column:last-child > * {\n margin-right: 0;\n }\n }\n @media (max-width: ${l-1}px) {\n .column {\n --column-max-width: ${l}px;\n }\n }\n `,this.shadowRoot.appendChild(c)}connectedCallback(){super.connectedCallback(),this._updateSize()}disconnectedCallback(){super.disconnectedCallback(),this._observer.disconnect()}async _updateSize(){var t,e,i;let s=this.getBoundingClientRect().width,o=0;o=Math.floor(s/((null===(t=this._config.layout)||void 0===t?void 0:t.width)||300)),o=Math.min(o,(null===(e=this._config.layout)||void 0===e?void 0:e.max_cols)||("docked"===(null===(i=this.hass)||void 0===i?void 0:i.dockedSidebar)?3:4)),o=Math.max(o,1),o!==this._columns&&(this._columns=o)}_shouldShow(t,e,i){if(!super._shouldShow(t,e,i))return!1;const s=this._mediaQueries[i];return!s||!!s.matches}isBreak(t){return"layout-break"===t.localName}async _makeLayout(){this._makeColumnLayout()}async _makeColumnLayout(){var t;if(this._observer.disconnect(),!this._columns)return;let e=[];for(let t=0;t{const i=this._config.cards[e];return{card:t,config:i,index:e,show:this._shouldShow(t,i,e)}}));await this._placeColumnCards(e,i.filter((t=>{var e;return(null===(e=this.lovelace)||void 0===e?void 0:e.editMode)||t.show}))),e=e.filter((t=>t.childElementCount>0)),(null===(t=this._config.layout)||void 0===t?void 0:t.rtl)&&e.reverse();const s=this.shadowRoot.querySelector("#columns");for(;s.firstChild;)s.removeChild(s.firstChild);for(const t of e)s.appendChild(t);this.requestUpdate().then((()=>this._observer.observe(this)))}async _placeColumnCards(t,e){}render(){return j`
${this._render_fab()} `}static get styles(){return[this._fab_styles,it` @@ -22,22 +22,27 @@ function t(t,e,i,s){var o,n=arguments.length,r=n<3?e:null===s?s=Object.getOwnPro } #columns { - display: flex; - flex-direction: row; + display: grid; + grid-auto-columns: minmax( + var(--column-width), + var(--column-max-width) + ); + grid-template-columns: var(--column-widths); justify-content: center; + justify-items: center; margin-left: 4px; margin-right: 4px; } .column { - flex: 1 0 0; + grid-row: 1/2; max-width: var(--column-max-width); - min-width: 0; + width: 100%; } .column > * { display: block; margin: var(--masonry-view-card-margin, 4px 4px 8px); } - `]}}t([J()],$t.prototype,"_columns",void 0),t([J()],$t.prototype,"_config",void 0);customElements.define("masonry-layout",class extends $t{async _placeColumnCards(t,e){var i;const s=(null===(i=this._config.layout)||void 0===i?void 0:i.min_height)||5;function o(){let e=0;for(let i=0;ithis._placeCards()))}else this._mediaQueries.push(null);if(null===(o=this._config.layout)||void 0===o?void 0:o.mediaquery)for(const[t,e]of Object.entries(null===(n=this._config.layout)||void 0===n?void 0:n.mediaquery)){const e=window.matchMedia(t);this._layoutMQs.push(e),e.addEventListener("change",(()=>this._setGridStyles()))}this._setGridStyles()}async updated(t){await super.updated(t),(t.has("cards")||t.has("_editMode"))&&this._placeCards()}async firstUpdated(){this._setGridStyles()}_setGridStyles(){const t=this.shadowRoot.querySelector("#root");if(!t)return;const e=e=>{for(const[i,s]of Object.entries(e))i.startsWith("grid")&&t.style.setProperty(i,s)};t.style.cssText="",this._config.layout&&e(this._config.layout);for(const t of this._layoutMQs)if(t.matches){e(this._config.layout.mediaquery[t.media]);break}}_shouldShow(t,e,i){if(!super._shouldShow(t,e,i))return!1;const s=this._mediaQueries[i];return!s||!!s.matches}_placeCards(){var t,e;const i=this.shadowRoot.querySelector("#root");for(;i.firstChild;)i.removeChild(i.firstChild);let s=this.cards.map(((t,e)=>{const i=this._config.cards[e];return{card:t,config:i,index:e,show:this._shouldShow(t,i,e)}}));for(const o of s.filter((t=>{var e;return(null===(e=this.lovelace)||void 0===e?void 0:e.editMode)||t.show}))){const s=this.getCardElement(o);for(const[i,n]of Object.entries(null!==(e=null===(t=o.config)||void 0===t?void 0:t.view_layout)&&void 0!==e?e:{}))i.startsWith("grid")&&s.style.setProperty(i,n);i.appendChild(s)}}render(){return j`
+ `]}}t([J()],$t.prototype,"_columns",void 0),t([J()],$t.prototype,"_config",void 0);customElements.define("masonry-layout",class extends $t{async _placeColumnCards(t,e){var i;const s=(null===(i=this._config.layout)||void 0===i?void 0:i.min_height)||5;function o(){let e=0;for(let i=0;ithis._placeCards()))}else this._mediaQueries.push(null);if(null===(o=this._config.layout)||void 0===o?void 0:o.mediaquery)for(const[t,e]of Object.entries(null===(n=this._config.layout)||void 0===n?void 0:n.mediaquery)){const e=window.matchMedia(t);this._layoutMQs.push(e),e.addEventListener("change",(()=>this._setGridStyles()))}this._setGridStyles()}async updated(t){await super.updated(t),(t.has("cards")||t.has("_editMode"))&&this._placeCards()}async firstUpdated(){this._setGridStyles()}_setGridStyles(){const t=this.shadowRoot.querySelector("#root");if(!t)return;const e=e=>{for(const[i,s]of Object.entries(e))(i.startsWith("grid")||"grid"===i||"place-items"===i)&&t.style.setProperty(i,s)};t.style.cssText="",this._config.layout&&e(this._config.layout);for(const t of this._layoutMQs)if(t.matches){e(this._config.layout.mediaquery[t.media]);break}}_shouldShow(t,e,i){if(!super._shouldShow(t,e,i))return!1;const s=this._mediaQueries[i];return!s||!!s.matches}_placeCards(){var t,e;const i=this.shadowRoot.querySelector("#root");for(;i.firstChild;)i.removeChild(i.firstChild);let s=this.cards.map(((t,e)=>{const i=this._config.cards[e];return{card:t,config:i,index:e,show:this._shouldShow(t,i,e)}}));for(const o of s.filter((t=>{var e;return(null===(e=this.lovelace)||void 0===e?void 0:e.editMode)||t.show}))){const s=this.getCardElement(o);for(const[i,n]of Object.entries(null!==(e=null===(t=o.config)||void 0===t?void 0:t.view_layout)&&void 0!==e?e:{}))(i.startsWith("grid")||"place-self"===i)&&s.style.setProperty(i,n);i.appendChild(s)}}render(){return j`
${this._render_fab()}`}static get styles(){return[this._fab_styles,it` :host { padding-top: 4px; @@ -202,4 +207,4 @@ function t(t,e,i,s){var o,n=arguments.length,r=n<3?e:null===s?s=Object.getOwnPro @value-changed=${this._layoutChanged} > - `}}),customElements.whenDefined("hui-card-element-editor").then((()=>{const t=customElements.get("hui-card-element-editor"),e=t.prototype.getConfigElement;t.prototype.getConfigElement=async function(){const t=await e.bind(this)();if(t){const e=t.setConfig;t.setConfig=function(t){let i=JSON.parse(JSON.stringify(t));this._layoutData=i.layout,delete i.view_layout,e.bind(this)(i)}}return t};const i=t.prototype._handleUIConfigChanged;t.prototype._handleUIConfigChanged=function(t){this._configElement&&this._configElement._layoutData&&(t.detail.config.view_layout=this._configElement._layoutData),i.bind(this)(t)}}));var Ot="2.2.2";console.info(`%cLAYOUT-CARD ${Ot} IS INSTALLED`,"color: green; font-weight: bold",""); + `}}),customElements.whenDefined("hui-card-element-editor").then((()=>{const t=customElements.get("hui-card-element-editor"),e=t.prototype.getConfigElement;t.prototype.getConfigElement=async function(){const t=await e.bind(this)();if(t){const e=t.setConfig;t.setConfig=function(t){let i=JSON.parse(JSON.stringify(t));this._layoutData=i.layout,delete i.view_layout,e.bind(this)(i)}}return t};const i=t.prototype._handleUIConfigChanged;t.prototype._handleUIConfigChanged=function(t){this._configElement&&this._configElement._layoutData&&(t.detail.config.view_layout=this._configElement._layoutData),i.bind(this)(t)}}));var Ot="2.2.3";console.info(`%cLAYOUT-CARD ${Ot} IS INSTALLED`,"color: green; font-weight: bold",""); diff --git a/package.json b/package.json index 0ec0812..242926e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "layout-card", - "version": "2.2.2", + "version": "2.2.3", "description": "", "private": true, "scripts": { diff --git a/src/layouts/base-column-layout.ts b/src/layouts/base-column-layout.ts index 5296410..3316c26 100644 --- a/src/layouts/base-column-layout.ts +++ b/src/layouts/base-column-layout.ts @@ -56,6 +56,7 @@ export class BaseColumnLayout extends BaseLayout { async firstUpdated() { this._updateSize(); + const column_width = this._config.layout?.width || 300; const column_max_width = this._config.layout?.max_width || (this._config.layout?.width @@ -64,10 +65,13 @@ export class BaseColumnLayout extends BaseLayout { const column_two_width = this._config.layout?.width ? this._config.layout.width * 2 : 600; + const styleEl = document.createElement("style"); styleEl.innerHTML = ` :host { --column-max-width: ${column_max_width}px; + --column-width: ${column_width}px; + --column-widths: ${this._config.layout?.column_widths ?? "none"}; } @media (max-width: ${column_max_width}px) { .column:first-child > * { @@ -185,16 +189,21 @@ export class BaseColumnLayout extends BaseLayout { } #columns { - display: flex; - flex-direction: row; + display: grid; + grid-auto-columns: minmax( + var(--column-width), + var(--column-max-width) + ); + grid-template-columns: var(--column-widths); justify-content: center; + justify-items: center; margin-left: 4px; margin-right: 4px; } .column { - flex: 1 0 0; + grid-row: 1/2; max-width: var(--column-max-width); - min-width: 0; + width: 100%; } .column > * { display: block; diff --git a/src/layouts/grid.ts b/src/layouts/grid.ts index 8020166..09c0234 100644 --- a/src/layouts/grid.ts +++ b/src/layouts/grid.ts @@ -56,7 +56,7 @@ class GridLayout extends BaseLayout { if (!root) return; const addStyles = (layout) => { for (const [key, value] of Object.entries(layout)) { - if (key.startsWith("grid")) + if (key.startsWith("grid") || key === "grid" || key === "place-items") root.style.setProperty(key, (value as any) as string); } }; @@ -100,7 +100,8 @@ class GridLayout extends BaseLayout { for (const [key, value] of Object.entries( card.config?.view_layout ?? {} )) { - if (key.startsWith("grid")) el.style.setProperty(key, value as string); + if (key.startsWith("grid") || key === "place-self") + el.style.setProperty(key, value as string); } root.appendChild(el); } diff --git a/src/types.ts b/src/types.ts index d880e71..3ad3d4f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -36,6 +36,7 @@ export interface ViewConfig { export interface ColumnViewConfig extends ViewConfig { layout?: { width?: number; + column_widths: string; max_width?: number; max_cols?: number; min_height?: number; diff --git a/test/lovelace/grid-view.yaml b/test/lovelace/grid-view.yaml index 8369110..b1fa2b8 100644 --- a/test/lovelace/grid-view.yaml +++ b/test/lovelace/grid-view.yaml @@ -53,9 +53,3 @@ cards: show_header_toggle: false view_layout: grid-area: main - - type: entities - entities: - - light.bed_light - - type: entities - entities: - - light.bed_light