diff --git a/README.md b/README.md index 58df552..b226b6f 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ entities: ![smallcaps](https://user-images.githubusercontent.com/1299821/59151624-9a1d7680-8a36-11e9-9b2d-598c80ff2aa1.png) -You can also use [templates](https://github.com/thomasloven/hass-config/wiki/Mod-plugin-templates) to change the styles dynamically. +You can also use [templating](https://www.home-assistant.io/docs/configuration/templating/) to change the styles dynamically. **Example** \ Make an `entity-button` card green when the light is on @@ -69,7 +69,7 @@ type: entity-button entity: light.bed_light style: | ha-card { - background: [[ if(light.bed_light == "on", "green", "") ]]; + background: {% if is_state('light.bed_light', 'on') %} green {% endif %}; } ``` @@ -100,9 +100,43 @@ style: | ## More examples More examples are available [here](https://github.com/thomasloven/lovelace-card-mod/blob/master/src/example.yaml). -![more](https://user-images.githubusercontent.com/1299821/59151861-9f7cc000-8a3a-11e9-90d2-ff192c4c10a7.gif) +![more](https://user-images.githubusercontent.com/1299821/63809565-eb951d80-c922-11e9-8630-697befb3c95f.png) +## Styling entity and glance cards + +To make things easier, rows in `entities` cards and buttons in `glance` cards can be styled individually. +For those, the styles will be applied to the shadowRoot of the element, so a good starting point (rather than `ha-card`) would be `:host`: + +```yaml +type: entities +entities: + - light.bed_light + - entity: light.kitchen_lights + style: | + :host { + color: red; + } + - entity: input_number.value + style: | + :host { + --paper-item-icon-color: + {% if states(config.entity)|int < 50 %} + blue + {% else %} + red + {% endif %} + ; +``` + +## Templating +Jinja templates have access to a few special variables. Those are: + +- `config` - an object containing the card, entity row or glance button configuration +- `user` - the username of the currently logged in user +- `browser` - the deviceID of the current browser (see [browser_mod](https://github.com/thomasloven/hass-browser_mod). +- `hash` - the hash part of the current URL. + ## Advanced usage When exploring the cards using the element inspector, you might run into something called a `shadow-root` and notice that you can't apply styles to anything inside that. @@ -147,6 +181,29 @@ entity: alarm_control_panel.alarm # FAQ +### How do I convert my old card-modder configuration to card-mod? +For cards, you just have to wrap everything in `ha-card {}` and format it as CSS. + +So, you go from: +```yaml +style: + "--ha-card-background": rgba(200, 0, 0, 0.5) + color: white +``` + +to +```yaml +style: | + ha-card { + --ha-card-background: rgba(200, 0, 0, 0.5); + color: white + } +``` + +For rows in an entities card, you replace `ha-card` with `:host` as described above. + +Note that some cards can't be modded with card-mod. See below. + ### Why won't this work for some cards? The cards this doesn't work for often are not really *cards* at all, but change how other cards work. Examples include: `conditional`, `entity-filter`, `horizontal-stack` and `vertical-stack` as well as some custom cards, like `layout-card`, `auto-entities` and `state-switch` among others. diff --git a/card-mod.js b/card-mod.js index 76df872..0c8dd41 100644 --- a/card-mod.js +++ b/card-mod.js @@ -1 +1 @@ -!function(e){var t={};function r(s){if(t[s])return t[s].exports;var o=t[s]={i:s,l:!1,exports:{}};return e[s].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,s){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:s})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var s=Object.create(null);if(r.r(s),Object.defineProperty(s,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(s,o,function(t){return e[t]}.bind(null,o));return s},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=3)}([function(e,t,r){"use strict";function s(){return document.querySelector("home-assistant").hass}r.d(t,"a",function(){return s})},function(module,__webpack_exports__,__webpack_require__){"use strict";__webpack_require__.d(__webpack_exports__,"a",function(){return parseTemplate});var _hass_js__WEBPACK_IMPORTED_MODULE_0__=__webpack_require__(0),_deviceID_js__WEBPACK_IMPORTED_MODULE_1__=__webpack_require__(2);function hasTemplate(e){return/\[\[\s+.*\s+\]\]/.test(e)}function parseTemplateString(str,specialData={}){if("string"!=typeof str)return text;const FUNCTION=/^[a-zA-Z0-9_]+\(.*\)$/,EXPR=/([^=<>!]+)\s*(==|!=|<|>|<=|>=)\s*([^=<>!]+)/,SPECIAL=/^\{.+\}$/,STRING=/^"[^"]*"|'[^']*'$/;"string"==typeof specialData&&(specialData={}),specialData=Object.assign({user:Object(_hass_js__WEBPACK_IMPORTED_MODULE_0__.a)().user.name,browser:_deviceID_js__WEBPACK_IMPORTED_MODULE_1__.a,hash:location.hash.substr(1)||" "},specialData);const _parse_function=e=>{let t=[e.substr(0,e.indexOf("(")).trim()];for(e=e.substr(e.indexOf("(")+1);e;){let r=0,s=0,o=!1;for(;e[r];){let t=e[r++];if(t===o&&r>1&&"\\"!==e[r-2]?o=!1:"\"'".includes(t)&&(o=t),!o){if("("===t)s+=1;else if(")"===t){s-=1;continue}if(!(s>0)&&",)".includes(t))break}}t.push(e.substr(0,r-1).trim()),e=e.substr(r)}return t},_parse_special=e=>(e=e.substr(1,e.length-2),specialData[e]||`{${e}}`),_parse_entity=e=>{let t;if((e=e.split("."))[0].match(SPECIAL))t=_parse_special(e.shift()),t=Object(_hass_js__WEBPACK_IMPORTED_MODULE_0__.a)().states[t]||t;else if(t=Object(_hass_js__WEBPACK_IMPORTED_MODULE_0__.a)().states[`${e.shift()}.${e.shift()}`],!e.length)return t.state;return e.forEach(e=>t=t[e]),t},_eval_expr=str=>{if(str=EXPR.exec(str),null===str)return!1;const lhs=parseTemplateString(str[1]),rhs=parseTemplateString(str[3]);var expr="";return expr=parseFloat(lhs)!=lhs?`"${lhs}" ${str[2]} "${rhs}"`:`${parseFloat(lhs)} ${str[2]} ${parseFloat(rhs)}`,eval(expr)},_eval_function=e=>{if("if"===e[0])return _eval_expr(e[1])?parseTemplateString(e[2]):parseTemplateString(e[3])};try{return str=str.trim(),str.match(STRING)?str.substr(1,str.length-2):str.match(SPECIAL)?_parse_special(str):str.match(FUNCTION)?_eval_function(_parse_function(str)):str.includes(".")?_parse_entity(str):str}catch(e){return`[[ Template matching failed: ${str} ]]`}}function parseTemplate(e,t={}){if("string"!=typeof e)return e;return e=e.replace(/\[\[\s(.*?)\s\]\]/g,(e,r,s,o)=>parseTemplateString(r,t))}},function(e,t,r){"use strict";r.d(t,"a",function(){return s});let s=function(){if(window.fully&&"function"==typeof fully.getDeviceId)return fully.getDeviceId();if(!localStorage["lovelace-player-device-id"]){const e=()=>Math.floor(1e5*(1+Math.random())).toString(16).substring(1);localStorage["lovelace-player-device-id"]=`${e()}${e()}-${e()}${e()}`}return localStorage["lovelace-player-device-id"]}()},function(e,t,r){"use strict";r.r(t);const s=Object.getPrototypeOf(customElements.get("home-assistant-main"));s.prototype.html,s.prototype.css;var o=r(1);customElements.whenDefined("ha-card").then(()=>{const e=customElements.get("ha-card"),t=function(e){return e.config?e.config:e._config?e._config:e.host?t(e.host):e.parentElement?t(e.parentElement):e.parentNode?t(e.parentNode):null},r=function(e,t,s){const n=function(e){s&&("string"==typeof e?console.log(" ".repeat(2*(s-1))+e):console.log(e))};if(e&&t)if("string"==typeof t){e.querySelector(".card-mod-style")&&e.removeChild(e.querySelector(".card-mod-style"));const r=document.createElement("style");r.classList="card-mod-style",r.innerHTML=Object(o.a)(t),e.appendChild(r),s&&(n("Applied styles to:"),console.log(e))}else Object.keys(t).forEach(o=>"."===o?(n(`Stepping into root of ${e.tagName}`),r(e,t[o],s?s+1:0)):"$"===o?(n(`Stepping into ShadowRoot of ${e.tagName}`),r(e.shadowRoot,t[o],s?s+1:0)):(n(`Searching for: "${o}". ${e.querySelectorAll(o).length} matches.`),void e.querySelectorAll(`${o}`).forEach(e=>{n(`Stepping into ${e.tagName}`),r(e,t[o],s?s+1:0)})))};e.prototype.updated=function(e){const s=t(this);s&&s.style&&(s.debug_cardmod&&console.log("--- APPLYING STYLES START ---"),r(this,s.style,s.debug_cardmod?1:0),s.debug_cardmod&&console.log("--- APPLYING STYLES END ---"))},e.prototype.firstUpdated=function(){const e=this.shadowRoot.querySelector(".card-header");e&&this.insertBefore(e,this.children[0]),document.querySelector("home-assistant").provideHass(this),window.addEventListener("location-changed",()=>this._requestUpdate())},Object.defineProperty(e.prototype,"hass",{get(){return this._hass},set(e){if(e!==this._hass){const t=this._hass;this._hass=e,this._requestUpdate("hass",t)}}}),function(e,t,r=null){if((e=new Event(e,{bubbles:!0,cancelable:!1,composed:!0})).detail=t||{},r)r.dispatchEvent(e);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(e)}}("ll-rebuild",{})})}]); \ No newline at end of file +!function(e){var t={};function n(o){if(t[o])return t[o].exports;var i=t[o]={i:o,l:!1,exports:{}};return e[o].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(o,i,function(t){return e[t]}.bind(null,i));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){"use strict";n.r(t);const o=customElements.get("home-assistant-main")?Object.getPrototypeOf(customElements.get("home-assistant-main")):Object.getPrototypeOf(customElements.get("hui-view"));o.prototype.html,o.prototype.css;function i(e,t,n=null){if((e=new Event(e,{bubbles:!0,cancelable:!1,composed:!0})).detail=t||{},n)n.dispatchEvent(e);else{var o=document.querySelector("home-assistant");(o=(o=(o=(o=(o=(o=(o=(o=(o=(o=(o=o&&o.shadowRoot)&&o.querySelector("home-assistant-main"))&&o.shadowRoot)&&o.querySelector("app-drawer-layout partial-panel-resolver"))&&o.shadowRoot||o)&&o.querySelector("ha-panel-lovelace"))&&o.shadowRoot)&&o.querySelector("hui-root"))&&o.shadowRoot)&&o.querySelector("ha-app-layout #view"))&&o.firstElementChild)&&o.dispatchEvent(e)}}function r(){return document.querySelector("home-assistant").hass}let a=function(){if(window.fully&&"function"==typeof fully.getDeviceId)return fully.getDeviceId();if(!localStorage["lovelace-player-device-id"]){const e=()=>Math.floor(1e5*(1+Math.random())).toString(16).substring(1);localStorage["lovelace-player-device-id"]=`${e()}${e()}-${e()}${e()}`}return localStorage["lovelace-player-device-id"]}();const l=async function(e,t,n,o){const i=function(e){o&&("string"==typeof e?console.log(" ".repeat(2*(o-1))+e):console.log(e))};if(e&&t)if(e.updateComplete&&await e.updateComplete,"string"==typeof t){const o=e.querySelector(".card-mod-style");o&&(o.cancelSubscription&&await o.cancelSubscription,e.removeChild(o));const l=document.createElement("style");l.classList="card-mod-style",l.cancelSubscription=async function(e,t,n){e||(e=r().connection);let o={user:r().user.name,browser:a,hash:location.hash.substr(1)||" ",...n.variables},i=n.template,l=n.entity_ids;return e.subscribeMessage(e=>t(e.result),{type:"render_template",template:i,variables:o,entity_ids:l})}(null,e=>{l.innerHTML=e},{template:t,variables:n.variables,entity_ids:n.entity_ids}),e.appendChild(l),i("Applied styles to:"),i(e)}else Object.keys(t).forEach(r=>"."===r?(i(`Stepping into root of ${e.tagName}`),l(e,t[r],n,o?o+1:0)):"$"===r?(i(`Stepping into ShadowRoot of ${e.tagName}`),l(e.shadowRoot,t[r],n,o?o+1:0)):(i(`Searching for: "${r}". ${e.querySelectorAll(r).length} matches.`),void e.querySelectorAll(`${r}`).forEach(e=>{i(`Stepping into ${e.tagName}`),l(e,t[r],n,o?o+1:0)})))},s=function(e){return"string"==typeof e?e.includes("config.entity"):Object.values(e).some(s)};customElements.whenDefined("ha-card").then(()=>{const e=customElements.get("ha-card"),t=function(e){return e.config?e.config:e._config?e._config:e.host?t(e.host):e.parentElement?t(e.parentElement):e.parentNode?t(e.parentNode):null};e.prototype.firstUpdated=function(){const e=this.shadowRoot.querySelector(".card-header");e&&this.insertBefore(e,this.children[0]);const n=t(this);if(!n||!n.style)return;let o=n.entity_ids;!o&&s(n.style)&&(o=[n.entity]);const i=()=>{l(this,n.style,{variables:{config:n},entity_ids:o},!!n.debug_cardmod)};i(),window.addEventListener("location-changed",()=>i())},i("ll-rebuild",{})}),customElements.whenDefined("hui-entities-card").then(()=>{const e=customElements.get("hui-entities-card"),t=e.prototype.renderEntity;e.prototype.renderEntity=function(e){const n=t.bind(this)(e);if(!e.style)return n;if(!n.values)return n;const o=n.values[0];if(!o||!o.updateComplete)return n;let i=e.entity_ids;!i&&s(e.style)&&(i=[e.entity]);const r=()=>{l(o.shadowRoot,e.style,{variables:{config:e},entity_ids:i},!!e.debug_cardmod)};return o.updateComplete.then(r),window.addEventListener("location-changed",r),n},i("ll-rebuild",{})}),customElements.whenDefined("hui-glance-card").then(()=>{customElements.get("hui-glance-card").prototype.firstUpdated=function(){this.shadowRoot.querySelectorAll("ha-card div.entity").forEach(e=>{const t=e.attachShadow({mode:"open"});[...e.children].forEach(e=>t.appendChild(e));const n=document.createElement("style");t.appendChild(n),n.innerHTML="\n :host {\n box-sizing: border-box;\n padding: 0 4px;\n display: flex;\n flex-direction: column;\n align-items: center;\n cursor: pointer;\n margin-bottom: 12px;\n width: var(--glance-column-width, 20%);\n }\n div {\n width: 100%;\n text-align: center;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n .name {\n min-height: var(--paper-font-body1_-_line-height, 20px);\n }\n state-badge {\n margin: 8px 0;\n }\n ";const o=e.entityConf;if(!o.style)return;let i=o.entity_ids;!i&&s(o.style)&&(i=[o.entity]);const r=()=>{l(t,o.style,{variables:{config:o},entity_ids:i},!!o.debug_cardmod)};r(),window.addEventListener("location-changed",r)})},i("ll-rebuild",{})})}]); \ No newline at end of file diff --git a/src/example.yaml b/src/example.yaml index 24e8fd7..dc62380 100644 --- a/src/example.yaml +++ b/src/example.yaml @@ -1,15 +1,13 @@ title: card-mod cards: + # Style the card, but use special styles for the header - type: entities - title: Default - entities: - - light.bed_light - - light.ceiling_lights - - light.kitchen_lights - - - type: entities + title: Simple configuration style: | ha-card { + color: blue; + } + .card-header { color: red; } entities: @@ -17,41 +15,55 @@ cards: - light.ceiling_lights - light.kitchen_lights - - type: glance - title: Glance card + # Style an entities card row separately + - type: entities + title: Styled rows + entities: + - light.bed_light + - entity: light.ceiling_lights + style: | + :host { + color: green; + } + - type: section + label: A section label + style: | + :host { + --primary-color: red; + } + - entity: light.kitchen_lights + + + # Styles that update to state changes using jinja2 templating + - type: entities + title: Dynamic styles style: | ha-card { - font-variant: small-caps; - } - .card-header { - font-size: 16px; + background: url(http://place{{ states("input_select.background") }}.com/g/600/400); + --primary-text-color: rgb(250,250,250); + --paper-listbox-background-color: black; + text-shadow: 1px 1px 0 #000; } entities: + - input_select.background - light.bed_light - light.ceiling_lights - light.kitchen_lights + # Using the "config" special variable for templates - type: entity-button entity: light.bed_light style: | ha-card { - background: [[ if(light.bed_light == "on", "green", "") ]]; - } - - - type: entity-button - entity: light.bed_light - style: | - @keyframes blink { - 50% { - background: red; - } - } - ha-card { - animation: blink 2s linear infinite; - } + background: + {% if is_state(config.entity, 'on') %} + green + {% endif %} + ; + # Advanced css selectors can be used - type: entities - title: Styled + title: Advanced CSS rules style: | ha-card { background: rgba(0,50,0,0.5); @@ -66,29 +78,48 @@ cards: - light.kitchen_lights - light.bed_light + # Cards can be made transparent by removing background and box-shadow - type: entities - title: Dynamic styles + title: Transparent card + show_header_toggle: false style: | ha-card { - background: url(http://place[[ input_select.background.state ]].com/g/600/400); - --primary-text-color: rgb(250,250,250); - color: [[ if(light.bed_light == "on", "rgb(250,250,250)", "red") ]]; - --paper-listbox-background-color: black; - text-shadow: 1px 1px 0 #000; + background: none; + box-shadow: none; + } + entities: + - type: custom:text-divider-row + text: blah + + # Entities in glance cards can also be styled individually + - type: glance + title: Glance card + style: | + ha-card { + font-variant: small-caps; + } + .card-header { + font-size: 16px; } entities: - - input_select.background - light.bed_light - - light.ceiling_lights + - entity: light.ceiling_lights + style: | + :host { + font-variant: none; + } - light.kitchen_lights + # Adjustable picture elements position + # This could be much more useful with, say, an x and y value taken from sensor values + # that e.g. contain your robot vacuum cleaner location... - type: vertical-stack cards: - type: picture-elements style: | ha-card { - --top: [[ input_number.y_pos.state ]]%; - --left: [[ input_number.x_pos.state ]]%; + --top: {{ states("input_number.y_pos") }}%; + --left: {{ states("input_number.x_pos") }}%; } title: Dynamic styling of elements image: "http://placekitten.com/g/800/600" @@ -103,6 +134,7 @@ cards: - input_number.x_pos - input_number.y_pos + # Some animations - type: horizontal-stack cards: - type: entity-button @@ -114,7 +146,7 @@ cards: } @keyframes blink { 50% { - filter: invert(100%); + background: red; } } - type: entity-button @@ -160,100 +192,7 @@ cards: } } - - - - type: horizontal-stack - cards: - - type: entity-button - entity: light.bed_light - name: swing - style: | - ha-card { - animation: swing 1s linear infinite alternate; - } - @keyframes swing { - 0% { - -webkit-transform: rotate(5deg); - transform: rotate(5deg); - } - 100% { - -webkit-transform: rotate(-5deg); - transform: rotate(-5deg); - } - } - - - type: entity-button - entity: light.bed_light - name: swell - style: | - ha-card { - animation: pulse 3s linear infinite alternate; - } - @keyframes blinka { - 50% { background: white; } - 100% { background: blue; } - } - @keyframes pulse { - 0% { - -webkit-transform: scaleX(1); - transform: scaleX(1); - } - 50% { - -webkit-transform: scale3d(1.15,1.15,1.15); - transform: scale3d(1.15,1.15,1.15); - } - 100% { - -webkit-transform: scaleX(1); - transform: scaleX(1); - } - - type: entity-button - entity: light.bed_light - name: flip - style: | - ha-card { - animation: flip 5s linear infinite - } - @keyframes blinka { - 50% { background: white; } - 100% { background: red; } - } - @keyframes flip { - 0% { - -webkit-animation-timing-function: ease-out; - -webkit-transform: perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn); - animation-timing-function: ease-out; - transform: perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn) - } - - 40% { - -webkit-animation-timing-function: ease-out; - -webkit-transform: perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg); - animation-timing-function: ease-out; - transform: perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg) - } - - 50% { - -webkit-animation-timing-function: ease-in; - -webkit-transform: perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg); - animation-timing-function: ease-in; - transform: perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg) - } - - 80% { - -webkit-animation-timing-function: ease-in; - -webkit-transform: perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg); - animation-timing-function: ease-in; - transform: perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg) - } - - to { - -webkit-animation-timing-function: ease-in; - -webkit-transform: perspective(400px) scaleX(1) translateZ(0) rotateY(0deg); - animation-timing-function: ease-in; - transform: perspective(400px) scaleX(1) translateZ(0) rotateY(0deg) - } - } - + # Keyframes are inherited into picture-elements elements as well... - type: picture-elements style: | @keyframes dvd { @@ -288,6 +227,9 @@ cards: animation: dvd 10s linear infinite left: 0% top: 50% + + # An example of going into shadowroots of stuff + # Not for the faint of heart - type: alarm-panel card_icon: mdi:bell name: Alarm Panel @@ -311,3 +253,44 @@ cards: --mdc-theme-primary: green; } entity: alarm_control_panel.alarm + + # By placing a stack in an entities card as a row, it can be modded as well. + - type: entities + style: | + ha-card { + --ha-card-background: red; + box-shadow: none; + background: none; + } + .card-content { + padding: 0 + } + entities: + - type: custom:hui-horizontal-stack-card + cards: + - type: entities + entities: + - light.bed_light + - light.kitchen_lights + - light.ceiling_lights + - type: glance + entities: + - light.bed_light + - light.kitchen_lights + - light.ceiling_lights + + # Automatically styled entity rows + # Makes use of config variable + - type: custom:auto-entities + filter: + include: + - domain: light + options: + style: | + :host { + color: {% if is_state(config.entity, 'on') %} green {% endif %}; + } + card: + type: entities + title: Auto-entities + diff --git a/src/main.js b/src/main.js index 8f48c4c..8fc58c1 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,78 @@ import {html, css} from "/card-tools/lit-element.js"; import {fireEvent} from "/card-tools/event.js"; -import {parseTemplate} from "/card-tools/templates.js"; +import {subscribeRenderTemplate} from "/card-tools/templates.js"; + +const applyStyle = async function(root, style, params, debug) { + + const debugPrint = function(str){ + if(!debug) return; + if(typeof str === "string") + console.log(' '.repeat(2*(debug-1)) + str); + else + console.log(str); + } + + if(!root || !style) return; + + if(root.updateComplete) + await root.updateComplete; + + if(typeof style === "string") { + const oldStyleEl = root.querySelector(".card-mod-style"); + if(oldStyleEl) + { + // Remove old styles and cancel template subscriptions + if(oldStyleEl.cancelSubscription) + { + await oldStyleEl.cancelSubscription; + } + root.removeChild(oldStyleEl) + } + + // Add new style tag to the root element + const styleEl = document.createElement('style'); + styleEl.classList = "card-mod-style"; + styleEl.cancelSubscription = subscribeRenderTemplate( + null, + (result) => { + styleEl.innerHTML = result; + }, + { + template: style, + variables: params.variables, + entity_ids: params.entity_ids, + } + ); + root.appendChild(styleEl); + debugPrint("Applied styles to:"); + debugPrint(root); + } else { + Object.keys(style).forEach((k) => { + if(k === ".") { + debugPrint(`Stepping into root of ${root.tagName}`) + return applyStyle(root, style[k], params, debug?debug+1:0); + } else if(k === "$") { + debugPrint(`Stepping into ShadowRoot of ${root.tagName}`) + return applyStyle(root.shadowRoot, style[k], params, debug?debug+1:0); + } else { + debugPrint(`Searching for: "${k}". ${root.querySelectorAll(k).length} matches.`); + root.querySelectorAll(`${k}`).forEach((el) => { + debugPrint(`Stepping into ${el.tagName}`) + applyStyle(el, style[k], params, debug?debug+1:0); + }); + return; + } + }); + } +} + +const find_entity_ids = function(data) { + if(typeof(data) === "string") { + return data.includes("config.entity"); + } + return Object.values(data).some(find_entity_ids); +} + customElements.whenDefined('ha-card').then(() => { const HaCard = customElements.get('ha-card'); @@ -19,63 +91,6 @@ customElements.whenDefined('ha-card').then(() => { return null; }; - const applyStyle = function(root, style, debug) { - - const debugPrint = function(str){ - if(!debug) return; - if(typeof str === "string") - console.log(' '.repeat(2*(debug-1)) + str); - else - console.log(str); - } - - if(!root || !style) return; - - if(typeof style === "string") { - // Remove old styles if we're updating - if(root.querySelector(".card-mod-style")) - root.removeChild(root.querySelector(".card-mod-style")); - - // Add new style tag to the root element - const styleEl = document.createElement('style'); - styleEl.classList = "card-mod-style"; - styleEl.innerHTML = parseTemplate(style); - root.appendChild(styleEl); - if(debug) { - debugPrint("Applied styles to:") - console.log(root); - } - } else { - Object.keys(style).forEach((k) => { - if(k === ".") { - debugPrint(`Stepping into root of ${root.tagName}`) - return applyStyle(root, style[k], debug?debug+1:0); - } else if(k === "$") { - debugPrint(`Stepping into ShadowRoot of ${root.tagName}`) - return applyStyle(root.shadowRoot, style[k], debug?debug+1:0); - } else { - debugPrint(`Searching for: "${k}". ${root.querySelectorAll(k).length} matches.`); - root.querySelectorAll(`${k}`).forEach((el) => { - debugPrint(`Stepping into ${el.tagName}`) - applyStyle(el, style[k], debug?debug+1:0); - }); - return; - } - }); - } - } - - HaCard.prototype.updated = function(_) { - // Apply styles after updates, if specified - const config = findConfig(this); - if(config && config.style) { - if(config.debug_cardmod) - console.log("--- APPLYING STYLES START ---"); - applyStyle(this, config.style, config.debug_cardmod ? 1 : 0); - if(config.debug_cardmod) - console.log("--- APPLYING STYLES END ---"); - } - } HaCard.prototype.firstUpdated = function() { // Move the header inside the slot instead of in the shadowDOM @@ -85,22 +100,114 @@ customElements.whenDefined('ha-card').then(() => { { this.insertBefore(header, this.children[0]); } - // Listen for changes to hass or the location and update - document.querySelector("home-assistant").provideHass(this); - window.addEventListener("location-changed", () => this._requestUpdate()); + + const config = findConfig(this); + if(!config || !config.style) return; + + let entity_ids = config.entity_ids; + if(!entity_ids && find_entity_ids(config.style)) + entity_ids = [config.entity]; + + const apply = () => { + applyStyle(this, config.style, { + variables: {config}, + entity_ids + }, !!config.debug_cardmod); + } + + apply(); + window.addEventListener("location-changed", () => apply()); + } + + fireEvent('ll-rebuild', {}); +}); + + +customElements.whenDefined('hui-entities-card').then(() => { + const EntitiesCard = customElements.get('hui-entities-card'); + + const oldRenderEntity = EntitiesCard.prototype.renderEntity; + EntitiesCard.prototype.renderEntity = function(config) { + + const retval = oldRenderEntity.bind(this)(config); + + if(!config.style) return retval; + if(!retval.values) return retval; + const row = retval.values[0]; + if(!row || !row.updateComplete) return retval; + + let entity_ids = config.entity_ids; + if (!entity_ids && find_entity_ids(config.style)) + entity_ids = [config.entity]; + + const apply = () => { + applyStyle(row.shadowRoot, config.style, { + variables: {config}, + entity_ids + }, !!config.debug_cardmod); + } + + row.updateComplete.then(apply); + window.addEventListener("location-changed", apply); + return retval; + } + fireEvent('ll-rebuild', {}); +}); + + +customElements.whenDefined('hui-glance-card').then(() => { + const GlanceCard = customElements.get('hui-glance-card'); + + GlanceCard.prototype.firstUpdated = function () { + const entities = this.shadowRoot.querySelectorAll("ha-card div.entity"); + entities.forEach((e) => { + const root = e.attachShadow({mode: "open"}); + [...e.children].forEach((el) => root.appendChild(el)); + const styletag = document.createElement("style"); + root.appendChild(styletag); + styletag.innerHTML = ` + :host { + box-sizing: border-box; + padding: 0 4px; + display: flex; + flex-direction: column; + align-items: center; + cursor: pointer; + margin-bottom: 12px; + width: var(--glance-column-width, 20%); + } + div { + width: 100%; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .name { + min-height: var(--paper-font-body1_-_line-height, 20px); + } + state-badge { + margin: 8px 0; + } + `; + + const config = e.entityConf; + if(!config.style) return; + let entity_ids = config.entity_ids; + if (!entity_ids && find_entity_ids(config.style)) + entity_ids = [config.entity]; + + const apply = () => { + applyStyle(root, config.style, { + variables: {config}, + entity_ids + }, !!config.debug_cardmod); + } + + apply(); + window.addEventListener("location-changed", apply); + }); } - Object.defineProperty(HaCard.prototype, 'hass', { - get() { - return this._hass; - }, - set(value) { - if(value !== this._hass) { - const oldval = this._hass; - this._hass = value; - this._requestUpdate('hass', oldval); - } - }, - }); fireEvent('ll-rebuild', {}); });