├── .gitignore ├── README.md ├── cover-stripes.png ├── dist └── cover-popup-card.js ├── hacs.json ├── package.json ├── rollup.config.js ├── screenshot.png ├── src └── cover-popup-card.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge)](https://github.com/custom-components/hacs) 2 | 3 | # Cover popup card (homekit style) 4 | Popup lovelace card with slider and optional actions to add more control for your cover. 5 | Can be used in combination with thomas loven browser_mod or in combination with my homekit style card: https://github.com/DBuit/Homekit-panel-card 6 | 7 | 8 | Buy Me A Coffee 9 | 10 | ## Configuration 11 | 12 | ### Installation instructions 13 | 14 | **HACS installation:** 15 | Go to the hacs store and use the repo url `https://github.com/DBuit/cover-popup-card` and add this as a custom repository under settings. 16 | 17 | Add the following to your ui-lovelace.yaml: 18 | ```yaml 19 | resources: 20 | url: /community_plugin/cover-popup-card/cover-popup-card.js 21 | type: module 22 | ``` 23 | 24 | **Manual installation:** 25 | Copy the .js file from the dist directory to your www directory and add the following to your ui-lovelace.yaml file: 26 | 27 | ```yaml 28 | resources: 29 | url: /local/cover-popup-card.js 30 | type: module 31 | ``` 32 | 33 | ### Main Options 34 | 35 | | Name | Type | Default | Supported options | Description | 36 | | -------------- | ----------- | ------------ | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 37 | | `entity` | string | **Required** | `cover.kitchen` | Entity of the light | 38 | | `icon` | string | optional | `mdi:lightbulb` | It will use customize entity icon or from the config as a fallback it used lightbulb icon | 39 | | `fullscreen` | boolean | optional | true | If false it will remove the pop-up wrapper which makes it fullscreen | 40 | | `actions` | object | optional | `actions:` | define actions that you can activate from the pop-up. | 41 | | `actionSize` | string | optional | `50px` | Set the size of the action buttons default `50px` | 42 | | `actionsInARow` | number | optional | 3 | number of action that will be placed in a row under the slider | 43 | | `sliderWidth` | string | optional | 150px | The width of the slider | 44 | | `sliderHeight` | string | optional | 400px | The height of the slider | 45 | | `borderRadius` | string | optional | 12px | The border radius of the slider and switch | 46 | | `sliderService` | string | **required** | `cover.set_cover_position` or `cover.set_cover_tilt_position` | Set if you want to set the position or the tilt by using the slider | 47 | | `sliderColor` | string | optional | "#FFF" | The color of the slider | 48 | | `sliderThumbColor` | string | optional | "#ddd" | The color of the line that you use to slide the slider | 49 | | `sliderTrackColor` | string | optional | "#ddd" | The track that if not filled by the slider | 50 | | `sliderTrackStripeColor` | string | optional | "#ddd" | The track can be striped by making this color different than the sliderTrackColor | 51 | | `sliderThumbBorderColor` | string | optional | "#ddd" | The color of the space around the thumb | 52 | | `settings` | boolean | optional | false | When it will add an settings button that displays the more-info content see settings example for my light popup for more options/information [here]: https://github.com/DBuit/light-popup-card#settings | 53 | | `settingsPosition` | string | optional | `bottom` | set position of the settings button options: `top` or `bottom`. | 54 | 55 | To show actions in the pop-up you add `actions:` in the config of the card follow bij multiple actions. 56 | These actions are calling a service with specific service data. 57 | ``` 58 | actions: 59 | - service: scene.turn_on 60 | service_data: 61 | entity_id: scene.energie 62 | color: "#8BCBDD" 63 | name: energie 64 | - service: homeassistant.toggle 65 | service_data: 66 | entity_id: light.voordeurlicht 67 | name: voordeur 68 | icon: mdi:lightbulb 69 | ``` 70 | The name option within a scene is **optional** 71 | You can also set the `entity_id` with value **this** if you use **this** it will be replaced with the entity the pop-up is opened for. 72 | 73 | 74 | Example configuration with actions 75 | ``` 76 | type: custom:cover-popup-card 77 | sliderService: cover.set_cover_position 78 | actionsInARow: 2 79 | actions: 80 | - service: cover.open_cover 81 | service_data: 82 | entity_id: this 83 | name: open 84 | icon: mdi:window-shutter-open 85 | - service: cover.close_cover 86 | service_data: 87 | entity_id: this 88 | name: close 89 | icon: mdi:window-shutter 90 | ``` 91 | 92 | Example configuration with stripes (see second screenshot) 93 | ``` 94 | type: custom:cover-popup-card 95 | sliderService: cover.set_cover_position 96 | sliderColor: "#ddd" 97 | sliderTrackColor: "#FFF" 98 | sliderThumbColor: "#ddd" 99 | sliderThumbBorderColor: "#FFF" 100 | sliderTrackStripeColor: "#ddd" 101 | ``` 102 | 103 | ### Screenshot 104 | 105 | ![Screenshot](screenshot.png) 106 | 107 | ![Screenshot](cover-stripes.png) 108 | -------------------------------------------------------------------------------- /cover-stripes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DBuit/cover-popup-card/650d0d786155ca49e0916a2b183c18bb187fb004/cover-stripes.png -------------------------------------------------------------------------------- /dist/cover-popup-card.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | const directives = new WeakMap(); 15 | const isDirective = (o) => { 16 | return typeof o === 'function' && directives.has(o); 17 | }; 18 | 19 | /** 20 | * @license 21 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 22 | * This code may only be used under the BSD style license found at 23 | * http://polymer.github.io/LICENSE.txt 24 | * The complete set of authors may be found at 25 | * http://polymer.github.io/AUTHORS.txt 26 | * The complete set of contributors may be found at 27 | * http://polymer.github.io/CONTRIBUTORS.txt 28 | * Code distributed by Google as part of the polymer project is also 29 | * subject to an additional IP rights grant found at 30 | * http://polymer.github.io/PATENTS.txt 31 | */ 32 | /** 33 | * True if the custom elements polyfill is in use. 34 | */ 35 | const isCEPolyfill = window.customElements !== undefined && 36 | window.customElements.polyfillWrapFlushCallback !== 37 | undefined; 38 | /** 39 | * Removes nodes, starting from `start` (inclusive) to `end` (exclusive), from 40 | * `container`. 41 | */ 42 | const removeNodes = (container, start, end = null) => { 43 | while (start !== end) { 44 | const n = start.nextSibling; 45 | container.removeChild(start); 46 | start = n; 47 | } 48 | }; 49 | 50 | /** 51 | * @license 52 | * Copyright (c) 2018 The Polymer Project Authors. All rights reserved. 53 | * This code may only be used under the BSD style license found at 54 | * http://polymer.github.io/LICENSE.txt 55 | * The complete set of authors may be found at 56 | * http://polymer.github.io/AUTHORS.txt 57 | * The complete set of contributors may be found at 58 | * http://polymer.github.io/CONTRIBUTORS.txt 59 | * Code distributed by Google as part of the polymer project is also 60 | * subject to an additional IP rights grant found at 61 | * http://polymer.github.io/PATENTS.txt 62 | */ 63 | /** 64 | * A sentinel value that signals that a value was handled by a directive and 65 | * should not be written to the DOM. 66 | */ 67 | const noChange = {}; 68 | /** 69 | * A sentinel value that signals a NodePart to fully clear its content. 70 | */ 71 | const nothing = {}; 72 | 73 | /** 74 | * @license 75 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 76 | * This code may only be used under the BSD style license found at 77 | * http://polymer.github.io/LICENSE.txt 78 | * The complete set of authors may be found at 79 | * http://polymer.github.io/AUTHORS.txt 80 | * The complete set of contributors may be found at 81 | * http://polymer.github.io/CONTRIBUTORS.txt 82 | * Code distributed by Google as part of the polymer project is also 83 | * subject to an additional IP rights grant found at 84 | * http://polymer.github.io/PATENTS.txt 85 | */ 86 | /** 87 | * An expression marker with embedded unique key to avoid collision with 88 | * possible text in templates. 89 | */ 90 | const marker = `{{lit-${String(Math.random()).slice(2)}}}`; 91 | /** 92 | * An expression marker used text-positions, multi-binding attributes, and 93 | * attributes with markup-like text values. 94 | */ 95 | const nodeMarker = ``; 96 | const markerRegex = new RegExp(`${marker}|${nodeMarker}`); 97 | /** 98 | * Suffix appended to all bound attribute names. 99 | */ 100 | const boundAttributeSuffix = '$lit$'; 101 | /** 102 | * An updateable Template that tracks the location of dynamic parts. 103 | */ 104 | class Template { 105 | constructor(result, element) { 106 | this.parts = []; 107 | this.element = element; 108 | const nodesToRemove = []; 109 | const stack = []; 110 | // Edge needs all 4 parameters present; IE11 needs 3rd parameter to be null 111 | const walker = document.createTreeWalker(element.content, 133 /* NodeFilter.SHOW_{ELEMENT|COMMENT|TEXT} */, null, false); 112 | // Keeps track of the last index associated with a part. We try to delete 113 | // unnecessary nodes, but we never want to associate two different parts 114 | // to the same index. They must have a constant node between. 115 | let lastPartIndex = 0; 116 | let index = -1; 117 | let partIndex = 0; 118 | const { strings, values: { length } } = result; 119 | while (partIndex < length) { 120 | const node = walker.nextNode(); 121 | if (node === null) { 122 | // We've exhausted the content inside a nested template element. 123 | // Because we still have parts (the outer for-loop), we know: 124 | // - There is a template in the stack 125 | // - The walker will find a nextNode outside the template 126 | walker.currentNode = stack.pop(); 127 | continue; 128 | } 129 | index++; 130 | if (node.nodeType === 1 /* Node.ELEMENT_NODE */) { 131 | if (node.hasAttributes()) { 132 | const attributes = node.attributes; 133 | const { length } = attributes; 134 | // Per 135 | // https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap, 136 | // attributes are not guaranteed to be returned in document order. 137 | // In particular, Edge/IE can return them out of order, so we cannot 138 | // assume a correspondence between part index and attribute index. 139 | let count = 0; 140 | for (let i = 0; i < length; i++) { 141 | if (endsWith(attributes[i].name, boundAttributeSuffix)) { 142 | count++; 143 | } 144 | } 145 | while (count-- > 0) { 146 | // Get the template literal section leading up to the first 147 | // expression in this attribute 148 | const stringForPart = strings[partIndex]; 149 | // Find the attribute name 150 | const name = lastAttributeNameRegex.exec(stringForPart)[2]; 151 | // Find the corresponding attribute 152 | // All bound attributes have had a suffix added in 153 | // TemplateResult#getHTML to opt out of special attribute 154 | // handling. To look up the attribute value we also need to add 155 | // the suffix. 156 | const attributeLookupName = name.toLowerCase() + boundAttributeSuffix; 157 | const attributeValue = node.getAttribute(attributeLookupName); 158 | node.removeAttribute(attributeLookupName); 159 | const statics = attributeValue.split(markerRegex); 160 | this.parts.push({ type: 'attribute', index, name, strings: statics }); 161 | partIndex += statics.length - 1; 162 | } 163 | } 164 | if (node.tagName === 'TEMPLATE') { 165 | stack.push(node); 166 | walker.currentNode = node.content; 167 | } 168 | } 169 | else if (node.nodeType === 3 /* Node.TEXT_NODE */) { 170 | const data = node.data; 171 | if (data.indexOf(marker) >= 0) { 172 | const parent = node.parentNode; 173 | const strings = data.split(markerRegex); 174 | const lastIndex = strings.length - 1; 175 | // Generate a new text node for each literal section 176 | // These nodes are also used as the markers for node parts 177 | for (let i = 0; i < lastIndex; i++) { 178 | let insert; 179 | let s = strings[i]; 180 | if (s === '') { 181 | insert = createMarker(); 182 | } 183 | else { 184 | const match = lastAttributeNameRegex.exec(s); 185 | if (match !== null && endsWith(match[2], boundAttributeSuffix)) { 186 | s = s.slice(0, match.index) + match[1] + 187 | match[2].slice(0, -boundAttributeSuffix.length) + match[3]; 188 | } 189 | insert = document.createTextNode(s); 190 | } 191 | parent.insertBefore(insert, node); 192 | this.parts.push({ type: 'node', index: ++index }); 193 | } 194 | // If there's no text, we must insert a comment to mark our place. 195 | // Else, we can trust it will stick around after cloning. 196 | if (strings[lastIndex] === '') { 197 | parent.insertBefore(createMarker(), node); 198 | nodesToRemove.push(node); 199 | } 200 | else { 201 | node.data = strings[lastIndex]; 202 | } 203 | // We have a part for each match found 204 | partIndex += lastIndex; 205 | } 206 | } 207 | else if (node.nodeType === 8 /* Node.COMMENT_NODE */) { 208 | if (node.data === marker) { 209 | const parent = node.parentNode; 210 | // Add a new marker node to be the startNode of the Part if any of 211 | // the following are true: 212 | // * We don't have a previousSibling 213 | // * The previousSibling is already the start of a previous part 214 | if (node.previousSibling === null || index === lastPartIndex) { 215 | index++; 216 | parent.insertBefore(createMarker(), node); 217 | } 218 | lastPartIndex = index; 219 | this.parts.push({ type: 'node', index }); 220 | // If we don't have a nextSibling, keep this node so we have an end. 221 | // Else, we can remove it to save future costs. 222 | if (node.nextSibling === null) { 223 | node.data = ''; 224 | } 225 | else { 226 | nodesToRemove.push(node); 227 | index--; 228 | } 229 | partIndex++; 230 | } 231 | else { 232 | let i = -1; 233 | while ((i = node.data.indexOf(marker, i + 1)) !== -1) { 234 | // Comment node has a binding marker inside, make an inactive part 235 | // The binding won't work, but subsequent bindings will 236 | // TODO (justinfagnani): consider whether it's even worth it to 237 | // make bindings in comments work 238 | this.parts.push({ type: 'node', index: -1 }); 239 | partIndex++; 240 | } 241 | } 242 | } 243 | } 244 | // Remove text binding nodes after the walk to not disturb the TreeWalker 245 | for (const n of nodesToRemove) { 246 | n.parentNode.removeChild(n); 247 | } 248 | } 249 | } 250 | const endsWith = (str, suffix) => { 251 | const index = str.length - suffix.length; 252 | return index >= 0 && str.slice(index) === suffix; 253 | }; 254 | const isTemplatePartActive = (part) => part.index !== -1; 255 | // Allows `document.createComment('')` to be renamed for a 256 | // small manual size-savings. 257 | const createMarker = () => document.createComment(''); 258 | /** 259 | * This regex extracts the attribute name preceding an attribute-position 260 | * expression. It does this by matching the syntax allowed for attributes 261 | * against the string literal directly preceding the expression, assuming that 262 | * the expression is in an attribute-value position. 263 | * 264 | * See attributes in the HTML spec: 265 | * https://www.w3.org/TR/html5/syntax.html#elements-attributes 266 | * 267 | * " \x09\x0a\x0c\x0d" are HTML space characters: 268 | * https://www.w3.org/TR/html5/infrastructure.html#space-characters 269 | * 270 | * "\0-\x1F\x7F-\x9F" are Unicode control characters, which includes every 271 | * space character except " ". 272 | * 273 | * So an attribute is: 274 | * * The name: any character except a control character, space character, ('), 275 | * ("), ">", "=", or "/" 276 | * * Followed by zero or more space characters 277 | * * Followed by "=" 278 | * * Followed by zero or more space characters 279 | * * Followed by: 280 | * * Any character except space, ('), ("), "<", ">", "=", (`), or 281 | * * (") then any non-("), or 282 | * * (') then any non-(') 283 | */ 284 | const lastAttributeNameRegex = /([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/; 285 | 286 | /** 287 | * @license 288 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 289 | * This code may only be used under the BSD style license found at 290 | * http://polymer.github.io/LICENSE.txt 291 | * The complete set of authors may be found at 292 | * http://polymer.github.io/AUTHORS.txt 293 | * The complete set of contributors may be found at 294 | * http://polymer.github.io/CONTRIBUTORS.txt 295 | * Code distributed by Google as part of the polymer project is also 296 | * subject to an additional IP rights grant found at 297 | * http://polymer.github.io/PATENTS.txt 298 | */ 299 | /** 300 | * An instance of a `Template` that can be attached to the DOM and updated 301 | * with new values. 302 | */ 303 | class TemplateInstance { 304 | constructor(template, processor, options) { 305 | this.__parts = []; 306 | this.template = template; 307 | this.processor = processor; 308 | this.options = options; 309 | } 310 | update(values) { 311 | let i = 0; 312 | for (const part of this.__parts) { 313 | if (part !== undefined) { 314 | part.setValue(values[i]); 315 | } 316 | i++; 317 | } 318 | for (const part of this.__parts) { 319 | if (part !== undefined) { 320 | part.commit(); 321 | } 322 | } 323 | } 324 | _clone() { 325 | // There are a number of steps in the lifecycle of a template instance's 326 | // DOM fragment: 327 | // 1. Clone - create the instance fragment 328 | // 2. Adopt - adopt into the main document 329 | // 3. Process - find part markers and create parts 330 | // 4. Upgrade - upgrade custom elements 331 | // 5. Update - set node, attribute, property, etc., values 332 | // 6. Connect - connect to the document. Optional and outside of this 333 | // method. 334 | // 335 | // We have a few constraints on the ordering of these steps: 336 | // * We need to upgrade before updating, so that property values will pass 337 | // through any property setters. 338 | // * We would like to process before upgrading so that we're sure that the 339 | // cloned fragment is inert and not disturbed by self-modifying DOM. 340 | // * We want custom elements to upgrade even in disconnected fragments. 341 | // 342 | // Given these constraints, with full custom elements support we would 343 | // prefer the order: Clone, Process, Adopt, Upgrade, Update, Connect 344 | // 345 | // But Safari dooes not implement CustomElementRegistry#upgrade, so we 346 | // can not implement that order and still have upgrade-before-update and 347 | // upgrade disconnected fragments. So we instead sacrifice the 348 | // process-before-upgrade constraint, since in Custom Elements v1 elements 349 | // must not modify their light DOM in the constructor. We still have issues 350 | // when co-existing with CEv0 elements like Polymer 1, and with polyfills 351 | // that don't strictly adhere to the no-modification rule because shadow 352 | // DOM, which may be created in the constructor, is emulated by being placed 353 | // in the light DOM. 354 | // 355 | // The resulting order is on native is: Clone, Adopt, Upgrade, Process, 356 | // Update, Connect. document.importNode() performs Clone, Adopt, and Upgrade 357 | // in one step. 358 | // 359 | // The Custom Elements v1 polyfill supports upgrade(), so the order when 360 | // polyfilled is the more ideal: Clone, Process, Adopt, Upgrade, Update, 361 | // Connect. 362 | const fragment = isCEPolyfill ? 363 | this.template.element.content.cloneNode(true) : 364 | document.importNode(this.template.element.content, true); 365 | const stack = []; 366 | const parts = this.template.parts; 367 | // Edge needs all 4 parameters present; IE11 needs 3rd parameter to be null 368 | const walker = document.createTreeWalker(fragment, 133 /* NodeFilter.SHOW_{ELEMENT|COMMENT|TEXT} */, null, false); 369 | let partIndex = 0; 370 | let nodeIndex = 0; 371 | let part; 372 | let node = walker.nextNode(); 373 | // Loop through all the nodes and parts of a template 374 | while (partIndex < parts.length) { 375 | part = parts[partIndex]; 376 | if (!isTemplatePartActive(part)) { 377 | this.__parts.push(undefined); 378 | partIndex++; 379 | continue; 380 | } 381 | // Progress the tree walker until we find our next part's node. 382 | // Note that multiple parts may share the same node (attribute parts 383 | // on a single element), so this loop may not run at all. 384 | while (nodeIndex < part.index) { 385 | nodeIndex++; 386 | if (node.nodeName === 'TEMPLATE') { 387 | stack.push(node); 388 | walker.currentNode = node.content; 389 | } 390 | if ((node = walker.nextNode()) === null) { 391 | // We've exhausted the content inside a nested template element. 392 | // Because we still have parts (the outer for-loop), we know: 393 | // - There is a template in the stack 394 | // - The walker will find a nextNode outside the template 395 | walker.currentNode = stack.pop(); 396 | node = walker.nextNode(); 397 | } 398 | } 399 | // We've arrived at our part's node. 400 | if (part.type === 'node') { 401 | const part = this.processor.handleTextExpression(this.options); 402 | part.insertAfterNode(node.previousSibling); 403 | this.__parts.push(part); 404 | } 405 | else { 406 | this.__parts.push(...this.processor.handleAttributeExpressions(node, part.name, part.strings, this.options)); 407 | } 408 | partIndex++; 409 | } 410 | if (isCEPolyfill) { 411 | document.adoptNode(fragment); 412 | customElements.upgrade(fragment); 413 | } 414 | return fragment; 415 | } 416 | } 417 | 418 | /** 419 | * @license 420 | * Copyright (c) 2017 The Polymer Project Authors. All rights reserved. 421 | * This code may only be used under the BSD style license found at 422 | * http://polymer.github.io/LICENSE.txt 423 | * The complete set of authors may be found at 424 | * http://polymer.github.io/AUTHORS.txt 425 | * The complete set of contributors may be found at 426 | * http://polymer.github.io/CONTRIBUTORS.txt 427 | * Code distributed by Google as part of the polymer project is also 428 | * subject to an additional IP rights grant found at 429 | * http://polymer.github.io/PATENTS.txt 430 | */ 431 | const commentMarker = ` ${marker} `; 432 | /** 433 | * The return type of `html`, which holds a Template and the values from 434 | * interpolated expressions. 435 | */ 436 | class TemplateResult { 437 | constructor(strings, values, type, processor) { 438 | this.strings = strings; 439 | this.values = values; 440 | this.type = type; 441 | this.processor = processor; 442 | } 443 | /** 444 | * Returns a string of HTML used to create a `