4338 lines
161 KiB
JavaScript
4338 lines
161 KiB
JavaScript
/*! *****************************************************************************
|
|
Copyright (c) Microsoft Corporation. All rights reserved.
|
|
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
this file except in compliance with the License. You may obtain a copy of the
|
|
License at http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
|
|
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
|
|
MERCHANTABLITY OR NON-INFRINGEMENT.
|
|
|
|
See the Apache Version 2.0 License for specific language governing permissions
|
|
and limitations under the License.
|
|
***************************************************************************** */
|
|
|
|
function __decorate(decorators, target, key, desc) {
|
|
var c = arguments.length,
|
|
r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc,
|
|
d;
|
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
}
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
const directives = new WeakMap();
|
|
/**
|
|
* Brands a function as a directive so that lit-html will call the function
|
|
* during template rendering, rather than passing as a value.
|
|
*
|
|
* @param f The directive factory function. Must be a function that returns a
|
|
* function of the signature `(part: Part) => void`. The returned function will
|
|
* be called with the part object
|
|
*
|
|
* @example
|
|
*
|
|
* ```
|
|
* import {directive, html} from 'lit-html';
|
|
*
|
|
* const immutable = directive((v) => (part) => {
|
|
* if (part.value !== v) {
|
|
* part.setValue(v)
|
|
* }
|
|
* });
|
|
* ```
|
|
*/
|
|
// tslint:disable-next-line:no-any
|
|
const directive = f => (...args) => {
|
|
const d = f(...args);
|
|
directives.set(d, true);
|
|
return d;
|
|
};
|
|
const isDirective = o => {
|
|
return typeof o === 'function' && directives.has(o);
|
|
};
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
/**
|
|
* True if the custom elements polyfill is in use.
|
|
*/
|
|
const isCEPolyfill = window.customElements !== undefined && window.customElements.polyfillWrapFlushCallback !== undefined;
|
|
/**
|
|
* Removes nodes, starting from `startNode` (inclusive) to `endNode`
|
|
* (exclusive), from `container`.
|
|
*/
|
|
const removeNodes = (container, startNode, endNode = null) => {
|
|
let node = startNode;
|
|
while (node !== endNode) {
|
|
const n = node.nextSibling;
|
|
container.removeChild(node);
|
|
node = n;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2018 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
/**
|
|
* A sentinel value that signals that a value was handled by a directive and
|
|
* should not be written to the DOM.
|
|
*/
|
|
const noChange = {};
|
|
/**
|
|
* A sentinel value that signals a NodePart to fully clear its content.
|
|
*/
|
|
const nothing = {};
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
/**
|
|
* An expression marker with embedded unique key to avoid collision with
|
|
* possible text in templates.
|
|
*/
|
|
const marker = `{{lit-${String(Math.random()).slice(2)}}}`;
|
|
/**
|
|
* An expression marker used text-positions, multi-binding attributes, and
|
|
* attributes with markup-like text values.
|
|
*/
|
|
const nodeMarker = `<!--${marker}-->`;
|
|
const markerRegex = new RegExp(`${marker}|${nodeMarker}`);
|
|
/**
|
|
* Suffix appended to all bound attribute names.
|
|
*/
|
|
const boundAttributeSuffix = '$lit$';
|
|
/**
|
|
* An updateable Template that tracks the location of dynamic parts.
|
|
*/
|
|
class Template {
|
|
constructor(result, element) {
|
|
this.parts = [];
|
|
this.element = element;
|
|
let index = -1;
|
|
let partIndex = 0;
|
|
const nodesToRemove = [];
|
|
const _prepareTemplate = template => {
|
|
const content = template.content;
|
|
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be
|
|
// null
|
|
const walker = document.createTreeWalker(content, 133 /* NodeFilter.SHOW_{ELEMENT|COMMENT|TEXT} */, null, false);
|
|
// Keeps track of the last index associated with a part. We try to delete
|
|
// unnecessary nodes, but we never want to associate two different parts
|
|
// to the same index. They must have a constant node between.
|
|
let lastPartIndex = 0;
|
|
while (walker.nextNode()) {
|
|
index++;
|
|
const node = walker.currentNode;
|
|
if (node.nodeType === 1 /* Node.ELEMENT_NODE */) {
|
|
if (node.hasAttributes()) {
|
|
const attributes = node.attributes;
|
|
// Per
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap,
|
|
// attributes are not guaranteed to be returned in document order.
|
|
// In particular, Edge/IE can return them out of order, so we cannot
|
|
// assume a correspondance between part index and attribute index.
|
|
let count = 0;
|
|
for (let i = 0; i < attributes.length; i++) {
|
|
if (attributes[i].value.indexOf(marker) >= 0) {
|
|
count++;
|
|
}
|
|
}
|
|
while (count-- > 0) {
|
|
// Get the template literal section leading up to the first
|
|
// expression in this attribute
|
|
const stringForPart = result.strings[partIndex];
|
|
// Find the attribute name
|
|
const name = lastAttributeNameRegex.exec(stringForPart)[2];
|
|
// Find the corresponding attribute
|
|
// All bound attributes have had a suffix added in
|
|
// TemplateResult#getHTML to opt out of special attribute
|
|
// handling. To look up the attribute value we also need to add
|
|
// the suffix.
|
|
const attributeLookupName = name.toLowerCase() + boundAttributeSuffix;
|
|
const attributeValue = node.getAttribute(attributeLookupName);
|
|
const strings = attributeValue.split(markerRegex);
|
|
this.parts.push({ type: 'attribute', index, name, strings });
|
|
node.removeAttribute(attributeLookupName);
|
|
partIndex += strings.length - 1;
|
|
}
|
|
}
|
|
if (node.tagName === 'TEMPLATE') {
|
|
_prepareTemplate(node);
|
|
}
|
|
} else if (node.nodeType === 3 /* Node.TEXT_NODE */) {
|
|
const data = node.data;
|
|
if (data.indexOf(marker) >= 0) {
|
|
const parent = node.parentNode;
|
|
const strings = data.split(markerRegex);
|
|
const lastIndex = strings.length - 1;
|
|
// Generate a new text node for each literal section
|
|
// These nodes are also used as the markers for node parts
|
|
for (let i = 0; i < lastIndex; i++) {
|
|
parent.insertBefore(strings[i] === '' ? createMarker() : document.createTextNode(strings[i]), node);
|
|
this.parts.push({ type: 'node', index: ++index });
|
|
}
|
|
// If there's no text, we must insert a comment to mark our place.
|
|
// Else, we can trust it will stick around after cloning.
|
|
if (strings[lastIndex] === '') {
|
|
parent.insertBefore(createMarker(), node);
|
|
nodesToRemove.push(node);
|
|
} else {
|
|
node.data = strings[lastIndex];
|
|
}
|
|
// We have a part for each match found
|
|
partIndex += lastIndex;
|
|
}
|
|
} else if (node.nodeType === 8 /* Node.COMMENT_NODE */) {
|
|
if (node.data === marker) {
|
|
const parent = node.parentNode;
|
|
// Add a new marker node to be the startNode of the Part if any of
|
|
// the following are true:
|
|
// * We don't have a previousSibling
|
|
// * The previousSibling is already the start of a previous part
|
|
if (node.previousSibling === null || index === lastPartIndex) {
|
|
index++;
|
|
parent.insertBefore(createMarker(), node);
|
|
}
|
|
lastPartIndex = index;
|
|
this.parts.push({ type: 'node', index });
|
|
// If we don't have a nextSibling, keep this node so we have an end.
|
|
// Else, we can remove it to save future costs.
|
|
if (node.nextSibling === null) {
|
|
node.data = '';
|
|
} else {
|
|
nodesToRemove.push(node);
|
|
index--;
|
|
}
|
|
partIndex++;
|
|
} else {
|
|
let i = -1;
|
|
while ((i = node.data.indexOf(marker, i + 1)) !== -1) {
|
|
// Comment node has a binding marker inside, make an inactive part
|
|
// The binding won't work, but subsequent bindings will
|
|
// TODO (justinfagnani): consider whether it's even worth it to
|
|
// make bindings in comments work
|
|
this.parts.push({ type: 'node', index: -1 });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
_prepareTemplate(element);
|
|
// Remove text binding nodes after the walk to not disturb the TreeWalker
|
|
for (const n of nodesToRemove) {
|
|
n.parentNode.removeChild(n);
|
|
}
|
|
}
|
|
}
|
|
const isTemplatePartActive = part => part.index !== -1;
|
|
// Allows `document.createComment('')` to be renamed for a
|
|
// small manual size-savings.
|
|
const createMarker = () => document.createComment('');
|
|
/**
|
|
* This regex extracts the attribute name preceding an attribute-position
|
|
* expression. It does this by matching the syntax allowed for attributes
|
|
* against the string literal directly preceding the expression, assuming that
|
|
* the expression is in an attribute-value position.
|
|
*
|
|
* See attributes in the HTML spec:
|
|
* https://www.w3.org/TR/html5/syntax.html#attributes-0
|
|
*
|
|
* "\0-\x1F\x7F-\x9F" are Unicode control characters
|
|
*
|
|
* " \x09\x0a\x0c\x0d" are HTML space characters:
|
|
* https://www.w3.org/TR/html5/infrastructure.html#space-character
|
|
*
|
|
* So an attribute is:
|
|
* * The name: any character except a control character, space character, ('),
|
|
* ("), ">", "=", or "/"
|
|
* * Followed by zero or more space characters
|
|
* * Followed by "="
|
|
* * Followed by zero or more space characters
|
|
* * Followed by:
|
|
* * Any character except space, ('), ("), "<", ">", "=", (`), or
|
|
* * (") then any non-("), or
|
|
* * (') then any non-(')
|
|
*/
|
|
const lastAttributeNameRegex = /([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F \x09\x0a\x0c\x0d"'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/;
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
/**
|
|
* An instance of a `Template` that can be attached to the DOM and updated
|
|
* with new values.
|
|
*/
|
|
class TemplateInstance {
|
|
constructor(template, processor, options) {
|
|
this._parts = [];
|
|
this.template = template;
|
|
this.processor = processor;
|
|
this.options = options;
|
|
}
|
|
update(values) {
|
|
let i = 0;
|
|
for (const part of this._parts) {
|
|
if (part !== undefined) {
|
|
part.setValue(values[i]);
|
|
}
|
|
i++;
|
|
}
|
|
for (const part of this._parts) {
|
|
if (part !== undefined) {
|
|
part.commit();
|
|
}
|
|
}
|
|
}
|
|
_clone() {
|
|
// When using the Custom Elements polyfill, clone the node, rather than
|
|
// importing it, to keep the fragment in the template's document. This
|
|
// leaves the fragment inert so custom elements won't upgrade and
|
|
// potentially modify their contents by creating a polyfilled ShadowRoot
|
|
// while we traverse the tree.
|
|
const fragment = isCEPolyfill ? this.template.element.content.cloneNode(true) : document.importNode(this.template.element.content, true);
|
|
const parts = this.template.parts;
|
|
let partIndex = 0;
|
|
let nodeIndex = 0;
|
|
const _prepareInstance = fragment => {
|
|
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be
|
|
// null
|
|
const walker = document.createTreeWalker(fragment, 133 /* NodeFilter.SHOW_{ELEMENT|COMMENT|TEXT} */, null, false);
|
|
let node = walker.nextNode();
|
|
// Loop through all the nodes and parts of a template
|
|
while (partIndex < parts.length && node !== null) {
|
|
const part = parts[partIndex];
|
|
// Consecutive Parts may have the same node index, in the case of
|
|
// multiple bound attributes on an element. So each iteration we either
|
|
// increment the nodeIndex, if we aren't on a node with a part, or the
|
|
// partIndex if we are. By not incrementing the nodeIndex when we find a
|
|
// part, we allow for the next part to be associated with the current
|
|
// node if neccessasry.
|
|
if (!isTemplatePartActive(part)) {
|
|
this._parts.push(undefined);
|
|
partIndex++;
|
|
} else if (nodeIndex === part.index) {
|
|
if (part.type === 'node') {
|
|
const part = this.processor.handleTextExpression(this.options);
|
|
part.insertAfterNode(node.previousSibling);
|
|
this._parts.push(part);
|
|
} else {
|
|
this._parts.push(...this.processor.handleAttributeExpressions(node, part.name, part.strings, this.options));
|
|
}
|
|
partIndex++;
|
|
} else {
|
|
nodeIndex++;
|
|
if (node.nodeName === 'TEMPLATE') {
|
|
_prepareInstance(node.content);
|
|
}
|
|
node = walker.nextNode();
|
|
}
|
|
}
|
|
};
|
|
_prepareInstance(fragment);
|
|
if (isCEPolyfill) {
|
|
document.adoptNode(fragment);
|
|
customElements.upgrade(fragment);
|
|
}
|
|
return fragment;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
/**
|
|
* The return type of `html`, which holds a Template and the values from
|
|
* interpolated expressions.
|
|
*/
|
|
class TemplateResult {
|
|
constructor(strings, values, type, processor) {
|
|
this.strings = strings;
|
|
this.values = values;
|
|
this.type = type;
|
|
this.processor = processor;
|
|
}
|
|
/**
|
|
* Returns a string of HTML used to create a `<template>` element.
|
|
*/
|
|
getHTML() {
|
|
const endIndex = this.strings.length - 1;
|
|
let html = '';
|
|
for (let i = 0; i < endIndex; i++) {
|
|
const s = this.strings[i];
|
|
// This exec() call does two things:
|
|
// 1) Appends a suffix to the bound attribute name to opt out of special
|
|
// attribute value parsing that IE11 and Edge do, like for style and
|
|
// many SVG attributes. The Template class also appends the same suffix
|
|
// when looking up attributes to create Parts.
|
|
// 2) Adds an unquoted-attribute-safe marker for the first expression in
|
|
// an attribute. Subsequent attribute expressions will use node markers,
|
|
// and this is safe since attributes with multiple expressions are
|
|
// guaranteed to be quoted.
|
|
const match = lastAttributeNameRegex.exec(s);
|
|
if (match) {
|
|
// We're starting a new bound attribute.
|
|
// Add the safe attribute suffix, and use unquoted-attribute-safe
|
|
// marker.
|
|
html += s.substr(0, match.index) + match[1] + match[2] + boundAttributeSuffix + match[3] + marker;
|
|
} else {
|
|
// We're either in a bound node, or trailing bound attribute.
|
|
// Either way, nodeMarker is safe to use.
|
|
html += s + nodeMarker;
|
|
}
|
|
}
|
|
return html + this.strings[endIndex];
|
|
}
|
|
getTemplateElement() {
|
|
const template = document.createElement('template');
|
|
template.innerHTML = this.getHTML();
|
|
return template;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
const isPrimitive = value => {
|
|
return value === null || !(typeof value === 'object' || typeof value === 'function');
|
|
};
|
|
/**
|
|
* Sets attribute values for AttributeParts, so that the value is only set once
|
|
* even if there are multiple parts for an attribute.
|
|
*/
|
|
class AttributeCommitter {
|
|
constructor(element, name, strings) {
|
|
this.dirty = true;
|
|
this.element = element;
|
|
this.name = name;
|
|
this.strings = strings;
|
|
this.parts = [];
|
|
for (let i = 0; i < strings.length - 1; i++) {
|
|
this.parts[i] = this._createPart();
|
|
}
|
|
}
|
|
/**
|
|
* Creates a single part. Override this to create a differnt type of part.
|
|
*/
|
|
_createPart() {
|
|
return new AttributePart(this);
|
|
}
|
|
_getValue() {
|
|
const strings = this.strings;
|
|
const l = strings.length - 1;
|
|
let text = '';
|
|
for (let i = 0; i < l; i++) {
|
|
text += strings[i];
|
|
const part = this.parts[i];
|
|
if (part !== undefined) {
|
|
const v = part.value;
|
|
if (v != null && (Array.isArray(v) ||
|
|
// tslint:disable-next-line:no-any
|
|
typeof v !== 'string' && v[Symbol.iterator])) {
|
|
for (const t of v) {
|
|
text += typeof t === 'string' ? t : String(t);
|
|
}
|
|
} else {
|
|
text += typeof v === 'string' ? v : String(v);
|
|
}
|
|
}
|
|
}
|
|
text += strings[l];
|
|
return text;
|
|
}
|
|
commit() {
|
|
if (this.dirty) {
|
|
this.dirty = false;
|
|
this.element.setAttribute(this.name, this._getValue());
|
|
}
|
|
}
|
|
}
|
|
class AttributePart {
|
|
constructor(comitter) {
|
|
this.value = undefined;
|
|
this.committer = comitter;
|
|
}
|
|
setValue(value) {
|
|
if (value !== noChange && (!isPrimitive(value) || value !== this.value)) {
|
|
this.value = value;
|
|
// If the value is a not a directive, dirty the committer so that it'll
|
|
// call setAttribute. If the value is a directive, it'll dirty the
|
|
// committer if it calls setValue().
|
|
if (!isDirective(value)) {
|
|
this.committer.dirty = true;
|
|
}
|
|
}
|
|
}
|
|
commit() {
|
|
while (isDirective(this.value)) {
|
|
const directive = this.value;
|
|
this.value = noChange;
|
|
directive(this);
|
|
}
|
|
if (this.value === noChange) {
|
|
return;
|
|
}
|
|
this.committer.commit();
|
|
}
|
|
}
|
|
class NodePart {
|
|
constructor(options) {
|
|
this.value = undefined;
|
|
this._pendingValue = undefined;
|
|
this.options = options;
|
|
}
|
|
/**
|
|
* Inserts this part into a container.
|
|
*
|
|
* This part must be empty, as its contents are not automatically moved.
|
|
*/
|
|
appendInto(container) {
|
|
this.startNode = container.appendChild(createMarker());
|
|
this.endNode = container.appendChild(createMarker());
|
|
}
|
|
/**
|
|
* Inserts this part between `ref` and `ref`'s next sibling. Both `ref` and
|
|
* its next sibling must be static, unchanging nodes such as those that appear
|
|
* in a literal section of a template.
|
|
*
|
|
* This part must be empty, as its contents are not automatically moved.
|
|
*/
|
|
insertAfterNode(ref) {
|
|
this.startNode = ref;
|
|
this.endNode = ref.nextSibling;
|
|
}
|
|
/**
|
|
* Appends this part into a parent part.
|
|
*
|
|
* This part must be empty, as its contents are not automatically moved.
|
|
*/
|
|
appendIntoPart(part) {
|
|
part._insert(this.startNode = createMarker());
|
|
part._insert(this.endNode = createMarker());
|
|
}
|
|
/**
|
|
* Appends this part after `ref`
|
|
*
|
|
* This part must be empty, as its contents are not automatically moved.
|
|
*/
|
|
insertAfterPart(ref) {
|
|
ref._insert(this.startNode = createMarker());
|
|
this.endNode = ref.endNode;
|
|
ref.endNode = this.startNode;
|
|
}
|
|
setValue(value) {
|
|
this._pendingValue = value;
|
|
}
|
|
commit() {
|
|
while (isDirective(this._pendingValue)) {
|
|
const directive = this._pendingValue;
|
|
this._pendingValue = noChange;
|
|
directive(this);
|
|
}
|
|
const value = this._pendingValue;
|
|
if (value === noChange) {
|
|
return;
|
|
}
|
|
if (isPrimitive(value)) {
|
|
if (value !== this.value) {
|
|
this._commitText(value);
|
|
}
|
|
} else if (value instanceof TemplateResult) {
|
|
this._commitTemplateResult(value);
|
|
} else if (value instanceof Node) {
|
|
this._commitNode(value);
|
|
} else if (Array.isArray(value) ||
|
|
// tslint:disable-next-line:no-any
|
|
value[Symbol.iterator]) {
|
|
this._commitIterable(value);
|
|
} else if (value === nothing) {
|
|
this.value = nothing;
|
|
this.clear();
|
|
} else {
|
|
// Fallback, will render the string representation
|
|
this._commitText(value);
|
|
}
|
|
}
|
|
_insert(node) {
|
|
this.endNode.parentNode.insertBefore(node, this.endNode);
|
|
}
|
|
_commitNode(value) {
|
|
if (this.value === value) {
|
|
return;
|
|
}
|
|
this.clear();
|
|
this._insert(value);
|
|
this.value = value;
|
|
}
|
|
_commitText(value) {
|
|
const node = this.startNode.nextSibling;
|
|
value = value == null ? '' : value;
|
|
if (node === this.endNode.previousSibling && node.nodeType === 3 /* Node.TEXT_NODE */) {
|
|
// If we only have a single text node between the markers, we can just
|
|
// set its value, rather than replacing it.
|
|
// TODO(justinfagnani): Can we just check if this.value is primitive?
|
|
node.data = value;
|
|
} else {
|
|
this._commitNode(document.createTextNode(typeof value === 'string' ? value : String(value)));
|
|
}
|
|
this.value = value;
|
|
}
|
|
_commitTemplateResult(value) {
|
|
const template = this.options.templateFactory(value);
|
|
if (this.value instanceof TemplateInstance && this.value.template === template) {
|
|
this.value.update(value.values);
|
|
} else {
|
|
// Make sure we propagate the template processor from the TemplateResult
|
|
// so that we use its syntax extension, etc. The template factory comes
|
|
// from the render function options so that it can control template
|
|
// caching and preprocessing.
|
|
const instance = new TemplateInstance(template, value.processor, this.options);
|
|
const fragment = instance._clone();
|
|
instance.update(value.values);
|
|
this._commitNode(fragment);
|
|
this.value = instance;
|
|
}
|
|
}
|
|
_commitIterable(value) {
|
|
// For an Iterable, we create a new InstancePart per item, then set its
|
|
// value to the item. This is a little bit of overhead for every item in
|
|
// an Iterable, but it lets us recurse easily and efficiently update Arrays
|
|
// of TemplateResults that will be commonly returned from expressions like:
|
|
// array.map((i) => html`${i}`), by reusing existing TemplateInstances.
|
|
// If _value is an array, then the previous render was of an
|
|
// iterable and _value will contain the NodeParts from the previous
|
|
// render. If _value is not an array, clear this part and make a new
|
|
// array for NodeParts.
|
|
if (!Array.isArray(this.value)) {
|
|
this.value = [];
|
|
this.clear();
|
|
}
|
|
// Lets us keep track of how many items we stamped so we can clear leftover
|
|
// items from a previous render
|
|
const itemParts = this.value;
|
|
let partIndex = 0;
|
|
let itemPart;
|
|
for (const item of value) {
|
|
// Try to reuse an existing part
|
|
itemPart = itemParts[partIndex];
|
|
// If no existing part, create a new one
|
|
if (itemPart === undefined) {
|
|
itemPart = new NodePart(this.options);
|
|
itemParts.push(itemPart);
|
|
if (partIndex === 0) {
|
|
itemPart.appendIntoPart(this);
|
|
} else {
|
|
itemPart.insertAfterPart(itemParts[partIndex - 1]);
|
|
}
|
|
}
|
|
itemPart.setValue(item);
|
|
itemPart.commit();
|
|
partIndex++;
|
|
}
|
|
if (partIndex < itemParts.length) {
|
|
// Truncate the parts array so _value reflects the current state
|
|
itemParts.length = partIndex;
|
|
this.clear(itemPart && itemPart.endNode);
|
|
}
|
|
}
|
|
clear(startNode = this.startNode) {
|
|
removeNodes(this.startNode.parentNode, startNode.nextSibling, this.endNode);
|
|
}
|
|
}
|
|
/**
|
|
* Implements a boolean attribute, roughly as defined in the HTML
|
|
* specification.
|
|
*
|
|
* If the value is truthy, then the attribute is present with a value of
|
|
* ''. If the value is falsey, the attribute is removed.
|
|
*/
|
|
class BooleanAttributePart {
|
|
constructor(element, name, strings) {
|
|
this.value = undefined;
|
|
this._pendingValue = undefined;
|
|
if (strings.length !== 2 || strings[0] !== '' || strings[1] !== '') {
|
|
throw new Error('Boolean attributes can only contain a single expression');
|
|
}
|
|
this.element = element;
|
|
this.name = name;
|
|
this.strings = strings;
|
|
}
|
|
setValue(value) {
|
|
this._pendingValue = value;
|
|
}
|
|
commit() {
|
|
while (isDirective(this._pendingValue)) {
|
|
const directive = this._pendingValue;
|
|
this._pendingValue = noChange;
|
|
directive(this);
|
|
}
|
|
if (this._pendingValue === noChange) {
|
|
return;
|
|
}
|
|
const value = !!this._pendingValue;
|
|
if (this.value !== value) {
|
|
if (value) {
|
|
this.element.setAttribute(this.name, '');
|
|
} else {
|
|
this.element.removeAttribute(this.name);
|
|
}
|
|
}
|
|
this.value = value;
|
|
this._pendingValue = noChange;
|
|
}
|
|
}
|
|
/**
|
|
* Sets attribute values for PropertyParts, so that the value is only set once
|
|
* even if there are multiple parts for a property.
|
|
*
|
|
* If an expression controls the whole property value, then the value is simply
|
|
* assigned to the property under control. If there are string literals or
|
|
* multiple expressions, then the strings are expressions are interpolated into
|
|
* a string first.
|
|
*/
|
|
class PropertyCommitter extends AttributeCommitter {
|
|
constructor(element, name, strings) {
|
|
super(element, name, strings);
|
|
this.single = strings.length === 2 && strings[0] === '' && strings[1] === '';
|
|
}
|
|
_createPart() {
|
|
return new PropertyPart(this);
|
|
}
|
|
_getValue() {
|
|
if (this.single) {
|
|
return this.parts[0].value;
|
|
}
|
|
return super._getValue();
|
|
}
|
|
commit() {
|
|
if (this.dirty) {
|
|
this.dirty = false;
|
|
// tslint:disable-next-line:no-any
|
|
this.element[this.name] = this._getValue();
|
|
}
|
|
}
|
|
}
|
|
class PropertyPart extends AttributePart {}
|
|
// Detect event listener options support. If the `capture` property is read
|
|
// from the options object, then options are supported. If not, then the thrid
|
|
// argument to add/removeEventListener is interpreted as the boolean capture
|
|
// value so we should only pass the `capture` property.
|
|
let eventOptionsSupported = false;
|
|
try {
|
|
const options = {
|
|
get capture() {
|
|
eventOptionsSupported = true;
|
|
return false;
|
|
}
|
|
};
|
|
// tslint:disable-next-line:no-any
|
|
window.addEventListener('test', options, options);
|
|
// tslint:disable-next-line:no-any
|
|
window.removeEventListener('test', options, options);
|
|
} catch (_e) {}
|
|
class EventPart {
|
|
constructor(element, eventName, eventContext) {
|
|
this.value = undefined;
|
|
this._pendingValue = undefined;
|
|
this.element = element;
|
|
this.eventName = eventName;
|
|
this.eventContext = eventContext;
|
|
this._boundHandleEvent = e => this.handleEvent(e);
|
|
}
|
|
setValue(value) {
|
|
this._pendingValue = value;
|
|
}
|
|
commit() {
|
|
while (isDirective(this._pendingValue)) {
|
|
const directive = this._pendingValue;
|
|
this._pendingValue = noChange;
|
|
directive(this);
|
|
}
|
|
if (this._pendingValue === noChange) {
|
|
return;
|
|
}
|
|
const newListener = this._pendingValue;
|
|
const oldListener = this.value;
|
|
const shouldRemoveListener = newListener == null || oldListener != null && (newListener.capture !== oldListener.capture || newListener.once !== oldListener.once || newListener.passive !== oldListener.passive);
|
|
const shouldAddListener = newListener != null && (oldListener == null || shouldRemoveListener);
|
|
if (shouldRemoveListener) {
|
|
this.element.removeEventListener(this.eventName, this._boundHandleEvent, this._options);
|
|
}
|
|
if (shouldAddListener) {
|
|
this._options = getOptions(newListener);
|
|
this.element.addEventListener(this.eventName, this._boundHandleEvent, this._options);
|
|
}
|
|
this.value = newListener;
|
|
this._pendingValue = noChange;
|
|
}
|
|
handleEvent(event) {
|
|
if (typeof this.value === 'function') {
|
|
this.value.call(this.eventContext || this.element, event);
|
|
} else {
|
|
this.value.handleEvent(event);
|
|
}
|
|
}
|
|
}
|
|
// We copy options because of the inconsistent behavior of browsers when reading
|
|
// the third argument of add/removeEventListener. IE11 doesn't support options
|
|
// at all. Chrome 41 only reads `capture` if the argument is an object.
|
|
const getOptions = o => o && (eventOptionsSupported ? { capture: o.capture, passive: o.passive, once: o.once } : o.capture);
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
/**
|
|
* Creates Parts when a template is instantiated.
|
|
*/
|
|
class DefaultTemplateProcessor {
|
|
/**
|
|
* Create parts for an attribute-position binding, given the event, attribute
|
|
* name, and string literals.
|
|
*
|
|
* @param element The element containing the binding
|
|
* @param name The attribute name
|
|
* @param strings The string literals. There are always at least two strings,
|
|
* event for fully-controlled bindings with a single expression.
|
|
*/
|
|
handleAttributeExpressions(element, name, strings, options) {
|
|
const prefix = name[0];
|
|
if (prefix === '.') {
|
|
const comitter = new PropertyCommitter(element, name.slice(1), strings);
|
|
return comitter.parts;
|
|
}
|
|
if (prefix === '@') {
|
|
return [new EventPart(element, name.slice(1), options.eventContext)];
|
|
}
|
|
if (prefix === '?') {
|
|
return [new BooleanAttributePart(element, name.slice(1), strings)];
|
|
}
|
|
const comitter = new AttributeCommitter(element, name, strings);
|
|
return comitter.parts;
|
|
}
|
|
/**
|
|
* Create parts for a text-position binding.
|
|
* @param templateFactory
|
|
*/
|
|
handleTextExpression(options) {
|
|
return new NodePart(options);
|
|
}
|
|
}
|
|
const defaultTemplateProcessor = new DefaultTemplateProcessor();
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
/**
|
|
* The default TemplateFactory which caches Templates keyed on
|
|
* result.type and result.strings.
|
|
*/
|
|
function templateFactory(result) {
|
|
let templateCache = templateCaches.get(result.type);
|
|
if (templateCache === undefined) {
|
|
templateCache = {
|
|
stringsArray: new WeakMap(),
|
|
keyString: new Map()
|
|
};
|
|
templateCaches.set(result.type, templateCache);
|
|
}
|
|
let template = templateCache.stringsArray.get(result.strings);
|
|
if (template !== undefined) {
|
|
return template;
|
|
}
|
|
// If the TemplateStringsArray is new, generate a key from the strings
|
|
// This key is shared between all templates with identical content
|
|
const key = result.strings.join(marker);
|
|
// Check if we already have a Template for this key
|
|
template = templateCache.keyString.get(key);
|
|
if (template === undefined) {
|
|
// If we have not seen this key before, create a new Template
|
|
template = new Template(result, result.getTemplateElement());
|
|
// Cache the Template for this key
|
|
templateCache.keyString.set(key, template);
|
|
}
|
|
// Cache all future queries for this TemplateStringsArray
|
|
templateCache.stringsArray.set(result.strings, template);
|
|
return template;
|
|
}
|
|
const templateCaches = new Map();
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
const parts = new WeakMap();
|
|
/**
|
|
* Renders a template to a container.
|
|
*
|
|
* To update a container with new values, reevaluate the template literal and
|
|
* call `render` with the new result.
|
|
*
|
|
* @param result a TemplateResult created by evaluating a template tag like
|
|
* `html` or `svg`.
|
|
* @param container A DOM parent to render to. The entire contents are either
|
|
* replaced, or efficiently updated if the same result type was previous
|
|
* rendered there.
|
|
* @param options RenderOptions for the entire render tree rendered to this
|
|
* container. Render options must *not* change between renders to the same
|
|
* container, as those changes will not effect previously rendered DOM.
|
|
*/
|
|
const render = (result, container, options) => {
|
|
let part = parts.get(container);
|
|
if (part === undefined) {
|
|
removeNodes(container, container.firstChild);
|
|
parts.set(container, part = new NodePart(Object.assign({ templateFactory }, options)));
|
|
part.appendInto(container);
|
|
}
|
|
part.setValue(result);
|
|
part.commit();
|
|
};
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
// IMPORTANT: do not change the property name or the assignment expression.
|
|
// This line will be used in regexes to search for lit-html usage.
|
|
// TODO(justinfagnani): inject version number at build time
|
|
(window['litHtmlVersions'] || (window['litHtmlVersions'] = [])).push('1.0.0');
|
|
/**
|
|
* Interprets a template literal as an HTML template that can efficiently
|
|
* render to and update a container.
|
|
*/
|
|
const html = (strings, ...values) => new TemplateResult(strings, values, 'html', defaultTemplateProcessor);
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
const walkerNodeFilter = 133 /* NodeFilter.SHOW_{ELEMENT|COMMENT|TEXT} */;
|
|
/**
|
|
* Removes the list of nodes from a Template safely. In addition to removing
|
|
* nodes from the Template, the Template part indices are updated to match
|
|
* the mutated Template DOM.
|
|
*
|
|
* As the template is walked the removal state is tracked and
|
|
* part indices are adjusted as needed.
|
|
*
|
|
* div
|
|
* div#1 (remove) <-- start removing (removing node is div#1)
|
|
* div
|
|
* div#2 (remove) <-- continue removing (removing node is still div#1)
|
|
* div
|
|
* div <-- stop removing since previous sibling is the removing node (div#1,
|
|
* removed 4 nodes)
|
|
*/
|
|
function removeNodesFromTemplate(template, nodesToRemove) {
|
|
const { element: { content }, parts } = template;
|
|
const walker = document.createTreeWalker(content, walkerNodeFilter, null, false);
|
|
let partIndex = nextActiveIndexInTemplateParts(parts);
|
|
let part = parts[partIndex];
|
|
let nodeIndex = -1;
|
|
let removeCount = 0;
|
|
const nodesToRemoveInTemplate = [];
|
|
let currentRemovingNode = null;
|
|
while (walker.nextNode()) {
|
|
nodeIndex++;
|
|
const node = walker.currentNode;
|
|
// End removal if stepped past the removing node
|
|
if (node.previousSibling === currentRemovingNode) {
|
|
currentRemovingNode = null;
|
|
}
|
|
// A node to remove was found in the template
|
|
if (nodesToRemove.has(node)) {
|
|
nodesToRemoveInTemplate.push(node);
|
|
// Track node we're removing
|
|
if (currentRemovingNode === null) {
|
|
currentRemovingNode = node;
|
|
}
|
|
}
|
|
// When removing, increment count by which to adjust subsequent part indices
|
|
if (currentRemovingNode !== null) {
|
|
removeCount++;
|
|
}
|
|
while (part !== undefined && part.index === nodeIndex) {
|
|
// If part is in a removed node deactivate it by setting index to -1 or
|
|
// adjust the index as needed.
|
|
part.index = currentRemovingNode !== null ? -1 : part.index - removeCount;
|
|
// go to the next active part.
|
|
partIndex = nextActiveIndexInTemplateParts(parts, partIndex);
|
|
part = parts[partIndex];
|
|
}
|
|
}
|
|
nodesToRemoveInTemplate.forEach(n => n.parentNode.removeChild(n));
|
|
}
|
|
const countNodes = node => {
|
|
let count = node.nodeType === 11 /* Node.DOCUMENT_FRAGMENT_NODE */ ? 0 : 1;
|
|
const walker = document.createTreeWalker(node, walkerNodeFilter, null, false);
|
|
while (walker.nextNode()) {
|
|
count++;
|
|
}
|
|
return count;
|
|
};
|
|
const nextActiveIndexInTemplateParts = (parts, startIndex = -1) => {
|
|
for (let i = startIndex + 1; i < parts.length; i++) {
|
|
const part = parts[i];
|
|
if (isTemplatePartActive(part)) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
};
|
|
/**
|
|
* Inserts the given node into the Template, optionally before the given
|
|
* refNode. In addition to inserting the node into the Template, the Template
|
|
* part indices are updated to match the mutated Template DOM.
|
|
*/
|
|
function insertNodeIntoTemplate(template, node, refNode = null) {
|
|
const { element: { content }, parts } = template;
|
|
// If there's no refNode, then put node at end of template.
|
|
// No part indices need to be shifted in this case.
|
|
if (refNode === null || refNode === undefined) {
|
|
content.appendChild(node);
|
|
return;
|
|
}
|
|
const walker = document.createTreeWalker(content, walkerNodeFilter, null, false);
|
|
let partIndex = nextActiveIndexInTemplateParts(parts);
|
|
let insertCount = 0;
|
|
let walkerIndex = -1;
|
|
while (walker.nextNode()) {
|
|
walkerIndex++;
|
|
const walkerNode = walker.currentNode;
|
|
if (walkerNode === refNode) {
|
|
insertCount = countNodes(node);
|
|
refNode.parentNode.insertBefore(node, refNode);
|
|
}
|
|
while (partIndex !== -1 && parts[partIndex].index === walkerIndex) {
|
|
// If we've inserted the node, simply adjust all subsequent parts
|
|
if (insertCount > 0) {
|
|
while (partIndex !== -1) {
|
|
parts[partIndex].index += insertCount;
|
|
partIndex = nextActiveIndexInTemplateParts(parts, partIndex);
|
|
}
|
|
return;
|
|
}
|
|
partIndex = nextActiveIndexInTemplateParts(parts, partIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
// Get a key to lookup in `templateCaches`.
|
|
const getTemplateCacheKey = (type, scopeName) => `${type}--${scopeName}`;
|
|
let compatibleShadyCSSVersion = true;
|
|
if (typeof window.ShadyCSS === 'undefined') {
|
|
compatibleShadyCSSVersion = false;
|
|
} else if (typeof window.ShadyCSS.prepareTemplateDom === 'undefined') {
|
|
console.warn(`Incompatible ShadyCSS version detected.` + `Please update to at least @webcomponents/webcomponentsjs@2.0.2 and` + `@webcomponents/shadycss@1.3.1.`);
|
|
compatibleShadyCSSVersion = false;
|
|
}
|
|
/**
|
|
* Template factory which scopes template DOM using ShadyCSS.
|
|
* @param scopeName {string}
|
|
*/
|
|
const shadyTemplateFactory = scopeName => result => {
|
|
const cacheKey = getTemplateCacheKey(result.type, scopeName);
|
|
let templateCache = templateCaches.get(cacheKey);
|
|
if (templateCache === undefined) {
|
|
templateCache = {
|
|
stringsArray: new WeakMap(),
|
|
keyString: new Map()
|
|
};
|
|
templateCaches.set(cacheKey, templateCache);
|
|
}
|
|
let template = templateCache.stringsArray.get(result.strings);
|
|
if (template !== undefined) {
|
|
return template;
|
|
}
|
|
const key = result.strings.join(marker);
|
|
template = templateCache.keyString.get(key);
|
|
if (template === undefined) {
|
|
const element = result.getTemplateElement();
|
|
if (compatibleShadyCSSVersion) {
|
|
window.ShadyCSS.prepareTemplateDom(element, scopeName);
|
|
}
|
|
template = new Template(result, element);
|
|
templateCache.keyString.set(key, template);
|
|
}
|
|
templateCache.stringsArray.set(result.strings, template);
|
|
return template;
|
|
};
|
|
const TEMPLATE_TYPES = ['html', 'svg'];
|
|
/**
|
|
* Removes all style elements from Templates for the given scopeName.
|
|
*/
|
|
const removeStylesFromLitTemplates = scopeName => {
|
|
TEMPLATE_TYPES.forEach(type => {
|
|
const templates = templateCaches.get(getTemplateCacheKey(type, scopeName));
|
|
if (templates !== undefined) {
|
|
templates.keyString.forEach(template => {
|
|
const { element: { content } } = template;
|
|
// IE 11 doesn't support the iterable param Set constructor
|
|
const styles = new Set();
|
|
Array.from(content.querySelectorAll('style')).forEach(s => {
|
|
styles.add(s);
|
|
});
|
|
removeNodesFromTemplate(template, styles);
|
|
});
|
|
}
|
|
});
|
|
};
|
|
const shadyRenderSet = new Set();
|
|
/**
|
|
* For the given scope name, ensures that ShadyCSS style scoping is performed.
|
|
* This is done just once per scope name so the fragment and template cannot
|
|
* be modified.
|
|
* (1) extracts styles from the rendered fragment and hands them to ShadyCSS
|
|
* to be scoped and appended to the document
|
|
* (2) removes style elements from all lit-html Templates for this scope name.
|
|
*
|
|
* Note, <style> elements can only be placed into templates for the
|
|
* initial rendering of the scope. If <style> elements are included in templates
|
|
* dynamically rendered to the scope (after the first scope render), they will
|
|
* not be scoped and the <style> will be left in the template and rendered
|
|
* output.
|
|
*/
|
|
const prepareTemplateStyles = (renderedDOM, template, scopeName) => {
|
|
shadyRenderSet.add(scopeName);
|
|
// Move styles out of rendered DOM and store.
|
|
const styles = renderedDOM.querySelectorAll('style');
|
|
// If there are no styles, skip unnecessary work
|
|
if (styles.length === 0) {
|
|
// Ensure prepareTemplateStyles is called to support adding
|
|
// styles via `prepareAdoptedCssText` since that requires that
|
|
// `prepareTemplateStyles` is called.
|
|
window.ShadyCSS.prepareTemplateStyles(template.element, scopeName);
|
|
return;
|
|
}
|
|
const condensedStyle = document.createElement('style');
|
|
// Collect styles into a single style. This helps us make sure ShadyCSS
|
|
// manipulations will not prevent us from being able to fix up template
|
|
// part indices.
|
|
// NOTE: collecting styles is inefficient for browsers but ShadyCSS
|
|
// currently does this anyway. When it does not, this should be changed.
|
|
for (let i = 0; i < styles.length; i++) {
|
|
const style = styles[i];
|
|
style.parentNode.removeChild(style);
|
|
condensedStyle.textContent += style.textContent;
|
|
}
|
|
// Remove styles from nested templates in this scope.
|
|
removeStylesFromLitTemplates(scopeName);
|
|
// And then put the condensed style into the "root" template passed in as
|
|
// `template`.
|
|
insertNodeIntoTemplate(template, condensedStyle, template.element.content.firstChild);
|
|
// Note, it's important that ShadyCSS gets the template that `lit-html`
|
|
// will actually render so that it can update the style inside when
|
|
// needed (e.g. @apply native Shadow DOM case).
|
|
window.ShadyCSS.prepareTemplateStyles(template.element, scopeName);
|
|
if (window.ShadyCSS.nativeShadow) {
|
|
// When in native Shadow DOM, re-add styling to rendered content using
|
|
// the style ShadyCSS produced.
|
|
const style = template.element.content.querySelector('style');
|
|
renderedDOM.insertBefore(style.cloneNode(true), renderedDOM.firstChild);
|
|
} else {
|
|
// When not in native Shadow DOM, at this point ShadyCSS will have
|
|
// removed the style from the lit template and parts will be broken as a
|
|
// result. To fix this, we put back the style node ShadyCSS removed
|
|
// and then tell lit to remove that node from the template.
|
|
// NOTE, ShadyCSS creates its own style so we can safely add/remove
|
|
// `condensedStyle` here.
|
|
template.element.content.insertBefore(condensedStyle, template.element.content.firstChild);
|
|
const removes = new Set();
|
|
removes.add(condensedStyle);
|
|
removeNodesFromTemplate(template, removes);
|
|
}
|
|
};
|
|
/**
|
|
* Extension to the standard `render` method which supports rendering
|
|
* to ShadowRoots when the ShadyDOM (https://github.com/webcomponents/shadydom)
|
|
* and ShadyCSS (https://github.com/webcomponents/shadycss) polyfills are used
|
|
* or when the webcomponentsjs
|
|
* (https://github.com/webcomponents/webcomponentsjs) polyfill is used.
|
|
*
|
|
* Adds a `scopeName` option which is used to scope element DOM and stylesheets
|
|
* when native ShadowDOM is unavailable. The `scopeName` will be added to
|
|
* the class attribute of all rendered DOM. In addition, any style elements will
|
|
* be automatically re-written with this `scopeName` selector and moved out
|
|
* of the rendered DOM and into the document `<head>`.
|
|
*
|
|
* It is common to use this render method in conjunction with a custom element
|
|
* which renders a shadowRoot. When this is done, typically the element's
|
|
* `localName` should be used as the `scopeName`.
|
|
*
|
|
* In addition to DOM scoping, ShadyCSS also supports a basic shim for css
|
|
* custom properties (needed only on older browsers like IE11) and a shim for
|
|
* a deprecated feature called `@apply` that supports applying a set of css
|
|
* custom properties to a given location.
|
|
*
|
|
* Usage considerations:
|
|
*
|
|
* * Part values in `<style>` elements are only applied the first time a given
|
|
* `scopeName` renders. Subsequent changes to parts in style elements will have
|
|
* no effect. Because of this, parts in style elements should only be used for
|
|
* values that will never change, for example parts that set scope-wide theme
|
|
* values or parts which render shared style elements.
|
|
*
|
|
* * Note, due to a limitation of the ShadyDOM polyfill, rendering in a
|
|
* custom element's `constructor` is not supported. Instead rendering should
|
|
* either done asynchronously, for example at microtask timing (for example
|
|
* `Promise.resolve()`), or be deferred until the first time the element's
|
|
* `connectedCallback` runs.
|
|
*
|
|
* Usage considerations when using shimmed custom properties or `@apply`:
|
|
*
|
|
* * Whenever any dynamic changes are made which affect
|
|
* css custom properties, `ShadyCSS.styleElement(element)` must be called
|
|
* to update the element. There are two cases when this is needed:
|
|
* (1) the element is connected to a new parent, (2) a class is added to the
|
|
* element that causes it to match different custom properties.
|
|
* To address the first case when rendering a custom element, `styleElement`
|
|
* should be called in the element's `connectedCallback`.
|
|
*
|
|
* * Shimmed custom properties may only be defined either for an entire
|
|
* shadowRoot (for example, in a `:host` rule) or via a rule that directly
|
|
* matches an element with a shadowRoot. In other words, instead of flowing from
|
|
* parent to child as do native css custom properties, shimmed custom properties
|
|
* flow only from shadowRoots to nested shadowRoots.
|
|
*
|
|
* * When using `@apply` mixing css shorthand property names with
|
|
* non-shorthand names (for example `border` and `border-width`) is not
|
|
* supported.
|
|
*/
|
|
const render$1 = (result, container, options) => {
|
|
const scopeName = options.scopeName;
|
|
const hasRendered = parts.has(container);
|
|
const needsScoping = container instanceof ShadowRoot && compatibleShadyCSSVersion && result instanceof TemplateResult;
|
|
// Handle first render to a scope specially...
|
|
const firstScopeRender = needsScoping && !shadyRenderSet.has(scopeName);
|
|
// On first scope render, render into a fragment; this cannot be a single
|
|
// fragment that is reused since nested renders can occur synchronously.
|
|
const renderContainer = firstScopeRender ? document.createDocumentFragment() : container;
|
|
render(result, renderContainer, Object.assign({ templateFactory: shadyTemplateFactory(scopeName) }, options));
|
|
// When performing first scope render,
|
|
// (1) We've rendered into a fragment so that there's a chance to
|
|
// `prepareTemplateStyles` before sub-elements hit the DOM
|
|
// (which might cause them to render based on a common pattern of
|
|
// rendering in a custom element's `connectedCallback`);
|
|
// (2) Scope the template with ShadyCSS one time only for this scope.
|
|
// (3) Render the fragment into the container and make sure the
|
|
// container knows its `part` is the one we just rendered. This ensures
|
|
// DOM will be re-used on subsequent renders.
|
|
if (firstScopeRender) {
|
|
const part = parts.get(renderContainer);
|
|
parts.delete(renderContainer);
|
|
if (part.value instanceof TemplateInstance) {
|
|
prepareTemplateStyles(renderContainer, part.value.template, scopeName);
|
|
}
|
|
removeNodes(container, container.firstChild);
|
|
container.appendChild(renderContainer);
|
|
parts.set(container, part);
|
|
}
|
|
// After elements have hit the DOM, update styling if this is the
|
|
// initial render to this container.
|
|
// This is needed whenever dynamic changes are made so it would be
|
|
// safest to do every render; however, this would regress performance
|
|
// so we leave it up to the user to call `ShadyCSSS.styleElement`
|
|
// for dynamic changes.
|
|
if (!hasRendered && needsScoping) {
|
|
window.ShadyCSS.styleElement(container.host);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
/**
|
|
* When using Closure Compiler, JSCompiler_renameProperty(property, object) is
|
|
* replaced at compile time by the munged name for object[property]. We cannot
|
|
* alias this function, so we have to use a small shim that has the same
|
|
* behavior when not compiling.
|
|
*/
|
|
window.JSCompiler_renameProperty = (prop, _obj) => prop;
|
|
const defaultConverter = {
|
|
toAttribute(value, type) {
|
|
switch (type) {
|
|
case Boolean:
|
|
return value ? '' : null;
|
|
case Object:
|
|
case Array:
|
|
// if the value is `null` or `undefined` pass this through
|
|
// to allow removing/no change behavior.
|
|
return value == null ? value : JSON.stringify(value);
|
|
}
|
|
return value;
|
|
},
|
|
fromAttribute(value, type) {
|
|
switch (type) {
|
|
case Boolean:
|
|
return value !== null;
|
|
case Number:
|
|
return value === null ? null : Number(value);
|
|
case Object:
|
|
case Array:
|
|
return JSON.parse(value);
|
|
}
|
|
return value;
|
|
}
|
|
};
|
|
/**
|
|
* Change function that returns true if `value` is different from `oldValue`.
|
|
* This method is used as the default for a property's `hasChanged` function.
|
|
*/
|
|
const notEqual = (value, old) => {
|
|
// This ensures (old==NaN, value==NaN) always returns false
|
|
return old !== value && (old === old || value === value);
|
|
};
|
|
const defaultPropertyDeclaration = {
|
|
attribute: true,
|
|
type: String,
|
|
converter: defaultConverter,
|
|
reflect: false,
|
|
hasChanged: notEqual
|
|
};
|
|
const microtaskPromise = Promise.resolve(true);
|
|
const STATE_HAS_UPDATED = 1;
|
|
const STATE_UPDATE_REQUESTED = 1 << 2;
|
|
const STATE_IS_REFLECTING_TO_ATTRIBUTE = 1 << 3;
|
|
const STATE_IS_REFLECTING_TO_PROPERTY = 1 << 4;
|
|
const STATE_HAS_CONNECTED = 1 << 5;
|
|
/**
|
|
* Base element class which manages element properties and attributes. When
|
|
* properties change, the `update` method is asynchronously called. This method
|
|
* should be supplied by subclassers to render updates as desired.
|
|
*/
|
|
class UpdatingElement extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this._updateState = 0;
|
|
this._instanceProperties = undefined;
|
|
this._updatePromise = microtaskPromise;
|
|
this._hasConnectedResolver = undefined;
|
|
/**
|
|
* Map with keys for any properties that have changed since the last
|
|
* update cycle with previous values.
|
|
*/
|
|
this._changedProperties = new Map();
|
|
/**
|
|
* Map with keys of properties that should be reflected when updated.
|
|
*/
|
|
this._reflectingProperties = undefined;
|
|
this.initialize();
|
|
}
|
|
/**
|
|
* Returns a list of attributes corresponding to the registered properties.
|
|
* @nocollapse
|
|
*/
|
|
static get observedAttributes() {
|
|
// note: piggy backing on this to ensure we're finalized.
|
|
this.finalize();
|
|
const attributes = [];
|
|
// Use forEach so this works even if for/of loops are compiled to for loops
|
|
// expecting arrays
|
|
this._classProperties.forEach((v, p) => {
|
|
const attr = this._attributeNameForProperty(p, v);
|
|
if (attr !== undefined) {
|
|
this._attributeToPropertyMap.set(attr, p);
|
|
attributes.push(attr);
|
|
}
|
|
});
|
|
return attributes;
|
|
}
|
|
/**
|
|
* Ensures the private `_classProperties` property metadata is created.
|
|
* In addition to `finalize` this is also called in `createProperty` to
|
|
* ensure the `@property` decorator can add property metadata.
|
|
*/
|
|
/** @nocollapse */
|
|
static _ensureClassProperties() {
|
|
// ensure private storage for property declarations.
|
|
if (!this.hasOwnProperty(JSCompiler_renameProperty('_classProperties', this))) {
|
|
this._classProperties = new Map();
|
|
// NOTE: Workaround IE11 not supporting Map constructor argument.
|
|
const superProperties = Object.getPrototypeOf(this)._classProperties;
|
|
if (superProperties !== undefined) {
|
|
superProperties.forEach((v, k) => this._classProperties.set(k, v));
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Creates a property accessor on the element prototype if one does not exist.
|
|
* The property setter calls the property's `hasChanged` property option
|
|
* or uses a strict identity check to determine whether or not to request
|
|
* an update.
|
|
* @nocollapse
|
|
*/
|
|
static createProperty(name, options = defaultPropertyDeclaration) {
|
|
// Note, since this can be called by the `@property` decorator which
|
|
// is called before `finalize`, we ensure storage exists for property
|
|
// metadata.
|
|
this._ensureClassProperties();
|
|
this._classProperties.set(name, options);
|
|
// Do not generate an accessor if the prototype already has one, since
|
|
// it would be lost otherwise and that would never be the user's intention;
|
|
// Instead, we expect users to call `requestUpdate` themselves from
|
|
// user-defined accessors. Note that if the super has an accessor we will
|
|
// still overwrite it
|
|
if (options.noAccessor || this.prototype.hasOwnProperty(name)) {
|
|
return;
|
|
}
|
|
const key = typeof name === 'symbol' ? Symbol() : `__${name}`;
|
|
Object.defineProperty(this.prototype, name, {
|
|
// tslint:disable-next-line:no-any no symbol in index
|
|
get() {
|
|
return this[key];
|
|
},
|
|
set(value) {
|
|
// tslint:disable-next-line:no-any no symbol in index
|
|
const oldValue = this[name];
|
|
// tslint:disable-next-line:no-any no symbol in index
|
|
this[key] = value;
|
|
this._requestUpdate(name, oldValue);
|
|
},
|
|
configurable: true,
|
|
enumerable: true
|
|
});
|
|
}
|
|
/**
|
|
* Creates property accessors for registered properties and ensures
|
|
* any superclasses are also finalized.
|
|
* @nocollapse
|
|
*/
|
|
static finalize() {
|
|
if (this.hasOwnProperty(JSCompiler_renameProperty('finalized', this)) && this.finalized) {
|
|
return;
|
|
}
|
|
// finalize any superclasses
|
|
const superCtor = Object.getPrototypeOf(this);
|
|
if (typeof superCtor.finalize === 'function') {
|
|
superCtor.finalize();
|
|
}
|
|
this.finalized = true;
|
|
this._ensureClassProperties();
|
|
// initialize Map populated in observedAttributes
|
|
this._attributeToPropertyMap = new Map();
|
|
// make any properties
|
|
// Note, only process "own" properties since this element will inherit
|
|
// any properties defined on the superClass, and finalization ensures
|
|
// the entire prototype chain is finalized.
|
|
if (this.hasOwnProperty(JSCompiler_renameProperty('properties', this))) {
|
|
const props = this.properties;
|
|
// support symbols in properties (IE11 does not support this)
|
|
const propKeys = [...Object.getOwnPropertyNames(props), ...(typeof Object.getOwnPropertySymbols === 'function' ? Object.getOwnPropertySymbols(props) : [])];
|
|
// This for/of is ok because propKeys is an array
|
|
for (const p of propKeys) {
|
|
// note, use of `any` is due to TypeSript lack of support for symbol in
|
|
// index types
|
|
// tslint:disable-next-line:no-any no symbol in index
|
|
this.createProperty(p, props[p]);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Returns the property name for the given attribute `name`.
|
|
* @nocollapse
|
|
*/
|
|
static _attributeNameForProperty(name, options) {
|
|
const attribute = options.attribute;
|
|
return attribute === false ? undefined : typeof attribute === 'string' ? attribute : typeof name === 'string' ? name.toLowerCase() : undefined;
|
|
}
|
|
/**
|
|
* Returns true if a property should request an update.
|
|
* Called when a property value is set and uses the `hasChanged`
|
|
* option for the property if present or a strict identity check.
|
|
* @nocollapse
|
|
*/
|
|
static _valueHasChanged(value, old, hasChanged = notEqual) {
|
|
return hasChanged(value, old);
|
|
}
|
|
/**
|
|
* Returns the property value for the given attribute value.
|
|
* Called via the `attributeChangedCallback` and uses the property's
|
|
* `converter` or `converter.fromAttribute` property option.
|
|
* @nocollapse
|
|
*/
|
|
static _propertyValueFromAttribute(value, options) {
|
|
const type = options.type;
|
|
const converter = options.converter || defaultConverter;
|
|
const fromAttribute = typeof converter === 'function' ? converter : converter.fromAttribute;
|
|
return fromAttribute ? fromAttribute(value, type) : value;
|
|
}
|
|
/**
|
|
* Returns the attribute value for the given property value. If this
|
|
* returns undefined, the property will *not* be reflected to an attribute.
|
|
* If this returns null, the attribute will be removed, otherwise the
|
|
* attribute will be set to the value.
|
|
* This uses the property's `reflect` and `type.toAttribute` property options.
|
|
* @nocollapse
|
|
*/
|
|
static _propertyValueToAttribute(value, options) {
|
|
if (options.reflect === undefined) {
|
|
return;
|
|
}
|
|
const type = options.type;
|
|
const converter = options.converter;
|
|
const toAttribute = converter && converter.toAttribute || defaultConverter.toAttribute;
|
|
return toAttribute(value, type);
|
|
}
|
|
/**
|
|
* Performs element initialization. By default captures any pre-set values for
|
|
* registered properties.
|
|
*/
|
|
initialize() {
|
|
this._saveInstanceProperties();
|
|
// ensures first update will be caught by an early access of `updateComplete`
|
|
this._requestUpdate();
|
|
}
|
|
/**
|
|
* Fixes any properties set on the instance before upgrade time.
|
|
* Otherwise these would shadow the accessor and break these properties.
|
|
* The properties are stored in a Map which is played back after the
|
|
* constructor runs. Note, on very old versions of Safari (<=9) or Chrome
|
|
* (<=41), properties created for native platform properties like (`id` or
|
|
* `name`) may not have default values set in the element constructor. On
|
|
* these browsers native properties appear on instances and therefore their
|
|
* default value will overwrite any element default (e.g. if the element sets
|
|
* this.id = 'id' in the constructor, the 'id' will become '' since this is
|
|
* the native platform default).
|
|
*/
|
|
_saveInstanceProperties() {
|
|
// Use forEach so this works even if for/of loops are compiled to for loops
|
|
// expecting arrays
|
|
this.constructor._classProperties.forEach((_v, p) => {
|
|
if (this.hasOwnProperty(p)) {
|
|
const value = this[p];
|
|
delete this[p];
|
|
if (!this._instanceProperties) {
|
|
this._instanceProperties = new Map();
|
|
}
|
|
this._instanceProperties.set(p, value);
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Applies previously saved instance properties.
|
|
*/
|
|
_applyInstanceProperties() {
|
|
// Use forEach so this works even if for/of loops are compiled to for loops
|
|
// expecting arrays
|
|
// tslint:disable-next-line:no-any
|
|
this._instanceProperties.forEach((v, p) => this[p] = v);
|
|
this._instanceProperties = undefined;
|
|
}
|
|
connectedCallback() {
|
|
this._updateState = this._updateState | STATE_HAS_CONNECTED;
|
|
// Ensure first connection completes an update. Updates cannot complete before
|
|
// connection and if one is pending connection the `_hasConnectionResolver`
|
|
// will exist. If so, resolve it to complete the update, otherwise
|
|
// requestUpdate.
|
|
if (this._hasConnectedResolver) {
|
|
this._hasConnectedResolver();
|
|
this._hasConnectedResolver = undefined;
|
|
}
|
|
}
|
|
/**
|
|
* Allows for `super.disconnectedCallback()` in extensions while
|
|
* reserving the possibility of making non-breaking feature additions
|
|
* when disconnecting at some point in the future.
|
|
*/
|
|
disconnectedCallback() {}
|
|
/**
|
|
* Synchronizes property values when attributes change.
|
|
*/
|
|
attributeChangedCallback(name, old, value) {
|
|
if (old !== value) {
|
|
this._attributeToProperty(name, value);
|
|
}
|
|
}
|
|
_propertyToAttribute(name, value, options = defaultPropertyDeclaration) {
|
|
const ctor = this.constructor;
|
|
const attr = ctor._attributeNameForProperty(name, options);
|
|
if (attr !== undefined) {
|
|
const attrValue = ctor._propertyValueToAttribute(value, options);
|
|
// an undefined value does not change the attribute.
|
|
if (attrValue === undefined) {
|
|
return;
|
|
}
|
|
// Track if the property is being reflected to avoid
|
|
// setting the property again via `attributeChangedCallback`. Note:
|
|
// 1. this takes advantage of the fact that the callback is synchronous.
|
|
// 2. will behave incorrectly if multiple attributes are in the reaction
|
|
// stack at time of calling. However, since we process attributes
|
|
// in `update` this should not be possible (or an extreme corner case
|
|
// that we'd like to discover).
|
|
// mark state reflecting
|
|
this._updateState = this._updateState | STATE_IS_REFLECTING_TO_ATTRIBUTE;
|
|
if (attrValue == null) {
|
|
this.removeAttribute(attr);
|
|
} else {
|
|
this.setAttribute(attr, attrValue);
|
|
}
|
|
// mark state not reflecting
|
|
this._updateState = this._updateState & ~STATE_IS_REFLECTING_TO_ATTRIBUTE;
|
|
}
|
|
}
|
|
_attributeToProperty(name, value) {
|
|
// Use tracking info to avoid deserializing attribute value if it was
|
|
// just set from a property setter.
|
|
if (this._updateState & STATE_IS_REFLECTING_TO_ATTRIBUTE) {
|
|
return;
|
|
}
|
|
const ctor = this.constructor;
|
|
const propName = ctor._attributeToPropertyMap.get(name);
|
|
if (propName !== undefined) {
|
|
const options = ctor._classProperties.get(propName) || defaultPropertyDeclaration;
|
|
// mark state reflecting
|
|
this._updateState = this._updateState | STATE_IS_REFLECTING_TO_PROPERTY;
|
|
this[propName] =
|
|
// tslint:disable-next-line:no-any
|
|
ctor._propertyValueFromAttribute(value, options);
|
|
// mark state not reflecting
|
|
this._updateState = this._updateState & ~STATE_IS_REFLECTING_TO_PROPERTY;
|
|
}
|
|
}
|
|
/**
|
|
* This private version of `requestUpdate` does not access or return the
|
|
* `updateComplete` promise. This promise can be overridden and is therefore
|
|
* not free to access.
|
|
*/
|
|
_requestUpdate(name, oldValue) {
|
|
let shouldRequestUpdate = true;
|
|
// If we have a property key, perform property update steps.
|
|
if (name !== undefined) {
|
|
const ctor = this.constructor;
|
|
const options = ctor._classProperties.get(name) || defaultPropertyDeclaration;
|
|
if (ctor._valueHasChanged(this[name], oldValue, options.hasChanged)) {
|
|
if (!this._changedProperties.has(name)) {
|
|
this._changedProperties.set(name, oldValue);
|
|
}
|
|
// Add to reflecting properties set.
|
|
// Note, it's important that every change has a chance to add the
|
|
// property to `_reflectingProperties`. This ensures setting
|
|
// attribute + property reflects correctly.
|
|
if (options.reflect === true && !(this._updateState & STATE_IS_REFLECTING_TO_PROPERTY)) {
|
|
if (this._reflectingProperties === undefined) {
|
|
this._reflectingProperties = new Map();
|
|
}
|
|
this._reflectingProperties.set(name, options);
|
|
}
|
|
} else {
|
|
// Abort the request if the property should not be considered changed.
|
|
shouldRequestUpdate = false;
|
|
}
|
|
}
|
|
if (!this._hasRequestedUpdate && shouldRequestUpdate) {
|
|
this._enqueueUpdate();
|
|
}
|
|
}
|
|
/**
|
|
* Requests an update which is processed asynchronously. This should
|
|
* be called when an element should update based on some state not triggered
|
|
* by setting a property. In this case, pass no arguments. It should also be
|
|
* called when manually implementing a property setter. In this case, pass the
|
|
* property `name` and `oldValue` to ensure that any configured property
|
|
* options are honored. Returns the `updateComplete` Promise which is resolved
|
|
* when the update completes.
|
|
*
|
|
* @param name {PropertyKey} (optional) name of requesting property
|
|
* @param oldValue {any} (optional) old value of requesting property
|
|
* @returns {Promise} A Promise that is resolved when the update completes.
|
|
*/
|
|
requestUpdate(name, oldValue) {
|
|
this._requestUpdate(name, oldValue);
|
|
return this.updateComplete;
|
|
}
|
|
/**
|
|
* Sets up the element to asynchronously update.
|
|
*/
|
|
async _enqueueUpdate() {
|
|
// Mark state updating...
|
|
this._updateState = this._updateState | STATE_UPDATE_REQUESTED;
|
|
let resolve;
|
|
let reject;
|
|
const previousUpdatePromise = this._updatePromise;
|
|
this._updatePromise = new Promise((res, rej) => {
|
|
resolve = res;
|
|
reject = rej;
|
|
});
|
|
try {
|
|
// Ensure any previous update has resolved before updating.
|
|
// This `await` also ensures that property changes are batched.
|
|
await previousUpdatePromise;
|
|
} catch (e) {}
|
|
// Ignore any previous errors. We only care that the previous cycle is
|
|
// done. Any error should have been handled in the previous update.
|
|
|
|
// Make sure the element has connected before updating.
|
|
if (!this._hasConnected) {
|
|
await new Promise(res => this._hasConnectedResolver = res);
|
|
}
|
|
try {
|
|
const result = this.performUpdate();
|
|
// If `performUpdate` returns a Promise, we await it. This is done to
|
|
// enable coordinating updates with a scheduler. Note, the result is
|
|
// checked to avoid delaying an additional microtask unless we need to.
|
|
if (result != null) {
|
|
await result;
|
|
}
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
resolve(!this._hasRequestedUpdate);
|
|
}
|
|
get _hasConnected() {
|
|
return this._updateState & STATE_HAS_CONNECTED;
|
|
}
|
|
get _hasRequestedUpdate() {
|
|
return this._updateState & STATE_UPDATE_REQUESTED;
|
|
}
|
|
get hasUpdated() {
|
|
return this._updateState & STATE_HAS_UPDATED;
|
|
}
|
|
/**
|
|
* Performs an element update. Note, if an exception is thrown during the
|
|
* update, `firstUpdated` and `updated` will not be called.
|
|
*
|
|
* You can override this method to change the timing of updates. If this
|
|
* method is overridden, `super.performUpdate()` must be called.
|
|
*
|
|
* For instance, to schedule updates to occur just before the next frame:
|
|
*
|
|
* ```
|
|
* protected async performUpdate(): Promise<unknown> {
|
|
* await new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
* super.performUpdate();
|
|
* }
|
|
* ```
|
|
*/
|
|
performUpdate() {
|
|
// Mixin instance properties once, if they exist.
|
|
if (this._instanceProperties) {
|
|
this._applyInstanceProperties();
|
|
}
|
|
let shouldUpdate = false;
|
|
const changedProperties = this._changedProperties;
|
|
try {
|
|
shouldUpdate = this.shouldUpdate(changedProperties);
|
|
if (shouldUpdate) {
|
|
this.update(changedProperties);
|
|
}
|
|
} catch (e) {
|
|
// Prevent `firstUpdated` and `updated` from running when there's an
|
|
// update exception.
|
|
shouldUpdate = false;
|
|
throw e;
|
|
} finally {
|
|
// Ensure element can accept additional updates after an exception.
|
|
this._markUpdated();
|
|
}
|
|
if (shouldUpdate) {
|
|
if (!(this._updateState & STATE_HAS_UPDATED)) {
|
|
this._updateState = this._updateState | STATE_HAS_UPDATED;
|
|
this.firstUpdated(changedProperties);
|
|
}
|
|
this.updated(changedProperties);
|
|
}
|
|
}
|
|
_markUpdated() {
|
|
this._changedProperties = new Map();
|
|
this._updateState = this._updateState & ~STATE_UPDATE_REQUESTED;
|
|
}
|
|
/**
|
|
* Returns a Promise that resolves when the element has completed updating.
|
|
* The Promise value is a boolean that is `true` if the element completed the
|
|
* update without triggering another update. The Promise result is `false` if
|
|
* a property was set inside `updated()`. If the Promise is rejected, an
|
|
* exception was thrown during the update. This getter can be implemented to
|
|
* await additional state. For example, it is sometimes useful to await a
|
|
* rendered element before fulfilling this Promise. To do this, first await
|
|
* `super.updateComplete` then any subsequent state.
|
|
*
|
|
* @returns {Promise} The Promise returns a boolean that indicates if the
|
|
* update resolved without triggering another update.
|
|
*/
|
|
get updateComplete() {
|
|
return this._updatePromise;
|
|
}
|
|
/**
|
|
* Controls whether or not `update` should be called when the element requests
|
|
* an update. By default, this method always returns `true`, but this can be
|
|
* customized to control when to update.
|
|
*
|
|
* * @param _changedProperties Map of changed properties with old values
|
|
*/
|
|
shouldUpdate(_changedProperties) {
|
|
return true;
|
|
}
|
|
/**
|
|
* Updates the element. This method reflects property values to attributes.
|
|
* It can be overridden to render and keep updated element DOM.
|
|
* Setting properties inside this method will *not* trigger
|
|
* another update.
|
|
*
|
|
* * @param _changedProperties Map of changed properties with old values
|
|
*/
|
|
update(_changedProperties) {
|
|
if (this._reflectingProperties !== undefined && this._reflectingProperties.size > 0) {
|
|
// Use forEach so this works even if for/of loops are compiled to for
|
|
// loops expecting arrays
|
|
this._reflectingProperties.forEach((v, k) => this._propertyToAttribute(k, this[k], v));
|
|
this._reflectingProperties = undefined;
|
|
}
|
|
}
|
|
/**
|
|
* Invoked whenever the element is updated. Implement to perform
|
|
* post-updating tasks via DOM APIs, for example, focusing an element.
|
|
*
|
|
* Setting properties inside this method will trigger the element to update
|
|
* again after this update cycle completes.
|
|
*
|
|
* * @param _changedProperties Map of changed properties with old values
|
|
*/
|
|
updated(_changedProperties) {}
|
|
/**
|
|
* Invoked when the element is first updated. Implement to perform one time
|
|
* work on the element after update.
|
|
*
|
|
* Setting properties inside this method will trigger the element to update
|
|
* again after this update cycle completes.
|
|
*
|
|
* * @param _changedProperties Map of changed properties with old values
|
|
*/
|
|
firstUpdated(_changedProperties) {}
|
|
}
|
|
/**
|
|
* Marks class as having finished creating properties.
|
|
*/
|
|
UpdatingElement.finalized = true;
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
const legacyCustomElement = (tagName, clazz) => {
|
|
window.customElements.define(tagName, clazz);
|
|
// Cast as any because TS doesn't recognize the return type as being a
|
|
// subtype of the decorated class when clazz is typed as
|
|
// `Constructor<HTMLElement>` for some reason.
|
|
// `Constructor<HTMLElement>` is helpful to make sure the decorator is
|
|
// applied to elements however.
|
|
// tslint:disable-next-line:no-any
|
|
return clazz;
|
|
};
|
|
const standardCustomElement = (tagName, descriptor) => {
|
|
const { kind, elements } = descriptor;
|
|
return {
|
|
kind,
|
|
elements,
|
|
// This callback is called once the class is otherwise fully defined
|
|
finisher(clazz) {
|
|
window.customElements.define(tagName, clazz);
|
|
}
|
|
};
|
|
};
|
|
/**
|
|
* Class decorator factory that defines the decorated class as a custom element.
|
|
*
|
|
* @param tagName the name of the custom element to define
|
|
*/
|
|
const customElement = tagName => classOrDescriptor => typeof classOrDescriptor === 'function' ? legacyCustomElement(tagName, classOrDescriptor) : standardCustomElement(tagName, classOrDescriptor);
|
|
const standardProperty = (options, element) => {
|
|
// When decorating an accessor, pass it through and add property metadata.
|
|
// Note, the `hasOwnProperty` check in `createProperty` ensures we don't
|
|
// stomp over the user's accessor.
|
|
if (element.kind === 'method' && element.descriptor && !('value' in element.descriptor)) {
|
|
return Object.assign({}, element, { finisher(clazz) {
|
|
clazz.createProperty(element.key, options);
|
|
} });
|
|
} else {
|
|
// createProperty() takes care of defining the property, but we still
|
|
// must return some kind of descriptor, so return a descriptor for an
|
|
// unused prototype field. The finisher calls createProperty().
|
|
return {
|
|
kind: 'field',
|
|
key: Symbol(),
|
|
placement: 'own',
|
|
descriptor: {},
|
|
// When @babel/plugin-proposal-decorators implements initializers,
|
|
// do this instead of the initializer below. See:
|
|
// https://github.com/babel/babel/issues/9260 extras: [
|
|
// {
|
|
// kind: 'initializer',
|
|
// placement: 'own',
|
|
// initializer: descriptor.initializer,
|
|
// }
|
|
// ],
|
|
// tslint:disable-next-line:no-any decorator
|
|
initializer() {
|
|
if (typeof element.initializer === 'function') {
|
|
this[element.key] = element.initializer.call(this);
|
|
}
|
|
},
|
|
finisher(clazz) {
|
|
clazz.createProperty(element.key, options);
|
|
}
|
|
};
|
|
}
|
|
};
|
|
const legacyProperty = (options, proto, name) => {
|
|
proto.constructor.createProperty(name, options);
|
|
};
|
|
/**
|
|
* A property decorator which creates a LitElement property which reflects a
|
|
* corresponding attribute value. A `PropertyDeclaration` may optionally be
|
|
* supplied to configure property features.
|
|
*
|
|
* @ExportDecoratedItems
|
|
*/
|
|
function property(options) {
|
|
// tslint:disable-next-line:no-any decorator
|
|
return (protoOrDescriptor, name) => name !== undefined ? legacyProperty(options, protoOrDescriptor, name) : standardProperty(options, protoOrDescriptor);
|
|
}
|
|
|
|
/**
|
|
@license
|
|
Copyright (c) 2019 The Polymer Project Authors. All rights reserved.
|
|
This code may only be used under the BSD style license found at
|
|
http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
|
|
http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
|
|
found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
|
|
part of the polymer project is also subject to an additional IP rights grant
|
|
found at http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
const supportsAdoptingStyleSheets = 'adoptedStyleSheets' in Document.prototype && 'replace' in CSSStyleSheet.prototype;
|
|
const constructionToken = Symbol();
|
|
class CSSResult {
|
|
constructor(cssText, safeToken) {
|
|
if (safeToken !== constructionToken) {
|
|
throw new Error('CSSResult is not constructable. Use `unsafeCSS` or `css` instead.');
|
|
}
|
|
this.cssText = cssText;
|
|
}
|
|
// Note, this is a getter so that it's lazy. In practice, this means
|
|
// stylesheets are not created until the first element instance is made.
|
|
get styleSheet() {
|
|
if (this._styleSheet === undefined) {
|
|
// Note, if `adoptedStyleSheets` is supported then we assume CSSStyleSheet
|
|
// is constructable.
|
|
if (supportsAdoptingStyleSheets) {
|
|
this._styleSheet = new CSSStyleSheet();
|
|
this._styleSheet.replaceSync(this.cssText);
|
|
} else {
|
|
this._styleSheet = null;
|
|
}
|
|
}
|
|
return this._styleSheet;
|
|
}
|
|
toString() {
|
|
return this.cssText;
|
|
}
|
|
}
|
|
const textFromCSSResult = value => {
|
|
if (value instanceof CSSResult) {
|
|
return value.cssText;
|
|
} else {
|
|
throw new Error(`Value passed to 'css' function must be a 'css' function result: ${value}. Use 'unsafeCSS' to pass non-literal values, but
|
|
take care to ensure page security.`);
|
|
}
|
|
};
|
|
/**
|
|
* Template tag which which can be used with LitElement's `style` property to
|
|
* set element styles. For security reasons, only literal string values may be
|
|
* used. To incorporate non-literal values `unsafeCSS` may be used inside a
|
|
* template string part.
|
|
*/
|
|
const css = (strings, ...values) => {
|
|
const cssText = values.reduce((acc, v, idx) => acc + textFromCSSResult(v) + strings[idx + 1], strings[0]);
|
|
return new CSSResult(cssText, constructionToken);
|
|
};
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
// IMPORTANT: do not change the property name or the assignment expression.
|
|
// This line will be used in regexes to search for LitElement usage.
|
|
// TODO(justinfagnani): inject version number at build time
|
|
(window['litElementVersions'] || (window['litElementVersions'] = [])).push('2.0.1');
|
|
/**
|
|
* Minimal implementation of Array.prototype.flat
|
|
* @param arr the array to flatten
|
|
* @param result the accumlated result
|
|
*/
|
|
function arrayFlat(styles, result = []) {
|
|
for (let i = 0, length = styles.length; i < length; i++) {
|
|
const value = styles[i];
|
|
if (Array.isArray(value)) {
|
|
arrayFlat(value, result);
|
|
} else {
|
|
result.push(value);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
/** Deeply flattens styles array. Uses native flat if available. */
|
|
const flattenStyles = styles => styles.flat ? styles.flat(Infinity) : arrayFlat(styles);
|
|
class LitElement extends UpdatingElement {
|
|
/** @nocollapse */
|
|
static finalize() {
|
|
super.finalize();
|
|
// Prepare styling that is stamped at first render time. Styling
|
|
// is built from user provided `styles` or is inherited from the superclass.
|
|
this._styles = this.hasOwnProperty(JSCompiler_renameProperty('styles', this)) ? this._getUniqueStyles() : this._styles || [];
|
|
}
|
|
/** @nocollapse */
|
|
static _getUniqueStyles() {
|
|
// Take care not to call `this.styles` multiple times since this generates
|
|
// new CSSResults each time.
|
|
// TODO(sorvell): Since we do not cache CSSResults by input, any
|
|
// shared styles will generate new stylesheet objects, which is wasteful.
|
|
// This should be addressed when a browser ships constructable
|
|
// stylesheets.
|
|
const userStyles = this.styles;
|
|
const styles = [];
|
|
if (Array.isArray(userStyles)) {
|
|
const flatStyles = flattenStyles(userStyles);
|
|
// As a performance optimization to avoid duplicated styling that can
|
|
// occur especially when composing via subclassing, de-duplicate styles
|
|
// preserving the last item in the list. The last item is kept to
|
|
// try to preserve cascade order with the assumption that it's most
|
|
// important that last added styles override previous styles.
|
|
const styleSet = flatStyles.reduceRight((set, s) => {
|
|
set.add(s);
|
|
// on IE set.add does not return the set.
|
|
return set;
|
|
}, new Set());
|
|
// Array.from does not work on Set in IE
|
|
styleSet.forEach(v => styles.unshift(v));
|
|
} else if (userStyles) {
|
|
styles.push(userStyles);
|
|
}
|
|
return styles;
|
|
}
|
|
/**
|
|
* Performs element initialization. By default this calls `createRenderRoot`
|
|
* to create the element `renderRoot` node and captures any pre-set values for
|
|
* registered properties.
|
|
*/
|
|
initialize() {
|
|
super.initialize();
|
|
this.renderRoot = this.createRenderRoot();
|
|
// Note, if renderRoot is not a shadowRoot, styles would/could apply to the
|
|
// element's getRootNode(). While this could be done, we're choosing not to
|
|
// support this now since it would require different logic around de-duping.
|
|
if (window.ShadowRoot && this.renderRoot instanceof window.ShadowRoot) {
|
|
this.adoptStyles();
|
|
}
|
|
}
|
|
/**
|
|
* Returns the node into which the element should render and by default
|
|
* creates and returns an open shadowRoot. Implement to customize where the
|
|
* element's DOM is rendered. For example, to render into the element's
|
|
* childNodes, return `this`.
|
|
* @returns {Element|DocumentFragment} Returns a node into which to render.
|
|
*/
|
|
createRenderRoot() {
|
|
return this.attachShadow({ mode: 'open' });
|
|
}
|
|
/**
|
|
* Applies styling to the element shadowRoot using the `static get styles`
|
|
* property. Styling will apply using `shadowRoot.adoptedStyleSheets` where
|
|
* available and will fallback otherwise. When Shadow DOM is polyfilled,
|
|
* ShadyCSS scopes styles and adds them to the document. When Shadow DOM
|
|
* is available but `adoptedStyleSheets` is not, styles are appended to the
|
|
* end of the `shadowRoot` to [mimic spec
|
|
* behavior](https://wicg.github.io/construct-stylesheets/#using-constructed-stylesheets).
|
|
*/
|
|
adoptStyles() {
|
|
const styles = this.constructor._styles;
|
|
if (styles.length === 0) {
|
|
return;
|
|
}
|
|
// There are three separate cases here based on Shadow DOM support.
|
|
// (1) shadowRoot polyfilled: use ShadyCSS
|
|
// (2) shadowRoot.adoptedStyleSheets available: use it.
|
|
// (3) shadowRoot.adoptedStyleSheets polyfilled: append styles after
|
|
// rendering
|
|
if (window.ShadyCSS !== undefined && !window.ShadyCSS.nativeShadow) {
|
|
window.ShadyCSS.ScopingShim.prepareAdoptedCssText(styles.map(s => s.cssText), this.localName);
|
|
} else if (supportsAdoptingStyleSheets) {
|
|
this.renderRoot.adoptedStyleSheets = styles.map(s => s.styleSheet);
|
|
} else {
|
|
// This must be done after rendering so the actual style insertion is done
|
|
// in `update`.
|
|
this._needsShimAdoptedStyleSheets = true;
|
|
}
|
|
}
|
|
connectedCallback() {
|
|
super.connectedCallback();
|
|
// Note, first update/render handles styleElement so we only call this if
|
|
// connected after first update.
|
|
if (this.hasUpdated && window.ShadyCSS !== undefined) {
|
|
window.ShadyCSS.styleElement(this);
|
|
}
|
|
}
|
|
/**
|
|
* Updates the element. This method reflects property values to attributes
|
|
* and calls `render` to render DOM via lit-html. Setting properties inside
|
|
* this method will *not* trigger another update.
|
|
* * @param _changedProperties Map of changed properties with old values
|
|
*/
|
|
update(changedProperties) {
|
|
super.update(changedProperties);
|
|
const templateResult = this.render();
|
|
if (templateResult instanceof TemplateResult) {
|
|
this.constructor.render(templateResult, this.renderRoot, { scopeName: this.localName, eventContext: this });
|
|
}
|
|
// When native Shadow DOM is used but adoptedStyles are not supported,
|
|
// insert styling after rendering to ensure adoptedStyles have highest
|
|
// priority.
|
|
if (this._needsShimAdoptedStyleSheets) {
|
|
this._needsShimAdoptedStyleSheets = false;
|
|
this.constructor._styles.forEach(s => {
|
|
const style = document.createElement('style');
|
|
style.textContent = s.cssText;
|
|
this.renderRoot.appendChild(style);
|
|
});
|
|
}
|
|
}
|
|
/**
|
|
* Invoked on each update to perform rendering tasks. This method must return
|
|
* a lit-html TemplateResult. Setting properties inside this method will *not*
|
|
* trigger the element to update.
|
|
*/
|
|
render() {}
|
|
}
|
|
/**
|
|
* Ensure this class is marked as `finalized` as an optimization ensuring
|
|
* it will not needlessly try to `finalize`.
|
|
*/
|
|
LitElement.finalized = true;
|
|
/**
|
|
* Render method used to render the lit-html TemplateResult to the element's
|
|
* DOM.
|
|
* @param {TemplateResult} Template to render.
|
|
* @param {Element|DocumentFragment} Node into which to render.
|
|
* @param {String} Element name.
|
|
* @nocollapse
|
|
*/
|
|
LitElement.render = render$1;
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2018 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
/**
|
|
* Stores the StyleInfo object applied to a given AttributePart.
|
|
* Used to unset existing values when a new StyleInfo object is applied.
|
|
*/
|
|
const styleMapCache = new WeakMap();
|
|
/**
|
|
* Stores AttributeParts that have had static styles applied (e.g. `height: 0;`
|
|
* in style="height: 0; ${styleMap()}"). Static styles are applied only the
|
|
* first time the directive is run on a part.
|
|
*/
|
|
// Note, could be a WeakSet, but prefer not requiring this polyfill.
|
|
const styleMapStatics = new WeakMap();
|
|
/**
|
|
* A directive that applies CSS properties to an element.
|
|
*
|
|
* `styleMap` can only be used in the `style` attribute and must be the only
|
|
* expression in the attribute. It takes the property names in the `styleInfo`
|
|
* object and adds the property values as CSS propertes. Property names with
|
|
* dashes (`-`) are assumed to be valid CSS property names and set on the
|
|
* element's style object using `setProperty()`. Names without dashes are
|
|
* assumed to be camelCased JavaScript property names and set on the element's
|
|
* style object using property assignment, allowing the style object to
|
|
* translate JavaScript-style names to CSS property names.
|
|
*
|
|
* For example `styleMap({backgroundColor: 'red', 'border-top': '5px', '--size':
|
|
* '0'})` sets the `background-color`, `border-top` and `--size` properties.
|
|
*
|
|
* @param styleInfo {StyleInfo}
|
|
*/
|
|
const styleMap = directive(styleInfo => part => {
|
|
if (!(part instanceof AttributePart) || part instanceof PropertyPart || part.committer.name !== 'style' || part.committer.parts.length > 1) {
|
|
throw new Error('The `styleMap` directive must be used in the style attribute ' + 'and must be the only part in the attribute.');
|
|
}
|
|
// Handle static styles the first time we see a Part
|
|
if (!styleMapStatics.has(part)) {
|
|
part.committer.element.style.cssText = part.committer.strings.join(' ');
|
|
styleMapStatics.set(part, true);
|
|
}
|
|
const style = part.committer.element.style;
|
|
// Remove old properties that no longer exist in styleInfo
|
|
const oldInfo = styleMapCache.get(part);
|
|
for (const name in oldInfo) {
|
|
if (!(name in styleInfo)) {
|
|
if (name.indexOf('-') === -1) {
|
|
// tslint:disable-next-line:no-any
|
|
style[name] = null;
|
|
} else {
|
|
style.removeProperty(name);
|
|
}
|
|
}
|
|
}
|
|
// Add or update properties
|
|
for (const name in styleInfo) {
|
|
if (name.indexOf('-') === -1) {
|
|
// tslint:disable-next-line:no-any
|
|
style[name] = styleInfo[name];
|
|
} else {
|
|
style.setProperty(name, styleInfo[name]);
|
|
}
|
|
}
|
|
styleMapCache.set(part, styleInfo);
|
|
});
|
|
|
|
/** Constants to be used in the frontend. */
|
|
// Constants should be alphabetically sorted by name.
|
|
// Arrays with values should be alphabetically sorted if order doesn't matter.
|
|
// Each constant should have a description what it is supposed to be used for.
|
|
/** Icon to use when no icon specified for domain. */
|
|
const DEFAULT_DOMAIN_ICON = "hass:bookmark";
|
|
/** States that we consider "off". */
|
|
const STATES_OFF = ["closed", "locked", "off"];
|
|
|
|
/**
|
|
* Return the icon to be used for a domain.
|
|
*
|
|
* Optionally pass in a state to influence the domain icon.
|
|
*/
|
|
const fixedIcons = {
|
|
alert: "hass:alert",
|
|
automation: "hass:playlist-play",
|
|
calendar: "hass:calendar",
|
|
camera: "hass:video",
|
|
climate: "hass:thermostat",
|
|
configurator: "hass:settings",
|
|
conversation: "hass:text-to-speech",
|
|
device_tracker: "hass:account",
|
|
fan: "hass:fan",
|
|
group: "hass:google-circles-communities",
|
|
history_graph: "hass:chart-line",
|
|
homeassistant: "hass:home-assistant",
|
|
homekit: "hass:home-automation",
|
|
image_processing: "hass:image-filter-frames",
|
|
input_boolean: "hass:drawing",
|
|
input_datetime: "hass:calendar-clock",
|
|
input_number: "hass:ray-vertex",
|
|
input_select: "hass:format-list-bulleted",
|
|
input_text: "hass:textbox",
|
|
light: "hass:lightbulb",
|
|
mailbox: "hass:mailbox",
|
|
notify: "hass:comment-alert",
|
|
person: "hass:account",
|
|
plant: "hass:flower",
|
|
proximity: "hass:apple-safari",
|
|
remote: "hass:remote",
|
|
scene: "hass:google-pages",
|
|
script: "hass:file-document",
|
|
sensor: "hass:eye",
|
|
simple_alarm: "hass:bell",
|
|
sun: "hass:white-balance-sunny",
|
|
switch: "hass:flash",
|
|
timer: "hass:timer",
|
|
updater: "hass:cloud-upload",
|
|
vacuum: "hass:robot-vacuum",
|
|
water_heater: "hass:thermometer",
|
|
weblink: "hass:open-in-new"
|
|
};
|
|
function domainIcon(domain, state) {
|
|
if (domain in fixedIcons) {
|
|
return fixedIcons[domain];
|
|
}
|
|
switch (domain) {
|
|
case "alarm_control_panel":
|
|
switch (state) {
|
|
case "armed_home":
|
|
return "hass:bell-plus";
|
|
case "armed_night":
|
|
return "hass:bell-sleep";
|
|
case "disarmed":
|
|
return "hass:bell-outline";
|
|
case "triggered":
|
|
return "hass:bell-ring";
|
|
default:
|
|
return "hass:bell";
|
|
}
|
|
case "binary_sensor":
|
|
return state && state === "off" ? "hass:radiobox-blank" : "hass:checkbox-marked-circle";
|
|
case "cover":
|
|
return state === "closed" ? "hass:window-closed" : "hass:window-open";
|
|
case "lock":
|
|
return state && state === "unlocked" ? "hass:lock-open" : "hass:lock";
|
|
case "media_player":
|
|
return state && state !== "off" && state !== "idle" ? "hass:cast-connected" : "hass:cast";
|
|
case "zwave":
|
|
switch (state) {
|
|
case "dead":
|
|
return "hass:emoticon-dead";
|
|
case "sleeping":
|
|
return "hass:sleep";
|
|
case "initializing":
|
|
return "hass:timer-sand";
|
|
default:
|
|
return "hass:z-wave";
|
|
}
|
|
default:
|
|
// tslint:disable-next-line
|
|
console.warn("Unable to find icon for domain " + domain + " (" + state + ")");
|
|
return DEFAULT_DOMAIN_ICON;
|
|
}
|
|
}
|
|
|
|
function bound01(n, max) {
|
|
if (isOnePointZero(n)) {
|
|
n = '100%';
|
|
}
|
|
var processPercent = isPercentage(n);
|
|
n = max === 360 ? n : Math.min(max, Math.max(0, parseFloat(n)));
|
|
if (processPercent) {
|
|
n = parseInt(String(n * max), 10) / 100;
|
|
}
|
|
if (Math.abs(n - max) < 0.000001) {
|
|
return 1;
|
|
}
|
|
if (max === 360) {
|
|
n = (n < 0 ? n % max + max : n % max) / parseFloat(String(max));
|
|
} else {
|
|
n = n % max / parseFloat(String(max));
|
|
}
|
|
return n;
|
|
}
|
|
function clamp01(val) {
|
|
return Math.min(1, Math.max(0, val));
|
|
}
|
|
function isOnePointZero(n) {
|
|
return typeof n === 'string' && n.indexOf('.') !== -1 && parseFloat(n) === 1;
|
|
}
|
|
function isPercentage(n) {
|
|
return typeof n === 'string' && n.indexOf('%') !== -1;
|
|
}
|
|
function boundAlpha(a) {
|
|
a = parseFloat(a);
|
|
if (isNaN(a) || a < 0 || a > 1) {
|
|
a = 1;
|
|
}
|
|
return a;
|
|
}
|
|
function convertToPercentage(n) {
|
|
if (n <= 1) {
|
|
return Number(n) * 100 + "%";
|
|
}
|
|
return n;
|
|
}
|
|
function pad2(c) {
|
|
return c.length === 1 ? '0' + c : String(c);
|
|
}
|
|
|
|
function rgbToRgb(r, g, b) {
|
|
return {
|
|
r: bound01(r, 255) * 255,
|
|
g: bound01(g, 255) * 255,
|
|
b: bound01(b, 255) * 255
|
|
};
|
|
}
|
|
function rgbToHsl(r, g, b) {
|
|
r = bound01(r, 255);
|
|
g = bound01(g, 255);
|
|
b = bound01(b, 255);
|
|
var max = Math.max(r, g, b);
|
|
var min = Math.min(r, g, b);
|
|
var h = 0;
|
|
var s = 0;
|
|
var l = (max + min) / 2;
|
|
if (max === min) {
|
|
s = 0;
|
|
h = 0;
|
|
} else {
|
|
var d = max - min;
|
|
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
switch (max) {
|
|
case r:
|
|
h = (g - b) / d + (g < b ? 6 : 0);
|
|
break;
|
|
case g:
|
|
h = (b - r) / d + 2;
|
|
break;
|
|
case b:
|
|
h = (r - g) / d + 4;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
h /= 6;
|
|
}
|
|
return { h: h, s: s, l: l };
|
|
}
|
|
function hslToRgb(h, s, l) {
|
|
var r;
|
|
var g;
|
|
var b;
|
|
h = bound01(h, 360);
|
|
s = bound01(s, 100);
|
|
l = bound01(l, 100);
|
|
function hue2rgb(p, q, t) {
|
|
if (t < 0) {
|
|
t += 1;
|
|
}
|
|
if (t > 1) {
|
|
t -= 1;
|
|
}
|
|
if (t < 1 / 6) {
|
|
return p + (q - p) * (6 * t);
|
|
}
|
|
if (t < 1 / 2) {
|
|
return q;
|
|
}
|
|
if (t < 2 / 3) {
|
|
return p + (q - p) * (2 / 3 - t) * 6;
|
|
}
|
|
return p;
|
|
}
|
|
if (s === 0) {
|
|
g = l;
|
|
b = l;
|
|
r = l;
|
|
} else {
|
|
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
var p = 2 * l - q;
|
|
r = hue2rgb(p, q, h + 1 / 3);
|
|
g = hue2rgb(p, q, h);
|
|
b = hue2rgb(p, q, h - 1 / 3);
|
|
}
|
|
return { r: r * 255, g: g * 255, b: b * 255 };
|
|
}
|
|
function rgbToHsv(r, g, b) {
|
|
r = bound01(r, 255);
|
|
g = bound01(g, 255);
|
|
b = bound01(b, 255);
|
|
var max = Math.max(r, g, b);
|
|
var min = Math.min(r, g, b);
|
|
var h = 0;
|
|
var v = max;
|
|
var d = max - min;
|
|
var s = max === 0 ? 0 : d / max;
|
|
if (max === min) {
|
|
h = 0;
|
|
} else {
|
|
switch (max) {
|
|
case r:
|
|
h = (g - b) / d + (g < b ? 6 : 0);
|
|
break;
|
|
case g:
|
|
h = (b - r) / d + 2;
|
|
break;
|
|
case b:
|
|
h = (r - g) / d + 4;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
h /= 6;
|
|
}
|
|
return { h: h, s: s, v: v };
|
|
}
|
|
function hsvToRgb(h, s, v) {
|
|
h = bound01(h, 360) * 6;
|
|
s = bound01(s, 100);
|
|
v = bound01(v, 100);
|
|
var i = Math.floor(h);
|
|
var f = h - i;
|
|
var p = v * (1 - s);
|
|
var q = v * (1 - f * s);
|
|
var t = v * (1 - (1 - f) * s);
|
|
var mod = i % 6;
|
|
var r = [v, q, p, p, t, v][mod];
|
|
var g = [t, v, v, q, p, p][mod];
|
|
var b = [p, p, t, v, v, q][mod];
|
|
return { r: r * 255, g: g * 255, b: b * 255 };
|
|
}
|
|
function rgbToHex(r, g, b, allow3Char) {
|
|
var hex = [pad2(Math.round(r).toString(16)), pad2(Math.round(g).toString(16)), pad2(Math.round(b).toString(16))];
|
|
if (allow3Char && hex[0].charAt(0) === hex[0].charAt(1) && hex[1].charAt(0) === hex[1].charAt(1) && hex[2].charAt(0) === hex[2].charAt(1)) {
|
|
return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
|
|
}
|
|
return hex.join('');
|
|
}
|
|
function rgbaToHex(r, g, b, a, allow4Char) {
|
|
var hex = [pad2(Math.round(r).toString(16)), pad2(Math.round(g).toString(16)), pad2(Math.round(b).toString(16)), pad2(convertDecimalToHex(a))];
|
|
if (allow4Char && hex[0].charAt(0) === hex[0].charAt(1) && hex[1].charAt(0) === hex[1].charAt(1) && hex[2].charAt(0) === hex[2].charAt(1) && hex[3].charAt(0) === hex[3].charAt(1)) {
|
|
return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0) + hex[3].charAt(0);
|
|
}
|
|
return hex.join('');
|
|
}
|
|
function convertDecimalToHex(d) {
|
|
return Math.round(parseFloat(d) * 255).toString(16);
|
|
}
|
|
function convertHexToDecimal(h) {
|
|
return parseIntFromHex(h) / 255;
|
|
}
|
|
function parseIntFromHex(val) {
|
|
return parseInt(val, 16);
|
|
}
|
|
|
|
var names = {
|
|
aliceblue: '#f0f8ff',
|
|
antiquewhite: '#faebd7',
|
|
aqua: '#00ffff',
|
|
aquamarine: '#7fffd4',
|
|
azure: '#f0ffff',
|
|
beige: '#f5f5dc',
|
|
bisque: '#ffe4c4',
|
|
black: '#000000',
|
|
blanchedalmond: '#ffebcd',
|
|
blue: '#0000ff',
|
|
blueviolet: '#8a2be2',
|
|
brown: '#a52a2a',
|
|
burlywood: '#deb887',
|
|
cadetblue: '#5f9ea0',
|
|
chartreuse: '#7fff00',
|
|
chocolate: '#d2691e',
|
|
coral: '#ff7f50',
|
|
cornflowerblue: '#6495ed',
|
|
cornsilk: '#fff8dc',
|
|
crimson: '#dc143c',
|
|
cyan: '#00ffff',
|
|
darkblue: '#00008b',
|
|
darkcyan: '#008b8b',
|
|
darkgoldenrod: '#b8860b',
|
|
darkgray: '#a9a9a9',
|
|
darkgreen: '#006400',
|
|
darkgrey: '#a9a9a9',
|
|
darkkhaki: '#bdb76b',
|
|
darkmagenta: '#8b008b',
|
|
darkolivegreen: '#556b2f',
|
|
darkorange: '#ff8c00',
|
|
darkorchid: '#9932cc',
|
|
darkred: '#8b0000',
|
|
darksalmon: '#e9967a',
|
|
darkseagreen: '#8fbc8f',
|
|
darkslateblue: '#483d8b',
|
|
darkslategray: '#2f4f4f',
|
|
darkslategrey: '#2f4f4f',
|
|
darkturquoise: '#00ced1',
|
|
darkviolet: '#9400d3',
|
|
deeppink: '#ff1493',
|
|
deepskyblue: '#00bfff',
|
|
dimgray: '#696969',
|
|
dimgrey: '#696969',
|
|
dodgerblue: '#1e90ff',
|
|
firebrick: '#b22222',
|
|
floralwhite: '#fffaf0',
|
|
forestgreen: '#228b22',
|
|
fuchsia: '#ff00ff',
|
|
gainsboro: '#dcdcdc',
|
|
ghostwhite: '#f8f8ff',
|
|
gold: '#ffd700',
|
|
goldenrod: '#daa520',
|
|
gray: '#808080',
|
|
green: '#008000',
|
|
greenyellow: '#adff2f',
|
|
grey: '#808080',
|
|
honeydew: '#f0fff0',
|
|
hotpink: '#ff69b4',
|
|
indianred: '#cd5c5c',
|
|
indigo: '#4b0082',
|
|
ivory: '#fffff0',
|
|
khaki: '#f0e68c',
|
|
lavender: '#e6e6fa',
|
|
lavenderblush: '#fff0f5',
|
|
lawngreen: '#7cfc00',
|
|
lemonchiffon: '#fffacd',
|
|
lightblue: '#add8e6',
|
|
lightcoral: '#f08080',
|
|
lightcyan: '#e0ffff',
|
|
lightgoldenrodyellow: '#fafad2',
|
|
lightgray: '#d3d3d3',
|
|
lightgreen: '#90ee90',
|
|
lightgrey: '#d3d3d3',
|
|
lightpink: '#ffb6c1',
|
|
lightsalmon: '#ffa07a',
|
|
lightseagreen: '#20b2aa',
|
|
lightskyblue: '#87cefa',
|
|
lightslategray: '#778899',
|
|
lightslategrey: '#778899',
|
|
lightsteelblue: '#b0c4de',
|
|
lightyellow: '#ffffe0',
|
|
lime: '#00ff00',
|
|
limegreen: '#32cd32',
|
|
linen: '#faf0e6',
|
|
magenta: '#ff00ff',
|
|
maroon: '#800000',
|
|
mediumaquamarine: '#66cdaa',
|
|
mediumblue: '#0000cd',
|
|
mediumorchid: '#ba55d3',
|
|
mediumpurple: '#9370db',
|
|
mediumseagreen: '#3cb371',
|
|
mediumslateblue: '#7b68ee',
|
|
mediumspringgreen: '#00fa9a',
|
|
mediumturquoise: '#48d1cc',
|
|
mediumvioletred: '#c71585',
|
|
midnightblue: '#191970',
|
|
mintcream: '#f5fffa',
|
|
mistyrose: '#ffe4e1',
|
|
moccasin: '#ffe4b5',
|
|
navajowhite: '#ffdead',
|
|
navy: '#000080',
|
|
oldlace: '#fdf5e6',
|
|
olive: '#808000',
|
|
olivedrab: '#6b8e23',
|
|
orange: '#ffa500',
|
|
orangered: '#ff4500',
|
|
orchid: '#da70d6',
|
|
palegoldenrod: '#eee8aa',
|
|
palegreen: '#98fb98',
|
|
paleturquoise: '#afeeee',
|
|
palevioletred: '#db7093',
|
|
papayawhip: '#ffefd5',
|
|
peachpuff: '#ffdab9',
|
|
peru: '#cd853f',
|
|
pink: '#ffc0cb',
|
|
plum: '#dda0dd',
|
|
powderblue: '#b0e0e6',
|
|
purple: '#800080',
|
|
rebeccapurple: '#663399',
|
|
red: '#ff0000',
|
|
rosybrown: '#bc8f8f',
|
|
royalblue: '#4169e1',
|
|
saddlebrown: '#8b4513',
|
|
salmon: '#fa8072',
|
|
sandybrown: '#f4a460',
|
|
seagreen: '#2e8b57',
|
|
seashell: '#fff5ee',
|
|
sienna: '#a0522d',
|
|
silver: '#c0c0c0',
|
|
skyblue: '#87ceeb',
|
|
slateblue: '#6a5acd',
|
|
slategray: '#708090',
|
|
slategrey: '#708090',
|
|
snow: '#fffafa',
|
|
springgreen: '#00ff7f',
|
|
steelblue: '#4682b4',
|
|
tan: '#d2b48c',
|
|
teal: '#008080',
|
|
thistle: '#d8bfd8',
|
|
tomato: '#ff6347',
|
|
turquoise: '#40e0d0',
|
|
violet: '#ee82ee',
|
|
wheat: '#f5deb3',
|
|
white: '#ffffff',
|
|
whitesmoke: '#f5f5f5',
|
|
yellow: '#ffff00',
|
|
yellowgreen: '#9acd32'
|
|
};
|
|
|
|
function inputToRGB(color) {
|
|
var rgb = { r: 0, g: 0, b: 0 };
|
|
var a = 1;
|
|
var s = null;
|
|
var v = null;
|
|
var l = null;
|
|
var ok = false;
|
|
var format = false;
|
|
if (typeof color === 'string') {
|
|
color = stringInputToObject(color);
|
|
}
|
|
if (typeof color === 'object') {
|
|
if (isValidCSSUnit(color.r) && isValidCSSUnit(color.g) && isValidCSSUnit(color.b)) {
|
|
rgb = rgbToRgb(color.r, color.g, color.b);
|
|
ok = true;
|
|
format = String(color.r).substr(-1) === '%' ? 'prgb' : 'rgb';
|
|
} else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.v)) {
|
|
s = convertToPercentage(color.s);
|
|
v = convertToPercentage(color.v);
|
|
rgb = hsvToRgb(color.h, s, v);
|
|
ok = true;
|
|
format = 'hsv';
|
|
} else if (isValidCSSUnit(color.h) && isValidCSSUnit(color.s) && isValidCSSUnit(color.l)) {
|
|
s = convertToPercentage(color.s);
|
|
l = convertToPercentage(color.l);
|
|
rgb = hslToRgb(color.h, s, l);
|
|
ok = true;
|
|
format = 'hsl';
|
|
}
|
|
if (Object.prototype.hasOwnProperty.call(color, 'a')) {
|
|
a = color.a;
|
|
}
|
|
}
|
|
a = boundAlpha(a);
|
|
return {
|
|
ok: ok,
|
|
format: color.format || format,
|
|
r: Math.min(255, Math.max(rgb.r, 0)),
|
|
g: Math.min(255, Math.max(rgb.g, 0)),
|
|
b: Math.min(255, Math.max(rgb.b, 0)),
|
|
a: a
|
|
};
|
|
}
|
|
var CSS_INTEGER = '[-\\+]?\\d+%?';
|
|
var CSS_NUMBER = '[-\\+]?\\d*\\.\\d+%?';
|
|
var CSS_UNIT = "(?:" + CSS_NUMBER + ")|(?:" + CSS_INTEGER + ")";
|
|
var PERMISSIVE_MATCH3 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
|
|
var PERMISSIVE_MATCH4 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
|
|
var matchers = {
|
|
CSS_UNIT: new RegExp(CSS_UNIT),
|
|
rgb: new RegExp('rgb' + PERMISSIVE_MATCH3),
|
|
rgba: new RegExp('rgba' + PERMISSIVE_MATCH4),
|
|
hsl: new RegExp('hsl' + PERMISSIVE_MATCH3),
|
|
hsla: new RegExp('hsla' + PERMISSIVE_MATCH4),
|
|
hsv: new RegExp('hsv' + PERMISSIVE_MATCH3),
|
|
hsva: new RegExp('hsva' + PERMISSIVE_MATCH4),
|
|
hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
|
|
hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
|
|
hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
|
|
hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/
|
|
};
|
|
function stringInputToObject(color) {
|
|
color = color.trim().toLowerCase();
|
|
if (color.length === 0) {
|
|
return false;
|
|
}
|
|
var named = false;
|
|
if (names[color]) {
|
|
color = names[color];
|
|
named = true;
|
|
} else if (color === 'transparent') {
|
|
return { r: 0, g: 0, b: 0, a: 0, format: 'name' };
|
|
}
|
|
var match = matchers.rgb.exec(color);
|
|
if (match) {
|
|
return { r: match[1], g: match[2], b: match[3] };
|
|
}
|
|
match = matchers.rgba.exec(color);
|
|
if (match) {
|
|
return { r: match[1], g: match[2], b: match[3], a: match[4] };
|
|
}
|
|
match = matchers.hsl.exec(color);
|
|
if (match) {
|
|
return { h: match[1], s: match[2], l: match[3] };
|
|
}
|
|
match = matchers.hsla.exec(color);
|
|
if (match) {
|
|
return { h: match[1], s: match[2], l: match[3], a: match[4] };
|
|
}
|
|
match = matchers.hsv.exec(color);
|
|
if (match) {
|
|
return { h: match[1], s: match[2], v: match[3] };
|
|
}
|
|
match = matchers.hsva.exec(color);
|
|
if (match) {
|
|
return { h: match[1], s: match[2], v: match[3], a: match[4] };
|
|
}
|
|
match = matchers.hex8.exec(color);
|
|
if (match) {
|
|
return {
|
|
r: parseIntFromHex(match[1]),
|
|
g: parseIntFromHex(match[2]),
|
|
b: parseIntFromHex(match[3]),
|
|
a: convertHexToDecimal(match[4]),
|
|
format: named ? 'name' : 'hex8'
|
|
};
|
|
}
|
|
match = matchers.hex6.exec(color);
|
|
if (match) {
|
|
return {
|
|
r: parseIntFromHex(match[1]),
|
|
g: parseIntFromHex(match[2]),
|
|
b: parseIntFromHex(match[3]),
|
|
format: named ? 'name' : 'hex'
|
|
};
|
|
}
|
|
match = matchers.hex4.exec(color);
|
|
if (match) {
|
|
return {
|
|
r: parseIntFromHex(match[1] + match[1]),
|
|
g: parseIntFromHex(match[2] + match[2]),
|
|
b: parseIntFromHex(match[3] + match[3]),
|
|
a: convertHexToDecimal(match[4] + match[4]),
|
|
format: named ? 'name' : 'hex8'
|
|
};
|
|
}
|
|
match = matchers.hex3.exec(color);
|
|
if (match) {
|
|
return {
|
|
r: parseIntFromHex(match[1] + match[1]),
|
|
g: parseIntFromHex(match[2] + match[2]),
|
|
b: parseIntFromHex(match[3] + match[3]),
|
|
format: named ? 'name' : 'hex'
|
|
};
|
|
}
|
|
return false;
|
|
}
|
|
function isValidCSSUnit(color) {
|
|
return Boolean(matchers.CSS_UNIT.exec(String(color)));
|
|
}
|
|
|
|
var TinyColor = function () {
|
|
function TinyColor(color, opts) {
|
|
if (color === void 0) {
|
|
color = '';
|
|
}
|
|
if (opts === void 0) {
|
|
opts = {};
|
|
}
|
|
if (color instanceof TinyColor) {
|
|
return color;
|
|
}
|
|
this.originalInput = color;
|
|
var rgb = inputToRGB(color);
|
|
this.originalInput = color;
|
|
this.r = rgb.r;
|
|
this.g = rgb.g;
|
|
this.b = rgb.b;
|
|
this.a = rgb.a;
|
|
this.roundA = Math.round(100 * this.a) / 100;
|
|
this.format = opts.format || rgb.format;
|
|
this.gradientType = opts.gradientType;
|
|
if (this.r < 1) {
|
|
this.r = Math.round(this.r);
|
|
}
|
|
if (this.g < 1) {
|
|
this.g = Math.round(this.g);
|
|
}
|
|
if (this.b < 1) {
|
|
this.b = Math.round(this.b);
|
|
}
|
|
this.isValid = rgb.ok;
|
|
}
|
|
TinyColor.prototype.isDark = function () {
|
|
return this.getBrightness() < 128;
|
|
};
|
|
TinyColor.prototype.isLight = function () {
|
|
return !this.isDark();
|
|
};
|
|
TinyColor.prototype.getBrightness = function () {
|
|
var rgb = this.toRgb();
|
|
return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
|
|
};
|
|
TinyColor.prototype.getLuminance = function () {
|
|
var rgb = this.toRgb();
|
|
var R;
|
|
var G;
|
|
var B;
|
|
var RsRGB = rgb.r / 255;
|
|
var GsRGB = rgb.g / 255;
|
|
var BsRGB = rgb.b / 255;
|
|
if (RsRGB <= 0.03928) {
|
|
R = RsRGB / 12.92;
|
|
} else {
|
|
R = Math.pow((RsRGB + 0.055) / 1.055, 2.4);
|
|
}
|
|
if (GsRGB <= 0.03928) {
|
|
G = GsRGB / 12.92;
|
|
} else {
|
|
G = Math.pow((GsRGB + 0.055) / 1.055, 2.4);
|
|
}
|
|
if (BsRGB <= 0.03928) {
|
|
B = BsRGB / 12.92;
|
|
} else {
|
|
B = Math.pow((BsRGB + 0.055) / 1.055, 2.4);
|
|
}
|
|
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
|
};
|
|
TinyColor.prototype.getAlpha = function () {
|
|
return this.a;
|
|
};
|
|
TinyColor.prototype.setAlpha = function (alpha) {
|
|
this.a = boundAlpha(alpha);
|
|
this.roundA = Math.round(100 * this.a) / 100;
|
|
return this;
|
|
};
|
|
TinyColor.prototype.toHsv = function () {
|
|
var hsv = rgbToHsv(this.r, this.g, this.b);
|
|
return { h: hsv.h * 360, s: hsv.s, v: hsv.v, a: this.a };
|
|
};
|
|
TinyColor.prototype.toHsvString = function () {
|
|
var hsv = rgbToHsv(this.r, this.g, this.b);
|
|
var h = Math.round(hsv.h * 360);
|
|
var s = Math.round(hsv.s * 100);
|
|
var v = Math.round(hsv.v * 100);
|
|
return this.a === 1 ? "hsv(" + h + ", " + s + "%, " + v + "%)" : "hsva(" + h + ", " + s + "%, " + v + "%, " + this.roundA + ")";
|
|
};
|
|
TinyColor.prototype.toHsl = function () {
|
|
var hsl = rgbToHsl(this.r, this.g, this.b);
|
|
return { h: hsl.h * 360, s: hsl.s, l: hsl.l, a: this.a };
|
|
};
|
|
TinyColor.prototype.toHslString = function () {
|
|
var hsl = rgbToHsl(this.r, this.g, this.b);
|
|
var h = Math.round(hsl.h * 360);
|
|
var s = Math.round(hsl.s * 100);
|
|
var l = Math.round(hsl.l * 100);
|
|
return this.a === 1 ? "hsl(" + h + ", " + s + "%, " + l + "%)" : "hsla(" + h + ", " + s + "%, " + l + "%, " + this.roundA + ")";
|
|
};
|
|
TinyColor.prototype.toHex = function (allow3Char) {
|
|
if (allow3Char === void 0) {
|
|
allow3Char = false;
|
|
}
|
|
return rgbToHex(this.r, this.g, this.b, allow3Char);
|
|
};
|
|
TinyColor.prototype.toHexString = function (allow3Char) {
|
|
if (allow3Char === void 0) {
|
|
allow3Char = false;
|
|
}
|
|
return '#' + this.toHex(allow3Char);
|
|
};
|
|
TinyColor.prototype.toHex8 = function (allow4Char) {
|
|
if (allow4Char === void 0) {
|
|
allow4Char = false;
|
|
}
|
|
return rgbaToHex(this.r, this.g, this.b, this.a, allow4Char);
|
|
};
|
|
TinyColor.prototype.toHex8String = function (allow4Char) {
|
|
if (allow4Char === void 0) {
|
|
allow4Char = false;
|
|
}
|
|
return '#' + this.toHex8(allow4Char);
|
|
};
|
|
TinyColor.prototype.toRgb = function () {
|
|
return {
|
|
r: Math.round(this.r),
|
|
g: Math.round(this.g),
|
|
b: Math.round(this.b),
|
|
a: this.a
|
|
};
|
|
};
|
|
TinyColor.prototype.toRgbString = function () {
|
|
var r = Math.round(this.r);
|
|
var g = Math.round(this.g);
|
|
var b = Math.round(this.b);
|
|
return this.a === 1 ? "rgb(" + r + ", " + g + ", " + b + ")" : "rgba(" + r + ", " + g + ", " + b + ", " + this.roundA + ")";
|
|
};
|
|
TinyColor.prototype.toPercentageRgb = function () {
|
|
var fmt = function (x) {
|
|
return Math.round(bound01(x, 255) * 100) + "%";
|
|
};
|
|
return {
|
|
r: fmt(this.r),
|
|
g: fmt(this.g),
|
|
b: fmt(this.b),
|
|
a: this.a
|
|
};
|
|
};
|
|
TinyColor.prototype.toPercentageRgbString = function () {
|
|
var rnd = function (x) {
|
|
return Math.round(bound01(x, 255) * 100);
|
|
};
|
|
return this.a === 1 ? "rgb(" + rnd(this.r) + "%, " + rnd(this.g) + "%, " + rnd(this.b) + "%)" : "rgba(" + rnd(this.r) + "%, " + rnd(this.g) + "%, " + rnd(this.b) + "%, " + this.roundA + ")";
|
|
};
|
|
TinyColor.prototype.toName = function () {
|
|
if (this.a === 0) {
|
|
return 'transparent';
|
|
}
|
|
if (this.a < 1) {
|
|
return false;
|
|
}
|
|
var hex = '#' + rgbToHex(this.r, this.g, this.b, false);
|
|
for (var _i = 0, _a = Object.keys(names); _i < _a.length; _i++) {
|
|
var key = _a[_i];
|
|
if (names[key] === hex) {
|
|
return key;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
TinyColor.prototype.toString = function (format) {
|
|
var formatSet = Boolean(format);
|
|
format = format || this.format;
|
|
var formattedString = false;
|
|
var hasAlpha = this.a < 1 && this.a >= 0;
|
|
var needsAlphaFormat = !formatSet && hasAlpha && (format.startsWith('hex') || format === 'name');
|
|
if (needsAlphaFormat) {
|
|
if (format === 'name' && this.a === 0) {
|
|
return this.toName();
|
|
}
|
|
return this.toRgbString();
|
|
}
|
|
if (format === 'rgb') {
|
|
formattedString = this.toRgbString();
|
|
}
|
|
if (format === 'prgb') {
|
|
formattedString = this.toPercentageRgbString();
|
|
}
|
|
if (format === 'hex' || format === 'hex6') {
|
|
formattedString = this.toHexString();
|
|
}
|
|
if (format === 'hex3') {
|
|
formattedString = this.toHexString(true);
|
|
}
|
|
if (format === 'hex4') {
|
|
formattedString = this.toHex8String(true);
|
|
}
|
|
if (format === 'hex8') {
|
|
formattedString = this.toHex8String();
|
|
}
|
|
if (format === 'name') {
|
|
formattedString = this.toName();
|
|
}
|
|
if (format === 'hsl') {
|
|
formattedString = this.toHslString();
|
|
}
|
|
if (format === 'hsv') {
|
|
formattedString = this.toHsvString();
|
|
}
|
|
return formattedString || this.toHexString();
|
|
};
|
|
TinyColor.prototype.clone = function () {
|
|
return new TinyColor(this.toString());
|
|
};
|
|
TinyColor.prototype.lighten = function (amount) {
|
|
if (amount === void 0) {
|
|
amount = 10;
|
|
}
|
|
var hsl = this.toHsl();
|
|
hsl.l += amount / 100;
|
|
hsl.l = clamp01(hsl.l);
|
|
return new TinyColor(hsl);
|
|
};
|
|
TinyColor.prototype.brighten = function (amount) {
|
|
if (amount === void 0) {
|
|
amount = 10;
|
|
}
|
|
var rgb = this.toRgb();
|
|
rgb.r = Math.max(0, Math.min(255, rgb.r - Math.round(255 * -(amount / 100))));
|
|
rgb.g = Math.max(0, Math.min(255, rgb.g - Math.round(255 * -(amount / 100))));
|
|
rgb.b = Math.max(0, Math.min(255, rgb.b - Math.round(255 * -(amount / 100))));
|
|
return new TinyColor(rgb);
|
|
};
|
|
TinyColor.prototype.darken = function (amount) {
|
|
if (amount === void 0) {
|
|
amount = 10;
|
|
}
|
|
var hsl = this.toHsl();
|
|
hsl.l -= amount / 100;
|
|
hsl.l = clamp01(hsl.l);
|
|
return new TinyColor(hsl);
|
|
};
|
|
TinyColor.prototype.tint = function (amount) {
|
|
if (amount === void 0) {
|
|
amount = 10;
|
|
}
|
|
return this.mix('white', amount);
|
|
};
|
|
TinyColor.prototype.shade = function (amount) {
|
|
if (amount === void 0) {
|
|
amount = 10;
|
|
}
|
|
return this.mix('black', amount);
|
|
};
|
|
TinyColor.prototype.desaturate = function (amount) {
|
|
if (amount === void 0) {
|
|
amount = 10;
|
|
}
|
|
var hsl = this.toHsl();
|
|
hsl.s -= amount / 100;
|
|
hsl.s = clamp01(hsl.s);
|
|
return new TinyColor(hsl);
|
|
};
|
|
TinyColor.prototype.saturate = function (amount) {
|
|
if (amount === void 0) {
|
|
amount = 10;
|
|
}
|
|
var hsl = this.toHsl();
|
|
hsl.s += amount / 100;
|
|
hsl.s = clamp01(hsl.s);
|
|
return new TinyColor(hsl);
|
|
};
|
|
TinyColor.prototype.greyscale = function () {
|
|
return this.desaturate(100);
|
|
};
|
|
TinyColor.prototype.spin = function (amount) {
|
|
var hsl = this.toHsl();
|
|
var hue = (hsl.h + amount) % 360;
|
|
hsl.h = hue < 0 ? 360 + hue : hue;
|
|
return new TinyColor(hsl);
|
|
};
|
|
TinyColor.prototype.mix = function (color, amount) {
|
|
if (amount === void 0) {
|
|
amount = 50;
|
|
}
|
|
var rgb1 = this.toRgb();
|
|
var rgb2 = new TinyColor(color).toRgb();
|
|
var p = amount / 100;
|
|
var rgba = {
|
|
r: (rgb2.r - rgb1.r) * p + rgb1.r,
|
|
g: (rgb2.g - rgb1.g) * p + rgb1.g,
|
|
b: (rgb2.b - rgb1.b) * p + rgb1.b,
|
|
a: (rgb2.a - rgb1.a) * p + rgb1.a
|
|
};
|
|
return new TinyColor(rgba);
|
|
};
|
|
TinyColor.prototype.analogous = function (results, slices) {
|
|
if (results === void 0) {
|
|
results = 6;
|
|
}
|
|
if (slices === void 0) {
|
|
slices = 30;
|
|
}
|
|
var hsl = this.toHsl();
|
|
var part = 360 / slices;
|
|
var ret = [this];
|
|
for (hsl.h = (hsl.h - (part * results >> 1) + 720) % 360; --results;) {
|
|
hsl.h = (hsl.h + part) % 360;
|
|
ret.push(new TinyColor(hsl));
|
|
}
|
|
return ret;
|
|
};
|
|
TinyColor.prototype.complement = function () {
|
|
var hsl = this.toHsl();
|
|
hsl.h = (hsl.h + 180) % 360;
|
|
return new TinyColor(hsl);
|
|
};
|
|
TinyColor.prototype.monochromatic = function (results) {
|
|
if (results === void 0) {
|
|
results = 6;
|
|
}
|
|
var hsv = this.toHsv();
|
|
var h = hsv.h;
|
|
var s = hsv.s;
|
|
var v = hsv.v;
|
|
var res = [];
|
|
var modification = 1 / results;
|
|
while (results--) {
|
|
res.push(new TinyColor({ h: h, s: s, v: v }));
|
|
v = (v + modification) % 1;
|
|
}
|
|
return res;
|
|
};
|
|
TinyColor.prototype.splitcomplement = function () {
|
|
var hsl = this.toHsl();
|
|
var h = hsl.h;
|
|
return [this, new TinyColor({ h: (h + 72) % 360, s: hsl.s, l: hsl.l }), new TinyColor({ h: (h + 216) % 360, s: hsl.s, l: hsl.l })];
|
|
};
|
|
TinyColor.prototype.triad = function () {
|
|
return this.polyad(3);
|
|
};
|
|
TinyColor.prototype.tetrad = function () {
|
|
return this.polyad(4);
|
|
};
|
|
TinyColor.prototype.polyad = function (n) {
|
|
var hsl = this.toHsl();
|
|
var h = hsl.h;
|
|
var result = [this];
|
|
var increment = 360 / n;
|
|
for (var i = 1; i < n; i++) {
|
|
result.push(new TinyColor({ h: (h + i * increment) % 360, s: hsl.s, l: hsl.l }));
|
|
}
|
|
return result;
|
|
};
|
|
TinyColor.prototype.equals = function (color) {
|
|
return this.toRgbString() === new TinyColor(color).toRgbString();
|
|
};
|
|
return TinyColor;
|
|
}();
|
|
|
|
function computeDomain(entityId) {
|
|
return entityId.substr(0, entityId.indexOf('.'));
|
|
}
|
|
function computeEntity(entityId) {
|
|
return entityId.substr(entityId.indexOf('.') + 1);
|
|
}
|
|
function getColorFromVariable(color) {
|
|
if (color.substring(0, 3) === 'var') {
|
|
return window.getComputedStyle(document.documentElement).getPropertyValue(color.substring(4).slice(0, -1)).trim();
|
|
}
|
|
return color;
|
|
}
|
|
function getFontColorBasedOnBackgroundColor(backgroundColor) {
|
|
const colorObj = new TinyColor(getColorFromVariable(backgroundColor));
|
|
if (colorObj.isValid && colorObj.getLuminance() > 0.5) {
|
|
return 'rgb(62, 62, 62)'; // bright colors - black font
|
|
} else {
|
|
return 'rgb(234, 234, 234)'; // dark colors - white font
|
|
}
|
|
}
|
|
function buildNameStateConcat(name, stateString) {
|
|
if (!name && !stateString) {
|
|
return undefined;
|
|
}
|
|
let nameStateString;
|
|
if (stateString) {
|
|
if (name) {
|
|
nameStateString = `${name}: ${stateString}`;
|
|
} else {
|
|
nameStateString = stateString;
|
|
}
|
|
} else {
|
|
nameStateString = name;
|
|
}
|
|
return nameStateString;
|
|
}
|
|
function applyBrightnessToColor(color, brightness) {
|
|
const colorObj = new TinyColor(getColorFromVariable(color));
|
|
if (colorObj.isValid) {
|
|
const validColor = colorObj.darken(100 - brightness).toString();
|
|
if (validColor) return validColor;
|
|
}
|
|
return color;
|
|
}
|
|
// Check if config or Entity changed
|
|
function hasConfigOrEntityChanged(element, changedProps, forceUpdate) {
|
|
if (changedProps.has('config') || forceUpdate) {
|
|
return true;
|
|
}
|
|
if (element.config.entity) {
|
|
const oldHass = changedProps.get('hass');
|
|
if (oldHass) {
|
|
return oldHass.states[element.config.entity] !== element.hass.states[element.config.entity];
|
|
}
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Polymer legacy event helpers used courtesy of the Polymer project.
|
|
//
|
|
// Copyright (c) 2017 The Polymer Authors. All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions are
|
|
// met:
|
|
//
|
|
// * Redistributions of source code must retain the above copyright
|
|
// notice, this list of conditions and the following disclaimer.
|
|
// * Redistributions in binary form must reproduce the above
|
|
// copyright notice, this list of conditions and the following disclaimer
|
|
// in the documentation and/or other materials provided with the
|
|
// distribution.
|
|
// * Neither the name of Google Inc. nor the names of its
|
|
// contributors may be used to endorse or promote products derived from
|
|
// this software without specific prior written permission.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
/**
|
|
* Dispatches a custom event with an optional detail value.
|
|
*
|
|
* @param {string} type Name of event type.
|
|
* @param {*=} detail Detail value containing event-specific
|
|
* payload.
|
|
* @param {{ bubbles: (boolean|undefined),
|
|
* cancelable: (boolean|undefined),
|
|
* composed: (boolean|undefined) }=}
|
|
* options Object specifying options. These may include:
|
|
* `bubbles` (boolean, defaults to `true`),
|
|
* `cancelable` (boolean, defaults to false), and
|
|
* `node` on which to fire the event (HTMLElement, defaults to `this`).
|
|
* @return {Event} The new event that was fired.
|
|
*/
|
|
const fireEvent = (node, type, detail, options) => {
|
|
options = options || {};
|
|
// @ts-ignore
|
|
detail = detail === null || detail === undefined ? {} : detail;
|
|
const event = new Event(type, {
|
|
bubbles: options.bubbles === undefined ? true : options.bubbles,
|
|
cancelable: Boolean(options.cancelable),
|
|
composed: options.composed === undefined ? true : options.composed
|
|
});
|
|
event.detail = detail;
|
|
node.dispatchEvent(event);
|
|
return event;
|
|
};
|
|
|
|
const navigate = (_node, path, replace = false) => {
|
|
if (replace) {
|
|
history.replaceState(null, "", path);
|
|
} else {
|
|
history.pushState(null, "", path);
|
|
}
|
|
fireEvent(window, "location-changed", {
|
|
replace
|
|
});
|
|
};
|
|
|
|
const turnOnOffEntity = (hass, entityId, turnOn = true) => {
|
|
const stateDomain = computeDomain(entityId);
|
|
const serviceDomain = stateDomain === "group" ? "homeassistant" : stateDomain;
|
|
let service;
|
|
switch (stateDomain) {
|
|
case "lock":
|
|
service = turnOn ? "unlock" : "lock";
|
|
break;
|
|
case "cover":
|
|
service = turnOn ? "open_cover" : "close_cover";
|
|
break;
|
|
default:
|
|
service = turnOn ? "turn_on" : "turn_off";
|
|
}
|
|
return hass.callService(serviceDomain, service, { entity_id: entityId });
|
|
};
|
|
|
|
const toggleEntity = (hass, entityId) => {
|
|
const turnOn = STATES_OFF.includes(hass.states[entityId].state);
|
|
return turnOnOffEntity(hass, entityId, turnOn);
|
|
};
|
|
|
|
/**
|
|
* Utility function that enables haptic feedback
|
|
*/
|
|
const forwardHaptic = (el, hapticType) => {
|
|
fireEvent(el, "haptic", hapticType);
|
|
};
|
|
|
|
const handleClick = (node, hass, config, hold) => {
|
|
let actionConfig;
|
|
if (hold && config.hold_action) {
|
|
actionConfig = config.hold_action;
|
|
} else if (!hold && config.tap_action) {
|
|
actionConfig = config.tap_action;
|
|
}
|
|
if (!actionConfig) {
|
|
actionConfig = {
|
|
action: "toggle"
|
|
};
|
|
}
|
|
switch (actionConfig.action) {
|
|
case "more-info":
|
|
if (config.entity || config.camera_image) {
|
|
fireEvent(node, "hass-more-info", {
|
|
entityId: config.entity ? config.entity : config.camera_image
|
|
});
|
|
if (actionConfig.haptic) forwardHaptic(node, actionConfig.haptic);
|
|
}
|
|
break;
|
|
case "navigate":
|
|
if (actionConfig.navigation_path) {
|
|
navigate(node, actionConfig.navigation_path);
|
|
if (actionConfig.haptic) forwardHaptic(node, actionConfig.haptic);
|
|
}
|
|
break;
|
|
case 'url':
|
|
actionConfig.url && window.open(actionConfig.url);
|
|
if (actionConfig.haptic) forwardHaptic(node, actionConfig.haptic);
|
|
break;
|
|
case "toggle":
|
|
if (config.entity) {
|
|
toggleEntity(hass, config.entity);
|
|
if (actionConfig.haptic) forwardHaptic(node, actionConfig.haptic);
|
|
}
|
|
break;
|
|
case "call-service":
|
|
{
|
|
if (!actionConfig.service) {
|
|
return;
|
|
}
|
|
const [domain, service] = actionConfig.service.split(".", 2);
|
|
hass.callService(domain, service, actionConfig.service_data);
|
|
if (actionConfig.haptic) forwardHaptic(node, actionConfig.haptic);
|
|
}
|
|
}
|
|
};
|
|
|
|
// See https://github.com/home-assistant/home-assistant-polymer/pull/2457
|
|
// on how to undo mwc -> paper migration
|
|
// import "@material/mwc-ripple";
|
|
const isTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
|
class LongPress extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.holdTime = 500;
|
|
this.ripple = document.createElement("paper-ripple");
|
|
this.timer = undefined;
|
|
this.held = false;
|
|
this.cooldownStart = false;
|
|
this.cooldownEnd = false;
|
|
}
|
|
connectedCallback() {
|
|
Object.assign(this.style, {
|
|
borderRadius: "50%",
|
|
position: "absolute",
|
|
width: isTouch ? "100px" : "50px",
|
|
height: isTouch ? "100px" : "50px",
|
|
transform: "translate(-50%, -50%)",
|
|
pointerEvents: "none"
|
|
});
|
|
this.appendChild(this.ripple);
|
|
this.ripple.style.color = "#03a9f4"; // paper-ripple
|
|
this.ripple.style.color = "var(--primary-color)"; // paper-ripple
|
|
// this.ripple.primary = true;
|
|
["touchcancel", "mouseout", "mouseup", "touchmove", "mousewheel", "wheel", "scroll"].forEach(ev => {
|
|
document.addEventListener(ev, () => {
|
|
clearTimeout(this.timer);
|
|
this.stopAnimation();
|
|
this.timer = undefined;
|
|
}, { passive: true });
|
|
});
|
|
}
|
|
bind(element) {
|
|
if (element.longPress) {
|
|
return;
|
|
}
|
|
element.longPress = true;
|
|
element.addEventListener("contextmenu", ev => {
|
|
const e = ev || window.event;
|
|
if (e.preventDefault) {
|
|
e.preventDefault();
|
|
}
|
|
if (e.stopPropagation) {
|
|
e.stopPropagation();
|
|
}
|
|
e.cancelBubble = true;
|
|
e.returnValue = false;
|
|
return false;
|
|
});
|
|
const clickStart = ev => {
|
|
if (this.cooldownStart) {
|
|
return;
|
|
}
|
|
this.held = false;
|
|
let x;
|
|
let y;
|
|
if (ev.touches) {
|
|
x = ev.touches[0].pageX;
|
|
y = ev.touches[0].pageY;
|
|
} else {
|
|
x = ev.pageX;
|
|
y = ev.pageY;
|
|
}
|
|
this.timer = window.setTimeout(() => {
|
|
this.startAnimation(x, y);
|
|
this.held = true;
|
|
}, this.holdTime);
|
|
this.cooldownStart = true;
|
|
window.setTimeout(() => this.cooldownStart = false, 100);
|
|
};
|
|
const clickEnd = ev => {
|
|
if (this.cooldownEnd || ["touchend", "touchcancel"].includes(ev.type) && this.timer === undefined) {
|
|
return;
|
|
}
|
|
clearTimeout(this.timer);
|
|
this.stopAnimation();
|
|
this.timer = undefined;
|
|
if (this.held) {
|
|
element.dispatchEvent(new Event("ha-hold"));
|
|
} else {
|
|
element.dispatchEvent(new Event("ha-click"));
|
|
}
|
|
this.cooldownEnd = true;
|
|
window.setTimeout(() => this.cooldownEnd = false, 100);
|
|
};
|
|
element.addEventListener("touchstart", clickStart, { passive: true });
|
|
element.addEventListener("touchend", clickEnd);
|
|
element.addEventListener("touchcancel", clickEnd);
|
|
element.addEventListener("mousedown", clickStart, { passive: true });
|
|
element.addEventListener("click", clickEnd);
|
|
}
|
|
startAnimation(x, y) {
|
|
Object.assign(this.style, {
|
|
left: `${x}px`,
|
|
top: `${y}px`,
|
|
display: null
|
|
});
|
|
this.ripple.holdDown = true; // paper-ripple
|
|
this.ripple.simulatedRipple(); // paper-ripple
|
|
// this.ripple.disabled = false;
|
|
// this.ripple.active = true;
|
|
// this.ripple.unbounded = true;
|
|
}
|
|
stopAnimation() {
|
|
this.ripple.holdDown = false; // paper-ripple
|
|
// this.ripple.active = false;
|
|
// this.ripple.disabled = true;
|
|
this.style.display = "none";
|
|
}
|
|
}
|
|
customElements.define("long-press-button-card", LongPress);
|
|
const getLongPress = () => {
|
|
const body = document.body;
|
|
if (body.querySelector("long-press")) {
|
|
return body.querySelector("long-press");
|
|
}
|
|
const longpress = document.createElement("long-press");
|
|
body.appendChild(longpress);
|
|
return longpress;
|
|
};
|
|
const longPressBind = element => {
|
|
const longpress = getLongPress();
|
|
if (!longpress) {
|
|
return;
|
|
}
|
|
longpress.bind(element);
|
|
};
|
|
const longPress = directive(() => part => {
|
|
longPressBind(part.committer.element);
|
|
});
|
|
|
|
const styles = css`
|
|
ha-card {
|
|
cursor: pointer;
|
|
overflow: hidden;
|
|
box-sizing: border-box;
|
|
}
|
|
ha-card.disabled {
|
|
pointer-events: none;
|
|
cursor: default;
|
|
}
|
|
ha-icon {
|
|
display: inline-block;
|
|
margin: auto;
|
|
}
|
|
ha-card.button-card-main {
|
|
padding: 4% 0px;
|
|
text-transform: none;
|
|
font-weight: 400;
|
|
font-size: 1.2rem;
|
|
align-items: center;
|
|
text-align: center;
|
|
letter-spacing: normal;
|
|
width: 100%;
|
|
}
|
|
div {
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
}
|
|
@keyframes blink{
|
|
0%{opacity:0;}
|
|
50%{opacity:1;}
|
|
100%{opacity:0;}
|
|
}
|
|
@-webkit-keyframes rotating /* Safari and Chrome */ {
|
|
from {
|
|
-webkit-transform: rotate(0deg);
|
|
-o-transform: rotate(0deg);
|
|
transform: rotate(0deg);
|
|
}
|
|
to {
|
|
-webkit-transform: rotate(360deg);
|
|
-o-transform: rotate(360deg);
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
@keyframes rotating {
|
|
from {
|
|
-ms-transform: rotate(0deg);
|
|
-moz-transform: rotate(0deg);
|
|
-webkit-transform: rotate(0deg);
|
|
-o-transform: rotate(0deg);
|
|
transform: rotate(0deg);
|
|
}
|
|
to {
|
|
-ms-transform: rotate(360deg);
|
|
-moz-transform: rotate(360deg);
|
|
-webkit-transform: rotate(360deg);
|
|
-o-transform: rotate(360deg);
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
[rotating] {
|
|
-webkit-animation: rotating 2s linear infinite;
|
|
-moz-animation: rotating 2s linear infinite;
|
|
-ms-animation: rotating 2s linear infinite;
|
|
-o-animation: rotating 2s linear infinite;
|
|
animation: rotating 2s linear infinite;
|
|
}
|
|
|
|
.container {
|
|
display: grid;
|
|
max-height: 100%;
|
|
text-align: center;
|
|
height: 100%;
|
|
align-items: center;
|
|
}
|
|
.img-cell {
|
|
grid-area: i;
|
|
height: 100%;
|
|
width: 100%;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.icon {
|
|
height: 100%;
|
|
max-width: 100%;
|
|
object-fit: scale;
|
|
overflow: hidden;
|
|
}
|
|
.name {
|
|
grid-area: n;
|
|
max-width: 100%;
|
|
align-self: center;
|
|
justify-self: center;
|
|
/* margin: auto; */
|
|
}
|
|
.state {
|
|
grid-area: s;
|
|
max-width: 100%;
|
|
align-self: center;
|
|
justify-self: center;
|
|
/* margin: auto; */
|
|
}
|
|
|
|
.label {
|
|
grid-area: l;
|
|
max-width: 100%;
|
|
align-self: center;
|
|
justify-self: center;
|
|
}
|
|
|
|
.container.vertical {
|
|
grid-template-areas: "i" "n" "s" "l";
|
|
grid-template-columns: 1fr;
|
|
grid-template-rows: 1fr min-content min-content min-content;
|
|
}
|
|
/* Vertical No Icon */
|
|
.container.vertical.no-icon {
|
|
grid-template-areas: "n" "s" "l";
|
|
grid-template-columns: 1fr;
|
|
grid-template-rows: 1fr min-content 1fr;
|
|
}
|
|
.container.vertical.no-icon .state {
|
|
align-self: center;
|
|
}
|
|
.container.vertical.no-icon .name {
|
|
align-self: end;
|
|
}
|
|
.container.vertical.no-icon .label {
|
|
align-self: start;
|
|
}
|
|
|
|
/* Vertical No Icon No Name */
|
|
.container.vertical.no-icon.no-name {
|
|
grid-template-areas: "s" "l";
|
|
grid-template-columns: 1fr;
|
|
grid-template-rows: 1fr 1fr;
|
|
}
|
|
.container.vertical.no-icon.no-name .state {
|
|
align-self: end;
|
|
}
|
|
.container.vertical.no-icon.no-name .label {
|
|
align-self: start;
|
|
}
|
|
|
|
/* Vertical No Icon No State */
|
|
.container.vertical.no-icon.no-state {
|
|
grid-template-areas: "n" "l";
|
|
grid-template-columns: 1fr;
|
|
grid-template-rows: 1fr 1fr;
|
|
}
|
|
.container.vertical.no-icon.no-state .name {
|
|
align-self: end;
|
|
}
|
|
.container.vertical.no-icon.no-state .label {
|
|
align-self: start;
|
|
}
|
|
|
|
/* Vertical No Icon No Label */
|
|
.container.vertical.no-icon.no-label {
|
|
grid-template-areas: "n" "s";
|
|
grid-template-columns: 1fr;
|
|
grid-template-rows: 1fr 1fr;
|
|
}
|
|
.container.vertical.no-icon.no-label .name {
|
|
align-self: end;
|
|
}
|
|
.container.vertical.no-icon.no-label .state {
|
|
align-self: start;
|
|
}
|
|
|
|
/* Vertical No Icon No Label No Name */
|
|
.container.vertical.no-icon.no-label.no-name {
|
|
grid-template-areas: "s";
|
|
grid-template-columns: 1fr;
|
|
grid-template-rows: 1fr;
|
|
}
|
|
.container.vertical.no-icon.no-label.no-name .state {
|
|
align-self: center;
|
|
}
|
|
/* Vertical No Icon No Label No State */
|
|
.container.vertical.no-icon.no-label.no-state {
|
|
grid-template-areas: "n";
|
|
grid-template-columns: 1fr;
|
|
grid-template-rows: 1fr;
|
|
}
|
|
.container.vertical.no-icon.no-label.no-state .name {
|
|
align-self: center;
|
|
}
|
|
|
|
/* Vertical No Icon No Name No State */
|
|
.container.vertical.no-icon.no-name.no-state {
|
|
grid-template-areas: "l";
|
|
grid-template-columns: 1fr;
|
|
grid-template-rows: 1fr;
|
|
}
|
|
.container.vertical.no-icon.no-name.no-state .label {
|
|
align-self: center;
|
|
}
|
|
|
|
.container.icon_name_state {
|
|
grid-template-areas: "i n" "l l";
|
|
grid-template-columns: 40% 1fr;
|
|
grid-template-rows: 1fr min-content;
|
|
}
|
|
|
|
.container.icon_name {
|
|
grid-template-areas: "i n" "s s" "l l";
|
|
grid-template-columns: 40% 1fr;
|
|
grid-template-rows: 1fr min-content min-content;
|
|
}
|
|
|
|
.container.icon_state {
|
|
grid-template-areas: "i s" "n n" "l l";
|
|
grid-template-columns: 40% 1fr;
|
|
grid-template-rows: 1fr min-content min-content;
|
|
}
|
|
|
|
.container.name_state {
|
|
grid-template-areas: "i" "n" "l";
|
|
grid-template-columns: 1fr;
|
|
grid-template-rows: 1fr min-content min-content;
|
|
}
|
|
.container.name_state.no-icon {
|
|
grid-template-areas: "n" "l";
|
|
grid-template-columns: 1fr;
|
|
grid-template-rows: 1fr 1fr;
|
|
}
|
|
.container.name_state.no-icon .name {
|
|
align-self: end
|
|
}
|
|
.container.name_state.no-icon .label {
|
|
align-self: start
|
|
}
|
|
|
|
.container.name_state.no-icon.no-label {
|
|
grid-template-areas: "n";
|
|
grid-template-columns: 1fr;
|
|
grid-template-rows: 1fr;
|
|
}
|
|
.container.name_state.no-icon.no-label .name {
|
|
align-self: center
|
|
}
|
|
|
|
/* icon_name_state2nd default */
|
|
.container.icon_name_state2nd {
|
|
grid-template-areas: "i n" "i s" "i l";
|
|
grid-template-columns: 40% 1fr;
|
|
grid-template-rows: 1fr min-content 1fr;
|
|
}
|
|
.container.icon_name_state2nd .name {
|
|
align-self: end;
|
|
}
|
|
.container.icon_name_state2nd .state {
|
|
align-self: center;
|
|
}
|
|
.container.icon_name_state2nd .label {
|
|
align-self: start;
|
|
}
|
|
|
|
/* icon_name_state2nd No Label */
|
|
.container.icon_name_state2nd.no-label {
|
|
grid-template-areas: "i n" "i s";
|
|
grid-template-columns: 40% 1fr;
|
|
grid-template-rows: 1fr 1fr;
|
|
}
|
|
.container.icon_name_state2nd .name {
|
|
align-self: end;
|
|
}
|
|
.container.icon_name_state2nd .state {
|
|
align-self: start;
|
|
}
|
|
|
|
/* icon_state_name2nd Default */
|
|
.container.icon_state_name2nd {
|
|
grid-template-areas: "i s" "i n" "i l";
|
|
grid-template-columns: 40% 1fr;
|
|
grid-template-rows: 1fr min-content 1fr;
|
|
}
|
|
.container.icon_state_name2nd .state {
|
|
align-self: end;
|
|
}
|
|
.container.icon_state_name2nd .name {
|
|
align-self: center;
|
|
}
|
|
.container.icon_state_name2nd .state {
|
|
align-self: start;
|
|
}
|
|
|
|
/* icon_state_name2nd No Label */
|
|
.container.icon_state_name2nd.no-label {
|
|
grid-template-areas: "i s" "i n";
|
|
grid-template-columns: 40% 1fr;
|
|
grid-template-rows: 1fr 1fr;
|
|
}
|
|
.container.icon_state_name2nd .state {
|
|
align-self: end;
|
|
}
|
|
.container.icon_state_name2nd .name {
|
|
align-self: start;
|
|
}
|
|
|
|
.container.icon_label {
|
|
grid-template-areas: "i l" "n n" "s s";
|
|
grid-template-columns: 40% 1fr;
|
|
grid-template-rows: 1fr min-content min-content;
|
|
}
|
|
`;
|
|
|
|
let ButtonCard = class ButtonCard extends LitElement {
|
|
static get styles() {
|
|
return styles;
|
|
}
|
|
render() {
|
|
if (!this.config || !this.hass) {
|
|
return html``;
|
|
}
|
|
return this._cardHtml();
|
|
}
|
|
shouldUpdate(changedProps) {
|
|
const state = this.config.entity ? this.hass.states[this.config.entity] : undefined;
|
|
const configState = this._getMatchingConfigState(state);
|
|
const forceUpdate = this.config.show_label && (configState && configState.label_template || this.config.label_template) || this.config.state && this.config.state.find(elt => {
|
|
return elt.operator === 'template';
|
|
}) ? true : false;
|
|
return hasConfigOrEntityChanged(this, changedProps, forceUpdate);
|
|
}
|
|
_getMatchingConfigState(state) {
|
|
if (!state || !this.config.state) {
|
|
return undefined;
|
|
}
|
|
let def;
|
|
const retval = this.config.state.find(elt => {
|
|
if (elt.operator) {
|
|
switch (elt.operator) {
|
|
case '==':
|
|
/* eslint eqeqeq: 0 */
|
|
return state.state == elt.value;
|
|
case '<=':
|
|
return state.state <= elt.value;
|
|
case '<':
|
|
return state.state < elt.value;
|
|
case '>=':
|
|
return state.state >= elt.value;
|
|
case '>':
|
|
return state.state > elt.value;
|
|
case '!=':
|
|
return state.state != elt.value;
|
|
case 'regex':
|
|
{
|
|
/* eslint no-unneeded-ternary: 0 */
|
|
const matches = state.state.match(elt.value) ? true : false;
|
|
return matches;
|
|
}
|
|
case 'template':
|
|
{
|
|
return new Function('states', 'entity', 'user', 'hass', `'use strict'; ${elt.value}`).call(this, this.hass.states, state, this.hass.user, this.hass);
|
|
}
|
|
case 'default':
|
|
def = elt;
|
|
return false;
|
|
default:
|
|
return false;
|
|
}
|
|
} else {
|
|
return elt.value == state.state;
|
|
}
|
|
});
|
|
if (!retval && def) {
|
|
return def;
|
|
}
|
|
return retval;
|
|
}
|
|
_getDefaultColorForState(state) {
|
|
switch (state.state) {
|
|
case 'on':
|
|
return this.config.color_on;
|
|
case 'off':
|
|
return this.config.color_off;
|
|
default:
|
|
return this.config.default_color;
|
|
}
|
|
}
|
|
_buildCssColorAttribute(state, configState) {
|
|
let colorValue = '';
|
|
let color;
|
|
if (configState && configState.color) {
|
|
colorValue = configState.color;
|
|
} else if (this.config.color !== 'auto' && state && state.state === 'off') {
|
|
colorValue = this.config.color_off;
|
|
} else if (this.config.color) {
|
|
colorValue = this.config.color;
|
|
}
|
|
if (colorValue == 'auto') {
|
|
if (state) {
|
|
if (state.attributes.rgb_color) {
|
|
color = `rgb(${state.attributes.rgb_color.join(',')})`;
|
|
if (state.attributes.brightness) {
|
|
color = applyBrightnessToColor(color, (state.attributes.brightness + 245) / 5);
|
|
}
|
|
} else if (state.attributes.brightness) {
|
|
color = applyBrightnessToColor(this._getDefaultColorForState(state), (state.attributes.brightness + 245) / 5);
|
|
} else {
|
|
color = this._getDefaultColorForState(state);
|
|
}
|
|
} else {
|
|
color = this.config.default_color;
|
|
}
|
|
} else if (colorValue) {
|
|
color = colorValue;
|
|
} else if (state) {
|
|
color = this._getDefaultColorForState(state);
|
|
} else {
|
|
color = this.config.default_color;
|
|
}
|
|
return color;
|
|
}
|
|
_buildIcon(state, configState) {
|
|
if (!this.config.show_icon) {
|
|
return undefined;
|
|
}
|
|
let icon;
|
|
if (configState && configState.icon) {
|
|
icon = configState.icon;
|
|
} else if (this.config.icon) {
|
|
icon = this.config.icon;
|
|
} else if (state && state.attributes) {
|
|
icon = state.attributes.icon ? state.attributes.icon : domainIcon(computeDomain(state.entity_id), state.state);
|
|
}
|
|
return icon;
|
|
}
|
|
_buildEntityPicture(state, configState) {
|
|
if (!this.config.show_entity_picture || !state && !configState && !this.config.entity_picture) {
|
|
return undefined;
|
|
}
|
|
let entityPicture;
|
|
if (configState && configState.entity_picture) {
|
|
entityPicture = configState.entity_picture;
|
|
} else if (this.config.entity_picture) {
|
|
entityPicture = this.config.entity_picture;
|
|
} else {
|
|
entityPicture = state && state.attributes && state.attributes.entity_picture ? state.attributes.entity_picture : undefined;
|
|
}
|
|
return entityPicture;
|
|
}
|
|
_buildStyle(state, configState) {
|
|
let cardStyle = {};
|
|
let styleArray;
|
|
if (state) {
|
|
if (configState && configState.style) {
|
|
styleArray = configState.style;
|
|
} else if (this.config.style) {
|
|
styleArray = this.config.style;
|
|
}
|
|
} else if (this.config.style) {
|
|
styleArray = this.config.style;
|
|
}
|
|
if (styleArray) {
|
|
cardStyle = Object.assign(cardStyle, ...styleArray);
|
|
}
|
|
return cardStyle;
|
|
}
|
|
_buildEntityPictureStyle(state, configState) {
|
|
let entityPictureStyle = {};
|
|
let styleArray;
|
|
if (state) {
|
|
if (configState && configState.entity_picture_style) {
|
|
styleArray = configState.entity_picture_style;
|
|
} else if (this.config.entity_picture_style) {
|
|
styleArray = this.config.entity_picture_style;
|
|
}
|
|
} else if (this.config.entity_picture_style) {
|
|
styleArray = this.config.entity_picture_style;
|
|
}
|
|
if (styleArray) {
|
|
entityPictureStyle = Object.assign(entityPictureStyle, ...styleArray);
|
|
}
|
|
return entityPictureStyle;
|
|
}
|
|
_buildName(state, configState) {
|
|
if (this.config.show_name === false) {
|
|
return undefined;
|
|
}
|
|
let name;
|
|
if (configState && configState.name) {
|
|
name = configState.name;
|
|
} else if (this.config.name) {
|
|
name = this.config.name;
|
|
} else if (state) {
|
|
name = state.attributes && state.attributes.friendly_name ? state.attributes.friendly_name : computeEntity(state.entity_id);
|
|
}
|
|
return name;
|
|
}
|
|
_buildStateString(state) {
|
|
let stateString;
|
|
if (this.config.show_state && state && state.state) {
|
|
const units = this._buildUnits(state);
|
|
if (units) {
|
|
stateString = `${state.state} ${units}`;
|
|
} else {
|
|
stateString = state.state;
|
|
}
|
|
}
|
|
return stateString;
|
|
}
|
|
_buildUnits(state) {
|
|
let units;
|
|
if (state) {
|
|
if (this.config.show_units) {
|
|
if (state.attributes && state.attributes.unit_of_measurement && !this.config.units) {
|
|
units = state.attributes.unit_of_measurement;
|
|
} else {
|
|
units = this.config.units ? this.config.units : undefined;
|
|
}
|
|
}
|
|
}
|
|
return units;
|
|
}
|
|
_buildLabel(state, configState) {
|
|
if (!this.config.show_label) {
|
|
return undefined;
|
|
}
|
|
let label;
|
|
let matchingLabelTemplate;
|
|
if (configState && configState.label_template) {
|
|
matchingLabelTemplate = configState.label_template;
|
|
} else {
|
|
matchingLabelTemplate = this.config.label_template;
|
|
}
|
|
if (!matchingLabelTemplate) {
|
|
if (configState && configState.label) {
|
|
label = configState.label;
|
|
} else {
|
|
label = this.config.label;
|
|
}
|
|
return label;
|
|
}
|
|
/* eslint no-new-func: 0 */
|
|
return new Function('states', 'entity', 'user', 'hass', `'use strict'; ${matchingLabelTemplate}`).call(this, this.hass.states, state, this.hass.user, this.hass);
|
|
}
|
|
_isClickable(state) {
|
|
let clickable = true;
|
|
if (this.config.tap_action.action === 'toggle' && this.config.hold_action.action === 'none' || this.config.hold_action.action === 'toggle' && this.config.tap_action.action === 'none') {
|
|
if (state) {
|
|
switch (computeDomain(state.entity_id)) {
|
|
case 'sensor':
|
|
case 'binary_sensor':
|
|
case 'device_tracker':
|
|
clickable = false;
|
|
break;
|
|
default:
|
|
clickable = true;
|
|
break;
|
|
}
|
|
} else {
|
|
clickable = false;
|
|
}
|
|
} else if (this.config.tap_action.action != 'none' || this.config.hold_action.action != 'none') {
|
|
clickable = true;
|
|
} else {
|
|
clickable = false;
|
|
}
|
|
return clickable;
|
|
}
|
|
_rotate(configState) {
|
|
return configState && configState.spin ? true : false;
|
|
}
|
|
_blankCardColoredHtml(state, cardStyle) {
|
|
const color = this._buildCssColorAttribute(state, undefined);
|
|
const fontColor = getFontColorBasedOnBackgroundColor(color);
|
|
return html`
|
|
<ha-card class="disabled" style=${styleMap(cardStyle)}>
|
|
<div style="color: ${fontColor}; background-color: ${color};"></div>
|
|
</ha-card>
|
|
`;
|
|
}
|
|
_cardHtml() {
|
|
const state = this.config.entity ? this.hass.states[this.config.entity] : undefined;
|
|
const configState = this._getMatchingConfigState(state);
|
|
const color = this._buildCssColorAttribute(state, configState);
|
|
let buttonColor = color;
|
|
let cardStyle = {};
|
|
const configCardStyle = this._buildStyle(state, configState);
|
|
if (configCardStyle.width) {
|
|
this.style.setProperty('flex', '0 0 auto');
|
|
this.style.setProperty('max-width', 'fit-content');
|
|
}
|
|
switch (this.config.color_type) {
|
|
case 'blank-card':
|
|
return this._blankCardColoredHtml(state, configCardStyle);
|
|
case 'card':
|
|
case 'label-card':
|
|
{
|
|
const fontColor = getFontColorBasedOnBackgroundColor(color);
|
|
cardStyle.color = fontColor;
|
|
cardStyle['background-color'] = color;
|
|
cardStyle = Object.assign({}, cardStyle, configCardStyle);
|
|
buttonColor = 'inherit';
|
|
break;
|
|
}
|
|
default:
|
|
cardStyle = configCardStyle;
|
|
break;
|
|
}
|
|
return html`
|
|
<ha-card class="button-card-main ${this._isClickable(state) ? '' : 'disabled'}" style=${styleMap(cardStyle)} @ha-click="${this._handleTap}" @ha-hold="${this._handleHold}" .longpress="${longPress()}" .config="${this.config}">
|
|
${this._buttonContent(state, configState, buttonColor)}
|
|
<mwc-ripple></mwc-ripple>
|
|
</ha-card>
|
|
`;
|
|
}
|
|
_buttonContent(state, configState, color) {
|
|
const name = this._buildName(state, configState);
|
|
const stateString = this._buildStateString(state);
|
|
const nameStateString = buildNameStateConcat(name, stateString);
|
|
switch (this.config.layout) {
|
|
case 'icon_name_state':
|
|
case 'name_state':
|
|
return this._gridHtml(state, configState, this.config.layout, color, nameStateString, undefined);
|
|
default:
|
|
return this._gridHtml(state, configState, this.config.layout, color, name, stateString);
|
|
}
|
|
}
|
|
_gridHtml(state, configState, containerClass, color, name, stateString) {
|
|
const iconTemplate = this._getIconHtml(state, configState, color);
|
|
const itemClass = ['container', containerClass];
|
|
const label = this._buildLabel(state, configState);
|
|
if (!iconTemplate) itemClass.push('no-icon');
|
|
if (!name) itemClass.push('no-name');
|
|
if (!stateString) itemClass.push('no-state');
|
|
if (!label) itemClass.push('no-label');
|
|
return html`
|
|
<div class=${itemClass.join(' ')}>
|
|
${iconTemplate ? iconTemplate : ''}
|
|
${name ? html`<div class="name">${name}</div>` : ''}
|
|
${stateString ? html`<div class="state">${stateString}</div>` : ''}
|
|
${label ? html`<div class="label">${label}</div>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
_getIconHtml(state, configState, color) {
|
|
const icon = this._buildIcon(state, configState);
|
|
const entityPicture = this._buildEntityPicture(state, configState);
|
|
const entityPictureStyleFromConfig = this._buildEntityPictureStyle(state, configState);
|
|
const haIconStyle = {
|
|
color,
|
|
width: this.config.size,
|
|
'min-width': this.config.size
|
|
};
|
|
const entityPictureStyle = Object.assign({}, haIconStyle, entityPictureStyleFromConfig);
|
|
if (icon || entityPicture) {
|
|
return html`
|
|
<div class="img-cell">
|
|
${icon && !entityPicture ? html`<ha-icon style=${styleMap(haIconStyle)}
|
|
.icon="${icon}" class="icon" ?rotating=${this._rotate(configState)}></ha-icon>` : ''}
|
|
${entityPicture ? html`<img src="${entityPicture}" style=${styleMap(entityPictureStyle)}
|
|
class="icon" ?rotating=${this._rotate(configState)} />` : ''}
|
|
</div>
|
|
`;
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
setConfig(config) {
|
|
if (!config) {
|
|
throw new Error('Invalid configuration');
|
|
}
|
|
this.config = Object.assign({ tap_action: { action: 'toggle' }, hold_action: { action: 'none' }, layout: 'vertical', size: '40%', color_type: 'icon', show_name: true, show_state: false, show_icon: true, show_units: true, show_label: false, show_entity_picture: false }, config);
|
|
this.config.default_color = 'var(--primary-text-color)';
|
|
if (this.config.color_type !== 'icon') {
|
|
this.config.color_off = 'var(--paper-card-background-color)';
|
|
} else {
|
|
this.config.color_off = 'var(--paper-item-icon-color)';
|
|
}
|
|
this.config.color_on = 'var(--paper-item-icon-active-color)';
|
|
}
|
|
// The height of your card. Home Assistant uses this to automatically
|
|
// distribute all cards over the available columns.
|
|
getCardSize() {
|
|
return 3;
|
|
}
|
|
_handleTap(ev) {
|
|
/* eslint no-alert: 0 */
|
|
if (this.config.confirmation && !window.confirm(this.config.confirmation)) {
|
|
return;
|
|
}
|
|
const config = ev.target.config;
|
|
handleClick(this, this.hass, config, false);
|
|
}
|
|
_handleHold(ev) {
|
|
/* eslint no-alert: 0 */
|
|
if (this.config.confirmation && !window.confirm(this.config.confirmation)) {
|
|
return;
|
|
}
|
|
const config = ev.target.config;
|
|
handleClick(this, this.hass, config, true);
|
|
}
|
|
};
|
|
__decorate([property()], ButtonCard.prototype, "hass", void 0);
|
|
__decorate([property()], ButtonCard.prototype, "config", void 0);
|
|
ButtonCard = __decorate([customElement('button-card')], ButtonCard);
|
|
//# sourceMappingURL=button-card.js.map
|