5385 lines
208 KiB
JavaScript
5385 lines
208 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
|
|
*/
|
|
/**
|
|
* True if the custom elements polyfill is in use.
|
|
*/
|
|
const isCEPolyfill = typeof window !== 'undefined' &&
|
|
window.customElements != null &&
|
|
window.customElements.polyfillWrapFlushCallback !==
|
|
undefined;
|
|
/**
|
|
* Removes nodes, starting from `start` (inclusive) to `end` (exclusive), from
|
|
* `container`.
|
|
*/
|
|
const removeNodes = (container, start, end = null) => {
|
|
while (start !== end) {
|
|
const n = start.nextSibling;
|
|
container.removeChild(start);
|
|
start = n;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @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 updatable Template that tracks the location of dynamic parts.
|
|
*/
|
|
class Template {
|
|
constructor(result, element) {
|
|
this.parts = [];
|
|
this.element = element;
|
|
const nodesToRemove = [];
|
|
const stack = [];
|
|
// Edge needs all 4 parameters present; IE11 needs 3rd parameter to be null
|
|
const walker = document.createTreeWalker(element.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;
|
|
let index = -1;
|
|
let partIndex = 0;
|
|
const { strings, values: { length } } = result;
|
|
while (partIndex < length) {
|
|
const node = walker.nextNode();
|
|
if (node === null) {
|
|
// We've exhausted the content inside a nested template element.
|
|
// Because we still have parts (the outer for-loop), we know:
|
|
// - There is a template in the stack
|
|
// - The walker will find a nextNode outside the template
|
|
walker.currentNode = stack.pop();
|
|
continue;
|
|
}
|
|
index++;
|
|
if (node.nodeType === 1 /* Node.ELEMENT_NODE */) {
|
|
if (node.hasAttributes()) {
|
|
const attributes = node.attributes;
|
|
const { length } = 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 correspondence between part index and attribute index.
|
|
let count = 0;
|
|
for (let i = 0; i < length; i++) {
|
|
if (endsWith(attributes[i].name, boundAttributeSuffix)) {
|
|
count++;
|
|
}
|
|
}
|
|
while (count-- > 0) {
|
|
// Get the template literal section leading up to the first
|
|
// expression in this attribute
|
|
const stringForPart = 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);
|
|
node.removeAttribute(attributeLookupName);
|
|
const statics = attributeValue.split(markerRegex);
|
|
this.parts.push({ type: 'attribute', index, name, strings: statics });
|
|
partIndex += statics.length - 1;
|
|
}
|
|
}
|
|
if (node.tagName === 'TEMPLATE') {
|
|
stack.push(node);
|
|
walker.currentNode = node.content;
|
|
}
|
|
}
|
|
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++) {
|
|
let insert;
|
|
let s = strings[i];
|
|
if (s === '') {
|
|
insert = createMarker();
|
|
}
|
|
else {
|
|
const match = lastAttributeNameRegex.exec(s);
|
|
if (match !== null && endsWith(match[2], boundAttributeSuffix)) {
|
|
s = s.slice(0, match.index) + match[1] +
|
|
match[2].slice(0, -boundAttributeSuffix.length) + match[3];
|
|
}
|
|
insert = document.createTextNode(s);
|
|
}
|
|
parent.insertBefore(insert, 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 });
|
|
partIndex++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Remove text binding nodes after the walk to not disturb the TreeWalker
|
|
for (const n of nodesToRemove) {
|
|
n.parentNode.removeChild(n);
|
|
}
|
|
}
|
|
}
|
|
const endsWith = (str, suffix) => {
|
|
const index = str.length - suffix.length;
|
|
return index >= 0 && str.slice(index) === suffix;
|
|
};
|
|
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#elements-attributes
|
|
*
|
|
* " \x09\x0a\x0c\x0d" are HTML space characters:
|
|
* https://www.w3.org/TR/html5/infrastructure.html#space-characters
|
|
*
|
|
* "\0-\x1F\x7F-\x9F" are Unicode control characters, which includes every
|
|
* space character except " ".
|
|
*
|
|
* 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 =
|
|
// eslint-disable-next-line no-control-regex
|
|
/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \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
|
|
*/
|
|
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
|
|
*/
|
|
const directives = new WeakMap();
|
|
/**
|
|
* Brands a function as a directive factory function so that lit-html will call
|
|
* the function during template rendering, rather than passing as a value.
|
|
*
|
|
* A _directive_ is a function that takes a Part as an argument. It has the
|
|
* signature: `(part: Part) => void`.
|
|
*
|
|
* A directive _factory_ is a function that takes arguments for data and
|
|
* configuration and returns a directive. Users of directive usually refer to
|
|
* the directive factory as the directive. For example, "The repeat directive".
|
|
*
|
|
* Usually a template author will invoke a directive factory in their template
|
|
* with relevant arguments, which will then return a directive function.
|
|
*
|
|
* Here's an example of using the `repeat()` directive factory that takes an
|
|
* array and a function to render an item:
|
|
*
|
|
* ```js
|
|
* html`<ul><${repeat(items, (item) => html`<li>${item}</li>`)}</ul>`
|
|
* ```
|
|
*
|
|
* When `repeat` is invoked, it returns a directive function that closes over
|
|
* `items` and the template function. When the outer template is rendered, the
|
|
* return directive function is called with the Part for the expression.
|
|
* `repeat` then performs it's custom logic to render multiple items.
|
|
*
|
|
* @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)
|
|
* }
|
|
* });
|
|
*/
|
|
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) 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 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() {
|
|
// There are a number of steps in the lifecycle of a template instance's
|
|
// DOM fragment:
|
|
// 1. Clone - create the instance fragment
|
|
// 2. Adopt - adopt into the main document
|
|
// 3. Process - find part markers and create parts
|
|
// 4. Upgrade - upgrade custom elements
|
|
// 5. Update - set node, attribute, property, etc., values
|
|
// 6. Connect - connect to the document. Optional and outside of this
|
|
// method.
|
|
//
|
|
// We have a few constraints on the ordering of these steps:
|
|
// * We need to upgrade before updating, so that property values will pass
|
|
// through any property setters.
|
|
// * We would like to process before upgrading so that we're sure that the
|
|
// cloned fragment is inert and not disturbed by self-modifying DOM.
|
|
// * We want custom elements to upgrade even in disconnected fragments.
|
|
//
|
|
// Given these constraints, with full custom elements support we would
|
|
// prefer the order: Clone, Process, Adopt, Upgrade, Update, Connect
|
|
//
|
|
// But Safari does not implement CustomElementRegistry#upgrade, so we
|
|
// can not implement that order and still have upgrade-before-update and
|
|
// upgrade disconnected fragments. So we instead sacrifice the
|
|
// process-before-upgrade constraint, since in Custom Elements v1 elements
|
|
// must not modify their light DOM in the constructor. We still have issues
|
|
// when co-existing with CEv0 elements like Polymer 1, and with polyfills
|
|
// that don't strictly adhere to the no-modification rule because shadow
|
|
// DOM, which may be created in the constructor, is emulated by being placed
|
|
// in the light DOM.
|
|
//
|
|
// The resulting order is on native is: Clone, Adopt, Upgrade, Process,
|
|
// Update, Connect. document.importNode() performs Clone, Adopt, and Upgrade
|
|
// in one step.
|
|
//
|
|
// The Custom Elements v1 polyfill supports upgrade(), so the order when
|
|
// polyfilled is the more ideal: Clone, Process, Adopt, Upgrade, Update,
|
|
// Connect.
|
|
const fragment = isCEPolyfill ?
|
|
this.template.element.content.cloneNode(true) :
|
|
document.importNode(this.template.element.content, true);
|
|
const stack = [];
|
|
const parts = this.template.parts;
|
|
// 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 partIndex = 0;
|
|
let nodeIndex = 0;
|
|
let part;
|
|
let node = walker.nextNode();
|
|
// Loop through all the nodes and parts of a template
|
|
while (partIndex < parts.length) {
|
|
part = parts[partIndex];
|
|
if (!isTemplatePartActive(part)) {
|
|
this.__parts.push(undefined);
|
|
partIndex++;
|
|
continue;
|
|
}
|
|
// Progress the tree walker until we find our next part's node.
|
|
// Note that multiple parts may share the same node (attribute parts
|
|
// on a single element), so this loop may not run at all.
|
|
while (nodeIndex < part.index) {
|
|
nodeIndex++;
|
|
if (node.nodeName === 'TEMPLATE') {
|
|
stack.push(node);
|
|
walker.currentNode = node.content;
|
|
}
|
|
if ((node = walker.nextNode()) === null) {
|
|
// We've exhausted the content inside a nested template element.
|
|
// Because we still have parts (the outer for-loop), we know:
|
|
// - There is a template in the stack
|
|
// - The walker will find a nextNode outside the template
|
|
walker.currentNode = stack.pop();
|
|
node = walker.nextNode();
|
|
}
|
|
}
|
|
// We've arrived at our part's node.
|
|
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++;
|
|
}
|
|
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
|
|
*/
|
|
const commentMarker = ` ${marker} `;
|
|
/**
|
|
* 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 l = this.strings.length - 1;
|
|
let html = '';
|
|
let isCommentBinding = false;
|
|
for (let i = 0; i < l; i++) {
|
|
const s = this.strings[i];
|
|
// For each binding we want to determine the kind of marker to insert
|
|
// into the template source before it's parsed by the browser's HTML
|
|
// parser. The marker type is based on whether the expression is in an
|
|
// attribute, text, or comment position.
|
|
// * For node-position bindings we insert a comment with the marker
|
|
// sentinel as its text content, like <!--{{lit-guid}}-->.
|
|
// * For attribute bindings we insert just the marker sentinel for the
|
|
// first binding, so that we support unquoted attribute bindings.
|
|
// Subsequent bindings can use a comment marker because multi-binding
|
|
// attributes must be quoted.
|
|
// * For comment bindings we insert just the marker sentinel so we don't
|
|
// close the comment.
|
|
//
|
|
// The following code scans the template source, but is *not* an HTML
|
|
// parser. We don't need to track the tree structure of the HTML, only
|
|
// whether a binding is inside a comment, and if not, if it appears to be
|
|
// the first binding in an attribute.
|
|
const commentOpen = s.lastIndexOf('<!--');
|
|
// We're in comment position if we have a comment open with no following
|
|
// comment close. Because <-- can appear in an attribute value there can
|
|
// be false positives.
|
|
isCommentBinding = (commentOpen > -1 || isCommentBinding) &&
|
|
s.indexOf('-->', commentOpen + 1) === -1;
|
|
// Check to see if we have an attribute-like sequence preceding the
|
|
// expression. This can match "name=value" like structures in text,
|
|
// comments, and attribute values, so there can be false-positives.
|
|
const attributeMatch = lastAttributeNameRegex.exec(s);
|
|
if (attributeMatch === null) {
|
|
// We're only in this branch if we don't have a attribute-like
|
|
// preceding sequence. For comments, this guards against unusual
|
|
// attribute values like <div foo="<!--${'bar'}">. Cases like
|
|
// <!-- foo=${'bar'}--> are handled correctly in the attribute branch
|
|
// below.
|
|
html += s + (isCommentBinding ? commentMarker : nodeMarker);
|
|
}
|
|
else {
|
|
// For attributes we use just a marker sentinel, and also append a
|
|
// $lit$ suffix to the name to opt-out of attribute-specific parsing
|
|
// that IE and Edge do for style and certain SVG attributes.
|
|
html += s.substr(0, attributeMatch.index) + attributeMatch[1] +
|
|
attributeMatch[2] + boundAttributeSuffix + attributeMatch[3] +
|
|
marker;
|
|
}
|
|
}
|
|
html += this.strings[l];
|
|
return html;
|
|
}
|
|
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'));
|
|
};
|
|
const isIterable = (value) => {
|
|
return Array.isArray(value) ||
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
!!(value && value[Symbol.iterator]);
|
|
};
|
|
/**
|
|
* Writes attribute values to the DOM for a group of AttributeParts bound to a
|
|
* single attribute. 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 (isPrimitive(v) || !isIterable(v)) {
|
|
text += typeof v === 'string' ? v : String(v);
|
|
}
|
|
else {
|
|
for (const t of v) {
|
|
text += typeof t === 'string' ? t : String(t);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
text += strings[l];
|
|
return text;
|
|
}
|
|
commit() {
|
|
if (this.dirty) {
|
|
this.dirty = false;
|
|
this.element.setAttribute(this.name, this._getValue());
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* A Part that controls all or part of an attribute value.
|
|
*/
|
|
class AttributePart {
|
|
constructor(committer) {
|
|
this.value = undefined;
|
|
this.committer = committer;
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
/**
|
|
* A Part that controls a location within a Node tree. Like a Range, NodePart
|
|
* has start and end locations and can set and update the Nodes between those
|
|
* locations.
|
|
*
|
|
* NodeParts support several value types: primitives, Nodes, TemplateResults,
|
|
* as well as arrays and iterables of those types.
|
|
*/
|
|
class NodePart {
|
|
constructor(options) {
|
|
this.value = undefined;
|
|
this.__pendingValue = undefined;
|
|
this.options = options;
|
|
}
|
|
/**
|
|
* Appends 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 after the `ref` node (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());
|
|
}
|
|
/**
|
|
* Inserts this part after the `ref` part.
|
|
*
|
|
* 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() {
|
|
if (this.startNode.parentNode === null) {
|
|
return;
|
|
}
|
|
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 (isIterable(value)) {
|
|
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 `value` isn't already a string, we explicitly convert it here in case
|
|
// it can't be implicitly converted - i.e. it's a symbol.
|
|
const valueAsString = typeof value === 'string' ? value : String(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 = valueAsString;
|
|
}
|
|
else {
|
|
this.__commitNode(document.createTextNode(valueAsString));
|
|
}
|
|
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;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-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 third
|
|
// argument to add/removeEventListener is interpreted as the boolean capture
|
|
// value so we should only pass the `capture` property.
|
|
let eventOptionsSupported = false;
|
|
// Wrap into an IIFE because MS Edge <= v41 does not support having try/catch
|
|
// blocks right into the body of a module
|
|
(() => {
|
|
try {
|
|
const options = {
|
|
get capture() {
|
|
eventOptionsSupported = true;
|
|
return false;
|
|
}
|
|
};
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
window.addEventListener('test', options, options);
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
window.removeEventListener('test', options, options);
|
|
}
|
|
catch (_e) {
|
|
// event options not supported
|
|
}
|
|
})();
|
|
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
|
|
*/
|
|
/**
|
|
* 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 result or other value to a container.
|
|
*
|
|
* To update a container with new values, reevaluate the template literal and
|
|
* call `render` with the new result.
|
|
*
|
|
* @param result Any value renderable by NodePart - typically 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
|
|
*/
|
|
/**
|
|
* 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 committer = new PropertyCommitter(element, name.slice(1), strings);
|
|
return committer.parts;
|
|
}
|
|
if (prefix === '@') {
|
|
return [new EventPart(element, name.slice(1), options.eventContext)];
|
|
}
|
|
if (prefix === '?') {
|
|
return [new BooleanAttributePart(element, name.slice(1), strings)];
|
|
}
|
|
const committer = new AttributeCommitter(element, name, strings);
|
|
return committer.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
|
|
*/
|
|
// 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
|
|
if (typeof window !== 'undefined') {
|
|
(window['litHtmlVersions'] || (window['litHtmlVersions'] = [])).push('1.2.1');
|
|
}
|
|
/**
|
|
* 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
|
|
*/
|
|
// 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 = (scopeName, renderedDOM, template) => {
|
|
shadyRenderSet.add(scopeName);
|
|
// If `renderedDOM` is stamped from a Template, then we need to edit that
|
|
// Template's underlying template element. Otherwise, we create one here
|
|
// to give to ShadyCSS, which still requires one while scoping.
|
|
const templateElement = !!template ? template.element : document.createElement('template');
|
|
// Move styles out of rendered DOM and store.
|
|
const styles = renderedDOM.querySelectorAll('style');
|
|
const { length } = styles;
|
|
// If there are no styles, skip unnecessary work
|
|
if (length === 0) {
|
|
// Ensure prepareTemplateStyles is called to support adding
|
|
// styles via `prepareAdoptedCssText` since that requires that
|
|
// `prepareTemplateStyles` is called.
|
|
//
|
|
// ShadyCSS will only update styles containing @apply in the template
|
|
// given to `prepareTemplateStyles`. If no lit Template was given,
|
|
// ShadyCSS will not be able to update uses of @apply in any relevant
|
|
// template. However, this is not a problem because we only create the
|
|
// template for the purpose of supporting `prepareAdoptedCssText`,
|
|
// which doesn't support @apply at all.
|
|
window.ShadyCSS.prepareTemplateStyles(templateElement, 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 < 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`.
|
|
const content = templateElement.content;
|
|
if (!!template) {
|
|
insertNodeIntoTemplate(template, condensedStyle, content.firstChild);
|
|
}
|
|
else {
|
|
content.insertBefore(condensedStyle, 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(templateElement, scopeName);
|
|
const style = content.querySelector('style');
|
|
if (window.ShadyCSS.nativeShadow && style !== null) {
|
|
// When in native Shadow DOM, ensure the style created by ShadyCSS is
|
|
// included in initially rendered output (`renderedDOM`).
|
|
renderedDOM.insertBefore(style.cloneNode(true), renderedDOM.firstChild);
|
|
}
|
|
else if (!!template) {
|
|
// When no style is left in the template, 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.
|
|
// There can be no style in the template in 2 cases (1) when Shady DOM
|
|
// is in use, ShadyCSS removes all styles, (2) when native Shadow DOM
|
|
// is in use ShadyCSS removes the style if it contains no content.
|
|
// NOTE, ShadyCSS creates its own style so we can safely add/remove
|
|
// `condensedStyle` here.
|
|
content.insertBefore(condensedStyle, 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) => {
|
|
if (!options || typeof options !== 'object' || !options.scopeName) {
|
|
throw new Error('The `scopeName` option is required.');
|
|
}
|
|
const scopeName = options.scopeName;
|
|
const hasRendered = parts.has(container);
|
|
const needsScoping = compatibleShadyCSSVersion &&
|
|
container.nodeType === 11 /* Node.DOCUMENT_FRAGMENT_NODE */ &&
|
|
!!container.host;
|
|
// 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);
|
|
// ShadyCSS might have style sheets (e.g. from `prepareAdoptedCssText`)
|
|
// that should apply to `renderContainer` even if the rendered value is
|
|
// not a TemplateInstance. However, it will only insert scoped styles
|
|
// into the document if `prepareTemplateStyles` has already been called
|
|
// for the given scope name.
|
|
const template = part.value instanceof TemplateInstance ?
|
|
part.value.template :
|
|
undefined;
|
|
prepareTemplateStyles(scopeName, renderContainer, template);
|
|
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 `ShadyCSS.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
|
|
*/
|
|
var _a;
|
|
/**
|
|
* 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 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;
|
|
/**
|
|
* The Closure JS Compiler doesn't currently have good support for static
|
|
* property semantics where "this" is dynamic (e.g.
|
|
* https://github.com/google/closure-compiler/issues/3177 and others) so we use
|
|
* this hack to bypass any rewriting by the compiler.
|
|
*/
|
|
const finalized = 'finalized';
|
|
/**
|
|
* 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;
|
|
// Initialize to an unresolved Promise so we can make sure the element has
|
|
// connected before first update.
|
|
this._updatePromise = new Promise((res) => this._enableUpdatingResolver = res);
|
|
/**
|
|
* 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
|
|
* and stores a PropertyDeclaration for the property with the given options.
|
|
* 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.
|
|
*
|
|
* This method may be overridden to customize properties; however,
|
|
* when doing so, it's important to call `super.createProperty` to ensure
|
|
* the property is setup correctly. This method calls
|
|
* `getPropertyDescriptor` internally to get a descriptor to install.
|
|
* To customize what properties do when they are get or set, override
|
|
* `getPropertyDescriptor`. To customize the options for a property,
|
|
* implement `createProperty` like this:
|
|
*
|
|
* static createProperty(name, options) {
|
|
* options = Object.assign(options, {myOption: true});
|
|
* super.createProperty(name, options);
|
|
* }
|
|
*
|
|
* @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}`;
|
|
const descriptor = this.getPropertyDescriptor(name, key, options);
|
|
if (descriptor !== undefined) {
|
|
Object.defineProperty(this.prototype, name, descriptor);
|
|
}
|
|
}
|
|
/**
|
|
* Returns a property descriptor to be defined on the given named property.
|
|
* If no descriptor is returned, the property will not become an accessor.
|
|
* For example,
|
|
*
|
|
* class MyElement extends LitElement {
|
|
* static getPropertyDescriptor(name, key, options) {
|
|
* const defaultDescriptor =
|
|
* super.getPropertyDescriptor(name, key, options);
|
|
* const setter = defaultDescriptor.set;
|
|
* return {
|
|
* get: defaultDescriptor.get,
|
|
* set(value) {
|
|
* setter.call(this, value);
|
|
* // custom action.
|
|
* },
|
|
* configurable: true,
|
|
* enumerable: true
|
|
* }
|
|
* }
|
|
* }
|
|
*
|
|
* @nocollapse
|
|
*/
|
|
static getPropertyDescriptor(name, key, _options) {
|
|
return {
|
|
// tslint:disable-next-line:no-any no symbol in index
|
|
get() {
|
|
return this[key];
|
|
},
|
|
set(value) {
|
|
const oldValue = this[name];
|
|
this[key] = value;
|
|
this._requestUpdate(name, oldValue);
|
|
},
|
|
configurable: true,
|
|
enumerable: true
|
|
};
|
|
}
|
|
/**
|
|
* Returns the property options associated with the given property.
|
|
* These options are defined with a PropertyDeclaration via the `properties`
|
|
* object or the `@property` decorator and are registered in
|
|
* `createProperty(...)`.
|
|
*
|
|
* Note, this method should be considered "final" and not overridden. To
|
|
* customize the options for a given property, override `createProperty`.
|
|
*
|
|
* @nocollapse
|
|
* @final
|
|
*/
|
|
static getPropertyOptions(name) {
|
|
return this._classProperties && this._classProperties.get(name) ||
|
|
defaultPropertyDeclaration;
|
|
}
|
|
/**
|
|
* Creates property accessors for registered properties and ensures
|
|
* any superclasses are also finalized.
|
|
* @nocollapse
|
|
*/
|
|
static finalize() {
|
|
// finalize any superclasses
|
|
const superCtor = Object.getPrototypeOf(this);
|
|
if (!superCtor.hasOwnProperty(finalized)) {
|
|
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() {
|
|
// Ensure first connection completes an update. Updates cannot complete
|
|
// before connection.
|
|
this.enableUpdating();
|
|
}
|
|
enableUpdating() {
|
|
if (this._enableUpdatingResolver !== undefined) {
|
|
this._enableUpdatingResolver();
|
|
this._enableUpdatingResolver = 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;
|
|
// Note, hint this as an `AttributeMap` so closure clearly understands
|
|
// the type; it has issues with tracking types through statics
|
|
// tslint:disable-next-line:no-unnecessary-type-assertion
|
|
const propName = ctor._attributeToPropertyMap.get(name);
|
|
if (propName !== undefined) {
|
|
const options = ctor.getPropertyOptions(propName);
|
|
// 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.getPropertyOptions(name);
|
|
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._updatePromise = 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() {
|
|
this._updateState = this._updateState | STATE_UPDATE_REQUESTED;
|
|
try {
|
|
// Ensure any previous update has resolved before updating.
|
|
// This `await` also ensures that property changes are batched.
|
|
await this._updatePromise;
|
|
}
|
|
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.
|
|
}
|
|
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;
|
|
}
|
|
return !this._hasRequestedUpdate;
|
|
}
|
|
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);
|
|
}
|
|
else {
|
|
this._markUpdated();
|
|
}
|
|
}
|
|
catch (e) {
|
|
// Prevent `firstUpdated` and `updated` from running when there's an
|
|
// update exception.
|
|
shouldUpdate = false;
|
|
// Ensure element can accept additional updates after an exception.
|
|
this._markUpdated();
|
|
throw e;
|
|
}
|
|
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.
|
|
*
|
|
* To await additional asynchronous work, override the `_getUpdateComplete`
|
|
* method. For example, it is sometimes useful to await a rendered element
|
|
* before fulfilling this Promise. To do this, first await
|
|
* `super._getUpdateComplete()`, 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._getUpdateComplete();
|
|
}
|
|
/**
|
|
* Override point for the `updateComplete` promise.
|
|
*
|
|
* It is not safe to override the `updateComplete` getter directly due to a
|
|
* limitation in TypeScript which means it is not possible to call a
|
|
* superclass getter (e.g. `super.updateComplete.then(...)`) when the target
|
|
* language is ES5 (https://github.com/microsoft/TypeScript/issues/338).
|
|
* This method should be overridden instead. For example:
|
|
*
|
|
* class MyElement extends LitElement {
|
|
* async _getUpdateComplete() {
|
|
* await super._getUpdateComplete();
|
|
* await this._myChild.updateComplete;
|
|
* }
|
|
* }
|
|
*/
|
|
_getUpdateComplete() {
|
|
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;
|
|
}
|
|
this._markUpdated();
|
|
}
|
|
/**
|
|
* 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) {
|
|
}
|
|
}
|
|
_a = finalized;
|
|
/**
|
|
* Marks class as having finished creating properties.
|
|
*/
|
|
UpdatingElement[_a] = 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.
|
|
*
|
|
* ```
|
|
* @customElement('my-element')
|
|
* class MyElement {
|
|
* render() {
|
|
* return html``;
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* @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(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,
|
|
// }
|
|
// ],
|
|
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.
|
|
*
|
|
* This decorator should only be used for public fields. Private or protected
|
|
* fields should use the internalProperty decorator.
|
|
*
|
|
* @example
|
|
*
|
|
* class MyElement {
|
|
* @property({ type: Boolean })
|
|
* clicked = false;
|
|
* }
|
|
*
|
|
* @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 if (typeof value === 'number') {
|
|
return value;
|
|
}
|
|
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.3.1');
|
|
/**
|
|
* Sentinal value used to avoid calling lit-html's render function when
|
|
* subclasses do not implement `render`
|
|
*/
|
|
const renderNotImplemented = {};
|
|
class LitElement extends UpdatingElement {
|
|
/**
|
|
* Return the array of styles to apply to the element.
|
|
* Override this method to integrate into a style management system.
|
|
*
|
|
* @nocollapse
|
|
*/
|
|
static getStyles() {
|
|
return this.styles;
|
|
}
|
|
/** @nocollapse */
|
|
static _getUniqueStyles() {
|
|
// Only gather styles once per class
|
|
if (this.hasOwnProperty(JSCompiler_renameProperty('_styles', this))) {
|
|
return;
|
|
}
|
|
// Take care not to call `this.getStyles()` 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.getStyles();
|
|
if (userStyles === undefined) {
|
|
this._styles = [];
|
|
}
|
|
else if (Array.isArray(userStyles)) {
|
|
// De-duplicate styles preserving the _last_ instance in the set.
|
|
// This is a performance optimization to avoid duplicated styles that can
|
|
// occur especially when composing via subclassing.
|
|
// The last item is kept to try to preserve the cascade order with the
|
|
// assumption that it's most important that last added styles override
|
|
// previous styles.
|
|
const addStyles = (styles, set) => styles.reduceRight((set, s) =>
|
|
// Note: On IE set.add() does not return the set
|
|
Array.isArray(s) ? addStyles(s, set) : (set.add(s), set), set);
|
|
// Array.from does not work on Set in IE, otherwise return
|
|
// Array.from(addStyles(userStyles, new Set<CSSResult>())).reverse()
|
|
const set = addStyles(userStyles, new Set());
|
|
const styles = [];
|
|
set.forEach((v) => styles.unshift(v));
|
|
this._styles = styles;
|
|
}
|
|
else {
|
|
this._styles = [userStyles];
|
|
}
|
|
}
|
|
/**
|
|
* 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.constructor._getUniqueStyles();
|
|
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) {
|
|
// Setting properties in `render` should not trigger an update. Since
|
|
// updates are allowed after super.update, it's important to call `render`
|
|
// before that.
|
|
const templateResult = this.render();
|
|
super.update(changedProperties);
|
|
// If render is not implemented by the component, don't call lit-html render
|
|
if (templateResult !== renderNotImplemented) {
|
|
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 may return
|
|
* any value renderable by lit-html's NodePart - typically a TemplateResult.
|
|
* Setting properties inside this method will *not* trigger the element to
|
|
* update.
|
|
*/
|
|
render() {
|
|
return renderNotImplemented;
|
|
}
|
|
}
|
|
/**
|
|
* Ensure this class is marked as `finalized` as an optimization ensuring
|
|
* it will not needlessly try to `finalize`.
|
|
*
|
|
* Note this property name is a string to prevent breaking Closure JS Compiler
|
|
* optimizations. See updating-element.ts for more information.
|
|
*/
|
|
LitElement['finalized'] = true;
|
|
/**
|
|
* Render method used to render the value to the element's DOM.
|
|
* @param result The value to render.
|
|
* @param container Node into which to render.
|
|
* @param options Element name.
|
|
* @nocollapse
|
|
*/
|
|
LitElement.render = render$1;
|
|
|
|
var token = /d{1,4}|M{1,4}|YY(?:YY)?|S{1,3}|Do|ZZ|Z|([HhMsDm])\1?|[aA]|"[^"]*"|'[^']*'/g;
|
|
var twoDigitsOptional = "[1-9]\\d?";
|
|
var twoDigits = "\\d\\d";
|
|
var threeDigits = "\\d{3}";
|
|
var fourDigits = "\\d{4}";
|
|
var word = "[^\\s]+";
|
|
var literal = /\[([^]*?)\]/gm;
|
|
function shorten(arr, sLen) {
|
|
var newArr = [];
|
|
for (var i = 0, len = arr.length; i < len; i++) {
|
|
newArr.push(arr[i].substr(0, sLen));
|
|
}
|
|
return newArr;
|
|
}
|
|
var monthUpdate = function (arrName) { return function (v, i18n) {
|
|
var lowerCaseArr = i18n[arrName].map(function (v) { return v.toLowerCase(); });
|
|
var index = lowerCaseArr.indexOf(v.toLowerCase());
|
|
if (index > -1) {
|
|
return index;
|
|
}
|
|
return null;
|
|
}; };
|
|
function assign(origObj) {
|
|
var args = [];
|
|
for (var _i = 1; _i < arguments.length; _i++) {
|
|
args[_i - 1] = arguments[_i];
|
|
}
|
|
for (var _a = 0, args_1 = args; _a < args_1.length; _a++) {
|
|
var obj = args_1[_a];
|
|
for (var key in obj) {
|
|
// @ts-ignore ex
|
|
origObj[key] = obj[key];
|
|
}
|
|
}
|
|
return origObj;
|
|
}
|
|
var dayNames = [
|
|
"Sunday",
|
|
"Monday",
|
|
"Tuesday",
|
|
"Wednesday",
|
|
"Thursday",
|
|
"Friday",
|
|
"Saturday"
|
|
];
|
|
var monthNames = [
|
|
"January",
|
|
"February",
|
|
"March",
|
|
"April",
|
|
"May",
|
|
"June",
|
|
"July",
|
|
"August",
|
|
"September",
|
|
"October",
|
|
"November",
|
|
"December"
|
|
];
|
|
var monthNamesShort = shorten(monthNames, 3);
|
|
var dayNamesShort = shorten(dayNames, 3);
|
|
var defaultI18n = {
|
|
dayNamesShort: dayNamesShort,
|
|
dayNames: dayNames,
|
|
monthNamesShort: monthNamesShort,
|
|
monthNames: monthNames,
|
|
amPm: ["am", "pm"],
|
|
DoFn: function (dayOfMonth) {
|
|
return (dayOfMonth +
|
|
["th", "st", "nd", "rd"][dayOfMonth % 10 > 3
|
|
? 0
|
|
: ((dayOfMonth - (dayOfMonth % 10) !== 10 ? 1 : 0) * dayOfMonth) % 10]);
|
|
}
|
|
};
|
|
var globalI18n = assign({}, defaultI18n);
|
|
var setGlobalDateI18n = function (i18n) {
|
|
return (globalI18n = assign(globalI18n, i18n));
|
|
};
|
|
var regexEscape = function (str) {
|
|
return str.replace(/[|\\{()[^$+*?.-]/g, "\\$&");
|
|
};
|
|
var pad = function (val, len) {
|
|
if (len === void 0) { len = 2; }
|
|
val = String(val);
|
|
while (val.length < len) {
|
|
val = "0" + val;
|
|
}
|
|
return val;
|
|
};
|
|
var formatFlags = {
|
|
D: function (dateObj) { return String(dateObj.getDate()); },
|
|
DD: function (dateObj) { return pad(dateObj.getDate()); },
|
|
Do: function (dateObj, i18n) {
|
|
return i18n.DoFn(dateObj.getDate());
|
|
},
|
|
d: function (dateObj) { return String(dateObj.getDay()); },
|
|
dd: function (dateObj) { return pad(dateObj.getDay()); },
|
|
ddd: function (dateObj, i18n) {
|
|
return i18n.dayNamesShort[dateObj.getDay()];
|
|
},
|
|
dddd: function (dateObj, i18n) {
|
|
return i18n.dayNames[dateObj.getDay()];
|
|
},
|
|
M: function (dateObj) { return String(dateObj.getMonth() + 1); },
|
|
MM: function (dateObj) { return pad(dateObj.getMonth() + 1); },
|
|
MMM: function (dateObj, i18n) {
|
|
return i18n.monthNamesShort[dateObj.getMonth()];
|
|
},
|
|
MMMM: function (dateObj, i18n) {
|
|
return i18n.monthNames[dateObj.getMonth()];
|
|
},
|
|
YY: function (dateObj) {
|
|
return pad(String(dateObj.getFullYear()), 4).substr(2);
|
|
},
|
|
YYYY: function (dateObj) { return pad(dateObj.getFullYear(), 4); },
|
|
h: function (dateObj) { return String(dateObj.getHours() % 12 || 12); },
|
|
hh: function (dateObj) { return pad(dateObj.getHours() % 12 || 12); },
|
|
H: function (dateObj) { return String(dateObj.getHours()); },
|
|
HH: function (dateObj) { return pad(dateObj.getHours()); },
|
|
m: function (dateObj) { return String(dateObj.getMinutes()); },
|
|
mm: function (dateObj) { return pad(dateObj.getMinutes()); },
|
|
s: function (dateObj) { return String(dateObj.getSeconds()); },
|
|
ss: function (dateObj) { return pad(dateObj.getSeconds()); },
|
|
S: function (dateObj) {
|
|
return String(Math.round(dateObj.getMilliseconds() / 100));
|
|
},
|
|
SS: function (dateObj) {
|
|
return pad(Math.round(dateObj.getMilliseconds() / 10), 2);
|
|
},
|
|
SSS: function (dateObj) { return pad(dateObj.getMilliseconds(), 3); },
|
|
a: function (dateObj, i18n) {
|
|
return dateObj.getHours() < 12 ? i18n.amPm[0] : i18n.amPm[1];
|
|
},
|
|
A: function (dateObj, i18n) {
|
|
return dateObj.getHours() < 12
|
|
? i18n.amPm[0].toUpperCase()
|
|
: i18n.amPm[1].toUpperCase();
|
|
},
|
|
ZZ: function (dateObj) {
|
|
var offset = dateObj.getTimezoneOffset();
|
|
return ((offset > 0 ? "-" : "+") +
|
|
pad(Math.floor(Math.abs(offset) / 60) * 100 + (Math.abs(offset) % 60), 4));
|
|
},
|
|
Z: function (dateObj) {
|
|
var offset = dateObj.getTimezoneOffset();
|
|
return ((offset > 0 ? "-" : "+") +
|
|
pad(Math.floor(Math.abs(offset) / 60), 2) +
|
|
":" +
|
|
pad(Math.abs(offset) % 60, 2));
|
|
}
|
|
};
|
|
var monthParse = function (v) { return +v - 1; };
|
|
var emptyDigits = [null, twoDigitsOptional];
|
|
var emptyWord = [null, word];
|
|
var amPm = [
|
|
"isPm",
|
|
word,
|
|
function (v, i18n) {
|
|
var val = v.toLowerCase();
|
|
if (val === i18n.amPm[0]) {
|
|
return 0;
|
|
}
|
|
else if (val === i18n.amPm[1]) {
|
|
return 1;
|
|
}
|
|
return null;
|
|
}
|
|
];
|
|
var timezoneOffset = [
|
|
"timezoneOffset",
|
|
"[^\\s]*?[\\+\\-]\\d\\d:?\\d\\d|[^\\s]*?Z?",
|
|
function (v) {
|
|
var parts = (v + "").match(/([+-]|\d\d)/gi);
|
|
if (parts) {
|
|
var minutes = +parts[1] * 60 + parseInt(parts[2], 10);
|
|
return parts[0] === "+" ? minutes : -minutes;
|
|
}
|
|
return 0;
|
|
}
|
|
];
|
|
var parseFlags = {
|
|
D: ["day", twoDigitsOptional],
|
|
DD: ["day", twoDigits],
|
|
Do: ["day", twoDigitsOptional + word, function (v) { return parseInt(v, 10); }],
|
|
M: ["month", twoDigitsOptional, monthParse],
|
|
MM: ["month", twoDigits, monthParse],
|
|
YY: [
|
|
"year",
|
|
twoDigits,
|
|
function (v) {
|
|
var now = new Date();
|
|
var cent = +("" + now.getFullYear()).substr(0, 2);
|
|
return +("" + (+v > 68 ? cent - 1 : cent) + v);
|
|
}
|
|
],
|
|
h: ["hour", twoDigitsOptional, undefined, "isPm"],
|
|
hh: ["hour", twoDigits, undefined, "isPm"],
|
|
H: ["hour", twoDigitsOptional],
|
|
HH: ["hour", twoDigits],
|
|
m: ["minute", twoDigitsOptional],
|
|
mm: ["minute", twoDigits],
|
|
s: ["second", twoDigitsOptional],
|
|
ss: ["second", twoDigits],
|
|
YYYY: ["year", fourDigits],
|
|
S: ["millisecond", "\\d", function (v) { return +v * 100; }],
|
|
SS: ["millisecond", twoDigits, function (v) { return +v * 10; }],
|
|
SSS: ["millisecond", threeDigits],
|
|
d: emptyDigits,
|
|
dd: emptyDigits,
|
|
ddd: emptyWord,
|
|
dddd: emptyWord,
|
|
MMM: ["month", word, monthUpdate("monthNamesShort")],
|
|
MMMM: ["month", word, monthUpdate("monthNames")],
|
|
a: amPm,
|
|
A: amPm,
|
|
ZZ: timezoneOffset,
|
|
Z: timezoneOffset
|
|
};
|
|
// Some common format strings
|
|
var globalMasks = {
|
|
default: "ddd MMM DD YYYY HH:mm:ss",
|
|
shortDate: "M/D/YY",
|
|
mediumDate: "MMM D, YYYY",
|
|
longDate: "MMMM D, YYYY",
|
|
fullDate: "dddd, MMMM D, YYYY",
|
|
isoDate: "YYYY-MM-DD",
|
|
isoDateTime: "YYYY-MM-DDTHH:mm:ssZ",
|
|
shortTime: "HH:mm",
|
|
mediumTime: "HH:mm:ss",
|
|
longTime: "HH:mm:ss.SSS"
|
|
};
|
|
var setGlobalDateMasks = function (masks) { return assign(globalMasks, masks); };
|
|
/***
|
|
* Format a date
|
|
* @method format
|
|
* @param {Date|number} dateObj
|
|
* @param {string} mask Format of the date, i.e. 'mm-dd-yy' or 'shortDate'
|
|
* @returns {string} Formatted date string
|
|
*/
|
|
var format = function (dateObj, mask, i18n) {
|
|
if (mask === void 0) { mask = globalMasks["default"]; }
|
|
if (i18n === void 0) { i18n = {}; }
|
|
if (typeof dateObj === "number") {
|
|
dateObj = new Date(dateObj);
|
|
}
|
|
if (Object.prototype.toString.call(dateObj) !== "[object Date]" ||
|
|
isNaN(dateObj.getTime())) {
|
|
throw new Error("Invalid Date pass to format");
|
|
}
|
|
mask = globalMasks[mask] || mask;
|
|
var literals = [];
|
|
// Make literals inactive by replacing them with @@@
|
|
mask = mask.replace(literal, function ($0, $1) {
|
|
literals.push($1);
|
|
return "@@@";
|
|
});
|
|
var combinedI18nSettings = assign(assign({}, globalI18n), i18n);
|
|
// Apply formatting rules
|
|
mask = mask.replace(token, function ($0) {
|
|
return formatFlags[$0](dateObj, combinedI18nSettings);
|
|
});
|
|
// Inline literal values back into the formatted value
|
|
return mask.replace(/@@@/g, function () { return literals.shift(); });
|
|
};
|
|
/**
|
|
* Parse a date string into a Javascript Date object /
|
|
* @method parse
|
|
* @param {string} dateStr Date string
|
|
* @param {string} format Date parse format
|
|
* @param {i18n} I18nSettingsOptional Full or subset of I18N settings
|
|
* @returns {Date|null} Returns Date object. Returns null what date string is invalid or doesn't match format
|
|
*/
|
|
function parse(dateStr, format, i18n) {
|
|
if (i18n === void 0) { i18n = {}; }
|
|
if (typeof format !== "string") {
|
|
throw new Error("Invalid format in fecha parse");
|
|
}
|
|
// Check to see if the format is actually a mask
|
|
format = globalMasks[format] || format;
|
|
// Avoid regular expression denial of service, fail early for really long strings
|
|
// https://www.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoS
|
|
if (dateStr.length > 1000) {
|
|
return null;
|
|
}
|
|
// Default to the beginning of the year.
|
|
var today = new Date();
|
|
var dateInfo = {
|
|
year: today.getFullYear(),
|
|
month: 0,
|
|
day: 1,
|
|
hour: 0,
|
|
minute: 0,
|
|
second: 0,
|
|
millisecond: 0,
|
|
isPm: null,
|
|
timezoneOffset: null
|
|
};
|
|
var parseInfo = [];
|
|
var literals = [];
|
|
// Replace all the literals with @@@. Hopefully a string that won't exist in the format
|
|
var newFormat = format.replace(literal, function ($0, $1) {
|
|
literals.push(regexEscape($1));
|
|
return "@@@";
|
|
});
|
|
var specifiedFields = {};
|
|
var requiredFields = {};
|
|
// Change every token that we find into the correct regex
|
|
newFormat = regexEscape(newFormat).replace(token, function ($0) {
|
|
var info = parseFlags[$0];
|
|
var field = info[0], regex = info[1], requiredField = info[3];
|
|
// Check if the person has specified the same field twice. This will lead to confusing results.
|
|
if (specifiedFields[field]) {
|
|
throw new Error("Invalid format. " + field + " specified twice in format");
|
|
}
|
|
specifiedFields[field] = true;
|
|
// Check if there are any required fields. For instance, 12 hour time requires AM/PM specified
|
|
if (requiredField) {
|
|
requiredFields[requiredField] = true;
|
|
}
|
|
parseInfo.push(info);
|
|
return "(" + regex + ")";
|
|
});
|
|
// Check all the required fields are present
|
|
Object.keys(requiredFields).forEach(function (field) {
|
|
if (!specifiedFields[field]) {
|
|
throw new Error("Invalid format. " + field + " is required in specified format");
|
|
}
|
|
});
|
|
// Add back all the literals after
|
|
newFormat = newFormat.replace(/@@@/g, function () { return literals.shift(); });
|
|
// Check if the date string matches the format. If it doesn't return null
|
|
var matches = dateStr.match(new RegExp(newFormat, "i"));
|
|
if (!matches) {
|
|
return null;
|
|
}
|
|
var combinedI18nSettings = assign(assign({}, globalI18n), i18n);
|
|
// For each match, call the parser function for that date part
|
|
for (var i = 1; i < matches.length; i++) {
|
|
var _a = parseInfo[i - 1], field = _a[0], parser = _a[2];
|
|
var value = parser
|
|
? parser(matches[i], combinedI18nSettings)
|
|
: +matches[i];
|
|
// If the parser can't make sense of the value, return null
|
|
if (value == null) {
|
|
return null;
|
|
}
|
|
dateInfo[field] = value;
|
|
}
|
|
if (dateInfo.isPm === 1 && dateInfo.hour != null && +dateInfo.hour !== 12) {
|
|
dateInfo.hour = +dateInfo.hour + 12;
|
|
}
|
|
else if (dateInfo.isPm === 0 && +dateInfo.hour === 12) {
|
|
dateInfo.hour = 0;
|
|
}
|
|
var dateWithoutTZ = new Date(dateInfo.year, dateInfo.month, dateInfo.day, dateInfo.hour, dateInfo.minute, dateInfo.second, dateInfo.millisecond);
|
|
var validateFields = [
|
|
["month", "getMonth"],
|
|
["day", "getDate"],
|
|
["hour", "getHours"],
|
|
["minute", "getMinutes"],
|
|
["second", "getSeconds"]
|
|
];
|
|
for (var i = 0, len = validateFields.length; i < len; i++) {
|
|
// Check to make sure the date field is within the allowed range. Javascript dates allows values
|
|
// outside the allowed range. If the values don't match the value was invalid
|
|
if (specifiedFields[validateFields[i][0]] &&
|
|
dateInfo[validateFields[i][0]] !== dateWithoutTZ[validateFields[i][1]]()) {
|
|
return null;
|
|
}
|
|
}
|
|
if (dateInfo.timezoneOffset == null) {
|
|
return dateWithoutTZ;
|
|
}
|
|
return new Date(Date.UTC(dateInfo.year, dateInfo.month, dateInfo.day, dateInfo.hour, dateInfo.minute - dateInfo.timezoneOffset, dateInfo.second, dateInfo.millisecond));
|
|
}
|
|
var fecha = {
|
|
format: format,
|
|
parse: parse,
|
|
defaultI18n: defaultI18n,
|
|
setGlobalDateI18n: setGlobalDateI18n,
|
|
setGlobalDateMasks: setGlobalDateMasks
|
|
};
|
|
|
|
var a=function(){try{(new Date).toLocaleDateString("i");}catch(e){return "RangeError"===e.name}return !1}()?function(e,t){return e.toLocaleDateString(t,{year:"numeric",month:"long",day:"numeric"})}:function(t){return fecha.format(t,"mediumDate")},r=function(){try{(new Date).toLocaleString("i");}catch(e){return "RangeError"===e.name}return !1}()?function(e,t){return e.toLocaleString(t,{year:"numeric",month:"long",day:"numeric",hour:"numeric",minute:"2-digit"})}:function(t){return fecha.format(t,"haDateTime")},n=function(){try{(new Date).toLocaleTimeString("i");}catch(e){return "RangeError"===e.name}return !1}()?function(e,t){return e.toLocaleTimeString(t,{hour:"numeric",minute:"2-digit"})}:function(t){return fecha.format(t,"shortTime")};function d(e){return e.substr(0,e.indexOf("."))}var _="hass:bookmark",D=["closed","locked","off"],C=function(e,t,a,r){r=r||{},a=null==a?{}:a;var n=new Event(t,{bubbles:void 0===r.bubbles||r.bubbles,cancelable:Boolean(r.cancelable),composed:void 0===r.composed||r.composed});return n.detail=a,e.dispatchEvent(n),n},N={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 O(e,t){if(e in N)return N[e];switch(e){case"alarm_control_panel":switch(t){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 t&&"off"===t?"hass:radiobox-blank":"hass:checkbox-marked-circle";case"cover":return "closed"===t?"hass:window-closed":"hass:window-open";case"lock":return t&&"unlocked"===t?"hass:lock-open":"hass:lock";case"media_player":return t&&"off"!==t&&"idle"!==t?"hass:cast-connected":"hass:cast";case"zwave":switch(t){case"dead":return "hass:emoticon-dead";case"sleeping":return "hass:sleep";case"initializing":return "hass:timer-sand";default:return "hass:z-wave"}default:return console.warn("Unable to find icon for domain "+e+" ("+t+")"),_}}var z=function(e){C(window,"haptic",e);},F=function(e,t,a){void 0===a&&(a=!1),a?history.replaceState(null,"",t):history.pushState(null,"",t),C(window,"location-changed",{replace:a});},I=function(e,t,a){void 0===a&&(a=!0);var r,n=d(t),s="group"===n?"homeassistant":n;switch(n){case"lock":r=a?"unlock":"lock";break;case"cover":r=a?"open_cover":"close_cover";break;default:r=a?"turn_on":"turn_off";}return e.callService(s,r,{entity_id:t})},B=function(e,t){var a=D.includes(e.states[t].state);return I(e,t,a)},U=function(e,t,a,r){var n;if("double_tap"===r&&a.double_tap_action?n=a.double_tap_action:"hold"===r&&a.hold_action?n=a.hold_action:"tap"===r&&a.tap_action&&(n=a.tap_action),n||(n={action:"more-info"}),!n.confirmation||n.confirmation.exemptions&&n.confirmation.exemptions.some(function(e){return e.user===t.user.id})||(z("warning"),confirm(n.confirmation.text||"Are you sure you want to "+n.action+"?")))switch(n.action){case"more-info":(a.entity||a.camera_image)&&C(e,"hass-more-info",{entityId:a.entity?a.entity:a.camera_image});break;case"navigate":n.navigation_path&&F(0,n.navigation_path);break;case"url":n.url_path&&window.open(n.url_path);break;case"toggle":a.entity&&(B(t,a.entity),z("success"));break;case"call-service":if(!n.service)return void z("failure");var s=n.service.split(".",2);t.callService(s[0],s[1],n.service_data),z("success");}};function W(e){return void 0!==e&&"none"!==e.action}
|
|
|
|
/**
|
|
* Performs a deep merge of objects and returns new object. Does not modify
|
|
* objects (immutable) and merges arrays via concatenation and filtering.
|
|
*
|
|
* @param {...object} objects - Objects to merge
|
|
* @returns {object} New object with merged key/values
|
|
*/
|
|
function mergeDeep(...objects) {
|
|
const isObject = (obj) => obj && typeof obj === 'object';
|
|
return objects.reduce((prev, obj) => {
|
|
Object.keys(obj).forEach(key => {
|
|
const pVal = prev[key];
|
|
const oVal = obj[key];
|
|
if (Array.isArray(pVal) && Array.isArray(oVal)) {
|
|
/* eslint no-param-reassign: 0 */
|
|
prev[key] = pVal.concat(...oVal);
|
|
}
|
|
else if (isObject(pVal) && isObject(oVal)) {
|
|
prev[key] = mergeDeep(pVal, oVal);
|
|
}
|
|
else {
|
|
prev[key] = oVal;
|
|
}
|
|
});
|
|
return prev;
|
|
}, {});
|
|
}
|
|
// Check if config or Entity changed
|
|
function hasConfigOrEntitiesChanged(element, changedProps, forceUpdate) {
|
|
if (changedProps.has('config') || forceUpdate) {
|
|
return true;
|
|
}
|
|
for (const config of element._configArray) {
|
|
if (config.entity) {
|
|
const oldHass = changedProps.get('hass');
|
|
if (oldHass) {
|
|
if (oldHass.states[config.entity] !== element.hass.states[config.entity]) {
|
|
return true;
|
|
}
|
|
else {
|
|
continue;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function createConfigArray(config) {
|
|
const configArray = [];
|
|
if (config.entities) {
|
|
for (const entityConfig of config.entities) {
|
|
if (typeof entityConfig == 'string') {
|
|
const clonedObject = mergeDeep({}, config);
|
|
delete clonedObject.entities;
|
|
const stringConfig = mergeDeep(clonedObject, { entity: entityConfig });
|
|
configArray.push(stringConfig);
|
|
}
|
|
else if (typeof entityConfig == 'object') {
|
|
const clonedObject = mergeDeep({}, config);
|
|
delete clonedObject.entities;
|
|
const objectConfig = mergeDeep(clonedObject, entityConfig);
|
|
configArray.push(objectConfig);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
configArray.push(config);
|
|
}
|
|
return configArray;
|
|
}
|
|
function createEditorConfigArray(config) {
|
|
const configArray = [];
|
|
if (config.entities) {
|
|
for (const entityConfig of config.entities) {
|
|
if (typeof entityConfig == 'string') {
|
|
const stringConfig = mergeDeep({}, { entity: entityConfig });
|
|
configArray.push(stringConfig);
|
|
}
|
|
else if (typeof entityConfig == 'object') {
|
|
const objectConfig = mergeDeep({}, entityConfig);
|
|
configArray.push(objectConfig);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
configArray.push(config);
|
|
}
|
|
return configArray;
|
|
}
|
|
function arrayMove(arr, fromIndex, toIndex) {
|
|
const element = arr[fromIndex];
|
|
const newArray = arr.slice();
|
|
newArray.splice(fromIndex, 1);
|
|
newArray.splice(toIndex, 0, element);
|
|
return newArray;
|
|
}
|
|
|
|
let BarCardEditor = class BarCardEditor extends LitElement {
|
|
constructor() {
|
|
super(...arguments);
|
|
this._configArray = [];
|
|
this._entityOptionsArray = [];
|
|
}
|
|
shouldUpdate(changedProps) {
|
|
return hasConfigOrEntitiesChanged(this, changedProps, true);
|
|
}
|
|
setConfig(config) {
|
|
this._config = Object.assign({}, config);
|
|
if (!config.entity && !config.entities) {
|
|
this._config.entity = 'none';
|
|
}
|
|
if (this._config.entity) {
|
|
this._configArray.push({ entity: config.entity });
|
|
this._config.entities = [{ entity: config.entity }];
|
|
delete this._config.entity;
|
|
}
|
|
this._configArray = createEditorConfigArray(this._config);
|
|
if (this._config.animation) {
|
|
if (Object.entries(this._config.animation).length === 0) {
|
|
delete this._config.animation;
|
|
C(this, 'config-changed', { config: this._config });
|
|
}
|
|
}
|
|
if (this._config.positions) {
|
|
if (Object.entries(this._config.positions).length === 0) {
|
|
delete this._config.positions;
|
|
C(this, 'config-changed', { config: this._config });
|
|
}
|
|
}
|
|
for (const entityConfig of this._configArray) {
|
|
if (entityConfig.animation) {
|
|
if (Object.entries(entityConfig.animation).length === 0) {
|
|
delete entityConfig.animation;
|
|
}
|
|
}
|
|
if (entityConfig.positions) {
|
|
if (Object.entries(entityConfig.positions).length === 0) {
|
|
delete entityConfig.positions;
|
|
}
|
|
}
|
|
}
|
|
this._config.entities = this._configArray;
|
|
C(this, 'config-changed', { config: this._config });
|
|
const barOptions = {
|
|
icon: 'format-list-numbered',
|
|
name: 'Bar',
|
|
secondary: 'Bar settings.',
|
|
show: false,
|
|
};
|
|
const valueOptions = {
|
|
icon: 'numeric',
|
|
name: 'Value',
|
|
secondary: 'Value settings.',
|
|
show: false,
|
|
};
|
|
const cardOptions = {
|
|
icon: 'card-bulleted',
|
|
name: 'Card',
|
|
secondary: 'Card settings.',
|
|
show: false,
|
|
};
|
|
const positionsOptions = {
|
|
icon: 'arrow-expand-horizontal',
|
|
name: 'Positions',
|
|
secondary: 'Set positions of card elements.',
|
|
show: false,
|
|
};
|
|
const actionsOptions = {
|
|
icon: 'gesture-tap',
|
|
name: 'Actions',
|
|
secondary: 'Coming soon... Use code editor for Actions.',
|
|
show: false,
|
|
};
|
|
const severityOptions = {
|
|
icon: 'exclamation-thick',
|
|
name: 'Severity',
|
|
secondary: 'Define bar colors based on value.',
|
|
show: false,
|
|
};
|
|
const animationOptions = {
|
|
icon: 'animation',
|
|
name: 'Animation',
|
|
secondary: 'Define animation settings.',
|
|
show: false,
|
|
};
|
|
const entityOptions = {
|
|
show: false,
|
|
options: {
|
|
positions: Object.assign({}, positionsOptions),
|
|
bar: Object.assign({}, barOptions),
|
|
value: Object.assign({}, valueOptions),
|
|
severity: Object.assign({}, severityOptions),
|
|
actions: Object.assign({}, actionsOptions),
|
|
animation: Object.assign({}, animationOptions),
|
|
},
|
|
};
|
|
for (const config of this._configArray) {
|
|
this._entityOptionsArray.push(Object.assign({}, entityOptions));
|
|
}
|
|
if (!this._options) {
|
|
this._options = {
|
|
entities: {
|
|
icon: 'tune',
|
|
name: 'Entities',
|
|
secondary: 'Manage card entities.',
|
|
show: true,
|
|
options: {
|
|
entities: this._entityOptionsArray,
|
|
},
|
|
},
|
|
appearance: {
|
|
icon: 'palette',
|
|
name: 'Appearance',
|
|
secondary: 'Customize the global name, icon, etc.',
|
|
show: false,
|
|
options: {
|
|
positions: positionsOptions,
|
|
bar: barOptions,
|
|
value: valueOptions,
|
|
card: cardOptions,
|
|
severity: severityOptions,
|
|
animation: animationOptions,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
}
|
|
render() {
|
|
return html `
|
|
${this._createEntitiesElement()} ${this._createAppearanceElement()}
|
|
`;
|
|
}
|
|
_createActionsElement(index) {
|
|
const options = this._options.entities.options.entities[index].options.actions;
|
|
return html `
|
|
<div class="sub-category" style="opacity: 0.5;">
|
|
<div>
|
|
<div class="row">
|
|
<ha-icon .icon=${`mdi:${options.icon}`}></ha-icon>
|
|
<div class="title">${options.name}</div>
|
|
</div>
|
|
<div class="secondary">${options.secondary}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
_createEntitiesValues() {
|
|
if (!this.hass || !this._config) {
|
|
return [html ``];
|
|
}
|
|
const options = this._options.entities;
|
|
const entities = Object.keys(this.hass.states);
|
|
const valueElementArray = [];
|
|
for (const config of this._configArray) {
|
|
const index = this._configArray.indexOf(config);
|
|
valueElementArray.push(html `
|
|
<div class="sub-category" style="display: flex; flex-direction: row; align-items: center;">
|
|
<div style="display: flex; align-items: center; flex-direction: column;">
|
|
<div
|
|
style="font-size: 10px; margin-bottom: -8px; opacity: 0.5;"
|
|
@click=${this._toggleThing}
|
|
.options=${options.options.entities[index]}
|
|
.optionsTarget=${options.options.entities}
|
|
.index=${index}
|
|
>
|
|
options
|
|
</div>
|
|
<ha-icon
|
|
icon="mdi:chevron-${options.options.entities[index].show ? 'up' : 'down'}"
|
|
@click=${this._toggleThing}
|
|
.options=${options.options.entities[index]}
|
|
.optionsTarget=${options.options.entities}
|
|
.index=${index}
|
|
></ha-icon>
|
|
</div>
|
|
<div class="value" style="flex-grow: 1;">
|
|
<paper-input
|
|
label="Entity"
|
|
@value-changed=${this._valueChanged}
|
|
.configAttribute=${'entity'}
|
|
.configObject=${this._configArray[index]}
|
|
.value=${config.entity}
|
|
>
|
|
</paper-input>
|
|
</div>
|
|
${index !== 0
|
|
? html `
|
|
<ha-icon
|
|
class="ha-icon-large"
|
|
icon="mdi:arrow-up"
|
|
@click=${this._moveEntity}
|
|
.configDirection=${'up'}
|
|
.configArray=${this._config.entities}
|
|
.arrayAttribute=${'entities'}
|
|
.arraySource=${this._config}
|
|
.index=${index}
|
|
></ha-icon>
|
|
`
|
|
: html `
|
|
<ha-icon icon="mdi:arrow-up" style="opacity: 25%;" class="ha-icon-large"></ha-icon>
|
|
`}
|
|
${index !== this._configArray.length - 1
|
|
? html `
|
|
<ha-icon
|
|
class="ha-icon-large"
|
|
icon="mdi:arrow-down"
|
|
@click=${this._moveEntity}
|
|
.configDirection=${'down'}
|
|
.configArray=${this._config.entities}
|
|
.arrayAttribute=${'entities'}
|
|
.arraySource=${this._config}
|
|
.index=${index}
|
|
></ha-icon>
|
|
`
|
|
: html `
|
|
<ha-icon icon="mdi:arrow-down" style="opacity: 25%;" class="ha-icon-large"></ha-icon>
|
|
`}
|
|
<ha-icon
|
|
class="ha-icon-large"
|
|
icon="mdi:close"
|
|
@click=${this._removeEntity}
|
|
.configAttribute=${'entity'}
|
|
.configArray=${'entities'}
|
|
.configIndex=${index}
|
|
></ha-icon>
|
|
</div>
|
|
${options.options.entities[index].show
|
|
? html `
|
|
<div class="options">
|
|
${this._createBarElement(index)} ${this._createValueElement(index)}
|
|
${this._createPositionsElement(index)} ${this._createSeverityElement(index)}
|
|
${this._createAnimationElement(index)} ${this._createActionsElement(index)}
|
|
</div>
|
|
`
|
|
: ''}
|
|
`);
|
|
}
|
|
return valueElementArray;
|
|
}
|
|
_createEntitiesElement() {
|
|
if (!this.hass || !this._config) {
|
|
return html ``;
|
|
}
|
|
const options = this._options.entities;
|
|
return html `
|
|
<div class="card-config">
|
|
<div class="option" @click=${this._toggleThing} .options=${options} .optionsTarget=${this._options}>
|
|
<div class="row">
|
|
<ha-icon .icon=${`mdi:${options.icon}`}></ha-icon>
|
|
<div class="title">${options.name}</div>
|
|
<ha-icon .icon=${options.show ? `mdi:chevron-up` : `mdi:chevron-down`} style="margin-left: auto;"></ha-icon>
|
|
</div>
|
|
<div class="secondary">${options.secondary}</div>
|
|
</div>
|
|
${options.show
|
|
? html `
|
|
<div class="card-background" style="max-height: 400px; overflow: auto;">
|
|
${this._createEntitiesValues()}
|
|
<div class="sub-category" style="display: flex; flex-direction: column; align-items: flex-end;">
|
|
<ha-fab
|
|
mini
|
|
icon="mdi:plus"
|
|
@click=${this._addEntity}
|
|
.configArray=${this._configArray}
|
|
.configAddValue=${'entity'}
|
|
.sourceArray=${this._config.entities}
|
|
></ha-fab>
|
|
</div>
|
|
</div>
|
|
`
|
|
: ''}
|
|
</div>
|
|
`;
|
|
}
|
|
_createAppearanceElement() {
|
|
if (!this.hass) {
|
|
return html ``;
|
|
}
|
|
const options = this._options.appearance;
|
|
return html `
|
|
<div class="option" @click=${this._toggleThing} .options=${options} .optionsTarget=${this._options}>
|
|
<div class="row">
|
|
<ha-icon .icon=${`mdi:${options.icon}`}></ha-icon>
|
|
<div class="title">${options.name}</div>
|
|
<ha-icon
|
|
.icon=${options.show ? `mdi:chevron-up` : `mdi:chevron-down`}
|
|
style="margin-left: auto;"
|
|
></ha-icon>
|
|
</div>
|
|
<div class="secondary">${options.secondary}</div>
|
|
</div>
|
|
${options.show
|
|
? html `
|
|
<div class="card-background">
|
|
${this._createCardElement()} ${this._createBarElement(null)} ${this._createValueElement(null)}
|
|
${this._createPositionsElement(null)} ${this._createSeverityElement(null)}
|
|
${this._createAnimationElement(null)}
|
|
</div>
|
|
`
|
|
: ''}
|
|
</div>`;
|
|
}
|
|
_createBarElement(index) {
|
|
let options;
|
|
let config;
|
|
if (index !== null) {
|
|
options = this._options.entities.options.entities[index].options.bar;
|
|
config = this._configArray[index];
|
|
}
|
|
else {
|
|
options = this._options.appearance.options.bar;
|
|
config = this._config;
|
|
}
|
|
return html `
|
|
<div class="category" id="bar">
|
|
<div
|
|
class="sub-category"
|
|
@click=${this._toggleThing}
|
|
.options=${options}
|
|
.optionsTarget=${this._options.appearance.options}
|
|
>
|
|
<div class="row">
|
|
<ha-icon .icon=${`mdi:${options.icon}`}></ha-icon>
|
|
<div class="title">${options.name}</div>
|
|
<ha-icon .icon=${options.show ? `mdi:chevron-up` : `mdi:chevron-down`} style="margin-left: auto;"></ha-icon>
|
|
</div>
|
|
<div class="secondary">${options.secondary}</div>
|
|
</div>
|
|
${options.show
|
|
? html `
|
|
<div class="value">
|
|
<div>
|
|
<paper-dropdown-menu
|
|
label="Direction"
|
|
@selected-item-changed=${this._valueChanged}
|
|
.configObject=${config}
|
|
.configAttribute=${'direction'}
|
|
.ignoreNull=${true}
|
|
>
|
|
<paper-listbox
|
|
slot="dropdown-content"
|
|
attr-for-selected="item-name"
|
|
selected="${config.direction ? config.direction : null}"
|
|
>
|
|
<paper-item item-name="right">right</paper-item>
|
|
<paper-item item-name="up">up</paper-item>
|
|
</paper-listbox>
|
|
</paper-dropdown-menu>
|
|
${config.direction
|
|
? html `
|
|
<ha-icon
|
|
class="ha-icon-large"
|
|
icon="mdi:close"
|
|
@click=${this._valueChanged}
|
|
.value=${''}
|
|
.configAttribute=${'direction'}
|
|
.configObject=${config}
|
|
></ha-icon>
|
|
`
|
|
: ''}
|
|
</div>
|
|
${index !== null
|
|
? html `
|
|
<paper-input
|
|
label="Name"
|
|
.value="${config.name ? config.name : ''}"
|
|
editable
|
|
.configAttribute=${'name'}
|
|
.configObject=${config}
|
|
@value-changed=${this._valueChanged}
|
|
></paper-input>
|
|
`
|
|
: ''}
|
|
<paper-input
|
|
label="Icon"
|
|
.value="${config.icon ? config.icon : ''}"
|
|
editable
|
|
.configAttribute=${'icon'}
|
|
.configObject=${config}
|
|
@value-changed=${this._valueChanged}
|
|
></paper-input>
|
|
<paper-input
|
|
label="Height"
|
|
.value="${config.height ? config.height : ''}"
|
|
editable
|
|
.configAttribute=${'height'}
|
|
.configObject=${config}
|
|
@value-changed=${this._valueChanged}
|
|
></paper-input>
|
|
<paper-input
|
|
label="Width"
|
|
.value="${config.width ? config.width : ''}"
|
|
editable
|
|
.configAttribute=${'width'}
|
|
.configObject=${config}
|
|
@value-changed=${this._valueChanged}
|
|
></paper-input>
|
|
<paper-input
|
|
label="Color"
|
|
.value="${config.color ? config.color : ''}"
|
|
editable
|
|
.configAttribute=${'color'}
|
|
.configObject=${config}
|
|
@value-changed=${this._valueChanged}
|
|
></paper-input>
|
|
</div>
|
|
`
|
|
: ''}
|
|
</div>
|
|
`;
|
|
}
|
|
_createAnimationElement(index) {
|
|
let options;
|
|
let config;
|
|
if (index !== null) {
|
|
options = this._options.entities.options.entities[index].options.animation;
|
|
config = this._configArray[index];
|
|
}
|
|
else {
|
|
options = this._options.appearance.options.animation;
|
|
config = this._config;
|
|
}
|
|
config.animation = Object.assign({}, config.animation);
|
|
return html `
|
|
<div class="category" id="bar">
|
|
<div
|
|
class="sub-category"
|
|
@click=${this._toggleThing}
|
|
.options=${options}
|
|
.optionsTarget=${this._options.appearance.options}
|
|
>
|
|
<div class="row">
|
|
<ha-icon .icon=${`mdi:${options.icon}`}></ha-icon>
|
|
<div class="title">${options.name}</div>
|
|
<ha-icon .icon=${options.show ? `mdi:chevron-up` : `mdi:chevron-down`} style="margin-left: auto;"></ha-icon>
|
|
</div>
|
|
<div class="secondary">${options.secondary}</div>
|
|
</div>
|
|
${options.show
|
|
? config.animation
|
|
? html `
|
|
<div class="value">
|
|
<div>
|
|
<paper-dropdown-menu
|
|
label="State"
|
|
@selected-item-changed=${this._valueChanged}
|
|
.configAttribute=${'state'}
|
|
.configObject=${config.animation}
|
|
.index=${index}
|
|
.ignoreNull=${true}
|
|
>
|
|
<paper-listbox
|
|
slot="dropdown-content"
|
|
attr-for-selected="item-name"
|
|
selected="${config.animation.state ? config.animation.state : null}"
|
|
>
|
|
<paper-item item-name="on">on</paper-item>
|
|
<paper-item item-name="off">off</paper-item>
|
|
</paper-listbox>
|
|
</paper-dropdown-menu>
|
|
${config.animation.state
|
|
? html `
|
|
<ha-icon
|
|
class="ha-icon-large"
|
|
icon="mdi:close"
|
|
@click=${this._valueChanged}
|
|
.value=${''}
|
|
.configAttribute=${'state'}
|
|
.configObject=${config.animation}
|
|
.index=${index}
|
|
></ha-icon>
|
|
`
|
|
: ''}
|
|
</div>
|
|
<paper-input
|
|
label="Speed"
|
|
.value="${config.animation.speed ? config.animation.speed : ''}"
|
|
editable
|
|
@value-changed=${this._valueChanged}
|
|
.configAttribute=${'speed'}
|
|
.configObject=${config.animation}
|
|
.index=${index}
|
|
></paper-input>
|
|
</div>
|
|
`
|
|
: html `
|
|
<div class="value">
|
|
<div>
|
|
<paper-dropdown-menu
|
|
label="State"
|
|
@selected-item-changed=${this._valueChanged}
|
|
.configObject=${config}
|
|
.configAttribute=${'state'}
|
|
.configAdd=${'animation'}
|
|
.index=${index}
|
|
.ignoreNull=${true}
|
|
>
|
|
<paper-listbox slot="dropdown-content" attr-for-selected="item-name">
|
|
<paper-item item-name="on">on</paper-item>
|
|
<paper-item item-name="off">off</paper-item>
|
|
</paper-listbox>
|
|
</paper-dropdown-menu>
|
|
</div>
|
|
<paper-input
|
|
label="Speed"
|
|
editable
|
|
.value=${''}
|
|
@value-changed=${this._valueChanged}
|
|
.configAttribute=${'speed'}
|
|
.configObject=${config}
|
|
.configAdd=${'animation'}
|
|
.index=${index}
|
|
></paper-input>
|
|
</div>
|
|
`
|
|
: ''}
|
|
</div>
|
|
`;
|
|
}
|
|
_createSeverityElement(index) {
|
|
let options;
|
|
let config;
|
|
if (index !== null) {
|
|
options = this._options.entities.options.entities[index].options.severity;
|
|
config = this._configArray[index];
|
|
}
|
|
else {
|
|
options = this._options.appearance.options.severity;
|
|
config = this._config;
|
|
}
|
|
const arrayLength = config.severity ? config.severity.length : 0;
|
|
return html `
|
|
<div class="category" id="bar">
|
|
<div
|
|
class="sub-category"
|
|
@click=${this._toggleThing}
|
|
.options=${options}
|
|
.optionsTarget=${this._options.appearance.options}
|
|
>
|
|
<div class="row">
|
|
<ha-icon .icon=${`mdi:${options.icon}`}></ha-icon>
|
|
<div class="title">${options.name}</div>
|
|
<ha-icon .icon=${options.show ? `mdi:chevron-up` : `mdi:chevron-down`} style="margin-left: auto;"></ha-icon>
|
|
</div>
|
|
<div class="secondary">${options.secondary}</div>
|
|
</div>
|
|
${options.show
|
|
? html `
|
|
<div class="card-background" style="overflow: auto; max-height: 420px;">
|
|
${arrayLength > 0
|
|
? html `
|
|
${this._createSeverityValues(index)}
|
|
`
|
|
: ''}
|
|
<div class="sub-category" style="display: flex; flex-direction: column; align-items: flex-end;">
|
|
<ha-fab mini icon="mdi:plus" @click=${this._addSeverity} .index=${index}></ha-fab>
|
|
</div>
|
|
</div>
|
|
`
|
|
: ''}
|
|
</div>
|
|
`;
|
|
}
|
|
_createSeverityValues(index) {
|
|
let config;
|
|
if (index === null) {
|
|
config = this._config;
|
|
}
|
|
else {
|
|
config = this._configArray[index];
|
|
}
|
|
const severityValuesArray = [];
|
|
for (const severity of config.severity) {
|
|
const severityIndex = config.severity.indexOf(severity);
|
|
severityValuesArray.push(html `
|
|
<div class="sub-category" style="display: flex; flex-direction: row; align-items: center;">
|
|
<div class="value">
|
|
<div style="display:flex;">
|
|
<paper-input
|
|
label="From"
|
|
type="number"
|
|
.value="${severity.from || severity.from === 0 ? severity.from : ''}"
|
|
editable
|
|
.severityAttribute=${'from'}
|
|
.index=${index}
|
|
.severityIndex=${severityIndex}
|
|
@value-changed=${this._updateSeverity}
|
|
></paper-input>
|
|
<paper-input
|
|
label="To"
|
|
type="number"
|
|
.value="${severity.to ? severity.to : ''}"
|
|
editable
|
|
.severityAttribute=${'to'}
|
|
.index=${index}
|
|
.severityIndex=${severityIndex}
|
|
@value-changed=${this._updateSeverity}
|
|
></paper-input>
|
|
</div>
|
|
<div style="display:flex;">
|
|
<paper-input
|
|
label="Color"
|
|
.value="${severity.color ? severity.color : ''}"
|
|
editable
|
|
.severityAttribute=${'color'}
|
|
.index=${index}
|
|
.severityIndex=${severityIndex}
|
|
@value-changed=${this._updateSeverity}
|
|
></paper-input>
|
|
<paper-input
|
|
label="Icon"
|
|
.value="${severity.icon ? severity.icon : ''}"
|
|
editable
|
|
.severityAttribute=${'icon'}
|
|
.index=${index}
|
|
.severityIndex=${severityIndex}
|
|
@value-changed=${this._updateSeverity}
|
|
></paper-input>
|
|
</div>
|
|
${severity.hide
|
|
? html `
|
|
<ha-switch
|
|
checked
|
|
.severityAttribute=${'hide'}
|
|
.index=${index}
|
|
.severityIndex=${severityIndex}
|
|
.value=${!severity.hide}
|
|
@change=${this._updateSeverity}
|
|
>Hide</ha-switch
|
|
>
|
|
`
|
|
: html `
|
|
<ha-switch
|
|
unchecked
|
|
.severityAttribute=${'hide'}
|
|
.index=${index}
|
|
.severityIndex=${severityIndex}
|
|
.value=${!severity.hide}
|
|
@change=${this._updateSeverity}
|
|
>Hide</ha-switch
|
|
>
|
|
`}
|
|
</div>
|
|
<div style="display: flex;">
|
|
${severityIndex !== 0
|
|
? html `
|
|
<ha-icon
|
|
class="ha-icon-large"
|
|
icon="mdi:arrow-up"
|
|
@click=${this._moveSeverity}
|
|
.configDirection=${'up'}
|
|
.index=${index}
|
|
.severityIndex=${severityIndex}
|
|
></ha-icon>
|
|
`
|
|
: html `
|
|
<ha-icon icon="mdi:arrow-up" style="opacity: 25%;" class="ha-icon-large"></ha-icon>
|
|
`}
|
|
${severityIndex !== config.severity.length - 1
|
|
? html `
|
|
<ha-icon
|
|
class="ha-icon-large"
|
|
icon="mdi:arrow-down"
|
|
@click=${this._moveSeverity}
|
|
.configDirection=${'down'}
|
|
.index=${index}
|
|
.severityIndex=${severityIndex}
|
|
></ha-icon>
|
|
`
|
|
: html `
|
|
<ha-icon icon="mdi:arrow-down" style="opacity: 25%;" class="ha-icon-large"></ha-icon>
|
|
`}
|
|
<ha-icon
|
|
class="ha-icon-large"
|
|
icon="mdi:close"
|
|
@click=${this._removeSeverity}
|
|
.index=${index}
|
|
.severityIndex=${severityIndex}
|
|
></ha-icon>
|
|
</div>
|
|
</div>
|
|
`);
|
|
}
|
|
return severityValuesArray;
|
|
}
|
|
_createCardElement() {
|
|
if (!this.hass) {
|
|
return html ``;
|
|
}
|
|
const config = this._config;
|
|
const options = this._options.appearance.options.card;
|
|
return html `
|
|
<div class="category" id="card">
|
|
<div
|
|
class="sub-category"
|
|
@click=${this._toggleThing}
|
|
.options=${options}
|
|
.optionsTarget=${this._options.appearance.options}
|
|
>
|
|
<div class="row">
|
|
<ha-icon .icon=${`mdi:${options.icon}`}></ha-icon>
|
|
<div class="title">${options.name}</div>
|
|
<ha-icon .icon=${options.show ? `mdi:chevron-up` : `mdi:chevron-down`} style="margin-left: auto;"></ha-icon>
|
|
</div>
|
|
<div class="secondary">${options.secondary}</div>
|
|
</div>
|
|
${options.show
|
|
? html `
|
|
<div class="value-container">
|
|
<paper-input
|
|
editable
|
|
label="Header Title"
|
|
.value="${config.title ? config.title : ''}"
|
|
.configObject=${config}
|
|
.configAttribute=${'title'}
|
|
@value-changed=${this._valueChanged}
|
|
></paper-input>
|
|
<paper-input
|
|
class="value-number"
|
|
type="number"
|
|
label="Columns"
|
|
.value=${config.columns ? config.columns : ''}
|
|
.configObject=${config}
|
|
.configAttribute=${'columns'}
|
|
@value-changed=${this._valueChanged}
|
|
></paper-input>
|
|
<div>
|
|
${config.entity_row
|
|
? html `
|
|
<ha-switch
|
|
checked
|
|
.configAttribute=${'entity_row'}
|
|
.configObject=${config}
|
|
.value=${!config.entity_row}
|
|
@change=${this._valueChanged}
|
|
>Entity Row</ha-switch
|
|
>
|
|
`
|
|
: html `
|
|
<ha-switch
|
|
unchecked
|
|
.configAttribute=${'entity_row'}
|
|
.configObject=${config}
|
|
.value=${!config.entity_row}
|
|
@change=${this._valueChanged}
|
|
>Entity Row</ha-switch
|
|
>
|
|
`}
|
|
</div>
|
|
</div>
|
|
`
|
|
: ''}
|
|
</div>
|
|
`;
|
|
}
|
|
_createPositionsValues(index) {
|
|
const defaultPositions = {
|
|
icon: 'outside',
|
|
indicator: 'outside',
|
|
name: 'inside',
|
|
minmax: 'off',
|
|
value: 'inside',
|
|
};
|
|
let config;
|
|
if (index === null) {
|
|
config = this._config;
|
|
}
|
|
else {
|
|
config = this._configArray[index];
|
|
}
|
|
config.positions = Object.assign({}, config.positions);
|
|
const positionElementsArray = [];
|
|
const objectKeys = Object.keys(defaultPositions);
|
|
for (const position of objectKeys) {
|
|
if (config.positions[position]) {
|
|
positionElementsArray.push(html `
|
|
<div class="value">
|
|
<paper-dropdown-menu
|
|
label="${position}"
|
|
@value-changed=${this._valueChanged}
|
|
.configAttribute=${position}
|
|
.configObject=${config.positions}
|
|
.ignoreNull=${true}
|
|
>
|
|
<paper-listbox
|
|
slot="dropdown-content"
|
|
attr-for-selected="item-name"
|
|
.selected=${config.positions[position]}
|
|
>
|
|
<paper-item item-name="inside">inside</paper-item>
|
|
<paper-item item-name="outside">outside</paper-item>
|
|
<paper-item item-name="off">off</paper-item>
|
|
</paper-listbox>
|
|
</paper-dropdown-menu>
|
|
<ha-icon
|
|
class="ha-icon-large"
|
|
icon="mdi:close"
|
|
@click=${this._valueChanged}
|
|
.value=${''}
|
|
.configAttribute=${position}
|
|
.configObject=${config.positions}
|
|
></ha-icon>
|
|
</div>
|
|
`);
|
|
}
|
|
else {
|
|
positionElementsArray.push(html `
|
|
<div class="value">
|
|
<paper-dropdown-menu
|
|
label="${position}"
|
|
@value-changed=${this._valueChanged}
|
|
.configAttribute=${position}
|
|
.configObject=${config.positions}
|
|
>
|
|
<paper-listbox slot="dropdown-content" .selected=${null}>
|
|
<paper-item>inside</paper-item>
|
|
<paper-item>outside</paper-item>
|
|
<paper-item>off</paper-item>
|
|
</paper-listbox>
|
|
</paper-dropdown-menu>
|
|
</div>
|
|
`);
|
|
}
|
|
}
|
|
return positionElementsArray;
|
|
}
|
|
_createPositionsElement(index) {
|
|
if (!this.hass) {
|
|
return html ``;
|
|
}
|
|
let options;
|
|
let config;
|
|
if (index === null) {
|
|
options = this._options.appearance.options.positions;
|
|
config = this._config;
|
|
}
|
|
else {
|
|
options = this._options.entities.options.entities[index].options.positions;
|
|
config = this._configArray[index];
|
|
}
|
|
return html `
|
|
<div class="category">
|
|
<div
|
|
class="sub-category"
|
|
@click=${this._toggleThing}
|
|
.options=${options}
|
|
.optionsTarget=${this._options.appearance.options}
|
|
>
|
|
<div class="row">
|
|
<ha-icon .icon=${`mdi:${options.icon}`}></ha-icon>
|
|
<div class="title">${options.name}</div>
|
|
<ha-icon .icon=${options.show ? `mdi:chevron-up` : `mdi:chevron-down`} style="margin-left: auto;"></ha-icon>
|
|
</div>
|
|
<div class="secondary">${options.secondary}</div>
|
|
</div>
|
|
${options.show
|
|
? html `
|
|
${this._createPositionsValues(index)}
|
|
`
|
|
: ``}
|
|
</div>
|
|
`;
|
|
}
|
|
_createValueElement(index) {
|
|
if (!this.hass) {
|
|
return html ``;
|
|
}
|
|
let options;
|
|
let config;
|
|
if (index !== null) {
|
|
options = this._options.entities.options.entities[index].options.value;
|
|
config = this._configArray[index];
|
|
}
|
|
else {
|
|
options = this._options.appearance.options.value;
|
|
config = this._config;
|
|
}
|
|
return html `
|
|
<div class="category" id="value">
|
|
<div
|
|
class="sub-category"
|
|
@click=${this._toggleThing}
|
|
.options=${options}
|
|
.optionsTarget=${this._options.appearance.options}
|
|
>
|
|
<div class="row">
|
|
<ha-icon .icon=${`mdi:${options.icon}`}></ha-icon>
|
|
<div class="title">${options.name}</div>
|
|
<ha-icon .icon=${options.show ? `mdi:chevron-up` : `mdi:chevron-down`} style="margin-left: auto;"></ha-icon>
|
|
</div>
|
|
<div class="secondary">${options.secondary}</div>
|
|
</div>
|
|
${options.show
|
|
? html `
|
|
<div class="value">
|
|
${config.limit_value
|
|
? html `
|
|
<ha-switch
|
|
checked
|
|
.configAttribute=${'limit_value'}
|
|
.configObject=${config}
|
|
.value=${!config.limit_value}
|
|
@change=${this._valueChanged}
|
|
>Limit Value</ha-switch
|
|
>
|
|
`
|
|
: html `
|
|
<ha-switch
|
|
unchecked
|
|
.configObject=${config}
|
|
.configAttribute=${'limit_value'}
|
|
.value=${!config.limit_value}
|
|
@change=${this._valueChanged}
|
|
>Limit Value</ha-switch
|
|
>
|
|
`}
|
|
${config.complementary
|
|
? html `
|
|
<ha-switch
|
|
checked
|
|
.configAttribute=${'complementary'}
|
|
.configObject=${config}
|
|
.value=${!config.complementary}
|
|
@change=${this._valueChanged}
|
|
>Complementary</ha-switch
|
|
>
|
|
`
|
|
: html `
|
|
<ha-switch
|
|
unchecked
|
|
.configObject=${config}
|
|
.configAttribute=${'complementary'}
|
|
.value=${!config.complementary}
|
|
@change=${this._valueChanged}
|
|
>Complementary</ha-switch
|
|
>
|
|
`}
|
|
<paper-input
|
|
class="value-number"
|
|
label="Decimal"
|
|
type="number"
|
|
.value="${config.decimal ? config.decimal : ''}"
|
|
editable
|
|
.configAttribute=${'decimal'}
|
|
.configObject=${config}
|
|
@value-changed=${this._valueChanged}
|
|
></paper-input>
|
|
<paper-input
|
|
class="value-number"
|
|
type="number"
|
|
label="Min"
|
|
.value="${config.min ? config.min : ''}"
|
|
editable
|
|
.configAttribute=${'min'}
|
|
.configObject=${config}
|
|
@value-changed=${this._valueChanged}
|
|
></paper-input>
|
|
<paper-input
|
|
class="value-number"
|
|
type="number"
|
|
label="Max"
|
|
.value="${config.max ? config.max : ''}"
|
|
editable
|
|
.configAttribute=${'max'}
|
|
.configObject=${config}
|
|
@value-changed=${this._valueChanged}
|
|
></paper-input>
|
|
<paper-input
|
|
class="value-number"
|
|
type="number"
|
|
label="Target"
|
|
.value="${config.target ? config.target : ''}"
|
|
editable
|
|
.configAttribute=${'target'}
|
|
.configObject=${config}
|
|
@value-changed=${this._valueChanged}
|
|
></paper-input>
|
|
<paper-input
|
|
label="Unit of Measurement"
|
|
.value="${config.unit_of_measurement ? config.unit_of_measurement : ''}"
|
|
editable
|
|
.configAttribute=${'unit_of_measurement'}
|
|
.configObject=${config}
|
|
@value-changed=${this._valueChanged}
|
|
></paper-input>
|
|
<paper-input
|
|
label="Attribute"
|
|
.value="${config.attribute ? config.attribute : ''}"
|
|
editable
|
|
.configAttribute=${'attribute'}
|
|
.configObject=${config}
|
|
@value-changed=${this._valueChanged}
|
|
></paper-input>
|
|
</div>
|
|
`
|
|
: ''}
|
|
</div>
|
|
`;
|
|
}
|
|
_toggleThing(ev) {
|
|
const options = ev.target.options;
|
|
const show = !options.show;
|
|
if (ev.target.optionsTarget) {
|
|
if (Array.isArray(ev.target.optionsTarget)) {
|
|
for (const options of ev.target.optionsTarget) {
|
|
options.show = false;
|
|
}
|
|
}
|
|
else {
|
|
for (const [key] of Object.entries(ev.target.optionsTarget)) {
|
|
ev.target.optionsTarget[key].show = false;
|
|
}
|
|
}
|
|
}
|
|
options.show = show;
|
|
this._toggle = !this._toggle;
|
|
}
|
|
_addEntity(ev) {
|
|
if (!this._config || !this.hass) {
|
|
return;
|
|
}
|
|
const target = ev.target;
|
|
let newObject;
|
|
if (target.configAddObject) {
|
|
newObject = target.configAddObject;
|
|
}
|
|
else {
|
|
newObject = { [target.configAddValue]: '' };
|
|
}
|
|
const newArray = target.configArray.slice();
|
|
newArray.push(newObject);
|
|
this._config.entities = newArray;
|
|
C(this, 'config-changed', { config: this._config });
|
|
}
|
|
_moveEntity(ev) {
|
|
if (!this._config || !this.hass) {
|
|
return;
|
|
}
|
|
const target = ev.target;
|
|
let newArray = target.configArray.slice();
|
|
if (target.configDirection == 'up')
|
|
newArray = arrayMove(newArray, target.index, target.index - 1);
|
|
else if (target.configDirection == 'down')
|
|
newArray = arrayMove(newArray, target.index, target.index + 1);
|
|
this._config.entities = newArray;
|
|
C(this, 'config-changed', { config: this._config });
|
|
}
|
|
_removeEntity(ev) {
|
|
if (!this._config || !this.hass) {
|
|
return;
|
|
}
|
|
const target = ev.target;
|
|
const entitiesArray = [];
|
|
let index = 0;
|
|
for (const config of this._configArray) {
|
|
if (target.configIndex !== index) {
|
|
entitiesArray.push(config);
|
|
}
|
|
index++;
|
|
}
|
|
const newConfig = { [target.configArray]: entitiesArray };
|
|
this._config = Object.assign(this._config, newConfig);
|
|
C(this, 'config-changed', { config: this._config });
|
|
}
|
|
_addSeverity(ev) {
|
|
if (!this._config || !this.hass) {
|
|
return;
|
|
}
|
|
const target = ev.target;
|
|
let severityArray;
|
|
if (target.index === null) {
|
|
severityArray = this._config.severity;
|
|
}
|
|
else {
|
|
severityArray = this._config.entities[target.index].severity;
|
|
}
|
|
if (!severityArray) {
|
|
severityArray = [];
|
|
}
|
|
const newObject = { from: '', to: '', color: '' };
|
|
const newArray = severityArray.slice();
|
|
newArray.push(newObject);
|
|
if (target.index === null) {
|
|
this._config.severity = newArray;
|
|
}
|
|
else {
|
|
this._configArray[target.index].severity = newArray;
|
|
}
|
|
this._config.entities = this._configArray;
|
|
C(this, 'config-changed', { config: this._config });
|
|
}
|
|
_moveSeverity(ev) {
|
|
if (!this._config || !this.hass) {
|
|
return;
|
|
}
|
|
const target = ev.target;
|
|
let severityArray;
|
|
if (target.index === null) {
|
|
severityArray = this._config.severity;
|
|
}
|
|
else {
|
|
severityArray = this._config.entities[target.index].severity;
|
|
}
|
|
let newArray = severityArray.slice();
|
|
if (target.configDirection == 'up') {
|
|
newArray = arrayMove(newArray, target.severityIndex, target.severityIndex - 1);
|
|
}
|
|
else if (target.configDirection == 'down') {
|
|
newArray = arrayMove(newArray, target.severityIndex, target.severityIndex + 1);
|
|
}
|
|
if (target.index === null) {
|
|
this._config.severity = newArray;
|
|
}
|
|
else {
|
|
this._configArray[target.index].severity = newArray;
|
|
}
|
|
this._config.entities = this._configArray;
|
|
C(this, 'config-changed', { config: this._config });
|
|
}
|
|
_removeSeverity(ev) {
|
|
if (!this._config || !this.hass) {
|
|
return;
|
|
}
|
|
const target = ev.target;
|
|
let severityArray;
|
|
if (target.index === null) {
|
|
severityArray = this._config.severity;
|
|
}
|
|
else {
|
|
severityArray = this._configArray[target.index].severity;
|
|
}
|
|
const clonedArray = severityArray.slice();
|
|
const newArray = [];
|
|
let arrayIndex = 0;
|
|
for (const config of clonedArray) {
|
|
if (target.severityIndex !== arrayIndex) {
|
|
newArray.push(clonedArray[arrayIndex]);
|
|
}
|
|
arrayIndex++;
|
|
}
|
|
if (target.index === null) {
|
|
if (newArray.length === 0) {
|
|
delete this._config.severity;
|
|
}
|
|
else {
|
|
this._config.severity = newArray;
|
|
}
|
|
}
|
|
else {
|
|
if (newArray.length === 0) {
|
|
delete this._configArray[target.index].severity;
|
|
}
|
|
else {
|
|
this._configArray[target.index].severity = newArray;
|
|
}
|
|
}
|
|
this._config.entities = this._configArray;
|
|
C(this, 'config-changed', { config: this._config });
|
|
}
|
|
_updateSeverity(ev) {
|
|
const target = ev.target;
|
|
let severityArray;
|
|
if (target.index === null) {
|
|
severityArray = this._config.severity;
|
|
}
|
|
else {
|
|
severityArray = this._configArray[target.index].severity;
|
|
}
|
|
const newSeverityArray = [];
|
|
for (const index in severityArray) {
|
|
if (target.severityIndex == index) {
|
|
const clonedObject = Object.assign({}, severityArray[index]);
|
|
const newObject = { [target.severityAttribute]: target.value };
|
|
const mergedObject = Object.assign(clonedObject, newObject);
|
|
if (target.value == '') {
|
|
delete mergedObject[target.severityAttribute];
|
|
}
|
|
newSeverityArray.push(mergedObject);
|
|
}
|
|
else {
|
|
newSeverityArray.push(severityArray[index]);
|
|
}
|
|
}
|
|
if (target.index === null) {
|
|
this._config.severity = newSeverityArray;
|
|
}
|
|
else {
|
|
this._configArray[target.index].severity = newSeverityArray;
|
|
}
|
|
this._config.entities = this._configArray;
|
|
C(this, 'config-changed', { config: this._config });
|
|
}
|
|
_valueChanged(ev) {
|
|
if (!this._config || !this.hass) {
|
|
return;
|
|
}
|
|
const target = ev.target;
|
|
if (target.configObject[target.configAttribute] == target.value) {
|
|
return;
|
|
}
|
|
if (target.configAdd && target.value !== '') {
|
|
target.configObject = Object.assign(target.configObject, {
|
|
[target.configAdd]: { [target.configAttribute]: target.value },
|
|
});
|
|
}
|
|
if (target.configAttribute && target.configObject && !target.configAdd) {
|
|
if (target.value == '' || target.value === false) {
|
|
if (target.ignoreNull == true)
|
|
return;
|
|
delete target.configObject[target.configAttribute];
|
|
}
|
|
else {
|
|
console.log(target.configObject);
|
|
target.configObject[target.configAttribute] = target.value;
|
|
}
|
|
}
|
|
this._config.entities = this._configArray;
|
|
C(this, 'config-changed', { config: this._config });
|
|
}
|
|
static get styles() {
|
|
return css `
|
|
.option {
|
|
padding: 4px 0px;
|
|
cursor: pointer;
|
|
}
|
|
.options {
|
|
background: var(--primary-background-color);
|
|
border-radius: var(--ha-card-border-radius);
|
|
cursor: pointer;
|
|
padding: 8px;
|
|
}
|
|
.sub-category {
|
|
cursor: pointer;
|
|
}
|
|
.row {
|
|
display: flex;
|
|
margin-bottom: -14px;
|
|
pointer-events: none;
|
|
margin-top: 14px;
|
|
}
|
|
.title {
|
|
padding-left: 16px;
|
|
margin-top: -6px;
|
|
pointer-events: none;
|
|
}
|
|
.secondary {
|
|
padding-left: 40px;
|
|
color: var(--secondary-text-color);
|
|
pointer-events: none;
|
|
}
|
|
.value {
|
|
padding: 0px 8px;
|
|
}
|
|
.value-container {
|
|
padding: 0px 8px;
|
|
transition: all 0.5s ease-in-out;
|
|
}
|
|
.value-container:target {
|
|
height: 50px;
|
|
}
|
|
.value-number {
|
|
width: 100px;
|
|
}
|
|
ha-fab {
|
|
margin: 8px;
|
|
}
|
|
ha-switch {
|
|
padding: 16px 0;
|
|
}
|
|
.card-background {
|
|
background: var(--paper-card-background-color);
|
|
border-radius: var(--ha-card-border-radius);
|
|
padding: 8px;
|
|
}
|
|
.category {
|
|
background: #0000;
|
|
}
|
|
.ha-icon-large {
|
|
cursor: pointer;
|
|
margin: 0px 4px;
|
|
}
|
|
`;
|
|
}
|
|
};
|
|
__decorate([
|
|
property()
|
|
], BarCardEditor.prototype, "hass", void 0);
|
|
__decorate([
|
|
property()
|
|
], BarCardEditor.prototype, "_config", void 0);
|
|
__decorate([
|
|
property()
|
|
], BarCardEditor.prototype, "_toggle", void 0);
|
|
BarCardEditor = __decorate([
|
|
customElement('bar-card-editor')
|
|
], BarCardEditor);
|
|
// @ts-ignore
|
|
window.customCards = window.customCards || [];
|
|
// @ts-ignore
|
|
window.customCards.push({
|
|
type: 'bar-card',
|
|
name: 'Bar Card',
|
|
preview: false,
|
|
description: 'A customizable bar card.',
|
|
});
|
|
|
|
const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
|
class ActionHandler extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.holdTime = 500;
|
|
this.ripple = document.createElement('mwc-ripple');
|
|
this.timer = undefined;
|
|
this.held = false;
|
|
this.cooldownStart = false;
|
|
this.cooldownEnd = false;
|
|
}
|
|
connectedCallback() {
|
|
Object.assign(this.style, {
|
|
position: 'absolute',
|
|
width: isTouch ? '100px' : '50px',
|
|
height: isTouch ? '100px' : '50px',
|
|
transform: 'translate(-50%, -50%)',
|
|
pointerEvents: 'none',
|
|
});
|
|
this.appendChild(this.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, options) {
|
|
if (element.actionHandler) {
|
|
return;
|
|
}
|
|
element.actionHandler = 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;
|
|
});
|
|
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) {
|
|
C(element, 'action', { action: 'hold' });
|
|
}
|
|
else if (options.hasDoubleTap) {
|
|
if (ev.detail === 1 || ev.type === 'keyup') {
|
|
this.dblClickTimeout = window.setTimeout(() => {
|
|
C(element, 'action', { action: 'tap' });
|
|
}, 250);
|
|
}
|
|
else {
|
|
clearTimeout(this.dblClickTimeout);
|
|
C(element, 'action', { action: 'double_tap' });
|
|
}
|
|
}
|
|
else {
|
|
C(element, 'action', { action: 'tap' });
|
|
}
|
|
this.cooldownEnd = true;
|
|
window.setTimeout(() => (this.cooldownEnd = false), 100);
|
|
};
|
|
const handleEnter = (ev) => {
|
|
if (ev.keyCode === 13) {
|
|
return clickEnd(ev);
|
|
}
|
|
};
|
|
element.addEventListener('touchstart', clickStart, { passive: true });
|
|
element.addEventListener('touchend', clickEnd);
|
|
element.addEventListener('touchcancel', clickEnd);
|
|
element.addEventListener('keyup', handleEnter);
|
|
// iOS 13 sends a complete normal touchstart-touchend series of events followed by a mousedown-click series.
|
|
// That might be a bug, but until it's fixed, this should make action-handler work.
|
|
// If it's not a bug that is fixed, this might need updating with the next iOS version.
|
|
// Note that all events (both touch and mouse) must be listened for in order to work on computers with both mouse and touchscreen.
|
|
const isIOS13 = /iPhone OS 13_/.test(window.navigator.userAgent);
|
|
if (!isIOS13) {
|
|
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.disabled = false;
|
|
this.ripple.active = true;
|
|
this.ripple.unbounded = true;
|
|
}
|
|
stopAnimation() {
|
|
this.ripple.active = false;
|
|
this.ripple.disabled = true;
|
|
this.style.display = 'none';
|
|
}
|
|
}
|
|
customElements.define('action-handler-bar', ActionHandler);
|
|
const getActionHandler = () => {
|
|
const body = document.body;
|
|
if (body.querySelector('action-handler-bar')) {
|
|
return body.querySelector('action-handler-bar');
|
|
}
|
|
const actionhandler = document.createElement('action-handler-bar');
|
|
body.appendChild(actionhandler);
|
|
return actionhandler;
|
|
};
|
|
const actionHandlerBind = (element, options) => {
|
|
const actionhandler = getActionHandler();
|
|
if (!actionhandler) {
|
|
return;
|
|
}
|
|
actionhandler.bind(element, options);
|
|
};
|
|
const actionHandler = directive((options = {}) => (part) => {
|
|
actionHandlerBind(part.committer.element, options);
|
|
});
|
|
|
|
const CARD_VERSION = '3.1.7';
|
|
|
|
var common = {
|
|
version: "Version",
|
|
invalid_configuration: "Invalid configuration",
|
|
show_warning: "Show Warning",
|
|
entity_not_available: "Entity not available"
|
|
};
|
|
var en = {
|
|
common: common
|
|
};
|
|
|
|
var en$1 = /*#__PURE__*/Object.freeze({
|
|
__proto__: null,
|
|
common: common,
|
|
'default': en
|
|
});
|
|
|
|
var common$1 = {
|
|
version: "Versjon",
|
|
invalid_configuration: "Ikke gyldig konfiguration",
|
|
show_warning: "Vis advarsel"
|
|
};
|
|
var nb = {
|
|
common: common$1
|
|
};
|
|
|
|
var nb$1 = /*#__PURE__*/Object.freeze({
|
|
__proto__: null,
|
|
common: common$1,
|
|
'default': nb
|
|
});
|
|
|
|
var languages = {
|
|
en: en$1,
|
|
nb: nb$1,
|
|
};
|
|
function localize(string, search = '', replace = '') {
|
|
const section = string.split('.')[0];
|
|
const key = string.split('.')[1];
|
|
const lang = (localStorage.getItem('selectedLanguage') || 'en').replace(/['"]+/g, '').replace('-', '_');
|
|
var tranlated;
|
|
try {
|
|
tranlated = languages[lang][section][key];
|
|
}
|
|
catch (e) {
|
|
tranlated = languages['en'][section][key];
|
|
}
|
|
if (tranlated === undefined)
|
|
tranlated = languages['en'][section][key];
|
|
if (search !== '' && replace !== '') {
|
|
tranlated = tranlated.replace(search, replace);
|
|
}
|
|
return tranlated;
|
|
}
|
|
|
|
const styles = html `
|
|
<style>
|
|
.warning {
|
|
display: block;
|
|
color: black;
|
|
background-color: #fce588;
|
|
padding: 8px;
|
|
}
|
|
#states {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex-grow: 1;
|
|
}
|
|
#states > * {
|
|
margin-bottom: 8px;
|
|
}
|
|
#states > :last-child {
|
|
margin-top: 0px;
|
|
margin-bottom: 0px;
|
|
}
|
|
#states > :first-child {
|
|
margin-top: 0px;
|
|
}
|
|
ha-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
}
|
|
bar-card-row {
|
|
display: flex;
|
|
flex-grow: 1;
|
|
}
|
|
bar-card-row > div {
|
|
flex-basis: 100%;
|
|
}
|
|
bar-card-row:empty {
|
|
display: none;
|
|
}
|
|
bar-card-card {
|
|
display: flex;
|
|
flex-basis: 100%;
|
|
flex-direction: row;
|
|
margin-right: 8px;
|
|
}
|
|
bar-card-card:last-child {
|
|
margin-right: 0px;
|
|
}
|
|
bar-card-background {
|
|
cursor: pointer;
|
|
flex-grow: 1;
|
|
position: relative;
|
|
}
|
|
bar-card-iconbar {
|
|
color: var(--icon-color, var(--paper-item-icon-color));
|
|
align-items: center;
|
|
align-self: center;
|
|
display: flex;
|
|
height: 40px;
|
|
justify-content: center;
|
|
position: relative;
|
|
width: 40px;
|
|
}
|
|
bar-card-currentbar,
|
|
bar-card-backgroundbar,
|
|
bar-card-contentbar,
|
|
bar-card-targetbar,
|
|
bar-card-animationbar {
|
|
position: absolute;
|
|
height: 100%;
|
|
width: 100%;
|
|
border-radius: var(--bar-card-border-radius, var(--ha-card-border-radius));
|
|
}
|
|
bar-card-contentbar {
|
|
align-items: center;
|
|
color: var(--primary-text-color);
|
|
display: flex;
|
|
justify-content: flex-start;
|
|
}
|
|
.contentbar-direction-right {
|
|
flex-direction: row;
|
|
}
|
|
.contentbar-direction-up {
|
|
flex-direction: column;
|
|
}
|
|
bar-card-backgroundbar {
|
|
background: var(--bar-color);
|
|
filter: brightness(0.5);
|
|
opacity: 0.25;
|
|
}
|
|
bar-card-currentbar {
|
|
background: linear-gradient(
|
|
to var(--bar-direction),
|
|
var(--bar-color) var(--bar-percent),
|
|
#0000 var(--bar-percent),
|
|
#0000 var(--bar-percent)
|
|
);
|
|
}
|
|
bar-card-targetbar {
|
|
background: linear-gradient(
|
|
to var(--bar-direction),
|
|
#0000 var(--bar-percent),
|
|
var(--bar-color) var(--bar-percent),
|
|
var(--bar-color) var(--bar-target-percent),
|
|
#0000 var(--bar-target-percent)
|
|
);
|
|
display: var(--target-display);
|
|
filter: brightness(0.66);
|
|
opacity: 0.33;
|
|
}
|
|
bar-card-markerbar {
|
|
background: var(--bar-color);
|
|
filter: brightness(0.75);
|
|
opacity: 50%;
|
|
position: absolute;
|
|
}
|
|
bar-card-animationbar {
|
|
background-repeat: no-repeat;
|
|
filter: brightness(0.75);
|
|
opacity: 0%;
|
|
}
|
|
.animationbar-horizontal {
|
|
background: linear-gradient(to var(--animation-direction), var(--bar-color) 0%, var(--bar-color) 1%, #0000 1%);
|
|
}
|
|
.animationbar-vertical {
|
|
background: linear-gradient(to var(--animation-direction), #0000 0%, #0000 1%, var(--bar-color) 1%);
|
|
}
|
|
@keyframes animation-increase {
|
|
0% {
|
|
opacity: 50%;
|
|
background-size: var(--bar-percent) 100%;
|
|
}
|
|
100% {
|
|
opacity: 0%;
|
|
background-size: 10000% 100%;
|
|
}
|
|
}
|
|
@keyframes animation-decrease {
|
|
0% {
|
|
opacity: 0%;
|
|
background-size: 10000%;
|
|
}
|
|
100% {
|
|
opacity: 50%;
|
|
background-size: var(--bar-percent);
|
|
}
|
|
}
|
|
@keyframes animation-increase-vertical {
|
|
0% {
|
|
opacity: 50%;
|
|
background-size: 100% var(--bar-percent);
|
|
}
|
|
100% {
|
|
background-size: 100% 0%;
|
|
opacity: 0%;
|
|
}
|
|
}
|
|
@keyframes animation-decrease-vertical {
|
|
0% {
|
|
background-size: 100% 100%;
|
|
opacity: 0%;
|
|
}
|
|
100% {
|
|
opacity: 50%;
|
|
background-size: 100% var(--bar-percent);
|
|
}
|
|
}
|
|
bar-card-indicator {
|
|
align-self: center;
|
|
color: var(--bar-color);
|
|
filter: brightness(0.75);
|
|
height: 16px;
|
|
width: 16px;
|
|
position: relative;
|
|
text-align: center;
|
|
}
|
|
.indicator-direction-right {
|
|
margin-right: -16px;
|
|
left: -6px;
|
|
}
|
|
.indicator-direction-up {
|
|
margin: 4px;
|
|
}
|
|
bar-card-name {
|
|
align-items: center;
|
|
align-self: center;
|
|
justify-content: center;
|
|
margin: 4px;
|
|
overflow: hidden;
|
|
position: relative;
|
|
text-align: left;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.name-outside {
|
|
margin-left: 16px;
|
|
}
|
|
bar-card-value,
|
|
bar-card-min,
|
|
bar-card-max,
|
|
bar-card-divider {
|
|
align-self: center;
|
|
position: relative;
|
|
}
|
|
bar-card-min,
|
|
bar-card-max,
|
|
bar-card-divider {
|
|
font-size: 10px;
|
|
margin: 2px;
|
|
opacity: 0.5;
|
|
}
|
|
.min-direction-up {
|
|
margin-top: auto;
|
|
}
|
|
.min-direction-right {
|
|
margin-left: auto;
|
|
}
|
|
bar-card-divider {
|
|
margin-left: 0px;
|
|
margin-right: 0px;
|
|
}
|
|
bar-card-value {
|
|
white-space: nowrap;
|
|
margin: 4px;
|
|
}
|
|
.value-direction-right {
|
|
margin-left: auto;
|
|
}
|
|
.value-direction-up {
|
|
margin-top: auto;
|
|
}
|
|
</style>
|
|
`;
|
|
|
|
/* eslint no-console: 0 */
|
|
console.info(`%c BAR-CARD \n%c ${localize('common.version')} ${CARD_VERSION} `, 'color: orange; font-weight: bold; background: black', 'color: white; font-weight: bold; background: dimgray');
|
|
// TODO Name your custom element
|
|
let BarCard = class BarCard extends LitElement {
|
|
constructor() {
|
|
super(...arguments);
|
|
this._configArray = [];
|
|
this._stateArray = [];
|
|
this._animationState = [];
|
|
this._rowAmount = 1;
|
|
}
|
|
static async getConfigElement() {
|
|
return document.createElement('bar-card-editor');
|
|
}
|
|
static getStubConfig() {
|
|
return {};
|
|
}
|
|
shouldUpdate(changedProps) {
|
|
return hasConfigOrEntitiesChanged(this, changedProps, false);
|
|
}
|
|
setConfig(config) {
|
|
if (!config) {
|
|
throw new Error(localize('common.invalid_configuration'));
|
|
}
|
|
this._config = mergeDeep({
|
|
animation: {
|
|
state: 'off',
|
|
speed: 5,
|
|
},
|
|
color: 'var(--bar-card-color, var(--primary-color))',
|
|
columns: 1,
|
|
direction: 'right',
|
|
max: 100,
|
|
min: 0,
|
|
positions: {
|
|
icon: 'outside',
|
|
indicator: 'outside',
|
|
name: 'inside',
|
|
minmax: 'off',
|
|
value: 'inside',
|
|
},
|
|
}, config);
|
|
if (this._config.stack == 'horizontal')
|
|
this._config.columns = this._config.entities.length;
|
|
this._configArray = createConfigArray(this._config);
|
|
this._rowAmount = this._configArray.length / this._config.columns;
|
|
}
|
|
render() {
|
|
if (!this._config || !this.hass) {
|
|
return html ``;
|
|
}
|
|
return html `
|
|
<ha-card
|
|
.header=${this._config.title ? this._config.title : null}
|
|
style="${this._config.entity_row ? 'background: #0000; box-shadow: none;' : ''}"
|
|
>
|
|
<div
|
|
id="states"
|
|
class="card-content"
|
|
style="${this._config.entity_row ? 'padding: 0px;' : ''} ${this._config.direction == 'up'
|
|
? ''
|
|
: 'flex-grow: 0;'}"
|
|
>
|
|
${this._createBarArray()}
|
|
</div>
|
|
</ha-card>
|
|
${styles}
|
|
`;
|
|
}
|
|
_createBarArray() {
|
|
// Create array containing number of bars per row.
|
|
const columnsArray = [];
|
|
for (let i = 0; i < this._configArray.length; i++) {
|
|
if ((columnsArray.length + 1) * this._config.columns == i) {
|
|
columnsArray.push(this._config.columns);
|
|
}
|
|
if (this._configArray.length == i + 1) {
|
|
columnsArray.push(this._configArray.length - columnsArray.length * this._config.columns);
|
|
}
|
|
}
|
|
// For each row add contained bars based on columnsArray.
|
|
const perRowArray = [];
|
|
for (let i = 0; i < columnsArray.length; i++) {
|
|
// For every number in columnsArray add bars.
|
|
const currentRowArray = [];
|
|
for (let x = 0; x < columnsArray[i]; x++) {
|
|
const index = i * this._config.columns + x;
|
|
const config = this._configArray[index];
|
|
const state = this.hass.states[config.entity];
|
|
if (!state) {
|
|
currentRowArray.push(html `
|
|
<div class="warning" style="margin-bottom: 8px;">
|
|
${localize('common.entity_not_available')}: ${config.entity}
|
|
</div>
|
|
`);
|
|
continue;
|
|
}
|
|
// If attribute is defined use attribute value as bar value.
|
|
let entityState;
|
|
if (config.attribute) {
|
|
entityState = state.attributes[config.attribute];
|
|
}
|
|
else {
|
|
entityState = state.state;
|
|
}
|
|
// Contine if severity hide is defined.
|
|
if (config.severity) {
|
|
if (this._computeSeverityVisibility(entityState, index)) {
|
|
continue;
|
|
}
|
|
}
|
|
// If limit_value is defined limit the displayed value to min and max.
|
|
if (config.limit_value) {
|
|
entityState = Math.min(entityState, config.max);
|
|
entityState = Math.max(entityState, config.min);
|
|
}
|
|
// If decimal is defined check if NaN and apply number fix.
|
|
if (!isNaN(Number(entityState))) {
|
|
if (config.decimal == 0)
|
|
entityState = Number(entityState).toFixed(0);
|
|
else if (config.decimal)
|
|
entityState = Number(entityState).toFixed(config.decimal);
|
|
}
|
|
// Defined height and check for configured height.
|
|
let barHeight = 40;
|
|
if (config.height)
|
|
barHeight = config.height;
|
|
// Set style variables based on direction.
|
|
let alignItems = 'stretch';
|
|
let backgroundMargin = '0px 0px 0px 13px';
|
|
let barDirection = 'right';
|
|
let flexDirection = 'row';
|
|
let markerDirection = 'left';
|
|
let markerStyle = 'height: 100%; width: 2px;';
|
|
switch (config.direction) {
|
|
case 'right':
|
|
barDirection = 'right';
|
|
markerDirection = 'left';
|
|
break;
|
|
case 'up':
|
|
backgroundMargin = '0px';
|
|
barDirection = 'top';
|
|
flexDirection = 'column-reverse';
|
|
markerDirection = 'bottom';
|
|
markerStyle = 'height: 2px; width: 100%;';
|
|
break;
|
|
}
|
|
// Set icon position html.
|
|
let iconOutside;
|
|
let iconInside;
|
|
let icon;
|
|
if (this._computeSeverityIcon(entityState, index)) {
|
|
icon = this._computeSeverityIcon(entityState, index);
|
|
}
|
|
else if (config.icon) {
|
|
icon = config.icon;
|
|
}
|
|
else if (state.attributes.icon) {
|
|
icon = state.attributes.icon;
|
|
}
|
|
else {
|
|
icon = O(d(config.entity), entityState);
|
|
}
|
|
switch (config.positions.icon) {
|
|
case 'outside':
|
|
iconOutside = html `
|
|
<bar-card-iconbar>
|
|
<ha-icon icon="${icon}"></ha-icon>
|
|
</bar-card-iconbar>
|
|
`;
|
|
break;
|
|
case 'inside':
|
|
iconInside = html `
|
|
<bar-card-iconbar>
|
|
<ha-icon icon="${icon}"></ha-icon>
|
|
</bar-card-iconbar>
|
|
`;
|
|
backgroundMargin = '0px';
|
|
break;
|
|
case 'off':
|
|
backgroundMargin = '0px';
|
|
break;
|
|
}
|
|
// Check for configured name otherwise use friendly name.
|
|
const name = config.name ? config.name : state.attributes.friendly_name;
|
|
// Set name html based on position.
|
|
let nameOutside;
|
|
let nameInside;
|
|
switch (config.positions.name) {
|
|
case 'outside':
|
|
nameOutside = html `
|
|
<bar-card-name
|
|
class="${config.entity_row ? 'name-outside' : ''}"
|
|
style="${config.direction == 'up' ? '' : config.width ? `width: calc(100% - ${config.width});` : ''}"
|
|
>${name}</bar-card-name
|
|
>
|
|
`;
|
|
backgroundMargin = '0px';
|
|
break;
|
|
case 'inside':
|
|
nameInside = html `
|
|
<bar-card-name>${name}</bar-card-name>
|
|
`;
|
|
break;
|
|
}
|
|
// Check for configured unit of measurement otherwise use attribute value.
|
|
let unitOfMeasurement;
|
|
if (isNaN(Number(entityState))) {
|
|
unitOfMeasurement = '';
|
|
}
|
|
else {
|
|
if (config.unit_of_measurement) {
|
|
unitOfMeasurement = config.unit_of_measurement;
|
|
}
|
|
else {
|
|
unitOfMeasurement = state.attributes.unit_of_measurement;
|
|
}
|
|
}
|
|
// Set min and max html based on position.
|
|
let minMaxOutside;
|
|
let minMaxInside;
|
|
switch (config.positions.minmax) {
|
|
case 'outside':
|
|
minMaxOutside = html `
|
|
<bar-card-min>${config.min}${unitOfMeasurement}</bar-card-min>
|
|
<bar-card-divider>/</bar-card-divider>
|
|
<bar-card-max>${config.max}${unitOfMeasurement}</bar-card-max>
|
|
`;
|
|
break;
|
|
case 'inside':
|
|
minMaxInside = html `
|
|
<bar-card-min class="${config.direction == 'up' ? 'min-direction-up' : 'min-direction-right'}"
|
|
>${config.min}${unitOfMeasurement}</bar-card-min
|
|
>
|
|
<bar-card-divider>/</bar-card-divider>
|
|
<bar-card-max> ${config.max}${unitOfMeasurement}</bar-card-max>
|
|
`;
|
|
break;
|
|
}
|
|
// Set value html based on position.
|
|
let valueOutside;
|
|
let valueInside;
|
|
switch (config.positions.value) {
|
|
case 'outside':
|
|
valueOutside = html `
|
|
<bar-card-value class="${config.direction == 'up' ? 'value-direction-up' : 'value-direction-right'}"
|
|
>${config.complementary ? config.max - entityState : entityState} ${unitOfMeasurement}</bar-card-value
|
|
>
|
|
`;
|
|
break;
|
|
case 'inside':
|
|
valueInside = html `
|
|
<bar-card-value
|
|
class="${config.positions.minmax == 'inside'
|
|
? ''
|
|
: config.direction == 'up'
|
|
? 'value-direction-up'
|
|
: 'value-direction-right'}"
|
|
>${config.complementary ? config.max - entityState : entityState} ${unitOfMeasurement}</bar-card-value
|
|
>
|
|
`;
|
|
break;
|
|
case 'off':
|
|
backgroundMargin = '0px';
|
|
break;
|
|
}
|
|
// Set indicator and animation state based on value change.
|
|
let indicatorText = '';
|
|
if (entityState > this._stateArray[index]) {
|
|
indicatorText = '▲';
|
|
if (config.direction == 'up')
|
|
this._animationState[index] = 'animation-increase-vertical';
|
|
else
|
|
this._animationState[index] = 'animation-increase';
|
|
}
|
|
else if (entityState < this._stateArray[index]) {
|
|
indicatorText = '▼';
|
|
if (config.direction == 'up')
|
|
this._animationState[index] = 'animation-decrease-vertical';
|
|
else
|
|
this._animationState[index] = 'animation-decrease';
|
|
}
|
|
else {
|
|
this._animationState[index] = this._animationState[index];
|
|
}
|
|
if (isNaN(Number(entityState))) {
|
|
indicatorText = '';
|
|
}
|
|
// Set bar color.
|
|
const barColor = this._computeBarColor(entityState, index);
|
|
// Set indicator html based on position.
|
|
let indicatorOutside;
|
|
let indicatorInside;
|
|
switch (config.positions.indicator) {
|
|
case 'outside':
|
|
indicatorOutside = html `
|
|
<bar-card-indicator
|
|
class="${config.direction == 'up' ? '' : 'indicator-direction-right'}"
|
|
style="--bar-color: ${barColor};"
|
|
>${indicatorText}</bar-card-indicator
|
|
>
|
|
`;
|
|
break;
|
|
case 'inside':
|
|
indicatorInside = html `
|
|
<bar-card-indicator style="--bar-color: ${barColor};">${indicatorText}</bar-card-indicator>
|
|
`;
|
|
break;
|
|
}
|
|
// Set bar percent and marker percent based on value difference.
|
|
const barPercent = this._computePercent(entityState, index);
|
|
const targetMarkerPercent = this._computePercent(config.target, index);
|
|
let targetStartPercent = barPercent;
|
|
let targetEndPercent = this._computePercent(config.target, index);
|
|
if (targetEndPercent < targetStartPercent) {
|
|
targetStartPercent = targetEndPercent;
|
|
targetEndPercent = barPercent;
|
|
}
|
|
// Set bar width if configured.
|
|
let barWidth = '';
|
|
if (config.width) {
|
|
alignItems = 'center';
|
|
barWidth = `width: ${config.width}`;
|
|
}
|
|
// Set animation state inside array.
|
|
const animation = this._animationState[index];
|
|
let animationDirection = 'right';
|
|
let animationPercent = barPercent * 100;
|
|
let animationClass = 'animationbar-horizontal';
|
|
if (animation == 'animation-increase-vertical' || animation == 'animation-decrease-vertical') {
|
|
animationDirection = 'bottom';
|
|
animationClass = 'animationbar-vertical';
|
|
animationPercent = (100 - barPercent) * 100;
|
|
}
|
|
// Add current bar to row array.
|
|
currentRowArray.push(html `
|
|
<bar-card-card
|
|
style="flex-direction: ${flexDirection}; align-items: ${alignItems};"
|
|
@action=${this._handleAction}
|
|
.config=${config}
|
|
.actionHandler=${actionHandler({
|
|
hasHold: W(config.hold_action),
|
|
hasDoubleClick: W(config.double_tap_action),
|
|
})}
|
|
>
|
|
${iconOutside} ${indicatorOutside} ${nameOutside}
|
|
<bar-card-background
|
|
style="margin: ${backgroundMargin}; height: ${barHeight}${typeof barHeight == 'number'
|
|
? 'px'
|
|
: ''}; ${barWidth}"
|
|
>
|
|
<bar-card-backgroundbar style="--bar-color: ${barColor};"></bar-card-backgroundbar>
|
|
${config.animation.state == 'on'
|
|
? html `
|
|
<bar-card-animationbar
|
|
style="animation: ${animation} ${config.animation
|
|
.speed}s infinite ease-out; --bar-percent: ${animationPercent}%; --bar-color: ${barColor}; --animation-direction: ${animationDirection};"
|
|
class="${animationClass}"
|
|
></bar-card-animationbar>
|
|
`
|
|
: ''}
|
|
<bar-card-currentbar
|
|
style="--bar-color: ${barColor}; --bar-percent: ${barPercent}%; --bar-direction: ${barDirection}"
|
|
></bar-card-currentbar>
|
|
${config.target
|
|
? html `
|
|
<bar-card-targetbar
|
|
style="--bar-color: ${barColor}; --bar-percent: ${targetStartPercent}%; --bar-target-percent: ${targetEndPercent}%; --bar-direction: ${barDirection};"
|
|
></bar-card-targetbar>
|
|
<bar-card-markerbar
|
|
style="--bar-color: ${barColor}; --bar-target-percent: ${targetMarkerPercent}%; ${markerDirection}: calc(${targetMarkerPercent}% - 1px); ${markerStyle}}"
|
|
></bar-card-markerbar>
|
|
`
|
|
: ''}
|
|
<bar-card-contentbar
|
|
class="${config.direction == 'up' ? 'contentbar-direction-up' : 'contentbar-direction-right'}"
|
|
>
|
|
${iconInside} ${indicatorInside} ${nameInside} ${minMaxInside} ${valueInside}
|
|
</bar-card-contentbar>
|
|
</bar-card-background>
|
|
${minMaxOutside} ${valueOutside}
|
|
</bar-card-card>
|
|
`);
|
|
// Set entity state inside array if changed.
|
|
if (entityState !== this._stateArray[index]) {
|
|
this._stateArray[index] = entityState;
|
|
}
|
|
}
|
|
// Add all bars for this row to array.
|
|
perRowArray.push(currentRowArray);
|
|
}
|
|
// Create array containing all rows.
|
|
let rowFlexDirection = 'column';
|
|
if (this._config.columns || this._config.stack)
|
|
rowFlexDirection = 'row';
|
|
const rowArray = [];
|
|
for (const row of perRowArray) {
|
|
rowArray.push(html `
|
|
<bar-card-row style="flex-direction: ${rowFlexDirection};">${row}</bar-card-row>
|
|
`);
|
|
}
|
|
return rowArray;
|
|
}
|
|
_computeBarColor(value, index) {
|
|
const config = this._configArray[index];
|
|
let barColor;
|
|
if (config.severity) {
|
|
barColor = this._computeSeverityColor(value, index);
|
|
}
|
|
else if (value == 'unavailable') {
|
|
barColor = `var(--bar-card-disabled-color, ${config.color})`;
|
|
}
|
|
else {
|
|
barColor = config.color;
|
|
}
|
|
return barColor;
|
|
}
|
|
_computeSeverityColor(value, index) {
|
|
const config = this._configArray[index];
|
|
const numberValue = Number(value);
|
|
const sections = config.severity;
|
|
let color;
|
|
if (isNaN(numberValue)) {
|
|
sections.forEach(section => {
|
|
if (value == section.text) {
|
|
color = section.color;
|
|
}
|
|
});
|
|
}
|
|
else {
|
|
sections.forEach(section => {
|
|
if (numberValue >= section.from && numberValue <= section.to) {
|
|
color = section.color;
|
|
}
|
|
});
|
|
}
|
|
if (color == undefined)
|
|
color = config.color;
|
|
return color;
|
|
}
|
|
_computeSeverityVisibility(value, index) {
|
|
const config = this._configArray[index];
|
|
const numberValue = Number(value);
|
|
const sections = config.severity;
|
|
let hide = false;
|
|
if (isNaN(numberValue)) {
|
|
sections.forEach(section => {
|
|
if (value == section.text) {
|
|
hide = section.hide;
|
|
}
|
|
});
|
|
}
|
|
else {
|
|
sections.forEach(section => {
|
|
if (numberValue >= section.from && numberValue <= section.to) {
|
|
hide = section.hide;
|
|
}
|
|
});
|
|
}
|
|
return hide;
|
|
}
|
|
_computeSeverityIcon(value, index) {
|
|
const config = this._configArray[index];
|
|
const numberValue = Number(value);
|
|
const sections = config.severity;
|
|
let icon = false;
|
|
if (!sections)
|
|
return false;
|
|
if (isNaN(numberValue)) {
|
|
sections.forEach(section => {
|
|
if (value == section.text) {
|
|
icon = section.icon;
|
|
}
|
|
});
|
|
}
|
|
else {
|
|
sections.forEach(section => {
|
|
if (numberValue >= section.from && numberValue <= section.to) {
|
|
icon = section.icon;
|
|
}
|
|
});
|
|
}
|
|
return icon;
|
|
}
|
|
_computePercent(value, index) {
|
|
const config = this._configArray[index];
|
|
const numberValue = Number(value);
|
|
if (value == 'unavailable')
|
|
return 0;
|
|
if (isNaN(numberValue))
|
|
return 100;
|
|
switch (config.direction) {
|
|
case 'right-reverse':
|
|
case 'left-reverse':
|
|
case 'up-reverse':
|
|
case 'down-reverse':
|
|
return 100 - (100 * (numberValue - config.min)) / (config.max - config.min);
|
|
default:
|
|
return (100 * (numberValue - config.min)) / (config.max - config.min);
|
|
}
|
|
}
|
|
_handleAction(ev) {
|
|
if (this.hass && ev.target.config && ev.detail.action) {
|
|
U(this, this.hass, ev.target.config, ev.detail.action);
|
|
}
|
|
}
|
|
getCardSize() {
|
|
if (this._config.height) {
|
|
const heightString = this._config.height.toString();
|
|
const cardSize = Math.trunc((Number(heightString.replace('px', '')) / 50) * this._rowAmount);
|
|
return cardSize + 1;
|
|
}
|
|
else {
|
|
return this._rowAmount + 1;
|
|
}
|
|
}
|
|
};
|
|
__decorate([
|
|
property()
|
|
], BarCard.prototype, "hass", void 0);
|
|
__decorate([
|
|
property()
|
|
], BarCard.prototype, "_config", void 0);
|
|
__decorate([
|
|
property()
|
|
], BarCard.prototype, "_configArray", void 0);
|
|
BarCard = __decorate([
|
|
customElement('bar-card')
|
|
], BarCard);
|
|
|
|
export { BarCard };
|