diff --git a/.devcontainer.json b/.devcontainer.json
new file mode 100644
index 0000000..3c90ed8
--- /dev/null
+++ b/.devcontainer.json
@@ -0,0 +1,42 @@
+{
+ "name": "Swiss Army Knife Development",
+
+ // Open the sub-folder with the source code
+ "workspaceFolder": "/workspaces/swiss-army-knife-card",
+
+// See https://aka.ms/vscode-remote/devcontainer.json for format details.
+ "remoteUser": "vscode",
+ "appPort": ["6000:6000", "9123:8123"],
+// "postCreateCommand": "yarn install && sudo container install",
+// "runArgs": ["-v", "${localWorkspaceFolder}/.devcontainer/www:/config/www"],
+ "customizations/vscode/extensions": [
+ "github.vscode-pull-request-github",
+ "eamodio.gitlens",
+ "dbaeumer.vscode-eslint",
+ "esbenp.prettier-vscode",
+ "bierner.lit-html",
+ "runem.lit-plugin",
+ "davidanson.vscode-markdownlint",
+ "redhat.vscode-yaml",
+ "msjsdiag.debugger-for-chrome",
+ "yzhang.markdown-all-in-one"
+ ],
+ "customizations/vscode/settings": {
+ "files.eol": "\n",
+ "editor.tabSize": 2,
+ "terminal.integrated.shell.linux": "/bin/bash",
+ "editor.formatOnPaste": false,
+ "editor.formatOnSave": true,
+ "editor.formatOnType": true,
+ "files.trimTrailingWhitespace": true,
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "markdown.extension.toc.githubCompatibility": true,
+ "files.watcherExclude": {
+ "**/.git/objects/**": true,
+ "**/.git/subtree-cache/**": true,
+ "**/node_modules/**": true,
+ "**/.hg/store/**": true,
+ "**/.rpt2_cache/**": true
+ }
+ }
+}
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000..e952eaf
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,42 @@
+{
+ "name": "Swiss Army Knife Development",
+
+ // Open the sub-folder with the source code
+ "workspaceFolder": "/workspaces/swiss-army-knife-card",
+
+// See https://aka.ms/vscode-remote/devcontainer.json for format details.
+ "remoteUser": "vscode",
+ "appPort": ["5000:5000", "9123:8123"],
+// "postCreateCommand": "yarn install && sudo container install",
+// "runArgs": ["-v", "${localWorkspaceFolder}/.devcontainer/www:/config/www"],
+ "customizations/vscode/extensions": [
+ "github.vscode-pull-request-github",
+ "eamodio.gitlens",
+ "dbaeumer.vscode-eslint",
+ "esbenp.prettier-vscode",
+ "bierner.lit-html",
+ "runem.lit-plugin",
+ "davidanson.vscode-markdownlint",
+ "redhat.vscode-yaml",
+ "msjsdiag.debugger-for-chrome",
+ "yzhang.markdown-all-in-one"
+ ],
+ "customizations/vscode/settings": {
+ "files.eol": "\n",
+ "editor.tabSize": 2,
+ "terminal.integrated.shell.linux": "/bin/bash",
+ "editor.formatOnPaste": false,
+ "editor.formatOnSave": true,
+ "editor.formatOnType": true,
+ "files.trimTrailingWhitespace": true,
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
+ "markdown.extension.toc.githubCompatibility": true,
+ "files.watcherExclude": {
+ "**/.git/objects/**": true,
+ "**/.git/subtree-cache/**": true,
+ "**/node_modules/**": true,
+ "**/.hg/store/**": true,
+ "**/.rpt2_cache/**": true
+ }
+ }
+}
diff --git a/.eslintrc.yaml b/.eslintrc.yaml
new file mode 100644
index 0000000..50446ae
--- /dev/null
+++ b/.eslintrc.yaml
@@ -0,0 +1,99 @@
+extends: airbnb-base
+parserOptions:
+ ecmaVersion: 2022
+ sourceType: module
+ignorePatterns:
+ - /distjs/*
+ - /dist/*
+ - /src/swiss-army-knife-card.js
+rules:
+ no-else-return: 0
+ no-underscore-dangle: 0
+ nonblock-statement-body-position: 0
+ # curly checks for all {} after if for instance
+ curly: 0
+ no-return-assign: 0
+ consistent-return: 0
+ no-mixed-operators: 0
+ class-methods-use-this: 0
+ no-nested-ternary: 0
+ camelcase: 0
+ # Added for convenience to check eslint...
+ # Settings handled:
+ no-param-reassign: 0
+ max-len:
+ - warn
+ - code: 220
+ ignoreComments: true
+ eqeqeq: 1
+ brace-style: 1
+ # - Prevent warnings in logging and multiple params on one line
+ function-call-argument-newline: 0
+ function-paren-newline: 0
+ # - Allow i++ / i-- in for loops
+ no-plusplus:
+ - warn
+ - allowForLoopAfterthoughts: true
+ no-irregular-whitespace: 0
+ no-bitwise:
+ - warn
+ - allow:
+ - "~"
+ # - Disable .js import warnings
+ import/extensions: 0
+ # - Allow the use of console.()
+ no-console: 0
+ # - Allow for calling .hasOwnProperty for instance directly
+ no-prototype-builtins: 0
+ # - Allow as-needed function names
+ func-names:
+ - warn
+ - as-needed
+ # - Just let me index arrays and get data from them
+ prefer-destructuring: 0
+ # - For now, keep using things like isNaN() as Number.isNan() is incompatible.
+ # Ignore the eslint advice to replace them. As a result everything crashes...
+ no-restricted-globals: 0
+ # Ignore identiation for now
+ indent: 0
+ no-unreachable: 0
+ # THINGS THAT MUST BE HANDLED LATER...
+ # - This is a thing from the segmented-arc, the only file with errors/warnings!
+ # - Status as of 2023.05.06 13:00
+ block-scoped-var: 0
+ vars-on-top: 0
+ no-var: 0
+ no-redeclare: 0
+ no-setter-return: 0
+ no-multi-assign: 0
+ no-empty: 0
+ no-unused-vars: 0
+ prefer-const: 0
+ no-lonely-if: 0
+ no-shadow: 0
+ no-loop-func: 0
+ # Settings handled and NOT occuring anymore!
+ no-undef: 2
+ no-use-before-define: 1
+ no-case-declarations: 1
+ no-inner-declarations: 1
+ array-callback-return: 1
+ max-classes-per-file: 1
+ no-new-func: 1
+ no-constant-condition: 1
+ default-case: 1
+ default-case-last: 1
+ operator-assignment: 1
+ no-sequences: 1
+ no-restricted-syntax: 1
+ no-unused-expressions: 1
+ no-useless-escape: 1
+ import/no-unresolved: 1
+ no-template-curly-in-string: 1
+ # Settings disabled for now, until handled
+
+globals:
+ browser: true
+ window: true
+ Event: true
+ customElements: true
diff --git a/.gitignore b/.gitignore
index 12cd4a4..1f49730 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,7 @@ swiss-army-knife-card.js-from-src
swiss-army-knife-card.js-from-src2
2022.07.16 swiss-army-knife-card.js
2022.07.16 swiss-army-knife-card.js
+node_modules
+*.js.map
+
+
diff --git a/dist/swiss-army-knife-card-bundle.js b/dist/swiss-army-knife-card-bundle.js
new file mode 100644
index 0000000..3728130
--- /dev/null
+++ b/dist/swiss-army-knife-card-bundle.js
@@ -0,0 +1,11576 @@
+/**
+ * @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;
+/**
+ * Reparents nodes, starting from `start` (inclusive) to `end` (exclusive),
+ * into another container (could be the same container), before `before`. If
+ * `before` is null, it appends the nodes to the container.
+ */
+const reparentNodes = (container, start, end = null, before = null) => {
+ while (start !== end) {
+ const n = start.nextSibling;
+ container.insertBefore(start, before);
+ start = n;
+ }
+};
+/**
+ * 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 = ``;
+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`
<${repeat(items, (item) => html`${item} `)} `
+ * ```
+ *
+ * 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
+ */
+/**
+ * Our TrustedTypePolicy for HTML which is declared using the html template
+ * tag function.
+ *
+ * That HTML is a developer-authored constant, and is parsed with innerHTML
+ * before any untrusted expressions have been mixed in. Therefor it is
+ * considered safe by construction.
+ */
+const policy = window.trustedTypes &&
+ trustedTypes.createPolicy('lit-html', { createHTML: (s) => s });
+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 `` 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 .
+ // * 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('', 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 . Cases like
+ // 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');
+ let value = this.getHTML();
+ if (policy !== undefined) {
+ // this is secure because `this.strings` is a TemplateStringsArray.
+ // TODO: validate this when
+ // https://github.com/tc39/proposal-array-is-template-object is
+ // implemented.
+ value = policy.createHTML(value);
+ }
+ template.innerHTML = value;
+ return template;
+ }
+}
+/**
+ * A TemplateResult for SVG fragments.
+ *
+ * This class wraps HTML in an `
` tag in order to parse its contents in the
+ * SVG namespace, then modifies the template to remove the `` tag so that
+ * clones only container the original fragment.
+ */
+class SVGTemplateResult extends TemplateResult {
+ getHTML() {
+ return `${super.getHTML()} `;
+ }
+ getTemplateElement() {
+ const template = super.getTemplateElement();
+ const content = template.content;
+ const svgElement = content.firstChild;
+ content.removeChild(svgElement);
+ reparentNodes(content, svgElement.firstChild);
+ 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;
+ const parts = this.parts;
+ // If we're assigning an attribute via syntax like:
+ // attr="${foo}" or attr=${foo}
+ // but not
+ // attr="${foo} ${bar}" or attr="${foo} baz"
+ // then we don't want to coerce the attribute value into one long
+ // string. Instead we want to just return the value itself directly,
+ // so that sanitizeDOMValue can get the actual value rather than
+ // String(value)
+ // The exception is if v is an array, in which case we do want to smash
+ // it together into a string without calling String() on the array.
+ //
+ // This also allows trusted values (when using TrustedTypes) being
+ // assigned to DOM sinks without being stringified in the process.
+ if (l === 1 && strings[0] === '' && strings[1] === '') {
+ const v = parts[0].value;
+ if (typeof v === 'symbol') {
+ return String(v);
+ }
+ if (typeof v === 'string' || !isIterable(v)) {
+ return v;
+ }
+ }
+ let text = '';
+ for (let i = 0; i < l; i++) {
+ text += strings[i];
+ const part = 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$1 = (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.4.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);
+/**
+ * Interprets a template literal as an SVG template that can efficiently
+ * render to and update a container.
+ */
+const svg = (strings, ...values) => new SVGTemplateResult(strings, values, 'svg', 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,
+ * to the this.handleTapEvent(e, this.config)} >
+
+ ${this._renderIcon()}
+
+ `;
+ }
+} // END of class
+
+/** ****************************************************************************
+ * EntityNameTool class
+ *
+ * Summary.
+ *
+ */
+
+class EntityNameTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_NAME_CONFIG = {
+ classes: {
+ tool: {
+ 'sak-name': true,
+ hover: true,
+ },
+ name: {
+ 'sak-name__name': true,
+ },
+ },
+ styles: {
+ tool: {
+ },
+ name: {
+ },
+ },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_NAME_CONFIG, argConfig), argPos);
+
+ this._name = {};
+ // Init classes
+ this.classes.tool = {};
+ this.classes.name = {};
+
+ // Init styles
+ this.styles.name = {};
+ if (this.dev.debug) console.log('EntityName constructor coords, dimensions', this.coords, this.dimensions, this.svg, this.config);
+ }
+
+ /** *****************************************************************************
+ * EntityNameTool::_buildName()
+ *
+ * Summary.
+ * Builds the Name string.
+ *
+ */
+
+ _buildName(entityState, entityConfig) {
+ return (
+ this.activeAnimation?.name // Name from animation
+ || entityConfig.name
+ || entityState.attributes.friendly_name
+ );
+ }
+
+ /** *****************************************************************************
+ * EntityNameTool::_renderEntityName()
+ *
+ * Summary.
+ * Renders the entity name using precalculated coordinates and dimensions.
+ * Only the runtime style is calculated before rendering the name
+ *
+ */
+
+ _renderEntityName() {
+ this.MergeAnimationClassIfChanged();
+ this.MergeColorFromState(this.styles.name);
+ this.MergeAnimationStyleIfChanged();
+
+ const name = this.textEllipsis(
+ this._buildName(
+ this._card.entities[this.defaultEntityIndex()],
+ this._card.config.entities[this.defaultEntityIndex()],
+ ),
+ this.config?.show?.ellipsis,
+ );
+
+ return svg`
+
+ ${name}
+
+ `;
+ }
+
+ /** *****************************************************************************
+ * EntityNameTool::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ */
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderEntityName()}
+
+ `;
+ }
+} // END of class
+
+/** ****************************************************************************
+ * EntityStateTool class
+ *
+ * Summary.
+ *
+ */
+
+class EntityStateTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_STATE_CONFIG = {
+ show: { uom: 'end' },
+ classes: {
+ tool: {
+ 'sak-state': true,
+ hover: true,
+ },
+ state: {
+ 'sak-state__value': true,
+ },
+ uom: {
+ 'sak-state__uom': true,
+ },
+ },
+ styles: {
+ state: {
+ },
+ uom: {
+ },
+ },
+ };
+ super(argToolset, Merge.mergeDeep(DEFAULT_STATE_CONFIG, argConfig), argPos);
+
+ this.classes.state = {};
+ this.classes.uom = {};
+
+ this.styles.state = {};
+ this.styles.uom = {};
+ if (this.dev.debug) console.log('EntityStateTool constructor coords, dimensions', this.coords, this.dimensions, this.svg, this.config);
+ }
+
+ // EntityStateTool::value
+ set value(state) {
+ super.value = state;
+ }
+
+ _renderState() {
+ this.MergeAnimationClassIfChanged();
+ this.MergeAnimationStyleIfChanged();
+ this.MergeColorFromState(this.styles.state);
+
+ // var inState = this._stateValue?.toLowerCase();
+ let inState = this._stateValue;
+
+ if ((inState) && isNaN(inState)) {
+ // const stateObj = this._card.config.entities[this.defaultEntityIndex()].entity;
+ const stateObj = this._card.entities[this.defaultEntityIndex()];
+ const domain = this._card._computeDomain(this._card.config.entities[this.defaultEntityIndex()].entity);
+
+ const localeTag = this.config.locale_tag ? this.config.locale_tag + inState.toLowerCase() : undefined;
+ const localeTag1 = stateObj.attributes?.device_class ? `component.${domain}.state.${stateObj.attributes.device_class}.${inState}` : '--';
+ const localeTag2 = `component.${domain}.state._.${inState}`;
+
+ inState = (localeTag && this._card.toLocale(localeTag, inState))
+ || (stateObj.attributes?.device_class
+ && this._card.toLocale(localeTag1, inState))
+ || this._card.toLocale(localeTag2, inState)
+ || stateObj.state;
+
+ inState = this.textEllipsis(inState, this.config?.show?.ellipsis);
+ }
+
+ return svg`
+
+ ${this.config?.text?.before ? this.config.text.before : ''}${inState}${this.config?.text?.after ? this.config.text.after : ''}
+ `;
+ }
+
+ _renderUom() {
+ if (this.config.show.uom === 'none') {
+ return svg``;
+ } else {
+ this.MergeAnimationStyleIfChanged();
+ this.MergeColorFromState(this.styles.uom);
+
+ let fsuomStr = this.styles.state['font-size'];
+
+ let fsuomValue = 0.5;
+ let fsuomType = 'em';
+ const fsuomSplit = fsuomStr.match(/\D+|\d*\.?\d+/g);
+ if (fsuomSplit.length === 2) {
+ fsuomValue = Number(fsuomSplit[0]) * 0.6;
+ fsuomType = fsuomSplit[1];
+ } else console.error('Cannot determine font-size for state/unit', fsuomStr);
+
+ fsuomStr = { 'font-size': fsuomValue + fsuomType };
+
+ this.styles.uom = Merge.mergeDeep(this.config.styles.uom, fsuomStr);
+
+ const uom = this._card._buildUom(this.derivedEntity, this._card.entities[this.defaultEntityIndex()], this._card.config.entities[this.defaultEntityIndex()]);
+
+ // Check for location of uom. end = next to state, bottom = below state ;-), etc.
+ if (this.config.show.uom === 'end') {
+ return svg`
+
+ ${uom}
+ `;
+ } else if (this.config.show.uom === 'bottom') {
+ return svg`
+
+ ${uom}
+ `;
+ } else if (this.config.show.uom === 'top') {
+ return svg`
+
+ ${uom}
+ `;
+ } else {
+ return svg``;
+ }
+ }
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ firstUpdated(changedProperties) {
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ updated(changedProperties) {
+ }
+
+ render() {
+ // eslint-disable-next-line no-constant-condition
+ {
+ return svg`
+
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderState()}
+ ${this._renderUom()}
+
+
+ `;
+ }
+ } // render()
+}
+
+/** ****************************************************************************
+ * HorseshoeTool class
+ *
+ * Summary.
+ *
+ */
+
+class HorseshoeTool extends BaseTool {
+ // Donut starts at -220 degrees and is 260 degrees in size.
+ // zero degrees is at 3 o'clock.
+
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_HORSESHOE_CONFIG = {
+ position: {
+ cx: 50,
+ cy: 50,
+ radius: 45,
+ },
+ card_filter: 'card--filter-none',
+ horseshoe_scale: {
+ min: 0,
+ max: 100,
+ width: 3,
+ color: 'var(--primary-background-color)',
+ },
+ horseshoe_state: {
+ width: 6,
+ color: 'var(--primary-color)',
+ },
+ show: {
+ horseshoe: true,
+ scale_tickmarks: false,
+ horseshoe_style: 'fixed',
+ },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_HORSESHOE_CONFIG, argConfig), argPos);
+
+ // Next consts are now variable. Should be calculated!!!!!!
+ this.HORSESHOE_RADIUS_SIZE = 0.45 * SVG_VIEW_BOX;
+ this.TICKMARKS_RADIUS_SIZE = 0.43 * SVG_VIEW_BOX;
+ this.HORSESHOE_PATH_LENGTH = 2 * 260 / 360 * Math.PI * this.HORSESHOE_RADIUS_SIZE;
+
+ // this.config = {...DEFAULT_HORSESHOE_CONFIG};
+ // this.config = {...this.config, ...argConfig};
+
+ // if (argConfig.styles) this.config.styles = {...argConfig.styles};
+ // this.config.styles = {...DEFAULT_HORSESHOE_CONFIG.styles, ...this.config.styles};
+
+ // //if (argConfig.show) this.config.show = Object.assign(...argConfig.show);
+ // this.config.show = {...DEFAULT_HORSESHOE_CONFIG.show, ...this.config.show};
+
+ // //if (argConfig.horseshoe_scale) this.config.horseshoe_scale = Object.assign(...argConfig.horseshoe_scale);
+ // this.config.horseshoe_scale = {...DEFAULT_HORSESHOE_CONFIG.horseshoe_scale, ...this.config.horseshoe_scale};
+
+ // // if (argConfig.horseshoe_state) this.config.horseshoe_state = Object.assign(...argConfig.horseshoe_state);
+ // this.config.horseshoe_state = {...DEFAULT_HORSESHOE_CONFIG.horseshoe_state, ...this.config.horseshoe_state};
+
+ this.config.entity_index = this.config.entity_index ? this.config.entity_index : 0;
+
+ this.svg.radius = Utils.calculateSvgDimension(this.config.position.radius);
+ this.svg.radius_ticks = Utils.calculateSvgDimension(0.95 * this.config.position.radius);
+
+ this.svg.horseshoe_scale = {};
+ this.svg.horseshoe_scale.width = Utils.calculateSvgDimension(this.config.horseshoe_scale.width);
+ this.svg.horseshoe_state = {};
+ this.svg.horseshoe_state.width = Utils.calculateSvgDimension(this.config.horseshoe_state.width);
+ this.svg.horseshoe_scale.dasharray = 2 * 26 / 36 * Math.PI * this.svg.radius;
+
+ // The horseshoe is rotated around its svg base point. This is NOT the center of the circle!
+ // Adjust x and y positions within the svg viewport to re-center the circle after rotating
+ this.svg.rotate = {};
+ this.svg.rotate.degrees = -220;
+ this.svg.rotate.cx = this.svg.cx;
+ this.svg.rotate.cy = this.svg.cy;
+
+ // Get colorstops and make a key/value store...
+ this.colorStops = {};
+ if (this.config.color_stops) {
+ Object.keys(this.config.color_stops).forEach((key) => {
+ this.colorStops[key] = this.config.color_stops[key];
+ });
+ }
+
+ this.sortedStops = Object.keys(this.colorStops).map((n) => Number(n)).sort((a, b) => a - b);
+
+ // Create a colorStopsMinMax list for autominmax color determination
+ this.colorStopsMinMax = {};
+ this.colorStopsMinMax[this.config.horseshoe_scale.min] = this.colorStops[this.sortedStops[0]];
+ this.colorStopsMinMax[this.config.horseshoe_scale.max] = this.colorStops[this.sortedStops[(this.sortedStops.length) - 1]];
+
+ // Now set the color0 and color1 for the gradient used in the horseshoe to the colors
+ // Use default for now!!
+ this.color0 = this.colorStops[this.sortedStops[0]];
+ this.color1 = this.colorStops[this.sortedStops[(this.sortedStops.length) - 1]];
+
+ this.angleCoords = {
+ x1: '0%', y1: '0%', x2: '100%', y2: '0%',
+ };
+ // this.angleCoords = angleCoords;
+ this.color1_offset = '0%';
+
+ //= ===================
+ // End setConfig part.
+
+ if (this.dev.debug) console.log('HorseshoeTool constructor coords, dimensions', this.coords, this.dimensions, this.svg, this.config);
+ }
+
+ /** *****************************************************************************
+ * HorseshoeTool::value()
+ *
+ * Summary.
+ * Sets the value of the horseshoe. Value updated via set hass.
+ * Calculate horseshoe settings & colors depening on config and new value.
+ *
+ */
+
+ set value(state) {
+ if (this._stateValue === state) return;
+
+ this._stateValuePrev = this._stateValue || state;
+ this._stateValue = state;
+ this._stateValueIsDirty = true;
+
+ // Calculate the size of the arc to fill the dasharray with this
+ // value. It will fill the horseshoe relative to the state and min/max
+ // values given in the configuration.
+
+ const min = this.config.horseshoe_scale.min || 0;
+ const max = this.config.horseshoe_scale.max || 100;
+ const val = Math.min(Utils.calculateValueBetween(min, max, state), 1);
+ const score = val * this.HORSESHOE_PATH_LENGTH;
+ const total = 10 * this.HORSESHOE_RADIUS_SIZE;
+ this.dashArray = `${score} ${total}`;
+
+ // We must draw the horseshoe. Depending on the stroke settings, we draw a fixed color, gradient, autominmax or colorstop
+ // #TODO: only if state or attribute has changed.
+
+ const strokeStyle = this.config.show.horseshoe_style;
+
+ if (strokeStyle === 'fixed') {
+ this.stroke_color = this.config.horseshoe_state.color;
+ this.color0 = this.config.horseshoe_state.color;
+ this.color1 = this.config.horseshoe_state.color;
+ this.color1_offset = '0%';
+ // We could set the circle attributes, but we do it with a variable as we are using a gradient
+ // to display the horseshoe circle .. .setAttribute('stroke', stroke);
+ } else if (strokeStyle === 'autominmax') {
+ // Use color0 and color1 for autoranging the color of the horseshoe
+ const stroke = Colors.calculateColor(state, this.colorStopsMinMax, true);
+
+ // We now use a gradient for the horseshoe, using two colors
+ // Set these colors to the colorstop color...
+ this.color0 = stroke;
+ this.color1 = stroke;
+ this.color1_offset = '0%';
+ } else if (strokeStyle === 'colorstop' || strokeStyle === 'colorstopgradient') {
+ const stroke = Colors.calculateColor(state, this.colorStops, strokeStyle === 'colorstopgradient');
+
+ // We now use a gradient for the horseshoe, using two colors
+ // Set these colors to the colorstop color...
+ this.color0 = stroke;
+ this.color1 = stroke;
+ this.color1_offset = '0%';
+ } else if (strokeStyle === 'lineargradient') {
+ // This has taken a lot of time to get a satisfying result, and it appeared much simpler than anticipated.
+ // I don't understand it, but for a circle, a gradient from left/right with adjusted stop is enough ?!?!?!
+ // No calculations to adjust the angle of the gradient, or rotating the gradient itself.
+ // Weird, but it works. Not a 100% match, but it is good enough for now...
+
+ // According to stackoverflow, these calculations / adjustments would be needed, but it isn't ;-)
+ // Added from https://stackoverflow.com/questions/9025678/how-to-get-a-rotated-linear-gradient-svg-for-use-as-a-background-image
+ const angleCoords = {
+ x1: '0%', y1: '0%', x2: '100%', y2: '0%',
+ };
+ this.color1_offset = `${Math.round((1 - val) * 100)}%`;
+
+ this.angleCoords = angleCoords;
+ }
+ if (this.dev.debug) console.log('HorseshoeTool set value', this.cardId, state);
+
+ // return true;
+ }
+
+ /** *****************************************************************************
+ * HorseshoeTool::_renderTickMarks()
+ *
+ * Summary.
+ * Renders the tick marks on the scale.
+ *
+ */
+
+ _renderTickMarks() {
+ const { config } = this;
+ // if (!config) return;
+ // if (!config.show) return;
+ if (!config.show.scale_tickmarks) return;
+
+ const stroke = config.horseshoe_scale.color ? config.horseshoe_scale.color : 'var(--primary-background-color)';
+ const tickSize = config.horseshoe_scale.ticksize ? config.horseshoe_scale.ticksize
+ : (config.horseshoe_scale.max - config.horseshoe_scale.min) / 10;
+
+ // fullScale is 260 degrees. Hard coded for now...
+ const fullScale = 260;
+ const remainder = config.horseshoe_scale.min % tickSize;
+ const startTickValue = config.horseshoe_scale.min + (remainder === 0 ? 0 : (tickSize - remainder));
+ const startAngle = ((startTickValue - config.horseshoe_scale.min)
+ / (config.horseshoe_scale.max - config.horseshoe_scale.min)) * fullScale;
+ const tickSteps = ((config.horseshoe_scale.max - startTickValue) / tickSize);
+
+ // new
+ let steps = Math.floor(tickSteps);
+ const angleStepSize = (fullScale - startAngle) / tickSteps;
+
+ // If steps exactly match the max. value/range, add extra step for that max value.
+ if ((Math.floor(((steps) * tickSize) + startTickValue)) <= (config.horseshoe_scale.max)) { steps += 1; }
+
+ const radius = this.svg.horseshoe_scale.width ? this.svg.horseshoe_scale.width / 2 : 6 / 2;
+ let angle;
+ const scaleItems = [];
+
+ // NTS:
+ // Value of -230 is weird. Should be -220. Can't find why...
+ let i;
+ for (i = 0; i < steps; i++) {
+ angle = startAngle + ((-230 + (360 - i * angleStepSize)) * Math.PI / 180);
+ scaleItems[i] = svg`
+
+ `;
+ }
+ return svg`${scaleItems}`;
+ }
+
+ /** *****************************************************************************
+ * HorseshoeTool::_renderHorseShoe()
+ *
+ * Summary.
+ * Renders the horseshoe group.
+ *
+ * Description.
+ * The horseshoes are rendered in a viewbox of 200x200 (SVG_VIEW_BOX).
+ * Both are centered with a radius of 45%, ie 200*0.45 = 90.
+ *
+ * The foreground horseshoe is always rendered as a gradient with two colors.
+ *
+ * The horseshoes are rotated 220 degrees and are 2 * 26/36 * Math.PI * r in size
+ * There you get your value of 408.4070449,180 ;-)
+ */
+
+ _renderHorseShoe() {
+ if (!this.config.show.horseshoe) return;
+
+ return svg`
+
+
+
+
+
+ ${this._renderTickMarks()}
+
+ `;
+ }
+
+ /** *****************************************************************************
+ * HorseshoeTool::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ */
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderHorseShoe()}
+
+
+
+
+
+
+
+
+
+ `;
+ }
+} // END of class
+
+/** ****************************************************************************
+ * LineTool class
+ *
+ * Summary.
+ *
+ */
+
+class LineTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_LINE_CONFIG = {
+ position: {
+ orientation: 'vertical',
+ length: '10',
+ cx: '50',
+ cy: '50',
+ },
+ classes: {
+ tool: {
+ 'sak-line': true,
+ hover: true,
+ },
+ line: {
+ 'sak-line__line': true,
+ },
+ },
+ styles: {
+ tool: {
+ },
+ line: {
+ },
+ },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_LINE_CONFIG, argConfig), argPos);
+
+ if (!['horizontal', 'vertical', 'fromto'].includes(this.config.position.orientation))
+ throw Error('LineTool::constructor - invalid orientation [vertical, horizontal, fromto] = ', this.config.position.orientation);
+
+ if (['horizontal', 'vertical'].includes(this.config.position.orientation))
+ this.svg.length = Utils.calculateSvgDimension(argConfig.position.length);
+
+ if (this.config.position.orientation === 'fromto') {
+ this.svg.x1 = Utils.calculateSvgCoordinate(argConfig.position.x1, this.toolsetPos.cx);
+ this.svg.y1 = Utils.calculateSvgCoordinate(argConfig.position.y1, this.toolsetPos.cy);
+ this.svg.x2 = Utils.calculateSvgCoordinate(argConfig.position.x2, this.toolsetPos.cx);
+ this.svg.y2 = Utils.calculateSvgCoordinate(argConfig.position.y2, this.toolsetPos.cy);
+ } else if (this.config.position.orientation === 'vertical') {
+ this.svg.x1 = this.svg.cx;
+ this.svg.y1 = this.svg.cy - this.svg.length / 2;
+ this.svg.x2 = this.svg.cx;
+ this.svg.y2 = this.svg.cy + this.svg.length / 2;
+ } else if (this.config.position.orientation === 'horizontal') {
+ this.svg.x1 = this.svg.cx - this.svg.length / 2;
+ this.svg.y1 = this.svg.cy;
+ this.svg.x2 = this.svg.cx + this.svg.length / 2;
+ this.svg.y2 = this.svg.cy;
+ }
+
+ this.classes.line = {};
+ this.styles.line = {};
+
+ if (this.dev.debug) console.log('LineTool constructor coords, dimensions', this.coords, this.dimensions, this.svg, this.config);
+ }
+
+ /** *****************************************************************************
+ * LineTool::_renderLine()
+ *
+ * Summary.
+ * Renders the line using precalculated coordinates and dimensions.
+ * Only the runtime style is calculated before rendering the line
+ *
+ * @returns {svg} Rendered line
+ *
+ */
+
+ _renderLine() {
+ this.MergeAnimationClassIfChanged();
+ this.MergeAnimationStyleIfChanged();
+ this.MergeColorFromState(this.styles.line);
+
+ if (this.dev.debug) console.log('_renderLine', this.config.position.orientation, this.svg.x1, this.svg.y1, this.svg.x2, this.svg.y2);
+ return svg`
+
+ `;
+ }
+
+ /** *****************************************************************************
+ * LineTool::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ * @returns {svg} Rendered line group
+ *
+ */
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderLine()}
+
+ `;
+ }
+} // END of class
+
+/** ***************************************************************************
+ * RangeSliderTool::constructor class
+ *
+ * Summary.
+ *
+ */
+
+class RangeSliderTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_RANGESLIDER_CONFIG = {
+ descr: 'none',
+ position: {
+ cx: 50,
+ cy: 50,
+ orientation: 'horizontal',
+ active: {
+ width: 0,
+ height: 0,
+ radius: 0,
+ },
+ track: {
+ width: 16,
+ height: 7,
+ radius: 3.5,
+ },
+ thumb: {
+ width: 9,
+ height: 9,
+ radius: 4.5,
+ offset: 4.5,
+ },
+ label: {
+ placement: 'none',
+ },
+ },
+ show: {
+ uom: 'end',
+ active: false,
+ },
+ classes: {
+ tool: {
+ 'sak-slider': true,
+ hover: true,
+ },
+ capture: {
+ 'sak-slider__capture': true,
+ },
+ active: {
+ 'sak-slider__active': true,
+ },
+ track: {
+ 'sak-slider__track': true,
+ },
+ thumb: {
+ 'sak-slider__thumb': true,
+ },
+ label: {
+ 'sak-slider__value': true,
+ },
+ uom: {
+ 'sak-slider__uom': true,
+ },
+ },
+ styles: {
+ tool: {
+ },
+ capture: {
+ },
+ active: {
+ },
+ track: {
+ },
+ thumb: {
+ },
+ label: {
+ },
+ uom: {
+ },
+ },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_RANGESLIDER_CONFIG, argConfig), argPos);
+
+ this.svg.activeTrack = {};
+ this.svg.activeTrack.radius = Utils.calculateSvgDimension(this.config.position.active.radius);
+ this.svg.activeTrack.height = Utils.calculateSvgDimension(this.config.position.active.height);
+ this.svg.activeTrack.width = Utils.calculateSvgDimension(this.config.position.active.width);
+
+ this.svg.track = {};
+ this.svg.track.radius = Utils.calculateSvgDimension(this.config.position.track.radius);
+
+ this.svg.thumb = {};
+ this.svg.thumb.radius = Utils.calculateSvgDimension(this.config.position.thumb.radius);
+ this.svg.thumb.offset = Utils.calculateSvgDimension(this.config.position.thumb.offset);
+
+ this.svg.capture = {};
+
+ this.svg.label = {};
+
+ switch (this.config.position.orientation) {
+ case 'horizontal':
+ case 'vertical':
+ this.svg.capture.width = Utils.calculateSvgDimension(this.config.position.capture.width || 1.1 * this.config.position.track.width);
+ this.svg.capture.height = Utils.calculateSvgDimension(this.config.position.capture.height || 3 * this.config.position.thumb.height);
+
+ this.svg.track.width = Utils.calculateSvgDimension(this.config.position.track.width);
+ this.svg.track.height = Utils.calculateSvgDimension(this.config.position.track.height);
+
+ this.svg.thumb.width = Utils.calculateSvgDimension(this.config.position.thumb.width);
+ this.svg.thumb.height = Utils.calculateSvgDimension(this.config.position.thumb.height);
+
+ // x1, y1 = topleft corner
+ this.svg.capture.x1 = this.svg.cx - this.svg.capture.width / 2;
+ this.svg.capture.y1 = this.svg.cy - this.svg.capture.height / 2;
+
+ // x1, y1 = topleft corner
+ this.svg.track.x1 = this.svg.cx - this.svg.track.width / 2;
+ this.svg.track.y1 = this.svg.cy - this.svg.track.height / 2;
+
+ // x1, y1 = topleft corner
+ this.svg.activeTrack.x1 = (this.config.position.orientation === 'horizontal') ? this.svg.track.x1 : this.svg.cx - this.svg.activeTrack.width / 2;
+ this.svg.activeTrack.y1 = this.svg.cy - this.svg.activeTrack.height / 2;
+ // this.svg.activeTrack.x1 = this.svg.track.x1;
+
+ this.svg.thumb.x1 = this.svg.cx - this.svg.thumb.width / 2;
+ this.svg.thumb.y1 = this.svg.cy - this.svg.thumb.height / 2;
+ break;
+
+ default:
+ console.error('RangeSliderTool - constructor: invalid orientation [vertical, horizontal] = ', this.config.position.orientation);
+ throw Error('RangeSliderTool::constructor - invalid orientation [vertical, horizontal] = ', this.config.position.orientation);
+ }
+
+ switch (this.config.position.orientation) {
+ case 'vertical':
+ this.svg.track.y2 = this.svg.cy + this.svg.track.height / 2;
+ this.svg.activeTrack.y2 = this.svg.track.y2;
+ break;
+ }
+ switch (this.config.position.label.placement) {
+ case 'position':
+ this.svg.label.cx = Utils.calculateSvgCoordinate(this.config.position.label.cx, 0);
+ this.svg.label.cy = Utils.calculateSvgCoordinate(this.config.position.label.cy, 0);
+ break;
+
+ case 'thumb':
+ this.svg.label.cx = this.svg.cx;
+ this.svg.label.cy = this.svg.cy;
+ break;
+
+ case 'none':
+ break;
+
+ default:
+ console.error('RangeSliderTool - constructor: invalid label placement [none, position, thumb] = ', this.config.position.label.placement);
+ throw Error('RangeSliderTool::constructor - invalid label placement [none, position, thumb] = ', this.config.position.label.placement);
+ }
+
+ // Init classes
+ this.classes.capture = {};
+ this.classes.track = {};
+ this.classes.thumb = {};
+ this.classes.label = {};
+ this.classes.uom = {};
+
+ // Init styles
+ this.styles.capture = {};
+ this.styles.track = {};
+ this.styles.thumb = {};
+ this.styles.label = {};
+ this.styles.uom = {};
+
+ // Init scale
+ this.svg.scale = {};
+ this.svg.scale.min = this.valueToSvg(this, this.config.scale.min);
+ this.svg.scale.max = this.valueToSvg(this, this.config.scale.max);
+ this.svg.scale.step = this.config.scale.step;
+
+ if (this.dev.debug) console.log('RangeSliderTool constructor coords, dimensions', this.coords, this.dimensions, this.svg, this.config);
+ }
+
+ /** *****************************************************************************
+ * RangeSliderTool::svgCoordinateToSliderValue()
+ *
+ * Summary.
+ * @returns {slider value} Translated svg coordinate to actual slider value
+ *
+ */
+
+ svgCoordinateToSliderValue(argThis, m) {
+ let state;
+ let scalePos;
+ let xpos;
+ let ypos;
+
+ switch (argThis.config.position.orientation) {
+ case 'horizontal':
+ xpos = m.x - argThis.svg.track.x1 - this.svg.thumb.width / 2;
+ scalePos = xpos / (argThis.svg.track.width - this.svg.thumb.width);
+ break;
+
+ case 'vertical':
+ // y is calculated from lower y value. So slider is from bottom to top...
+ ypos = argThis.svg.track.y2 - this.svg.thumb.height / 2 - m.y;
+ scalePos = ypos / (argThis.svg.track.height - this.svg.thumb.height);
+ break;
+ }
+ state = ((argThis.config.scale.max - argThis.config.scale.min) * scalePos) + argThis.config.scale.min;
+ state = Math.round(state / this.svg.scale.step) * this.svg.scale.step;
+ state = Math.max(Math.min(this.config.scale.max, state), this.config.scale.min);
+
+ return state;
+ }
+
+ valueToSvg(argThis, argValue) {
+ if (argThis.config.position.orientation === 'horizontal') {
+ const state = Utils.calculateValueBetween(argThis.config.scale.min, argThis.config.scale.max, argValue);
+
+ const xposp = state * (argThis.svg.track.width - this.svg.thumb.width);
+ const xpos = argThis.svg.track.x1 + this.svg.thumb.width / 2 + xposp;
+ return xpos;
+ } else if (argThis.config.position.orientation === 'vertical') {
+ const state = Utils.calculateValueBetween(argThis.config.scale.min, argThis.config.scale.max, argValue);
+
+ const yposp = state * (argThis.svg.track.height - this.svg.thumb.height);
+ const ypos = argThis.svg.track.y2 - this.svg.thumb.height / 2 - yposp;
+ return ypos;
+ }
+ }
+
+ updateValue(argThis, m) {
+ this._value = this.svgCoordinateToSliderValue(argThis, m);
+ // set dist to 0 to cancel animation frame
+ const dist = 0;
+ // improvement
+ if (Math.abs(dist) < 0.01) {
+ if (this.rid) {
+ window.cancelAnimationFrame(this.rid);
+ this.rid = null;
+ }
+ }
+ }
+
+ updateThumb(argThis, m) {
+ switch (argThis.config.position.orientation) {
+ // eslint-disable-next-line default-case-last
+ default:
+ case 'horizontal':
+ // eslint-disable-next-line no-empty
+ if (this.config.position.label.placement === 'thumb') ;
+
+ if (this.dragging) {
+ const yUp = (this.config.position.label.placement === 'thumb') ? -50 : 0;
+ const yUpStr = `translate(${m.x - this.svg.cx}px , ${yUp}px)`;
+
+ argThis.elements.thumbGroup.style.transform = yUpStr;
+ } else {
+ argThis.elements.thumbGroup.style.transform = `translate(${m.x - this.svg.cx}px, ${0}px)`;
+ }
+ break;
+
+ case 'vertical':
+ if (this.dragging) {
+ const xUp = (this.config.position.label.placement === 'thumb') ? -50 : 0;
+ const xUpStr = `translate(${xUp}px, ${m.y - this.svg.cy}px)`;
+ argThis.elements.thumbGroup.style.transform = xUpStr;
+ } else {
+ argThis.elements.thumbGroup.style.transform = `translate(${0}px, ${m.y - this.svg.cy}px)`;
+ }
+ break;
+ }
+
+ argThis.updateLabel(argThis, m);
+ }
+
+ updateActiveTrack(argThis, m) {
+ if (!argThis.config.show.active) return;
+
+ switch (argThis.config.position.orientation) {
+ // eslint-disable-next-line default-case-last
+ default:
+ case 'horizontal':
+ if (this.dragging) {
+ argThis.elements.activeTrack.setAttribute('width', Math.abs(this.svg.activeTrack.x1 - m.x + this.svg.cx));
+ }
+ break;
+
+ case 'vertical':
+ if (this.dragging) {
+ argThis.elements.activeTrack.setAttribute('y', m.y - this.svg.cy);
+ argThis.elements.activeTrack.setAttribute('height', Math.abs(argThis.svg.activeTrack.y2 - m.y + this.svg.cx));
+ }
+ break;
+ }
+ }
+
+ updateLabel(argThis, m) {
+ if (this.dev.debug) console.log('SLIDER - updateLabel start', m, argThis.config.position.orientation);
+
+ const dec = (this._card.config.entities[this.defaultEntityIndex()].decimals || 0);
+ const x = 10 ** dec;
+ argThis.labelValue2 = (Math.round(argThis.svgCoordinateToSliderValue(argThis, m) * x) / x).toFixed(dec);
+
+ if (this.config.position.label.placement !== 'none') {
+ argThis.elements.label.textContent = argThis.labelValue2;
+ }
+ }
+
+ /*
+ * mouseEventToPoint
+ *
+ * Translate mouse/touch client window coordinates to SVG window coordinates
+ *
+ */
+ // mouseEventToPoint(e) {
+ // var p = this.elements.svg.createSVGPoint();
+ // p.x = e.touches ? e.touches[0].clientX : e.clientX;
+ // p.y = e.touches ? e.touches[0].clientY : e.clientY;
+ // const ctm = this.elements.svg.getScreenCTM().inverse();
+ // var p = p.matrixTransform(ctm);
+ // return p;
+ // }
+ mouseEventToPoint(e) {
+ let p = this.elements.svg.createSVGPoint();
+ p.x = e.touches ? e.touches[0].clientX : e.clientX;
+ p.y = e.touches ? e.touches[0].clientY : e.clientY;
+ const ctm = this.elements.svg.getScreenCTM().inverse();
+ p = p.matrixTransform(ctm);
+ return p;
+ }
+
+ callDragService() {
+ if (typeof this.labelValue2 === 'undefined') return;
+
+ if (this.labelValuePrev !== this.labelValue2) {
+ this.labelValuePrev = this.labelValue2;
+
+ this._processTapEvent(
+ this._card,
+ this._card._hass,
+ this.config,
+ this.config.user_actions.tap_action,
+ this._card.config.entities[this.defaultEntityIndex()]?.entity,
+ this.labelValue2,
+ );
+ }
+ if (this.dragging)
+ this.timeOutId = setTimeout(() => this.callDragService(), this.config.user_actions.drag_action.update_interval);
+ }
+
+ callTapService() {
+ if (typeof this.labelValue2 === 'undefined') return;
+
+ if (this.labelValuePrev !== this.labelValue2) {
+ this.labelValuePrev = this.labelValue2;
+
+ this._processTapEvent(
+ this._card,
+ this._card._hass,
+ this.config,
+ this.config.user_actions?.tap_action,
+ this._card.config.entities[this.defaultEntityIndex()]?.entity,
+ this.labelValue2,
+ );
+ }
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ firstUpdated(changedProperties) {
+ // const thisValue = this;
+ this.labelValue = this._stateValue;
+
+ // function Frame() {
+ // thisValue.rid = window.requestAnimationFrame(Frame);
+ // thisValue.updateValue(thisValue, thisValue.m);
+ // thisValue.updateThumb(thisValue, thisValue.m);
+ // thisValue.updateActiveTrack(thisValue, thisValue.m);
+ // }
+
+ function Frame2() {
+ this.rid = window.requestAnimationFrame(Frame2);
+ this.updateValue(this, this.m);
+ this.updateThumb(this, this.m);
+ this.updateActiveTrack(this, this.m);
+ }
+
+ function pointerMove(e) {
+ let scaleValue;
+
+ e.preventDefault();
+
+ if (this.dragging) {
+ this.m = this.mouseEventToPoint(e);
+
+ switch (this.config.position.orientation) {
+ case 'horizontal':
+ scaleValue = this.svgCoordinateToSliderValue(this, this.m);
+ this.m.x = this.valueToSvg(this, scaleValue);
+ this.m.x = Math.max(this.svg.scale.min, Math.min(this.m.x, this.svg.scale.max));
+ this.m.x = (Math.round(this.m.x / this.svg.scale.step) * this.svg.scale.step);
+ break;
+
+ case 'vertical':
+ scaleValue = this.svgCoordinateToSliderValue(this, this.m);
+ this.m.y = this.valueToSvg(this, scaleValue);
+ this.m.y = (Math.round(this.m.y / this.svg.scale.step) * this.svg.scale.step);
+ break;
+ }
+ Frame2.call(this);
+ }
+ }
+
+ if (this.dev.debug) console.log('slider - firstUpdated');
+ this.elements = {};
+ this.elements.svg = this._card.shadowRoot.getElementById('rangeslider-'.concat(this.toolId));
+ this.elements.capture = this.elements.svg.querySelector('#capture');
+ this.elements.track = this.elements.svg.querySelector('#rs-track');
+ this.elements.activeTrack = this.elements.svg.querySelector('#active-track');
+ this.elements.thumbGroup = this.elements.svg.querySelector('#rs-thumb-group');
+ this.elements.thumb = this.elements.svg.querySelector('#rs-thumb');
+ this.elements.label = this.elements.svg.querySelector('#rs-label tspan');
+
+ if (this.dev.debug) console.log('slider - firstUpdated svg = ', this.elements.svg, 'path=', this.elements.path, 'thumb=', this.elements.thumb, 'label=', this.elements.label, 'text=', this.elements.text);
+
+ function pointerDown(e) {
+ e.preventDefault();
+
+ // @NTS: Keep this comment for later!!
+ // Safari: We use mouse stuff for pointerdown, but have to use pointer stuff to make sliding work on Safari. WHY??
+ window.addEventListener('pointermove', pointerMove.bind(this), false);
+ // eslint-disable-next-line no-use-before-define
+ window.addEventListener('pointerup', pointerUp.bind(this), false);
+
+ // @NTS: Keep this comment for later!!
+ // Below lines prevent slider working on Safari...
+ //
+ // window.addEventListener('mousemove', pointerMove.bind(this), false);
+ // window.addEventListener('touchmove', pointerMove.bind(this), false);
+ // window.addEventListener('mouseup', pointerUp.bind(this), false);
+ // window.addEventListener('touchend', pointerUp.bind(this), false);
+
+ const mousePos = this.mouseEventToPoint(e);
+ const thumbPos = (this.svg.thumb.x1 + this.svg.thumb.cx);
+ if ((mousePos.x > (thumbPos - 10)) && (mousePos.x < (thumbPos + this.svg.thumb.width + 10))) {
+ ne(window, 'haptic', 'heavy');
+ } else {
+ ne(window, 'haptic', 'error');
+ return;
+ }
+
+ // User is dragging the thumb of the slider!
+ this.dragging = true;
+
+ // Check for drag_action. If none specified, or update_interval = 0, don't update while dragging...
+
+ if ((this.config.user_actions?.drag_action) && (this.config.user_actions?.drag_action.update_interval)) {
+ if (this.config.user_actions.drag_action.update_interval > 0) {
+ this.timeOutId = setTimeout(() => this.callDragService(), this.config.user_actions.drag_action.update_interval);
+ } else {
+ this.timeOutId = null;
+ }
+ }
+ this.m = this.mouseEventToPoint(e);
+
+ if (this.config.position.orientation === 'horizontal') {
+ this.m.x = (Math.round(this.m.x / this.svg.scale.step) * this.svg.scale.step);
+ } else {
+ this.m.y = (Math.round(this.m.y / this.svg.scale.step) * this.svg.scale.step);
+ }
+ if (this.dev.debug) console.log('pointerDOWN', Math.round(this.m.x * 100) / 100);
+ Frame2.call(this);
+ }
+
+ function pointerUp(e) {
+ e.preventDefault();
+
+ // @NTS: Keep this comment for later!!
+ // Safari: Fixes unable to grab pointer
+ window.removeEventListener('pointermove', pointerMove.bind(this), false);
+ window.removeEventListener('pointerup', pointerUp.bind(this), false);
+
+ window.removeEventListener('mousemove', pointerMove.bind(this), false);
+ window.removeEventListener('touchmove', pointerMove.bind(this), false);
+ window.removeEventListener('mouseup', pointerUp.bind(this), false);
+ window.removeEventListener('touchend', pointerUp.bind(this), false);
+
+ if (!this.dragging) return;
+
+ this.dragging = false;
+ clearTimeout(this.timeOutId);
+ this.target = 0;
+ if (this.dev.debug) console.log('pointerUP');
+ Frame2.call(this);
+ this.callTapService();
+ }
+
+ // @NTS: Keep this comment for later!!
+ // For things to work in Safari, we need separate touch and mouse down handlers...
+ // DON't ask WHY! The pointerdown method prevents listening on window events later on.
+ // ie, we can't move our finger
+
+ // this.elements.svg.addEventListener("pointerdown", pointerDown.bind(this), false);
+
+ this.elements.svg.addEventListener('touchstart', pointerDown.bind(this), false);
+ this.elements.svg.addEventListener('mousedown', pointerDown.bind(this), false);
+ }
+
+ /** *****************************************************************************
+ * RangeSliderTool::value()
+ *
+ * Summary.
+ * Receive new state data for the entity this rangeslider is linked to. Called from set hass;
+ * Sets the brightness value of the slider. This is a value 0..255. We display %, so translate
+ *
+ */
+ set value(state) {
+ super.value = state;
+ if (!this.dragging) this.labelValue = this._stateValue;
+ }
+
+ _renderUom() {
+ if (this.config.show.uom === 'none') {
+ return svg``;
+ } else {
+ this.MergeAnimationStyleIfChanged();
+ this.MergeColorFromState(this.styles.uom);
+
+ let fsuomStr = this.styles.label['font-size'];
+
+ let fsuomValue = 0.5;
+ let fsuomType = 'em';
+ const fsuomSplit = fsuomStr.match(/\D+|\d*\.?\d+/g);
+ if (fsuomSplit.length === 2) {
+ fsuomValue = Number(fsuomSplit[0]) * 0.6;
+ fsuomType = fsuomSplit[1];
+ } else console.error('Cannot determine font-size for state/unit', fsuomStr);
+
+ fsuomStr = { 'font-size': fsuomValue + fsuomType };
+
+ this.styles.uom = Merge.mergeDeep(this.config.styles.uom, fsuomStr);
+
+ const uom = this._card._buildUom(this.derivedEntity, this._card.entities[this.defaultEntityIndex()], this._card.config.entities[this.defaultEntityIndex()]);
+
+ // Check for location of uom. end = next to state, bottom = below state ;-), etc.
+ if (this.config.show.uom === 'end') {
+ return svg`
+
+ ${uom}
+ `;
+ } else if (this.config.show.uom === 'bottom') {
+ return svg`
+
+ ${uom}
+ `;
+ } else if (this.config.show.uom === 'top') {
+ return svg`
+
+ ${uom}
+ `;
+ } else {
+ return svg`
+
+ ERRR
+ `;
+ }
+ }
+ }
+
+ /** *****************************************************************************
+ * RangeSliderTool::_renderRangeSlider()
+ *
+ * Summary.
+ * Renders the range slider
+ *
+ */
+
+ _renderRangeSlider() {
+ if (this.dev.debug) console.log('slider - _renderRangeSlider');
+
+ this.MergeAnimationClassIfChanged();
+ // this.MergeColorFromState(this.styles);
+ // this.MergeAnimationStyleIfChanged(this.styles);
+ // this.MergeColorFromState(this.styles);
+
+ this.MergeColorFromState();
+ this.MergeAnimationStyleIfChanged();
+ this.MergeColorFromState();
+
+ // this.MergeAnimationStyleIfChanged();
+ // console.log("renderRangeSlider, styles", this.styles);
+
+ this.renderValue = this._stateValue;
+ if (this.dragging) {
+ this.renderValue = this.labelValue2;
+ } else if (this.elements?.label) this.elements.label.textContent = this.renderValue;
+
+ // Calculate cx and cy: the relative move of the thumb from the center of the track
+ let cx; let
+ cy;
+ switch (this.config.position.label.placement) {
+ case 'none':
+ this.styles.label.display = 'none';
+ this.styles.uom.display = 'none';
+ break;
+ case 'position':
+ cx = (this.config.position.orientation === 'horizontal'
+ ? this.valueToSvg(this, Number(this.renderValue)) - this.svg.cx
+ : 0);
+ cy = (this.config.position.orientation === 'vertical'
+ ? this.valueToSvg(this, Number(this.renderValue)) - this.svg.cy
+ : 0);
+ break;
+
+ case 'thumb':
+ cx = (this.config.position.orientation === 'horizontal'
+ ? -this.svg.label.cx + this.valueToSvg(this, Number(this.renderValue))
+ : 0);
+ cy = (this.config.position.orientation === 'vertical'
+ ? this.valueToSvg(this, Number(this.renderValue))
+ : 0);
+ // eslint-disable-next-line no-unused-expressions
+ if (this.dragging) { (this.config.position.orientation === 'horizontal') ? cy -= 50 : cx -= 50; }
+ break;
+
+ default:
+ console.error('_renderRangeSlider(), invalid label placement', this.config.position.label.placement);
+ }
+ this.svg.thumb.cx = cx;
+ this.svg.thumb.cy = cy;
+
+ function renderActiveTrack() {
+ if (!this.config.show.active) return svg``;
+
+ if (this.config.position.orientation === 'horizontal') {
+ return svg`
+ `;
+ } else {
+ return svg`
+ `;
+ }
+ }
+
+ function renderLabel(argGroup) {
+ if ((this.config.position.label.placement === 'thumb') && argGroup) {
+ return svg`
+
+
+ ${this.renderValue}
+ ${this._renderUom()}
+
+ `;
+ }
+
+ if ((this.config.position.label.placement === 'position') && !argGroup) {
+ return svg`
+
+ ${this.renderValue ? this.renderValue : ''}
+ ${this.renderValue ? this._renderUom() : ''}
+
+ `;
+ }
+ }
+
+ function renderThumbGroup() {
+ return svg`
+
+
+
+
+ ${renderLabel.call(this, true)}
+
+ `;
+ }
+
+ const svgItems = [];
+ svgItems.push(svg`
+
+
+
+
+ ${renderActiveTrack.call(this)}
+ ${renderThumbGroup.call(this)}
+ ${renderLabel.call(this, false)}
+
+
+ `);
+
+ return svgItems;
+ }
+
+ /** *****************************************************************************
+ * RangeSliderTool::render()
+ *
+ * Summary.
+ * The render() function for this object. The conversion of pointer events need
+ * an SVG as grouping object!
+ *
+ * NOTE:
+ * It is imperative that the style overflow=visible is set on the svg.
+ * The weird thing is that if using an svg as grouping object, AND a class, the overflow=visible
+ * seems to be ignored by both chrome and safari. If the overflow=visible is directly set as style,
+ * the setting works.
+ *
+ * Works on svg with direct styling:
+ * ---
+ * return svg`
+ *
+ * ${this._renderRangeSlider()}
+ *
+ * `;
+ *
+ * Does NOT work on svg with class styling:
+ * ---
+ * return svg`
+ *
+ * ${this._renderRangeSlider()}
+ *
+ * `;
+ * where the class has the overflow=visible setting...
+ *
+ */
+ render() {
+ return svg`
+
+ ${this._renderRangeSlider()}
+
+ `;
+ }
+} // END of class
+
+/** ****************************************************************************
+ * RectangleTool class
+ *
+ * Summary.
+ *
+ */
+
+class RectangleTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_RECTANGLE_CONFIG = {
+ position: {
+ cx: 50,
+ cy: 50,
+ width: 50,
+ height: 50,
+ rx: 0,
+ },
+ classes: {
+ tool: {
+ 'sak-rectangle': true,
+ hover: true,
+ },
+ rectangle: {
+ 'sak-rectangle__rectangle': true,
+ },
+ },
+ styles: {
+ rectangle: {
+ },
+ },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_RECTANGLE_CONFIG, argConfig), argPos);
+ this.svg.rx = argConfig.position.rx ? Utils.calculateSvgDimension(argConfig.position.rx) : 0;
+
+ this.classes.rectangle = {};
+ this.styles.rectangle = {};
+
+ if (this.dev.debug) console.log('RectangleTool constructor config, svg', this.toolId, this.config, this.svg);
+ }
+
+ /** *****************************************************************************
+ * RectangleTool::value()
+ *
+ * Summary.
+ * Receive new state data for the entity this rectangle is linked to. Called from set hass;
+ *
+ */
+ set value(state) {
+ super.value = state;
+ }
+
+ /** *****************************************************************************
+ * RectangleTool::_renderRectangle()
+ *
+ * Summary.
+ * Renders the circle using precalculated coordinates and dimensions.
+ * Only the runtime style is calculated before rendering the circle
+ *
+ */
+
+ _renderRectangle() {
+ this.MergeAnimationClassIfChanged();
+ this.MergeAnimationStyleIfChanged();
+ this.MergeColorFromState(this.styles.rectangle);
+
+ return svg`
+
+ `;
+ }
+
+ /** *****************************************************************************
+ * RectangleTool::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ */
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderRectangle()}
+
+ `;
+ }
+} // END of class
+
+/** ****************************************************************************
+ * RectangleToolEx class
+ *
+ * Summary.
+ *
+ */
+
+class RectangleToolEx extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_RECTANGLEEX_CONFIG = {
+ position: {
+ cx: 50,
+ cy: 50,
+ width: 50,
+ height: 50,
+ radius: {
+ all: 0,
+ },
+ },
+ classes: {
+ tool: {
+ 'sak-rectex': true,
+ hover: true,
+ },
+ rectex: {
+ 'sak-rectex__rectex': true,
+ },
+ },
+ styles: {
+ rectex: {
+ },
+ },
+ };
+ super(argToolset, Merge.mergeDeep(DEFAULT_RECTANGLEEX_CONFIG, argConfig), argPos);
+
+ this.classes.rectex = {};
+ this.styles.rectex = {};
+
+ // #TODO:
+ // Verify max radius, or just let it go, and let the user handle that right value.
+ // A q can be max height of rectangle, ie both corners added must be less than the height, but also less then the width...
+
+ const maxRadius = Math.min(this.svg.height, this.svg.width) / 2;
+ let radius = 0;
+ radius = Utils.calculateSvgDimension(this.config.position.radius.all);
+ this.svg.radiusTopLeft = +Math.min(maxRadius, Math.max(0, Utils.calculateSvgDimension(
+ this.config.position.radius.top_left || this.config.position.radius.left || this.config.position.radius.top || radius,
+ ))) || 0;
+
+ this.svg.radiusTopRight = +Math.min(maxRadius, Math.max(0, Utils.calculateSvgDimension(
+ this.config.position.radius.top_right || this.config.position.radius.right || this.config.position.radius.top || radius,
+ ))) || 0;
+
+ this.svg.radiusBottomLeft = +Math.min(maxRadius, Math.max(0, Utils.calculateSvgDimension(
+ this.config.position.radius.bottom_left || this.config.position.radius.left || this.config.position.radius.bottom || radius,
+ ))) || 0;
+
+ this.svg.radiusBottomRight = +Math.min(maxRadius, Math.max(0, Utils.calculateSvgDimension(
+ this.config.position.radius.bottom_right || this.config.position.radius.right || this.config.position.radius.bottom || radius,
+ ))) || 0;
+
+ if (this.dev.debug) console.log('RectangleToolEx constructor config, svg', this.toolId, this.config, this.svg);
+ }
+
+ /** *****************************************************************************
+ * RectangleToolEx::value()
+ *
+ */
+ set value(state) {
+ super.value = state;
+ }
+
+ /** *****************************************************************************
+ * RectangleToolEx::_renderRectangleEx()
+ *
+ * Summary.
+ * Renders the rectangle using lines and bezier curves with precalculated coordinates and dimensions.
+ *
+ * Refs for creating the path online:
+ * - https://mavo.io/demos/svgpath/
+ *
+ */
+
+ _renderRectangleEx() {
+ this.MergeAnimationClassIfChanged();
+
+ // WIP
+ this.MergeAnimationStyleIfChanged(this.styles);
+ this.MergeAnimationStyleIfChanged();
+ if (this.config.hasOwnProperty('csnew')) {
+ this.MergeColorFromState2(this.styles.rectex, 'rectex');
+ } else {
+ this.MergeColorFromState(this.styles.rectex);
+ }
+
+ if (!this.counter) { this.counter = 0; }
+ this.counter += 1;
+
+ const svgItems = svg`
+
+
+
+ `;
+ return svg`${svgItems}`;
+ }
+
+ /** *****************************************************************************
+ * RectangleToolEx::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ */
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderRectangleEx()}
+
+ `;
+ }
+} // END of class
+
+/** ****************************************************************************
+ * RegPolyTool class
+ *
+ * Summary.
+ *
+ */
+
+class RegPolyTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_REGPOLY_CONFIG = {
+ position: {
+ cx: 50,
+ cy: 50,
+ radius: 50,
+ side_count: 6,
+ side_skip: 1,
+ angle_offset: 0,
+ },
+ classes: {
+ tool: {
+ 'sak-polygon': true,
+ hover: true,
+ },
+ regpoly: {
+ 'sak-polygon__regpoly': true,
+ },
+ },
+ styles: {
+ tool: {
+ },
+ regpoly: {
+ },
+ },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_REGPOLY_CONFIG, argConfig), argPos);
+
+ this.svg.radius = Utils.calculateSvgDimension(argConfig.position.radius);
+
+ this.classes.regpoly = {};
+ this.styles.regpoly = {};
+ if (this.dev.debug) console.log('RegPolyTool constructor config, svg', this.toolId, this.config, this.svg);
+ }
+
+ /** *****************************************************************************
+ * RegPolyTool::value()
+ *
+ * Summary.
+ * Receive new state data for the entity this circle is linked to. Called from set hass;
+ *
+ */
+ set value(state) {
+ super.value = state;
+ }
+
+ /** *****************************************************************************
+ * RegPolyTool::_renderRegPoly()
+ *
+ * Summary.
+ * Renders the regular polygon using precalculated coordinates and dimensions.
+ * Only the runtime style is calculated before rendering the regular polygon
+ *
+ */
+
+ _renderRegPoly() {
+ const generatePoly = function (p, q, r, a, cx, cy) {
+ const base_angle = 2 * Math.PI / p;
+ let angle = a + base_angle;
+ let x; let y; let
+ d_attr = '';
+
+ for (let i = 0; i < p; i++) {
+ angle += q * base_angle;
+
+ // Use ~~ as it is faster then Math.floor()
+ x = cx + ~~(r * Math.cos(angle));
+ y = cy + ~~(r * Math.sin(angle));
+
+ d_attr
+ += `${((i === 0) ? 'M' : 'L') + x} ${y} `;
+
+ if (i * q % p === 0 && i > 0) {
+ angle += base_angle;
+ x = cx + ~~(r * Math.cos(angle));
+ y = cy + ~~(r * Math.sin(angle));
+
+ d_attr += `M${x} ${y} `;
+ }
+ }
+
+ d_attr += 'z';
+ return d_attr;
+ };
+
+ this.MergeAnimationStyleIfChanged();
+ this.MergeColorFromState(this.styles.regpoly);
+
+ return svg`
+
+ `;
+ }
+
+ /** *****************************************************************************
+ * RegPolyTool::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ */
+ // @click=${e => this._card.handlePopup(e, this._card.entities[this.defaultEntityIndex()])} >
+
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderRegPoly()}
+
+ `;
+ }
+} // END of class
+
+/** *****************************************************************************
+ * SegmentedArcTool class
+ *
+ * Summary.
+ *
+ */
+
+class SegmentedArcTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_SEGARC_CONFIG = {
+ position: {
+ cx: 50,
+ cy: 50,
+ radius: 45,
+ width: 3,
+ margin: 1.5,
+ },
+ color: 'var(--primary-color)',
+ classes: {
+ tool: {
+ },
+ foreground: {
+ },
+ background: {
+ },
+ },
+ styles: {
+ foreground: {
+ },
+ background: {
+ },
+ },
+ segments: {},
+ colorstops: [],
+ scale: {
+ min: 0,
+ max: 100,
+ width: 2,
+ offset: -3.5,
+ },
+ show: {
+ style: 'fixedcolor',
+ scale: false,
+ },
+ isScale: false,
+ animation: {
+ duration: 1.5,
+ },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_SEGARC_CONFIG, argConfig), argPos);
+
+ if (this.dev.performance) console.time(`--> ${this.toolId} PERFORMANCE SegmentedArcTool::constructor`);
+
+ this.svg.radius = Utils.calculateSvgDimension(argConfig.position.radius);
+ this.svg.radiusX = Utils.calculateSvgDimension(argConfig.position.radius_x || argConfig.position.radius);
+ this.svg.radiusY = Utils.calculateSvgDimension(argConfig.position.radius_y || argConfig.position.radius);
+
+ this.svg.segments = {};
+ // #TODO:
+ // Get gap from colorlist, colorstop or something else. Not from the default segments gap.
+ this.svg.segments.gap = Utils.calculateSvgDimension(this.config.segments.gap);
+ this.svg.scale_offset = Utils.calculateSvgDimension(this.config.scale.offset);
+
+ // Added for confusion???????
+ this._firstUpdatedCalled = false;
+
+ // Remember the values to be able to render from/to
+ this._stateValue = null;
+ this._stateValuePrev = null;
+ this._stateValueIsDirty = false;
+ this._renderFrom = null;
+ this._renderTo = null;
+
+ this.rAFid = null;
+ this.cancelAnimation = false;
+
+ this.arcId = null;
+
+ // Cache path (d= value) of segments drawn in map by segment index (counter). Simple array.
+ this._cache = [];
+
+ this._segmentAngles = [];
+ this._segments = {};
+
+ // Precalculate segments with start and end angle!
+ this._arc = {};
+ this._arc.size = Math.abs(this.config.position.end_angle - this.config.position.start_angle);
+ this._arc.clockwise = this.config.position.end_angle > this.config.position.start_angle;
+ this._arc.direction = this._arc.clockwise ? 1 : -1;
+
+ let tcolorlist = {};
+ let colorlist = null;
+ // New template testing for colorstops
+ if (this.config.segments.colorlist?.template) {
+ colorlist = this.config.segments.colorlist;
+ if (this._card.lovelace.config.sak_user_templates.templates[colorlist.template.name]) {
+ if (this.dev.debug) console.log('SegmentedArcTool::constructor - templates colorlist found', colorlist.template.name);
+ tcolorlist = Templates.replaceVariables2(colorlist.template.variables, this._card.lovelace.config.sak_user_templates.templates[colorlist.template.name]);
+ this.config.segments.colorlist = tcolorlist;
+ }
+ }
+
+ // FIXEDCOLOR
+ if (this.config.show.style === 'fixedcolor') ; else if (this.config.show.style === 'colorlist') {
+ // Get number of segments, and their size in degrees.
+ this._segments.count = this.config.segments.colorlist.colors.length;
+ this._segments.size = this._arc.size / this._segments.count;
+ this._segments.gap = (this.config.segments.colorlist.gap !== 'undefined') ? this.config.segments.colorlist.gap : 1;
+ this._segments.sizeList = [];
+ for (var i = 0; i < this._segments.count; i++) {
+ this._segments.sizeList[i] = this._segments.size;
+ }
+
+ // Use a running total for the size of the segments...
+ var segmentRunningSize = 0;
+ for (var i = 0; i < this._segments.count; i++) {
+ this._segmentAngles[i] = {
+ boundsStart: this.config.position.start_angle + (segmentRunningSize * this._arc.direction),
+ boundsEnd: this.config.position.start_angle + ((segmentRunningSize + this._segments.sizeList[i]) * this._arc.direction),
+ drawStart: this.config.position.start_angle + (segmentRunningSize * this._arc.direction) + (this._segments.gap * this._arc.direction),
+ drawEnd: this.config.position.start_angle + ((segmentRunningSize + this._segments.sizeList[i]) * this._arc.direction) - (this._segments.gap * this._arc.direction),
+ };
+ segmentRunningSize += this._segments.sizeList[i];
+ }
+
+ if (this.dev.debug) console.log('colorstuff - COLORLIST', this._segments, this._segmentAngles);
+
+ // COLORSTOPS
+ } else if (this.config.show.style === 'colorstops') {
+ // Get colorstops, remove outliers and make a key/value store...
+
+ this._segments.colorStops = {};
+ Object.keys(this.config.segments.colorstops.colors).forEach((key) => {
+ if ((key >= this.config.scale.min)
+ && (key <= this.config.scale.max))
+ this._segments.colorStops[key] = this.config.segments.colorstops.colors[key];
+ });
+
+ this._segments.sortedStops = Object.keys(this._segments.colorStops).map((n) => Number(n)).sort((a, b) => a - b);
+
+ // Insert extra stopcolor for max scale if not defined. Otherwise color calculations won't work as expected...
+ if (typeof (this._segments.colorStops[this.config.scale.max]) === 'undefined') {
+ this._segments.colorStops[this.config.scale.max] = this._segments.colorStops[this._segments.sortedStops[this._segments.sortedStops.length - 1]];
+ this._segments.sortedStops = Object.keys(this._segments.colorStops).map((n) => Number(n)).sort((a, b) => a - b);
+ }
+
+ this._segments.count = this._segments.sortedStops.length - 1;
+ this._segments.gap = this.config.segments.colorstops.gap !== 'undefined' ? this.config.segments.colorstops.gap : 1;
+
+ // Now depending on the colorstops and min/max values, calculate the size of each segment relative to the total arc size.
+ // First color in the list starts from Min!
+
+ let runningColorStop = this.config.scale.min;
+ const scaleRange = this.config.scale.max - this.config.scale.min;
+ this._segments.sizeList = [];
+ for (var i = 0; i < this._segments.count; i++) {
+ const colorSize = this._segments.sortedStops[i + 1] - runningColorStop;
+ runningColorStop += colorSize;
+ // Calculate fraction [0..1] of colorSize of min/max scale range
+ const fraction = colorSize / scaleRange;
+ const angleSize = fraction * this._arc.size;
+ this._segments.sizeList[i] = angleSize;
+ }
+
+ // Use a running total for the size of the segments...
+ var segmentRunningSize = 0;
+ for (var i = 0; i < this._segments.count; i++) {
+ this._segmentAngles[i] = {
+ boundsStart: this.config.position.start_angle + (segmentRunningSize * this._arc.direction),
+ boundsEnd: this.config.position.start_angle + ((segmentRunningSize + this._segments.sizeList[i]) * this._arc.direction),
+ drawStart: this.config.position.start_angle + (segmentRunningSize * this._arc.direction) + (this._segments.gap * this._arc.direction),
+ drawEnd: this.config.position.start_angle + ((segmentRunningSize + this._segments.sizeList[i]) * this._arc.direction) - (this._segments.gap * this._arc.direction),
+ };
+ segmentRunningSize += this._segments.sizeList[i];
+ if (this.dev.debug) console.log('colorstuff - COLORSTOPS++ segments', segmentRunningSize, this._segmentAngles[i]);
+ }
+
+ if (this.dev.debug) console.log('colorstuff - COLORSTOPS++', this._segments, this._segmentAngles, this._arc.direction, this._segments.count);
+
+ // SIMPLEGRADIENT
+ } else if (this.config.show.style === 'simplegradient') ;
+
+ // Just dump to console for verification. Nothing is used yet of the new calculation method...
+
+ if (this.config.isScale) {
+ this._stateValue = this.config.scale.max;
+ // this.config.show.scale = false;
+ } else {
+ // Nope. I'm the main arc. Check if a scale is defined and clone myself with some options...
+ if (this.config.show.scale) {
+ const scaleConfig = Merge.mergeDeep(this.config);
+ scaleConfig.id += '-scale';
+
+ // Cloning done. Now set specific scale options.
+ scaleConfig.show.scale = false;
+ scaleConfig.isScale = true;
+ scaleConfig.position.width = this.config.scale.width;
+ scaleConfig.position.radius = this.config.position.radius - (this.config.position.width / 2) + (scaleConfig.position.width / 2) + (this.config.scale.offset);
+ scaleConfig.position.radius_x = ((this.config.position.radius_x || this.config.position.radius)) - (this.config.position.width / 2) + (scaleConfig.position.width / 2) + (this.config.scale.offset);
+ scaleConfig.position.radius_y = ((this.config.position.radius_y || this.config.position.radius)) - (this.config.position.width / 2) + (scaleConfig.position.width / 2) + (this.config.scale.offset);
+
+ this._segmentedArcScale = new SegmentedArcTool(this, scaleConfig, argPos);
+ } else {
+ this._segmentedArcScale = null;
+ }
+ }
+
+ // testing. use below two lines and sckip the calculation of the segmentAngles. Those are done above with different calculation...
+ this.skipOriginal = ((this.config.show.style === 'colorstops') || (this.config.show.style === 'colorlist'));
+
+ // Set scale to new value. Never changes of course!!
+ if (this.skipOriginal) {
+ if (this.config.isScale) this._stateValuePrev = this._stateValue;
+ this._initialDraw = false;
+ }
+
+ this._arc.parts = Math.floor(this._arc.size / Math.abs(this.config.segments.dash));
+ this._arc.partsPartialSize = this._arc.size - (this._arc.parts * this.config.segments.dash);
+
+ if (this.skipOriginal) {
+ this._arc.parts = this._segmentAngles.length;
+ this._arc.partsPartialSize = 0;
+ } else {
+ for (var i = 0; i < this._arc.parts; i++) {
+ this._segmentAngles[i] = {
+ boundsStart: this.config.position.start_angle + (i * this.config.segments.dash * this._arc.direction),
+ boundsEnd: this.config.position.start_angle + ((i + 1) * this.config.segments.dash * this._arc.direction),
+ drawStart: this.config.position.start_angle + (i * this.config.segments.dash * this._arc.direction) + (this.config.segments.gap * this._arc.direction),
+ drawEnd: this.config.position.start_angle + ((i + 1) * this.config.segments.dash * this._arc.direction) - (this.config.segments.gap * this._arc.direction),
+ };
+ }
+ if (this._arc.partsPartialSize > 0) {
+ this._segmentAngles[i] = {
+ boundsStart: this.config.position.start_angle + (i * this.config.segments.dash * this._arc.direction),
+ boundsEnd: this.config.position.start_angle + ((i + 0) * this.config.segments.dash * this._arc.direction)
+ + (this._arc.partsPartialSize * this._arc.direction),
+
+ drawStart: this.config.position.start_angle + (i * this.config.segments.dash * this._arc.direction) + (this.config.segments.gap * this._arc.direction),
+ drawEnd: this.config.position.start_angle + ((i + 0) * this.config.segments.dash * this._arc.direction)
+ + (this._arc.partsPartialSize * this._arc.direction) - (this.config.segments.gap * this._arc.direction),
+ };
+ }
+ }
+
+ this.starttime = null;
+
+ if (this.dev.debug) console.log('SegmentedArcTool constructor coords, dimensions', this.coords, this.dimensions, this.svg, this.config);
+ if (this.dev.debug) console.log('SegmentedArcTool - init', this.toolId, this.config.isScale, this._segmentAngles);
+
+ if (this.dev.performance) console.timeEnd(`--> ${this.toolId} PERFORMANCE SegmentedArcTool::constructor`);
+ }
+
+ // SegmentedArcTool::objectId
+ get objectId() {
+ return this.toolId;
+ }
+
+ // SegmentedArcTool::value
+ set value(state) {
+ if (this.dev.debug) console.log('SegmentedArcTool - set value IN');
+
+ if (this.config.isScale) return false;
+
+ if (this._stateValue === state) return false;
+
+ const changed = super.value = state;
+
+ return changed;
+ }
+
+ // SegmentedArcTool::firstUpdated
+ // Me is updated. Get arc id for animations...
+ firstUpdated(changedProperties) {
+ if (this.dev.debug) console.log('SegmentedArcTool - firstUpdated IN with _arcId/id', this._arcId, this.toolId, this.config.isScale);
+ this._arcId = this._card.shadowRoot.getElementById('arc-'.concat(this.toolId));
+
+ this._firstUpdatedCalled = true;
+
+ // Just a try.
+ //
+ // was this a bug. The scale was never called with updated. Hence always no arcId...
+ this._segmentedArcScale?.firstUpdated(changedProperties);
+
+ if (this.skipOriginal) {
+ if (this.dev.debug) console.log('RENDERNEW - firstUpdated IN with _arcId/id/isScale/scale/connected', this._arcId, this.toolId, this.config.isScale, this._segmentedArcScale, this._card.connected);
+ if (!this.config.isScale) this._stateValuePrev = null;
+ this._initialDraw = true;
+ this._card.requestUpdate();
+ }
+ }
+
+ // SegmentedArcTool::updated
+
+ // eslint-disable-next-line no-unused-vars
+ updated(changedProperties) {
+ if (this.dev.debug) console.log('SegmentedArcTool - updated IN');
+ }
+
+ // SegmentedArcTool::render
+
+ render() {
+ if (this.dev.debug) console.log('SegmentedArcTool RENDERNEW - Render IN');
+ return svg`
+
+
+ ${this._renderSegments()}
+
+ ${this._renderScale()}
+
+ `;
+ }
+
+ _renderScale() {
+ if (this._segmentedArcScale) return this._segmentedArcScale.render();
+ }
+
+ _renderSegments() {
+ // migrate to new solution to draw segmented arc...
+
+ if (this.skipOriginal) {
+ // Here we can rebuild all needed. Much will be the same I guess...
+
+ let arcEnd;
+ let arcEndPrev;
+ const arcWidth = this.svg.width;
+ const arcRadiusX = this.svg.radiusX;
+ const arcRadiusY = this.svg.radiusY;
+
+ let d;
+
+ if (this.dev.debug) console.log('RENDERNEW - IN _arcId, firstUpdatedCalled', this._arcId, this._firstUpdatedCalled);
+ // calculate real end angle depending on value set in object and min/max scale
+ const val = Utils.calculateValueBetween(this.config.scale.min, this.config.scale.max, this._stateValue);
+ const valPrev = Utils.calculateValueBetween(this.config.scale.min, this.config.scale.max, this._stateValuePrev);
+ if (this.dev.debug) if (!this._stateValuePrev) console.log('*****UNDEFINED', this._stateValue, this._stateValuePrev, valPrev);
+ if (val !== valPrev) if (this.dev.debug) console.log('RENDERNEW _renderSegments diff value old new', this.toolId, valPrev, val);
+
+ arcEnd = (val * this._arc.size * this._arc.direction) + this.config.position.start_angle;
+ arcEndPrev = (valPrev * this._arc.size * this._arc.direction) + this.config.position.start_angle;
+
+ const svgItems = [];
+
+ // NO background needed for drawing scale...
+ if (!this.config.isScale) {
+ for (let k = 0; k < this._segmentAngles.length; k++) {
+ d = this.buildArcPath(
+ this._segmentAngles[k].drawStart,
+ this._segmentAngles[k].drawEnd,
+ this._arc.clockwise,
+ this.svg.radiusX,
+ this.svg.radiusY,
+ this.svg.width,
+ );
+
+ svgItems.push(svg` `);
+ }
+ }
+
+ // Check if arcId does exist
+ if (this._firstUpdatedCalled) {
+ // if ((this._arcId)) {
+ if (this.dev.debug) console.log('RENDERNEW _arcId DOES exist', this._arcId, this.toolId, this._firstUpdatedCalled);
+
+ // Render current from cache
+ this._cache.forEach((item, index) => {
+ d = item;
+
+ // extra, set color from colorlist as a test
+ if (this.config.isScale) {
+ let fill = this.config.color;
+ if (this.config.show.style === 'colorlist') {
+ fill = this.config.segments.colorlist.colors[index];
+ }
+ if (this.config.show.style === 'colorstops') {
+ fill = this._segments.colorStops[this._segments.sortedStops[index]];
+ // stroke = this.config.segments.colorstops.stroke ? this._segments.colorStops[this._segments.sortedStops[index]] : '';
+ }
+
+ if (!this.styles.foreground[index]) {
+ this.styles.foreground[index] = Merge.mergeDeep(this.config.styles.foreground);
+ }
+
+ this.styles.foreground[index].fill = fill;
+ // this.styles.foreground[index]['stroke'] = stroke;
+ }
+
+ svgItems.push(svg` `);
+ });
+
+ const tween = {};
+
+ // eslint-disable-next-line no-inner-declarations
+ function animateSegmentsNEW(timestamp, thisTool) {
+ // eslint-disable-next-line no-plusplus
+ const easeOut = (progress) => --progress ** 5 + 1;
+
+ let frameSegment;
+ let runningSegment;
+
+ var timestamp = timestamp || new Date().getTime();
+ if (!tween.startTime) {
+ tween.startTime = timestamp;
+ tween.runningAngle = tween.fromAngle;
+ }
+
+ if (thisTool.debug) console.log('RENDERNEW - in animateSegmentsNEW', thisTool.toolId, tween);
+
+ const runtime = timestamp - tween.startTime;
+ tween.progress = Math.min(runtime / tween.duration, 1);
+ tween.progress = easeOut(tween.progress);
+
+ const increase = ((thisTool._arc.clockwise)
+ ? (tween.toAngle > tween.fromAngle) : (tween.fromAngle > tween.toAngle));
+
+ // Calculate where the animation angle should be now in this animation frame: angle and segment.
+ tween.frameAngle = tween.fromAngle + ((tween.toAngle - tween.fromAngle) * tween.progress);
+ frameSegment = thisTool._segmentAngles.findIndex((currentValue, index) => (thisTool._arc.clockwise
+ ? ((tween.frameAngle <= currentValue.boundsEnd) && (tween.frameAngle >= currentValue.boundsStart))
+ : ((tween.frameAngle <= currentValue.boundsStart) && (tween.frameAngle >= currentValue.boundsEnd))));
+
+ if (frameSegment === -1) {
+ /* if (thisTool.debug) */ console.log('RENDERNEW animateSegments frameAngle not found', tween, thisTool._segmentAngles);
+ console.log('config', thisTool.config);
+ }
+
+ // Check where we actually are now. This might be in a different segment...
+ runningSegment = thisTool._segmentAngles.findIndex((currentValue, index) => (thisTool._arc.clockwise
+ ? ((tween.runningAngle <= currentValue.boundsEnd) && (tween.runningAngle >= currentValue.boundsStart))
+ : ((tween.runningAngle <= currentValue.boundsStart) && (tween.runningAngle >= currentValue.boundsEnd))));
+
+ // Weird stuff. runningSegment is sometimes -1. Ie not FOUND !! WTF??
+ // if (runningSegment == -1) runningSegment = 0;
+
+ // Do render segments until the animation angle is at the requested animation frame angle.
+ do {
+ const aniStartAngle = thisTool._segmentAngles[runningSegment].drawStart;
+ var runningSegmentAngle = thisTool._arc.clockwise
+ ? Math.min(thisTool._segmentAngles[runningSegment].boundsEnd, tween.frameAngle)
+ : Math.max(thisTool._segmentAngles[runningSegment].boundsEnd, tween.frameAngle);
+ const aniEndAngle = thisTool._arc.clockwise
+ ? Math.min(thisTool._segmentAngles[runningSegment].drawEnd, tween.frameAngle)
+ : Math.max(thisTool._segmentAngles[runningSegment].drawEnd, tween.frameAngle);
+ // First phase. Just draw and ignore segments...
+ d = thisTool.buildArcPath(aniStartAngle, aniEndAngle, thisTool._arc.clockwise, arcRadiusX, arcRadiusY, arcWidth);
+
+ if (!thisTool.myarc) thisTool.myarc = {};
+ if (!thisTool.as) thisTool.as = {};
+
+ let as;
+ const myarc = 'arc-segment-'.concat(thisTool.toolId).concat('-').concat(runningSegment);
+ // as = thisTool._card.shadowRoot.getElementById(myarc);
+ if (!thisTool.as[runningSegment])
+ thisTool.as[runningSegment] = thisTool._card.shadowRoot.getElementById(myarc);
+ as = thisTool.as[runningSegment];
+ // Extra. Remember id's and references
+ // Quick hack...
+ thisTool.myarc[runningSegment] = myarc;
+ // thisTool.as[runningSegment] = as;
+
+ if (as) {
+ // var e = as.getAttribute("d");
+ as.setAttribute('d', d);
+
+ // We also have to set the style fill if the color stops and gradients are implemented
+ // As we're using styles, attributes won't work. Must use as.style.fill = 'calculated color'
+ // #TODO
+ // Can't use gradients probably because of custom path. Conic-gradient would be fine.
+ //
+ // First try...
+ if (thisTool.config.show.style === 'colorlist') {
+ as.style.fill = thisTool.config.segments.colorlist.colors[runningSegment];
+ thisTool.styles.foreground[runningSegment].fill = thisTool.config.segments.colorlist.colors[runningSegment];
+ }
+ // #WIP
+ // Testing 'lastcolor'
+ if (thisTool.config.show.lastcolor) {
+ var fill;
+
+ const boundsStart = thisTool._arc.clockwise
+ ? (thisTool._segmentAngles[runningSegment].drawStart)
+ : (thisTool._segmentAngles[runningSegment].drawEnd);
+ const boundsEnd = thisTool._arc.clockwise
+ ? (thisTool._segmentAngles[runningSegment].drawEnd)
+ : (thisTool._segmentAngles[runningSegment].drawStart);
+ const value = Math.min(Math.max(0, (runningSegmentAngle - boundsStart) / (boundsEnd - boundsStart)), 1);
+ // 2022.07.03 Fixing lastcolor for true stop
+ if (thisTool.config.show.style === 'colorstops') {
+ fill = Colors.getGradientValue(
+ thisTool._segments.colorStops[thisTool._segments.sortedStops[runningSegment]],
+ thisTool._segments.colorStops[thisTool._segments.sortedStops[runningSegment]],
+ value,
+ );
+ } else {
+ // 2022.07.12 Fix bug as this is no colorstops, but a colorlist!!!!
+ if (thisTool.config.show.style === 'colorlist') {
+ fill = thisTool.config.segments.colorlist.colors[runningSegment];
+ }
+ }
+
+ thisTool.styles.foreground[0].fill = fill;
+ thisTool.as[0].style.fill = fill;
+
+ if (runningSegment > 0) {
+ for (let j = runningSegment; j >= 0; j--) { // +1
+ if (thisTool.styles.foreground[j].fill !== fill) {
+ thisTool.styles.foreground[j].fill = fill;
+ thisTool.as[j].style.fill = fill;
+ }
+ thisTool.styles.foreground[j].fill = fill;
+ thisTool.as[j].style.fill = fill;
+ }
+ }
+ }
+ }
+ thisTool._cache[runningSegment] = d;
+
+ // If at end of animation, don't do the add to force going to next segment
+ if (tween.frameAngle !== runningSegmentAngle) {
+ runningSegmentAngle += (0.000001 * thisTool._arc.direction);
+ }
+
+ var runningSegmentPrev = runningSegment;
+ runningSegment = thisTool._segmentAngles.findIndex((currentValue, index) => (thisTool._arc.clockwise
+ ? ((runningSegmentAngle <= currentValue.boundsEnd) && (runningSegmentAngle >= currentValue.boundsStart))
+ : ((runningSegmentAngle <= currentValue.boundsStart) && (runningSegmentAngle >= currentValue.boundsEnd))));
+
+ if (!increase) {
+ if (runningSegmentPrev !== runningSegment) {
+ if (thisTool.debug) console.log('RENDERNEW movit - remove path', thisTool.toolId, runningSegmentPrev);
+ if (thisTool._arc.clockwise) {
+ as.removeAttribute('d');
+ thisTool._cache[runningSegmentPrev] = null;
+ } else {
+ as.removeAttribute('d');
+ thisTool._cache[runningSegmentPrev] = null;
+ }
+ }
+ }
+ tween.runningAngle = runningSegmentAngle;
+ if (thisTool.debug) console.log('RENDERNEW - animation loop tween', thisTool.toolId, tween, runningSegment, runningSegmentPrev);
+ } while ((tween.runningAngle !== tween.frameAngle) /* && (runningSegment == runningSegmentPrev) */);
+
+ // NTS @ 2020.10.14
+ // In a fast paced animation - say 10msec - multiple segments should be drawn,
+ // while tween.progress already has the value of 1.
+ // This means only the first segment is drawn - due to the "&& (runningSegment == runningSegmentPrev)" test above.
+ // To fix this:
+ // - either remove that test (why was it there????)... Or
+ // - add the line "|| (runningSegment != runningSegmentPrev)" to the if() below to make sure another animation frame is requested
+ // although tween.progress == 1.
+ if ((tween.progress !== 1) /* || (runningSegment != runningSegmentPrev) */) {
+ // eslint-disable-next-line no-undef
+ thisTool.rAFid = requestAnimationFrame((timestamp) => {
+ animateSegmentsNEW(timestamp, thisTool);
+ });
+ } else {
+ tween.startTime = null;
+ if (thisTool.debug) console.log('RENDERNEW - animation loop ENDING tween', thisTool.toolId, tween, runningSegment, runningSegmentPrev);
+ }
+ } // function animateSegmentsNEW
+
+ const mySelf = this;
+ // 2021.10.31
+ // Edge case where brightness percentage is set to undefined (attribute is gone) if light is set to off.
+ // Now if light is switched on again, the brightness is set to old value, and val and valPrev are the same again, so NO drawing!!!!!
+ //
+ // Remove test for val/valPrev...
+
+ // Check if values changed and we should animate to another target then previously rendered
+ if (/* (val != valPrev) && */ (this._card.connected === true) && (this._renderTo !== this._stateValue)) {
+ // if ( (val != valPrev) && (this._card.connected == true) && (this._renderTo != this._stateValue)) {
+ this._renderTo = this._stateValue;
+ // if (this.dev.debug) console.log('RENDERNEW val != valPrev', val, valPrev, 'prev/end/cur', arcEndPrev, arcEnd, arcCur);
+
+ // If previous animation active, cancel this one before starting a new one...
+ if (this.rAFid) {
+ // if (this.dev.debug) console.log('RENDERNEW canceling rAFid', this._card.cardId, this.toolId, 'rAFid', this.rAFid);
+ // eslint-disable-next-line no-undef
+ cancelAnimationFrame(this.rAFid);
+ }
+
+ // Start new animation with calculated settings...
+ // counter var not defined???
+ // if (this.dev.debug) console.log('starting animationframe timer...', this._card.cardId, this.toolId, counter);
+ tween.fromAngle = arcEndPrev;
+ tween.toAngle = arcEnd;
+ tween.runningAngle = arcEndPrev;
+
+ // @2021.10.31
+ // Handle edge case where - for some reason - arcEnd and arcEndPrev are equal.
+ // Do NOT render anything in this case to prevent errors...
+
+ // The check is removed temporarily. Brightness is again not shown for light. Still the same problem...
+
+ // eslint-disable-next-line no-constant-condition
+ {
+ // Render like an idiot the first time. Performs MUCH better @first load then having a zillion animations...
+ // NOt so heavy on an average PC, but my iPad and iPhone need some more time for this!
+
+ tween.duration = Math.min(Math.max(this._initialDraw ? 100 : 500, this._initialDraw ? 16 : this.config.animation.duration * 1000), 5000);
+ tween.startTime = null;
+ if (this.dev.debug) console.log('RENDERNEW - tween', this.toolId, tween);
+ // this._initialDraw = false;
+ // eslint-disable-next-line no-undef
+ this.rAFid = requestAnimationFrame((timestamp) => {
+ animateSegmentsNEW(timestamp, mySelf);
+ });
+ this._initialDraw = false;
+ }
+ }
+
+ return svg`${svgItems}`;
+ } else {
+ // Initial FIRST draw.
+ // What if we 'abuse' the animation to do this, and we just create empty elements.
+ // Then we don't have to do difficult things.
+ // Just set some values to 0 and 'force' a full animation...
+ //
+ // Hmm. Stuff is not yet rendered, so DOM objects don't exist yet. How can we abuse the
+ // animation function to do the drawing then??
+ // --> Can use firstUpdated perhaps?? That was the first render, then do the first actual draw??
+ //
+
+ if (this.dev.debug) console.log('RENDERNEW _arcId does NOT exist', this._arcId, this.toolId);
+
+ // Create empty elements, so no problem in animation function. All path's exist...
+ // An empty element has a width of 0!
+ for (let i = 0; i < this._segmentAngles.length; i++) {
+ d = this.buildArcPath(
+ this._segmentAngles[i].drawStart,
+ this._segmentAngles[i].drawEnd,
+ this._arc.clockwise,
+ this.svg.radiusX,
+ this.svg.radiusY,
+ this.config.isScale ? this.svg.width : 0,
+ );
+
+ this._cache[i] = d;
+
+ // extra, set color from colorlist as a test
+ let fill = this.config.color;
+ if (this.config.show.style === 'colorlist') {
+ fill = this.config.segments.colorlist.colors[i];
+ }
+ if (this.config.show.style === 'colorstops') {
+ fill = this._segments.colorStops[this._segments.sortedStops[i]];
+ }
+ // style="${styleMap(this.config.styles.foreground)} fill: ${fill};"
+ if (!this.styles.foreground) {
+ this.styles.foreground = {};
+ }
+ if (!this.styles.foreground[i]) {
+ this.styles.foreground[i] = Merge.mergeDeep(this.config.styles.foreground);
+ }
+ this.styles.foreground[i].fill = fill;
+
+ // #WIP
+ // Testing 'lastcolor'
+ if (this.config.show.lastcolor) {
+ if (i > 0) {
+ for (let j = i - 1; j > 0; j--) {
+ this.styles.foreground[j].fill = fill;
+ }
+ }
+ }
+
+ svgItems.push(svg` `);
+ }
+
+ if (this.dev.debug) console.log('RENDERNEW - svgItems', svgItems, this._firstUpdatedCalled);
+ return svg`${svgItems}`;
+ }
+
+ // END OF NEW METHOD OF RENDERING
+ }
+ }
+
+ polarToCartesian(centerX, centerY, radiusX, radiusY, angleInDegrees) {
+ const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;
+
+ return {
+ x: centerX + (radiusX * Math.cos(angleInRadians)),
+ y: centerY + (radiusY * Math.sin(angleInRadians)),
+ };
+ }
+
+ /*
+ *
+ * start = 10, end = 30, clockwise -> size is 20
+ * start = 10, end = 30, anticlockwise -> size is (360 - 20) = 340
+ *
+ *
+ */
+ buildArcPath(argStartAngle, argEndAngle, argClockwise, argRadiusX, argRadiusY, argWidth) {
+ const start = this.polarToCartesian(this.svg.cx, this.svg.cy, argRadiusX, argRadiusY, argEndAngle);
+ const end = this.polarToCartesian(this.svg.cx, this.svg.cy, argRadiusX, argRadiusY, argStartAngle);
+ const largeArcFlag = Math.abs(argEndAngle - argStartAngle) <= 180 ? '0' : '1';
+
+ const sweepFlag = argClockwise ? '0' : '1';
+
+ const cutoutRadiusX = argRadiusX - argWidth;
+ const cutoutRadiusY = argRadiusY - argWidth;
+ const start2 = this.polarToCartesian(this.svg.cx, this.svg.cy, cutoutRadiusX, cutoutRadiusY, argEndAngle);
+ const end2 = this.polarToCartesian(this.svg.cx, this.svg.cy, cutoutRadiusX, cutoutRadiusY, argStartAngle);
+
+ const d = [
+ 'M', start.x, start.y,
+ 'A', argRadiusX, argRadiusY, 0, largeArcFlag, sweepFlag, end.x, end.y,
+ 'L', end2.x, end2.y,
+ 'A', cutoutRadiusX, cutoutRadiusY, 0, largeArcFlag, sweepFlag === '0' ? '1' : '0', start2.x, start2.y,
+ 'Z',
+ ].join(' ');
+ return d;
+ }
+} // END of class
+
+/** ****************************************************************************
+ * SparklineBarChartTool class
+ *
+ * Summary.
+ *
+ */
+class SparklineBarChartTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_BARCHART_CONFIG = {
+ position: {
+ cx: 50,
+ cy: 50,
+ height: 25,
+ width: 25,
+ margin: 0.5,
+ orientation: 'vertical',
+ },
+ hours: 24,
+ barhours: 1,
+ color: 'var(--primary-color)',
+ classes: {
+ tool: {
+ 'sak-barchart': true,
+ hover: true,
+ },
+ bar: {
+ },
+ line: {
+ 'sak-barchart__line': true,
+ hover: true,
+ },
+ },
+ styles: {
+ tool: {
+ },
+ line: {
+ },
+ bar: {
+ },
+ },
+ colorstops: [],
+ show: { style: 'fixedcolor' },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_BARCHART_CONFIG, argConfig), argPos);
+
+ this.svg.margin = Utils.calculateSvgDimension(this.config.position.margin);
+ const theWidth = (this.config.position.orientation === 'vertical') ? this.svg.width : this.svg.height;
+
+ this.svg.barWidth = (theWidth - (((this.config.hours / this.config.barhours) - 1)
+ * this.svg.margin)) / (this.config.hours / this.config.barhours);
+ this._data = [];
+ this._bars = [];
+ this._scale = {};
+ this._needsRendering = false;
+
+ this.classes.bar = {};
+
+ this.styles.tool = {};
+ this.styles.line = {};
+ this.stylesBar = {};
+
+ if (this.dev.debug) console.log('SparkleBarChart constructor coords, dimensions', this.coords, this.dimensions, this.svg, this.config);
+ }
+
+ /** *****************************************************************************
+ * SparklineBarChartTool::computeMinMax()
+ *
+ * Summary.
+ * Compute min/max values of bars to scale them to the maximum amount.
+ *
+ */
+ computeMinMax() {
+ let min = this._series[0]; let
+ max = this._series[0];
+
+ for (let i = 1, len = this._series.length; i < len; i++) {
+ const v = this._series[i];
+ min = (v < min) ? v : min;
+ max = (v > max) ? v : max;
+ }
+ this._scale.min = min;
+ this._scale.max = max;
+ this._scale.size = (max - min);
+
+ // 2020.11.05
+ // Add 5% to the size of the scale and adjust the minimum value displayed.
+ // So every bar is displayed, instead of the min value having a bar length of zero!
+ this._scale.size = (max - min) * 1.05;
+ this._scale.min = max - this._scale.size;
+ }
+
+ /** *****************************************************************************
+ * SparklineBarChartTool::set series
+ *
+ * Summary.
+ * Sets the timeseries for the barchart tool. Is an array of states.
+ * If this is historical data, the caller has taken the time to create this.
+ * This tool only displays the result...
+ *
+ */
+ set data(states) {
+ this._series = Object.assign(states);
+ this.computeBars();
+ this._needsRendering = true;
+ }
+
+ set series(states) {
+ this._series = Object.assign(states);
+ this.computeBars();
+ this._needsRendering = true;
+ }
+
+ hasSeries() {
+ return this.defaultEntityIndex();
+ }
+
+ /** *****************************************************************************
+ * SparklineBarChartTool::computeBars()
+ *
+ * Summary.
+ * Compute start and end of bars for easy rendering.
+ *
+ */
+ computeBars({ _bars } = this) {
+ this.computeMinMax();
+
+ if (this.config.show.style === 'minmaxgradient') {
+ this.colorStopsMinMax = {};
+ this.colorStopsMinMax = {
+ [this._scale.min.toString()]: this.config.minmaxgradient.colors.min,
+ [this._scale.max.toString()]: this.config.minmaxgradient.colors.max,
+ };
+ }
+
+ // VERTICAL
+ if (this.config.position.orientation === 'vertical') {
+ if (this.dev.debug) console.log('bar is vertical');
+ this._series.forEach((item, index) => {
+ if (!_bars[index]) _bars[index] = {};
+ _bars[index].length = (this._scale.size === 0) ? 0 : ((item - this._scale.min) / (this._scale.size)) * this.svg.height;
+ _bars[index].x1 = this.svg.x + this.svg.barWidth / 2 + ((this.svg.barWidth + this.svg.margin) * index);
+ _bars[index].x2 = _bars[index].x1;
+ _bars[index].y1 = this.svg.y + this.svg.height;
+ _bars[index].y2 = _bars[index].y1 - this._bars[index].length;
+ _bars[index].dataLength = this._bars[index].length;
+ });
+ // HORIZONTAL
+ } else if (this.config.position.orientation === 'horizontal') {
+ if (this.dev.debug) console.log('bar is horizontal');
+ this._data.forEach((item, index) => {
+ if (!_bars[index]) _bars[index] = {};
+ // if (!item || isNaN(item)) item = this._scale.min;
+ _bars[index].length = (this._scale.size === 0) ? 0 : ((item - this._scale.min) / (this._scale.size)) * this.svg.width;
+ _bars[index].y1 = this.svg.y + this.svg.barWidth / 2 + ((this.svg.barWidth + this.svg.margin) * index);
+ _bars[index].y2 = _bars[index].y1;
+ _bars[index].x1 = this.svg.x;
+ _bars[index].x2 = _bars[index].x1 + this._bars[index].length;
+ _bars[index].dataLength = this._bars[index].length;
+ });
+ } else if (this.dev.debug) console.log('SparklineBarChartTool - unknown barchart orientation (horizontal or vertical)');
+ }
+
+ /** *****************************************************************************
+ * SparklineBarChartTool::_renderBars()
+ *
+ * Summary.
+ * Render all the bars. Number of bars depend on hours and barhours settings.
+ *
+ */
+ // _renderBars({ _bars } = this) {
+ _renderBars() {
+ const svgItems = [];
+
+ if (this._bars.length === 0) return;
+
+ if (this.dev.debug) console.log('_renderBars IN', this.toolId);
+
+ this._bars.forEach((item, index) => {
+ if (this.dev.debug) console.log('_renderBars - bars', item, index);
+
+ const stroke = this.getColorFromState(this._series[index]);
+
+ if (!this.stylesBar[index])
+ this.stylesBar[index] = { ...this.config.styles.bar };
+
+ // NOTE @ 2021.10.27
+ // Lijkt dat this.classes niet gevuld wordt. geen merge enzo. is dat een bug?
+ // Nu tijdelijk opgelost door this.config te gebruiken, maar hoort niet natuurlijk als je kijkt
+ // naar de andere tools...
+
+ // Safeguard...
+ if (!(this._bars[index].y2)) console.log('sparklebarchart y2 invalid', this._bars[index]);
+ svgItems.push(svg`
+
+ `);
+ });
+ if (this.dev.debug) console.log('_renderBars OUT', this.toolId);
+
+ return svg`${svgItems}`;
+ }
+
+ /** *****************************************************************************
+ * SparklineBarChartTool::render()
+ *
+ * Summary.
+ * The actual render() function called by the card for each tool.
+ *
+ */
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderBars()}
+
+ `;
+ }
+}
+
+/** ****************************************************************************
+ * SwitchTool class
+ *
+ * Summary.
+ *
+ *
+ * NTS:
+ * - .mdc-switch__native-control uses:
+ * - width: 68px, 17em
+ * - height: 48px, 12em
+ * - and if checked (.mdc-switch--checked):
+ * - transform: translateX(-20px)
+ *
+ * .mdc-switch.mdc-switch--checked .mdc-switch__thumb {
+ * background-color: var(--switch-checked-button-color);
+ * border-color: var(--switch-checked-button-color);
+ *
+ */
+
+class SwitchTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_SWITCH_CONFIG = {
+ position: {
+ cx: 50,
+ cy: 50,
+ orientation: 'horizontal',
+ track: {
+ width: 16,
+ height: 7,
+ radius: 3.5,
+ },
+ thumb: {
+ width: 9,
+ height: 9,
+ radius: 4.5,
+ offset: 4.5,
+ },
+ },
+ classes: {
+ tool: {
+ 'sak-switch': true,
+ hover: true,
+ },
+ track: {
+ 'sak-switch__track': true,
+ },
+ thumb: {
+ 'sak-switch__thumb': true,
+ },
+ },
+ styles: {
+ tool: {
+ },
+ track: {
+ },
+ thumb: {
+ },
+ },
+ };
+
+ const HORIZONTAL_SWITCH_CONFIG = {
+ animations: [
+ {
+ state: 'on',
+ id: 1,
+ styles: {
+ track: {
+ fill: 'var(--switch-checked-track-color)',
+ 'pointer-events': 'auto',
+ },
+ thumb: {
+ fill: 'var(--switch-checked-button-color)',
+ transform: 'translateX(4.5em)',
+ 'pointer-events': 'auto',
+ },
+ },
+ },
+ {
+ state: 'off',
+ id: 0,
+ styles: {
+ track: {
+ fill: 'var(--switch-unchecked-track-color)',
+ 'pointer-events': 'auto',
+ },
+ thumb: {
+ fill: 'var(--switch-unchecked-button-color)',
+ transform: 'translateX(-4.5em)',
+ 'pointer-events': 'auto',
+ },
+ },
+ },
+ ],
+ };
+
+ const VERTICAL_SWITCH_CONFIG = {
+ animations: [
+ {
+ state: 'on',
+ id: 1,
+ styles: {
+ track: {
+ fill: 'var(--switch-checked-track-color)',
+ 'pointer-events': 'auto',
+ },
+ thumb: {
+ fill: 'var(--switch-checked-button-color)',
+ transform: 'translateY(-4.5em)',
+ 'pointer-events': 'auto',
+ },
+ },
+ },
+ {
+ state: 'off',
+ id: 0,
+ styles: {
+ track: {
+ fill: 'var(--switch-unchecked-track-color)',
+ 'pointer-events': 'auto',
+ },
+ thumb: {
+ fill: 'var(--switch-unchecked-button-color)',
+ transform: 'translateY(4.5em)',
+ 'pointer-events': 'auto',
+ },
+ },
+ },
+ ],
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_SWITCH_CONFIG, argConfig), argPos);
+
+ if (!['horizontal', 'vertical'].includes(this.config.position.orientation))
+ throw Error('SwitchTool::constructor - invalid orientation [vertical, horizontal] = ', this.config.position.orientation);
+
+ this.svg.track = {};
+ this.svg.track.radius = Utils.calculateSvgDimension(this.config.position.track.radius);
+
+ this.svg.thumb = {};
+ this.svg.thumb.radius = Utils.calculateSvgDimension(this.config.position.thumb.radius);
+ this.svg.thumb.offset = Utils.calculateSvgDimension(this.config.position.thumb.offset);
+
+ switch (this.config.position.orientation) {
+ // eslint-disable-next-line default-case-last
+ default:
+ case 'horizontal':
+ this.config = Merge.mergeDeep(DEFAULT_SWITCH_CONFIG, HORIZONTAL_SWITCH_CONFIG, argConfig);
+
+ this.svg.track.width = Utils.calculateSvgDimension(this.config.position.track.width);
+ this.svg.track.height = Utils.calculateSvgDimension(this.config.position.track.height);
+ this.svg.thumb.width = Utils.calculateSvgDimension(this.config.position.thumb.width);
+ this.svg.thumb.height = Utils.calculateSvgDimension(this.config.position.thumb.height);
+
+ this.svg.track.x1 = this.svg.cx - this.svg.track.width / 2;
+ this.svg.track.y1 = this.svg.cy - this.svg.track.height / 2;
+
+ this.svg.thumb.x1 = this.svg.cx - this.svg.thumb.width / 2;
+ this.svg.thumb.y1 = this.svg.cy - this.svg.thumb.height / 2;
+ break;
+
+ case 'vertical':
+ this.config = Merge.mergeDeep(DEFAULT_SWITCH_CONFIG, VERTICAL_SWITCH_CONFIG, argConfig);
+
+ this.svg.track.width = Utils.calculateSvgDimension(this.config.position.track.height);
+ this.svg.track.height = Utils.calculateSvgDimension(this.config.position.track.width);
+ this.svg.thumb.width = Utils.calculateSvgDimension(this.config.position.thumb.height);
+ this.svg.thumb.height = Utils.calculateSvgDimension(this.config.position.thumb.width);
+
+ this.svg.track.x1 = this.svg.cx - this.svg.track.width / 2;
+ this.svg.track.y1 = this.svg.cy - this.svg.track.height / 2;
+
+ this.svg.thumb.x1 = this.svg.cx - this.svg.thumb.width / 2;
+ this.svg.thumb.y1 = this.svg.cy - this.svg.thumb.height / 2;
+ break;
+ }
+
+ this.classes.track = {};
+ this.classes.thumb = {};
+
+ this.styles.track = {};
+ this.styles.thumb = {};
+ if (this.dev.debug) console.log('SwitchTool constructor config, svg', this.toolId, this.config, this.svg);
+ }
+
+ /** *****************************************************************************
+ * SwitchTool::value()
+ *
+ * Summary.
+ * Receive new state data for the entity this switch is linked to. Called from set hass;
+ *
+ */
+ set value(state) {
+ super.value = state;
+ }
+
+ /**
+ * SwitchTool::_renderSwitch()
+ *
+ * Summary.
+ * Renders the switch using precalculated coordinates and dimensions.
+ * Only the runtime style is calculated before rendering the switch
+ *
+ */
+
+ _renderSwitch() {
+ this.MergeAnimationClassIfChanged();
+ // this.MergeColorFromState(this.styles);
+ this.MergeAnimationStyleIfChanged(this.styles);
+ // this.MergeAnimationStyleIfChanged(this.styles.thumb);
+
+ return svg`
+
+
+
+
+ `;
+ }
+
+ /** *****************************************************************************
+ * SwitchTool::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ * https://codepen.io/joegaffey/pen/vrVZaN
+ *
+ */
+
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderSwitch()}
+
+ `;
+ }
+} // END of class
+
+/** ****************************************************************************
+ * TextTool class
+ *
+ * Summary.
+ *
+ */
+
+class TextTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_TEXT_CONFIG = {
+ classes: {
+ tool: {
+ 'sak-text': true,
+ },
+ text: {
+ 'sak-text__text': true,
+ hover: false,
+ },
+ },
+ styles: {
+ tool: {
+ },
+ text: {
+ },
+ },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_TEXT_CONFIG, argConfig), argPos);
+
+ this.EnableHoverForInteraction();
+ this.text = this.config.text;
+ this.styles.text = {};
+ if (this.dev.debug) console.log('TextTool constructor coords, dimensions', this.coords, this.dimensions, this.svg, this.config);
+ }
+
+ /** *****************************************************************************
+ * TextTool::_renderText()
+ *
+ * Summary.
+ * Renders the text using precalculated coordinates and dimensions.
+ * Only the runtime style is calculated before rendering the text
+ *
+ */
+
+ _renderText() {
+ this.MergeAnimationClassIfChanged();
+ this.MergeColorFromState(this.styles.text);
+ this.MergeAnimationStyleIfChanged();
+
+ return svg`
+
+ ${this.text}
+
+ `;
+ }
+
+ /** *****************************************************************************
+ * TextTool::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ */
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderText()}
+
+ `;
+ }
+} // END of class
+
+/******************************************************************************
+Copyright (c) Microsoft Corporation.
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
+***************************************************************************** */
+
+function __spreadArray(to, from, pack) {
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
+ if (ar || !(i in from)) {
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
+ ar[i] = from[i];
+ }
+ }
+ return to.concat(ar || Array.prototype.slice.call(from));
+}
+
+/*!
+ * content-type
+ * Copyright(c) 2015 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+/**
+ * RegExp to match *( ";" parameter ) in RFC 7231 sec 3.1.1.1
+ *
+ * parameter = token "=" ( token / quoted-string )
+ * token = 1*tchar
+ * tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
+ * / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
+ * / DIGIT / ALPHA
+ * ; any VCHAR, except delimiters
+ * quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
+ * qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
+ * obs-text = %x80-FF
+ * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
+ */
+var PARAM_REGEXP = /; *([!#$%&'*+.^_`|~0-9A-Za-z-]+) *= *("(?:[\u000b\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u000b\u0020-\u00ff])*"|[!#$%&'*+.^_`|~0-9A-Za-z-]+) */g; // eslint-disable-line no-control-regex
+
+/**
+ * RegExp to match quoted-pair in RFC 7230 sec 3.2.6
+ *
+ * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
+ * obs-text = %x80-FF
+ */
+var QESC_REGEXP = /\\([\u000b\u0020-\u00ff])/g; // eslint-disable-line no-control-regex
+
+/**
+ * RegExp to match type in RFC 7231 sec 3.1.1.1
+ *
+ * media-type = type "/" subtype
+ * type = token
+ * subtype = token
+ */
+var TYPE_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
+var parse_1 = parse;
+
+/**
+ * Parse media type to object.
+ *
+ * @param {string|object} string
+ * @return {Object}
+ * @public
+ */
+
+function parse (string) {
+ if (!string) {
+ throw new TypeError('argument string is required')
+ }
+
+ // support req/res-like objects as argument
+ var header = typeof string === 'object'
+ ? getcontenttype(string)
+ : string;
+
+ if (typeof header !== 'string') {
+ throw new TypeError('argument string is required to be a string')
+ }
+
+ var index = header.indexOf(';');
+ var type = index !== -1
+ ? header.slice(0, index).trim()
+ : header.trim();
+
+ if (!TYPE_REGEXP.test(type)) {
+ throw new TypeError('invalid media type')
+ }
+
+ var obj = new ContentType(type.toLowerCase());
+
+ // parse parameters
+ if (index !== -1) {
+ var key;
+ var match;
+ var value;
+
+ PARAM_REGEXP.lastIndex = index;
+
+ while ((match = PARAM_REGEXP.exec(header))) {
+ if (match.index !== index) {
+ throw new TypeError('invalid parameter format')
+ }
+
+ index += match[0].length;
+ key = match[1].toLowerCase();
+ value = match[2];
+
+ if (value.charCodeAt(0) === 0x22 /* " */) {
+ // remove quotes
+ value = value.slice(1, -1);
+
+ // remove escapes
+ if (value.indexOf('\\') !== -1) {
+ value = value.replace(QESC_REGEXP, '$1');
+ }
+ }
+
+ obj.parameters[key] = value;
+ }
+
+ if (index !== header.length) {
+ throw new TypeError('invalid parameter format')
+ }
+ }
+
+ return obj
+}
+
+/**
+ * Get content-type from req/res objects.
+ *
+ * @param {object}
+ * @return {Object}
+ * @private
+ */
+
+function getcontenttype (obj) {
+ var header;
+
+ if (typeof obj.getHeader === 'function') {
+ // res-like
+ header = obj.getHeader('content-type');
+ } else if (typeof obj.headers === 'object') {
+ // req-like
+ header = obj.headers && obj.headers['content-type'];
+ }
+
+ if (typeof header !== 'string') {
+ throw new TypeError('content-type header is missing from object')
+ }
+
+ return header
+}
+
+/**
+ * Class to represent a content type.
+ * @private
+ */
+function ContentType (type) {
+ this.parameters = Object.create(null);
+ this.type = type;
+}
+
+var cache = new Map();
+
+var cloneSvg = function cloneSvg(sourceSvg) {
+ return sourceSvg.cloneNode(true);
+};
+
+var isLocal = function isLocal() {
+ return window.location.protocol === 'file:';
+};
+
+var makeAjaxRequest = function makeAjaxRequest(url, httpRequestWithCredentials, callback) {
+ var httpRequest = new XMLHttpRequest();
+ httpRequest.onreadystatechange = function () {
+ try {
+ if (!/\.svg/i.test(url) && httpRequest.readyState === 2) {
+ var contentType = httpRequest.getResponseHeader('Content-Type');
+ if (!contentType) {
+ throw new Error('Content type not found');
+ }
+ var type = parse_1(contentType).type;
+ if (!(type === 'image/svg+xml' || type === 'text/plain')) {
+ throw new Error("Invalid content type: ".concat(type));
+ }
+ }
+ if (httpRequest.readyState === 4) {
+ if (httpRequest.status === 404 || httpRequest.responseXML === null) {
+ throw new Error(isLocal() ? 'Note: SVG injection ajax calls do not work locally without ' + 'adjusting security settings in your browser. Or consider ' + 'using a local webserver.' : 'Unable to load SVG file: ' + url);
+ }
+ if (httpRequest.status === 200 || isLocal() && httpRequest.status === 0) {
+ callback(null, httpRequest);
+ } else {
+ throw new Error('There was a problem injecting the SVG: ' + httpRequest.status + ' ' + httpRequest.statusText);
+ }
+ }
+ } catch (error) {
+ httpRequest.abort();
+ if (error instanceof Error) {
+ callback(error, httpRequest);
+ } else {
+ throw error;
+ }
+ }
+ };
+ httpRequest.open('GET', url);
+ httpRequest.withCredentials = httpRequestWithCredentials;
+ if (httpRequest.overrideMimeType) {
+ httpRequest.overrideMimeType('text/xml');
+ }
+ httpRequest.send();
+};
+
+var requestQueue = {};
+var queueRequest = function queueRequest(url, callback) {
+ requestQueue[url] = requestQueue[url] || [];
+ requestQueue[url].push(callback);
+};
+var processRequestQueue = function processRequestQueue(url) {
+ var _loop_1 = function _loop_1(i, len) {
+ setTimeout(function () {
+ if (Array.isArray(requestQueue[url])) {
+ var cacheValue = cache.get(url);
+ var callback = requestQueue[url][i];
+ if (cacheValue instanceof SVGSVGElement) {
+ callback(null, cloneSvg(cacheValue));
+ }
+ if (cacheValue instanceof Error) {
+ callback(cacheValue);
+ }
+ if (i === requestQueue[url].length - 1) {
+ delete requestQueue[url];
+ }
+ }
+ }, 0);
+ };
+ for (var i = 0, len = requestQueue[url].length; i < len; i++) {
+ _loop_1(i);
+ }
+};
+
+var loadSvgCached = function loadSvgCached(url, httpRequestWithCredentials, callback) {
+ if (cache.has(url)) {
+ var cacheValue = cache.get(url);
+ if (cacheValue === undefined) {
+ queueRequest(url, callback);
+ return;
+ }
+ if (cacheValue instanceof SVGSVGElement) {
+ callback(null, cloneSvg(cacheValue));
+ return;
+ }
+ }
+ cache.set(url, undefined);
+ queueRequest(url, callback);
+ makeAjaxRequest(url, httpRequestWithCredentials, function (error, httpRequest) {
+ var _a;
+ if (error) {
+ cache.set(url, error);
+ } else if (((_a = httpRequest.responseXML) === null || _a === void 0 ? void 0 : _a.documentElement) instanceof SVGSVGElement) {
+ cache.set(url, httpRequest.responseXML.documentElement);
+ }
+ processRequestQueue(url);
+ });
+};
+
+var loadSvgUncached = function loadSvgUncached(url, httpRequestWithCredentials, callback) {
+ makeAjaxRequest(url, httpRequestWithCredentials, function (error, httpRequest) {
+ var _a;
+ if (error) {
+ callback(error);
+ } else if (((_a = httpRequest.responseXML) === null || _a === void 0 ? void 0 : _a.documentElement) instanceof SVGSVGElement) {
+ callback(null, httpRequest.responseXML.documentElement);
+ }
+ });
+};
+
+var idCounter = 0;
+var uniqueId = function uniqueId() {
+ return ++idCounter;
+};
+
+var injectedElements = [];
+var ranScripts = {};
+var svgNamespace = 'http://www.w3.org/2000/svg';
+var xlinkNamespace = 'http://www.w3.org/1999/xlink';
+var injectElement = function injectElement(el, evalScripts, renumerateIRIElements, cacheRequests, httpRequestWithCredentials, beforeEach, callback) {
+ var elUrl = el.getAttribute('data-src') || el.getAttribute('src');
+ if (!elUrl) {
+ callback(new Error('Invalid data-src or src attribute'));
+ return;
+ }
+ if (injectedElements.indexOf(el) !== -1) {
+ injectedElements.splice(injectedElements.indexOf(el), 1);
+ el = null;
+ return;
+ }
+ injectedElements.push(el);
+ el.setAttribute('src', '');
+ var loadSvg = cacheRequests ? loadSvgCached : loadSvgUncached;
+ loadSvg(elUrl, httpRequestWithCredentials, function (error, svg) {
+ if (!svg) {
+ injectedElements.splice(injectedElements.indexOf(el), 1);
+ el = null;
+ callback(error);
+ return;
+ }
+ var elId = el.getAttribute('id');
+ if (elId) {
+ svg.setAttribute('id', elId);
+ }
+ var elTitle = el.getAttribute('title');
+ if (elTitle) {
+ svg.setAttribute('title', elTitle);
+ }
+ var elWidth = el.getAttribute('width');
+ if (elWidth) {
+ svg.setAttribute('width', elWidth);
+ }
+ var elHeight = el.getAttribute('height');
+ if (elHeight) {
+ svg.setAttribute('height', elHeight);
+ }
+ var mergedClasses = Array.from(new Set(__spreadArray(__spreadArray(__spreadArray([], (svg.getAttribute('class') || '').split(' '), true), ['injected-svg'], false), (el.getAttribute('class') || '').split(' '), true))).join(' ').trim();
+ svg.setAttribute('class', mergedClasses);
+ var elStyle = el.getAttribute('style');
+ if (elStyle) {
+ svg.setAttribute('style', elStyle);
+ }
+ svg.setAttribute('data-src', elUrl);
+ var elData = [].filter.call(el.attributes, function (at) {
+ return /^data-\w[\w-]*$/.test(at.name);
+ });
+ Array.prototype.forEach.call(elData, function (dataAttr) {
+ if (dataAttr.name && dataAttr.value) {
+ svg.setAttribute(dataAttr.name, dataAttr.value);
+ }
+ });
+ if (renumerateIRIElements) {
+ var iriElementsAndProperties_1 = {
+ clipPath: ['clip-path'],
+ 'color-profile': ['color-profile'],
+ cursor: ['cursor'],
+ filter: ['filter'],
+ linearGradient: ['fill', 'stroke'],
+ marker: ['marker', 'marker-start', 'marker-mid', 'marker-end'],
+ mask: ['mask'],
+ path: [],
+ pattern: ['fill', 'stroke'],
+ radialGradient: ['fill', 'stroke']
+ };
+ var element_1;
+ var elements_1;
+ var properties_1;
+ var currentId_1;
+ var newId_1;
+ Object.keys(iriElementsAndProperties_1).forEach(function (key) {
+ element_1 = key;
+ properties_1 = iriElementsAndProperties_1[key];
+ elements_1 = svg.querySelectorAll(element_1 + '[id]');
+ var _loop_1 = function _loop_1(a, elementsLen) {
+ currentId_1 = elements_1[a].id;
+ newId_1 = currentId_1 + '-' + uniqueId();
+ var referencingElements;
+ Array.prototype.forEach.call(properties_1, function (property) {
+ referencingElements = svg.querySelectorAll('[' + property + '*="' + currentId_1 + '"]');
+ for (var b = 0, referencingElementLen = referencingElements.length; b < referencingElementLen; b++) {
+ var attrValue = referencingElements[b].getAttribute(property);
+ if (attrValue && !attrValue.match(new RegExp('url\\("?#' + currentId_1 + '"?\\)'))) {
+ continue;
+ }
+ referencingElements[b].setAttribute(property, 'url(#' + newId_1 + ')');
+ }
+ });
+ var allLinks = svg.querySelectorAll('[*|href]');
+ var links = [];
+ for (var c = 0, allLinksLen = allLinks.length; c < allLinksLen; c++) {
+ var href = allLinks[c].getAttributeNS(xlinkNamespace, 'href');
+ if (href && href.toString() === '#' + elements_1[a].id) {
+ links.push(allLinks[c]);
+ }
+ }
+ for (var d = 0, linksLen = links.length; d < linksLen; d++) {
+ links[d].setAttributeNS(xlinkNamespace, 'href', '#' + newId_1);
+ }
+ elements_1[a].id = newId_1;
+ };
+ for (var a = 0, elementsLen = elements_1.length; a < elementsLen; a++) {
+ _loop_1(a);
+ }
+ });
+ }
+ svg.removeAttribute('xmlns:a');
+ var scripts = svg.querySelectorAll('script');
+ var scriptsToEval = [];
+ var script;
+ var scriptType;
+ for (var i = 0, scriptsLen = scripts.length; i < scriptsLen; i++) {
+ scriptType = scripts[i].getAttribute('type');
+ if (!scriptType || scriptType === 'application/ecmascript' || scriptType === 'application/javascript' || scriptType === 'text/javascript') {
+ script = scripts[i].innerText || scripts[i].textContent;
+ if (script) {
+ scriptsToEval.push(script);
+ }
+ svg.removeChild(scripts[i]);
+ }
+ }
+ if (scriptsToEval.length > 0 && (evalScripts === 'always' || evalScripts === 'once' && !ranScripts[elUrl])) {
+ for (var l = 0, scriptsToEvalLen = scriptsToEval.length; l < scriptsToEvalLen; l++) {
+ new Function(scriptsToEval[l])(window);
+ }
+ ranScripts[elUrl] = true;
+ }
+ var styleTags = svg.querySelectorAll('style');
+ Array.prototype.forEach.call(styleTags, function (styleTag) {
+ styleTag.textContent += '';
+ });
+ svg.setAttribute('xmlns', svgNamespace);
+ svg.setAttribute('xmlns:xlink', xlinkNamespace);
+ beforeEach(svg);
+ if (!el.parentNode) {
+ injectedElements.splice(injectedElements.indexOf(el), 1);
+ el = null;
+ callback(new Error('Parent node is null'));
+ return;
+ }
+ el.parentNode.replaceChild(svg, el);
+ injectedElements.splice(injectedElements.indexOf(el), 1);
+ el = null;
+ callback(null, svg);
+ });
+};
+
+var SVGInjector = function SVGInjector(elements, _a) {
+ var _b = _a === void 0 ? {} : _a,
+ _c = _b.afterAll,
+ afterAll = _c === void 0 ? function () {
+ return undefined;
+ } : _c,
+ _d = _b.afterEach,
+ afterEach = _d === void 0 ? function () {
+ return undefined;
+ } : _d,
+ _e = _b.beforeEach,
+ beforeEach = _e === void 0 ? function () {
+ return undefined;
+ } : _e,
+ _f = _b.cacheRequests,
+ cacheRequests = _f === void 0 ? true : _f,
+ _g = _b.evalScripts,
+ evalScripts = _g === void 0 ? 'never' : _g,
+ _h = _b.httpRequestWithCredentials,
+ httpRequestWithCredentials = _h === void 0 ? false : _h,
+ _j = _b.renumerateIRIElements,
+ renumerateIRIElements = _j === void 0 ? true : _j;
+ if (elements && 'length' in elements) {
+ var elementsLoaded_1 = 0;
+ for (var i = 0, j = elements.length; i < j; i++) {
+ injectElement(elements[i], evalScripts, renumerateIRIElements, cacheRequests, httpRequestWithCredentials, beforeEach, function (error, svg) {
+ afterEach(error, svg);
+ if (elements && 'length' in elements && elements.length === ++elementsLoaded_1) {
+ afterAll(elementsLoaded_1);
+ }
+ });
+ }
+ } else if (elements) {
+ injectElement(elements, evalScripts, renumerateIRIElements, cacheRequests, httpRequestWithCredentials, beforeEach, function (error, svg) {
+ afterEach(error, svg);
+ afterAll(1);
+ elements = null;
+ });
+ } else {
+ afterAll(0);
+ }
+};
+
+/** ****************************************************************************
+ * UserSvgTool class, UserSvgTool::constructor
+ *
+ * Summary.
+ *
+ */
+
+class UserSvgTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_USERSVG_CONFIG = {
+ position: {
+ cx: 50,
+ cy: 50,
+ height: 50,
+ width: 50,
+ },
+ options: {
+ svginject: true,
+ },
+ styles: {
+ usersvg: {
+ },
+ mask: {
+ fill: 'white',
+ },
+ },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_USERSVG_CONFIG, argConfig), argPos);
+
+ this.images = {};
+ this.images = Object.assign({}, ...this.config.images);
+
+ this.item = {};
+ this.item.image = 'default';
+ // Remember the SVG image to load, as we cache those SVG files
+ this.imageCur = 'none';
+ this.imagePrev = 'none';
+
+ this.injector = {};
+ this.injector.svg = null;
+ this.injector.cache = [];
+
+ this.clipPath = {};
+
+ if (this.config.clip_path) {
+ this.svg.cp_cx = Utils.calculateSvgCoordinate(this.config.clip_path.position.cx || this.config.position.cx, 0);
+ this.svg.cp_cy = Utils.calculateSvgCoordinate(this.config.clip_path.position.cy || this.config.position.cy, 0);
+ this.svg.cp_height = Utils.calculateSvgDimension(this.config.clip_path.position.height || this.config.position.height);
+ this.svg.cp_width = Utils.calculateSvgDimension(this.config.clip_path.position.width || this.config.position.width);
+
+ const maxRadius = Math.min(this.svg.cp_height, this.svg.cp_width) / 2;
+
+ this.svg.radiusTopLeft = +Math.min(maxRadius, Math.max(0, Utils.calculateSvgDimension(
+ this.config.clip_path.position.radius.top_left || this.config.clip_path.position.radius.left
+ || this.config.clip_path.position.radius.top || this.config.clip_path.position.radius.all,
+ ))) || 0;
+
+ this.svg.radiusTopRight = +Math.min(maxRadius, Math.max(0, Utils.calculateSvgDimension(
+ this.config.clip_path.position.radius.top_right || this.config.clip_path.position.radius.right
+ || this.config.clip_path.position.radius.top || this.config.clip_path.position.radius.all,
+ ))) || 0;
+
+ this.svg.radiusBottomLeft = +Math.min(maxRadius, Math.max(0, Utils.calculateSvgDimension(
+ this.config.clip_path.position.radius.bottom_left || this.config.clip_path.position.radius.left
+ || this.config.clip_path.position.radius.bottom || this.config.clip_path.position.radius.all,
+ ))) || 0;
+
+ this.svg.radiusBottomRight = +Math.min(maxRadius, Math.max(0, Utils.calculateSvgDimension(
+ this.config.clip_path.position.radius.bottom_right || this.config.clip_path.position.radius.right
+ || this.config.clip_path.position.radius.bottom || this.config.clip_path.position.radius.all,
+ ))) || 0;
+ }
+
+ if (this.dev.debug) console.log('UserSvgTool constructor config, svg', this.toolId, this.config, this.svg);
+ }
+
+ /** *****************************************************************************
+ * UserSvgTool::value()
+ *
+ * Summary.
+ * Receive new state data for the entity this usersvg is linked to. Called from set hass;
+ *
+ */
+ set value(state) {
+ super.value = state;
+ }
+
+ /**
+ * Summary.
+ * Use firstUpdated(). updated() gives a loop of updates of the SVG if more than one SVG
+ * is defined in the card: things start to blink, as each SVG is removed/rendered in a loop
+ * so it seems. Either a bug in the Injector, or the UserSvg tool...
+ *
+ * @param {()} changedProperties
+ * @returns
+ */
+ // eslint-disable-next-line no-unused-vars
+ updated(changedProperties) {
+ var myThis = this;
+
+ // No need to check SVG injection, if same image, and in cache
+ if ((!this.config.options.svginject) || this.injector.cache[this.imageCur]) {
+ return;
+ }
+
+ this.injector.elementsToInject = this._card.shadowRoot.getElementById(
+ 'usersvg-'.concat(this.toolId)).querySelectorAll('svg[data-src]:not(.injected-svg)');
+ if (this.injector.elementsToInject.length !== 0) {
+ SVGInjector(this.injector.elementsToInject, {
+ afterAll(elementsLoaded) {
+ // Request async update of card if all SVG files are loaded using async http request
+ setTimeout(() => { myThis._card.requestUpdate(); }, 0);
+ },
+ afterEach(err, svg) {
+ if (err) {
+ throw err;
+ }
+ myThis.injector.cache[myThis.imageCur] = svg;
+ },
+ beforeEach(svg) {
+ // Remove height and width attributes before injecting
+ svg.removeAttribute('height');
+ svg.removeAttribute('width');
+ },
+ cacheRequests: false,
+ evalScripts: 'once',
+ httpRequestWithCredentials: false,
+ renumerateIRIElements: false,
+ });
+ }
+ }
+
+ /** *****************************************************************************
+ * UserSvgTool::_renderUserSvg()
+ *
+ * Summary.
+ * Renders the usersvg using precalculated coordinates and dimensions.
+ * Only the runtime style is calculated before rendering the usersvg
+ *
+ */
+
+ _renderUserSvg() {
+ this.MergeAnimationStyleIfChanged();
+
+ const images = Templates.getJsTemplateOrValue(this, this._stateValue, Merge.mergeDeep(this.images));
+ this.imagePrev = this.imageCur;
+ this.imageCur = images[this.item.image];
+
+ // Render nothing if no image found
+ if (images[this.item.image] === 'none')
+ return svg``;
+
+ let cachedSvg = this.injector.cache[this.imageCur];
+
+ // construct clip path if specified
+ let clipPath = '';
+ if (this.config.clip_path) {
+ clipPath = svg`
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ // If jpg or png, use default image renderer...
+ if (['png', 'jpg'].includes((images[this.item.image].substring(images[this.item.image].lastIndexOf('.') + 1)))) {
+ // Render jpg or png
+ return svg`
+
+ "${clipPath}"
+
+
+ `;
+ // Must be svg. Render for the first time, if not in cache...
+ } else if ((!cachedSvg) || (!this.config.options.svginject)) {
+ return svg`
+
+ "${clipPath}"
+
+
+ `;
+ // Render from cache and pass clip path and mask as reference...
+ } else {
+ return svg`
+
+ "${clipPath}"
+ ${cachedSvg};
+
+ `;
+ }
+ }
+
+ /** *****************************************************************************
+ * UserSvgTool::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ */
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderUserSvg()}
+
+ `;
+ }
+} // END of class
+
+/** ***************************************************************************
+ * Toolset class
+ *
+ * Summary.
+ *
+ */
+
+class Toolset {
+ constructor(argCard, argConfig) {
+ this.toolsetId = Math.random().toString(36).substr(2, 9);
+ this._card = argCard;
+ this.dev = { ...this._card.dev };
+ if (this.dev.performance) console.time(`--> ${this.toolsetId} PERFORMANCE Toolset::constructor`);
+
+ this.config = argConfig;
+ this.tools = [];
+
+ // Get SVG coordinates.
+ this.svg = {};
+ this.svg.cx = Utils.calculateSvgCoordinate(argConfig.position.cx, SVG_DEFAULT_DIMENSIONS_HALF);
+ this.svg.cy = Utils.calculateSvgCoordinate(argConfig.position.cy, SVG_DEFAULT_DIMENSIONS_HALF);
+
+ this.svg.x = (this.svg.cx) - (SVG_DEFAULT_DIMENSIONS_HALF);
+ this.svg.y = (this.svg.cy) - (SVG_DEFAULT_DIMENSIONS_HALF);
+
+ // Group scaling experiment. Calc translate values for SVG using the toolset scale value
+ this.transform = {};
+ this.transform.scale = {};
+ // eslint-disable-next-line no-multi-assign
+ this.transform.scale.x = this.transform.scale.y = 1;
+ this.transform.rotate = {};
+ // eslint-disable-next-line no-multi-assign
+ this.transform.rotate.x = this.transform.rotate.y = 0;
+ this.transform.skew = {};
+ // eslint-disable-next-line no-multi-assign
+ this.transform.skew.x = this.transform.skew.y = 0;
+
+ if (this.config.position.scale) {
+ // eslint-disable-next-line no-multi-assign
+ this.transform.scale.x = this.transform.scale.y = this.config.position.scale;
+ }
+ if (this.config.position.rotate) {
+ // eslint-disable-next-line no-multi-assign
+ this.transform.rotate.x = this.transform.rotate.y = this.config.position.rotate;
+ }
+
+ this.transform.scale.x = this.config.position.scale_x || this.config.position.scale || 1;
+ this.transform.scale.y = this.config.position.scale_y || this.config.position.scale || 1;
+
+ this.transform.rotate.x = this.config.position.rotate_x || this.config.position.rotate || 0;
+ this.transform.rotate.y = this.config.position.rotate_y || this.config.position.rotate || 0;
+
+ if (this.dev.debug) console.log('Toolset::constructor config/svg', this.toolsetId, this.config, this.svg);
+
+ // Create the tools configured in the toolset list.
+ const toolsNew = {
+ area: EntityAreaTool,
+ circslider: CircularSliderTool,
+ badge: BadgeTool,
+ bar: SparklineBarChartTool,
+ circle: CircleTool,
+ ellipse: EllipseTool,
+ horseshoe: HorseshoeTool,
+ icon: EntityIconTool,
+ line: LineTool,
+ name: EntityNameTool,
+ rectangle: RectangleTool,
+ rectex: RectangleToolEx,
+ regpoly: RegPolyTool,
+ segarc: SegmentedArcTool,
+ state: EntityStateTool,
+ slider: RangeSliderTool,
+ switch: SwitchTool,
+ text: TextTool,
+ usersvg: UserSvgTool,
+ };
+
+ this.config.tools.map((toolConfig) => {
+ const newConfig = { ...toolConfig };
+
+ const newPos = {
+ cx: 0 / 100 * SVG_DEFAULT_DIMENSIONS,
+ cy: 0 / 100 * SVG_DEFAULT_DIMENSIONS,
+ scale: this.config.position.scale ? this.config.position.scale : 1,
+ };
+
+ if (this.dev.debug) console.log('Toolset::constructor toolConfig', this.toolsetId, newConfig, newPos);
+
+ if (!toolConfig.disabled) {
+ const newTool = new toolsNew[toolConfig.type](this, newConfig, newPos);
+ // eslint-disable-next-line no-bitwise
+ this._card.entityHistory.needed |= (toolConfig.type === 'bar');
+ this.tools.push({ type: toolConfig.type, index: toolConfig.id, tool: newTool });
+ }
+ return true;
+ });
+
+ if (this.dev.performance) console.timeEnd(`--> ${this.toolsetId} PERFORMANCE Toolset::constructor`);
+ }
+
+ /** *****************************************************************************
+ * Toolset::updateValues()
+ *
+ * Summary.
+ * Called from set hass to update values for tools
+ *
+ */
+
+ // #TODO:
+ // Update only the changed entity_index, not all indexes. Now ALL tools are updated...
+ updateValues() {
+ if (this.dev.performance) console.time(`--> ${this.toolsetId} PERFORMANCE Toolset::updateValues`);
+ if (this.tools) {
+ this.tools.map((item, index) => {
+ // eslint-disable-next-line no-constant-condition
+ {
+ if ((item.tool.config.hasOwnProperty('entity_index'))) {
+ if (this.dev.debug) console.log('Toolset::updateValues', item, index);
+ // if (this.dev.debug) console.log('Toolset::updateValues', typeof item.tool._stateValue);
+
+ // #IDEA @2021.11.20
+ // What if for attribute and secondaryinfo the entity state itself is also passsed automatically
+ // In that case that state is always present and can be used in animations by default.
+ // No need to pass an extra entity_index.
+ // A tool using the light brightness can also use the state (on/off) in that case for styling.
+ //
+ // Test can be done on 'state', 'attr', or 'secinfo' for default entity_index.
+ //
+ // Should pass a record in here orso as value { state : xx, attr: yy }
+
+ item.tool.value = this._card.attributesStr[item.tool.config.entity_index]
+ ? this._card.attributesStr[item.tool.config.entity_index]
+ : this._card.secondaryInfoStr[item.tool.config.entity_index]
+ ? this._card.secondaryInfoStr[item.tool.config.entity_index]
+ : this._card.entitiesStr[item.tool.config.entity_index];
+ }
+
+ // Check for multiple entities specified, and pass them to the tool
+ if ((item.tool.config.hasOwnProperty('entity_indexes'))) {
+ // Update list of entities in single record and pass that to the tool
+ // The first entity is used as the state, additional entities can help with animations,
+ // (used for formatting classes/styles) or can be used in a derived entity
+
+ const valueList = [];
+ for (let i = 0; i < item.tool.config.entity_indexes.length; ++i) {
+ valueList[i] = this._card.attributesStr[item.tool.config.entity_indexes[i].entity_index]
+ ? this._card.attributesStr[item.tool.config.entity_indexes[i].entity_index]
+ : this._card.secondaryInfoStr[item.tool.config.entity_indexes[i].entity_index]
+ ? this._card.secondaryInfoStr[item.tool.config.entity_indexes[i].entity_index]
+ : this._card.entitiesStr[item.tool.config.entity_indexes[i].entity_index];
+ }
+
+ item.tool.values = valueList;
+ }
+ }
+ return true;
+ });
+ }
+ if (this.dev.performance) console.timeEnd(`--> ${this.toolsetId} PERFORMANCE Toolset::updateValues`);
+ }
+
+ /** *****************************************************************************
+ * Toolset::connectedCallback()
+ *
+ * Summary.
+ *
+ */
+ connectedCallback() {
+ if (this.dev.performance) console.time(`--> ${this.toolsetId} PERFORMANCE Toolset::connectedCallback`);
+
+ if (this.dev.debug) console.log('*****Event - connectedCallback', this.toolsetId, new Date().getTime());
+ if (this.dev.performance) console.timeEnd(`--> ${this.toolsetId} PERFORMANCE Toolset::connectedCallback`);
+ }
+
+ /** *****************************************************************************
+ * Toolset::disconnectedCallback()
+ *
+ * Summary.
+ *
+ */
+ disconnectedCallback() {
+ if (this.dev.performance) console.time(`--> ${this.cardId} PERFORMANCE Toolset::disconnectedCallback`);
+
+ if (this.dev.debug) console.log('*****Event - disconnectedCallback', this.toolsetId, new Date().getTime());
+ if (this.dev.performance) console.timeEnd(`--> ${this.cardId} PERFORMANCE Toolset::disconnectedCallback`);
+ }
+
+ /** *****************************************************************************
+ * Toolset::firstUpdated()
+ *
+ * Summary.
+ *
+ */
+ firstUpdated(changedProperties) {
+ if (this.dev.debug) console.log('*****Event - Toolset::firstUpdated', this.toolsetId, new Date().getTime());
+
+ if (this.tools) {
+ this.tools.map((item) => {
+ if (typeof item.tool.firstUpdated === 'function') {
+ item.tool.firstUpdated(changedProperties);
+ return true;
+ }
+ return false;
+ });
+ }
+ }
+
+ /** *****************************************************************************
+ * Toolset::updated()
+ *
+ * Summary.
+ *
+ */
+ updated(changedProperties) {
+ if (this.dev.debug) console.log('*****Event - Updated', this.toolsetId, new Date().getTime());
+
+ if (this.tools) {
+ this.tools.map((item) => {
+ if (typeof item.tool.updated === 'function') {
+ item.tool.updated(changedProperties);
+ return true;
+ }
+ return false;
+ });
+ }
+ }
+
+ /** *****************************************************************************
+ * Toolset::renderToolset()
+ *
+ * Summary.
+ *
+ */
+ renderToolset() {
+ if (this.dev.debug) console.log('*****Event - renderToolset', this.toolsetId, new Date().getTime());
+
+ const svgItems = this.tools.map((item) => svg`
+ ${item.tool.render()}
+ `);
+ return svg`${svgItems}`;
+ }
+
+ /** *****************************************************************************
+ * Toolset::render()
+ *
+ * Summary.
+ * The render() function for this toolset renders all the tools within this set.
+ *
+ * Important notes:
+ * - the toolset position is set on the svg. That one accepts x,y
+ * - scaling, rotating and skewing (and translating) is done on the parent group.
+ *
+ * The order of transformations are done from the child's perspective!!
+ * So, the child (tools) gets positioned FIRST, and then scaled/rotated.
+ *
+ * See comments for different render paths for Apple/Safari and any other browser...
+ *
+ */
+
+ render() {
+ // Note:
+ // Rotating a card can produce different results on several browsers.
+ // A 1:1 card / toolset gives the same results, but other aspect ratio's may give different results.
+
+ if (((this._card.isSafari) || (this._card.iOS)) && (!this._card.isSafari16)) {
+ //
+ // Render path for Safari if not Safari 16:
+ //
+ // Safari seems to ignore - although not always - the transform-box:fill-box setting.
+ // - It needs the explicit center point when rotating. So this is added to the rotate() command.
+ // - scale around center uses the "move object to 0,0 -> scale -> move object back to position" trick,
+ // where the second move takes scaling into account!
+ // - Does not apply transforms from the child's point of view.
+ // Transform of toolset_position MUST take scaling of one level higher into account!
+ //
+ // Note: rotate is done around the defined center (cx,cy) of the toolsets position!
+ //
+ // More:
+ // - Safari NEEDS the overflow:visible on the element, as it defaults to "svg:{overflow: hidden;}".
+ // Other browsers don't need that, they default to: "svg:not(:root) {overflow: hidden;}"
+ //
+ // Without this setting, objects are cut-off or become invisible while scaled!
+
+ return svg`
+
+
+
+ ${this.renderToolset()}
+
+
+
+ `;
+ } else {
+ //
+ // Render path for ANY other browser that usually follows the standards:
+ //
+ // - use transform-box:fill-box to make sure every transform is about the object itself!
+ // - applying the rules seen from the child's point of view.
+ // So the transform on the toolset_position is NOT scaled, as the scaling is done one level higher.
+ //
+ // Note: rotate is done around the center of the bounding box. This might NOT be the toolsets center (cx,cy) position!
+ //
+ return svg`
+
+
+
+ ${this.renderToolset()}
+
+
+
+ `;
+ }
+ }
+} // END of class
+
+/*
+*
+* Card : swiss-army-knife-card.js
+* Project : Home Assistant
+* Repository: https://github.com/AmoebeLabs/swiss-army-knife-card
+*
+* Author : Mars @ AmoebeLabs.com
+*
+* License : MIT
+*
+* -----
+* Description:
+* The swiss army knife card, a versatile multi-tool custom card for
+# the one and only Home Assistant.
+*
+* Documentation Refs:
+* - https://swiss-army-knife-card-manual.amoebelabs.com/
+* - https://material3-themes-manual.amoebelabs.com/
+*
+*******************************************************************************
+*/
+
+// Original injector is buggy. Use a patched version, and store this local...
+// import * as SvgInjector from '../dist/SVGInjector.min.js'; // lgtm[js/unused-local-variable]
+
+console.info(
+ `%c SWISS-ARMY-KNIFE-CARD \n%c Version ${version} `,
+ 'color: yellow; font-weight: bold; background: black',
+ 'color: white; font-weight: bold; background: dimgray',
+);
+
+// https://github.com/d3/d3-selection/blob/master/src/selection/data.js
+//
+
+/**
+ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ */
+
+class SwissArmyKnifeCard extends LitElement {
+ // card::constructor
+ constructor() {
+ super();
+
+ this.connected = false;
+
+ Colors.setElement(this);
+
+ // Get cardId for unique SVG gradient Id
+ this.cardId = Math.random().toString(36).substr(2, 9);
+ this.entities = [];
+ this.entitiesStr = [];
+ this.attributesStr = [];
+ this.secondaryInfoStr = [];
+ this.viewBoxSize = SVG_VIEW_BOX;
+ this.viewBox = { width: SVG_VIEW_BOX, height: SVG_VIEW_BOX };
+
+ // Create the lists for the toolsets and the tools
+ // - toolsets contain a list of toolsets with tools
+ // - tools contain the full list of tools!
+ this.toolsets = [];
+ this.tools = [];
+
+ // 2022.01.24
+ // Add card styles functionality
+ this.styles = {};
+ this.styles.card = {};
+
+ // For history query interval updates.
+ this.entityHistory = {};
+ this.entityHistory.needed = false;
+ this.stateChanged = true;
+ this.entityHistory.updating = false;
+ this.entityHistory.update_interval = 300;
+ // console.log("SAK Constructor,", this.entityHistory);
+
+ // Development settings
+ this.dev = {};
+ this.dev.debug = false;
+ this.dev.performance = false;
+ this.dev.m3 = false;
+
+ this.configIsSet = false;
+
+ // Theme mode support
+ this.theme = {};
+ this.theme.modeChanged = false;
+ this.theme.darkMode = false;
+
+ // Safari is the new IE.
+ // Check for iOS / iPadOS / Safari to be able to work around some 'features'
+ // Some bugs are already 9 years old, and not fixed yet by Apple!
+ //
+ // However: there is a new SVG engine on its way that might be released in 2023.
+ // That should fix a lot of problems, adhere to standards, allow for hardware
+ // acceleration and mixing HTML - through the foreignObject - with SVG!
+ //
+ // The first small fixes are in 16.2-16.4, which is why I have to check for
+ // Safari 16, as that version can use the same renderpath as Chrome and Firefox!! WOW!!
+ //
+ // Detection from: http://jsfiddle.net/jlubean/dL5cLjxt/
+ //
+ // this.isSafari = !!navigator.userAgent.match(/Version\/[\d\.]+.*Safari/);
+ // this.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
+
+ // See: https://javascriptio.com/view/10924/detect-if-device-is-ios
+ // After iOS 13 you should detect iOS devices like this, since iPad will not be detected as iOS devices
+ // by old ways (due to new "desktop" options, enabled by default)
+
+ // eslint-disable-next-line no-useless-escape
+ this.isSafari = !!window.navigator.userAgent.match(/Version\/[\d\.]+.*Safari/);
+ this.iOS = (/iPad|iPhone|iPod/.test(window.navigator.userAgent)
+ || (window.navigator.platform === 'MacIntel' && window.navigator.maxTouchPoints > 1))
+ && !window.MSStream;
+ this.isSafari14 = this.isSafari && /Version\/14\.[0-9]/.test(window.navigator.userAgent);
+ this.isSafari15 = this.isSafari && /Version\/15\.[0-9]/.test(window.navigator.userAgent);
+ this.isSafari16 = this.isSafari && /Version\/16\.[0-9]/.test(window.navigator.userAgent);
+ this.isSafari16 = this.isSafari && /Version\/16\.[0-9]/.test(window.navigator.userAgent);
+
+ // The iOS app does not use a standard agent string...
+ // See: https://github.com/home-assistant/iOS/blob/master/Sources/Shared/API/HAAPI.swift
+ // It contains strings like "like Safari" and "OS 14_2", and "iOS 14.2.0"
+
+ this.isSafari14 = this.isSafari14 || /os 15.*like safari/.test(window.navigator.userAgent.toLowerCase());
+ this.isSafari15 = this.isSafari15 || /os 14.*like safari/.test(window.navigator.userAgent.toLowerCase());
+ this.isSafari16 = this.isSafari16 || /os 16.*like safari/.test(window.navigator.userAgent.toLowerCase());
+
+ this.lovelace = SwissArmyKnifeCard.lovelace;
+
+ if (!this.lovelace) {
+ console.error("card::constructor - Can't get Lovelace panel");
+ throw Error("card::constructor - Can't get Lovelace panel");
+ }
+
+ if (!SwissArmyKnifeCard.colorCache) {
+ SwissArmyKnifeCard.colorCache = [];
+ }
+
+ if (this.dev.debug) console.log('*****Event - card - constructor', this.cardId, new Date().getTime());
+ }
+
+ static getSystemStyles() {
+ return css`
+ :host {
+ cursor: default;
+ font-size: ${FONT_SIZE}px;
+ }
+
+ /* Default settings for the card */
+ /* - default cursor */
+ /* - SVG overflow is not displayed, ie cutoff by the card edges */
+ ha-card {
+ cursor: default;
+ overflow: hidden;
+
+ -webkit-touch-callout: none;
+ }
+
+ /* For disabled parts of tools/toolsets */
+ /* - No input */
+ ha-card.disabled {
+ pointer-events: none;
+ cursor: default;
+ }
+
+ .disabled {
+ pointer-events: none !important;
+ cursor: default !important;
+ }
+
+ /* For 'active' tools/toolsets */
+ /* - Show cursor as pointer */
+ .hover {
+ cursor: pointer;
+ }
+
+ /* For hidden tools/toolsets where state for instance is undefined */
+ .hidden {
+ opacity: 0;
+ visibility: hidden;
+ transition: visibility 0s 1s, opacity 0.5s linear;
+ }
+
+ focus {
+ outline: none;
+ }
+ focus-visible {
+ outline: 3px solid blanchedalmond; /* That'll show 'em */
+ }
+
+
+ @media (print), (prefers-reduced-motion: reduce) {
+ .animated {
+ animation-duration: 1ms !important;
+ transition-duration: 1ms !important;
+ animation-iteration-count: 1 !important;
+ }
+ }
+
+
+ /* Set default host font-size to 10 pixels.
+ * In that case 1em = 10 pixels = 1% of 100x100 matrix used
+ */
+ @media screen and (min-width: 467px) {
+ :host {
+ font-size: ${FONT_SIZE}px;
+ }
+ }
+ @media screen and (max-width: 466px) {
+ :host {
+ font-size: ${FONT_SIZE}px;
+ }
+ }
+
+ :host ha-card {
+ padding: 0px 0px 0px 0px;
+ }
+
+ .container {
+ position: relative;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .labelContainer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 65%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-end;
+ }
+
+ .ellipsis {
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ }
+
+ .state {
+ position: relative;
+ display: flex;
+ flex-wrap: wrap;
+ max-width: 100%;
+ min-width: 0px;
+ }
+
+ #label {
+ display: flex;
+ line-height: 1;
+ }
+
+ #label.bold {
+ font-weight: bold;
+ }
+
+ #label, #name {
+ margin: 3% 0;
+ }
+
+ .shadow {
+ font-size: 30px;
+ font-weight: 700;
+ text-anchor: middle;
+ }
+
+ .card--dropshadow-5 {
+ filter: drop-shadow(0 1px 0 #ccc)
+ drop-shadow(0 2px 0 #c9c9c9)
+ drop-shadow(0 3px 0 #bbb)
+ drop-shadow(0 4px 0 #b9b9b9)
+ drop-shadow(0 5px 0 #aaa)
+ drop-shadow(0 6px 1px rgba(0,0,0,.1))
+ drop-shadow(0 0 5px rgba(0,0,0,.1))
+ drop-shadow(0 1px 3px rgba(0,0,0,.3))
+ drop-shadow(0 3px 5px rgba(0,0,0,.2))
+ drop-shadow(0 5px 10px rgba(0,0,0,.25))
+ drop-shadow(0 10px 10px rgba(0,0,0,.2))
+ drop-shadow(0 20px 20px rgba(0,0,0,.15));
+ }
+ .card--dropshadow-medium--opaque--sepia90 {
+ filter: drop-shadow(0.0em 0.05em 0px #b2a98f22)
+ drop-shadow(0.0em 0.07em 0px #b2a98f55)
+ drop-shadow(0.0em 0.10em 0px #b2a98f88)
+ drop-shadow(0px 0.6em 0.9em rgba(0,0,0,0.15))
+ drop-shadow(0px 1.2em 0.15em rgba(0,0,0,0.1))
+ drop-shadow(0px 2.4em 2.5em rgba(0,0,0,0.1))
+ sepia(90%);
+ }
+
+ .card--dropshadow-heavy--sepia90 {
+ filter: drop-shadow(0.0em 0.05em 0px #b2a98f22)
+ drop-shadow(0.0em 0.07em 0px #b2a98f55)
+ drop-shadow(0.0em 0.10em 0px #b2a98f88)
+ drop-shadow(0px 0.3em 0.45em rgba(0,0,0,0.5))
+ drop-shadow(0px 0.6em 0.07em rgba(0,0,0,0.3))
+ drop-shadow(0px 1.2em 1.25em rgba(0,0,0,1))
+ drop-shadow(0px 1.8em 1.6em rgba(0,0,0,0.1))
+ drop-shadow(0px 2.4em 2.0em rgba(0,0,0,0.1))
+ drop-shadow(0px 3.0em 2.5em rgba(0,0,0,0.1))
+ sepia(90%);
+ }
+
+ .card--dropshadow-heavy {
+ filter: drop-shadow(0.0em 0.05em 0px #b2a98f22)
+ drop-shadow(0.0em 0.07em 0px #b2a98f55)
+ drop-shadow(0.0em 0.10em 0px #b2a98f88)
+ drop-shadow(0px 0.3em 0.45em rgba(0,0,0,0.5))
+ drop-shadow(0px 0.6em 0.07em rgba(0,0,0,0.3))
+ drop-shadow(0px 1.2em 1.25em rgba(0,0,0,1))
+ drop-shadow(0px 1.8em 1.6em rgba(0,0,0,0.1))
+ drop-shadow(0px 2.4em 2.0em rgba(0,0,0,0.1))
+ drop-shadow(0px 3.0em 2.5em rgba(0,0,0,0.1));
+ }
+
+ .card--dropshadow-medium--sepia90 {
+ filter: drop-shadow(0.0em 0.05em 0px #b2a98f)
+ drop-shadow(0.0em 0.15em 0px #b2a98f)
+ drop-shadow(0.0em 0.15em 0px #b2a98f)
+ drop-shadow(0px 0.6em 0.9em rgba(0,0,0,0.15))
+ drop-shadow(0px 1.2em 0.15em rgba(0,0,0,0.1))
+ drop-shadow(0px 2.4em 2.5em rgba(0,0,0,0.1))
+ sepia(90%);
+ }
+
+ .card--dropshadow-medium {
+ filter: drop-shadow(0.0em 0.05em 0px #b2a98f)
+ drop-shadow(0.0em 0.15em 0px #b2a98f)
+ drop-shadow(0.0em 0.15em 0px #b2a98f)
+ drop-shadow(0px 0.6em 0.9em rgba(0,0,0,0.15))
+ drop-shadow(0px 1.2em 0.15em rgba(0,0,0,0.1))
+ drop-shadow(0px 2.4em 2.5em rgba(0,0,0,0.1));
+ }
+
+ .card--dropshadow-light--sepia90 {
+ filter: drop-shadow(0px 0.10em 0px #b2a98f)
+ drop-shadow(0.1em 0.5em 0.2em rgba(0, 0, 0, .5))
+ sepia(90%);
+ }
+
+ .card--dropshadow-light {
+ filter: drop-shadow(0px 0.10em 0px #b2a98f)
+ drop-shadow(0.1em 0.5em 0.2em rgba(0, 0, 0, .5));
+ }
+
+ .card--dropshadow-down-and-distant {
+ filter: drop-shadow(0px 0.05em 0px #b2a98f)
+ drop-shadow(0px 14px 10px rgba(0,0,0,0.15))
+ drop-shadow(0px 24px 2px rgba(0,0,0,0.1))
+ drop-shadow(0px 34px 30px rgba(0,0,0,0.1));
+ }
+
+ .card--filter-none {
+ }
+
+ .horseshoe__svg__group {
+ transform: translateY(15%);
+ }
+
+ `;
+ }
+
+ /** *****************************************************************************
+ * card::getUserStyles()
+ *
+ * Summary.
+ * Returns the user defined CSS styles for the card in sak_user_templates config
+ * section in lovelace configuration.
+ *
+ */
+
+ static getUserStyles() {
+ this.userContent = '';
+
+ if ((SwissArmyKnifeCard.lovelace.config.sak_user_templates)
+ && (SwissArmyKnifeCard.lovelace.config.sak_user_templates.definitions.user_css_definitions)) {
+ this.userContent = SwissArmyKnifeCard.lovelace.config.sak_user_templates.definitions.user_css_definitions.reduce((accumulator, currentValue) => accumulator + currentValue.content, '');
+ }
+
+ return css`${unsafeCSS(this.userContent)}`;
+ }
+
+ static getSakStyles() {
+ this.sakContent = '';
+
+ if ((SwissArmyKnifeCard.lovelace.config.sak_sys_templates)
+ && (SwissArmyKnifeCard.lovelace.config.sak_sys_templates.definitions.sak_css_definitions)) {
+ this.sakContent = SwissArmyKnifeCard.lovelace.config.sak_sys_templates.definitions.sak_css_definitions.reduce((accumulator, currentValue) => accumulator + currentValue.content, '');
+ }
+
+ return css`${unsafeCSS(this.sakContent)}`;
+ }
+
+ static getSakSvgDefinitions() {
+ SwissArmyKnifeCard.lovelace.sakSvgContent = null;
+ let sakSvgContent = '';
+
+ if ((SwissArmyKnifeCard.lovelace.config.sak_sys_templates)
+ && (SwissArmyKnifeCard.lovelace.config.sak_sys_templates.definitions.sak_svg_definitions)) {
+ sakSvgContent = SwissArmyKnifeCard.lovelace.config.sak_sys_templates.definitions.sak_svg_definitions.reduce((accumulator, currentValue) => accumulator + currentValue.content, '');
+ }
+ // Cache result for later use in other cards
+ SwissArmyKnifeCard.sakSvgContent = unsafeSVG(sakSvgContent);
+ }
+
+ static getUserSvgDefinitions() {
+ SwissArmyKnifeCard.lovelace.userSvgContent = null;
+ let userSvgContent = '';
+
+ if ((SwissArmyKnifeCard.lovelace.config.sak_user_templates)
+ && (SwissArmyKnifeCard.lovelace.config.sak_user_templates.definitions.user_svg_definitions)) {
+ userSvgContent = SwissArmyKnifeCard.lovelace.config.sak_user_templates.definitions.user_svg_definitions.reduce((accumulator, currentValue) => accumulator + currentValue.content, '');
+ }
+ // Cache result for later use other cards
+ SwissArmyKnifeCard.userSvgContent = unsafeSVG(userSvgContent);
+ }
+
+ /** *****************************************************************************
+ * card::get styles()
+ *
+ * Summary.
+ * Returns the static CSS styles for the lit-element
+ *
+ * Note:
+ * - The BEM (http://getbem.com/naming/) naming style for CSS is used
+ * Of course, if no mistakes are made ;-)
+ *
+ * Note2:
+ * - get styles is a static function and is called ONCE at initialization.
+ * So, we need to get lovelace here...
+ */
+ static get styles() {
+ // console.log('SAK - get styles');
+ if (!SwissArmyKnifeCard.lovelace) SwissArmyKnifeCard.lovelace = Utils.getLovelace();
+
+ if (!SwissArmyKnifeCard.lovelace) {
+ console.error("SAK - Can't get reference to Lovelace");
+ throw Error("card::get styles - Can't get Lovelace panel");
+ }
+ if (!SwissArmyKnifeCard.lovelace.config.sak_sys_templates) {
+ console.error('SAK - System Templates reference NOT defined.');
+ throw Error('card::get styles - System Templates reference NOT defined!');
+ }
+ if (!SwissArmyKnifeCard.lovelace.config.sak_user_templates) {
+ console.warning('SAK - User Templates reference NOT defined. Did you NOT include them?');
+ }
+
+ // #TESTING
+ // Testing dark/light mode support for future functionality
+ // const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+ // console.log('get styles', darkModeMediaQuery);
+ // darkModeMediaQuery.addListener((e) => {
+ // const darkModeOn = e.matches;
+ // console.log(`Dark mode is ${darkModeOn ? '🌒 on' : '☀️ off'}.`);
+ // });
+ // console.log('get styles 2', darkModeMediaQuery);
+
+ // Get - only ONCE - the external SVG definitions for both SAK and UserSvgTool
+ // These definitions are cached into the static class of the card
+ //
+ // Note: If you change a view, and do a refresh (F5) everything is loaded.
+ // But after that: HA asks you to refresh the page --> BAM, all Lovelace
+ // cached data is gone. So we need a check/reload in a card...
+
+ SwissArmyKnifeCard.getSakSvgDefinitions();
+ SwissArmyKnifeCard.getUserSvgDefinitions();
+
+ return css`
+ ${SwissArmyKnifeCard.getSystemStyles()}
+ ${SwissArmyKnifeCard.getSakStyles()}
+ ${SwissArmyKnifeCard.getUserStyles()}
+ `;
+ }
+
+ /** *****************************************************************************
+ * card::set hass()
+ *
+ * Summary.
+ * Updates hass data for the card
+ *
+ */
+
+ set hass(hass) {
+ if (!this.counter) this.counter = 0;
+ this.counter += 1;
+
+ // Check for theme mode and theme mode change...
+ if (hass.themes.darkMode !== this.theme.darkMode) {
+ this.theme.darkMode = hass.themes.darkMode;
+ this.theme.modeChanged = true;
+ }
+
+ // Set ref to hass, use "_"for the name ;-)
+ if (this.dev.debug) console.log('*****Event - card::set hass', this.cardId, new Date().getTime());
+ this._hass = hass;
+
+ if (!this.connected) {
+ if (this.dev.debug) console.log('set hass but NOT connected', this.cardId);
+
+ // 2020.02.10 Troubles with connectcallback late, so windows are not yet calculated. ie
+ // things around icons go wrong...
+ // what if return is here..
+ // return;
+ }
+
+ if (!this.config.entities) {
+ return;
+ }
+
+ let entityHasChanged = false;
+
+ // Update state strings and check for changes.
+ // Only if changed, continue and force render
+ let value;
+ let index = 0;
+
+ let secInfoSet = false;
+ let newSecInfoState;
+ let newSecInfoStateStr;
+
+ let attrSet = false;
+ let newStateStr;
+ // eslint-disable-next-line no-restricted-syntax, no-unused-vars
+ for (value of this.config.entities) {
+ this.entities[index] = hass.states[this.config.entities[index].entity];
+
+ if (this.entities[index] === undefined) {
+ console.error('SAK - set hass, entity undefined: ', this.config.entities[index].entity);
+ // Temp disable throw Error(`Set hass, entity undefined: ${this.config.entities[index].entity}`);
+ }
+
+ // Get secondary info state if specified and available
+ if (this.config.entities[index].secondary_info) {
+ secInfoSet = true;
+ newSecInfoState = this.entities[index][this.config.entities[index].secondary_info];
+ newSecInfoStateStr = this._buildSecondaryInfo(newSecInfoState, this.config.entities[index]);
+
+ if (newSecInfoStateStr !== this.secondaryInfoStr[index]) {
+ this.secondaryInfoStr[index] = newSecInfoStateStr;
+ entityHasChanged = true;
+ }
+ }
+
+ // Get attribute state if specified and available
+ if (this.config.entities[index].attribute) {
+ // #WIP:
+ // Check for indexed or mapped attributes, like weather forecast (array of 5 days with a map containing attributes)....
+ //
+ // states['weather.home'].attributes['forecast'][0].detailed_description
+ // attribute: forecast[0].condition
+ //
+
+ let { attribute } = this.config.entities[index];
+ let attrMore = '';
+ let attributeState = '';
+
+ const arrayPos = this.config.entities[index].attribute.indexOf('[');
+ const dotPos = this.config.entities[index].attribute.indexOf('.');
+ let arrayIdx = 0;
+ let arrayMap = '';
+
+ if (arrayPos !== -1) {
+ // We have an array. Split...
+ attribute = this.config.entities[index].attribute.substr(0, arrayPos);
+ attrMore = this.config.entities[index].attribute.substr(arrayPos, this.config.entities[index].attribute.length - arrayPos);
+
+ // Just hack, assume single digit index...
+ arrayIdx = attrMore[1];
+ arrayMap = attrMore.substr(4, attrMore.length - 4);
+
+ // Fetch state
+ attributeState = this.entities[index].attributes[attribute][arrayIdx][arrayMap];
+ // console.log('set hass, attributes with array/map', this.config.entities[index].attribute, attribute, attrMore, arrayIdx, arrayMap, attributeState);
+ } else if (dotPos !== -1) {
+ // We have a map. Split...
+ attribute = this.config.entities[index].attribute.substr(0, dotPos);
+ attrMore = this.config.entities[index].attribute.substr(arrayPos, this.config.entities[index].attribute.length - arrayPos);
+ arrayMap = attrMore.substr(1, attrMore.length - 1);
+
+ // Fetch state
+ attributeState = this.entities[index].attributes[attribute][arrayMap];
+
+ console.log('set hass, attributes with map', this.config.entities[index].attribute, attribute, attrMore);
+ } else {
+ // default attribute handling...
+ attributeState = this.entities[index].attributes[attribute];
+ }
+
+ // eslint-disable-next-line no-constant-condition
+ { // (typeof attributeState != 'undefined') {
+ newStateStr = this._buildState(attributeState, this.config.entities[index]);
+ if (newStateStr !== this.attributesStr[index]) {
+ this.attributesStr[index] = newStateStr;
+ entityHasChanged = true;
+ }
+ attrSet = true;
+ }
+ // 2021.10.30
+ // Due to change in light percentage, check for undefined.
+ // If bulb is off, NO percentage is given anymore, so is probably 'undefined'.
+ // Any tool should still react to a percentage going from a valid value to undefined!
+ }
+ if ((!attrSet) && (!secInfoSet)) {
+ newStateStr = this._buildState(this.entities[index].state, this.config.entities[index]);
+ if (newStateStr !== this.entitiesStr[index]) {
+ this.entitiesStr[index] = newStateStr;
+ entityHasChanged = true;
+ }
+ if (this.dev.debug) console.log('set hass - attrSet=false', this.cardId, `${new Date().getSeconds().toString()}.${new Date().getMilliseconds().toString()}`, newStateStr);
+ }
+
+ index += 1;
+ attrSet = false;
+ secInfoSet = false;
+ }
+
+ if ((!entityHasChanged) && (!this.theme.modeChanged)) {
+ // console.timeEnd("--> " + this.cardId + " PERFORMANCE card::hass");
+
+ return;
+ }
+
+ // Either one of the entities has changed, or the theme mode. So update all toolsets with new data.
+ if (this.toolsets) {
+ this.toolsets.map((item) => {
+ item.updateValues();
+ return true;
+ });
+ }
+
+ // Always request update to render the card if any of the states, attributes or theme mode have changed...
+
+ this.requestUpdate();
+
+ // An update has been requested to recalculate / redraw the tools, so reset theme mode changed
+ this.theme.modeChanged = false;
+
+ this.counter -= 1;
+
+ // console.timeEnd("--> " + this.cardId + " PERFORMANCE card::hass");
+ }
+
+ /** *****************************************************************************
+ * card::setConfig()
+ *
+ * Summary.
+ * Sets/Updates the card configuration. Rarely called if the doc is right
+ *
+ */
+
+ setConfig(config) {
+ if (this.dev.performance) console.time(`--> ${this.cardId} PERFORMANCE card::setConfig`);
+
+ if (this.dev.debug) console.log('*****Event - setConfig', this.cardId, new Date().getTime());
+ config = JSON.parse(JSON.stringify(config));
+
+ if (config.dev) this.dev = { ...this.dev, ...config.dev };
+
+ if (this.dev.debug) console.log('setConfig', this.cardId);
+
+ if (!config.layout) {
+ throw Error('card::setConfig - No layout defined');
+ }
+
+ // Temp disable for layout template check...
+ // if (!config.layout.toolsets) {
+ // throw Error('card::setConfig - No toolsets defined');
+ // }
+
+ // testing
+ if (config.entities) {
+ const newdomain = this._computeDomain(config.entities[0].entity);
+ if (newdomain !== 'sensor') {
+ // If not a sensor, check if attribute is a number. If so, continue, otherwise Error...
+ if (config.entities[0].attribute && !isNaN(config.entities[0].attribute)) {
+ throw Error('card::setConfig - First entity or attribute must be a numbered sensorvalue, but is NOT');
+ }
+ }
+ }
+
+ // Copy config, as we must have write access to replace templates!
+ const newConfig = Merge.mergeDeep(config);
+
+ // #TODO must be removed after removal of segmented arcs part below
+ this.config = newConfig;
+
+ // NEW for ts processing
+ this.toolset = [];
+
+ const thisMe = this;
+ function findTemplate(key, value) {
+ // Filtering out properties
+ // console.log("findTemplate, key=", key, "value=", value);
+ if (value?.template) {
+ const template = thisMe.lovelace.config.sak_user_templates.templates[value.template.name];
+ if (!template) {
+ console.error('Template not found...', value.template, template);
+ }
+
+ const replacedValue = Templates.replaceVariables3(value.template.variables, template);
+ // Hmm. cannot add .template var. object is not extensible...
+ // replacedValue.template = 'replaced';
+ const secondValue = Merge.mergeDeep(replacedValue);
+ // secondValue.from_template = 'replaced';
+
+ return secondValue;
+ }
+ if (key === 'template') {
+ // Template is gone via replace!!!! No template anymore, as there is no merge done.
+ console.log('findTemplate return key=template/value', key, undefined);
+
+ return value;
+ }
+ // console.log("findTemplate return key/value", key, value);
+ return value;
+ }
+
+ // Find & Replace template definitions. This also supports layout templates
+ const cfg = JSON.stringify(this.config, findTemplate);
+
+ // To further process toolset templates, get reference to toolsets
+ const cfgobj = JSON.parse(cfg).layout.toolsets;
+
+ // Set layout template if found
+ if (this.config.layout.template) {
+ this.config.layout = JSON.parse(cfg).layout;
+ }
+
+ // Continue to check & replace partial toolset templates and overrides
+ this.config.layout.toolsets.map((toolsetCfg, toolidx) => {
+ let toolList = null;
+
+ if (!this.toolsets) this.toolsets = [];
+
+ // eslint-disable-next-line no-constant-condition
+ {
+ let found = false;
+ let toolAdd = [];
+
+ toolList = cfgobj[toolidx].tools;
+ // Check for empty tool list. This can be if template is used. Tools come from template, not from config...
+ if (toolsetCfg.tools) {
+ toolsetCfg.tools.map((tool, index) => {
+ cfgobj[toolidx].tools.map((toolT, indexT) => {
+ if (tool.id === toolT.id) {
+ if (toolsetCfg.template) {
+ if (this.config.layout.toolsets[toolidx].position)
+ cfgobj[toolidx].position = Merge.mergeDeep(this.config.layout.toolsets[toolidx].position);
+
+ toolList[indexT] = Merge.mergeDeep(toolList[indexT], tool);
+
+ // After merging/replacing. We might get some template definitions back!!!!!!
+ toolList[indexT] = JSON.parse(JSON.stringify(toolList[indexT], findTemplate));
+
+ found = true;
+ }
+ if (this.dev.debug) console.log('card::setConfig - got toolsetCfg toolid', tool, index, toolT, indexT, tool);
+ }
+ cfgobj[toolidx].tools[indexT] = Templates.getJsTemplateOrValueConfig(cfgobj[toolidx].tools[indexT], Merge.mergeDeep(cfgobj[toolidx].tools[indexT]));
+ return found;
+ });
+ if (!found) toolAdd = toolAdd.concat(toolsetCfg.tools[index]);
+ return found;
+ });
+ }
+ toolList = toolList.concat(toolAdd);
+ }
+
+ toolsetCfg = cfgobj[toolidx];
+ const newToolset = new Toolset(this, toolsetCfg);
+ this.toolsets.push(newToolset);
+ return true;
+ });
+
+ // Special case. Abuse card for m3 conversion to output
+ if (this.dev.m3) {
+ console.log('*** M3 - Checking for m3.yaml template to convert...');
+
+ if (this.lovelace.config.sak_user_templates.templates.m3) {
+ const { m3 } = this.lovelace.config.sak_user_templates.templates;
+
+ console.log('*** M3 - Found. Material 3 conversion starting...');
+ // These variables are used of course, but eslint thinks they are NOT.
+ // If I remove them, eslint complains about undefined variables...
+ // eslint-disable-next-line no-unused-vars
+ let palette = '';
+ // eslint-disable-next-line no-unused-vars
+ let colordefault = '';
+ // eslint-disable-next-line no-unused-vars
+ let colorlight = '';
+ // eslint-disable-next-line no-unused-vars
+ let colordark = '';
+
+ let surfacelight = '';
+ let primarylight = '';
+ let neutrallight = '';
+
+ let surfacedark = '';
+ let primarydark = '';
+ let neutraldark = '';
+
+ const colorEntities = {};
+ const cssNames = {};
+ const cssNamesRgb = {};
+
+ m3.entities.map((entity) => {
+ if (['ref.palette', 'sys.color', 'sys.color.light', 'sys.color.dark'].includes(entity.category_id)) {
+ if (!entity.tags.includes('alias')) {
+ colorEntities[entity.id] = { value: entity.value, tags: entity.tags };
+ }
+ }
+
+ if (entity.category_id === 'ref.palette') {
+ palette += `${entity.id}: '${entity.value}'\n`;
+
+ // test for primary light color...
+ if (entity.id === 'md.ref.palette.primary40') {
+ primarylight = entity.value;
+ }
+ // test for primary dark color...
+ if (entity.id === 'md.ref.palette.primary80') {
+ primarydark = entity.value;
+ }
+
+ // test for neutral light color...
+ if (entity.id === 'md.ref.palette.neutral40') {
+ neutrallight = entity.value;
+ }
+ // test for neutral light color...
+ if (entity.id === 'md.ref.palette.neutral80') {
+ neutraldark = entity.value;
+ }
+ }
+
+ if (entity.category_id === 'sys.color') {
+ colordefault += `${entity.id}: '${entity.value}'\n`;
+ }
+
+ if (entity.category_id === 'sys.color.light') {
+ colorlight += `${entity.id}: '${entity.value}'\n`;
+
+ // test for surface light color...
+ if (entity.id === 'md.sys.color.surface.light') {
+ surfacelight = entity.value;
+ }
+ }
+
+ if (entity.category_id === 'sys.color.dark') {
+ colordark += `${entity.id}: '${entity.value}'\n`;
+
+ // test for surface light color...
+ if (entity.id === 'md.sys.color.surface.dark') {
+ surfacedark = entity.value;
+ }
+ }
+ return true;
+ });
+
+ ['primary', 'secondary', 'tertiary', 'error', 'neutral', 'neutral-variant'].forEach((paletteName) => {
+ [5, 15, 25, 35, 45, 65, 75, 85].forEach((step) => {
+ colorEntities[`md.ref.palette.${paletteName}${step.toString()}`] = {
+ value: Colors.getGradientValue(
+ colorEntities[`md.ref.palette.${paletteName}${(step - 5).toString()}`].value,
+ colorEntities[`md.ref.palette.${paletteName}${(step + 5).toString()}`].value,
+ 0.5,
+ ),
+ tags: [...colorEntities[`md.ref.palette.${paletteName}${(step - 5).toString()}`].tags],
+ };
+ colorEntities[`md.ref.palette.${paletteName}${step.toString()}`].tags[3] = paletteName + step.toString();
+ });
+ colorEntities[`md.ref.palette.${paletteName}7`] = {
+ value: Colors.getGradientValue(
+ colorEntities[`md.ref.palette.${paletteName}5`].value,
+ colorEntities[`md.ref.palette.${paletteName}10`].value,
+ 0.5,
+ ),
+ tags: [...colorEntities[`md.ref.palette.${paletteName}10`].tags],
+ };
+ colorEntities[`md.ref.palette.${paletteName}7`].tags[3] = `${paletteName}7`;
+
+ colorEntities[`md.ref.palette.${paletteName}92`] = {
+ value: Colors.getGradientValue(
+ colorEntities[`md.ref.palette.${paletteName}90`].value,
+ colorEntities[`md.ref.palette.${paletteName}95`].value,
+ 0.5,
+ ),
+ tags: [...colorEntities[`md.ref.palette.${paletteName}90`].tags],
+ };
+ colorEntities[`md.ref.palette.${paletteName}92`].tags[3] = `${paletteName}92`;
+
+ colorEntities[`md.ref.palette.${paletteName}97`] = {
+ value: Colors.getGradientValue(
+ colorEntities[`md.ref.palette.${paletteName}95`].value,
+ colorEntities[`md.ref.palette.${paletteName}99`].value,
+ 0.5,
+ ),
+ tags: [...colorEntities[`md.ref.palette.${paletteName}90`].tags],
+ };
+ colorEntities[`md.ref.palette.${paletteName}97`].tags[3] = `${paletteName}97`;
+ });
+
+ // eslint-disable-next-line no-restricted-syntax
+ for (const [index, entity] of Object.entries(colorEntities)) {
+ // eslint-disable-next-line no-use-before-define
+ cssNames[index] = `theme-${entity.tags[1]}-${entity.tags[2]}-${entity.tags[3]}: rgb(${hex2rgb(entity.value)})`;
+ // eslint-disable-next-line no-use-before-define
+ cssNamesRgb[index] = `theme-${entity.tags[1]}-${entity.tags[2]}-${entity.tags[3]}-rgb: ${hex2rgb(entity.value)}`;
+ }
+
+ // https://filosophy.org/code/online-tool-to-lighten-color-without-alpha-channel/
+
+ // eslint-disable-next-line no-inner-declarations
+ function hex2rgb(hexColor) {
+ const rgbCol = {};
+
+ rgbCol.r = Math.round(parseInt(hexColor.substr(1, 2), 16));
+ rgbCol.g = Math.round(parseInt(hexColor.substr(3, 2), 16));
+ rgbCol.b = Math.round(parseInt(hexColor.substr(5, 2), 16));
+
+ // const cssRgbColor = "rgb(" + rgbCol.r + "," + rgbCol.g + "," + rgbCol.b + ")";
+ const cssRgbColor = `${rgbCol.r},${rgbCol.g},${rgbCol.b}`;
+ return cssRgbColor;
+ }
+
+ // eslint-disable-next-line no-inner-declarations
+ function getSurfaces(surfaceColor, paletteColor, opacities, cssName, mode) {
+ const bgCol = {};
+ const fgCol = {};
+
+ bgCol.r = Math.round(parseInt(surfaceColor.substr(1, 2), 16));
+ bgCol.g = Math.round(parseInt(surfaceColor.substr(3, 2), 16));
+ bgCol.b = Math.round(parseInt(surfaceColor.substr(5, 2), 16));
+
+ fgCol.r = Math.round(parseInt(paletteColor.substr(1, 2), 16));
+ fgCol.g = Math.round(parseInt(paletteColor.substr(3, 2), 16));
+ fgCol.b = Math.round(parseInt(paletteColor.substr(5, 2), 16));
+
+ let surfaceColors = '';
+ let r; let g; let b;
+ opacities.forEach((opacity, index) => {
+ r = Math.round(opacity * fgCol.r + (1 - opacity) * bgCol.r);
+ g = Math.round(opacity * fgCol.g + (1 - opacity) * bgCol.g);
+ b = Math.round(opacity * fgCol.b + (1 - opacity) * bgCol.b);
+
+ surfaceColors += `${cssName + (index + 1).toString()}-${mode}: rgb(${r},${g},${b})\n`;
+ surfaceColors += `${cssName + (index + 1).toString()}-${mode}-rgb: ${r},${g},${b}\n`;
+ });
+
+ return surfaceColors;
+ }
+
+ // Generate surfaces for dark and light...
+ const opacitysurfacelight = [0.03, 0.05, 0.08, 0.11, 0.15, 0.19, 0.24, 0.29, 0.35, 0.4];
+ const opacitysurfacedark = [0.05, 0.08, 0.11, 0.15, 0.19, 0.24, 0.29, 0.35, 0.40, 0.45];
+
+ const surfacenL = getSurfaces(surfacelight, neutrallight, opacitysurfacelight, ' theme-ref-elevation-surface-neutral', 'light');
+
+ const neutralvariantlight = colorEntities['md.ref.palette.neutral-variant40'].value;
+ const surfacenvL = getSurfaces(surfacelight, neutralvariantlight, opacitysurfacelight, ' theme-ref-elevation-surface-neutral-variant', 'light');
+
+ const surfacepL = getSurfaces(surfacelight, primarylight, opacitysurfacelight, ' theme-ref-elevation-surface-primary', 'light');
+
+ const secondarylight = colorEntities['md.ref.palette.secondary40'].value;
+ const surfacesL = getSurfaces(surfacelight, secondarylight, opacitysurfacelight, ' theme-ref-elevation-surface-secondary', 'light');
+
+ const tertiarylight = colorEntities['md.ref.palette.tertiary40'].value;
+ const surfacetL = getSurfaces(surfacelight, tertiarylight, opacitysurfacelight, ' theme-ref-elevation-surface-tertiary', 'light');
+
+ const errorlight = colorEntities['md.ref.palette.error40'].value;
+ const surfaceeL = getSurfaces(surfacelight, errorlight, opacitysurfacelight, ' theme-ref-elevation-surface-error', 'light');
+
+ // DARK
+ const surfacenD = getSurfaces(surfacedark, neutraldark, opacitysurfacedark, ' theme-ref-elevation-surface-neutral', 'dark');
+
+ const neutralvariantdark = colorEntities['md.ref.palette.neutral-variant80'].value;
+ const surfacenvD = getSurfaces(surfacedark, neutralvariantdark, opacitysurfacedark, ' theme-ref-elevation-surface-neutral-variant', 'dark');
+
+ const surfacepD = getSurfaces(surfacedark, primarydark, opacitysurfacedark, ' theme-ref-elevation-surface-primary', 'dark');
+
+ const secondarydark = colorEntities['md.ref.palette.secondary80'].value;
+ const surfacesD = getSurfaces(surfacedark, secondarydark, opacitysurfacedark, ' theme-ref-elevation-surface-secondary', 'dark');
+
+ const tertiarydark = colorEntities['md.ref.palette.tertiary80'].value;
+ const surfacetD = getSurfaces(surfacedark, tertiarydark, opacitysurfacedark, ' theme-ref-elevation-surface-tertiary', 'dark');
+
+ const errordark = colorEntities['md.ref.palette.error80'].value;
+ const surfaceeD = getSurfaces(surfacedark, errordark, opacitysurfacedark, ' theme-ref-elevation-surface-error', 'dark');
+
+ let themeDefs = '';
+ // eslint-disable-next-line no-restricted-syntax
+ for (const [index, cssName] of Object.entries(cssNames)) { // lgtm[js/unused-local-variable]
+ if (cssName.substring(0, 9) === 'theme-ref') {
+ themeDefs += ` ${cssName}\n`;
+ themeDefs += ` ${cssNamesRgb[index]}\n`;
+ }
+ }
+ // Dump full theme contents to console.
+ // User should copy this content into the theme definition YAML file.
+ console.log(surfacenL + surfacenvL + surfacepL + surfacesL + surfacetL + surfaceeL
+ + surfacenD + surfacenvD + surfacepD + surfacesD + surfacetD + surfaceeD
+ + themeDefs);
+
+ console.log('*** M3 - Material 3 conversion DONE. You should copy the above output...');
+ }
+ }
+
+ // Get aspectratio. This can be defined at card level or layout level
+ this.aspectratio = (this.config.layout.aspectratio || this.config.aspectratio || '1/1').trim();
+
+ const ar = this.aspectratio.split('/');
+ if (!this.viewBox) this.viewBox = {};
+ this.viewBox.width = ar[0] * SVG_DEFAULT_DIMENSIONS;
+ this.viewBox.height = ar[1] * SVG_DEFAULT_DIMENSIONS;
+
+ if (this.config.layout.styles?.card) {
+ this.styles.card = this.config.layout.styles.card;
+ }
+
+ if (this.dev.debug) console.log('Step 5: toolconfig, list of toolsets', this.toolsets);
+ if (this.dev.debug) console.log('debug - setConfig', this.cardId, this.config);
+ if (this.dev.performance) console.timeEnd(`--> ${this.cardId} PERFORMANCE card::setConfig`);
+
+ this.configIsSet = true;
+ }
+
+ /** *****************************************************************************
+ * card::connectedCallback()
+ *
+ * Summary.
+ *
+ */
+ connectedCallback() {
+ if (this.dev.performance) console.time(`--> ${this.cardId} PERFORMANCE card::connectedCallback`);
+
+ if (this.dev.debug) console.log('*****Event - connectedCallback', this.cardId, new Date().getTime());
+ this.connected = true;
+ super.connectedCallback();
+
+ if (this.entityHistory.update_interval) {
+ // Fix crash while set hass not yet called, and thus no access to entities!
+ this.updateOnInterval();
+ // #TODO, modify to total interval
+ // Use fast interval at start, and normal interval after that, if _hass is defined...
+ clearInterval(this.interval);
+ this.interval = setInterval(
+ () => this.updateOnInterval(),
+ this._hass ? this.entityHistory.update_interval * 1000 : 1000,
+ );
+ }
+ if (this.dev.debug) console.log('ConnectedCallback', this.cardId);
+
+ // MUST request updates again, as no card is displayed otherwise as long as there is no data coming in...
+ this.requestUpdate();
+ if (this.dev.performance) console.timeEnd(`--> ${this.cardId} PERFORMANCE card::connectedCallback`);
+ }
+
+ /** *****************************************************************************
+ * card::disconnectedCallback()
+ *
+ * Summary.
+ *
+ */
+ disconnectedCallback() {
+ if (this.dev.performance) console.time(`--> ${this.cardId} PERFORMANCE card::disconnectedCallback`);
+
+ if (this.dev.debug) console.log('*****Event - disconnectedCallback', this.cardId, new Date().getTime());
+ if (this.interval) {
+ clearInterval(this.interval);
+ this.interval = 0;
+ }
+ super.disconnectedCallback();
+ if (this.dev.debug) console.log('disconnectedCallback', this.cardId);
+ this.connected = false;
+ if (this.dev.performance) console.timeEnd(`--> ${this.cardId} PERFORMANCE card::disconnectedCallback`);
+ }
+
+ /** *****************************************************************************
+ * card::firstUpdated()
+ *
+ * Summary.
+ * firstUpdated fires after the first time the card hs been updated using its render method,
+ * but before the browser has had a chance to paint.
+ *
+ */
+
+ firstUpdated(changedProperties) {
+ if (this.dev.debug) console.log('*****Event - card::firstUpdated', this.cardId, new Date().getTime());
+
+ if (this.toolsets) {
+ this.toolsets.map(async (item) => {
+ item.firstUpdated(changedProperties);
+ return true;
+ });
+ }
+ }
+
+ /** *****************************************************************************
+ * card::updated()
+ *
+ * Summary.
+ *
+ */
+ updated(changedProperties) {
+ if (this.dev.debug) console.log('*****Event - Updated', this.cardId, new Date().getTime());
+
+ if (this.toolsets) {
+ this.toolsets.map(async (item) => {
+ item.updated(changedProperties);
+ return true;
+ });
+ }
+ }
+
+ /** *****************************************************************************
+ * card::render()
+ *
+ * Summary.
+ * Renders the complete SVG based card according to the specified layout.
+ *
+ * render ICON TESTING pathh lzwzmegla undefined undefined
+ * render ICON TESTING pathh lzwzmegla undefined NodeList [ha-svg-icon]
+ * render ICON TESTING pathh lzwzmegla M7,2V13H10V22L17,10H13L17,2H7Z NodeList [ha-svg-icon]
+ */
+
+ render() {
+ if (this.dev.performance) console.time(`--> ${this.cardId} PERFORMANCE card::render`);
+ if (this.dev.debug) console.log('*****Event - render', this.cardId, new Date().getTime());
+
+ if (!this.connected) {
+ if (this.dev.debug) console.log('render but NOT connected', this.cardId, new Date().getTime());
+ return;
+ }
+
+ let myHtml;
+
+ try {
+ if (this.config.disable_card) {
+ myHtml = html`
+
+ ${this._renderSvg()}
+
+ `;
+ } else {
+ myHtml = html`
+
+
+ ${this._renderSvg()}
+
+
+ `;
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ if (this.dev.performance) console.timeEnd(`--> ${this.cardId} PERFORMANCE card::render`);
+
+ return myHtml;
+ }
+
+ _renderSakSvgDefinitions() {
+ return svg`
+ ${SwissArmyKnifeCard.sakSvgContent}
+ `;
+ }
+
+ _renderUserSvgDefinitions() {
+ return svg`
+ ${SwissArmyKnifeCard.userSvgContent}
+ `;
+ }
+
+ themeIsDarkMode() {
+ return (this.theme.darkMode === true);
+ }
+
+ themeIsLightMode() {
+ return (this.theme.darkMode === false);
+ }
+
+ /** *****************************************************************************
+ * card::_RenderToolsets()
+ *
+ * Summary.
+ * Renders the toolsets
+ *
+ */
+
+ _RenderToolsets() {
+ if (this.dev.debug) console.log('all the tools in renderTools', this.tools);
+
+ return svg`
+
+ ${this.toolsets.map((toolset) => toolset.render())}
+
+
+
+ ${this._renderSakSvgDefinitions()}
+ ${this._renderUserSvgDefinitions()}
+
+ `;
+ }
+
+ /** *****************************************************************************
+ * card::_renderSvg()
+ *
+ * Summary.
+ * Renders the SVG
+ *
+ * NTS:
+ * If height and width given for svg it equals the viewbox. The card is not scaled
+ * anymore to the full dimensions of the card given by hass/lovelace.
+ * Card or svg is also placed default at start of viewport (not box), and can be
+ * placed at start, center or end of viewport (Use align-self to center it).
+ *
+ * 1. If height and width are ommitted, the ha-card/viewport is forced to the x/y
+ * aspect ratio of the viewbox, ie 1:1. EXACTLY WHAT WE WANT!
+ * 2. If height and width are set to 100%, the viewport (or ha-card) forces the
+ * aspect-ratio on the svg. Although GetCardSize is set to 4, it seems the
+ * height is forced to 150px, so part of the viewbox/svg is not shown or
+ * out of proportion!
+ *
+ */
+
+ _renderCardAttributes() {
+ let entityValue;
+ const attributes = [];
+
+ this._attributes = '';
+
+ for (let i = 0; i < this.entities.length; i++) {
+ entityValue = this.attributesStr[i]
+ ? this.attributesStr[i]
+ : this.secondaryInfoStr[i]
+ ? this.secondaryInfoStr[i]
+ : this.entitiesStr[i];
+ attributes.push(entityValue);
+ }
+ this._attributes = attributes;
+ return attributes;
+ }
+
+ _renderSvg() {
+ const cardFilter = this.config.card_filter ? this.config.card_filter : 'card--filter-none';
+
+ const svgItems = [];
+
+ // The extra group is required for Safari to have filters work and updates are rendered.
+ // If group omitted, some cards do update, and some not!!!! Don't ask why!
+ // style="${styleMap(this.styles.card)}"
+
+ this._renderCardAttributes();
+
+ // @2022.01.26 Timing / Ordering problem:
+ // - the _RenderToolsets() function renders tools, which build the this.styles/this.classes maps.
+ // - However: this means that higher styles won't render until the next render, ie this.styles.card
+ // won't render, as this variable is already cached as it seems by Polymer.
+ // - This is also the case for this.styles.tools/toolsets: they also don't work!
+ //
+ // Fix for card styles: render toolsets first, and then push the svg data!!
+
+ const toolsetsSvg = this._RenderToolsets();
+
+ svgItems.push(svg`
+
+
+ ${toolsetsSvg}
+
+ `);
+
+ return svg`${svgItems}`;
+ }
+
+ /** *****************************************************************************
+ * card::_buildUom()
+ *
+ * Summary.
+ * Builds the Unit of Measurement string.
+ *
+ */
+
+ _buildUom(derivedEntity, entityState, entityConfig) {
+ return (
+ derivedEntity?.unit
+ || entityConfig?.unit
+ || entityState?.attributes.unit_of_measurement
+ || ''
+ );
+ }
+
+ toLocale(string, fallback = 'unknown') {
+ const lang = this._hass.selectedLanguage || this._hass.language;
+ const resources = this._hass.resources[lang];
+ return (resources && resources[string] ? resources[string] : fallback);
+ }
+
+ /** *****************************************************************************
+ * card::_buildState()
+ *
+ * Summary.
+ * Builds the State string.
+ * If state is not a number, the state is returned AS IS, otherwise the state
+ * is build according to the specified number of decimals.
+ *
+ * NOTE:
+ * - a number value of "-0" is translated to "0". The sign is gone...
+ *
+ * IMPORTANT NOTE:
+ * - do NOT replace isNaN() by Number.isNaN(). They are INCOMPATIBLE !!!!!!!!!
+ */
+
+ _buildState(inState, entityConfig) {
+ // if (typeof inState !== 'number') {
+ if (isNaN(inState)) {
+ if (inState === 'unavailable') return '-ua-';
+ return inState;
+ }
+
+ if (entityConfig.format === 'brightness') {
+ return `${Math.round((inState / 255) * 100)}`;
+ }
+
+ const state = Math.abs(Number(inState));
+ const sign = Math.sign(inState);
+
+ if (['0', '-0'].includes(sign)) return sign;
+
+ if (entityConfig.decimals === undefined || Number.isNaN(entityConfig.decimals) || Number.isNaN(state))
+ return (sign === '-1' ? `-${(Math.round(state * 100) / 100).toString()}` : (Math.round(state * 100) / 100).toString());
+
+ const x = 10 ** entityConfig.decimals;
+ return (sign === '-1' ? `-${(Math.round(state * x) / x).toFixed(entityConfig.decimals).toString()}`
+ : (Math.round(state * x) / x).toFixed(entityConfig.decimals).toString());
+ }
+
+ /** *****************************************************************************
+ * card::_buildSecondaryInfo()
+ *
+ * Summary.
+ * Builds the SecondaryInfo string.
+ *
+ */
+
+ _buildSecondaryInfo(inSecInfoState, entityConfig) {
+ const leftPad = (num) => (num < 10 ? `0${num}` : num);
+
+ function secondsToDuration(d) {
+ const h = Math.floor(d / 3600);
+ const m = Math.floor((d % 3600) / 60);
+ const s = Math.floor((d % 3600) % 60);
+
+ if (h > 0) {
+ return `${h}:${leftPad(m)}:${leftPad(s)}`;
+ }
+ if (m > 0) {
+ return `${m}:${leftPad(s)}`;
+ }
+ if (s > 0) {
+ return `${s}`;
+ }
+ return null;
+ }
+
+ const lang = this._hass.selectedLanguage || this._hass.language;
+
+ // this.polyfill(lang);
+
+ if (['relative', 'total', 'date', 'time', 'datetime'].includes(entityConfig.format)) {
+ const timestamp = new Date(inSecInfoState);
+ if (!(timestamp instanceof Date) || isNaN(timestamp.getTime())) {
+ return inSecInfoState;
+ }
+
+ let retValue;
+ // return date/time according to formatting...
+ switch (entityConfig.format) {
+ case 'relative':
+ // eslint-disable-next-line no-case-declarations
+ const diff = selectUnit(timestamp, new Date());
+ retValue = new Intl.RelativeTimeFormat(lang, { numeric: 'auto' }).format(diff.value, diff.unit);
+ break;
+ case 'total':
+ case 'precision':
+ retValue = 'Not Yet Supported';
+ break;
+ case 'date':
+ retValue = new Intl.DateTimeFormat(lang, { year: 'numeric', month: 'numeric', day: 'numeric' }).format(timestamp);
+ break;
+ case 'time':
+ retValue = new Intl.DateTimeFormat(lang, { hour: 'numeric', minute: 'numeric', second: 'numeric' }).format(timestamp);
+ break;
+ case 'datetime':
+ retValue = new Intl.DateTimeFormat(lang, {
+ year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric',
+ }).format(timestamp);
+ break;
+ }
+ return retValue;
+ }
+
+ if (isNaN(parseFloat(inSecInfoState)) || !isFinite(inSecInfoState)) {
+ return inSecInfoState;
+ }
+ if (entityConfig.format === 'brightness') {
+ return `${Math.round((inSecInfoState / 255) * 100)} %`;
+ }
+ if (entityConfig.format === 'duration') {
+ return secondsToDuration(inSecInfoState);
+ }
+ }
+
+ /** *****************************************************************************
+ * card::_computeState()
+ *
+ * Summary.
+ *
+ */
+
+ _computeState(inState, dec) {
+ if (isNaN(inState)) {
+ console.log('computestate - NAN', inState, dec);
+ return inState;
+ }
+
+ const state = Number(inState);
+
+ if (dec === undefined || isNaN(dec) || isNaN(state)) {
+ return Math.round(state * 100) / 100;
+ }
+
+ const x = 10 ** dec;
+ return (Math.round(state * x) / x).toFixed(dec);
+ }
+
+ _computeDomain(entityId) {
+ return entityId.substr(0, entityId.indexOf('.'));
+ }
+
+ _computeEntity(entityId) {
+ return entityId.substr(entityId.indexOf('.') + 1);
+ }
+
+ // 2022.01.25 #TODO
+ // Reset interval to 5 minutes: is now short I think after connectedCallback().
+ // Only if _hass exists / is set --> set to 5 minutes!
+ //
+ // BUG: If no history entity, the interval check keeps running. Initially set to 2000ms, and
+ // keeps running with that interval. If history present, interval is larger ????????
+ //
+ // There is no check yet, if history is requested. That is the only reason to have this
+ // interval active!
+ updateOnInterval() {
+ // Only update if hass is already set, this might be not the case the first few calls...
+ // console.log("updateOnInterval -> check...");
+ if (!this._hass) {
+ if (this.dev.debug) console.log('UpdateOnInterval - NO hass, returning');
+ return;
+ }
+ if (this.stateChanged && !this.entityHistory.updating) {
+ // 2020.10.24
+ // Leave true, as multiple entities can be fetched. fetch every 5 minutes...
+ // this.stateChanged = false;
+ this.updateData();
+ // console.log("*RC* updateOnInterval -> updateData", this.entityHistory);
+ }
+
+ if (!this.entityHistory.needed) {
+ // console.log("*RC* updateOnInterval -> stop timer", this.entityHistory, this.interval);
+ if (this.interval) {
+ window.clearInterval(this.interval);
+ this.interval = 0;
+ }
+ } else {
+ window.clearInterval(this.interval);
+ this.interval = setInterval(
+ () => this.updateOnInterval(),
+ // 5 * 1000);
+ this.entityHistory.update_interval * 1000,
+ );
+ // console.log("*RC* updateOnInterval -> start timer", this.entityHistory, this.interval);
+ }
+ }
+
+ async fetchRecent(entityId, start, end, skipInitialState) {
+ let url = 'history/period';
+ if (start) url += `/${start.toISOString()}`;
+ url += `?filter_entity_id=${entityId}`;
+ if (end) url += `&end_time=${end.toISOString()}`;
+ if (skipInitialState) url += '&skip_initial_state';
+ url += '&minimal_response';
+
+ // console.log('fetchRecent - call is', entityId, start, end, skipInitialState, url);
+ return this._hass.callApi('GET', url);
+ }
+
+ // async updateData({ config } = this) {
+ async updateData() {
+ this.entityHistory.updating = true;
+
+ if (this.dev.debug) console.log('card::updateData - ENTRY', this.cardId);
+
+ // We have a list of objects that might need some history update
+ // Create list to fetch.
+ const entityList = [];
+ let j = 0;
+
+ // #TODO
+ // Lookup in this.tools for bars, or better tools that need history...
+ // get that entity_index for that object
+ // add to list...
+ this.toolsets.map((toolset, k) => {
+ toolset.tools.map((item, i) => {
+ if (item.type === 'bar') {
+ const end = new Date();
+ const start = new Date();
+ start.setHours(end.getHours() - item.tool.config.hours);
+ const attr = this.config.entities[item.tool.config.entity_index].attribute ? this.config.entities[item.tool.config.entity_index].attribute : null;
+
+ entityList[j] = ({
+ tsidx: k, entityIndex: item.tool.config.entity_index, entityId: this.entities[item.tool.config.entity_index].entity_id, attrId: attr, start, end, type: 'bar', idx: i,
+ });
+ j += 1;
+ }
+ return true;
+ });
+ return true;
+ });
+
+ if (this.dev.debug) console.log('card::updateData - LENGTH', this.cardId, entityList.length, entityList);
+
+ // #TODO
+ // Quick hack to block updates if entrylist is empty
+ this.stateChanged = false;
+
+ if (this.dev.debug) console.log('card::updateData, entityList from tools', entityList);
+
+ try {
+ // const promise = this.config.layout.vbars.map((item, i) => this.updateEntity(item, entity, i, start, end));
+ const promise = entityList.map((item, i) => this.updateEntity(item, i, item.start, item.end));
+ await Promise.all(promise);
+ } finally {
+ this.entityHistory.updating = false;
+ }
+ }
+
+ async updateEntity(entity, index, initStart, end) {
+ let stateHistory = [];
+ const start = initStart;
+ const skipInitialState = false;
+
+ // Get history for this entity and/or attribute.
+ let newStateHistory = await this.fetchRecent(entity.entityId, start, end, skipInitialState);
+
+ // Now we have some history, check if it has valid data and filter out either the entity state or
+ // the entity attribute. Ain't that nice!
+
+ let theState;
+
+ if (newStateHistory[0] && newStateHistory[0].length > 0) {
+ if (entity.attrId) {
+ theState = this.entities[entity.entityIndex].attributes[this.config.entities[entity.entityIndex].attribute];
+ entity.state = theState;
+ }
+ newStateHistory = newStateHistory[0].filter((item) => (entity.attrId ? !isNaN(parseFloat(item.attributes[entity.attrId])) : !isNaN(parseFloat(item.state))));
+
+ newStateHistory = newStateHistory.map((item) => ({
+ last_changed: item.last_changed,
+ state: entity.attrId ? Number(item.attributes[entity.attrId]) : Number(item.state),
+ }));
+ }
+
+ stateHistory = [...stateHistory, ...newStateHistory];
+
+ this.uppdate(entity, stateHistory);
+ }
+
+ uppdate(entity, hist) {
+ if (!hist) return;
+
+ // #LGTM: Unused variable getMin.
+ // Keep this one for later use!!!!!!!!!!!!!!!!!
+ // const getMin = (arr, val) => arr.reduce((min, p) => (
+ // Number(p[val]) < Number(min[val]) ? p : min
+ // ), arr[0]);
+
+ const getAvg = (arr, val) => arr.reduce((sum, p) => (
+ sum + Number(p[val])
+ ), 0) / arr.length;
+
+ const now = new Date().getTime();
+
+ let hours = 24;
+ let barhours = 2;
+
+ if (entity.type === 'bar') {
+ if (this.dev.debug) console.log('entity.type == bar', entity);
+
+ hours = this.toolsets[entity.tsidx].tools[entity.idx].tool.config.hours;
+ barhours = this.toolsets[entity.tsidx].tools[entity.idx].tool.config.barhours;
+ }
+
+ const reduce = (res, item) => {
+ const age = now - new Date(item.last_changed).getTime();
+ const interval = (age / (1000 * 3600) / barhours) - (hours / barhours);
+ const key = Math.floor(Math.abs(interval));
+ if (!res[key]) res[key] = [];
+ res[key].push(item);
+ return res;
+ };
+ const coords = hist.reduce((res, item) => reduce(res, item), []);
+ coords.length = Math.ceil(hours / barhours);
+
+ // If no intervals found, return...
+ if (Object.keys(coords).length === 0) {
+ return;
+ }
+
+ // That STUPID STUPID Math.min/max can't handle empty arrays which are put into it below
+ // so add some data to the array, and everything works!!!!!!
+
+ // check if first interval contains data, if not find first in interval and use first entry as value...
+
+ const firstInterval = Object.keys(coords)[0];
+ if (firstInterval !== '0') {
+ // first index doesn't contain data.
+ coords[0] = [];
+
+ coords[0].push(coords[firstInterval][0]);
+ }
+
+ for (let i = 0; i < (hours / barhours); i++) {
+ if (!coords[i]) {
+ coords[i] = [];
+ coords[i].push(coords[i - 1][coords[i - 1].length - 1]);
+ }
+ }
+ this.coords = coords;
+ let theData = [];
+ theData = [];
+ theData = coords.map((item) => getAvg(item, 'state'));
+
+ // now push data into object...
+ if (entity.type === 'bar') {
+ this.toolsets[entity.tsidx].tools[entity.idx].tool.series = [...theData];
+ }
+
+ // Request a rerender of the card after receiving new data
+ this.requestUpdate();
+ }
+
+ /** *****************************************************************************
+ * card::getCardSize()
+ *
+ * Summary.
+ * Return a fixed value of 4 as the height.
+ *
+ */
+
+ getCardSize() {
+ return (4);
+ }
+}
+
+/**
+ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ */
+
+// Define the custom Swiss Army Knife card, so Lovelace / Lit can find the custom element!
+customElements.define('swiss-army-knife-card', SwissArmyKnifeCard);
+//# sourceMappingURL=swiss-army-knife-card-bundle.js.map
diff --git a/distjs/README.md b/distjs/README.md
deleted file mode 100644
index 8b7a79b..0000000
--- a/distjs/README.md
+++ /dev/null
@@ -1 +0,0 @@
-Split
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..601f225
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,2865 @@
+{
+ "name": "swiss-army-knife",
+ "version": "2.4.2",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "swiss-army-knife",
+ "version": "2.4.2",
+ "dependencies": {
+ "@formatjs/intl-utils": "^3.8.4",
+ "@tanem/svg-injector": "^10.1.53",
+ "custom-card-helpers": "^1.8.0",
+ "home-assistant-js-websocket": "^5.7.0",
+ "lit-element": "^2.5.1",
+ "lit-html": "^1.4.1"
+ },
+ "devDependencies": {
+ "@rollup/plugin-commonjs": "^24.1.0",
+ "@rollup/plugin-json": "^6.0.0",
+ "@rollup/plugin-node-resolve": "^15.0.2",
+ "@rollup/plugin-terser": "^0.4.1",
+ "eslint": "^8.39.0",
+ "eslint-config-airbnb-base": "^15.0.0",
+ "rollup": "^3.21.4",
+ "rollup-plugin-serve": "^2.0.2"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.21.5",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz",
+ "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==",
+ "dependencies": {
+ "regenerator-runtime": "^0.13.11"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+ "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz",
+ "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==",
+ "dev": true,
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz",
+ "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.5.1",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "8.39.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz",
+ "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@formatjs/ecma402-abstract": {
+ "version": "1.11.4",
+ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz",
+ "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==",
+ "dependencies": {
+ "@formatjs/intl-localematcher": "0.2.25",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/fast-memoize": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz",
+ "integrity": "sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/icu-messageformat-parser": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz",
+ "integrity": "sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "1.11.4",
+ "@formatjs/icu-skeleton-parser": "1.3.6",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/icu-skeleton-parser": {
+ "version": "1.3.6",
+ "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.6.tgz",
+ "integrity": "sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "1.11.4",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/intl-localematcher": {
+ "version": "0.2.25",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz",
+ "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@formatjs/intl-utils": {
+ "version": "3.8.4",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-3.8.4.tgz",
+ "integrity": "sha512-j5C6NyfKevIxsfLK8KwO1C0vvP7k1+h4A9cFpc+cr6mEwCc1sPkr17dzh0Ke6k9U5pQccAQoXdcNBl3IYa4+ZQ==",
+ "deprecated": "the package is rather renamed to @formatjs/ecma-abstract with some changes in functionality (primarily selectUnit is removed and we don't plan to make any further changes to this package",
+ "dependencies": {
+ "emojis-list": "^3.0.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.11.8",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
+ "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
+ "dev": true,
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^1.2.1",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+ "dev": true
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
+ "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/set-array": "^1.0.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
+ "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
+ "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/source-map": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz",
+ "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.0",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.4.15",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
+ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
+ "dev": true
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.18",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz",
+ "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "3.1.0",
+ "@jridgewell/sourcemap-codec": "1.4.14"
+ }
+ },
+ "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.4.14",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
+ "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
+ "dev": true
+ },
+ "node_modules/@lit-labs/ssr-dom-shim": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz",
+ "integrity": "sha512-kXOeFbfCm4fFf2A3WwVEeQj55tMZa8c8/f9AKHMobQMkzNUfUj+antR3fRPaZJawsa1aZiP/Da3ndpZrwEe4rQ=="
+ },
+ "node_modules/@lit/reactive-element": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.1.tgz",
+ "integrity": "sha512-va15kYZr7KZNNPZdxONGQzpUr+4sxVu7V/VG7a8mRfPPXUyhEYj5RzXCQmGrlP3tAh0L3HHm5AjBMFYRqlM9SA==",
+ "dependencies": {
+ "@lit-labs/ssr-dom-shim": "^1.0.0"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@rollup/plugin-commonjs": {
+ "version": "24.1.0",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.1.0.tgz",
+ "integrity": "sha512-eSL45hjhCWI0jCCXcNtLVqM5N1JlBGvlFfY0m6oOYnLCJ6N0qEXoZql4sY2MOUArzhH4SA/qBpTxvvZp2Sc+DQ==",
+ "dev": true,
+ "dependencies": {
+ "@rollup/pluginutils": "^5.0.1",
+ "commondir": "^1.0.1",
+ "estree-walker": "^2.0.2",
+ "glob": "^8.0.3",
+ "is-reference": "1.2.1",
+ "magic-string": "^0.27.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^2.68.0||^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/plugin-commonjs/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@rollup/plugin-commonjs/node_modules/glob": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
+ "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^5.0.1",
+ "once": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@rollup/plugin-commonjs/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@rollup/plugin-json": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.0.0.tgz",
+ "integrity": "sha512-i/4C5Jrdr1XUarRhVu27EEwjt4GObltD7c+MkCIpO2QIbojw8MUs+CCTqOphQi3Qtg1FLmYt+l+6YeoIf51J7w==",
+ "dev": true,
+ "dependencies": {
+ "@rollup/pluginutils": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0||^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/plugin-node-resolve": {
+ "version": "15.0.2",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.0.2.tgz",
+ "integrity": "sha512-Y35fRGUjC3FaurG722uhUuG8YHOJRJQbI6/CkbRkdPotSpDj9NtIN85z1zrcyDcCQIW4qp5mgG72U+gJ0TAFEg==",
+ "dev": true,
+ "dependencies": {
+ "@rollup/pluginutils": "^5.0.1",
+ "@types/resolve": "1.20.2",
+ "deepmerge": "^4.2.2",
+ "is-builtin-module": "^3.2.1",
+ "is-module": "^1.0.0",
+ "resolve": "^1.22.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^2.78.0||^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/plugin-terser": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.1.tgz",
+ "integrity": "sha512-aKS32sw5a7hy+fEXVy+5T95aDIwjpGHCTv833HXVtyKMDoVS7pBr5K3L9hEQoNqbJFjfANPrNpIXlTQ7is00eA==",
+ "dev": true,
+ "dependencies": {
+ "serialize-javascript": "^6.0.0",
+ "smob": "^0.0.6",
+ "terser": "^5.15.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^2.x || ^3.x"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/pluginutils": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz",
+ "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "estree-walker": "^2.0.2",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0||^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@tanem/svg-injector": {
+ "version": "10.1.53",
+ "resolved": "https://registry.npmjs.org/@tanem/svg-injector/-/svg-injector-10.1.53.tgz",
+ "integrity": "sha512-tR2Kh0GcTk+7hFTUsCo7JcEsAxz7j28dvaeC77jDp+acgGVdWXtk1v6lY8G0v1g813sgBrBzUr3St8RPBSmjEQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.21.5",
+ "content-type": "^1.0.5",
+ "tslib": "^2.5.0"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
+ "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==",
+ "dev": true
+ },
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/@types/resolve": {
+ "version": "1.20.2",
+ "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
+ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
+ "dev": true
+ },
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz",
+ "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g=="
+ },
+ "node_modules/acorn": {
+ "version": "8.8.2",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
+ "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz",
+ "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "is-array-buffer": "^3.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz",
+ "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "get-intrinsic": "^1.1.3",
+ "is-string": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz",
+ "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz",
+ "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
+ "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+ "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true
+ },
+ "node_modules/commondir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
+ "dev": true
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
+ },
+ "node_modules/confusing-browser-globals": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
+ "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==",
+ "dev": true
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/custom-card-helpers": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/custom-card-helpers/-/custom-card-helpers-1.9.0.tgz",
+ "integrity": "sha512-5IW4OXq3MiiCqDvqeu+MYsM1NmntKW1WfJhyJFsdP2tbzqEI4BOnqRz2qzdp08lE4QLVhYfRLwe0WAqgQVNeFg==",
+ "dependencies": {
+ "@formatjs/intl-utils": "^3.8.4",
+ "home-assistant-js-websocket": "^6.0.1",
+ "intl-messageformat": "^9.11.1",
+ "lit": "^2.1.1",
+ "rollup": "^2.63.0",
+ "superstruct": "^0.15.3",
+ "typescript": "^4.5.4"
+ }
+ },
+ "node_modules/custom-card-helpers/node_modules/home-assistant-js-websocket": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/home-assistant-js-websocket/-/home-assistant-js-websocket-6.1.1.tgz",
+ "integrity": "sha512-TnZFzF4mn5F/v0XKUTK2GMQXrn/+eQpgaSDSELl6U0HSwSbFwRhGWLz330YT+hiKMspDflamsye//RPL+zwhDw=="
+ },
+ "node_modules/custom-card-helpers/node_modules/rollup": {
+ "version": "2.79.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz",
+ "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==",
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
+ },
+ "node_modules/deepmerge": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz",
+ "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==",
+ "dev": true,
+ "dependencies": {
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/emojis-list": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
+ "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.21.2",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz",
+ "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==",
+ "dev": true,
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.0",
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "es-set-tostringtag": "^2.0.1",
+ "es-to-primitive": "^1.2.1",
+ "function.prototype.name": "^1.1.5",
+ "get-intrinsic": "^1.2.0",
+ "get-symbol-description": "^1.0.0",
+ "globalthis": "^1.0.3",
+ "gopd": "^1.0.1",
+ "has": "^1.0.3",
+ "has-property-descriptors": "^1.0.0",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.5",
+ "is-array-buffer": "^3.0.2",
+ "is-callable": "^1.2.7",
+ "is-negative-zero": "^2.0.2",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.2",
+ "is-string": "^1.0.7",
+ "is-typed-array": "^1.1.10",
+ "is-weakref": "^1.0.2",
+ "object-inspect": "^1.12.3",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.4",
+ "regexp.prototype.flags": "^1.4.3",
+ "safe-regex-test": "^1.0.0",
+ "string.prototype.trim": "^1.2.7",
+ "string.prototype.trimend": "^1.0.6",
+ "string.prototype.trimstart": "^1.0.6",
+ "typed-array-length": "^1.0.4",
+ "unbox-primitive": "^1.0.2",
+ "which-typed-array": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz",
+ "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.3",
+ "has": "^1.0.3",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz",
+ "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "has": "^1.0.3"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dev": true,
+ "dependencies": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.39.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz",
+ "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.4.0",
+ "@eslint/eslintrc": "^2.0.2",
+ "@eslint/js": "8.39.0",
+ "@humanwhocodes/config-array": "^0.11.8",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "ajv": "^6.10.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.2.0",
+ "eslint-visitor-keys": "^3.4.0",
+ "espree": "^9.5.1",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "grapheme-splitter": "^1.0.4",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-sdsl": "^4.1.4",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.1",
+ "strip-ansi": "^6.0.1",
+ "strip-json-comments": "^3.1.0",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-config-airbnb-base": {
+ "version": "15.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz",
+ "integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==",
+ "dev": true,
+ "dependencies": {
+ "confusing-browser-globals": "^1.0.10",
+ "object.assign": "^4.1.2",
+ "object.entries": "^1.1.5",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^7.32.0 || ^8.2.0",
+ "eslint-plugin-import": "^2.25.2"
+ }
+ },
+ "node_modules/eslint-import-resolver-node": {
+ "version": "0.3.7",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz",
+ "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "debug": "^3.2.7",
+ "is-core-module": "^2.11.0",
+ "resolve": "^1.22.1"
+ }
+ },
+ "node_modules/eslint-import-resolver-node/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-module-utils": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz",
+ "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "debug": "^3.2.7"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-module-utils/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import": {
+ "version": "2.27.5",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz",
+ "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "array.prototype.flatmap": "^1.3.1",
+ "debug": "^3.2.7",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.7",
+ "eslint-module-utils": "^2.7.4",
+ "has": "^1.0.3",
+ "is-core-module": "^2.11.0",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.values": "^1.1.6",
+ "resolve": "^1.22.1",
+ "semver": "^6.3.0",
+ "tsconfig-paths": "^3.14.1"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
+ "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz",
+ "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.5.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz",
+ "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.8.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+ "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "dev": true
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true
+ },
+ "node_modules/fastq": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
+ "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+ "dev": true,
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
+ "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+ "dev": true,
+ "dependencies": {
+ "flatted": "^3.1.0",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
+ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
+ "dev": true
+ },
+ "node_modules/for-each": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+ "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+ "dev": true,
+ "dependencies": {
+ "is-callable": "^1.1.3"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz",
+ "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.0",
+ "functions-have-names": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
+ "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
+ "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "13.20.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
+ "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
+ "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/grapheme-splitter": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
+ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
+ "dev": true
+ },
+ "node_modules/has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-bigints": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
+ "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
+ "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+ "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
+ "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/home-assistant-js-websocket": {
+ "version": "5.12.0",
+ "resolved": "https://registry.npmjs.org/home-assistant-js-websocket/-/home-assistant-js-websocket-5.12.0.tgz",
+ "integrity": "sha512-ZSoHeV4CU9RDJ4MfABaGFfM4jef73zG7+53LJr/5BfTJytAsFZD0A+GhtbwAIB1n/ARJ+6orb8Mbi3mfmUyEMA=="
+ },
+ "node_modules/ignore": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
+ "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "dev": true,
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "node_modules/internal-slot": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
+ "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.2.0",
+ "has": "^1.0.3",
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/intl-messageformat": {
+ "version": "9.13.0",
+ "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.13.0.tgz",
+ "integrity": "sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "1.11.4",
+ "@formatjs/fast-memoize": "1.2.1",
+ "@formatjs/icu-messageformat-parser": "2.1.0",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
+ "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.2.0",
+ "is-typed-array": "^1.1.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bigint": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
+ "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+ "dev": true,
+ "dependencies": {
+ "has-bigints": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
+ "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-builtin-module": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
+ "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
+ "dev": true,
+ "dependencies": {
+ "builtin-modules": "^3.3.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-builtin-module/node_modules/builtin-modules": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
+ "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz",
+ "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==",
+ "dev": true,
+ "dependencies": {
+ "has": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+ "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-module": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
+ "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==",
+ "dev": true
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
+ "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
+ "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-reference": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
+ "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+ "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
+ "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
+ "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+ "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz",
+ "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==",
+ "dev": true,
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
+ "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "node_modules/js-sdsl": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz",
+ "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==",
+ "dev": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/js-sdsl"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true
+ },
+ "node_modules/json5": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lit": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/lit/-/lit-2.7.3.tgz",
+ "integrity": "sha512-0a+u+vVbmgSfPu+fyvqjMPBX0Kwbyj9QOv9MbQFZhWGlV2cyk3lEwgfUQgYN+i/lx++1Z3wZknSIp3QCKxHLyg==",
+ "dependencies": {
+ "@lit/reactive-element": "^1.6.0",
+ "lit-element": "^3.3.0",
+ "lit-html": "^2.7.0"
+ }
+ },
+ "node_modules/lit-element": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.5.1.tgz",
+ "integrity": "sha512-ogu7PiJTA33bEK0xGu1dmaX5vhcRjBXCFexPja0e7P7jqLhTpNKYRPmE+GmiCaRVAbiQKGkUgkh/i6+bh++dPQ==",
+ "dependencies": {
+ "lit-html": "^1.1.1"
+ }
+ },
+ "node_modules/lit-html": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.4.1.tgz",
+ "integrity": "sha512-B9btcSgPYb1q4oSOb/PrOT6Z/H+r6xuNzfH4lFli/AWhYwdtrgQkQWBbIc6mdnf6E2IL3gDXdkkqNktpU0OZQA=="
+ },
+ "node_modules/lit/node_modules/lit-element": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.2.tgz",
+ "integrity": "sha512-xXAeVWKGr4/njq0rGC9dethMnYCq5hpKYrgQZYTzawt9YQhMiXfD+T1RgrdY3NamOxwq2aXlb0vOI6e29CKgVQ==",
+ "dependencies": {
+ "@lit-labs/ssr-dom-shim": "^1.1.0",
+ "@lit/reactive-element": "^1.3.0",
+ "lit-html": "^2.7.0"
+ }
+ },
+ "node_modules/lit/node_modules/lit-html": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.7.3.tgz",
+ "integrity": "sha512-9DyLzcn/kbRGowz2vFmSANFbRZTxYUgYYFqzie89w6GLpPUiBCDHfcdeRUV/k3Q2ueYxNjfv46yPCtKAEAPOVw==",
+ "dependencies": {
+ "@types/trusted-types": "^2.0.2"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
+ "node_modules/magic-string": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz",
+ "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.4.13"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/mime": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
+ "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
+ "dev": true,
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "peer": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true
+ },
+ "node_modules/object-inspect": {
+ "version": "1.12.3",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
+ "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
+ "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "has-symbols": "^1.0.3",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.entries": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.6.tgz",
+ "integrity": "sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz",
+ "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/opener": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
+ "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
+ "dev": true,
+ "bin": {
+ "opener": "bin/opener-bin.js"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
+ "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+ "dev": true,
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.3"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
+ "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.13.11",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
+ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz",
+ "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "functions-have-names": "^1.2.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.2",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
+ "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.11.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true,
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "3.21.4",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.4.tgz",
+ "integrity": "sha512-N5LxpvDolOm9ueiCp4NfB80omMDqb45ShtsQw2+OT3f11uJ197dv703NZvznYHP6RWR85wfxanXurXKG3ux2GQ==",
+ "dev": true,
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=14.18.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rollup-plugin-serve": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-serve/-/rollup-plugin-serve-2.0.2.tgz",
+ "integrity": "sha512-ALqyTbPhlf7FZ5RzlbDvMYvbKuCHWginJkTo6dMsbgji/a78IbsXox+pC83HENdkTRz8OXrTj+aShp3+3ratpg==",
+ "dev": true,
+ "dependencies": {
+ "mime": ">=2.4.6",
+ "opener": "1"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
+ "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.3",
+ "is-regex": "^1.1.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/serialize-javascript": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
+ "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
+ "dev": true,
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/smob": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/smob/-/smob-0.0.6.tgz",
+ "integrity": "sha512-V21+XeNni+tTyiST1MHsa84AQhT1aFZipzPpOFAVB8DkHzwJyjjAmt9bgwnuZiZWnIbMo2duE29wybxv/7HWUw==",
+ "dev": true
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz",
+ "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz",
+ "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz",
+ "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/superstruct": {
+ "version": "0.15.5",
+ "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.15.5.tgz",
+ "integrity": "sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ=="
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/terser": {
+ "version": "5.17.1",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.1.tgz",
+ "integrity": "sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/source-map": "^0.3.2",
+ "acorn": "^8.5.0",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true
+ },
+ "node_modules/tsconfig-paths": {
+ "version": "3.14.2",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
+ "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
+ "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg=="
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz",
+ "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "is-typed-array": "^1.1.9"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "4.9.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
+ "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=4.2.0"
+ }
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
+ "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.0.3",
+ "which-boxed-primitive": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+ "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+ "dev": true,
+ "dependencies": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz",
+ "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==",
+ "dev": true,
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.0",
+ "is-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
+ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..9baac09
--- /dev/null
+++ b/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "swiss-army-knife",
+ "version": "2.4.2",
+ "description": "Swiss Army Knife for Lovelace",
+ "main": "src/main.js",
+ "type": "module",
+ "scripts": {
+ "build": "npm run lint && npm run rollup",
+ "rollup": "rollup -c",
+ "lint": "eslint src/*.js",
+ "watch": "rollup -c --watch",
+ "postversion": "npm run build",
+ "audit-fix": "npx yarn-audit-fix",
+ "serve": "rollup -c serve.config.js --watch"
+ },
+ "devDependencies": {
+ "@rollup/plugin-commonjs": "^24.1.0",
+ "@rollup/plugin-json": "^6.0.0",
+ "@rollup/plugin-node-resolve": "^15.0.2",
+ "@rollup/plugin-terser": "^0.4.1",
+ "eslint": "^8.39.0",
+ "eslint-config-airbnb-base": "^15.0.0",
+ "rollup": "^3.21.4",
+ "rollup-plugin-serve": "^2.0.2"
+ },
+ "dependencies": {
+ "@formatjs/intl-utils": "^3.8.4",
+ "custom-card-helpers": "^1.8.0",
+ "home-assistant-js-websocket": "^5.7.0",
+ "lit-element": "^2.5.1",
+ "lit-html": "^1.4.1",
+ "@tanem/svg-injector": "^10.1.53"
+ }
+}
diff --git a/rollup.config.js b/rollup.config.js
new file mode 100644
index 0000000..6f11953
--- /dev/null
+++ b/rollup.config.js
@@ -0,0 +1,54 @@
+import resolve from '@rollup/plugin-node-resolve';
+import json from '@rollup/plugin-json';
+import serve from 'rollup-plugin-serve';
+import commonjs from '@rollup/plugin-commonjs';
+import terser from '@rollup/plugin-terser';
+
+const dev = process.env.ROLLUP_WATCH;
+
+const serveopts = {
+ contentBase: ['dist'],
+ host: '0.0.0.0',
+ port: 5050,
+ open: true,
+ allowCrossOrigin: true,
+ headers: {
+ 'Access-Control-Allow-Origin': '*',
+ },
+};
+
+export default {
+ input: 'src/main.js',
+ output: {
+ file: 'dist/swiss-army-knife-card-bundle.js',
+ format: 'es',
+ name: 'SwissArmyKnifeCard',
+ sourcemap: !!dev,
+ },
+ onwarn(warning, warn) {
+ // See: https://github.com/reduxjs/redux-toolkit/issues/1466
+ // Skip certain warnings
+ // should intercept ... but doesn't in some rollup versions
+ if (warning.code === 'THIS_IS_UNDEFINED') { return; }
+ // console.warn everything else
+ warn(warning);
+ },
+ watch: {
+ exclude: 'node_modules/**',
+ },
+ plugins: [
+ commonjs(),
+ // json({
+ // include: 'package.json',
+ // preferConst: true,
+ // }),
+ json(),
+ resolve(),
+ dev && serve(serveopts),
+ !dev && terser({
+ mangle: {
+ safari10: true,
+ },
+ }),
+ ],
+};
diff --git a/serve.config.js b/serve.config.js
new file mode 100644
index 0000000..d507454
--- /dev/null
+++ b/serve.config.js
@@ -0,0 +1,25 @@
+import serve from 'rollup-plugin-serve';
+
+const serveopts = {
+ contentBase: ['distjs'],
+ host: '0.0.0.0',
+ port: 5050,
+ open: true,
+ allowCrossOrigin: true,
+ headers: {
+ 'Access-Control-Allow-Origin': '*',
+ },
+};
+
+export default {
+ input: 'srcjs/swiss-army-knife-card.js',
+ output: {
+ file: 'distjs/swiss-army-knife-card-bundle.js',
+ format: 'es',
+ name: 'SwissArmyKnifeCard',
+ sourcemap: true,
+ },
+ plugins: [
+ serve(serveopts),
+ ],
+};
diff --git a/src/README.md b/src/README.md
deleted file mode 100644
index ae62232..0000000
--- a/src/README.md
+++ /dev/null
@@ -1,2 +0,0 @@
-## NOTE:
-This source folder will be used once the source has been split into multiple files and the remote docker container has been setup finally...
diff --git a/src/badge-tool.js b/src/badge-tool.js
new file mode 100644
index 0000000..0d3a4fb
--- /dev/null
+++ b/src/badge-tool.js
@@ -0,0 +1,134 @@
+import { svg } from 'lit-element';
+import { classMap } from 'lit-html/directives/class-map.js';
+import { styleMap } from 'lit-html/directives/style-map.js';
+
+import Merge from './merge';
+import Utils from './utils';
+import BaseTool from './base-tool';
+
+/** ****************************************************************************
+ * BadgeTool class
+ *
+ * Summary.
+ *
+ */
+
+export default class BadgeTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_BADGE_CONFIG = {
+ position: {
+ cx: 50,
+ cy: 50,
+ width: 100,
+ height: 25,
+ radius: 5,
+ ratio: 30,
+ divider: 30,
+ },
+ classes: {
+ tool: {
+ 'sak-badge': true,
+ hover: true,
+ },
+ left: {
+ 'sak-badge__left': true,
+ },
+ right: {
+ 'sak-badge__right': true,
+ },
+ },
+ styles: {
+ left: {
+ },
+ right: {
+ },
+ },
+ };
+ super(argToolset, Merge.mergeDeep(DEFAULT_BADGE_CONFIG, argConfig), argPos);
+
+ // Coordinates from left and right part.
+ this.svg.radius = Utils.calculateSvgDimension(argConfig.position.radius);
+ this.svg.leftXpos = this.svg.x;
+ this.svg.leftYpos = this.svg.y;
+ this.svg.leftWidth = (this.config.position.ratio / 100) * this.svg.width;
+ this.svg.arrowSize = (this.svg.height * this.config.position.divider / 100) / 2;
+ this.svg.divSize = (this.svg.height * (100 - this.config.position.divider) / 100) / 2;
+
+ this.svg.rightXpos = this.svg.x + this.svg.leftWidth;
+ this.svg.rightYpos = this.svg.y;
+ this.svg.rightWidth = ((100 - this.config.position.ratio) / 100) * this.svg.width;
+
+ this.classes.left = {};
+ this.classes.right = {};
+ this.styles.left = {};
+ this.styles.right = {};
+ if (this.dev.debug) console.log('BadgeTool constructor coords, dimensions', this.svg, this.config);
+ }
+
+ /** *****************************************************************************
+ * BadgeTool::_renderBadge()
+ *
+ * Summary.
+ * Renders the badge using precalculated coordinates and dimensions.
+ * Only the runtime style is calculated before rendering the badge
+ *
+ * Refs for creating the path online:
+ * - https://mavo.io/demos/svgpath/
+ *
+ */
+
+ _renderBadge() {
+ let svgItems = [];
+
+ this.MergeAnimationClassIfChanged();
+ this.MergeAnimationStyleIfChanged();
+
+ svgItems = svg`
+
+
+
+
+
+ `;
+
+ return svg`${svgItems}`;
+ }
+
+ /** *****************************************************************************
+ * BadgeTool::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ */
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderBadge()}
+
+ `;
+ }
+} // END of class
diff --git a/src/base-tool.js b/src/base-tool.js
new file mode 100644
index 0000000..a217751
--- /dev/null
+++ b/src/base-tool.js
@@ -0,0 +1,630 @@
+import { fireEvent } from 'custom-card-helpers';
+
+import Merge from './merge';
+import Utils from './utils';
+import Templates from './templates';
+import Colors from './colors';
+
+/** ***************************************************************************
+ * BaseTool class
+ *
+ * Summary.
+ *
+ */
+
+export default class BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ this.toolId = Math.random().toString(36).substr(2, 9);
+ this.toolset = argToolset;
+ this._card = this.toolset._card;
+ this.config = argConfig;
+
+ this.dev = { ...this._card.dev };
+
+ // The position is the absolute position of the GROUP within the svg viewport.
+ // The tool is positioned relative to this origin. A tool is always relative
+ // to a 200x200 default svg viewport. A (50,50) position of the tool
+ // centers the tool on the absolute position of the GROUP!
+ this.toolsetPos = argPos;
+
+ // Get SVG coordinates.
+ this.svg = {};
+
+ this.svg.cx = Utils.calculateSvgCoordinate(argConfig.position.cx, 0);
+ this.svg.cy = Utils.calculateSvgCoordinate(argConfig.position.cy, 0);
+
+ this.svg.height = argConfig.position.height ? Utils.calculateSvgDimension(argConfig.position.height) : 0;
+ this.svg.width = argConfig.position.width ? Utils.calculateSvgDimension(argConfig.position.width) : 0;
+
+ this.svg.x = (this.svg.cx) - (this.svg.width / 2);
+ this.svg.y = (this.svg.cy) - (this.svg.height / 2);
+
+ this.classes = {};
+ this.classes.card = {};
+ this.classes.toolset = {};
+ this.classes.tool = {};
+
+ this.styles = {};
+ this.styles.card = {};
+ this.styles.toolset = {};
+ this.styles.tool = {};
+
+ // Setup animation class and style and force initial processing by setting changed to true
+ this.animationClass = {};
+ this.animationClassHasChanged = true;
+
+ this.animationStyle = {};
+ this.animationStyleHasChanged = true;
+
+ // Process basic color stuff.
+ if (!this.config?.show?.style) {
+ if (!this.config.show) this.config.show = {};
+ this.config.show.style = 'default';
+ }
+ // Get colorstops and make a key/value store...
+ this.colorStops = {};
+ if ((this.config.colorstops) && (this.config.colorstops.colors)) {
+ Object.keys(this.config.colorstops.colors).forEach((key) => {
+ this.colorStops[key] = this.config.colorstops.colors[key];
+ });
+ }
+
+ if ((this.config.show.style === 'colorstop') && (this.config?.colorstops.colors)) {
+ this.sortedColorStops = Object.keys(this.config.colorstops.colors).map((n) => Number(n)).sort((a, b) => a - b);
+ }
+
+ this.csnew = {};
+ if ((this.config.csnew) && (this.config.csnew.colors)) {
+ this.config.csnew.colors.forEach((item, i) => {
+ this.csnew[item.stop] = this.config.csnew.colors[i];
+ });
+
+ this.sortedcsnew = Object.keys(this.csnew).map((n) => Number(n)).sort((a, b) => a - b);
+ }
+ }
+
+ /** *****************************************************************************
+ * BaseTool::textEllipsis()
+ *
+ * Summary.
+ * Very simple form of ellipsis, which is not supported by SVG.
+ * Cutoff text at number of characters and add '...'.
+ * This does NOT take into account the actual width of a character!
+ *
+ */
+ textEllipsis(argText, argEllipsis) {
+ if ((argEllipsis) && (argEllipsis < argText.length)) {
+ return argText.slice(0, argEllipsis - 1).concat('...');
+ } else {
+ return argText;
+ }
+ }
+
+ defaultEntityIndex() {
+ if (!this.default) {
+ this.default = {};
+ if (this.config.hasOwnProperty('entity_indexes')) {
+ this.default.entity_index = this.config.entity_indexes[0].entity_index;
+ } else {
+ // Must have entity_index! If not, just crash!
+ this.default.entity_index = this.config.entity_index;
+ }
+ }
+ return this.default.entity_index;
+ }
+
+ /** *****************************************************************************
+ * BaseTool::set value()
+ *
+ * Summary.
+ * Receive new state data for the entity this is linked to. Called from set hass;
+ *
+ */
+ set value(state) {
+ let localState = state;
+
+ if (this.dev.debug) console.log('BaseTool set value(state)', localState);
+ if (typeof (localState) !== 'undefined') if (this._stateValue?.toLowerCase() === localState.toLowerCase()) return;
+
+ this.derivedEntity = null;
+
+ if (this.config.derived_entity) {
+ this.derivedEntity = Templates.getJsTemplateOrValue(this, state, Merge.mergeDeep(this.config.derived_entity));
+
+ localState = this.derivedEntity.state?.toString();
+ }
+
+ this._stateValuePrev = this._stateValue || localState;
+ this._stateValue = localState;
+ this._stateValueIsDirty = true;
+
+ // If animations defined, calculate style for current state.
+
+ // 2022.07.04 Temp disable this return, as animations should be able to process the 'undefined' state too!!!!
+ // if (this._stateValue == undefined) return;
+ // if (typeof(this._stateValue) === 'undefined') return;
+
+ let isMatch = false;
+ // #TODO:
+ // Modify this loop using .find() orso. It now keeps returning true for all items in animations list.
+ // It works, but can be more efficient ;-)
+
+ this.activeAnimation = null;
+
+ if (this.config.animations) Object.keys(this.config.animations).map((animation) => {
+ // NEW!!!
+ // Config more than 1 level deep is overwritten, so never changed after first evaluation. Stuff is overwritten???
+ const tempConfig = JSON.parse(JSON.stringify(this.config.animations[animation]));
+
+ const item = Templates.getJsTemplateOrValue(this, this._stateValue, Merge.mergeDeep(tempConfig));
+ // var item = Templates.getJsTemplateOrValue(this, this._stateValue, Merge.mergeDeep(this.config.animations[animation]));
+
+ if (isMatch) return true;
+
+ // The state builder renames 'unavailable' to '-ua-'
+ // Change this temporary in here to match this...
+ if (item.state === 'unavailable') { item.state = '-ua-'; }
+
+ // #TODO:
+ // Default is item.state. But can also be item.custom_field[x], so you can compare with custom value
+ // Should index then not with item.state but item[custom_field[x]].toLowerCase() or similar...
+ // Or above, with the mapping of the item using the name?????
+
+ // Assume equals operator if not defined...
+ const operator = item.operator ? item.operator : '==';
+
+ switch (operator) {
+ case '==':
+ if (typeof (this._stateValue) === 'undefined') {
+ isMatch = (typeof item.state === 'undefined') || (item.state.toLowerCase() === 'undefined');
+ } else {
+ isMatch = this._stateValue.toLowerCase() === item.state.toLowerCase();
+ }
+ break;
+ case '!=':
+ if (typeof (this._stateValue) === 'undefined') {
+ isMatch = (item.state.toLowerCase() !== 'undefined');
+ } else {
+ isMatch = this._stateValue.toLowerCase() !== item.state.toLowerCase();
+ }
+ break;
+ case '>':
+ if (typeof (this._stateValue) !== 'undefined')
+ isMatch = Number(this._stateValue.toLowerCase()) > Number(item.state.toLowerCase());
+ break;
+ case '<':
+ if (typeof (this._stateValue) !== 'undefined')
+ isMatch = Number(this._stateValue.toLowerCase()) < Number(item.state.toLowerCase());
+ break;
+ case '>=':
+ if (typeof (this._stateValue) !== 'undefined')
+ isMatch = Number(this._stateValue.toLowerCase()) >= Number(item.state.toLowerCase());
+ break;
+ case '<=':
+ if (typeof (this._stateValue) !== 'undefined') {
+ isMatch = Number(this._stateValue.toLowerCase()) <= Number(item.state.toLowerCase());
+ }
+ break;
+ default:
+ // Unknown operator. Just do nothing and return;
+ isMatch = false;
+ }
+ // Revert state
+ if (item.state === '-ua-') { item.state = 'unavailable'; }
+
+ if (this.dev.debug) console.log('BaseTool, animation, match, value, config, operator', isMatch, this._stateValue, item.state, item.operator);
+ if (!isMatch) return true;
+
+ if (!this.animationClass || !item.reuse) { this.animationClass = {}; }
+ if (item.classes) {
+ this.animationClass = Merge.mergeDeep(this.animationClass, item.classes);
+ }
+
+ if (!this.animationStyle || !item.reuse) this.animationStyle = {};
+ if (item.styles) {
+ this.animationStyle = Merge.mergeDeep(this.animationStyle, item.styles);
+ }
+
+ this.animationStyleHasChanged = true;
+
+ // #TODO:
+ // Store activeAnimation. Should be renamed, and used for more purposes, as via this method
+ // you can override any value from within an animation, not just the css style settings.
+ this.item = item;
+ this.activeAnimation = item;
+
+ return isMatch;
+ });
+ }
+
+ /** *****************************************************************************
+ * BaseTool::set values()
+ *
+ * Summary.
+ * Receive new state data for the entity this is linked to. Called from set hass;
+ *
+ */
+
+ getEntityIndexFromAnimation(animation) {
+ // Check if animation has entity_index specified
+ if (animation.hasOwnProperty('entity_index')) return animation.entity_index;
+
+ // We need to get the default entity.
+ // If entity_index defined use that one...
+ if (this.config.hasOwnProperty('entity_index')) return this.config.entity_index;
+
+ // If entity_indexes is defined, take the
+ // first entity_index in the list as the default entity_index to use
+ if (this.config.entity_indexes) return (this.config.entity_indexes[0].entity_index);
+ }
+
+ getIndexInEntityIndexes(entityIdx) {
+ return this.config.entity_indexes.findIndex((element) => element.entity_index === entityIdx);
+ }
+
+ stateIsMatch(animation, state) {
+ let isMatch;
+ // NEW!!!
+ // Config more than 1 level deep is overwritten, so never changed after first evaluation. Stuff is overwritten???
+ const tempConfig = JSON.parse(JSON.stringify(animation));
+
+ const item = Templates.getJsTemplateOrValue(this, state, Merge.mergeDeep(tempConfig));
+
+ // Assume equals operator if not defined...
+ const operator = item.operator ? item.operator : '==';
+
+ switch (operator) {
+ case '==':
+ if (typeof (state) === 'undefined') {
+ isMatch = (typeof item.state === 'undefined') || (item.state.toLowerCase() === 'undefined');
+ } else {
+ isMatch = state.toLowerCase() === item.state.toLowerCase();
+ }
+ break;
+ case '!=':
+ if (typeof (state) === 'undefined') {
+ isMatch = (typeof item.state !== 'undefined') || (item.state.toLowerCase() !== 'undefined');
+ } else {
+ isMatch = state.toLowerCase() !== item.state.toLowerCase();
+ }
+ break;
+ case '>':
+ if (typeof (state) !== 'undefined')
+ isMatch = Number(state.toLowerCase()) > Number(item.state.toLowerCase());
+ break;
+ case '<':
+ if (typeof (state) !== 'undefined')
+ isMatch = Number(state.toLowerCase()) < Number(item.state.toLowerCase());
+ break;
+ case '>=':
+ if (typeof (state) !== 'undefined')
+ isMatch = Number(state.toLowerCase()) >= Number(item.state.toLowerCase());
+ break;
+ case '<=':
+ if (typeof (state) !== 'undefined')
+ isMatch = Number(state.toLowerCase()) <= Number(item.state.toLowerCase());
+ break;
+ default:
+ // Unknown operator. Just do nothing and return;
+ isMatch = false;
+ }
+ return isMatch;
+ }
+
+ mergeAnimationData(animation) {
+ if (!this.animationClass || !animation.reuse) this.animationClass = {};
+ if (animation.classes) {
+ this.animationClass = Merge.mergeDeep(this.animationClass, animation.classes);
+ }
+
+ if (!this.animationStyle || !animation.reuse) this.animationStyle = {};
+ if (animation.styles) {
+ this.animationStyle = Merge.mergeDeep(this.animationStyle, animation.styles);
+ }
+
+ this.animationStyleHasChanged = true;
+
+ // With more than 1 matching state (more entities), we have to preserve some
+ // extra data, such as setting the icon, name, area, etc. HOW?? Merge??
+
+ if (!this.item) this.item = {};
+ this.item = Merge.mergeDeep(this.item, animation);
+ this.activeAnimation = { ...animation }; // Merge.mergeDeep(this.activeAnimation, animation);
+ }
+
+ set values(states) {
+ if (!this._lastStateValues) this._lastStateValues = [];
+ if (!this._stateValues) this._stateValues = [];
+
+ const localStates = [...states];
+
+ if (this.dev.debug) console.log('BaseTool set values(state)', localStates);
+
+ // Loop through all values...
+ // var state;
+ for (let index = 0; index < states.length; ++index) {
+ // state = states[index];
+
+ // eslint-disable-next-line no-empty
+ if (typeof (localStates[index]) !== 'undefined') if (this._stateValues[index]?.toLowerCase() === localStates[index].toLowerCase()) {} else {
+ // State has changed, process...
+
+ // eslint-disable-next-line no-lonely-if
+ if (this.config.derived_entities) {
+ this.derivedEntities[index] = Templates.getJsTemplateOrValue(this, states[index], Merge.mergeDeep(this.config.derived_entities[index]));
+
+ localStates[index] = this.derivedEntities[index].state?.toString();
+ }
+ }
+
+ this._lastStateValues[index] = this._stateValues[index] || localStates[index];
+ this._stateValues[index] = localStates[index];
+ this._stateValueIsDirty = true;
+
+ let isMatch = false;
+
+ this.activeAnimation = null;
+
+ // eslint-disable-next-line no-loop-func, no-unused-vars
+ if (this.config.animations) Object.keys(this.config.animations.map((aniKey, aniValue) => {
+ const statesIndex = this.getIndexInEntityIndexes(this.getEntityIndexFromAnimation(aniKey));
+ isMatch = this.stateIsMatch(aniKey, states[statesIndex]);
+
+ // console.log("set values, animations", aniKey, aniValue, statesIndex, isMatch, states);
+
+ if (isMatch) {
+ this.mergeAnimationData(aniKey);
+ return true;
+ } else {
+ return false;
+ }
+ }));
+ }
+ this._stateValue = this._stateValues[this.getIndexInEntityIndexes(this.defaultEntityIndex())];
+ this._stateValuePrev = this._lastStateValues[this.getIndexInEntityIndexes(this.defaultEntityIndex())];
+ }
+
+ EnableHoverForInteraction() {
+ const hover = (this.config.hasOwnProperty('entity_index') || (this.config?.user_actions?.tap_action));
+ this.classes.tool.hover = !!hover;
+ }
+
+ /** *****************************************************************************
+ * BaseTool::MergeAnimationStyleIfChanged()
+ *
+ * Summary.
+ * Merge changed animationStyle with configured static styles.
+ *
+ */
+ MergeAnimationStyleIfChanged(argDefaultStyles) {
+ if (this.animationStyleHasChanged) {
+ this.animationStyleHasChanged = false;
+ if (argDefaultStyles) {
+ this.styles = Merge.mergeDeep(argDefaultStyles, this.config.styles, this.animationStyle);
+ } else {
+ this.styles = Merge.mergeDeep(this.config.styles, this.animationStyle);
+ }
+
+ if (this.styles.card) {
+ if (Object.keys(this.styles.card).length !== 0) {
+ this._card.styles.card = Merge.mergeDeep(this.styles.card);
+ }
+ }
+ }
+ }
+
+ /** *****************************************************************************
+ * BaseTool::MergeAnimationClassIfChanged()
+ *
+ * Summary.
+ * Merge changed animationclass with configured static styles.
+ *
+ */
+ MergeAnimationClassIfChanged(argDefaultClasses) {
+ // Hack
+ // @TODO This setting is still required for some reason. So this change is not detected...
+ this.animationClassHasChanged = true;
+
+ if (this.animationClassHasChanged) {
+ this.animationClassHasChanged = false;
+ if (argDefaultClasses) {
+ this.classes = Merge.mergeDeep(argDefaultClasses, this.config.classes, this.animationClass);
+ } else {
+ this.classes = Merge.mergeDeep(this.config.classes, this.animationClass);
+ }
+ }
+ }
+
+ /** *****************************************************************************
+ * BaseTool::MergeColorFromState()
+ *
+ * Summary.
+ * Merge color depending on state into colorStyle
+ *
+ */
+
+ MergeColorFromState(argStyleMap) {
+ if (this.config.hasOwnProperty('entity_index')) {
+ const color = this.getColorFromState(this._stateValue);
+ if (color !== '') {
+ argStyleMap.fill = this.config[this.config.show.style].fill ? color : '';
+ argStyleMap.stroke = this.config[this.config.show.style].stroke ? color : '';
+
+ // this.config[this.config.show.style].fill ? argStyleMap['fill'] = color : '';
+ // this.config[this.config.show.style].stroke ? argStyleMap['stroke'] = color : '';
+ }
+ }
+ }
+
+ /** *****************************************************************************
+ * BaseTool::MergeColorFromState2()
+ *
+ * Summary.
+ * Merge color depending on state into colorStyle
+ *
+ */
+
+ MergeColorFromState2(argStyleMap, argPart) {
+ if (this.config.hasOwnProperty('entity_index')) {
+ const fillColor = this.config[this.config.show.style].fill ? this.getColorFromState2(this._stateValue, argPart, 'fill') : '';
+ const strokeColor = this.config[this.config.show.style].stroke ? this.getColorFromState2(this._stateValue, argPart, 'stroke') : '';
+ if (fillColor !== '') {
+ argStyleMap.fill = fillColor;
+ }
+ if (strokeColor !== '') {
+ argStyleMap.stroke = strokeColor;
+ }
+ }
+ }
+
+ /** *****************************************************************************
+ * BaseTool::getColorFromState()
+ *
+ * Summary.
+ * Get color from colorstop or gradient depending on state.
+ *
+ */
+ getColorFromState(argValue) {
+ let color = '';
+ switch (this.config.show.style) {
+ case 'default':
+ break;
+ case 'fixedcolor':
+ color = this.config.color;
+ break;
+ case 'colorstop':
+ case 'colorstops':
+ case 'colorstopgradient':
+ color = Colors.calculateColor(argValue, this.colorStops, (this.config.show.style === 'colorstopgradient'));
+ break;
+ case 'minmaxgradient':
+ color = Colors.calculateColor(argValue, this.colorStopsMinMax, true);
+ break;
+ default:
+ }
+ return color;
+ }
+
+ /** *****************************************************************************
+ * BaseTool::getColorFromState2()
+ *
+ * Summary.
+ * Get color from colorstop or gradient depending on state.
+ *
+ */
+ getColorFromState2(argValue, argPart, argProperty) {
+ let color = '';
+ switch (this.config.show.style) {
+ case 'colorstop':
+ case 'colorstops':
+ case 'colorstopgradient':
+ color = Colors.calculateColor2(argValue, this.csnew, argPart, argProperty, (this.config.show.style === 'colorstopgradient'));
+ break;
+ case 'minmaxgradient':
+ color = Colors.calculateColor2(argValue, this.colorStopsMinMax, argPart, argProperty, true);
+ break;
+ default:
+ }
+ return color;
+ }
+
+ /** *****************************************************************************
+ * BaseTool::_processTapEvent()
+ *
+ * Summary.
+ * Processes the mouse click of the user and dispatches the event to the
+ * configure handler.
+ *
+ */
+
+ _processTapEvent(node, hass, config, actionConfig, entityId, parameterValue) {
+ let e;
+
+ if (!actionConfig) return;
+ fireEvent(node, 'haptic', actionConfig.haptic || 'medium');
+
+ if (this.dev.debug) console.log('_processTapEvent', config, actionConfig, entityId, parameterValue);
+ for (let i = 0; i < actionConfig.actions.length; i++) {
+ switch (actionConfig.actions[i].action) {
+ case 'more-info': {
+ if (typeof entityId !== 'undefined') {
+ e = new Event('hass-more-info', { composed: true });
+ e.detail = { entityId };
+ node.dispatchEvent(e);
+ }
+ break;
+ }
+ case 'navigate': {
+ if (!actionConfig.actions[i].navigation_path) return;
+ window.history.pushState(null, '', actionConfig.actions[i].navigation_path);
+ e = new Event('location-changed', { composed: true });
+ e.detail = { replace: false };
+ window.dispatchEvent(e);
+ break;
+ }
+ case 'call-service': {
+ if (!actionConfig.actions[i].service) return;
+ const [domain, service] = actionConfig.actions[i].service.split('.', 2);
+ const serviceData = { ...actionConfig.actions[i].service_data };
+
+ // Fill with current entity_id if none given
+ if (!serviceData.entity_id) {
+ serviceData.entity_id = entityId;
+ }
+ // If parameter defined, add this one with the parameterValue
+ if (actionConfig.actions[i].parameter) {
+ serviceData[actionConfig.actions[i].parameter] = parameterValue;
+ }
+ hass.callService(domain, service, serviceData);
+ break;
+ }
+ default: {
+ console.error('Unknown Event definition', actionConfig);
+ }
+ }
+ }
+ }
+
+ /** *****************************************************************************
+ * BaseTool::handleTapEvent()
+ *
+ * Summary.
+ * Handles the first part of mouse click processing.
+ * It stops propagation to the parent and processes the event.
+ *
+ * The action can be configured per tool.
+ *
+ */
+
+ handleTapEvent(argEvent, argToolConfig) {
+ argEvent.stopPropagation();
+ argEvent.preventDefault();
+
+ let tapConfig;
+ // If no user_actions defined, AND there is an entity_index,
+ // define a default 'more-info' tap action
+ if (argToolConfig.hasOwnProperty('entity_index') && (!argToolConfig.user_actions)) {
+ tapConfig = {
+ haptic: 'light',
+ actions: [{
+ action: 'more-info',
+ }],
+ };
+ } else {
+ tapConfig = argToolConfig.user_actions?.tap_action;
+ }
+
+ if (!tapConfig) return;
+
+ this._processTapEvent(
+ this._card,
+ this._card._hass,
+ this.config,
+ tapConfig,
+ this._card.config.hasOwnProperty('entities')
+ ? this._card.config.entities[argToolConfig.entity_index]?.entity
+ : undefined,
+ undefined,
+ );
+ }
+} // -- CLASS
diff --git a/src/circle-tool.js b/src/circle-tool.js
new file mode 100644
index 0000000..6f325b0
--- /dev/null
+++ b/src/circle-tool.js
@@ -0,0 +1,101 @@
+import { svg } from 'lit-element';
+import { classMap } from 'lit-html/directives/class-map.js';
+import { styleMap } from 'lit-html/directives/style-map.js';
+
+import Merge from './merge';
+import Utils from './utils';
+import BaseTool from './base-tool';
+
+/** ****************************************************************************
+ * CircleTool class
+ *
+ * Summary.
+ *
+ */
+
+export default class CircleTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_CIRCLE_CONFIG = {
+ position: {
+ cx: 50,
+ cy: 50,
+ radius: 50,
+ },
+ classes: {
+ tool: {
+ 'sak-circle': true,
+ hover: true,
+ },
+ circle: {
+ 'sak-circle__circle': true,
+ },
+ },
+ styles: {
+ tool: {
+ },
+ circle: {
+ },
+ },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_CIRCLE_CONFIG, argConfig), argPos);
+ this.EnableHoverForInteraction();
+
+ this.svg.radius = Utils.calculateSvgDimension(argConfig.position.radius);
+
+ this.classes.circle = {};
+ this.styles.circle = {};
+ if (this.dev.debug) console.log('CircleTool constructor config, svg', this.toolId, this.config, this.svg);
+ }
+
+ /** *****************************************************************************
+ * CircleTool::value()
+ *
+ * Summary.
+ * Receive new state data for the entity this circle is linked to. Called from set hass;
+ *
+ */
+ set value(state) {
+ super.value = state;
+ }
+
+ /** *****************************************************************************
+ * CircleTool::_renderCircle()
+ *
+ * Summary.
+ * Renders the circle using precalculated coordinates and dimensions.
+ * Only the runtime style is calculated before rendering the circle
+ *
+ */
+
+ _renderCircle() {
+ this.MergeAnimationClassIfChanged();
+ this.MergeAnimationStyleIfChanged();
+ this.MergeColorFromState(this.styles.circle);
+
+ return svg`
+
+
+ `;
+ }
+
+ /** *****************************************************************************
+ * CircleTool::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ */
+
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderCircle()}
+
+ `;
+ }
+} // END of class
diff --git a/src/circular-slider-tool.js b/src/circular-slider-tool.js
new file mode 100644
index 0000000..a0dc797
--- /dev/null
+++ b/src/circular-slider-tool.js
@@ -0,0 +1,994 @@
+import { svg } from 'lit-element';
+import { classMap } from 'lit-html/directives/class-map.js';
+import { styleMap } from 'lit-html/directives/style-map.js';
+
+import Merge from './merge';
+import Utils from './utils';
+import BaseTool from './base-tool';
+// eslint-disable-next-line object-curly-newline
+import { angle360, range, round, clamp } from './const';
+
+/** ****************************************************************************
+ * CircularSliderTool::constructor class
+ *
+ * Summary.
+ *
+ */
+
+export default class CircularSliderTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_ARCSLIDER_CONFIG = {
+ position: {
+ cx: 50,
+ cy: 50,
+ radius: 45,
+ start_angle: 30,
+ end_angle: 230,
+ track: {
+ width: 2,
+ },
+ active: {
+ width: 4,
+ },
+ thumb: {
+ height: 10,
+ width: 10,
+ radius: 5,
+ },
+ capture: {
+ height: 25,
+ width: 25,
+ radius: 25,
+ },
+ label: {
+ placement: 'none',
+ cx: 10,
+ cy: 10,
+ },
+ },
+ show: {
+ uom: 'end',
+ active: false,
+ },
+ classes: {
+ tool: {
+ 'sak-circslider': true,
+ hover: true,
+ },
+ capture: {
+ 'sak-circslider__capture': true,
+ hover: true,
+ },
+ active: {
+ 'sak-circslider__active': true,
+ },
+ track: {
+ 'sak-circslider__track': true,
+ },
+ thumb: {
+ 'sak-circslider__thumb': true,
+ hover: true,
+ },
+ label: {
+ 'sak-circslider__value': true,
+ },
+ uom: {
+ 'sak-circslider__uom': true,
+ },
+ },
+ styles: {
+ tool: {
+ },
+ active: {
+ },
+ capture: {
+ },
+ track: {
+ },
+ thumb: {
+ },
+ label: {
+ },
+ uom: {
+ },
+ },
+ scale: {
+ min: 0,
+ max: 100,
+ step: 1,
+ },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_ARCSLIDER_CONFIG, argConfig), argPos);
+
+ this.svg.radius = Utils.calculateSvgDimension(this.config.position.radius);
+
+ // Init arc settings
+ this.arc = {};
+ this.arc.startAngle = this.config.position.start_angle;
+ this.arc.endAngle = this.config.position.end_angle;
+ this.arc.size = range(this.config.position.end_angle, this.config.position.start_angle);
+ this.arc.clockwise = this.config.position.end_angle > this.config.position.start_angle;
+ this.arc.direction = this.arc.clockwise ? 1 : -1;
+ this.arc.pathLength = 2 * this.arc.size / 360 * Math.PI * this.svg.radius;
+ this.arc.arcLength = 2 * Math.PI * this.svg.radius;
+
+ this.arc.startAngle360 = angle360(this.arc.startAngle, this.arc.startAngle, this.arc.endAngle);
+ this.arc.endAngle360 = angle360(this.arc.startAngle, this.arc.endAngle, this.arc.endAngle);
+
+ this.arc.startAngleSvgPoint = this.polarToCartesian(this.svg.cx, this.svg.cy, this.svg.radius, this.svg.radius, this.arc.startAngle360);
+ this.arc.endAngleSvgPoint = this.polarToCartesian(this.svg.cx, this.svg.cy, this.svg.radius, this.svg.radius, this.arc.endAngle360);
+
+ this.arc.scaleDasharray = 2 * this.arc.size / 360 * Math.PI * this.svg.radius;
+ this.arc.dashOffset = this.arc.clockwise ? 0 : -this.arc.scaleDasharray - this.arc.arcLength;
+
+ this.arc.currentAngle = this.arc.startAngle;
+
+ this.svg.startAngle = this.config.position.start_angle;
+ this.svg.endAngle = this.config.position.end_angle;
+ this.svg.diffAngle = (this.config.position.end_angle - this.config.position.start_angle);
+
+ // this.svg.pathLength = 2 * 260/360 * Math.PI * this.svg.radius;
+ this.svg.pathLength = 2 * this.arc.size / 360 * Math.PI * this.svg.radius;
+ this.svg.circleLength = 2 * Math.PI * this.svg.radius;
+
+ this.svg.label = {};
+ switch (this.config.position.label.placement) {
+ case 'position':
+ this.svg.label.cx = Utils.calculateSvgCoordinate(this.config.position.label.cx, 0);
+ this.svg.label.cy = Utils.calculateSvgCoordinate(this.config.position.label.cy, 0);
+ break;
+
+ case 'thumb':
+ this.svg.label.cx = this.svg.cx;
+ this.svg.label.cy = this.svg.cy;
+ break;
+
+ case 'none':
+ break;
+
+ default:
+ console.error('CircularSliderTool - constructor: invalid label placement [none, position, thumb] = ', this.config.position.label.placement);
+ throw Error('CircularSliderTool::constructor - invalid label placement [none, position, thumb] = ', this.config.position.label.placement);
+ }
+
+ this.svg.track = {};
+ this.svg.track.width = Utils.calculateSvgDimension(this.config.position.track.width);
+ this.svg.active = {};
+ this.svg.active.width = Utils.calculateSvgDimension(this.config.position.active.width);
+ this.svg.thumb = {};
+ this.svg.thumb.width = Utils.calculateSvgDimension(this.config.position.thumb.width);
+ this.svg.thumb.height = Utils.calculateSvgDimension(this.config.position.thumb.height);
+ this.svg.thumb.radius = Utils.calculateSvgDimension(this.config.position.thumb.radius);
+ this.svg.thumb.cx = this.svg.cx;
+ this.svg.thumb.cy = this.svg.cy;
+ this.svg.thumb.x1 = this.svg.cx - this.svg.thumb.width / 2;
+ this.svg.thumb.y1 = this.svg.cy - this.svg.thumb.height / 2;
+
+ // This should be a moving capture element, larger than the thumb!!
+ this.svg.capture = {};
+ this.svg.capture.width = Utils.calculateSvgDimension(Math.max(this.config.position.capture.width, this.config.position.thumb.width * 1.2));
+ this.svg.capture.height = Utils.calculateSvgDimension(Math.max(this.config.position.capture.height, this.config.position.thumb.height * 1.2));
+ this.svg.capture.radius = Utils.calculateSvgDimension(this.config.position.capture.radius);
+ this.svg.capture.x1 = this.svg.cx - this.svg.capture.width / 2;
+ this.svg.capture.y1 = this.svg.cy - this.svg.capture.height / 2;
+
+ // The CircularSliderTool is rotated around its svg base point. This is NOT the center of the circle!
+ // Adjust x and y positions within the svg viewport to re-center the circle after rotating
+ this.svg.rotate = {};
+ this.svg.rotate.degrees = this.arc.clockwise ? (-90 + this.arc.startAngle) : (this.arc.endAngle360 - 90);
+ this.svg.rotate.cx = this.svg.cx;
+ this.svg.rotate.cy = this.svg.cy;
+
+ // Init classes
+ this.classes.track = {};
+ this.classes.active = {};
+ this.classes.thumb = {};
+ this.classes.label = {};
+ this.classes.uom = {};
+
+ // Init styles
+ this.styles.track = {};
+ this.styles.active = {};
+ this.styles.thumb = {};
+ this.styles.label = {};
+ this.styles.uom = {};
+
+ // Init scale
+ this.svg.scale = {};
+ this.svg.scale.min = this.config.scale.min;
+ this.svg.scale.max = this.config.scale.max;
+ // this.svg.scale.min = myScale.min;
+ // this.svg.scale.max = myScale.max;
+
+ this.svg.scale.center = Math.abs(this.svg.scale.max - this.svg.scale.min) / 2 + this.svg.scale.min;
+ this.svg.scale.svgPointMin = this.sliderValueToPoint(this.svg.scale.min);
+ this.svg.scale.svgPointMax = this.sliderValueToPoint(this.svg.scale.max);
+ this.svg.scale.svgPointCenter = this.sliderValueToPoint(this.svg.scale.center);
+ this.svg.scale.step = this.config.scale.step;
+
+ this.rid = null;
+
+ // Hmmm. Does not help on safari to get the refresh ok. After data change, everything is ok!!
+ this.thumbPos = this.sliderValueToPoint(this.config.scale.min);
+ this.svg.thumb.x1 = this.thumbPos.x - this.svg.thumb.width / 2;
+ this.svg.thumb.y1 = this.thumbPos.y - this.svg.thumb.height / 2;
+
+ this.svg.capture.x1 = this.thumbPos.x - this.svg.capture.width / 2;
+ this.svg.capture.y1 = this.thumbPos.y - this.svg.capture.height / 2;
+
+ if (this.dev.debug) console.log('CircularSliderTool::constructor', this.config, this.svg);
+ }
+
+ // From roundSlider... https://github.com/soundar24/roundSlider/blob/master/src/roundslider.js
+
+ // eslint-disable-next-line no-unused-vars
+ pointToAngle360(point, center, isDrag) {
+ const radian = Math.atan2(point.y - center.y, center.x - point.x);
+ let angle = (-radian / (Math.PI / 180));
+ // the angle value between -180 to 180.. so convert to a 360 angle
+ angle += -90;
+
+ if (angle < 0) angle += 360;
+
+ // With this addition, the clockwise stuff, including passing 0 works. but anti clockwise stopped working!!
+ if (this.arc.clockwise) if (angle < this.arc.startAngle360) angle += 360;
+
+ // Yep. Should add another to get this working...
+ if (!this.arc.clockwise) if (angle < this.arc.endAngle360) angle += 360;
+
+ return angle;
+ }
+
+ isAngle360InBetween(argAngle) {
+ let inBetween;
+ if (this.arc.clockwise) {
+ inBetween = ((argAngle >= this.arc.startAngle360) && (argAngle <= this.arc.endAngle360));
+ } else {
+ inBetween = ((argAngle <= this.arc.startAngle360) && (argAngle >= this.arc.endAngle360));
+ }
+ return !!inBetween;
+ }
+
+ polarToCartesian(centerX, centerY, radiusX, radiusY, angleInDegrees) {
+ const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;
+
+ return {
+ x: centerX + (radiusX * Math.cos(angleInRadians)),
+ y: centerY + (radiusY * Math.sin(angleInRadians)),
+ };
+ }
+
+ // SVGPoint deprecated. Use DOMPoint!!
+ // DOMPoint.fromPoint(); ??? Or just keep using SVGPoint...
+ pointToSliderValue(m) {
+ let state;
+ let scalePos;
+
+ const center = {};
+ center.x = this.svg.cx;
+ center.y = this.svg.cy;
+ const newAngle = this.pointToAngle360(m, center, true);
+ let { myAngle } = this;
+
+ const inBetween = this.isAngle360InBetween(newAngle);
+ if (inBetween) {
+ this.myAngle = newAngle;
+ myAngle = newAngle;
+ this.arc.currentAngle = myAngle;
+ }
+
+ this.arc.currentAngle = myAngle;
+ if (this.arc.clockwise) scalePos = (myAngle - this.arc.startAngle360) / this.arc.size;
+ if (!this.arc.clockwise) scalePos = (this.arc.startAngle360 - myAngle) / this.arc.size;
+
+ state = ((this.config.scale.max - this.config.scale.min) * scalePos) + this.config.scale.min;
+ state = Math.round(state / this.svg.scale.step) * this.svg.scale.step;
+ state = Math.max(Math.min(this.config.scale.max, state), this.config.scale.min);
+
+ this.arc.currentAngle = myAngle;
+
+ if ((this.dragging) && (!inBetween)) {
+ // Clip to max or min value
+ state = round(this.svg.scale.min, state, this.svg.scale.max);
+ this.m = this.sliderValueToPoint(state);
+ }
+
+ return state;
+ }
+
+ sliderValueToPoint(argValue) {
+ let state = Utils.calculateValueBetween(this.config.scale.min, this.config.scale.max, argValue);
+ if (isNaN(state)) state = 0;
+ let angle;
+ if (this.arc.clockwise) {
+ angle = (this.arc.size * state) + this.arc.startAngle360;
+ } else {
+ angle = (this.arc.size * (1 - state)) + this.arc.endAngle360;
+ }
+
+ if (angle < 0) angle += 360;
+ const svgPoint = this.polarToCartesian(this.svg.cx, this.svg.cy, this.svg.radius, this.svg.radius, angle);
+
+ this.arc.currentAngle = angle;
+
+ return svgPoint;
+ }
+
+ updateValue(m) {
+ this._value = this.pointToSliderValue(m);
+ // set dist to 0 to cancel animation frame
+ const dist = 0;
+ // improvement
+ if (Math.abs(dist) < 0.01) {
+ if (this.rid) {
+ window.cancelAnimationFrame(this.rid);
+ this.rid = null;
+ }
+ }
+ }
+
+ updateThumb(m) {
+ if (this.dragging) {
+ this.thumbPos = this.sliderValueToPoint(this._value);
+ this.svg.thumb.x1 = this.thumbPos.x - this.svg.thumb.width / 2;
+ this.svg.thumb.y1 = this.thumbPos.y - this.svg.thumb.height / 2;
+
+ this.svg.capture.x1 = this.thumbPos.x - this.svg.capture.width / 2;
+ this.svg.capture.y1 = this.thumbPos.y - this.svg.capture.height / 2;
+
+ const rotateStr = `rotate(${this.arc.currentAngle} ${this.svg.capture.width / 2} ${this.svg.capture.height / 2})`;
+ this.elements.thumb.setAttribute('transform', rotateStr);
+
+ this.elements.thumbGroup.setAttribute('x', this.svg.capture.x1);
+ this.elements.thumbGroup.setAttribute('y', this.svg.capture.y1);
+ }
+
+ this.updateLabel(m);
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ updateActiveTrack(m) {
+ const min = this.config.scale.min || 0;
+ const max = this.config.scale.max || 100;
+ let val = Utils.calculateValueBetween(min, max, this.labelValue);
+ if (isNaN(val)) val = 0;
+ const score = val * this.svg.pathLength;
+ this.dashArray = `${score} ${this.svg.circleLength}`;
+
+ if (this.dragging) {
+ this.elements.activeTrack.setAttribute('stroke-dasharray', this.dashArray);
+ }
+ }
+
+ updateLabel(m) {
+ if (this.dev.debug) console.log('SLIDER - updateLabel start', m, this.config.position.orientation);
+
+ // const dec = (this._card.config.entities[this.config.entity_index].decimals || 0);
+ const dec = (this._card.config.entities[this.defaultEntityIndex()].decimals || 0);
+
+ const x = 10 ** dec;
+ this.labelValue2 = (Math.round(this.pointToSliderValue(m) * x) / x).toFixed(dec);
+
+ if (this.config.position.label.placement !== 'none') {
+ this.elements.label.textContent = this.labelValue2;
+ }
+ }
+
+ /*
+ * mouseEventToPoint
+ *
+ * Translate mouse/touch client window coordinates to SVG window coordinates
+ *
+ */
+ mouseEventToPoint(e) {
+ let p = this.elements.svg.createSVGPoint();
+ p.x = e.touches ? e.touches[0].clientX : e.clientX;
+ p.y = e.touches ? e.touches[0].clientY : e.clientY;
+ const ctm = this.elements.svg.getScreenCTM().inverse();
+ p = p.matrixTransform(ctm);
+ return p;
+ }
+
+ callDragService() {
+ if (typeof this.labelValue2 === 'undefined') return;
+
+ if (this.labelValuePrev !== this.labelValue2) {
+ this.labelValuePrev = this.labelValue2;
+
+ this._processTapEvent(
+ this._card,
+ this._card._hass,
+ this.config,
+ this.config.user_actions.tap_action,
+ this._card.config.entities[this.defaultEntityIndex()]?.entity,
+ this.labelValue2,
+ );
+ }
+ if (this.dragging)
+ this.timeOutId = setTimeout(() => this.callDragService(), this.config.user_actions.drag_action.update_interval);
+ }
+
+ callTapService() {
+ if (typeof this.labelValue2 === 'undefined') return;
+
+ this._processTapEvent(
+ this._card,
+ this._card._hass,
+ this.config,
+ this.config.user_actions?.tap_action,
+ this._card.config.entities[this.defaultEntityIndex()]?.entity,
+ this.labelValue2,
+ );
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ firstUpdated(changedProperties) {
+ this.labelValue = this._stateValue;
+
+ function FrameArc() {
+ this.rid = window.requestAnimationFrame(FrameArc);
+ this.updateValue(this.m);
+ this.updateThumb(this.m);
+ this.updateActiveTrack(this.m);
+ }
+
+ if (this.dev.debug) console.log('circslider - firstUpdated');
+ this.elements = {};
+ this.elements.svg = this._card.shadowRoot.getElementById('circslider-'.concat(this.toolId));
+ this.elements.track = this.elements.svg.querySelector('#track');
+ this.elements.activeTrack = this.elements.svg.querySelector('#active-track');
+ this.elements.capture = this.elements.svg.querySelector('#capture');
+ this.elements.thumbGroup = this.elements.svg.querySelector('#thumb-group');
+ this.elements.thumb = this.elements.svg.querySelector('#thumb');
+ this.elements.label = this.elements.svg.querySelector('#label tspan');
+
+ if (this.dev.debug) console.log('circslider - firstUpdated svg = ',
+ this.elements.svg, 'activeTrack=', this.elements.activeTrack,
+ 'thumb=', this.elements.thumb, 'label=', this.elements.label, 'text=', this.elements.text,
+ );
+
+ const protectBorderPassing = () => {
+ const diffMax = range(this.svg.scale.max, this.labelValue) <= this.rangeMax;
+ const diffMin = range(this.svg.scale.min, this.labelValue) <= this.rangeMin;
+
+ // passing borders from max to min...
+ const fromMaxToMin = !!(diffMin && this.diffMax);
+ const fromMinToMax = !!(diffMax && this.diffMin);
+ if (fromMaxToMin) {
+ this.labelValue = this.svg.scale.max;
+ this.m = this.sliderValueToPoint(this.labelValue);
+ this.rangeMax = this.svg.scale.max / 10;
+ this.rangeMin = range(this.svg.scale.max, this.svg.scale.min + (this.svg.scale.max / 5));
+ } else if (fromMinToMax) {
+ this.labelValue = this.svg.scale.min;
+ this.m = this.sliderValueToPoint(this.labelValue);
+ this.rangeMax = range(this.svg.scale.min, this.svg.scale.max - (this.svg.scale.max / 5));
+ this.rangeMin = this.svg.scale.max / 10;
+ } else {
+ this.diffMax = diffMax;
+ this.diffMin = diffMin;
+ this.rangeMin = (this.svg.scale.max / 5);
+ this.rangeMax = (this.svg.scale.max / 5);
+ }
+ };
+
+ const pointerMove = (e) => {
+ e.preventDefault();
+
+ if (this.dragging) {
+ this.m = this.mouseEventToPoint(e);
+ this.labelValue = this.pointToSliderValue(this.m);
+
+ protectBorderPassing();
+
+ FrameArc.call(this);
+ }
+ };
+
+ const pointerDown = (e) => {
+ e.preventDefault();
+
+ // User is dragging the thumb of the slider!
+ this.dragging = true;
+
+ // NEW:
+ // We use mouse stuff for pointerdown, but have to use pointer stuff to make sliding work on Safari. Why??
+ window.addEventListener('pointermove', pointerMove, false);
+ // eslint-disable-next-line no-use-before-define
+ window.addEventListener('pointerup', pointerUp, false);
+
+ // const mousePos = this.mouseEventToPoint(e);
+ // console.log("pointerdown", mousePos, this.svg.thumb, this.m);
+
+ // Check for drag_action. If none specified, or update_interval = 0, don't update while dragging...
+
+ if ((this.config.user_actions?.drag_action) && (this.config.user_actions?.drag_action.update_interval)) {
+ if (this.config.user_actions.drag_action.update_interval > 0) {
+ this.timeOutId = setTimeout(() => this.callDragService(), this.config.user_actions.drag_action.update_interval);
+ } else {
+ this.timeOutId = null;
+ }
+ }
+ this.m = this.mouseEventToPoint(e);
+ this.labelValue = this.pointToSliderValue(this.m);
+
+ protectBorderPassing();
+
+ if (this.dev.debug) console.log('pointerDOWN', Math.round(this.m.x * 100) / 100);
+ FrameArc.call(this);
+ };
+
+ const pointerUp = (e) => {
+ e.preventDefault();
+ if (this.dev.debug) console.log('pointerUP');
+
+ window.removeEventListener('pointermove', pointerMove, false);
+ window.removeEventListener('pointerup', pointerUp, false);
+
+ window.removeEventListener('mousemove', pointerMove, false);
+ window.removeEventListener('touchmove', pointerMove, false);
+ window.removeEventListener('mouseup', pointerUp, false);
+ window.removeEventListener('touchend', pointerUp, false);
+
+ this.labelValuePrev = this.labelValue;
+
+ // If we were not dragging, do check for passing border stuff!
+
+ if (!this.dragging) {
+ protectBorderPassing();
+ return;
+ }
+
+ this.dragging = false;
+ clearTimeout(this.timeOutId);
+ this.timeOutId = null;
+ this.target = 0;
+ this.labelValue2 = this.labelValue;
+
+ FrameArc.call(this);
+ this.callTapService();
+ };
+
+ const mouseWheel = (e) => {
+ e.preventDefault();
+
+ clearTimeout(this.wheelTimeOutId);
+ this.dragging = true;
+ this.wheelTimeOutId = setTimeout(() => {
+ clearTimeout(this.timeOutId);
+ this.timeOutId = null;
+ this.labelValue2 = this.labelValue;
+ this.dragging = false;
+ this.callTapService();
+ }, 500);
+
+ if ((this.config.user_actions?.drag_action) && (this.config.user_actions?.drag_action.update_interval)) {
+ if (this.config.user_actions.drag_action.update_interval > 0) {
+ this.timeOutId = setTimeout(() => this.callDragService(), this.config.user_actions.drag_action.update_interval);
+ } else {
+ this.timeOutId = null;
+ }
+ }
+ const newValue = +this.labelValue + +((e.altKey ? 10 * this.svg.scale.step : this.svg.scale.step) * Math.sign(e.deltaY));
+
+ this.labelValue = clamp(this.svg.scale.min, newValue, this.svg.scale.max);
+ this.m = this.sliderValueToPoint(this.labelValue);
+ this.pointToSliderValue(this.m);
+
+ FrameArc.call(this);
+
+ this.labelValue2 = this.labelValue;
+ };
+ this.elements.thumbGroup.addEventListener('touchstart', pointerDown, false);
+ this.elements.thumbGroup.addEventListener('mousedown', pointerDown, false);
+
+ this.elements.svg.addEventListener('wheel', mouseWheel, false);
+ }
+ /** *****************************************************************************
+ * CircularSliderTool::value()
+ *
+ * Summary.
+ * Sets the value of the CircularSliderTool. Value updated via set hass.
+ * Calculate CircularSliderTool settings & colors depending on config and new value.
+ *
+ */
+
+ set value(state) {
+ super.value = state;
+ if (!this.dragging) this.labelValue = this._stateValue;
+
+ // Calculate the size of the arc to fill the dasharray with this
+ // value. It will fill the CircularSliderTool relative to the state and min/max
+ // values given in the configuration.
+
+ if (!this.dragging) {
+ const min = this.config.scale.min || 0;
+ const max = this.config.scale.max || 100;
+ let val = Math.min(Utils.calculateValueBetween(min, max, this._stateValue), 1);
+
+ // Don't display anything, that is NO track, thumb to start...
+ if (isNaN(val)) val = 0;
+ const score = val * this.svg.pathLength;
+ this.dashArray = `${score} ${this.svg.circleLength}`;
+
+ const thumbPos = this.sliderValueToPoint(this._stateValue);
+ this.svg.thumb.x1 = thumbPos.x - this.svg.thumb.width / 2;
+ this.svg.thumb.y1 = thumbPos.y - this.svg.thumb.height / 2;
+
+ this.svg.capture.x1 = thumbPos.x - this.svg.capture.width / 2;
+ this.svg.capture.y1 = thumbPos.y - this.svg.capture.height / 2;
+ }
+ }
+
+ set values(states) {
+ super.values = states;
+ if (!this.dragging) this.labelValue = this._stateValues[this.getIndexInEntityIndexes(this.defaultEntityIndex())];
+
+ // Calculate the size of the arc to fill the dasharray with this
+ // value. It will fill the CircularSliderTool relative to the state and min/max
+ // values given in the configuration.
+
+ if (!this.dragging) {
+ const min = this.config.scale.min || 0;
+ const max = this.config.scale.max || 100;
+ let val = Math.min(Utils.calculateValueBetween(min, max, this._stateValues[this.getIndexInEntityIndexes(this.defaultEntityIndex())]), 1);
+
+ // Don't display anything, that is NO track, thumb to start...
+ if (isNaN(val)) val = 0;
+ const score = val * this.svg.pathLength;
+ this.dashArray = `${score} ${this.svg.circleLength}`;
+
+ const thumbPos = this.sliderValueToPoint(this._stateValues[this.getIndexInEntityIndexes(this.defaultEntityIndex())]);
+ this.svg.thumb.x1 = thumbPos.x - this.svg.thumb.width / 2;
+ this.svg.thumb.y1 = thumbPos.y - this.svg.thumb.height / 2;
+
+ this.svg.capture.x1 = thumbPos.x - this.svg.capture.width / 2;
+ this.svg.capture.y1 = thumbPos.y - this.svg.capture.height / 2;
+ }
+ }
+
+ _renderUom() {
+ if (this.config.show.uom === 'none') {
+ return svg``;
+ } else {
+ this.MergeAnimationStyleIfChanged();
+ this.MergeColorFromState(this.styles.uom);
+
+ let fsuomStr = this.styles.label['font-size'];
+
+ let fsuomValue = 0.5;
+ let fsuomType = 'em';
+ const fsuomSplit = fsuomStr.match(/\D+|\d*\.?\d+/g);
+ if (fsuomSplit.length === 2) {
+ fsuomValue = Number(fsuomSplit[0]) * 0.6;
+ fsuomType = fsuomSplit[1];
+ } else console.error('Cannot determine font-size for state/unit', fsuomStr);
+
+ fsuomStr = { 'font-size': fsuomValue + fsuomType };
+
+ this.styles.uom = Merge.mergeDeep(this.config.styles.uom, fsuomStr);
+
+ const uom = this._card._buildUom(this.derivedEntity, this._card.entities[this.defaultEntityIndex()], this._card.config.entities[this.defaultEntityIndex()]);
+
+ // Check for location of uom. end = next to state, bottom = below state ;-), etc.
+ if (this.config.show.uom === 'end') {
+ return svg`
+
+ ${uom}
+ `;
+ } else if (this.config.show.uom === 'bottom') {
+ return svg`
+
+ ${uom}
+ `;
+ } else if (this.config.show.uom === 'top') {
+ return svg`
+
+ ${uom}
+ `;
+ } else {
+ return svg`
+
+ ERR
+ `;
+ }
+ }
+ }
+
+ /** *****************************************************************************
+ * CircularSliderTool::_renderCircSlider()
+ *
+ * Summary.
+ * Renders the CircularSliderTool
+ *
+ * Description.
+ * The horseshoes are rendered in a viewbox of 200x200 (SVG_VIEW_BOX).
+ * Both are centered with a radius of 45%, ie 200*0.45 = 90.
+ *
+ * The horseshoes are rotated 220 degrees and are 2 * 26/36 * Math.PI * r in size
+ * There you get your value of 408.4070449,180 ;-)
+ */
+
+ _renderCircSlider() {
+ this.MergeAnimationClassIfChanged();
+ this.MergeColorFromState();
+ this.MergeAnimationStyleIfChanged();
+
+ // this.MergeColorFromState();
+
+ this.renderValue = this._stateValue;
+ if (this.dragging) {
+ this.renderValue = this.labelValue2;
+ } else if (this.elements?.label) this.elements.label.textContent = this.renderValue;
+ function renderLabel(argGroup) {
+ if ((this.config.position.label.placement === 'thumb') && argGroup) {
+ return svg`
+
+
+ ${this.renderValue}
+ ${this._renderUom()}
+
+ `;
+ }
+
+ if ((this.config.position.label.placement === 'position') && !argGroup) {
+ return svg`
+
+ ${this.renderValue ? this.renderValue : ''}
+ ${this.renderValue ? this._renderUom() : ''}
+
+ `;
+ }
+ }
+
+ function renderThumbGroup() {
+ // Original version but with SVG.
+ // Works in both Chrome and Safari 15.5. But rotate is only on rect... NOT on group!!!!
+ // transform="rotate(${this.arc.currentAngle} ${this.svg.thumb.cx} ${this.svg.thumb.cy})"
+ // This one works ...
+ return svg`
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ // Original version but with SVG.
+ // Works in both Chrome and Safari 15.5. But rotate is only on rect... NOT on group!!!!
+ // transform="rotate(${this.arc.currentAngle} ${this.svg.thumb.cx} ${this.svg.thumb.cy})"
+ // This one works ... BUT...
+ // Now again not after refresh on safari. Ok after udpate. Change is using a style for rotate(xxdeg), instead of transform=rotate()...
+ // Works on Safari, not on Chrome. Only change is no extra group level...
+ // return svg`
+ //
+ //
+ //
+
+ //
+ //
+ //
+ // `;
+
+ // Original version but with SVG.
+ // Works in both Chrome and Safari 15.5. But rotate is only on rect... NOT on group!!!!
+ // transform="rotate(${this.arc.currentAngle} ${this.svg.thumb.cx} ${this.svg.thumb.cy})"
+ // return svg`
+ //
+ //
+
+ //
+
+ //
+ //
+ //
+ // `;
+
+ // WIP!!!!!!!!!!!
+ // Now without tests for Safari and 15.1...
+ // Same behaviour in safari: first refresh wrong, then after data ok.
+ // return svg`
+ //
+ //
+ //
+ //
+ //
+ //
+ // `;
+
+ // Original version. Working on Chrome and Safari 15.5, NOT on Safari 15.1.
+ // But I want grouping to rotate and move all the components, so should be changed anyway...
+ // return svg`
+ //
+ //
+ //
+ //
+ //
+ //
+ // `;
+
+ // WIP!!!!!!!!!!!
+ // This one works on Safari 15.5 and Chrome, but on Safari not on initial refresh, but after data update...
+ // Seems the other way around compared to the solution below for 15.1 etc.
+ // return svg`
+ //
+ //
+ //
+ //
+ //
+ //
+ // `;
+
+ // This version working in all browsers, but has no rotate... So logical...
+ // return svg`
+ //
+ //
+ //
+ //
+ //
+ //
+ // `;
+
+ // This version works on Safari 14, but NOT on Safari 15 and Chrome. The thumb has weird locations...
+ // Uses an SVG to position stuff. Rest is relative positions in SVG...
+ // Rotate is from center of SVG...
+ //
+ // Works on Safari 15.5 after refresh, but not when data changes. WHY???????????????????
+ // Something seems to ruin stuff when data comes in...
+ // return svg`
+ //
+ //
+ //
+ //
+ //
+ //
+ // `;
+
+ // Original version. Working on Chrome and Safari 15.5, NOT on Safari 15.1.
+ // But I want grouping to rotate and move all the components, so should be changed anyway...
+ // return svg`
+ //
+ //
+ //
+ //
+ //
+ //
+ // `;
+
+ // return svg`
+ //
+ //
+ //
+ //
+ // ${renderLabel.call(this, true)}
+ //
+ // `;
+ }
+
+ return svg`
+
+
+
+
+
+
+ ${renderThumbGroup.call(this)}
+ ${renderLabel.call(this, false)}
+
+
+ `;
+ }
+
+ /** *****************************************************************************
+ * CircularSliderTool::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ */
+ render() {
+ return svg`
+
+ ${this._renderCircSlider()}
+
+
+ `;
+ }
+} // END of class
diff --git a/src/colors.js b/src/colors.js
new file mode 100644
index 0000000..351493f
--- /dev/null
+++ b/src/colors.js
@@ -0,0 +1,231 @@
+/** ***************************************************************************
+ * Colors class
+ *
+ * Summary.
+ *
+ */
+
+export default class Colors {
+ /** *****************************************************************************
+ * Colors::static properties()
+ *
+ * Summary.
+ * Declares the static class properties.
+ * Needs eslint parserOptions ecmaVersion: 2022
+ *
+ */
+ static {
+ Colors.colorCache = {};
+ Colors.element = undefined;
+ }
+
+ /** *****************************************************************************
+ * Colors::setElement()
+ *
+ * Summary.
+ * Sets the HTML element (the custom card) to work with getting colors
+ *
+ */
+
+ static setElement(argElement) {
+ Colors.element = argElement;
+ }
+
+ /** *****************************************************************************
+ * card::_calculateColor()
+ *
+ * Summary.
+ *
+ * #TODO:
+ * replace by TinyColor library? Is that possible/feasible??
+ *
+ */
+
+ static calculateColor(argState, argStops, argIsGradient) {
+ const sortedStops = Object.keys(argStops).map((n) => Number(n)).sort((a, b) => a - b);
+
+ let start; let end; let
+ val;
+ const l = sortedStops.length;
+
+ if (argState <= sortedStops[0]) {
+ return argStops[sortedStops[0]];
+ } else if (argState >= sortedStops[l - 1]) {
+ return argStops[sortedStops[l - 1]];
+ } else {
+ for (let i = 0; i < l - 1; i++) {
+ const s1 = sortedStops[i];
+ const s2 = sortedStops[i + 1];
+ if (argState >= s1 && argState < s2) {
+ [start, end] = [argStops[s1], argStops[s2]];
+ if (!argIsGradient) {
+ return start;
+ }
+ val = Colors.calculateValueBetween(s1, s2, argState);
+ break;
+ }
+ }
+ }
+ return Colors.getGradientValue(start, end, val);
+ }
+
+ /** *****************************************************************************
+ * card::_calculateColor2()
+ *
+ * Summary.
+ *
+ * #TODO:
+ * replace by TinyColor library? Is that possible/feasible??
+ *
+ */
+
+ static calculateColor2(argState, argStops, argPart, argProperty, argIsGradient) {
+ const sortedStops = Object.keys(argStops).map((n) => Number(n)).sort((a, b) => a - b);
+
+ let start; let end; let
+ val;
+ const l = sortedStops.length;
+
+ if (argState <= sortedStops[0]) {
+ return argStops[sortedStops[0]];
+ } else if (argState >= sortedStops[l - 1]) {
+ return argStops[sortedStops[l - 1]];
+ } else {
+ for (let i = 0; i < l - 1; i++) {
+ const s1 = sortedStops[i];
+ const s2 = sortedStops[i + 1];
+ if (argState >= s1 && argState < s2) {
+ // console.log('calculateColor2 ', argStops[s1], argStops[s2]);
+ [start, end] = [argStops[s1].styles[argPart][argProperty], argStops[s2].styles[argPart][argProperty]];
+ if (!argIsGradient) {
+ return start;
+ }
+ val = Colors.calculateValueBetween(s1, s2, argState);
+ break;
+ }
+ }
+ }
+ return Colors.getGradientValue(start, end, val);
+ }
+
+ /** *****************************************************************************
+ * card::_calculateValueBetween()
+ *
+ * Summary.
+ * Clips the argValue value between argStart and argEnd, and returns the between value ;-)
+ *
+ * Returns NaN if argValue is undefined
+ *
+ * NOTE: Rename to valueToPercentage ??
+ */
+
+ static calculateValueBetween(argStart, argEnd, argValue) {
+ return (Math.min(Math.max(argValue, argStart), argEnd) - argStart) / (argEnd - argStart);
+ }
+
+ /** *****************************************************************************
+ * card::_getColorVariable()
+ *
+ * Summary.
+ * Get value of CSS color variable, specified as var(--color-value)
+ * These variables are defined in the Lovelace element so it appears...
+ *
+ */
+
+ static getColorVariable(argColor) {
+ const newColor = argColor.substr(4, argColor.length - 5);
+
+ const returnColor = window.getComputedStyle(Colors.element).getPropertyValue(newColor);
+ return returnColor;
+ }
+
+ /** *****************************************************************************
+ * card::_getGradientValue()
+ *
+ * Summary.
+ * Get gradient value of color as a result of a color_stop.
+ * An RGBA value is calculated, so transparency is possible...
+ *
+ * The colors (colorA and colorB) can be specified as:
+ * - a css variable, var(--color-value)
+ * - a hex value, #fff or #ffffff
+ * - an rgb() or rgba() value
+ * - a hsl() or hsla() value
+ * - a named css color value, such as white.
+ *
+ */
+
+ static getGradientValue(argColorA, argColorB, argValue) {
+ const resultColorA = Colors.colorToRGBA(argColorA);
+ const resultColorB = Colors.colorToRGBA(argColorB);
+
+ // We have a rgba() color array from cache or canvas.
+ // Calculate color in between, and return #hex value as a result.
+ //
+
+ const v1 = 1 - argValue;
+ const v2 = argValue;
+ const rDec = Math.floor((resultColorA[0] * v1) + (resultColorB[0] * v2));
+ const gDec = Math.floor((resultColorA[1] * v1) + (resultColorB[1] * v2));
+ const bDec = Math.floor((resultColorA[2] * v1) + (resultColorB[2] * v2));
+ const aDec = Math.floor((resultColorA[3] * v1) + (resultColorB[3] * v2));
+
+ // And convert full RRGGBBAA value to #hex.
+ const rHex = Colors.padZero(rDec.toString(16));
+ const gHex = Colors.padZero(gDec.toString(16));
+ const bHex = Colors.padZero(bDec.toString(16));
+ const aHex = Colors.padZero(aDec.toString(16));
+
+ return `#${rHex}${gHex}${bHex}${aHex}`;
+ }
+
+ static padZero(argValue) {
+ if (argValue.length < 2) {
+ argValue = `0${argValue}`;
+ }
+ return argValue.substr(0, 2);
+ }
+
+ /** *****************************************************************************
+ * card::_colorToRGBA()
+ *
+ * Summary.
+ * Get RGBA color value of argColor.
+ *
+ * The argColor can be specified as:
+ * - a css variable, var(--color-value)
+ * - a hex value, #fff or #ffffff
+ * - an rgb() or rgba() value
+ * - a hsl() or hsla() value
+ * - a named css color value, such as white.
+ *
+ */
+
+ static colorToRGBA(argColor) {
+ // return color if found in colorCache...
+ const retColor = Colors.colorCache[argColor];
+ if (retColor) return retColor;
+
+ let theColor = argColor;
+ // Check for 'var' colors
+ const a0 = argColor.substr(0, 3);
+ if (a0.valueOf() === 'var') {
+ theColor = Colors.getColorVariable(argColor);
+ }
+
+ // Get color from canvas. This always returns an rgba() value...
+ const canvas = window.document.createElement('canvas');
+ // eslint-disable-next-line no-multi-assign
+ canvas.width = canvas.height = 1;
+ const ctx = canvas.getContext('2d');
+
+ ctx.clearRect(0, 0, 1, 1);
+ ctx.fillStyle = theColor;
+ ctx.fillRect(0, 0, 1, 1);
+ const outColor = [...ctx.getImageData(0, 0, 1, 1).data];
+
+ Colors.colorCache[argColor] = outColor;
+
+ return outColor;
+ }
+} // END OF CLASS
diff --git a/src/const.js b/src/const.js
new file mode 100644
index 0000000..bd3eb04
--- /dev/null
+++ b/src/const.js
@@ -0,0 +1,34 @@
+// Set sizes:
+// If svg size is changed, change the font size accordingly.
+// These two are related ;-) For font-size, 1em = 1%
+const SCALE_DIMENSIONS = 2;
+const SVG_DEFAULT_DIMENSIONS = 200 * SCALE_DIMENSIONS;
+const SVG_DEFAULT_DIMENSIONS_HALF = SVG_DEFAULT_DIMENSIONS / 2;
+const SVG_VIEW_BOX = SVG_DEFAULT_DIMENSIONS;
+const FONT_SIZE = SVG_DEFAULT_DIMENSIONS / 100;
+
+// Clamp number between two values
+const clamp = (min, num, max) => Math.min(Math.max(num, min), max);
+
+// Round to nearest value
+const round = (min, num, max) => ((Math.abs(num - min) > Math.abs(max - num)) ? max : min);
+
+// Force angle between 0 and 360, or even more for angle comparisons!
+const angle360 = (start, angle, end) => ((start < 0 || end < 0) ? angle + 360 : angle);
+
+// Size or range given by two values
+const range = (value1, value2) => Math.abs(value1 - value2);
+
+// const radianToDegrees = (radian) => (-radian / (Math.PI / 180));
+
+export {
+ SCALE_DIMENSIONS,
+ SVG_DEFAULT_DIMENSIONS,
+ SVG_DEFAULT_DIMENSIONS_HALF,
+ SVG_VIEW_BOX,
+ FONT_SIZE,
+ clamp,
+ round,
+ angle360,
+ range,
+};
diff --git a/src/ellipse-tool.js b/src/ellipse-tool.js
new file mode 100644
index 0000000..910304a
--- /dev/null
+++ b/src/ellipse-tool.js
@@ -0,0 +1,90 @@
+import { svg } from 'lit-element';
+import { classMap } from 'lit-html/directives/class-map.js';
+import { styleMap } from 'lit-html/directives/style-map.js';
+
+import Merge from './merge';
+import Utils from './utils';
+import BaseTool from './base-tool';
+
+/** ****************************************************************************
+ * EllipseTool class
+ *
+ * Summary.
+ *
+ */
+
+export default class EllipseTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_ELLIPSE_CONFIG = {
+ position: {
+ cx: 50,
+ cy: 50,
+ radiusx: 50,
+ radiusy: 25,
+ },
+ classes: {
+ tool: {
+ 'sak-ellipse': true,
+ hover: true,
+ },
+ ellipse: {
+ 'sak-ellipse__ellipse': true,
+ },
+ },
+ styles: {
+ ellipse: {
+ },
+ },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_ELLIPSE_CONFIG, argConfig), argPos);
+
+ this.svg.radiusx = Utils.calculateSvgDimension(argConfig.position.radiusx);
+ this.svg.radiusy = Utils.calculateSvgDimension(argConfig.position.radiusy);
+
+ this.classes.ellipse = {};
+ this.styles.ellipse = {};
+
+ if (this.dev.debug) console.log('EllipseTool constructor coords, dimensions', this.coords, this.dimensions, this.svg, this.config);
+ }
+
+ /** *****************************************************************************
+ * EllipseTool::_renderEllipse()
+ *
+ * Summary.
+ * Renders the ellipse using precalculated coordinates and dimensions.
+ * Only the runtime style is calculated before rendering the ellipse
+ *
+ */
+
+ _renderEllipse() {
+ this.MergeAnimationClassIfChanged();
+ this.MergeAnimationStyleIfChanged();
+ this.MergeColorFromState(this.styles.ellipse);
+
+ if (this.dev.debug) console.log('EllipseTool - renderEllipse', this.svg.cx, this.svg.cy, this.svg.radiusx, this.svg.radiusy);
+
+ return svg`
+
+ `;
+ }
+
+ /** *****************************************************************************
+ * EllipseTool::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ */
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderEllipse()}
+
+ `;
+ }
+} // END of class
diff --git a/src/entity-area-tool.js b/src/entity-area-tool.js
new file mode 100644
index 0000000..c9bdab1
--- /dev/null
+++ b/src/entity-area-tool.js
@@ -0,0 +1,102 @@
+import { svg } from 'lit-element';
+import { classMap } from 'lit-html/directives/class-map.js';
+import { styleMap } from 'lit-html/directives/style-map.js';
+
+import Merge from './merge';
+import BaseTool from './base-tool';
+
+/** ****************************************************************************
+ * EntityAreaTool class
+ *
+ * Summary.
+ *
+ */
+
+export default class EntityAreaTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_AREA_CONFIG = {
+ classes: {
+ tool: {
+ },
+ area: {
+ 'sak-area__area': true,
+ hover: true,
+ },
+ },
+ styles: {
+ tool: {
+ },
+ area: {
+ },
+ },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_AREA_CONFIG, argConfig), argPos);
+
+ // Text is rendered in its own context. No need for SVG coordinates.
+ this.classes.area = {};
+ this.styles.area = {};
+ if (this.dev.debug) console.log('EntityAreaTool constructor coords, dimensions', this.coords, this.dimensions, this.svg, this.config);
+ }
+
+ /** *****************************************************************************
+ * EntityAreaTool::_buildArea()
+ *
+ * Summary.
+ * Builds the Area string.
+ *
+ */
+
+ _buildArea(entityState, entityConfig) {
+ return (
+ entityConfig.area
+ || '?'
+ );
+ }
+
+ /** *****************************************************************************
+ * EntityAreaTool::_renderEntityArea()
+ *
+ * Summary.
+ * Renders the entity area using precalculated coordinates and dimensions.
+ * Only the runtime style is calculated before rendering the area
+ *
+ */
+
+ _renderEntityArea() {
+ this.MergeAnimationClassIfChanged();
+ this.MergeColorFromState(this.styles.area);
+ this.MergeAnimationStyleIfChanged();
+
+ const area = this.textEllipsis(
+ this._buildArea(
+ this._card.entities[this.defaultEntityIndex()],
+ this._card.config.entities[this.defaultEntityIndex()],
+ ),
+ this.config?.show?.ellipsis,
+ );
+
+ return svg`
+
+ ${area}
+
+ `;
+ }
+
+ /** *****************************************************************************
+ * EntityAreaTool::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ */
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderEntityArea()}
+
+ `;
+ }
+} // END of class
diff --git a/src/entity-icon-tool.js b/src/entity-icon-tool.js
new file mode 100644
index 0000000..e974c89
--- /dev/null
+++ b/src/entity-icon-tool.js
@@ -0,0 +1,278 @@
+import { svg } from 'lit-element';
+import { classMap } from 'lit-html/directives/class-map.js';
+import { styleMap } from 'lit-html/directives/style-map.js';
+import { stateIcon } from 'custom-card-helpers';
+
+import { FONT_SIZE } from './const';
+import Merge from './merge';
+import BaseTool from './base-tool';
+import Utils from './utils';
+
+/** ****************************************************************************
+ * EntityIconTool class
+ *
+ * Summary.
+ *
+ */
+
+export default class EntityIconTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_ICON_CONFIG = {
+ classes: {
+ tool: {
+ 'sak-icon': true,
+ hover: true,
+ },
+ icon: {
+ 'sak-icon__icon': true,
+ },
+ },
+ styles: {
+ icon: {
+ },
+ },
+ };
+ super(argToolset, Merge.mergeDeep(DEFAULT_ICON_CONFIG, argConfig), argPos);
+
+ // from original
+ // this.config.entity = this.config.entity ? this.config.entity : 0;
+
+ // get icon size, and calculate the foreignObject position and size. This must match the icon size
+ // 1em = FONT_SIZE pixels, so we can calculate the icon size, and x/y positions of the foreignObject
+ // the viewport is 200x200, so we can calulate the offset.
+ //
+ // NOTE:
+ // Safari doesn't use the svg viewport for rendering of the foreignObject, but the real clientsize.
+ // So positioning an icon doesn't work correctly...
+
+ this.svg.iconSize = this.config.position.icon_size ? this.config.position.icon_size : 3;
+ this.svg.iconPixels = this.svg.iconSize * FONT_SIZE;
+
+ const align = this.config.position.align ? this.config.position.align : 'center';
+ const adjust = (align === 'center' ? 0.5 : (align === 'start' ? -1 : +1));
+
+ const clientWidth = 400; // testing
+ const correction = clientWidth / this._card.viewBox.width;
+
+ this.svg.xpx = this.svg.cx;
+ this.svg.ypx = this.svg.cy;
+
+ if (((this._card.isSafari) || (this._card.iOS)) && (!this._card.isSafari16)) {
+ this.svg.iconSize *= correction;
+
+ this.svg.xpx = (this.svg.xpx * correction) - (this.svg.iconPixels * adjust * correction);
+ this.svg.ypx = (this.svg.ypx * correction) - (this.svg.iconPixels * 0.5 * correction) - (this.svg.iconPixels * 0.25 * correction);// - (iconPixels * 0.25 / 1.86);
+ } else {
+ // Get x,y in viewbox dimensions and center with half of size of icon.
+ // Adjust horizontal for aligning. Can be 1, 0.5 and -1
+ // Adjust vertical for half of height... and correct for 0.25em textfont to align.
+ this.svg.xpx -= (this.svg.iconPixels * adjust);
+ this.svg.ypx = this.svg.ypx - (this.svg.iconPixels * 0.5) - (this.svg.iconPixels * 0.25);
+ }
+ this.classes.icon = {};
+ this.styles.icon = {};
+
+ if (this.dev.debug) console.log('EntityIconTool constructor coords, dimensions, config', this.coords, this.dimensions, this.config);
+ }
+
+ /** *****************************************************************************
+ * EntityIconTool::static properties()
+ *
+ * Summary.
+ * Declares the static class properties.
+ * Needs eslint parserOptions ecmaVersion: 2022
+ *
+ * Replaces older style declarations in the constructor, such as
+ *
+ * if (!EntityIconTool.sakIconCache) {
+ * EntityIconTool.sakIconCache = {};
+ * }
+ *
+ */
+ static {
+ EntityIconTool.sakIconCache = {};
+ }
+
+ /** *****************************************************************************
+ * EntityIconTool::_buildIcon()
+ *
+ * Summary.
+ * Builds the Icon specification name.
+ *
+ */
+ _buildIcon(entityState, entityConfig, toolIcon) {
+ return (
+ this.activeAnimation?.icon // Icon from animation
+ || toolIcon // Defined by tool
+ || entityConfig?.icon // Defined by configuration
+ || entityState?.attributes?.icon // Using entity icon
+ || stateIcon(entityState) // Use card helper logic (2021.11.21)
+ );
+ }
+
+ /** *****************************************************************************
+ * EntityIconTool::_renderIcon()
+ *
+ * Summary.
+ * Renders the icon using precalculated coordinates and dimensions.
+ * Only the runtime style is calculated before rendering the icon
+ *
+ * THIS IS THE ONE!!!!
+ */
+
+ _renderIcon() {
+ this.MergeAnimationClassIfChanged();
+ this.MergeAnimationStyleIfChanged();
+ this.MergeColorFromState(this.styles.icon);
+
+ const icon = this._buildIcon(
+ this._card.entities[this.defaultEntityIndex()],
+ this.config.hasOwnProperty('entity_index') ? this._card.config.entities[this.defaultEntityIndex()] : undefined,
+ this.config.icon,
+ );
+
+ // eslint-disable-next-line no-constant-condition
+ if (true || (this.svg.xpx === 0)) {
+ this.svg.iconSize = this.config.position.icon_size ? this.config.position.icon_size : 2;
+ this.svg.iconPixels = this.svg.iconSize * FONT_SIZE;
+
+ // NEW NEW NEW Use % for size of icon...
+ this.svg.iconSize = this.config.position.icon_size ? this.config.position.icon_size : 2;
+ this.svg.iconPixels = Utils.calculateSvgDimension(this.svg.iconSize);
+
+ const align = this.config.position.align ? this.config.position.align : 'center';
+ const adjust = (align === 'center' ? 0.5 : (align === 'start' ? -1 : +1));
+
+ const clientWidth = 400;
+ const correction = clientWidth / (this._card.viewBox.width);
+
+ this.svg.xpx = this.svg.cx;// (x * this._card.viewBox.width);
+ this.svg.ypx = this.svg.cy;// (y * this._card.viewBox.height);
+
+ if (((this._card.isSafari) || (this._card.iOS)) && (!this._card.isSafari16)) {
+ // correction = 1; //
+ this.svg.iconSize *= correction;
+ this.svg.iconPixels *= correction;
+
+ this.svg.xpx = (this.svg.xpx * correction) - (this.svg.iconPixels * adjust * correction);
+ this.svg.ypx = (this.svg.ypx * correction) - (this.svg.iconPixels * 0.9 * correction);
+ // - (this.svg.iconPixels * 0.25 * correction);// - (iconPixels * 0.25 / 1.86);
+ this.svg.xpx = (this.svg.cx * correction) - (this.svg.iconPixels * adjust * correction);
+ this.svg.ypx = (this.svg.cy * correction) - (this.svg.iconPixels * adjust * correction);
+ } else {
+ // Get x,y in viewbox dimensions and center with half of size of icon.
+ // Adjust horizontal for aligning. Can be 1, 0.5 and -1
+
+ this.svg.xpx = this.svg.cx - (this.svg.iconPixels * adjust);
+ this.svg.ypx = this.svg.cy - (this.svg.iconPixels * adjust);
+
+ if (this.dev.debug) console.log('EntityIconTool::_renderIcon - svg values =', this.toolId, this.svg, this.config.cx, this.config.cy, align, adjust);
+ }
+ }
+
+ if (!this.alternateColor) { this.alternateColor = 'rgba(0,0,0,0)'; }
+
+ if (!EntityIconTool.sakIconCache[icon]) {
+ const theQuery = this._card.shadowRoot.getElementById('icon-'.concat(this.toolId))?.shadowRoot?.querySelectorAll('*');
+ if (theQuery) {
+ this.iconSvg = theQuery[0]?.path;
+ } else {
+ this.iconSvg = undefined;
+ }
+
+ if (this.iconSvg) {
+ EntityIconTool.sakIconCache[icon] = this.iconSvg;
+ // console.log('EntityIconTool, cache - Store: ', icon);
+ }
+ } else {
+ this.iconSvg = EntityIconTool.sakIconCache[icon];
+ // console.log('EntityIconTool, cache - Fetch: ', icon);
+ }
+
+ let scale;
+
+ // NTS@20201.12.24
+ // Add (true) to force rendering the Safari like solution for icons.
+ // After the above fix, it seems to work for both Chrome and Safari browsers.
+ // That is nice. Now animations also work on Chrome...
+
+ if (this.iconSvg) {
+ // Use original size, not the corrected one!
+ this.svg.iconSize = this.config.position.icon_size ? this.config.position.icon_size : 2;
+ this.svg.iconPixels = Utils.calculateSvgDimension(this.svg.iconSize);
+
+ this.svg.x1 = this.svg.cx - this.svg.iconPixels / 2;
+ this.svg.y1 = this.svg.cy - this.svg.iconPixels / 2;
+ this.svg.x1 = this.svg.cx - (this.svg.iconPixels * 0.5);
+ this.svg.y1 = this.svg.cy - (this.svg.iconPixels * 0.5);
+
+ scale = this.svg.iconPixels / 24;
+ // scale = 1;
+ // Icon is default drawn at 0,0. As there is no separate viewbox, a transform is required
+ // to position the icon on its desired location.
+ // Icon is also drawn in a default 24x24 viewbox. So scale the icon to the required size using scale()
+ return svg`
+
+
+
+
+ `;
+ } else {
+ // Note @2022.06.26
+ // overflow="hidden" is ignored by latest and greatest Safari 15.5. Wow. Nice! Good work!
+ // So use a fill/color of rgba(0,0,0,0)...
+ return svg`
+
+
+
+ this._handleAnimationEvent(e, this)}
+ @animationiteration=${(e) => this._handleAnimationEvent(e, this)}
+ style="animation: flash 0.15s 20;">
+
+
+
+ `;
+ }
+ }
+
+ _handleAnimationEvent(argEvent, argThis) {
+ argEvent.stopPropagation();
+ argEvent.preventDefault();
+
+ argThis.iconSvg = this._card.shadowRoot.getElementById('icon-'.concat(this.toolId))?.shadowRoot?.querySelectorAll('*')[0]?.path;
+ if (argThis.iconSvg) {
+ argThis._card.requestUpdate();
+ }
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ firstUpdated(changedProperties) {
+
+ }
+
+ /** *****************************************************************************
+ * EntityIconTool::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ * NTS:
+ * Adding
+ * to the this.handleTapEvent(e, this.config)} >
+
+ ${this._renderIcon()}
+
+ `;
+ }
+} // END of class
diff --git a/src/entity-name-tool.js b/src/entity-name-tool.js
new file mode 100644
index 0000000..846458b
--- /dev/null
+++ b/src/entity-name-tool.js
@@ -0,0 +1,107 @@
+import { svg } from 'lit-element';
+import { classMap } from 'lit-html/directives/class-map.js';
+import { styleMap } from 'lit-html/directives/style-map.js';
+
+import Merge from './merge';
+import BaseTool from './base-tool';
+
+/** ****************************************************************************
+ * EntityNameTool class
+ *
+ * Summary.
+ *
+ */
+
+export default class EntityNameTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_NAME_CONFIG = {
+ classes: {
+ tool: {
+ 'sak-name': true,
+ hover: true,
+ },
+ name: {
+ 'sak-name__name': true,
+ },
+ },
+ styles: {
+ tool: {
+ },
+ name: {
+ },
+ },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_NAME_CONFIG, argConfig), argPos);
+
+ this._name = {};
+ // Init classes
+ this.classes.tool = {};
+ this.classes.name = {};
+
+ // Init styles
+ this.styles.name = {};
+ if (this.dev.debug) console.log('EntityName constructor coords, dimensions', this.coords, this.dimensions, this.svg, this.config);
+ }
+
+ /** *****************************************************************************
+ * EntityNameTool::_buildName()
+ *
+ * Summary.
+ * Builds the Name string.
+ *
+ */
+
+ _buildName(entityState, entityConfig) {
+ return (
+ this.activeAnimation?.name // Name from animation
+ || entityConfig.name
+ || entityState.attributes.friendly_name
+ );
+ }
+
+ /** *****************************************************************************
+ * EntityNameTool::_renderEntityName()
+ *
+ * Summary.
+ * Renders the entity name using precalculated coordinates and dimensions.
+ * Only the runtime style is calculated before rendering the name
+ *
+ */
+
+ _renderEntityName() {
+ this.MergeAnimationClassIfChanged();
+ this.MergeColorFromState(this.styles.name);
+ this.MergeAnimationStyleIfChanged();
+
+ const name = this.textEllipsis(
+ this._buildName(
+ this._card.entities[this.defaultEntityIndex()],
+ this._card.config.entities[this.defaultEntityIndex()],
+ ),
+ this.config?.show?.ellipsis,
+ );
+
+ return svg`
+
+ ${name}
+
+ `;
+ }
+
+ /** *****************************************************************************
+ * EntityNameTool::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ */
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderEntityName()}
+
+ `;
+ }
+} // END of class
diff --git a/src/entity-state-tool.js b/src/entity-state-tool.js
new file mode 100644
index 0000000..a6433b0
--- /dev/null
+++ b/src/entity-state-tool.js
@@ -0,0 +1,169 @@
+import { svg } from 'lit-element';
+import { classMap } from 'lit-html/directives/class-map.js';
+import { styleMap } from 'lit-html/directives/style-map.js';
+
+import Merge from './merge';
+import BaseTool from './base-tool';
+
+/** ****************************************************************************
+ * EntityStateTool class
+ *
+ * Summary.
+ *
+ */
+
+export default class EntityStateTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_STATE_CONFIG = {
+ show: { uom: 'end' },
+ classes: {
+ tool: {
+ 'sak-state': true,
+ hover: true,
+ },
+ state: {
+ 'sak-state__value': true,
+ },
+ uom: {
+ 'sak-state__uom': true,
+ },
+ },
+ styles: {
+ state: {
+ },
+ uom: {
+ },
+ },
+ };
+ super(argToolset, Merge.mergeDeep(DEFAULT_STATE_CONFIG, argConfig), argPos);
+
+ this.classes.state = {};
+ this.classes.uom = {};
+
+ this.styles.state = {};
+ this.styles.uom = {};
+ if (this.dev.debug) console.log('EntityStateTool constructor coords, dimensions', this.coords, this.dimensions, this.svg, this.config);
+ }
+
+ // EntityStateTool::value
+ set value(state) {
+ super.value = state;
+ }
+
+ _renderState() {
+ this.MergeAnimationClassIfChanged();
+ this.MergeAnimationStyleIfChanged();
+ this.MergeColorFromState(this.styles.state);
+
+ // var inState = this._stateValue?.toLowerCase();
+ let inState = this._stateValue;
+
+ if ((inState) && isNaN(inState)) {
+ // const stateObj = this._card.config.entities[this.defaultEntityIndex()].entity;
+ const stateObj = this._card.entities[this.defaultEntityIndex()];
+ const domain = this._card._computeDomain(this._card.config.entities[this.defaultEntityIndex()].entity);
+
+ const localeTag = this.config.locale_tag ? this.config.locale_tag + inState.toLowerCase() : undefined;
+ const localeTag1 = stateObj.attributes?.device_class ? `component.${domain}.state.${stateObj.attributes.device_class}.${inState}` : '--';
+ const localeTag2 = `component.${domain}.state._.${inState}`;
+
+ inState = (localeTag && this._card.toLocale(localeTag, inState))
+ || (stateObj.attributes?.device_class
+ && this._card.toLocale(localeTag1, inState))
+ || this._card.toLocale(localeTag2, inState)
+ || stateObj.state;
+
+ inState = this.textEllipsis(inState, this.config?.show?.ellipsis);
+ }
+
+ return svg`
+
+ ${this.config?.text?.before ? this.config.text.before : ''}${inState}${this.config?.text?.after ? this.config.text.after : ''}
+ `;
+ }
+
+ _renderUom() {
+ if (this.config.show.uom === 'none') {
+ return svg``;
+ } else {
+ this.MergeAnimationStyleIfChanged();
+ this.MergeColorFromState(this.styles.uom);
+
+ let fsuomStr = this.styles.state['font-size'];
+
+ let fsuomValue = 0.5;
+ let fsuomType = 'em';
+ const fsuomSplit = fsuomStr.match(/\D+|\d*\.?\d+/g);
+ if (fsuomSplit.length === 2) {
+ fsuomValue = Number(fsuomSplit[0]) * 0.6;
+ fsuomType = fsuomSplit[1];
+ } else console.error('Cannot determine font-size for state/unit', fsuomStr);
+
+ fsuomStr = { 'font-size': fsuomValue + fsuomType };
+
+ this.styles.uom = Merge.mergeDeep(this.config.styles.uom, fsuomStr);
+
+ const uom = this._card._buildUom(this.derivedEntity, this._card.entities[this.defaultEntityIndex()], this._card.config.entities[this.defaultEntityIndex()]);
+
+ // Check for location of uom. end = next to state, bottom = below state ;-), etc.
+ if (this.config.show.uom === 'end') {
+ return svg`
+
+ ${uom}
+ `;
+ } else if (this.config.show.uom === 'bottom') {
+ return svg`
+
+ ${uom}
+ `;
+ } else if (this.config.show.uom === 'top') {
+ return svg`
+
+ ${uom}
+ `;
+ } else {
+ return svg``;
+ }
+ }
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ firstUpdated(changedProperties) {
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ updated(changedProperties) {
+ }
+
+ render() {
+ // eslint-disable-next-line no-constant-condition
+ if (true || (this._card._computeDomain(this._card.entities[this.defaultEntityIndex()].entity_id) === 'sensor')) {
+ return svg`
+
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderState()}
+ ${this._renderUom()}
+
+
+ `;
+ } else {
+ // Not a sensor. Might be any other domain. Unit can only be specified using the units: in the configuration.
+ // Still check for using an attribute value for the domain...
+ // return svg`
+ // this.handleTapEvent(e, this.config)}>
+ //
+ // ${state}
+ //
+ // ${uom}
+ //
+ // `;
+ }
+ } // render()
+}
diff --git a/src/horseshoe-tool.js b/src/horseshoe-tool.js
new file mode 100644
index 0000000..9c6263f
--- /dev/null
+++ b/src/horseshoe-tool.js
@@ -0,0 +1,308 @@
+import { svg } from 'lit-element';
+import { SVG_VIEW_BOX } from './const';
+import Merge from './merge';
+import Utils from './utils';
+import BaseTool from './base-tool';
+import Colors from './colors';
+
+/** ****************************************************************************
+ * HorseshoeTool class
+ *
+ * Summary.
+ *
+ */
+
+export default class HorseshoeTool extends BaseTool {
+ // Donut starts at -220 degrees and is 260 degrees in size.
+ // zero degrees is at 3 o'clock.
+
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_HORSESHOE_CONFIG = {
+ position: {
+ cx: 50,
+ cy: 50,
+ radius: 45,
+ },
+ card_filter: 'card--filter-none',
+ horseshoe_scale: {
+ min: 0,
+ max: 100,
+ width: 3,
+ color: 'var(--primary-background-color)',
+ },
+ horseshoe_state: {
+ width: 6,
+ color: 'var(--primary-color)',
+ },
+ show: {
+ horseshoe: true,
+ scale_tickmarks: false,
+ horseshoe_style: 'fixed',
+ },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_HORSESHOE_CONFIG, argConfig), argPos);
+
+ // Next consts are now variable. Should be calculated!!!!!!
+ this.HORSESHOE_RADIUS_SIZE = 0.45 * SVG_VIEW_BOX;
+ this.TICKMARKS_RADIUS_SIZE = 0.43 * SVG_VIEW_BOX;
+ this.HORSESHOE_PATH_LENGTH = 2 * 260 / 360 * Math.PI * this.HORSESHOE_RADIUS_SIZE;
+
+ // this.config = {...DEFAULT_HORSESHOE_CONFIG};
+ // this.config = {...this.config, ...argConfig};
+
+ // if (argConfig.styles) this.config.styles = {...argConfig.styles};
+ // this.config.styles = {...DEFAULT_HORSESHOE_CONFIG.styles, ...this.config.styles};
+
+ // //if (argConfig.show) this.config.show = Object.assign(...argConfig.show);
+ // this.config.show = {...DEFAULT_HORSESHOE_CONFIG.show, ...this.config.show};
+
+ // //if (argConfig.horseshoe_scale) this.config.horseshoe_scale = Object.assign(...argConfig.horseshoe_scale);
+ // this.config.horseshoe_scale = {...DEFAULT_HORSESHOE_CONFIG.horseshoe_scale, ...this.config.horseshoe_scale};
+
+ // // if (argConfig.horseshoe_state) this.config.horseshoe_state = Object.assign(...argConfig.horseshoe_state);
+ // this.config.horseshoe_state = {...DEFAULT_HORSESHOE_CONFIG.horseshoe_state, ...this.config.horseshoe_state};
+
+ this.config.entity_index = this.config.entity_index ? this.config.entity_index : 0;
+
+ this.svg.radius = Utils.calculateSvgDimension(this.config.position.radius);
+ this.svg.radius_ticks = Utils.calculateSvgDimension(0.95 * this.config.position.radius);
+
+ this.svg.horseshoe_scale = {};
+ this.svg.horseshoe_scale.width = Utils.calculateSvgDimension(this.config.horseshoe_scale.width);
+ this.svg.horseshoe_state = {};
+ this.svg.horseshoe_state.width = Utils.calculateSvgDimension(this.config.horseshoe_state.width);
+ this.svg.horseshoe_scale.dasharray = 2 * 26 / 36 * Math.PI * this.svg.radius;
+
+ // The horseshoe is rotated around its svg base point. This is NOT the center of the circle!
+ // Adjust x and y positions within the svg viewport to re-center the circle after rotating
+ this.svg.rotate = {};
+ this.svg.rotate.degrees = -220;
+ this.svg.rotate.cx = this.svg.cx;
+ this.svg.rotate.cy = this.svg.cy;
+
+ // Get colorstops and make a key/value store...
+ this.colorStops = {};
+ if (this.config.color_stops) {
+ Object.keys(this.config.color_stops).forEach((key) => {
+ this.colorStops[key] = this.config.color_stops[key];
+ });
+ }
+
+ this.sortedStops = Object.keys(this.colorStops).map((n) => Number(n)).sort((a, b) => a - b);
+
+ // Create a colorStopsMinMax list for autominmax color determination
+ this.colorStopsMinMax = {};
+ this.colorStopsMinMax[this.config.horseshoe_scale.min] = this.colorStops[this.sortedStops[0]];
+ this.colorStopsMinMax[this.config.horseshoe_scale.max] = this.colorStops[this.sortedStops[(this.sortedStops.length) - 1]];
+
+ // Now set the color0 and color1 for the gradient used in the horseshoe to the colors
+ // Use default for now!!
+ this.color0 = this.colorStops[this.sortedStops[0]];
+ this.color1 = this.colorStops[this.sortedStops[(this.sortedStops.length) - 1]];
+
+ this.angleCoords = {
+ x1: '0%', y1: '0%', x2: '100%', y2: '0%',
+ };
+ // this.angleCoords = angleCoords;
+ this.color1_offset = '0%';
+
+ //= ===================
+ // End setConfig part.
+
+ if (this.dev.debug) console.log('HorseshoeTool constructor coords, dimensions', this.coords, this.dimensions, this.svg, this.config);
+ }
+
+ /** *****************************************************************************
+ * HorseshoeTool::value()
+ *
+ * Summary.
+ * Sets the value of the horseshoe. Value updated via set hass.
+ * Calculate horseshoe settings & colors depening on config and new value.
+ *
+ */
+
+ set value(state) {
+ if (this._stateValue === state) return;
+
+ this._stateValuePrev = this._stateValue || state;
+ this._stateValue = state;
+ this._stateValueIsDirty = true;
+
+ // Calculate the size of the arc to fill the dasharray with this
+ // value. It will fill the horseshoe relative to the state and min/max
+ // values given in the configuration.
+
+ const min = this.config.horseshoe_scale.min || 0;
+ const max = this.config.horseshoe_scale.max || 100;
+ const val = Math.min(Utils.calculateValueBetween(min, max, state), 1);
+ const score = val * this.HORSESHOE_PATH_LENGTH;
+ const total = 10 * this.HORSESHOE_RADIUS_SIZE;
+ this.dashArray = `${score} ${total}`;
+
+ // We must draw the horseshoe. Depending on the stroke settings, we draw a fixed color, gradient, autominmax or colorstop
+ // #TODO: only if state or attribute has changed.
+
+ const strokeStyle = this.config.show.horseshoe_style;
+
+ if (strokeStyle === 'fixed') {
+ this.stroke_color = this.config.horseshoe_state.color;
+ this.color0 = this.config.horseshoe_state.color;
+ this.color1 = this.config.horseshoe_state.color;
+ this.color1_offset = '0%';
+ // We could set the circle attributes, but we do it with a variable as we are using a gradient
+ // to display the horseshoe circle .. .setAttribute('stroke', stroke);
+ } else if (strokeStyle === 'autominmax') {
+ // Use color0 and color1 for autoranging the color of the horseshoe
+ const stroke = Colors.calculateColor(state, this.colorStopsMinMax, true);
+
+ // We now use a gradient for the horseshoe, using two colors
+ // Set these colors to the colorstop color...
+ this.color0 = stroke;
+ this.color1 = stroke;
+ this.color1_offset = '0%';
+ } else if (strokeStyle === 'colorstop' || strokeStyle === 'colorstopgradient') {
+ const stroke = Colors.calculateColor(state, this.colorStops, strokeStyle === 'colorstopgradient');
+
+ // We now use a gradient for the horseshoe, using two colors
+ // Set these colors to the colorstop color...
+ this.color0 = stroke;
+ this.color1 = stroke;
+ this.color1_offset = '0%';
+ } else if (strokeStyle === 'lineargradient') {
+ // This has taken a lot of time to get a satisfying result, and it appeared much simpler than anticipated.
+ // I don't understand it, but for a circle, a gradient from left/right with adjusted stop is enough ?!?!?!
+ // No calculations to adjust the angle of the gradient, or rotating the gradient itself.
+ // Weird, but it works. Not a 100% match, but it is good enough for now...
+
+ // According to stackoverflow, these calculations / adjustments would be needed, but it isn't ;-)
+ // Added from https://stackoverflow.com/questions/9025678/how-to-get-a-rotated-linear-gradient-svg-for-use-as-a-background-image
+ const angleCoords = {
+ x1: '0%', y1: '0%', x2: '100%', y2: '0%',
+ };
+ this.color1_offset = `${Math.round((1 - val) * 100)}%`;
+
+ this.angleCoords = angleCoords;
+ }
+ if (this.dev.debug) console.log('HorseshoeTool set value', this.cardId, state);
+
+ // return true;
+ }
+
+ /** *****************************************************************************
+ * HorseshoeTool::_renderTickMarks()
+ *
+ * Summary.
+ * Renders the tick marks on the scale.
+ *
+ */
+
+ _renderTickMarks() {
+ const { config } = this;
+ // if (!config) return;
+ // if (!config.show) return;
+ if (!config.show.scale_tickmarks) return;
+
+ const stroke = config.horseshoe_scale.color ? config.horseshoe_scale.color : 'var(--primary-background-color)';
+ const tickSize = config.horseshoe_scale.ticksize ? config.horseshoe_scale.ticksize
+ : (config.horseshoe_scale.max - config.horseshoe_scale.min) / 10;
+
+ // fullScale is 260 degrees. Hard coded for now...
+ const fullScale = 260;
+ const remainder = config.horseshoe_scale.min % tickSize;
+ const startTickValue = config.horseshoe_scale.min + (remainder === 0 ? 0 : (tickSize - remainder));
+ const startAngle = ((startTickValue - config.horseshoe_scale.min)
+ / (config.horseshoe_scale.max - config.horseshoe_scale.min)) * fullScale;
+ const tickSteps = ((config.horseshoe_scale.max - startTickValue) / tickSize);
+
+ // new
+ let steps = Math.floor(tickSteps);
+ const angleStepSize = (fullScale - startAngle) / tickSteps;
+
+ // If steps exactly match the max. value/range, add extra step for that max value.
+ if ((Math.floor(((steps) * tickSize) + startTickValue)) <= (config.horseshoe_scale.max)) { steps += 1; }
+
+ const radius = this.svg.horseshoe_scale.width ? this.svg.horseshoe_scale.width / 2 : 6 / 2;
+ let angle;
+ const scaleItems = [];
+
+ // NTS:
+ // Value of -230 is weird. Should be -220. Can't find why...
+ let i;
+ for (i = 0; i < steps; i++) {
+ angle = startAngle + ((-230 + (360 - i * angleStepSize)) * Math.PI / 180);
+ scaleItems[i] = svg`
+
+ `;
+ }
+ return svg`${scaleItems}`;
+ }
+
+ /** *****************************************************************************
+ * HorseshoeTool::_renderHorseShoe()
+ *
+ * Summary.
+ * Renders the horseshoe group.
+ *
+ * Description.
+ * The horseshoes are rendered in a viewbox of 200x200 (SVG_VIEW_BOX).
+ * Both are centered with a radius of 45%, ie 200*0.45 = 90.
+ *
+ * The foreground horseshoe is always rendered as a gradient with two colors.
+ *
+ * The horseshoes are rotated 220 degrees and are 2 * 26/36 * Math.PI * r in size
+ * There you get your value of 408.4070449,180 ;-)
+ */
+
+ _renderHorseShoe() {
+ if (!this.config.show.horseshoe) return;
+
+ return svg`
+
+
+
+
+
+ ${this._renderTickMarks()}
+
+ `;
+ }
+
+ /** *****************************************************************************
+ * HorseshoeTool::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ */
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderHorseShoe()}
+
+
+
+
+
+
+
+
+
+ `;
+ }
+} // END of class
diff --git a/src/line-tool.js b/src/line-tool.js
new file mode 100644
index 0000000..10ad70b
--- /dev/null
+++ b/src/line-tool.js
@@ -0,0 +1,117 @@
+import { svg } from 'lit-element';
+import { classMap } from 'lit-html/directives/class-map.js';
+import { styleMap } from 'lit-html/directives/style-map.js';
+
+import Merge from './merge';
+import Utils from './utils';
+import BaseTool from './base-tool';
+
+/** ****************************************************************************
+ * LineTool class
+ *
+ * Summary.
+ *
+ */
+
+export default class LineTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_LINE_CONFIG = {
+ position: {
+ orientation: 'vertical',
+ length: '10',
+ cx: '50',
+ cy: '50',
+ },
+ classes: {
+ tool: {
+ 'sak-line': true,
+ hover: true,
+ },
+ line: {
+ 'sak-line__line': true,
+ },
+ },
+ styles: {
+ tool: {
+ },
+ line: {
+ },
+ },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_LINE_CONFIG, argConfig), argPos);
+
+ if (!['horizontal', 'vertical', 'fromto'].includes(this.config.position.orientation))
+ throw Error('LineTool::constructor - invalid orientation [vertical, horizontal, fromto] = ', this.config.position.orientation);
+
+ if (['horizontal', 'vertical'].includes(this.config.position.orientation))
+ this.svg.length = Utils.calculateSvgDimension(argConfig.position.length);
+
+ if (this.config.position.orientation === 'fromto') {
+ this.svg.x1 = Utils.calculateSvgCoordinate(argConfig.position.x1, this.toolsetPos.cx);
+ this.svg.y1 = Utils.calculateSvgCoordinate(argConfig.position.y1, this.toolsetPos.cy);
+ this.svg.x2 = Utils.calculateSvgCoordinate(argConfig.position.x2, this.toolsetPos.cx);
+ this.svg.y2 = Utils.calculateSvgCoordinate(argConfig.position.y2, this.toolsetPos.cy);
+ } else if (this.config.position.orientation === 'vertical') {
+ this.svg.x1 = this.svg.cx;
+ this.svg.y1 = this.svg.cy - this.svg.length / 2;
+ this.svg.x2 = this.svg.cx;
+ this.svg.y2 = this.svg.cy + this.svg.length / 2;
+ } else if (this.config.position.orientation === 'horizontal') {
+ this.svg.x1 = this.svg.cx - this.svg.length / 2;
+ this.svg.y1 = this.svg.cy;
+ this.svg.x2 = this.svg.cx + this.svg.length / 2;
+ this.svg.y2 = this.svg.cy;
+ }
+
+ this.classes.line = {};
+ this.styles.line = {};
+
+ if (this.dev.debug) console.log('LineTool constructor coords, dimensions', this.coords, this.dimensions, this.svg, this.config);
+ }
+
+ /** *****************************************************************************
+ * LineTool::_renderLine()
+ *
+ * Summary.
+ * Renders the line using precalculated coordinates and dimensions.
+ * Only the runtime style is calculated before rendering the line
+ *
+ * @returns {svg} Rendered line
+ *
+ */
+
+ _renderLine() {
+ this.MergeAnimationClassIfChanged();
+ this.MergeAnimationStyleIfChanged();
+ this.MergeColorFromState(this.styles.line);
+
+ if (this.dev.debug) console.log('_renderLine', this.config.position.orientation, this.svg.x1, this.svg.y1, this.svg.x2, this.svg.y2);
+ return svg`
+
+ `;
+ }
+
+ /** *****************************************************************************
+ * LineTool::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ * @returns {svg} Rendered line group
+ *
+ */
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderLine()}
+
+ `;
+ }
+} // END of class
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 0000000..49c1474
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,1721 @@
+/*
+*
+* Card : swiss-army-knife-card.js
+* Project : Home Assistant
+* Repository: https://github.com/AmoebeLabs/swiss-army-knife-card
+*
+* Author : Mars @ AmoebeLabs.com
+*
+* License : MIT
+*
+* -----
+* Description:
+* The swiss army knife card, a versatile multi-tool custom card for
+# the one and only Home Assistant.
+*
+* Documentation Refs:
+* - https://swiss-army-knife-card-manual.amoebelabs.com/
+* - https://material3-themes-manual.amoebelabs.com/
+*
+*******************************************************************************
+*/
+
+// NTS @2021.10.31
+// Check compatibility when upgrading lit stuff. Many versions have conflicts!
+// Use compatible lit-* stuff, ie lit-element@2 and lit-html@1.
+// Combining other versions may lead to incompatibility, and thus lots of errors and
+// tools not working anymore!
+
+import {
+ LitElement, html, css, svg, unsafeCSS,
+} from 'lit-element';
+
+import { styleMap } from 'lit-html/directives/style-map.js';
+import { unsafeSVG } from 'lit-html/directives/unsafe-svg.js';
+import { ifDefined } from 'lit-html/directives/if-defined.js';
+import { selectUnit } from '@formatjs/intl-utils';
+import { version } from '../package.json';
+
+import {
+ SVG_DEFAULT_DIMENSIONS,
+ SVG_VIEW_BOX,
+ FONT_SIZE,
+} from './const';
+
+import Merge from './merge';
+import Utils from './utils';
+import Templates from './templates';
+import Toolset from './toolset';
+import Colors from './colors';
+
+// Original injector is buggy. Use a patched version, and store this local...
+// import * as SvgInjector from '../dist/SVGInjector.min.js'; // lgtm[js/unused-local-variable]
+
+console.info(
+ `%c SWISS-ARMY-KNIFE-CARD \n%c Version ${version} `,
+ 'color: yellow; font-weight: bold; background: black',
+ 'color: white; font-weight: bold; background: dimgray',
+);
+
+// https://github.com/d3/d3-selection/blob/master/src/selection/data.js
+//
+
+/**
+ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ */
+
+class SwissArmyKnifeCard extends LitElement {
+ // card::constructor
+ constructor() {
+ super();
+
+ this.connected = false;
+
+ Colors.setElement(this);
+
+ // Get cardId for unique SVG gradient Id
+ this.cardId = Math.random().toString(36).substr(2, 9);
+ this.entities = [];
+ this.entitiesStr = [];
+ this.attributesStr = [];
+ this.secondaryInfoStr = [];
+ this.viewBoxSize = SVG_VIEW_BOX;
+ this.viewBox = { width: SVG_VIEW_BOX, height: SVG_VIEW_BOX };
+
+ // Create the lists for the toolsets and the tools
+ // - toolsets contain a list of toolsets with tools
+ // - tools contain the full list of tools!
+ this.toolsets = [];
+ this.tools = [];
+
+ // 2022.01.24
+ // Add card styles functionality
+ this.styles = {};
+ this.styles.card = {};
+
+ // For history query interval updates.
+ this.entityHistory = {};
+ this.entityHistory.needed = false;
+ this.stateChanged = true;
+ this.entityHistory.updating = false;
+ this.entityHistory.update_interval = 300;
+ // console.log("SAK Constructor,", this.entityHistory);
+
+ // Development settings
+ this.dev = {};
+ this.dev.debug = false;
+ this.dev.performance = false;
+ this.dev.m3 = false;
+
+ this.configIsSet = false;
+
+ // Theme mode support
+ this.theme = {};
+ this.theme.modeChanged = false;
+ this.theme.darkMode = false;
+
+ // Safari is the new IE.
+ // Check for iOS / iPadOS / Safari to be able to work around some 'features'
+ // Some bugs are already 9 years old, and not fixed yet by Apple!
+ //
+ // However: there is a new SVG engine on its way that might be released in 2023.
+ // That should fix a lot of problems, adhere to standards, allow for hardware
+ // acceleration and mixing HTML - through the foreignObject - with SVG!
+ //
+ // The first small fixes are in 16.2-16.4, which is why I have to check for
+ // Safari 16, as that version can use the same renderpath as Chrome and Firefox!! WOW!!
+ //
+ // Detection from: http://jsfiddle.net/jlubean/dL5cLjxt/
+ //
+ // this.isSafari = !!navigator.userAgent.match(/Version\/[\d\.]+.*Safari/);
+ // this.iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
+
+ // See: https://javascriptio.com/view/10924/detect-if-device-is-ios
+ // After iOS 13 you should detect iOS devices like this, since iPad will not be detected as iOS devices
+ // by old ways (due to new "desktop" options, enabled by default)
+
+ // eslint-disable-next-line no-useless-escape
+ this.isSafari = !!window.navigator.userAgent.match(/Version\/[\d\.]+.*Safari/);
+ this.iOS = (/iPad|iPhone|iPod/.test(window.navigator.userAgent)
+ || (window.navigator.platform === 'MacIntel' && window.navigator.maxTouchPoints > 1))
+ && !window.MSStream;
+ this.isSafari14 = this.isSafari && /Version\/14\.[0-9]/.test(window.navigator.userAgent);
+ this.isSafari15 = this.isSafari && /Version\/15\.[0-9]/.test(window.navigator.userAgent);
+ this.isSafari16 = this.isSafari && /Version\/16\.[0-9]/.test(window.navigator.userAgent);
+ this.isSafari16 = this.isSafari && /Version\/16\.[0-9]/.test(window.navigator.userAgent);
+
+ // The iOS app does not use a standard agent string...
+ // See: https://github.com/home-assistant/iOS/blob/master/Sources/Shared/API/HAAPI.swift
+ // It contains strings like "like Safari" and "OS 14_2", and "iOS 14.2.0"
+
+ this.isSafari14 = this.isSafari14 || /os 15.*like safari/.test(window.navigator.userAgent.toLowerCase());
+ this.isSafari15 = this.isSafari15 || /os 14.*like safari/.test(window.navigator.userAgent.toLowerCase());
+ this.isSafari16 = this.isSafari16 || /os 16.*like safari/.test(window.navigator.userAgent.toLowerCase());
+
+ this.lovelace = SwissArmyKnifeCard.lovelace;
+
+ if (!this.lovelace) {
+ console.error("card::constructor - Can't get Lovelace panel");
+ throw Error("card::constructor - Can't get Lovelace panel");
+ }
+
+ if (!SwissArmyKnifeCard.colorCache) {
+ SwissArmyKnifeCard.colorCache = [];
+ }
+
+ if (this.dev.debug) console.log('*****Event - card - constructor', this.cardId, new Date().getTime());
+ }
+
+ static getSystemStyles() {
+ return css`
+ :host {
+ cursor: default;
+ font-size: ${FONT_SIZE}px;
+ }
+
+ /* Default settings for the card */
+ /* - default cursor */
+ /* - SVG overflow is not displayed, ie cutoff by the card edges */
+ ha-card {
+ cursor: default;
+ overflow: hidden;
+
+ -webkit-touch-callout: none;
+ }
+
+ /* For disabled parts of tools/toolsets */
+ /* - No input */
+ ha-card.disabled {
+ pointer-events: none;
+ cursor: default;
+ }
+
+ .disabled {
+ pointer-events: none !important;
+ cursor: default !important;
+ }
+
+ /* For 'active' tools/toolsets */
+ /* - Show cursor as pointer */
+ .hover {
+ cursor: pointer;
+ }
+
+ /* For hidden tools/toolsets where state for instance is undefined */
+ .hidden {
+ opacity: 0;
+ visibility: hidden;
+ transition: visibility 0s 1s, opacity 0.5s linear;
+ }
+
+ focus {
+ outline: none;
+ }
+ focus-visible {
+ outline: 3px solid blanchedalmond; /* That'll show 'em */
+ }
+
+
+ @media (print), (prefers-reduced-motion: reduce) {
+ .animated {
+ animation-duration: 1ms !important;
+ transition-duration: 1ms !important;
+ animation-iteration-count: 1 !important;
+ }
+ }
+
+
+ /* Set default host font-size to 10 pixels.
+ * In that case 1em = 10 pixels = 1% of 100x100 matrix used
+ */
+ @media screen and (min-width: 467px) {
+ :host {
+ font-size: ${FONT_SIZE}px;
+ }
+ }
+ @media screen and (max-width: 466px) {
+ :host {
+ font-size: ${FONT_SIZE}px;
+ }
+ }
+
+ :host ha-card {
+ padding: 0px 0px 0px 0px;
+ }
+
+ .container {
+ position: relative;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .labelContainer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 65%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-end;
+ }
+
+ .ellipsis {
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ }
+
+ .state {
+ position: relative;
+ display: flex;
+ flex-wrap: wrap;
+ max-width: 100%;
+ min-width: 0px;
+ }
+
+ #label {
+ display: flex;
+ line-height: 1;
+ }
+
+ #label.bold {
+ font-weight: bold;
+ }
+
+ #label, #name {
+ margin: 3% 0;
+ }
+
+ .shadow {
+ font-size: 30px;
+ font-weight: 700;
+ text-anchor: middle;
+ }
+
+ .card--dropshadow-5 {
+ filter: drop-shadow(0 1px 0 #ccc)
+ drop-shadow(0 2px 0 #c9c9c9)
+ drop-shadow(0 3px 0 #bbb)
+ drop-shadow(0 4px 0 #b9b9b9)
+ drop-shadow(0 5px 0 #aaa)
+ drop-shadow(0 6px 1px rgba(0,0,0,.1))
+ drop-shadow(0 0 5px rgba(0,0,0,.1))
+ drop-shadow(0 1px 3px rgba(0,0,0,.3))
+ drop-shadow(0 3px 5px rgba(0,0,0,.2))
+ drop-shadow(0 5px 10px rgba(0,0,0,.25))
+ drop-shadow(0 10px 10px rgba(0,0,0,.2))
+ drop-shadow(0 20px 20px rgba(0,0,0,.15));
+ }
+ .card--dropshadow-medium--opaque--sepia90 {
+ filter: drop-shadow(0.0em 0.05em 0px #b2a98f22)
+ drop-shadow(0.0em 0.07em 0px #b2a98f55)
+ drop-shadow(0.0em 0.10em 0px #b2a98f88)
+ drop-shadow(0px 0.6em 0.9em rgba(0,0,0,0.15))
+ drop-shadow(0px 1.2em 0.15em rgba(0,0,0,0.1))
+ drop-shadow(0px 2.4em 2.5em rgba(0,0,0,0.1))
+ sepia(90%);
+ }
+
+ .card--dropshadow-heavy--sepia90 {
+ filter: drop-shadow(0.0em 0.05em 0px #b2a98f22)
+ drop-shadow(0.0em 0.07em 0px #b2a98f55)
+ drop-shadow(0.0em 0.10em 0px #b2a98f88)
+ drop-shadow(0px 0.3em 0.45em rgba(0,0,0,0.5))
+ drop-shadow(0px 0.6em 0.07em rgba(0,0,0,0.3))
+ drop-shadow(0px 1.2em 1.25em rgba(0,0,0,1))
+ drop-shadow(0px 1.8em 1.6em rgba(0,0,0,0.1))
+ drop-shadow(0px 2.4em 2.0em rgba(0,0,0,0.1))
+ drop-shadow(0px 3.0em 2.5em rgba(0,0,0,0.1))
+ sepia(90%);
+ }
+
+ .card--dropshadow-heavy {
+ filter: drop-shadow(0.0em 0.05em 0px #b2a98f22)
+ drop-shadow(0.0em 0.07em 0px #b2a98f55)
+ drop-shadow(0.0em 0.10em 0px #b2a98f88)
+ drop-shadow(0px 0.3em 0.45em rgba(0,0,0,0.5))
+ drop-shadow(0px 0.6em 0.07em rgba(0,0,0,0.3))
+ drop-shadow(0px 1.2em 1.25em rgba(0,0,0,1))
+ drop-shadow(0px 1.8em 1.6em rgba(0,0,0,0.1))
+ drop-shadow(0px 2.4em 2.0em rgba(0,0,0,0.1))
+ drop-shadow(0px 3.0em 2.5em rgba(0,0,0,0.1));
+ }
+
+ .card--dropshadow-medium--sepia90 {
+ filter: drop-shadow(0.0em 0.05em 0px #b2a98f)
+ drop-shadow(0.0em 0.15em 0px #b2a98f)
+ drop-shadow(0.0em 0.15em 0px #b2a98f)
+ drop-shadow(0px 0.6em 0.9em rgba(0,0,0,0.15))
+ drop-shadow(0px 1.2em 0.15em rgba(0,0,0,0.1))
+ drop-shadow(0px 2.4em 2.5em rgba(0,0,0,0.1))
+ sepia(90%);
+ }
+
+ .card--dropshadow-medium {
+ filter: drop-shadow(0.0em 0.05em 0px #b2a98f)
+ drop-shadow(0.0em 0.15em 0px #b2a98f)
+ drop-shadow(0.0em 0.15em 0px #b2a98f)
+ drop-shadow(0px 0.6em 0.9em rgba(0,0,0,0.15))
+ drop-shadow(0px 1.2em 0.15em rgba(0,0,0,0.1))
+ drop-shadow(0px 2.4em 2.5em rgba(0,0,0,0.1));
+ }
+
+ .card--dropshadow-light--sepia90 {
+ filter: drop-shadow(0px 0.10em 0px #b2a98f)
+ drop-shadow(0.1em 0.5em 0.2em rgba(0, 0, 0, .5))
+ sepia(90%);
+ }
+
+ .card--dropshadow-light {
+ filter: drop-shadow(0px 0.10em 0px #b2a98f)
+ drop-shadow(0.1em 0.5em 0.2em rgba(0, 0, 0, .5));
+ }
+
+ .card--dropshadow-down-and-distant {
+ filter: drop-shadow(0px 0.05em 0px #b2a98f)
+ drop-shadow(0px 14px 10px rgba(0,0,0,0.15))
+ drop-shadow(0px 24px 2px rgba(0,0,0,0.1))
+ drop-shadow(0px 34px 30px rgba(0,0,0,0.1));
+ }
+
+ .card--filter-none {
+ }
+
+ .horseshoe__svg__group {
+ transform: translateY(15%);
+ }
+
+ `;
+ }
+
+ /** *****************************************************************************
+ * card::getUserStyles()
+ *
+ * Summary.
+ * Returns the user defined CSS styles for the card in sak_user_templates config
+ * section in lovelace configuration.
+ *
+ */
+
+ static getUserStyles() {
+ this.userContent = '';
+
+ if ((SwissArmyKnifeCard.lovelace.config.sak_user_templates)
+ && (SwissArmyKnifeCard.lovelace.config.sak_user_templates.definitions.user_css_definitions)) {
+ this.userContent = SwissArmyKnifeCard.lovelace.config.sak_user_templates.definitions.user_css_definitions.reduce((accumulator, currentValue) => accumulator + currentValue.content, '');
+ }
+
+ return css`${unsafeCSS(this.userContent)}`;
+ }
+
+ static getSakStyles() {
+ this.sakContent = '';
+
+ if ((SwissArmyKnifeCard.lovelace.config.sak_sys_templates)
+ && (SwissArmyKnifeCard.lovelace.config.sak_sys_templates.definitions.sak_css_definitions)) {
+ this.sakContent = SwissArmyKnifeCard.lovelace.config.sak_sys_templates.definitions.sak_css_definitions.reduce((accumulator, currentValue) => accumulator + currentValue.content, '');
+ }
+
+ return css`${unsafeCSS(this.sakContent)}`;
+ }
+
+ static getSakSvgDefinitions() {
+ SwissArmyKnifeCard.lovelace.sakSvgContent = null;
+ let sakSvgContent = '';
+
+ if ((SwissArmyKnifeCard.lovelace.config.sak_sys_templates)
+ && (SwissArmyKnifeCard.lovelace.config.sak_sys_templates.definitions.sak_svg_definitions)) {
+ sakSvgContent = SwissArmyKnifeCard.lovelace.config.sak_sys_templates.definitions.sak_svg_definitions.reduce((accumulator, currentValue) => accumulator + currentValue.content, '');
+ }
+ // Cache result for later use in other cards
+ SwissArmyKnifeCard.sakSvgContent = unsafeSVG(sakSvgContent);
+ }
+
+ static getUserSvgDefinitions() {
+ SwissArmyKnifeCard.lovelace.userSvgContent = null;
+ let userSvgContent = '';
+
+ if ((SwissArmyKnifeCard.lovelace.config.sak_user_templates)
+ && (SwissArmyKnifeCard.lovelace.config.sak_user_templates.definitions.user_svg_definitions)) {
+ userSvgContent = SwissArmyKnifeCard.lovelace.config.sak_user_templates.definitions.user_svg_definitions.reduce((accumulator, currentValue) => accumulator + currentValue.content, '');
+ }
+ // Cache result for later use other cards
+ SwissArmyKnifeCard.userSvgContent = unsafeSVG(userSvgContent);
+ }
+
+ /** *****************************************************************************
+ * card::get styles()
+ *
+ * Summary.
+ * Returns the static CSS styles for the lit-element
+ *
+ * Note:
+ * - The BEM (http://getbem.com/naming/) naming style for CSS is used
+ * Of course, if no mistakes are made ;-)
+ *
+ * Note2:
+ * - get styles is a static function and is called ONCE at initialization.
+ * So, we need to get lovelace here...
+ */
+ static get styles() {
+ // console.log('SAK - get styles');
+ if (!SwissArmyKnifeCard.lovelace) SwissArmyKnifeCard.lovelace = Utils.getLovelace();
+
+ if (!SwissArmyKnifeCard.lovelace) {
+ console.error("SAK - Can't get reference to Lovelace");
+ throw Error("card::get styles - Can't get Lovelace panel");
+ }
+ if (!SwissArmyKnifeCard.lovelace.config.sak_sys_templates) {
+ console.error('SAK - System Templates reference NOT defined.');
+ throw Error('card::get styles - System Templates reference NOT defined!');
+ }
+ if (!SwissArmyKnifeCard.lovelace.config.sak_user_templates) {
+ console.warning('SAK - User Templates reference NOT defined. Did you NOT include them?');
+ }
+
+ // #TESTING
+ // Testing dark/light mode support for future functionality
+ // const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+ // console.log('get styles', darkModeMediaQuery);
+ // darkModeMediaQuery.addListener((e) => {
+ // const darkModeOn = e.matches;
+ // console.log(`Dark mode is ${darkModeOn ? '🌒 on' : '☀️ off'}.`);
+ // });
+ // console.log('get styles 2', darkModeMediaQuery);
+
+ // Get - only ONCE - the external SVG definitions for both SAK and UserSvgTool
+ // These definitions are cached into the static class of the card
+ //
+ // Note: If you change a view, and do a refresh (F5) everything is loaded.
+ // But after that: HA asks you to refresh the page --> BAM, all Lovelace
+ // cached data is gone. So we need a check/reload in a card...
+
+ SwissArmyKnifeCard.getSakSvgDefinitions();
+ SwissArmyKnifeCard.getUserSvgDefinitions();
+
+ return css`
+ ${SwissArmyKnifeCard.getSystemStyles()}
+ ${SwissArmyKnifeCard.getSakStyles()}
+ ${SwissArmyKnifeCard.getUserStyles()}
+ `;
+ }
+
+ /** *****************************************************************************
+ * card::set hass()
+ *
+ * Summary.
+ * Updates hass data for the card
+ *
+ */
+
+ set hass(hass) {
+ if (!this.counter) this.counter = 0;
+ this.counter += 1;
+
+ // Check for theme mode and theme mode change...
+ if (hass.themes.darkMode !== this.theme.darkMode) {
+ this.theme.darkMode = hass.themes.darkMode;
+ this.theme.modeChanged = true;
+ }
+
+ // Set ref to hass, use "_"for the name ;-)
+ if (this.dev.debug) console.log('*****Event - card::set hass', this.cardId, new Date().getTime());
+ this._hass = hass;
+
+ if (!this.connected) {
+ if (this.dev.debug) console.log('set hass but NOT connected', this.cardId);
+
+ // 2020.02.10 Troubles with connectcallback late, so windows are not yet calculated. ie
+ // things around icons go wrong...
+ // what if return is here..
+ // return;
+ } else {
+ // #WIP
+ // this.requestUpdate();
+ }
+
+ if (!this.config.entities) {
+ return;
+ }
+
+ let entityHasChanged = false;
+
+ // Update state strings and check for changes.
+ // Only if changed, continue and force render
+ let value;
+ let index = 0;
+
+ let secInfoSet = false;
+ let newSecInfoState;
+ let newSecInfoStateStr;
+
+ let attrSet = false;
+ let newStateStr;
+ // eslint-disable-next-line no-restricted-syntax, no-unused-vars
+ for (value of this.config.entities) {
+ this.entities[index] = hass.states[this.config.entities[index].entity];
+
+ if (this.entities[index] === undefined) {
+ console.error('SAK - set hass, entity undefined: ', this.config.entities[index].entity);
+ // Temp disable throw Error(`Set hass, entity undefined: ${this.config.entities[index].entity}`);
+ }
+
+ // Get secondary info state if specified and available
+ if (this.config.entities[index].secondary_info) {
+ secInfoSet = true;
+ newSecInfoState = this.entities[index][this.config.entities[index].secondary_info];
+ newSecInfoStateStr = this._buildSecondaryInfo(newSecInfoState, this.config.entities[index]);
+
+ if (newSecInfoStateStr !== this.secondaryInfoStr[index]) {
+ this.secondaryInfoStr[index] = newSecInfoStateStr;
+ entityHasChanged = true;
+ }
+ }
+
+ // Get attribute state if specified and available
+ if (this.config.entities[index].attribute) {
+ // #WIP:
+ // Check for indexed or mapped attributes, like weather forecast (array of 5 days with a map containing attributes)....
+ //
+ // states['weather.home'].attributes['forecast'][0].detailed_description
+ // attribute: forecast[0].condition
+ //
+
+ let { attribute } = this.config.entities[index];
+ let attrMore = '';
+ let attributeState = '';
+
+ const arrayPos = this.config.entities[index].attribute.indexOf('[');
+ const dotPos = this.config.entities[index].attribute.indexOf('.');
+ let arrayIdx = 0;
+ let arrayMap = '';
+
+ if (arrayPos !== -1) {
+ // We have an array. Split...
+ attribute = this.config.entities[index].attribute.substr(0, arrayPos);
+ attrMore = this.config.entities[index].attribute.substr(arrayPos, this.config.entities[index].attribute.length - arrayPos);
+
+ // Just hack, assume single digit index...
+ arrayIdx = attrMore[1];
+ arrayMap = attrMore.substr(4, attrMore.length - 4);
+
+ // Fetch state
+ attributeState = this.entities[index].attributes[attribute][arrayIdx][arrayMap];
+ // console.log('set hass, attributes with array/map', this.config.entities[index].attribute, attribute, attrMore, arrayIdx, arrayMap, attributeState);
+ } else if (dotPos !== -1) {
+ // We have a map. Split...
+ attribute = this.config.entities[index].attribute.substr(0, dotPos);
+ attrMore = this.config.entities[index].attribute.substr(arrayPos, this.config.entities[index].attribute.length - arrayPos);
+ arrayMap = attrMore.substr(1, attrMore.length - 1);
+
+ // Fetch state
+ attributeState = this.entities[index].attributes[attribute][arrayMap];
+
+ console.log('set hass, attributes with map', this.config.entities[index].attribute, attribute, attrMore);
+ } else {
+ // default attribute handling...
+ attributeState = this.entities[index].attributes[attribute];
+ }
+
+ // eslint-disable-next-line no-constant-condition
+ if (true) { // (typeof attributeState != 'undefined') {
+ newStateStr = this._buildState(attributeState, this.config.entities[index]);
+ if (newStateStr !== this.attributesStr[index]) {
+ this.attributesStr[index] = newStateStr;
+ entityHasChanged = true;
+ }
+ attrSet = true;
+ }
+ // 2021.10.30
+ // Due to change in light percentage, check for undefined.
+ // If bulb is off, NO percentage is given anymore, so is probably 'undefined'.
+ // Any tool should still react to a percentage going from a valid value to undefined!
+ }
+ if ((!attrSet) && (!secInfoSet)) {
+ newStateStr = this._buildState(this.entities[index].state, this.config.entities[index]);
+ if (newStateStr !== this.entitiesStr[index]) {
+ this.entitiesStr[index] = newStateStr;
+ entityHasChanged = true;
+ }
+ if (this.dev.debug) console.log('set hass - attrSet=false', this.cardId, `${new Date().getSeconds().toString()}.${new Date().getMilliseconds().toString()}`, newStateStr);
+ }
+
+ index += 1;
+ attrSet = false;
+ secInfoSet = false;
+ }
+
+ if ((!entityHasChanged) && (!this.theme.modeChanged)) {
+ // console.timeEnd("--> " + this.cardId + " PERFORMANCE card::hass");
+
+ return;
+ }
+
+ // Either one of the entities has changed, or the theme mode. So update all toolsets with new data.
+ if (this.toolsets) {
+ this.toolsets.map((item) => {
+ item.updateValues();
+ return true;
+ });
+ }
+
+ // Always request update to render the card if any of the states, attributes or theme mode have changed...
+
+ this.requestUpdate();
+
+ // An update has been requested to recalculate / redraw the tools, so reset theme mode changed
+ this.theme.modeChanged = false;
+
+ this.counter -= 1;
+
+ // console.timeEnd("--> " + this.cardId + " PERFORMANCE card::hass");
+ }
+
+ /** *****************************************************************************
+ * card::setConfig()
+ *
+ * Summary.
+ * Sets/Updates the card configuration. Rarely called if the doc is right
+ *
+ */
+
+ setConfig(config) {
+ if (this.dev.performance) console.time(`--> ${this.cardId} PERFORMANCE card::setConfig`);
+
+ if (this.dev.debug) console.log('*****Event - setConfig', this.cardId, new Date().getTime());
+ config = JSON.parse(JSON.stringify(config));
+
+ if (config.dev) this.dev = { ...this.dev, ...config.dev };
+
+ if (this.dev.debug) console.log('setConfig', this.cardId);
+
+ if (!config.layout) {
+ throw Error('card::setConfig - No layout defined');
+ }
+
+ // Temp disable for layout template check...
+ // if (!config.layout.toolsets) {
+ // throw Error('card::setConfig - No toolsets defined');
+ // }
+
+ // testing
+ if (config.entities) {
+ const newdomain = this._computeDomain(config.entities[0].entity);
+ if (newdomain !== 'sensor') {
+ // If not a sensor, check if attribute is a number. If so, continue, otherwise Error...
+ if (config.entities[0].attribute && !isNaN(config.entities[0].attribute)) {
+ throw Error('card::setConfig - First entity or attribute must be a numbered sensorvalue, but is NOT');
+ }
+ }
+ }
+
+ // Copy config, as we must have write access to replace templates!
+ const newConfig = Merge.mergeDeep(config);
+
+ // #TODO must be removed after removal of segmented arcs part below
+ this.config = newConfig;
+
+ // NEW for ts processing
+ this.toolset = [];
+
+ const thisMe = this;
+ function findTemplate(key, value) {
+ // Filtering out properties
+ // console.log("findTemplate, key=", key, "value=", value);
+ if (value?.template) {
+ const template = thisMe.lovelace.config.sak_user_templates.templates[value.template.name];
+ if (!template) {
+ console.error('Template not found...', value.template, template);
+ }
+
+ const replacedValue = Templates.replaceVariables3(value.template.variables, template);
+ // Hmm. cannot add .template var. object is not extensible...
+ // replacedValue.template = 'replaced';
+ const secondValue = Merge.mergeDeep(replacedValue);
+ // secondValue.from_template = 'replaced';
+
+ return secondValue;
+ }
+ if (key === 'template') {
+ // Template is gone via replace!!!! No template anymore, as there is no merge done.
+ console.log('findTemplate return key=template/value', key, undefined);
+
+ return value;
+ }
+ // console.log("findTemplate return key/value", key, value);
+ return value;
+ }
+
+ // Find & Replace template definitions. This also supports layout templates
+ const cfg = JSON.stringify(this.config, findTemplate);
+
+ // To further process toolset templates, get reference to toolsets
+ const cfgobj = JSON.parse(cfg).layout.toolsets;
+
+ // Set layout template if found
+ if (this.config.layout.template) {
+ this.config.layout = JSON.parse(cfg).layout;
+ }
+
+ // Continue to check & replace partial toolset templates and overrides
+ this.config.layout.toolsets.map((toolsetCfg, toolidx) => {
+ let toolList = null;
+
+ if (!this.toolsets) this.toolsets = [];
+
+ // eslint-disable-next-line no-constant-condition
+ if (true) {
+ let found = false;
+ let toolAdd = [];
+
+ toolList = cfgobj[toolidx].tools;
+ // Check for empty tool list. This can be if template is used. Tools come from template, not from config...
+ if (toolsetCfg.tools) {
+ toolsetCfg.tools.map((tool, index) => {
+ cfgobj[toolidx].tools.map((toolT, indexT) => {
+ if (tool.id === toolT.id) {
+ if (toolsetCfg.template) {
+ if (this.config.layout.toolsets[toolidx].position)
+ cfgobj[toolidx].position = Merge.mergeDeep(this.config.layout.toolsets[toolidx].position);
+
+ toolList[indexT] = Merge.mergeDeep(toolList[indexT], tool);
+
+ // After merging/replacing. We might get some template definitions back!!!!!!
+ toolList[indexT] = JSON.parse(JSON.stringify(toolList[indexT], findTemplate));
+
+ found = true;
+ }
+ if (this.dev.debug) console.log('card::setConfig - got toolsetCfg toolid', tool, index, toolT, indexT, tool);
+ }
+ cfgobj[toolidx].tools[indexT] = Templates.getJsTemplateOrValueConfig(cfgobj[toolidx].tools[indexT], Merge.mergeDeep(cfgobj[toolidx].tools[indexT]));
+ return found;
+ });
+ if (!found) toolAdd = toolAdd.concat(toolsetCfg.tools[index]);
+ return found;
+ });
+ }
+ toolList = toolList.concat(toolAdd);
+ }
+
+ toolsetCfg = cfgobj[toolidx];
+ const newToolset = new Toolset(this, toolsetCfg);
+ this.toolsets.push(newToolset);
+ return true;
+ });
+
+ // Special case. Abuse card for m3 conversion to output
+ if (this.dev.m3) {
+ console.log('*** M3 - Checking for m3.yaml template to convert...');
+
+ if (this.lovelace.config.sak_user_templates.templates.m3) {
+ const { m3 } = this.lovelace.config.sak_user_templates.templates;
+
+ console.log('*** M3 - Found. Material 3 conversion starting...');
+ // These variables are used of course, but eslint thinks they are NOT.
+ // If I remove them, eslint complains about undefined variables...
+ // eslint-disable-next-line no-unused-vars
+ let palette = '';
+ // eslint-disable-next-line no-unused-vars
+ let colordefault = '';
+ // eslint-disable-next-line no-unused-vars
+ let colorlight = '';
+ // eslint-disable-next-line no-unused-vars
+ let colordark = '';
+
+ let surfacelight = '';
+ let primarylight = '';
+ let neutrallight = '';
+
+ let surfacedark = '';
+ let primarydark = '';
+ let neutraldark = '';
+
+ const colorEntities = {};
+ const cssNames = {};
+ const cssNamesRgb = {};
+
+ m3.entities.map((entity) => {
+ if (['ref.palette', 'sys.color', 'sys.color.light', 'sys.color.dark'].includes(entity.category_id)) {
+ if (!entity.tags.includes('alias')) {
+ colorEntities[entity.id] = { value: entity.value, tags: entity.tags };
+ }
+ }
+
+ if (entity.category_id === 'ref.palette') {
+ palette += `${entity.id}: '${entity.value}'\n`;
+
+ // test for primary light color...
+ if (entity.id === 'md.ref.palette.primary40') {
+ primarylight = entity.value;
+ }
+ // test for primary dark color...
+ if (entity.id === 'md.ref.palette.primary80') {
+ primarydark = entity.value;
+ }
+
+ // test for neutral light color...
+ if (entity.id === 'md.ref.palette.neutral40') {
+ neutrallight = entity.value;
+ }
+ // test for neutral light color...
+ if (entity.id === 'md.ref.palette.neutral80') {
+ neutraldark = entity.value;
+ }
+ }
+
+ if (entity.category_id === 'sys.color') {
+ colordefault += `${entity.id}: '${entity.value}'\n`;
+ }
+
+ if (entity.category_id === 'sys.color.light') {
+ colorlight += `${entity.id}: '${entity.value}'\n`;
+
+ // test for surface light color...
+ if (entity.id === 'md.sys.color.surface.light') {
+ surfacelight = entity.value;
+ }
+ }
+
+ if (entity.category_id === 'sys.color.dark') {
+ colordark += `${entity.id}: '${entity.value}'\n`;
+
+ // test for surface light color...
+ if (entity.id === 'md.sys.color.surface.dark') {
+ surfacedark = entity.value;
+ }
+ }
+ return true;
+ });
+
+ ['primary', 'secondary', 'tertiary', 'error', 'neutral', 'neutral-variant'].forEach((paletteName) => {
+ [5, 15, 25, 35, 45, 65, 75, 85].forEach((step) => {
+ colorEntities[`md.ref.palette.${paletteName}${step.toString()}`] = {
+ value: Colors.getGradientValue(
+ colorEntities[`md.ref.palette.${paletteName}${(step - 5).toString()}`].value,
+ colorEntities[`md.ref.palette.${paletteName}${(step + 5).toString()}`].value,
+ 0.5,
+ ),
+ tags: [...colorEntities[`md.ref.palette.${paletteName}${(step - 5).toString()}`].tags],
+ };
+ colorEntities[`md.ref.palette.${paletteName}${step.toString()}`].tags[3] = paletteName + step.toString();
+ });
+ colorEntities[`md.ref.palette.${paletteName}7`] = {
+ value: Colors.getGradientValue(
+ colorEntities[`md.ref.palette.${paletteName}5`].value,
+ colorEntities[`md.ref.palette.${paletteName}10`].value,
+ 0.5,
+ ),
+ tags: [...colorEntities[`md.ref.palette.${paletteName}10`].tags],
+ };
+ colorEntities[`md.ref.palette.${paletteName}7`].tags[3] = `${paletteName}7`;
+
+ colorEntities[`md.ref.palette.${paletteName}92`] = {
+ value: Colors.getGradientValue(
+ colorEntities[`md.ref.palette.${paletteName}90`].value,
+ colorEntities[`md.ref.palette.${paletteName}95`].value,
+ 0.5,
+ ),
+ tags: [...colorEntities[`md.ref.palette.${paletteName}90`].tags],
+ };
+ colorEntities[`md.ref.palette.${paletteName}92`].tags[3] = `${paletteName}92`;
+
+ colorEntities[`md.ref.palette.${paletteName}97`] = {
+ value: Colors.getGradientValue(
+ colorEntities[`md.ref.palette.${paletteName}95`].value,
+ colorEntities[`md.ref.palette.${paletteName}99`].value,
+ 0.5,
+ ),
+ tags: [...colorEntities[`md.ref.palette.${paletteName}90`].tags],
+ };
+ colorEntities[`md.ref.palette.${paletteName}97`].tags[3] = `${paletteName}97`;
+ });
+
+ // eslint-disable-next-line no-restricted-syntax
+ for (const [index, entity] of Object.entries(colorEntities)) {
+ // eslint-disable-next-line no-use-before-define
+ cssNames[index] = `theme-${entity.tags[1]}-${entity.tags[2]}-${entity.tags[3]}: rgb(${hex2rgb(entity.value)})`;
+ // eslint-disable-next-line no-use-before-define
+ cssNamesRgb[index] = `theme-${entity.tags[1]}-${entity.tags[2]}-${entity.tags[3]}-rgb: ${hex2rgb(entity.value)}`;
+ }
+
+ // https://filosophy.org/code/online-tool-to-lighten-color-without-alpha-channel/
+
+ // eslint-disable-next-line no-inner-declarations
+ function hex2rgb(hexColor) {
+ const rgbCol = {};
+
+ rgbCol.r = Math.round(parseInt(hexColor.substr(1, 2), 16));
+ rgbCol.g = Math.round(parseInt(hexColor.substr(3, 2), 16));
+ rgbCol.b = Math.round(parseInt(hexColor.substr(5, 2), 16));
+
+ // const cssRgbColor = "rgb(" + rgbCol.r + "," + rgbCol.g + "," + rgbCol.b + ")";
+ const cssRgbColor = `${rgbCol.r},${rgbCol.g},${rgbCol.b}`;
+ return cssRgbColor;
+ }
+
+ // eslint-disable-next-line no-inner-declarations
+ function getSurfaces(surfaceColor, paletteColor, opacities, cssName, mode) {
+ const bgCol = {};
+ const fgCol = {};
+
+ bgCol.r = Math.round(parseInt(surfaceColor.substr(1, 2), 16));
+ bgCol.g = Math.round(parseInt(surfaceColor.substr(3, 2), 16));
+ bgCol.b = Math.round(parseInt(surfaceColor.substr(5, 2), 16));
+
+ fgCol.r = Math.round(parseInt(paletteColor.substr(1, 2), 16));
+ fgCol.g = Math.round(parseInt(paletteColor.substr(3, 2), 16));
+ fgCol.b = Math.round(parseInt(paletteColor.substr(5, 2), 16));
+
+ let surfaceColors = '';
+ let r; let g; let b;
+ opacities.forEach((opacity, index) => {
+ r = Math.round(opacity * fgCol.r + (1 - opacity) * bgCol.r);
+ g = Math.round(opacity * fgCol.g + (1 - opacity) * bgCol.g);
+ b = Math.round(opacity * fgCol.b + (1 - opacity) * bgCol.b);
+
+ surfaceColors += `${cssName + (index + 1).toString()}-${mode}: rgb(${r},${g},${b})\n`;
+ surfaceColors += `${cssName + (index + 1).toString()}-${mode}-rgb: ${r},${g},${b}\n`;
+ });
+
+ return surfaceColors;
+ }
+
+ // Generate surfaces for dark and light...
+ const opacitysurfacelight = [0.03, 0.05, 0.08, 0.11, 0.15, 0.19, 0.24, 0.29, 0.35, 0.4];
+ const opacitysurfacedark = [0.05, 0.08, 0.11, 0.15, 0.19, 0.24, 0.29, 0.35, 0.40, 0.45];
+
+ const surfacenL = getSurfaces(surfacelight, neutrallight, opacitysurfacelight, ' theme-ref-elevation-surface-neutral', 'light');
+
+ const neutralvariantlight = colorEntities['md.ref.palette.neutral-variant40'].value;
+ const surfacenvL = getSurfaces(surfacelight, neutralvariantlight, opacitysurfacelight, ' theme-ref-elevation-surface-neutral-variant', 'light');
+
+ const surfacepL = getSurfaces(surfacelight, primarylight, opacitysurfacelight, ' theme-ref-elevation-surface-primary', 'light');
+
+ const secondarylight = colorEntities['md.ref.palette.secondary40'].value;
+ const surfacesL = getSurfaces(surfacelight, secondarylight, opacitysurfacelight, ' theme-ref-elevation-surface-secondary', 'light');
+
+ const tertiarylight = colorEntities['md.ref.palette.tertiary40'].value;
+ const surfacetL = getSurfaces(surfacelight, tertiarylight, opacitysurfacelight, ' theme-ref-elevation-surface-tertiary', 'light');
+
+ const errorlight = colorEntities['md.ref.palette.error40'].value;
+ const surfaceeL = getSurfaces(surfacelight, errorlight, opacitysurfacelight, ' theme-ref-elevation-surface-error', 'light');
+
+ // DARK
+ const surfacenD = getSurfaces(surfacedark, neutraldark, opacitysurfacedark, ' theme-ref-elevation-surface-neutral', 'dark');
+
+ const neutralvariantdark = colorEntities['md.ref.palette.neutral-variant80'].value;
+ const surfacenvD = getSurfaces(surfacedark, neutralvariantdark, opacitysurfacedark, ' theme-ref-elevation-surface-neutral-variant', 'dark');
+
+ const surfacepD = getSurfaces(surfacedark, primarydark, opacitysurfacedark, ' theme-ref-elevation-surface-primary', 'dark');
+
+ const secondarydark = colorEntities['md.ref.palette.secondary80'].value;
+ const surfacesD = getSurfaces(surfacedark, secondarydark, opacitysurfacedark, ' theme-ref-elevation-surface-secondary', 'dark');
+
+ const tertiarydark = colorEntities['md.ref.palette.tertiary80'].value;
+ const surfacetD = getSurfaces(surfacedark, tertiarydark, opacitysurfacedark, ' theme-ref-elevation-surface-tertiary', 'dark');
+
+ const errordark = colorEntities['md.ref.palette.error80'].value;
+ const surfaceeD = getSurfaces(surfacedark, errordark, opacitysurfacedark, ' theme-ref-elevation-surface-error', 'dark');
+
+ let themeDefs = '';
+ // eslint-disable-next-line no-restricted-syntax
+ for (const [index, cssName] of Object.entries(cssNames)) { // lgtm[js/unused-local-variable]
+ if (cssName.substring(0, 9) === 'theme-ref') {
+ themeDefs += ` ${cssName}\n`;
+ themeDefs += ` ${cssNamesRgb[index]}\n`;
+ }
+ }
+ // Dump full theme contents to console.
+ // User should copy this content into the theme definition YAML file.
+ console.log(surfacenL + surfacenvL + surfacepL + surfacesL + surfacetL + surfaceeL
+ + surfacenD + surfacenvD + surfacepD + surfacesD + surfacetD + surfaceeD
+ + themeDefs);
+
+ console.log('*** M3 - Material 3 conversion DONE. You should copy the above output...');
+ }
+ }
+
+ // Get aspectratio. This can be defined at card level or layout level
+ this.aspectratio = (this.config.layout.aspectratio || this.config.aspectratio || '1/1').trim();
+
+ const ar = this.aspectratio.split('/');
+ if (!this.viewBox) this.viewBox = {};
+ this.viewBox.width = ar[0] * SVG_DEFAULT_DIMENSIONS;
+ this.viewBox.height = ar[1] * SVG_DEFAULT_DIMENSIONS;
+
+ if (this.config.layout.styles?.card) {
+ this.styles.card = this.config.layout.styles.card;
+ }
+
+ if (this.dev.debug) console.log('Step 5: toolconfig, list of toolsets', this.toolsets);
+ if (this.dev.debug) console.log('debug - setConfig', this.cardId, this.config);
+ if (this.dev.performance) console.timeEnd(`--> ${this.cardId} PERFORMANCE card::setConfig`);
+
+ this.configIsSet = true;
+ }
+
+ /** *****************************************************************************
+ * card::connectedCallback()
+ *
+ * Summary.
+ *
+ */
+ connectedCallback() {
+ if (this.dev.performance) console.time(`--> ${this.cardId} PERFORMANCE card::connectedCallback`);
+
+ if (this.dev.debug) console.log('*****Event - connectedCallback', this.cardId, new Date().getTime());
+ this.connected = true;
+ super.connectedCallback();
+
+ if (this.entityHistory.update_interval) {
+ // Fix crash while set hass not yet called, and thus no access to entities!
+ this.updateOnInterval();
+ // #TODO, modify to total interval
+ // Use fast interval at start, and normal interval after that, if _hass is defined...
+ clearInterval(this.interval);
+ this.interval = setInterval(
+ () => this.updateOnInterval(),
+ this._hass ? this.entityHistory.update_interval * 1000 : 1000,
+ );
+ }
+ if (this.dev.debug) console.log('ConnectedCallback', this.cardId);
+
+ // MUST request updates again, as no card is displayed otherwise as long as there is no data coming in...
+ this.requestUpdate();
+ if (this.dev.performance) console.timeEnd(`--> ${this.cardId} PERFORMANCE card::connectedCallback`);
+ }
+
+ /** *****************************************************************************
+ * card::disconnectedCallback()
+ *
+ * Summary.
+ *
+ */
+ disconnectedCallback() {
+ if (this.dev.performance) console.time(`--> ${this.cardId} PERFORMANCE card::disconnectedCallback`);
+
+ if (this.dev.debug) console.log('*****Event - disconnectedCallback', this.cardId, new Date().getTime());
+ if (this.interval) {
+ clearInterval(this.interval);
+ this.interval = 0;
+ }
+ super.disconnectedCallback();
+ if (this.dev.debug) console.log('disconnectedCallback', this.cardId);
+ this.connected = false;
+ if (this.dev.performance) console.timeEnd(`--> ${this.cardId} PERFORMANCE card::disconnectedCallback`);
+ }
+
+ /** *****************************************************************************
+ * card::firstUpdated()
+ *
+ * Summary.
+ * firstUpdated fires after the first time the card hs been updated using its render method,
+ * but before the browser has had a chance to paint.
+ *
+ */
+
+ firstUpdated(changedProperties) {
+ if (this.dev.debug) console.log('*****Event - card::firstUpdated', this.cardId, new Date().getTime());
+
+ if (this.toolsets) {
+ this.toolsets.map(async (item) => {
+ item.firstUpdated(changedProperties);
+ return true;
+ });
+ }
+ }
+
+ /** *****************************************************************************
+ * card::updated()
+ *
+ * Summary.
+ *
+ */
+ updated(changedProperties) {
+ if (this.dev.debug) console.log('*****Event - Updated', this.cardId, new Date().getTime());
+
+ if (this.toolsets) {
+ this.toolsets.map(async (item) => {
+ item.updated(changedProperties);
+ return true;
+ });
+ }
+ }
+
+ /** *****************************************************************************
+ * card::render()
+ *
+ * Summary.
+ * Renders the complete SVG based card according to the specified layout.
+ *
+ * render ICON TESTING pathh lzwzmegla undefined undefined
+ * render ICON TESTING pathh lzwzmegla undefined NodeList [ha-svg-icon]
+ * render ICON TESTING pathh lzwzmegla M7,2V13H10V22L17,10H13L17,2H7Z NodeList [ha-svg-icon]
+ */
+
+ render() {
+ if (this.dev.performance) console.time(`--> ${this.cardId} PERFORMANCE card::render`);
+ if (this.dev.debug) console.log('*****Event - render', this.cardId, new Date().getTime());
+
+ if (!this.connected) {
+ if (this.dev.debug) console.log('render but NOT connected', this.cardId, new Date().getTime());
+ return;
+ }
+
+ let myHtml;
+
+ try {
+ if (this.config.disable_card) {
+ myHtml = html`
+
+ ${this._renderSvg()}
+
+ `;
+ } else {
+ myHtml = html`
+
+
+ ${this._renderSvg()}
+
+
+ `;
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ if (this.dev.performance) console.timeEnd(`--> ${this.cardId} PERFORMANCE card::render`);
+
+ return myHtml;
+ }
+
+ _renderSakSvgDefinitions() {
+ return svg`
+ ${SwissArmyKnifeCard.sakSvgContent}
+ `;
+ }
+
+ _renderUserSvgDefinitions() {
+ return svg`
+ ${SwissArmyKnifeCard.userSvgContent}
+ `;
+ }
+
+ themeIsDarkMode() {
+ return (this.theme.darkMode === true);
+ }
+
+ themeIsLightMode() {
+ return (this.theme.darkMode === false);
+ }
+
+ /** *****************************************************************************
+ * card::_RenderToolsets()
+ *
+ * Summary.
+ * Renders the toolsets
+ *
+ */
+
+ _RenderToolsets() {
+ if (this.dev.debug) console.log('all the tools in renderTools', this.tools);
+
+ return svg`
+
+ ${this.toolsets.map((toolset) => toolset.render())}
+
+
+
+ ${this._renderSakSvgDefinitions()}
+ ${this._renderUserSvgDefinitions()}
+
+ `;
+ }
+
+ /** *****************************************************************************
+ * card::_renderSvg()
+ *
+ * Summary.
+ * Renders the SVG
+ *
+ * NTS:
+ * If height and width given for svg it equals the viewbox. The card is not scaled
+ * anymore to the full dimensions of the card given by hass/lovelace.
+ * Card or svg is also placed default at start of viewport (not box), and can be
+ * placed at start, center or end of viewport (Use align-self to center it).
+ *
+ * 1. If height and width are ommitted, the ha-card/viewport is forced to the x/y
+ * aspect ratio of the viewbox, ie 1:1. EXACTLY WHAT WE WANT!
+ * 2. If height and width are set to 100%, the viewport (or ha-card) forces the
+ * aspect-ratio on the svg. Although GetCardSize is set to 4, it seems the
+ * height is forced to 150px, so part of the viewbox/svg is not shown or
+ * out of proportion!
+ *
+ */
+
+ _renderCardAttributes() {
+ let entityValue;
+ const attributes = [];
+
+ this._attributes = '';
+
+ for (let i = 0; i < this.entities.length; i++) {
+ entityValue = this.attributesStr[i]
+ ? this.attributesStr[i]
+ : this.secondaryInfoStr[i]
+ ? this.secondaryInfoStr[i]
+ : this.entitiesStr[i];
+ attributes.push(entityValue);
+ }
+ this._attributes = attributes;
+ return attributes;
+ }
+
+ _renderSvg() {
+ const cardFilter = this.config.card_filter ? this.config.card_filter : 'card--filter-none';
+
+ const svgItems = [];
+
+ // The extra group is required for Safari to have filters work and updates are rendered.
+ // If group omitted, some cards do update, and some not!!!! Don't ask why!
+ // style="${styleMap(this.styles.card)}"
+
+ this._renderCardAttributes();
+
+ // @2022.01.26 Timing / Ordering problem:
+ // - the _RenderToolsets() function renders tools, which build the this.styles/this.classes maps.
+ // - However: this means that higher styles won't render until the next render, ie this.styles.card
+ // won't render, as this variable is already cached as it seems by Polymer.
+ // - This is also the case for this.styles.tools/toolsets: they also don't work!
+ //
+ // Fix for card styles: render toolsets first, and then push the svg data!!
+
+ const toolsetsSvg = this._RenderToolsets();
+
+ svgItems.push(svg`
+
+
+ ${toolsetsSvg}
+
+ `);
+
+ return svg`${svgItems}`;
+ }
+
+ /** *****************************************************************************
+ * card::_buildUom()
+ *
+ * Summary.
+ * Builds the Unit of Measurement string.
+ *
+ */
+
+ _buildUom(derivedEntity, entityState, entityConfig) {
+ return (
+ derivedEntity?.unit
+ || entityConfig?.unit
+ || entityState?.attributes.unit_of_measurement
+ || ''
+ );
+ }
+
+ toLocale(string, fallback = 'unknown') {
+ const lang = this._hass.selectedLanguage || this._hass.language;
+ const resources = this._hass.resources[lang];
+ return (resources && resources[string] ? resources[string] : fallback);
+ }
+
+ /** *****************************************************************************
+ * card::_buildState()
+ *
+ * Summary.
+ * Builds the State string.
+ * If state is not a number, the state is returned AS IS, otherwise the state
+ * is build according to the specified number of decimals.
+ *
+ * NOTE:
+ * - a number value of "-0" is translated to "0". The sign is gone...
+ *
+ * IMPORTANT NOTE:
+ * - do NOT replace isNaN() by Number.isNaN(). They are INCOMPATIBLE !!!!!!!!!
+ */
+
+ _buildState(inState, entityConfig) {
+ // if (typeof inState !== 'number') {
+ if (isNaN(inState)) {
+ if (inState === 'unavailable') return '-ua-';
+ return inState;
+ }
+
+ if (entityConfig.format === 'brightness') {
+ return `${Math.round((inState / 255) * 100)}`;
+ }
+
+ const state = Math.abs(Number(inState));
+ const sign = Math.sign(inState);
+
+ if (['0', '-0'].includes(sign)) return sign;
+
+ if (entityConfig.decimals === undefined || Number.isNaN(entityConfig.decimals) || Number.isNaN(state))
+ return (sign === '-1' ? `-${(Math.round(state * 100) / 100).toString()}` : (Math.round(state * 100) / 100).toString());
+
+ const x = 10 ** entityConfig.decimals;
+ return (sign === '-1' ? `-${(Math.round(state * x) / x).toFixed(entityConfig.decimals).toString()}`
+ : (Math.round(state * x) / x).toFixed(entityConfig.decimals).toString());
+ }
+
+ /** *****************************************************************************
+ * card::_buildSecondaryInfo()
+ *
+ * Summary.
+ * Builds the SecondaryInfo string.
+ *
+ */
+
+ _buildSecondaryInfo(inSecInfoState, entityConfig) {
+ const leftPad = (num) => (num < 10 ? `0${num}` : num);
+
+ function secondsToDuration(d) {
+ const h = Math.floor(d / 3600);
+ const m = Math.floor((d % 3600) / 60);
+ const s = Math.floor((d % 3600) % 60);
+
+ if (h > 0) {
+ return `${h}:${leftPad(m)}:${leftPad(s)}`;
+ }
+ if (m > 0) {
+ return `${m}:${leftPad(s)}`;
+ }
+ if (s > 0) {
+ return `${s}`;
+ }
+ return null;
+ }
+
+ const lang = this._hass.selectedLanguage || this._hass.language;
+
+ // this.polyfill(lang);
+
+ if (['relative', 'total', 'date', 'time', 'datetime'].includes(entityConfig.format)) {
+ const timestamp = new Date(inSecInfoState);
+ if (!(timestamp instanceof Date) || isNaN(timestamp.getTime())) {
+ return inSecInfoState;
+ }
+
+ let retValue;
+ // return date/time according to formatting...
+ switch (entityConfig.format) {
+ case 'relative':
+ // eslint-disable-next-line no-case-declarations
+ const diff = selectUnit(timestamp, new Date());
+ retValue = new Intl.RelativeTimeFormat(lang, { numeric: 'auto' }).format(diff.value, diff.unit);
+ break;
+ case 'total':
+ case 'precision':
+ retValue = 'Not Yet Supported';
+ break;
+ case 'date':
+ retValue = new Intl.DateTimeFormat(lang, { year: 'numeric', month: 'numeric', day: 'numeric' }).format(timestamp);
+ break;
+ case 'time':
+ retValue = new Intl.DateTimeFormat(lang, { hour: 'numeric', minute: 'numeric', second: 'numeric' }).format(timestamp);
+ break;
+ case 'datetime':
+ retValue = new Intl.DateTimeFormat(lang, {
+ year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric',
+ }).format(timestamp);
+ break;
+ default:
+ }
+ return retValue;
+ }
+
+ if (isNaN(parseFloat(inSecInfoState)) || !isFinite(inSecInfoState)) {
+ return inSecInfoState;
+ }
+ if (entityConfig.format === 'brightness') {
+ return `${Math.round((inSecInfoState / 255) * 100)} %`;
+ }
+ if (entityConfig.format === 'duration') {
+ return secondsToDuration(inSecInfoState);
+ }
+ }
+
+ /** *****************************************************************************
+ * card::_computeState()
+ *
+ * Summary.
+ *
+ */
+
+ _computeState(inState, dec) {
+ if (isNaN(inState)) {
+ console.log('computestate - NAN', inState, dec);
+ return inState;
+ }
+
+ const state = Number(inState);
+
+ if (dec === undefined || isNaN(dec) || isNaN(state)) {
+ return Math.round(state * 100) / 100;
+ }
+
+ const x = 10 ** dec;
+ return (Math.round(state * x) / x).toFixed(dec);
+ }
+
+ _computeDomain(entityId) {
+ return entityId.substr(0, entityId.indexOf('.'));
+ }
+
+ _computeEntity(entityId) {
+ return entityId.substr(entityId.indexOf('.') + 1);
+ }
+
+ // 2022.01.25 #TODO
+ // Reset interval to 5 minutes: is now short I think after connectedCallback().
+ // Only if _hass exists / is set --> set to 5 minutes!
+ //
+ // BUG: If no history entity, the interval check keeps running. Initially set to 2000ms, and
+ // keeps running with that interval. If history present, interval is larger ????????
+ //
+ // There is no check yet, if history is requested. That is the only reason to have this
+ // interval active!
+ updateOnInterval() {
+ // Only update if hass is already set, this might be not the case the first few calls...
+ // console.log("updateOnInterval -> check...");
+ if (!this._hass) {
+ if (this.dev.debug) console.log('UpdateOnInterval - NO hass, returning');
+ return;
+ }
+ if (this.stateChanged && !this.entityHistory.updating) {
+ // 2020.10.24
+ // Leave true, as multiple entities can be fetched. fetch every 5 minutes...
+ // this.stateChanged = false;
+ this.updateData();
+ // console.log("*RC* updateOnInterval -> updateData", this.entityHistory);
+ }
+
+ if (!this.entityHistory.needed) {
+ // console.log("*RC* updateOnInterval -> stop timer", this.entityHistory, this.interval);
+ if (this.interval) {
+ window.clearInterval(this.interval);
+ this.interval = 0;
+ }
+ } else {
+ window.clearInterval(this.interval);
+ this.interval = setInterval(
+ () => this.updateOnInterval(),
+ // 5 * 1000);
+ this.entityHistory.update_interval * 1000,
+ );
+ // console.log("*RC* updateOnInterval -> start timer", this.entityHistory, this.interval);
+ }
+ }
+
+ async fetchRecent(entityId, start, end, skipInitialState) {
+ let url = 'history/period';
+ if (start) url += `/${start.toISOString()}`;
+ url += `?filter_entity_id=${entityId}`;
+ if (end) url += `&end_time=${end.toISOString()}`;
+ if (skipInitialState) url += '&skip_initial_state';
+ url += '&minimal_response';
+
+ // console.log('fetchRecent - call is', entityId, start, end, skipInitialState, url);
+ return this._hass.callApi('GET', url);
+ }
+
+ // async updateData({ config } = this) {
+ async updateData() {
+ this.entityHistory.updating = true;
+
+ if (this.dev.debug) console.log('card::updateData - ENTRY', this.cardId);
+
+ // We have a list of objects that might need some history update
+ // Create list to fetch.
+ const entityList = [];
+ let j = 0;
+
+ // #TODO
+ // Lookup in this.tools for bars, or better tools that need history...
+ // get that entity_index for that object
+ // add to list...
+ this.toolsets.map((toolset, k) => {
+ toolset.tools.map((item, i) => {
+ if (item.type === 'bar') {
+ const end = new Date();
+ const start = new Date();
+ start.setHours(end.getHours() - item.tool.config.hours);
+ const attr = this.config.entities[item.tool.config.entity_index].attribute ? this.config.entities[item.tool.config.entity_index].attribute : null;
+
+ entityList[j] = ({
+ tsidx: k, entityIndex: item.tool.config.entity_index, entityId: this.entities[item.tool.config.entity_index].entity_id, attrId: attr, start, end, type: 'bar', idx: i,
+ });
+ j += 1;
+ }
+ return true;
+ });
+ return true;
+ });
+
+ if (this.dev.debug) console.log('card::updateData - LENGTH', this.cardId, entityList.length, entityList);
+
+ // #TODO
+ // Quick hack to block updates if entrylist is empty
+ this.stateChanged = false;
+
+ if (this.dev.debug) console.log('card::updateData, entityList from tools', entityList);
+
+ try {
+ // const promise = this.config.layout.vbars.map((item, i) => this.updateEntity(item, entity, i, start, end));
+ const promise = entityList.map((item, i) => this.updateEntity(item, i, item.start, item.end));
+ await Promise.all(promise);
+ } finally {
+ this.entityHistory.updating = false;
+ }
+ }
+
+ async updateEntity(entity, index, initStart, end) {
+ let stateHistory = [];
+ const start = initStart;
+ const skipInitialState = false;
+
+ // Get history for this entity and/or attribute.
+ let newStateHistory = await this.fetchRecent(entity.entityId, start, end, skipInitialState);
+
+ // Now we have some history, check if it has valid data and filter out either the entity state or
+ // the entity attribute. Ain't that nice!
+
+ let theState;
+
+ if (newStateHistory[0] && newStateHistory[0].length > 0) {
+ if (entity.attrId) {
+ theState = this.entities[entity.entityIndex].attributes[this.config.entities[entity.entityIndex].attribute];
+ entity.state = theState;
+ }
+ newStateHistory = newStateHistory[0].filter((item) => (entity.attrId ? !isNaN(parseFloat(item.attributes[entity.attrId])) : !isNaN(parseFloat(item.state))));
+
+ newStateHistory = newStateHistory.map((item) => ({
+ last_changed: item.last_changed,
+ state: entity.attrId ? Number(item.attributes[entity.attrId]) : Number(item.state),
+ }));
+ }
+
+ stateHistory = [...stateHistory, ...newStateHistory];
+
+ this.uppdate(entity, stateHistory);
+ }
+
+ uppdate(entity, hist) {
+ if (!hist) return;
+
+ // #LGTM: Unused variable getMin.
+ // Keep this one for later use!!!!!!!!!!!!!!!!!
+ // const getMin = (arr, val) => arr.reduce((min, p) => (
+ // Number(p[val]) < Number(min[val]) ? p : min
+ // ), arr[0]);
+
+ const getAvg = (arr, val) => arr.reduce((sum, p) => (
+ sum + Number(p[val])
+ ), 0) / arr.length;
+
+ const now = new Date().getTime();
+
+ let hours = 24;
+ let barhours = 2;
+
+ if (entity.type === 'bar') {
+ if (this.dev.debug) console.log('entity.type == bar', entity);
+
+ hours = this.toolsets[entity.tsidx].tools[entity.idx].tool.config.hours;
+ barhours = this.toolsets[entity.tsidx].tools[entity.idx].tool.config.barhours;
+ }
+
+ const reduce = (res, item) => {
+ const age = now - new Date(item.last_changed).getTime();
+ const interval = (age / (1000 * 3600) / barhours) - (hours / barhours);
+ const key = Math.floor(Math.abs(interval));
+ if (!res[key]) res[key] = [];
+ res[key].push(item);
+ return res;
+ };
+ const coords = hist.reduce((res, item) => reduce(res, item), []);
+ coords.length = Math.ceil(hours / barhours);
+
+ // If no intervals found, return...
+ if (Object.keys(coords).length === 0) {
+ return;
+ }
+
+ // That STUPID STUPID Math.min/max can't handle empty arrays which are put into it below
+ // so add some data to the array, and everything works!!!!!!
+
+ // check if first interval contains data, if not find first in interval and use first entry as value...
+
+ const firstInterval = Object.keys(coords)[0];
+ if (firstInterval !== '0') {
+ // first index doesn't contain data.
+ coords[0] = [];
+
+ coords[0].push(coords[firstInterval][0]);
+ }
+
+ for (let i = 0; i < (hours / barhours); i++) {
+ if (!coords[i]) {
+ coords[i] = [];
+ coords[i].push(coords[i - 1][coords[i - 1].length - 1]);
+ }
+ }
+ this.coords = coords;
+ let theData = [];
+ theData = [];
+ theData = coords.map((item) => getAvg(item, 'state'));
+
+ // now push data into object...
+ if (entity.type === 'bar') {
+ this.toolsets[entity.tsidx].tools[entity.idx].tool.series = [...theData];
+ }
+
+ // Request a rerender of the card after receiving new data
+ this.requestUpdate();
+ }
+
+ /** *****************************************************************************
+ * card::getCardSize()
+ *
+ * Summary.
+ * Return a fixed value of 4 as the height.
+ *
+ */
+
+ getCardSize() {
+ return (4);
+ }
+}
+
+/**
+ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+ */
+
+// Define the custom Swiss Army Knife card, so Lovelace / Lit can find the custom element!
+customElements.define('swiss-army-knife-card', SwissArmyKnifeCard);
diff --git a/src/merge.js b/src/merge.js
new file mode 100644
index 0000000..6269a17
--- /dev/null
+++ b/src/merge.js
@@ -0,0 +1,35 @@
+/**
+ * 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
+ */
+export default class Merge {
+ static 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 */
+ // Only if pVal is empty???
+
+ // #TODO:
+ // Should check for .id to match both arrays ?!?!?!?!
+ // Only concat if no ID or match found, otherwise mergeDeep ??
+ //
+ // concatenate and then reduce/merge the array based on id's if present??
+ //
+ prev[key] = pVal.concat(...oVal);
+ } else if (isObject(pVal) && isObject(oVal)) {
+ prev[key] = this.mergeDeep(pVal, oVal);
+ } else {
+ prev[key] = oVal;
+ }
+ });
+ return prev;
+ }, {});
+ }
+}
diff --git a/src/range-slider-tool.js b/src/range-slider-tool.js
new file mode 100644
index 0000000..66ff9de
--- /dev/null
+++ b/src/range-slider-tool.js
@@ -0,0 +1,768 @@
+import { fireEvent } from 'custom-card-helpers';
+import { svg } from 'lit-element';
+import { classMap } from 'lit-html/directives/class-map.js';
+import { styleMap } from 'lit-html/directives/style-map.js';
+
+import Merge from './merge';
+import Utils from './utils';
+import BaseTool from './base-tool';
+
+/** ***************************************************************************
+ * RangeSliderTool::constructor class
+ *
+ * Summary.
+ *
+ */
+
+export default class RangeSliderTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_RANGESLIDER_CONFIG = {
+ descr: 'none',
+ position: {
+ cx: 50,
+ cy: 50,
+ orientation: 'horizontal',
+ active: {
+ width: 0,
+ height: 0,
+ radius: 0,
+ },
+ track: {
+ width: 16,
+ height: 7,
+ radius: 3.5,
+ },
+ thumb: {
+ width: 9,
+ height: 9,
+ radius: 4.5,
+ offset: 4.5,
+ },
+ label: {
+ placement: 'none',
+ },
+ },
+ show: {
+ uom: 'end',
+ active: false,
+ },
+ classes: {
+ tool: {
+ 'sak-slider': true,
+ hover: true,
+ },
+ capture: {
+ 'sak-slider__capture': true,
+ },
+ active: {
+ 'sak-slider__active': true,
+ },
+ track: {
+ 'sak-slider__track': true,
+ },
+ thumb: {
+ 'sak-slider__thumb': true,
+ },
+ label: {
+ 'sak-slider__value': true,
+ },
+ uom: {
+ 'sak-slider__uom': true,
+ },
+ },
+ styles: {
+ tool: {
+ },
+ capture: {
+ },
+ active: {
+ },
+ track: {
+ },
+ thumb: {
+ },
+ label: {
+ },
+ uom: {
+ },
+ },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_RANGESLIDER_CONFIG, argConfig), argPos);
+
+ this.svg.activeTrack = {};
+ this.svg.activeTrack.radius = Utils.calculateSvgDimension(this.config.position.active.radius);
+ this.svg.activeTrack.height = Utils.calculateSvgDimension(this.config.position.active.height);
+ this.svg.activeTrack.width = Utils.calculateSvgDimension(this.config.position.active.width);
+
+ this.svg.track = {};
+ this.svg.track.radius = Utils.calculateSvgDimension(this.config.position.track.radius);
+
+ this.svg.thumb = {};
+ this.svg.thumb.radius = Utils.calculateSvgDimension(this.config.position.thumb.radius);
+ this.svg.thumb.offset = Utils.calculateSvgDimension(this.config.position.thumb.offset);
+
+ this.svg.capture = {};
+
+ this.svg.label = {};
+
+ switch (this.config.position.orientation) {
+ case 'horizontal':
+ case 'vertical':
+ this.svg.capture.width = Utils.calculateSvgDimension(this.config.position.capture.width || 1.1 * this.config.position.track.width);
+ this.svg.capture.height = Utils.calculateSvgDimension(this.config.position.capture.height || 3 * this.config.position.thumb.height);
+
+ this.svg.track.width = Utils.calculateSvgDimension(this.config.position.track.width);
+ this.svg.track.height = Utils.calculateSvgDimension(this.config.position.track.height);
+
+ this.svg.thumb.width = Utils.calculateSvgDimension(this.config.position.thumb.width);
+ this.svg.thumb.height = Utils.calculateSvgDimension(this.config.position.thumb.height);
+
+ // x1, y1 = topleft corner
+ this.svg.capture.x1 = this.svg.cx - this.svg.capture.width / 2;
+ this.svg.capture.y1 = this.svg.cy - this.svg.capture.height / 2;
+
+ // x1, y1 = topleft corner
+ this.svg.track.x1 = this.svg.cx - this.svg.track.width / 2;
+ this.svg.track.y1 = this.svg.cy - this.svg.track.height / 2;
+
+ // x1, y1 = topleft corner
+ this.svg.activeTrack.x1 = (this.config.position.orientation === 'horizontal') ? this.svg.track.x1 : this.svg.cx - this.svg.activeTrack.width / 2;
+ this.svg.activeTrack.y1 = this.svg.cy - this.svg.activeTrack.height / 2;
+ // this.svg.activeTrack.x1 = this.svg.track.x1;
+
+ this.svg.thumb.x1 = this.svg.cx - this.svg.thumb.width / 2;
+ this.svg.thumb.y1 = this.svg.cy - this.svg.thumb.height / 2;
+ break;
+
+ default:
+ console.error('RangeSliderTool - constructor: invalid orientation [vertical, horizontal] = ', this.config.position.orientation);
+ throw Error('RangeSliderTool::constructor - invalid orientation [vertical, horizontal] = ', this.config.position.orientation);
+ }
+
+ switch (this.config.position.orientation) {
+ case 'vertical':
+ this.svg.track.y2 = this.svg.cy + this.svg.track.height / 2;
+ this.svg.activeTrack.y2 = this.svg.track.y2;
+ break;
+ default:
+ }
+ switch (this.config.position.label.placement) {
+ case 'position':
+ this.svg.label.cx = Utils.calculateSvgCoordinate(this.config.position.label.cx, 0);
+ this.svg.label.cy = Utils.calculateSvgCoordinate(this.config.position.label.cy, 0);
+ break;
+
+ case 'thumb':
+ this.svg.label.cx = this.svg.cx;
+ this.svg.label.cy = this.svg.cy;
+ break;
+
+ case 'none':
+ break;
+
+ default:
+ console.error('RangeSliderTool - constructor: invalid label placement [none, position, thumb] = ', this.config.position.label.placement);
+ throw Error('RangeSliderTool::constructor - invalid label placement [none, position, thumb] = ', this.config.position.label.placement);
+ }
+
+ // Init classes
+ this.classes.capture = {};
+ this.classes.track = {};
+ this.classes.thumb = {};
+ this.classes.label = {};
+ this.classes.uom = {};
+
+ // Init styles
+ this.styles.capture = {};
+ this.styles.track = {};
+ this.styles.thumb = {};
+ this.styles.label = {};
+ this.styles.uom = {};
+
+ // Init scale
+ this.svg.scale = {};
+ this.svg.scale.min = this.valueToSvg(this, this.config.scale.min);
+ this.svg.scale.max = this.valueToSvg(this, this.config.scale.max);
+ this.svg.scale.step = this.config.scale.step;
+
+ if (this.dev.debug) console.log('RangeSliderTool constructor coords, dimensions', this.coords, this.dimensions, this.svg, this.config);
+ }
+
+ /** *****************************************************************************
+ * RangeSliderTool::svgCoordinateToSliderValue()
+ *
+ * Summary.
+ * @returns {slider value} Translated svg coordinate to actual slider value
+ *
+ */
+
+ svgCoordinateToSliderValue(argThis, m) {
+ let state;
+ let scalePos;
+ let xpos;
+ let ypos;
+
+ switch (argThis.config.position.orientation) {
+ case 'horizontal':
+ xpos = m.x - argThis.svg.track.x1 - this.svg.thumb.width / 2;
+ scalePos = xpos / (argThis.svg.track.width - this.svg.thumb.width);
+ break;
+
+ case 'vertical':
+ // y is calculated from lower y value. So slider is from bottom to top...
+ ypos = argThis.svg.track.y2 - this.svg.thumb.height / 2 - m.y;
+ scalePos = ypos / (argThis.svg.track.height - this.svg.thumb.height);
+ break;
+
+ default:
+ }
+ state = ((argThis.config.scale.max - argThis.config.scale.min) * scalePos) + argThis.config.scale.min;
+ state = Math.round(state / this.svg.scale.step) * this.svg.scale.step;
+ state = Math.max(Math.min(this.config.scale.max, state), this.config.scale.min);
+
+ return state;
+ }
+
+ valueToSvg(argThis, argValue) {
+ if (argThis.config.position.orientation === 'horizontal') {
+ const state = Utils.calculateValueBetween(argThis.config.scale.min, argThis.config.scale.max, argValue);
+
+ const xposp = state * (argThis.svg.track.width - this.svg.thumb.width);
+ const xpos = argThis.svg.track.x1 + this.svg.thumb.width / 2 + xposp;
+ return xpos;
+ } else if (argThis.config.position.orientation === 'vertical') {
+ const state = Utils.calculateValueBetween(argThis.config.scale.min, argThis.config.scale.max, argValue);
+
+ const yposp = state * (argThis.svg.track.height - this.svg.thumb.height);
+ const ypos = argThis.svg.track.y2 - this.svg.thumb.height / 2 - yposp;
+ return ypos;
+ }
+ }
+
+ updateValue(argThis, m) {
+ this._value = this.svgCoordinateToSliderValue(argThis, m);
+ // set dist to 0 to cancel animation frame
+ const dist = 0;
+ // improvement
+ if (Math.abs(dist) < 0.01) {
+ if (this.rid) {
+ window.cancelAnimationFrame(this.rid);
+ this.rid = null;
+ }
+ }
+ }
+
+ updateThumb(argThis, m) {
+ switch (argThis.config.position.orientation) {
+ // eslint-disable-next-line default-case-last
+ default:
+ case 'horizontal':
+ // eslint-disable-next-line no-empty
+ if (this.config.position.label.placement === 'thumb') {
+ }
+
+ if (this.dragging) {
+ const yUp = (this.config.position.label.placement === 'thumb') ? -50 : 0;
+ const yUpStr = `translate(${m.x - this.svg.cx}px , ${yUp}px)`;
+
+ argThis.elements.thumbGroup.style.transform = yUpStr;
+ } else {
+ argThis.elements.thumbGroup.style.transform = `translate(${m.x - this.svg.cx}px, ${0}px)`;
+ }
+ break;
+
+ case 'vertical':
+ if (this.dragging) {
+ const xUp = (this.config.position.label.placement === 'thumb') ? -50 : 0;
+ const xUpStr = `translate(${xUp}px, ${m.y - this.svg.cy}px)`;
+ argThis.elements.thumbGroup.style.transform = xUpStr;
+ } else {
+ argThis.elements.thumbGroup.style.transform = `translate(${0}px, ${m.y - this.svg.cy}px)`;
+ }
+ break;
+ }
+
+ argThis.updateLabel(argThis, m);
+ }
+
+ updateActiveTrack(argThis, m) {
+ if (!argThis.config.show.active) return;
+
+ switch (argThis.config.position.orientation) {
+ // eslint-disable-next-line default-case-last
+ default:
+ case 'horizontal':
+ if (this.dragging) {
+ argThis.elements.activeTrack.setAttribute('width', Math.abs(this.svg.activeTrack.x1 - m.x + this.svg.cx));
+ }
+ break;
+
+ case 'vertical':
+ if (this.dragging) {
+ argThis.elements.activeTrack.setAttribute('y', m.y - this.svg.cy);
+ argThis.elements.activeTrack.setAttribute('height', Math.abs(argThis.svg.activeTrack.y2 - m.y + this.svg.cx));
+ }
+ break;
+ }
+ }
+
+ updateLabel(argThis, m) {
+ if (this.dev.debug) console.log('SLIDER - updateLabel start', m, argThis.config.position.orientation);
+
+ const dec = (this._card.config.entities[this.defaultEntityIndex()].decimals || 0);
+ const x = 10 ** dec;
+ argThis.labelValue2 = (Math.round(argThis.svgCoordinateToSliderValue(argThis, m) * x) / x).toFixed(dec);
+
+ if (this.config.position.label.placement !== 'none') {
+ argThis.elements.label.textContent = argThis.labelValue2;
+ }
+ }
+
+ /*
+ * mouseEventToPoint
+ *
+ * Translate mouse/touch client window coordinates to SVG window coordinates
+ *
+ */
+ // mouseEventToPoint(e) {
+ // var p = this.elements.svg.createSVGPoint();
+ // p.x = e.touches ? e.touches[0].clientX : e.clientX;
+ // p.y = e.touches ? e.touches[0].clientY : e.clientY;
+ // const ctm = this.elements.svg.getScreenCTM().inverse();
+ // var p = p.matrixTransform(ctm);
+ // return p;
+ // }
+ mouseEventToPoint(e) {
+ let p = this.elements.svg.createSVGPoint();
+ p.x = e.touches ? e.touches[0].clientX : e.clientX;
+ p.y = e.touches ? e.touches[0].clientY : e.clientY;
+ const ctm = this.elements.svg.getScreenCTM().inverse();
+ p = p.matrixTransform(ctm);
+ return p;
+ }
+
+ callDragService() {
+ if (typeof this.labelValue2 === 'undefined') return;
+
+ if (this.labelValuePrev !== this.labelValue2) {
+ this.labelValuePrev = this.labelValue2;
+
+ this._processTapEvent(
+ this._card,
+ this._card._hass,
+ this.config,
+ this.config.user_actions.tap_action,
+ this._card.config.entities[this.defaultEntityIndex()]?.entity,
+ this.labelValue2,
+ );
+ }
+ if (this.dragging)
+ this.timeOutId = setTimeout(() => this.callDragService(), this.config.user_actions.drag_action.update_interval);
+ }
+
+ callTapService() {
+ if (typeof this.labelValue2 === 'undefined') return;
+
+ if (this.labelValuePrev !== this.labelValue2) {
+ this.labelValuePrev = this.labelValue2;
+
+ this._processTapEvent(
+ this._card,
+ this._card._hass,
+ this.config,
+ this.config.user_actions?.tap_action,
+ this._card.config.entities[this.defaultEntityIndex()]?.entity,
+ this.labelValue2,
+ );
+ }
+ }
+
+ // eslint-disable-next-line no-unused-vars
+ firstUpdated(changedProperties) {
+ // const thisValue = this;
+ this.labelValue = this._stateValue;
+
+ // function Frame() {
+ // thisValue.rid = window.requestAnimationFrame(Frame);
+ // thisValue.updateValue(thisValue, thisValue.m);
+ // thisValue.updateThumb(thisValue, thisValue.m);
+ // thisValue.updateActiveTrack(thisValue, thisValue.m);
+ // }
+
+ function Frame2() {
+ this.rid = window.requestAnimationFrame(Frame2);
+ this.updateValue(this, this.m);
+ this.updateThumb(this, this.m);
+ this.updateActiveTrack(this, this.m);
+ }
+
+ function pointerMove(e) {
+ let scaleValue;
+
+ e.preventDefault();
+
+ if (this.dragging) {
+ this.m = this.mouseEventToPoint(e);
+
+ switch (this.config.position.orientation) {
+ case 'horizontal':
+ scaleValue = this.svgCoordinateToSliderValue(this, this.m);
+ this.m.x = this.valueToSvg(this, scaleValue);
+ this.m.x = Math.max(this.svg.scale.min, Math.min(this.m.x, this.svg.scale.max));
+ this.m.x = (Math.round(this.m.x / this.svg.scale.step) * this.svg.scale.step);
+ break;
+
+ case 'vertical':
+ scaleValue = this.svgCoordinateToSliderValue(this, this.m);
+ this.m.y = this.valueToSvg(this, scaleValue);
+ this.m.y = (Math.round(this.m.y / this.svg.scale.step) * this.svg.scale.step);
+ break;
+
+ default:
+ }
+ Frame2.call(this);
+ }
+ }
+
+ if (this.dev.debug) console.log('slider - firstUpdated');
+ this.elements = {};
+ this.elements.svg = this._card.shadowRoot.getElementById('rangeslider-'.concat(this.toolId));
+ this.elements.capture = this.elements.svg.querySelector('#capture');
+ this.elements.track = this.elements.svg.querySelector('#rs-track');
+ this.elements.activeTrack = this.elements.svg.querySelector('#active-track');
+ this.elements.thumbGroup = this.elements.svg.querySelector('#rs-thumb-group');
+ this.elements.thumb = this.elements.svg.querySelector('#rs-thumb');
+ this.elements.label = this.elements.svg.querySelector('#rs-label tspan');
+
+ if (this.dev.debug) console.log('slider - firstUpdated svg = ', this.elements.svg, 'path=', this.elements.path, 'thumb=', this.elements.thumb, 'label=', this.elements.label, 'text=', this.elements.text);
+
+ function pointerDown(e) {
+ e.preventDefault();
+
+ // @NTS: Keep this comment for later!!
+ // Safari: We use mouse stuff for pointerdown, but have to use pointer stuff to make sliding work on Safari. WHY??
+ window.addEventListener('pointermove', pointerMove.bind(this), false);
+ // eslint-disable-next-line no-use-before-define
+ window.addEventListener('pointerup', pointerUp.bind(this), false);
+
+ // @NTS: Keep this comment for later!!
+ // Below lines prevent slider working on Safari...
+ //
+ // window.addEventListener('mousemove', pointerMove.bind(this), false);
+ // window.addEventListener('touchmove', pointerMove.bind(this), false);
+ // window.addEventListener('mouseup', pointerUp.bind(this), false);
+ // window.addEventListener('touchend', pointerUp.bind(this), false);
+
+ const mousePos = this.mouseEventToPoint(e);
+ const thumbPos = (this.svg.thumb.x1 + this.svg.thumb.cx);
+ if ((mousePos.x > (thumbPos - 10)) && (mousePos.x < (thumbPos + this.svg.thumb.width + 10))) {
+ fireEvent(window, 'haptic', 'heavy');
+ } else {
+ fireEvent(window, 'haptic', 'error');
+ return;
+ }
+
+ // User is dragging the thumb of the slider!
+ this.dragging = true;
+
+ // Check for drag_action. If none specified, or update_interval = 0, don't update while dragging...
+
+ if ((this.config.user_actions?.drag_action) && (this.config.user_actions?.drag_action.update_interval)) {
+ if (this.config.user_actions.drag_action.update_interval > 0) {
+ this.timeOutId = setTimeout(() => this.callDragService(), this.config.user_actions.drag_action.update_interval);
+ } else {
+ this.timeOutId = null;
+ }
+ }
+ this.m = this.mouseEventToPoint(e);
+
+ if (this.config.position.orientation === 'horizontal') {
+ this.m.x = (Math.round(this.m.x / this.svg.scale.step) * this.svg.scale.step);
+ } else {
+ this.m.y = (Math.round(this.m.y / this.svg.scale.step) * this.svg.scale.step);
+ }
+ if (this.dev.debug) console.log('pointerDOWN', Math.round(this.m.x * 100) / 100);
+ Frame2.call(this);
+ }
+
+ function pointerUp(e) {
+ e.preventDefault();
+
+ // @NTS: Keep this comment for later!!
+ // Safari: Fixes unable to grab pointer
+ window.removeEventListener('pointermove', pointerMove.bind(this), false);
+ window.removeEventListener('pointerup', pointerUp.bind(this), false);
+
+ window.removeEventListener('mousemove', pointerMove.bind(this), false);
+ window.removeEventListener('touchmove', pointerMove.bind(this), false);
+ window.removeEventListener('mouseup', pointerUp.bind(this), false);
+ window.removeEventListener('touchend', pointerUp.bind(this), false);
+
+ if (!this.dragging) return;
+
+ this.dragging = false;
+ clearTimeout(this.timeOutId);
+ this.target = 0;
+ if (this.dev.debug) console.log('pointerUP');
+ Frame2.call(this);
+ this.callTapService();
+ }
+
+ // @NTS: Keep this comment for later!!
+ // For things to work in Safari, we need separate touch and mouse down handlers...
+ // DON't ask WHY! The pointerdown method prevents listening on window events later on.
+ // ie, we can't move our finger
+
+ // this.elements.svg.addEventListener("pointerdown", pointerDown.bind(this), false);
+
+ this.elements.svg.addEventListener('touchstart', pointerDown.bind(this), false);
+ this.elements.svg.addEventListener('mousedown', pointerDown.bind(this), false);
+ }
+
+ /** *****************************************************************************
+ * RangeSliderTool::value()
+ *
+ * Summary.
+ * Receive new state data for the entity this rangeslider is linked to. Called from set hass;
+ * Sets the brightness value of the slider. This is a value 0..255. We display %, so translate
+ *
+ */
+ set value(state) {
+ super.value = state;
+ if (!this.dragging) this.labelValue = this._stateValue;
+ }
+
+ _renderUom() {
+ if (this.config.show.uom === 'none') {
+ return svg``;
+ } else {
+ this.MergeAnimationStyleIfChanged();
+ this.MergeColorFromState(this.styles.uom);
+
+ let fsuomStr = this.styles.label['font-size'];
+
+ let fsuomValue = 0.5;
+ let fsuomType = 'em';
+ const fsuomSplit = fsuomStr.match(/\D+|\d*\.?\d+/g);
+ if (fsuomSplit.length === 2) {
+ fsuomValue = Number(fsuomSplit[0]) * 0.6;
+ fsuomType = fsuomSplit[1];
+ } else console.error('Cannot determine font-size for state/unit', fsuomStr);
+
+ fsuomStr = { 'font-size': fsuomValue + fsuomType };
+
+ this.styles.uom = Merge.mergeDeep(this.config.styles.uom, fsuomStr);
+
+ const uom = this._card._buildUom(this.derivedEntity, this._card.entities[this.defaultEntityIndex()], this._card.config.entities[this.defaultEntityIndex()]);
+
+ // Check for location of uom. end = next to state, bottom = below state ;-), etc.
+ if (this.config.show.uom === 'end') {
+ return svg`
+
+ ${uom}
+ `;
+ } else if (this.config.show.uom === 'bottom') {
+ return svg`
+
+ ${uom}
+ `;
+ } else if (this.config.show.uom === 'top') {
+ return svg`
+
+ ${uom}
+ `;
+ } else {
+ return svg`
+
+ ERRR
+ `;
+ }
+ }
+ }
+
+ /** *****************************************************************************
+ * RangeSliderTool::_renderRangeSlider()
+ *
+ * Summary.
+ * Renders the range slider
+ *
+ */
+
+ _renderRangeSlider() {
+ if (this.dev.debug) console.log('slider - _renderRangeSlider');
+
+ this.MergeAnimationClassIfChanged();
+ // this.MergeColorFromState(this.styles);
+ // this.MergeAnimationStyleIfChanged(this.styles);
+ // this.MergeColorFromState(this.styles);
+
+ this.MergeColorFromState();
+ this.MergeAnimationStyleIfChanged();
+ this.MergeColorFromState();
+
+ // this.MergeAnimationStyleIfChanged();
+ // console.log("renderRangeSlider, styles", this.styles);
+
+ this.renderValue = this._stateValue;
+ if (this.dragging) {
+ this.renderValue = this.labelValue2;
+ } else if (this.elements?.label) this.elements.label.textContent = this.renderValue;
+
+ // Calculate cx and cy: the relative move of the thumb from the center of the track
+ let cx; let
+ cy;
+ switch (this.config.position.label.placement) {
+ case 'none':
+ this.styles.label.display = 'none';
+ this.styles.uom.display = 'none';
+ break;
+ case 'position':
+ cx = (this.config.position.orientation === 'horizontal'
+ ? this.valueToSvg(this, Number(this.renderValue)) - this.svg.cx
+ : 0);
+ cy = (this.config.position.orientation === 'vertical'
+ ? this.valueToSvg(this, Number(this.renderValue)) - this.svg.cy
+ : 0);
+ break;
+
+ case 'thumb':
+ cx = (this.config.position.orientation === 'horizontal'
+ ? -this.svg.label.cx + this.valueToSvg(this, Number(this.renderValue))
+ : 0);
+ cy = (this.config.position.orientation === 'vertical'
+ ? this.valueToSvg(this, Number(this.renderValue))
+ : 0);
+ // eslint-disable-next-line no-unused-expressions
+ if (this.dragging) { (this.config.position.orientation === 'horizontal') ? cy -= 50 : cx -= 50; }
+ break;
+
+ default:
+ console.error('_renderRangeSlider(), invalid label placement', this.config.position.label.placement);
+ }
+ this.svg.thumb.cx = cx;
+ this.svg.thumb.cy = cy;
+
+ function renderActiveTrack() {
+ if (!this.config.show.active) return svg``;
+
+ if (this.config.position.orientation === 'horizontal') {
+ return svg`
+ `;
+ } else {
+ return svg`
+ `;
+ }
+ }
+
+ function renderLabel(argGroup) {
+ if ((this.config.position.label.placement === 'thumb') && argGroup) {
+ return svg`
+
+
+ ${this.renderValue}
+ ${this._renderUom()}
+
+ `;
+ }
+
+ if ((this.config.position.label.placement === 'position') && !argGroup) {
+ return svg`
+
+ ${this.renderValue ? this.renderValue : ''}
+ ${this.renderValue ? this._renderUom() : ''}
+
+ `;
+ }
+ }
+
+ function renderThumbGroup() {
+ return svg`
+
+
+
+
+ ${renderLabel.call(this, true)}
+
+ `;
+ }
+
+ const svgItems = [];
+ svgItems.push(svg`
+
+
+
+
+ ${renderActiveTrack.call(this)}
+ ${renderThumbGroup.call(this)}
+ ${renderLabel.call(this, false)}
+
+
+ `);
+
+ return svgItems;
+ }
+
+ /** *****************************************************************************
+ * RangeSliderTool::render()
+ *
+ * Summary.
+ * The render() function for this object. The conversion of pointer events need
+ * an SVG as grouping object!
+ *
+ * NOTE:
+ * It is imperative that the style overflow=visible is set on the svg.
+ * The weird thing is that if using an svg as grouping object, AND a class, the overflow=visible
+ * seems to be ignored by both chrome and safari. If the overflow=visible is directly set as style,
+ * the setting works.
+ *
+ * Works on svg with direct styling:
+ * ---
+ * return svg`
+ *
+ * ${this._renderRangeSlider()}
+ *
+ * `;
+ *
+ * Does NOT work on svg with class styling:
+ * ---
+ * return svg`
+ *
+ * ${this._renderRangeSlider()}
+ *
+ * `;
+ * where the class has the overflow=visible setting...
+ *
+ */
+ render() {
+ return svg`
+
+ ${this._renderRangeSlider()}
+
+ `;
+ }
+} // END of class
diff --git a/src/rectangle-ex-tool.js b/src/rectangle-ex-tool.js
new file mode 100644
index 0000000..70fa306
--- /dev/null
+++ b/src/rectangle-ex-tool.js
@@ -0,0 +1,142 @@
+import { svg } from 'lit-element';
+import { classMap } from 'lit-html/directives/class-map.js';
+import { styleMap } from 'lit-html/directives/style-map.js';
+
+import Merge from './merge';
+import Utils from './utils';
+import BaseTool from './base-tool';
+
+/** ****************************************************************************
+ * RectangleToolEx class
+ *
+ * Summary.
+ *
+ */
+
+export default class RectangleToolEx extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_RECTANGLEEX_CONFIG = {
+ position: {
+ cx: 50,
+ cy: 50,
+ width: 50,
+ height: 50,
+ radius: {
+ all: 0,
+ },
+ },
+ classes: {
+ tool: {
+ 'sak-rectex': true,
+ hover: true,
+ },
+ rectex: {
+ 'sak-rectex__rectex': true,
+ },
+ },
+ styles: {
+ rectex: {
+ },
+ },
+ };
+ super(argToolset, Merge.mergeDeep(DEFAULT_RECTANGLEEX_CONFIG, argConfig), argPos);
+
+ this.classes.rectex = {};
+ this.styles.rectex = {};
+
+ // #TODO:
+ // Verify max radius, or just let it go, and let the user handle that right value.
+ // A q can be max height of rectangle, ie both corners added must be less than the height, but also less then the width...
+
+ const maxRadius = Math.min(this.svg.height, this.svg.width) / 2;
+ let radius = 0;
+ radius = Utils.calculateSvgDimension(this.config.position.radius.all);
+ this.svg.radiusTopLeft = +Math.min(maxRadius, Math.max(0, Utils.calculateSvgDimension(
+ this.config.position.radius.top_left || this.config.position.radius.left || this.config.position.radius.top || radius,
+ ))) || 0;
+
+ this.svg.radiusTopRight = +Math.min(maxRadius, Math.max(0, Utils.calculateSvgDimension(
+ this.config.position.radius.top_right || this.config.position.radius.right || this.config.position.radius.top || radius,
+ ))) || 0;
+
+ this.svg.radiusBottomLeft = +Math.min(maxRadius, Math.max(0, Utils.calculateSvgDimension(
+ this.config.position.radius.bottom_left || this.config.position.radius.left || this.config.position.radius.bottom || radius,
+ ))) || 0;
+
+ this.svg.radiusBottomRight = +Math.min(maxRadius, Math.max(0, Utils.calculateSvgDimension(
+ this.config.position.radius.bottom_right || this.config.position.radius.right || this.config.position.radius.bottom || radius,
+ ))) || 0;
+
+ if (this.dev.debug) console.log('RectangleToolEx constructor config, svg', this.toolId, this.config, this.svg);
+ }
+
+ /** *****************************************************************************
+ * RectangleToolEx::value()
+ *
+ */
+ set value(state) {
+ super.value = state;
+ }
+
+ /** *****************************************************************************
+ * RectangleToolEx::_renderRectangleEx()
+ *
+ * Summary.
+ * Renders the rectangle using lines and bezier curves with precalculated coordinates and dimensions.
+ *
+ * Refs for creating the path online:
+ * - https://mavo.io/demos/svgpath/
+ *
+ */
+
+ _renderRectangleEx() {
+ this.MergeAnimationClassIfChanged();
+
+ // WIP
+ this.MergeAnimationStyleIfChanged(this.styles);
+ this.MergeAnimationStyleIfChanged();
+ if (this.config.hasOwnProperty('csnew')) {
+ this.MergeColorFromState2(this.styles.rectex, 'rectex');
+ } else {
+ this.MergeColorFromState(this.styles.rectex);
+ }
+
+ if (!this.counter) { this.counter = 0; }
+ this.counter += 1;
+
+ const svgItems = svg`
+
+
+
+ `;
+ return svg`${svgItems}`;
+ }
+
+ /** *****************************************************************************
+ * RectangleToolEx::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ */
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderRectangleEx()}
+
+ `;
+ }
+} // END of class
diff --git a/src/rectangle-tool.js b/src/rectangle-tool.js
new file mode 100644
index 0000000..40da6c9
--- /dev/null
+++ b/src/rectangle-tool.js
@@ -0,0 +1,97 @@
+import { svg } from 'lit-element';
+import { classMap } from 'lit-html/directives/class-map.js';
+import { styleMap } from 'lit-html/directives/style-map.js';
+
+import Merge from './merge';
+import Utils from './utils';
+import BaseTool from './base-tool';
+
+/** ****************************************************************************
+ * RectangleTool class
+ *
+ * Summary.
+ *
+ */
+
+export default class RectangleTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_RECTANGLE_CONFIG = {
+ position: {
+ cx: 50,
+ cy: 50,
+ width: 50,
+ height: 50,
+ rx: 0,
+ },
+ classes: {
+ tool: {
+ 'sak-rectangle': true,
+ hover: true,
+ },
+ rectangle: {
+ 'sak-rectangle__rectangle': true,
+ },
+ },
+ styles: {
+ rectangle: {
+ },
+ },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_RECTANGLE_CONFIG, argConfig), argPos);
+ this.svg.rx = argConfig.position.rx ? Utils.calculateSvgDimension(argConfig.position.rx) : 0;
+
+ this.classes.rectangle = {};
+ this.styles.rectangle = {};
+
+ if (this.dev.debug) console.log('RectangleTool constructor config, svg', this.toolId, this.config, this.svg);
+ }
+
+ /** *****************************************************************************
+ * RectangleTool::value()
+ *
+ * Summary.
+ * Receive new state data for the entity this rectangle is linked to. Called from set hass;
+ *
+ */
+ set value(state) {
+ super.value = state;
+ }
+
+ /** *****************************************************************************
+ * RectangleTool::_renderRectangle()
+ *
+ * Summary.
+ * Renders the circle using precalculated coordinates and dimensions.
+ * Only the runtime style is calculated before rendering the circle
+ *
+ */
+
+ _renderRectangle() {
+ this.MergeAnimationClassIfChanged();
+ this.MergeAnimationStyleIfChanged();
+ this.MergeColorFromState(this.styles.rectangle);
+
+ return svg`
+
+ `;
+ }
+
+ /** *****************************************************************************
+ * RectangleTool::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ */
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderRectangle()}
+
+ `;
+ }
+} // END of class
diff --git a/src/regular-polygon-tool.js b/src/regular-polygon-tool.js
new file mode 100644
index 0000000..b6d596f
--- /dev/null
+++ b/src/regular-polygon-tool.js
@@ -0,0 +1,131 @@
+import { svg } from 'lit-element';
+import { classMap } from 'lit-html/directives/class-map.js';
+import { styleMap } from 'lit-html/directives/style-map.js';
+
+import Merge from './merge';
+import Utils from './utils';
+import BaseTool from './base-tool';
+
+/** ****************************************************************************
+ * RegPolyTool class
+ *
+ * Summary.
+ *
+ */
+
+export default class RegPolyTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_REGPOLY_CONFIG = {
+ position: {
+ cx: 50,
+ cy: 50,
+ radius: 50,
+ side_count: 6,
+ side_skip: 1,
+ angle_offset: 0,
+ },
+ classes: {
+ tool: {
+ 'sak-polygon': true,
+ hover: true,
+ },
+ regpoly: {
+ 'sak-polygon__regpoly': true,
+ },
+ },
+ styles: {
+ tool: {
+ },
+ regpoly: {
+ },
+ },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_REGPOLY_CONFIG, argConfig), argPos);
+
+ this.svg.radius = Utils.calculateSvgDimension(argConfig.position.radius);
+
+ this.classes.regpoly = {};
+ this.styles.regpoly = {};
+ if (this.dev.debug) console.log('RegPolyTool constructor config, svg', this.toolId, this.config, this.svg);
+ }
+
+ /** *****************************************************************************
+ * RegPolyTool::value()
+ *
+ * Summary.
+ * Receive new state data for the entity this circle is linked to. Called from set hass;
+ *
+ */
+ set value(state) {
+ super.value = state;
+ }
+
+ /** *****************************************************************************
+ * RegPolyTool::_renderRegPoly()
+ *
+ * Summary.
+ * Renders the regular polygon using precalculated coordinates and dimensions.
+ * Only the runtime style is calculated before rendering the regular polygon
+ *
+ */
+
+ _renderRegPoly() {
+ const generatePoly = function (p, q, r, a, cx, cy) {
+ const base_angle = 2 * Math.PI / p;
+ let angle = a + base_angle;
+ let x; let y; let
+ d_attr = '';
+
+ for (let i = 0; i < p; i++) {
+ angle += q * base_angle;
+
+ // Use ~~ as it is faster then Math.floor()
+ x = cx + ~~(r * Math.cos(angle));
+ y = cy + ~~(r * Math.sin(angle));
+
+ d_attr
+ += `${((i === 0) ? 'M' : 'L') + x} ${y} `;
+
+ if (i * q % p === 0 && i > 0) {
+ angle += base_angle;
+ x = cx + ~~(r * Math.cos(angle));
+ y = cy + ~~(r * Math.sin(angle));
+
+ d_attr += `M${x} ${y} `;
+ }
+ }
+
+ d_attr += 'z';
+ return d_attr;
+ };
+
+ this.MergeAnimationStyleIfChanged();
+ this.MergeColorFromState(this.styles.regpoly);
+
+ return svg`
+
+ `;
+ }
+
+ /** *****************************************************************************
+ * RegPolyTool::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ */
+ // @click=${e => this._card.handlePopup(e, this._card.entities[this.defaultEntityIndex()])} >
+
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderRegPoly()}
+
+ `;
+ }
+} // END of class
diff --git a/src/segmented-arc-tool.js b/src/segmented-arc-tool.js
new file mode 100644
index 0000000..cff94a8
--- /dev/null
+++ b/src/segmented-arc-tool.js
@@ -0,0 +1,739 @@
+import { svg } from 'lit-element';
+import { styleMap } from 'lit-html/directives/style-map.js';
+
+import Merge from './merge';
+import BaseTool from './base-tool';
+import Utils from './utils';
+import Templates from './templates';
+import Colors from './colors';
+
+/** *****************************************************************************
+ * SegmentedArcTool class
+ *
+ * Summary.
+ *
+ */
+
+export default class SegmentedArcTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_SEGARC_CONFIG = {
+ position: {
+ cx: 50,
+ cy: 50,
+ radius: 45,
+ width: 3,
+ margin: 1.5,
+ },
+ color: 'var(--primary-color)',
+ classes: {
+ tool: {
+ },
+ foreground: {
+ },
+ background: {
+ },
+ },
+ styles: {
+ foreground: {
+ },
+ background: {
+ },
+ },
+ segments: {},
+ colorstops: [],
+ scale: {
+ min: 0,
+ max: 100,
+ width: 2,
+ offset: -3.5,
+ },
+ show: {
+ style: 'fixedcolor',
+ scale: false,
+ },
+ isScale: false,
+ animation: {
+ duration: 1.5,
+ },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_SEGARC_CONFIG, argConfig), argPos);
+
+ if (this.dev.performance) console.time(`--> ${this.toolId} PERFORMANCE SegmentedArcTool::constructor`);
+
+ this.svg.radius = Utils.calculateSvgDimension(argConfig.position.radius);
+ this.svg.radiusX = Utils.calculateSvgDimension(argConfig.position.radius_x || argConfig.position.radius);
+ this.svg.radiusY = Utils.calculateSvgDimension(argConfig.position.radius_y || argConfig.position.radius);
+
+ this.svg.segments = {};
+ // #TODO:
+ // Get gap from colorlist, colorstop or something else. Not from the default segments gap.
+ this.svg.segments.gap = Utils.calculateSvgDimension(this.config.segments.gap);
+ this.svg.scale_offset = Utils.calculateSvgDimension(this.config.scale.offset);
+
+ // Added for confusion???????
+ this._firstUpdatedCalled = false;
+
+ // Remember the values to be able to render from/to
+ this._stateValue = null;
+ this._stateValuePrev = null;
+ this._stateValueIsDirty = false;
+ this._renderFrom = null;
+ this._renderTo = null;
+
+ this.rAFid = null;
+ this.cancelAnimation = false;
+
+ this.arcId = null;
+
+ // Cache path (d= value) of segments drawn in map by segment index (counter). Simple array.
+ this._cache = [];
+
+ this._segmentAngles = [];
+ this._segments = {};
+
+ // Precalculate segments with start and end angle!
+ this._arc = {};
+ this._arc.size = Math.abs(this.config.position.end_angle - this.config.position.start_angle);
+ this._arc.clockwise = this.config.position.end_angle > this.config.position.start_angle;
+ this._arc.direction = this._arc.clockwise ? 1 : -1;
+
+ let tcolorlist = {};
+ let colorlist = null;
+ // New template testing for colorstops
+ if (this.config.segments.colorlist?.template) {
+ colorlist = this.config.segments.colorlist;
+ if (this._card.lovelace.config.sak_user_templates.templates[colorlist.template.name]) {
+ if (this.dev.debug) console.log('SegmentedArcTool::constructor - templates colorlist found', colorlist.template.name);
+ tcolorlist = Templates.replaceVariables2(colorlist.template.variables, this._card.lovelace.config.sak_user_templates.templates[colorlist.template.name]);
+ this.config.segments.colorlist = tcolorlist;
+ }
+ }
+
+ // FIXEDCOLOR
+ if (this.config.show.style === 'fixedcolor') {
+
+ // COLORLIST
+ } else if (this.config.show.style === 'colorlist') {
+ // Get number of segments, and their size in degrees.
+ this._segments.count = this.config.segments.colorlist.colors.length;
+ this._segments.size = this._arc.size / this._segments.count;
+ this._segments.gap = (this.config.segments.colorlist.gap !== 'undefined') ? this.config.segments.colorlist.gap : 1;
+ this._segments.sizeList = [];
+ for (var i = 0; i < this._segments.count; i++) {
+ this._segments.sizeList[i] = this._segments.size;
+ }
+
+ // Use a running total for the size of the segments...
+ var segmentRunningSize = 0;
+ for (var i = 0; i < this._segments.count; i++) {
+ this._segmentAngles[i] = {
+ boundsStart: this.config.position.start_angle + (segmentRunningSize * this._arc.direction),
+ boundsEnd: this.config.position.start_angle + ((segmentRunningSize + this._segments.sizeList[i]) * this._arc.direction),
+ drawStart: this.config.position.start_angle + (segmentRunningSize * this._arc.direction) + (this._segments.gap * this._arc.direction),
+ drawEnd: this.config.position.start_angle + ((segmentRunningSize + this._segments.sizeList[i]) * this._arc.direction) - (this._segments.gap * this._arc.direction),
+ };
+ segmentRunningSize += this._segments.sizeList[i];
+ }
+
+ if (this.dev.debug) console.log('colorstuff - COLORLIST', this._segments, this._segmentAngles);
+
+ // COLORSTOPS
+ } else if (this.config.show.style === 'colorstops') {
+ // Get colorstops, remove outliers and make a key/value store...
+
+ this._segments.colorStops = {};
+ Object.keys(this.config.segments.colorstops.colors).forEach((key) => {
+ if ((key >= this.config.scale.min)
+ && (key <= this.config.scale.max))
+ this._segments.colorStops[key] = this.config.segments.colorstops.colors[key];
+ });
+
+ this._segments.sortedStops = Object.keys(this._segments.colorStops).map((n) => Number(n)).sort((a, b) => a - b);
+
+ // Insert extra stopcolor for max scale if not defined. Otherwise color calculations won't work as expected...
+ if (typeof (this._segments.colorStops[this.config.scale.max]) === 'undefined') {
+ this._segments.colorStops[this.config.scale.max] = this._segments.colorStops[this._segments.sortedStops[this._segments.sortedStops.length - 1]];
+ this._segments.sortedStops = Object.keys(this._segments.colorStops).map((n) => Number(n)).sort((a, b) => a - b);
+ }
+
+ this._segments.count = this._segments.sortedStops.length - 1;
+ this._segments.gap = this.config.segments.colorstops.gap !== 'undefined' ? this.config.segments.colorstops.gap : 1;
+
+ // Now depending on the colorstops and min/max values, calculate the size of each segment relative to the total arc size.
+ // First color in the list starts from Min!
+
+ let runningColorStop = this.config.scale.min;
+ const scaleRange = this.config.scale.max - this.config.scale.min;
+ this._segments.sizeList = [];
+ for (var i = 0; i < this._segments.count; i++) {
+ const colorSize = this._segments.sortedStops[i + 1] - runningColorStop;
+ runningColorStop += colorSize;
+ // Calculate fraction [0..1] of colorSize of min/max scale range
+ const fraction = colorSize / scaleRange;
+ const angleSize = fraction * this._arc.size;
+ this._segments.sizeList[i] = angleSize;
+ }
+
+ // Use a running total for the size of the segments...
+ var segmentRunningSize = 0;
+ for (var i = 0; i < this._segments.count; i++) {
+ this._segmentAngles[i] = {
+ boundsStart: this.config.position.start_angle + (segmentRunningSize * this._arc.direction),
+ boundsEnd: this.config.position.start_angle + ((segmentRunningSize + this._segments.sizeList[i]) * this._arc.direction),
+ drawStart: this.config.position.start_angle + (segmentRunningSize * this._arc.direction) + (this._segments.gap * this._arc.direction),
+ drawEnd: this.config.position.start_angle + ((segmentRunningSize + this._segments.sizeList[i]) * this._arc.direction) - (this._segments.gap * this._arc.direction),
+ };
+ segmentRunningSize += this._segments.sizeList[i];
+ if (this.dev.debug) console.log('colorstuff - COLORSTOPS++ segments', segmentRunningSize, this._segmentAngles[i]);
+ }
+
+ if (this.dev.debug) console.log('colorstuff - COLORSTOPS++', this._segments, this._segmentAngles, this._arc.direction, this._segments.count);
+
+ // SIMPLEGRADIENT
+ } else if (this.config.show.style === 'simplegradient') {
+ }
+
+ // Just dump to console for verification. Nothing is used yet of the new calculation method...
+
+ if (this.config.isScale) {
+ this._stateValue = this.config.scale.max;
+ // this.config.show.scale = false;
+ } else {
+ // Nope. I'm the main arc. Check if a scale is defined and clone myself with some options...
+ if (this.config.show.scale) {
+ const scaleConfig = Merge.mergeDeep(this.config);
+ scaleConfig.id += '-scale';
+
+ // Cloning done. Now set specific scale options.
+ scaleConfig.show.scale = false;
+ scaleConfig.isScale = true;
+ scaleConfig.position.width = this.config.scale.width;
+ scaleConfig.position.radius = this.config.position.radius - (this.config.position.width / 2) + (scaleConfig.position.width / 2) + (this.config.scale.offset);
+ scaleConfig.position.radius_x = ((this.config.position.radius_x || this.config.position.radius)) - (this.config.position.width / 2) + (scaleConfig.position.width / 2) + (this.config.scale.offset);
+ scaleConfig.position.radius_y = ((this.config.position.radius_y || this.config.position.radius)) - (this.config.position.width / 2) + (scaleConfig.position.width / 2) + (this.config.scale.offset);
+
+ this._segmentedArcScale = new SegmentedArcTool(this, scaleConfig, argPos);
+ } else {
+ this._segmentedArcScale = null;
+ }
+ }
+
+ // testing. use below two lines and sckip the calculation of the segmentAngles. Those are done above with different calculation...
+ this.skipOriginal = ((this.config.show.style === 'colorstops') || (this.config.show.style === 'colorlist'));
+
+ // Set scale to new value. Never changes of course!!
+ if (this.skipOriginal) {
+ if (this.config.isScale) this._stateValuePrev = this._stateValue;
+ this._initialDraw = false;
+ }
+
+ this._arc.parts = Math.floor(this._arc.size / Math.abs(this.config.segments.dash));
+ this._arc.partsPartialSize = this._arc.size - (this._arc.parts * this.config.segments.dash);
+
+ if (this.skipOriginal) {
+ this._arc.parts = this._segmentAngles.length;
+ this._arc.partsPartialSize = 0;
+ } else {
+ for (var i = 0; i < this._arc.parts; i++) {
+ this._segmentAngles[i] = {
+ boundsStart: this.config.position.start_angle + (i * this.config.segments.dash * this._arc.direction),
+ boundsEnd: this.config.position.start_angle + ((i + 1) * this.config.segments.dash * this._arc.direction),
+ drawStart: this.config.position.start_angle + (i * this.config.segments.dash * this._arc.direction) + (this.config.segments.gap * this._arc.direction),
+ drawEnd: this.config.position.start_angle + ((i + 1) * this.config.segments.dash * this._arc.direction) - (this.config.segments.gap * this._arc.direction),
+ };
+ }
+ if (this._arc.partsPartialSize > 0) {
+ this._segmentAngles[i] = {
+ boundsStart: this.config.position.start_angle + (i * this.config.segments.dash * this._arc.direction),
+ boundsEnd: this.config.position.start_angle + ((i + 0) * this.config.segments.dash * this._arc.direction)
+ + (this._arc.partsPartialSize * this._arc.direction),
+
+ drawStart: this.config.position.start_angle + (i * this.config.segments.dash * this._arc.direction) + (this.config.segments.gap * this._arc.direction),
+ drawEnd: this.config.position.start_angle + ((i + 0) * this.config.segments.dash * this._arc.direction)
+ + (this._arc.partsPartialSize * this._arc.direction) - (this.config.segments.gap * this._arc.direction),
+ };
+ }
+ }
+
+ this.starttime = null;
+
+ if (this.dev.debug) console.log('SegmentedArcTool constructor coords, dimensions', this.coords, this.dimensions, this.svg, this.config);
+ if (this.dev.debug) console.log('SegmentedArcTool - init', this.toolId, this.config.isScale, this._segmentAngles);
+
+ if (this.dev.performance) console.timeEnd(`--> ${this.toolId} PERFORMANCE SegmentedArcTool::constructor`);
+ }
+
+ // SegmentedArcTool::objectId
+ get objectId() {
+ return this.toolId;
+ }
+
+ // SegmentedArcTool::value
+ set value(state) {
+ if (this.dev.debug) console.log('SegmentedArcTool - set value IN');
+
+ if (this.config.isScale) return false;
+
+ if (this._stateValue === state) return false;
+
+ const changed = super.value = state;
+
+ return changed;
+ }
+
+ // SegmentedArcTool::firstUpdated
+ // Me is updated. Get arc id for animations...
+ firstUpdated(changedProperties) {
+ if (this.dev.debug) console.log('SegmentedArcTool - firstUpdated IN with _arcId/id', this._arcId, this.toolId, this.config.isScale);
+ this._arcId = this._card.shadowRoot.getElementById('arc-'.concat(this.toolId));
+
+ this._firstUpdatedCalled = true;
+
+ // Just a try.
+ //
+ // was this a bug. The scale was never called with updated. Hence always no arcId...
+ this._segmentedArcScale?.firstUpdated(changedProperties);
+
+ if (this.skipOriginal) {
+ if (this.dev.debug) console.log('RENDERNEW - firstUpdated IN with _arcId/id/isScale/scale/connected', this._arcId, this.toolId, this.config.isScale, this._segmentedArcScale, this._card.connected);
+ if (!this.config.isScale) this._stateValuePrev = null;
+ this._initialDraw = true;
+ this._card.requestUpdate();
+ }
+ }
+
+ // SegmentedArcTool::updated
+
+ // eslint-disable-next-line no-unused-vars
+ updated(changedProperties) {
+ if (this.dev.debug) console.log('SegmentedArcTool - updated IN');
+ }
+
+ // SegmentedArcTool::render
+
+ render() {
+ if (this.dev.debug) console.log('SegmentedArcTool RENDERNEW - Render IN');
+ return svg`
+
+
+ ${this._renderSegments()}
+
+ ${this._renderScale()}
+
+ `;
+ }
+
+ _renderScale() {
+ if (this._segmentedArcScale) return this._segmentedArcScale.render();
+ }
+
+ _renderSegments() {
+ // migrate to new solution to draw segmented arc...
+
+ if (this.skipOriginal) {
+ // Here we can rebuild all needed. Much will be the same I guess...
+
+ let arcEnd;
+ let arcEndPrev;
+ const arcWidth = this.svg.width;
+ const arcRadiusX = this.svg.radiusX;
+ const arcRadiusY = this.svg.radiusY;
+
+ let d;
+
+ if (this.dev.debug) console.log('RENDERNEW - IN _arcId, firstUpdatedCalled', this._arcId, this._firstUpdatedCalled);
+ // calculate real end angle depending on value set in object and min/max scale
+ const val = Utils.calculateValueBetween(this.config.scale.min, this.config.scale.max, this._stateValue);
+ const valPrev = Utils.calculateValueBetween(this.config.scale.min, this.config.scale.max, this._stateValuePrev);
+ if (this.dev.debug) if (!this._stateValuePrev) console.log('*****UNDEFINED', this._stateValue, this._stateValuePrev, valPrev);
+ if (val !== valPrev) if (this.dev.debug) console.log('RENDERNEW _renderSegments diff value old new', this.toolId, valPrev, val);
+
+ arcEnd = (val * this._arc.size * this._arc.direction) + this.config.position.start_angle;
+ arcEndPrev = (valPrev * this._arc.size * this._arc.direction) + this.config.position.start_angle;
+
+ const svgItems = [];
+
+ // NO background needed for drawing scale...
+ if (!this.config.isScale) {
+ for (let k = 0; k < this._segmentAngles.length; k++) {
+ d = this.buildArcPath(
+ this._segmentAngles[k].drawStart,
+ this._segmentAngles[k].drawEnd,
+ this._arc.clockwise,
+ this.svg.radiusX,
+ this.svg.radiusY,
+ this.svg.width,
+ );
+
+ svgItems.push(svg` `);
+ }
+ }
+
+ // Check if arcId does exist
+ if (this._firstUpdatedCalled) {
+ // if ((this._arcId)) {
+ if (this.dev.debug) console.log('RENDERNEW _arcId DOES exist', this._arcId, this.toolId, this._firstUpdatedCalled);
+
+ // Render current from cache
+ this._cache.forEach((item, index) => {
+ d = item;
+
+ // extra, set color from colorlist as a test
+ if (this.config.isScale) {
+ let fill = this.config.color;
+ if (this.config.show.style === 'colorlist') {
+ fill = this.config.segments.colorlist.colors[index];
+ }
+ if (this.config.show.style === 'colorstops') {
+ fill = this._segments.colorStops[this._segments.sortedStops[index]];
+ // stroke = this.config.segments.colorstops.stroke ? this._segments.colorStops[this._segments.sortedStops[index]] : '';
+ }
+
+ if (!this.styles.foreground[index]) {
+ this.styles.foreground[index] = Merge.mergeDeep(this.config.styles.foreground);
+ }
+
+ this.styles.foreground[index].fill = fill;
+ // this.styles.foreground[index]['stroke'] = stroke;
+ }
+
+ svgItems.push(svg` `);
+ });
+
+ const tween = {};
+
+ // eslint-disable-next-line no-inner-declarations
+ function animateSegmentsNEW(timestamp, thisTool) {
+ // eslint-disable-next-line no-plusplus
+ const easeOut = (progress) => --progress ** 5 + 1;
+
+ let frameSegment;
+ let runningSegment;
+
+ var timestamp = timestamp || new Date().getTime();
+ if (!tween.startTime) {
+ tween.startTime = timestamp;
+ tween.runningAngle = tween.fromAngle;
+ }
+
+ if (thisTool.debug) console.log('RENDERNEW - in animateSegmentsNEW', thisTool.toolId, tween);
+
+ const runtime = timestamp - tween.startTime;
+ tween.progress = Math.min(runtime / tween.duration, 1);
+ tween.progress = easeOut(tween.progress);
+
+ const increase = ((thisTool._arc.clockwise)
+ ? (tween.toAngle > tween.fromAngle) : (tween.fromAngle > tween.toAngle));
+
+ // Calculate where the animation angle should be now in this animation frame: angle and segment.
+ tween.frameAngle = tween.fromAngle + ((tween.toAngle - tween.fromAngle) * tween.progress);
+ frameSegment = thisTool._segmentAngles.findIndex((currentValue, index) => (thisTool._arc.clockwise
+ ? ((tween.frameAngle <= currentValue.boundsEnd) && (tween.frameAngle >= currentValue.boundsStart))
+ : ((tween.frameAngle <= currentValue.boundsStart) && (tween.frameAngle >= currentValue.boundsEnd))));
+
+ if (frameSegment === -1) {
+ /* if (thisTool.debug) */ console.log('RENDERNEW animateSegments frameAngle not found', tween, thisTool._segmentAngles);
+ console.log('config', thisTool.config);
+ }
+
+ // Check where we actually are now. This might be in a different segment...
+ runningSegment = thisTool._segmentAngles.findIndex((currentValue, index) => (thisTool._arc.clockwise
+ ? ((tween.runningAngle <= currentValue.boundsEnd) && (tween.runningAngle >= currentValue.boundsStart))
+ : ((tween.runningAngle <= currentValue.boundsStart) && (tween.runningAngle >= currentValue.boundsEnd))));
+
+ // Weird stuff. runningSegment is sometimes -1. Ie not FOUND !! WTF??
+ // if (runningSegment == -1) runningSegment = 0;
+
+ // Do render segments until the animation angle is at the requested animation frame angle.
+ do {
+ const aniStartAngle = thisTool._segmentAngles[runningSegment].drawStart;
+ var runningSegmentAngle = thisTool._arc.clockwise
+ ? Math.min(thisTool._segmentAngles[runningSegment].boundsEnd, tween.frameAngle)
+ : Math.max(thisTool._segmentAngles[runningSegment].boundsEnd, tween.frameAngle);
+ const aniEndAngle = thisTool._arc.clockwise
+ ? Math.min(thisTool._segmentAngles[runningSegment].drawEnd, tween.frameAngle)
+ : Math.max(thisTool._segmentAngles[runningSegment].drawEnd, tween.frameAngle);
+ // First phase. Just draw and ignore segments...
+ d = thisTool.buildArcPath(aniStartAngle, aniEndAngle, thisTool._arc.clockwise, arcRadiusX, arcRadiusY, arcWidth);
+
+ if (!thisTool.myarc) thisTool.myarc = {};
+ if (!thisTool.as) thisTool.as = {};
+
+ let as;
+ const myarc = 'arc-segment-'.concat(thisTool.toolId).concat('-').concat(runningSegment);
+ // as = thisTool._card.shadowRoot.getElementById(myarc);
+ if (!thisTool.as[runningSegment])
+ thisTool.as[runningSegment] = thisTool._card.shadowRoot.getElementById(myarc);
+ as = thisTool.as[runningSegment];
+ // Extra. Remember id's and references
+ // Quick hack...
+ thisTool.myarc[runningSegment] = myarc;
+ // thisTool.as[runningSegment] = as;
+
+ if (as) {
+ // var e = as.getAttribute("d");
+ as.setAttribute('d', d);
+
+ // We also have to set the style fill if the color stops and gradients are implemented
+ // As we're using styles, attributes won't work. Must use as.style.fill = 'calculated color'
+ // #TODO
+ // Can't use gradients probably because of custom path. Conic-gradient would be fine.
+ //
+ // First try...
+ if (thisTool.config.show.style === 'colorlist') {
+ as.style.fill = thisTool.config.segments.colorlist.colors[runningSegment];
+ thisTool.styles.foreground[runningSegment].fill = thisTool.config.segments.colorlist.colors[runningSegment];
+ }
+ // #WIP
+ // Testing 'lastcolor'
+ if (thisTool.config.show.lastcolor) {
+ var fill;
+
+ const boundsStart = thisTool._arc.clockwise
+ ? (thisTool._segmentAngles[runningSegment].drawStart)
+ : (thisTool._segmentAngles[runningSegment].drawEnd);
+ const boundsEnd = thisTool._arc.clockwise
+ ? (thisTool._segmentAngles[runningSegment].drawEnd)
+ : (thisTool._segmentAngles[runningSegment].drawStart);
+ const value = Math.min(Math.max(0, (runningSegmentAngle - boundsStart) / (boundsEnd - boundsStart)), 1);
+ // 2022.07.03 Fixing lastcolor for true stop
+ if (thisTool.config.show.style === 'colorstops') {
+ fill = Colors.getGradientValue(
+ thisTool._segments.colorStops[thisTool._segments.sortedStops[runningSegment]],
+ thisTool._segments.colorStops[thisTool._segments.sortedStops[runningSegment]],
+ value,
+ );
+ } else {
+ // 2022.07.12 Fix bug as this is no colorstops, but a colorlist!!!!
+ if (thisTool.config.show.style === 'colorlist') {
+ fill = thisTool.config.segments.colorlist.colors[runningSegment];
+ }
+ }
+
+ thisTool.styles.foreground[0].fill = fill;
+ thisTool.as[0].style.fill = fill;
+
+ if (runningSegment > 0) {
+ for (let j = runningSegment; j >= 0; j--) { // +1
+ if (thisTool.styles.foreground[j].fill !== fill) {
+ thisTool.styles.foreground[j].fill = fill;
+ thisTool.as[j].style.fill = fill;
+ }
+ thisTool.styles.foreground[j].fill = fill;
+ thisTool.as[j].style.fill = fill;
+ }
+ } else {
+ }
+ }
+ }
+ thisTool._cache[runningSegment] = d;
+
+ // If at end of animation, don't do the add to force going to next segment
+ if (tween.frameAngle !== runningSegmentAngle) {
+ runningSegmentAngle += (0.000001 * thisTool._arc.direction);
+ }
+
+ var runningSegmentPrev = runningSegment;
+ runningSegment = thisTool._segmentAngles.findIndex((currentValue, index) => (thisTool._arc.clockwise
+ ? ((runningSegmentAngle <= currentValue.boundsEnd) && (runningSegmentAngle >= currentValue.boundsStart))
+ : ((runningSegmentAngle <= currentValue.boundsStart) && (runningSegmentAngle >= currentValue.boundsEnd))));
+
+ if (!increase) {
+ if (runningSegmentPrev !== runningSegment) {
+ if (thisTool.debug) console.log('RENDERNEW movit - remove path', thisTool.toolId, runningSegmentPrev);
+ if (thisTool._arc.clockwise) {
+ as.removeAttribute('d');
+ thisTool._cache[runningSegmentPrev] = null;
+ } else {
+ as.removeAttribute('d');
+ thisTool._cache[runningSegmentPrev] = null;
+ }
+ }
+ }
+ tween.runningAngle = runningSegmentAngle;
+ if (thisTool.debug) console.log('RENDERNEW - animation loop tween', thisTool.toolId, tween, runningSegment, runningSegmentPrev);
+ } while ((tween.runningAngle !== tween.frameAngle) /* && (runningSegment == runningSegmentPrev) */);
+
+ // NTS @ 2020.10.14
+ // In a fast paced animation - say 10msec - multiple segments should be drawn,
+ // while tween.progress already has the value of 1.
+ // This means only the first segment is drawn - due to the "&& (runningSegment == runningSegmentPrev)" test above.
+ // To fix this:
+ // - either remove that test (why was it there????)... Or
+ // - add the line "|| (runningSegment != runningSegmentPrev)" to the if() below to make sure another animation frame is requested
+ // although tween.progress == 1.
+ if ((tween.progress !== 1) /* || (runningSegment != runningSegmentPrev) */) {
+ // eslint-disable-next-line no-undef
+ thisTool.rAFid = requestAnimationFrame((timestamp) => {
+ animateSegmentsNEW(timestamp, thisTool);
+ });
+ } else {
+ tween.startTime = null;
+ if (thisTool.debug) console.log('RENDERNEW - animation loop ENDING tween', thisTool.toolId, tween, runningSegment, runningSegmentPrev);
+ }
+ } // function animateSegmentsNEW
+
+ const mySelf = this;
+ // 2021.10.31
+ // Edge case where brightness percentage is set to undefined (attribute is gone) if light is set to off.
+ // Now if light is switched on again, the brightness is set to old value, and val and valPrev are the same again, so NO drawing!!!!!
+ //
+ // Remove test for val/valPrev...
+
+ // Check if values changed and we should animate to another target then previously rendered
+ if (/* (val != valPrev) && */ (this._card.connected === true) && (this._renderTo !== this._stateValue)) {
+ // if ( (val != valPrev) && (this._card.connected == true) && (this._renderTo != this._stateValue)) {
+ this._renderTo = this._stateValue;
+ // if (this.dev.debug) console.log('RENDERNEW val != valPrev', val, valPrev, 'prev/end/cur', arcEndPrev, arcEnd, arcCur);
+
+ // If previous animation active, cancel this one before starting a new one...
+ if (this.rAFid) {
+ // if (this.dev.debug) console.log('RENDERNEW canceling rAFid', this._card.cardId, this.toolId, 'rAFid', this.rAFid);
+ // eslint-disable-next-line no-undef
+ cancelAnimationFrame(this.rAFid);
+ }
+
+ // Start new animation with calculated settings...
+ // counter var not defined???
+ // if (this.dev.debug) console.log('starting animationframe timer...', this._card.cardId, this.toolId, counter);
+ tween.fromAngle = arcEndPrev;
+ tween.toAngle = arcEnd;
+ tween.runningAngle = arcEndPrev;
+
+ // @2021.10.31
+ // Handle edge case where - for some reason - arcEnd and arcEndPrev are equal.
+ // Do NOT render anything in this case to prevent errors...
+
+ // The check is removed temporarily. Brightness is again not shown for light. Still the same problem...
+
+ // eslint-disable-next-line no-constant-condition
+ if (true || !(arcEnd === arcEndPrev)) {
+ // Render like an idiot the first time. Performs MUCH better @first load then having a zillion animations...
+ // NOt so heavy on an average PC, but my iPad and iPhone need some more time for this!
+
+ tween.duration = Math.min(Math.max(this._initialDraw ? 100 : 500, this._initialDraw ? 16 : this.config.animation.duration * 1000), 5000);
+ tween.startTime = null;
+ if (this.dev.debug) console.log('RENDERNEW - tween', this.toolId, tween);
+ // this._initialDraw = false;
+ // eslint-disable-next-line no-undef
+ this.rAFid = requestAnimationFrame((timestamp) => {
+ animateSegmentsNEW(timestamp, mySelf);
+ });
+ this._initialDraw = false;
+ }
+ }
+
+ return svg`${svgItems}`;
+ } else {
+ // Initial FIRST draw.
+ // What if we 'abuse' the animation to do this, and we just create empty elements.
+ // Then we don't have to do difficult things.
+ // Just set some values to 0 and 'force' a full animation...
+ //
+ // Hmm. Stuff is not yet rendered, so DOM objects don't exist yet. How can we abuse the
+ // animation function to do the drawing then??
+ // --> Can use firstUpdated perhaps?? That was the first render, then do the first actual draw??
+ //
+
+ if (this.dev.debug) console.log('RENDERNEW _arcId does NOT exist', this._arcId, this.toolId);
+
+ // Create empty elements, so no problem in animation function. All path's exist...
+ // An empty element has a width of 0!
+ for (let i = 0; i < this._segmentAngles.length; i++) {
+ d = this.buildArcPath(
+ this._segmentAngles[i].drawStart,
+ this._segmentAngles[i].drawEnd,
+ this._arc.clockwise,
+ this.svg.radiusX,
+ this.svg.radiusY,
+ this.config.isScale ? this.svg.width : 0,
+ );
+
+ this._cache[i] = d;
+
+ // extra, set color from colorlist as a test
+ let fill = this.config.color;
+ if (this.config.show.style === 'colorlist') {
+ fill = this.config.segments.colorlist.colors[i];
+ }
+ if (this.config.show.style === 'colorstops') {
+ fill = this._segments.colorStops[this._segments.sortedStops[i]];
+ }
+ // style="${styleMap(this.config.styles.foreground)} fill: ${fill};"
+ if (!this.styles.foreground) {
+ this.styles.foreground = {};
+ }
+ if (!this.styles.foreground[i]) {
+ this.styles.foreground[i] = Merge.mergeDeep(this.config.styles.foreground);
+ }
+ this.styles.foreground[i].fill = fill;
+
+ // #WIP
+ // Testing 'lastcolor'
+ if (this.config.show.lastcolor) {
+ if (i > 0) {
+ for (let j = i - 1; j > 0; j--) {
+ this.styles.foreground[j].fill = fill;
+ }
+ }
+ }
+
+ svgItems.push(svg` `);
+ }
+
+ if (this.dev.debug) console.log('RENDERNEW - svgItems', svgItems, this._firstUpdatedCalled);
+ return svg`${svgItems}`;
+ }
+
+ // END OF NEW METHOD OF RENDERING
+ }
+ }
+
+ polarToCartesian(centerX, centerY, radiusX, radiusY, angleInDegrees) {
+ const angleInRadians = (angleInDegrees - 90) * Math.PI / 180.0;
+
+ return {
+ x: centerX + (radiusX * Math.cos(angleInRadians)),
+ y: centerY + (radiusY * Math.sin(angleInRadians)),
+ };
+ }
+
+ /*
+ *
+ * start = 10, end = 30, clockwise -> size is 20
+ * start = 10, end = 30, anticlockwise -> size is (360 - 20) = 340
+ *
+ *
+ */
+ buildArcPath(argStartAngle, argEndAngle, argClockwise, argRadiusX, argRadiusY, argWidth) {
+ const start = this.polarToCartesian(this.svg.cx, this.svg.cy, argRadiusX, argRadiusY, argEndAngle);
+ const end = this.polarToCartesian(this.svg.cx, this.svg.cy, argRadiusX, argRadiusY, argStartAngle);
+ const largeArcFlag = Math.abs(argEndAngle - argStartAngle) <= 180 ? '0' : '1';
+
+ const sweepFlag = argClockwise ? '0' : '1';
+
+ const cutoutRadiusX = argRadiusX - argWidth;
+ const cutoutRadiusY = argRadiusY - argWidth;
+ const start2 = this.polarToCartesian(this.svg.cx, this.svg.cy, cutoutRadiusX, cutoutRadiusY, argEndAngle);
+ const end2 = this.polarToCartesian(this.svg.cx, this.svg.cy, cutoutRadiusX, cutoutRadiusY, argStartAngle);
+
+ const d = [
+ 'M', start.x, start.y,
+ 'A', argRadiusX, argRadiusY, 0, largeArcFlag, sweepFlag, end.x, end.y,
+ 'L', end2.x, end2.y,
+ 'A', cutoutRadiusX, cutoutRadiusY, 0, largeArcFlag, sweepFlag === '0' ? '1' : '0', start2.x, start2.y,
+ 'Z',
+ ].join(' ');
+ return d;
+ }
+} // END of class
diff --git a/src/sparkline-barchart-tool.js b/src/sparkline-barchart-tool.js
new file mode 100644
index 0000000..0a1472b
--- /dev/null
+++ b/src/sparkline-barchart-tool.js
@@ -0,0 +1,235 @@
+import { svg } from 'lit-element';
+import { classMap } from 'lit-html/directives/class-map.js';
+import { styleMap } from 'lit-html/directives/style-map.js';
+
+import Merge from './merge';
+import BaseTool from './base-tool';
+import Utils from './utils';
+
+/** ****************************************************************************
+ * SparklineBarChartTool class
+ *
+ * Summary.
+ *
+ */
+export default class SparklineBarChartTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_BARCHART_CONFIG = {
+ position: {
+ cx: 50,
+ cy: 50,
+ height: 25,
+ width: 25,
+ margin: 0.5,
+ orientation: 'vertical',
+ },
+ hours: 24,
+ barhours: 1,
+ color: 'var(--primary-color)',
+ classes: {
+ tool: {
+ 'sak-barchart': true,
+ hover: true,
+ },
+ bar: {
+ },
+ line: {
+ 'sak-barchart__line': true,
+ hover: true,
+ },
+ },
+ styles: {
+ tool: {
+ },
+ line: {
+ },
+ bar: {
+ },
+ },
+ colorstops: [],
+ show: { style: 'fixedcolor' },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_BARCHART_CONFIG, argConfig), argPos);
+
+ this.svg.margin = Utils.calculateSvgDimension(this.config.position.margin);
+ const theWidth = (this.config.position.orientation === 'vertical') ? this.svg.width : this.svg.height;
+
+ this.svg.barWidth = (theWidth - (((this.config.hours / this.config.barhours) - 1)
+ * this.svg.margin)) / (this.config.hours / this.config.barhours);
+ this._data = [];
+ this._bars = [];
+ this._scale = {};
+ this._needsRendering = false;
+
+ this.classes.bar = {};
+
+ this.styles.tool = {};
+ this.styles.line = {};
+ this.stylesBar = {};
+
+ if (this.dev.debug) console.log('SparkleBarChart constructor coords, dimensions', this.coords, this.dimensions, this.svg, this.config);
+ }
+
+ /** *****************************************************************************
+ * SparklineBarChartTool::computeMinMax()
+ *
+ * Summary.
+ * Compute min/max values of bars to scale them to the maximum amount.
+ *
+ */
+ computeMinMax() {
+ let min = this._series[0]; let
+ max = this._series[0];
+
+ for (let i = 1, len = this._series.length; i < len; i++) {
+ const v = this._series[i];
+ min = (v < min) ? v : min;
+ max = (v > max) ? v : max;
+ }
+ this._scale.min = min;
+ this._scale.max = max;
+ this._scale.size = (max - min);
+
+ // 2020.11.05
+ // Add 5% to the size of the scale and adjust the minimum value displayed.
+ // So every bar is displayed, instead of the min value having a bar length of zero!
+ this._scale.size = (max - min) * 1.05;
+ this._scale.min = max - this._scale.size;
+ }
+
+ /** *****************************************************************************
+ * SparklineBarChartTool::set series
+ *
+ * Summary.
+ * Sets the timeseries for the barchart tool. Is an array of states.
+ * If this is historical data, the caller has taken the time to create this.
+ * This tool only displays the result...
+ *
+ */
+ set data(states) {
+ this._series = Object.assign(states);
+ this.computeBars();
+ this._needsRendering = true;
+ }
+
+ set series(states) {
+ this._series = Object.assign(states);
+ this.computeBars();
+ this._needsRendering = true;
+ }
+
+ hasSeries() {
+ return this.defaultEntityIndex();
+ }
+
+ /** *****************************************************************************
+ * SparklineBarChartTool::computeBars()
+ *
+ * Summary.
+ * Compute start and end of bars for easy rendering.
+ *
+ */
+ computeBars({ _bars } = this) {
+ this.computeMinMax();
+
+ if (this.config.show.style === 'minmaxgradient') {
+ this.colorStopsMinMax = {};
+ this.colorStopsMinMax = {
+ [this._scale.min.toString()]: this.config.minmaxgradient.colors.min,
+ [this._scale.max.toString()]: this.config.minmaxgradient.colors.max,
+ };
+ }
+
+ // VERTICAL
+ if (this.config.position.orientation === 'vertical') {
+ if (this.dev.debug) console.log('bar is vertical');
+ this._series.forEach((item, index) => {
+ if (!_bars[index]) _bars[index] = {};
+ _bars[index].length = (this._scale.size === 0) ? 0 : ((item - this._scale.min) / (this._scale.size)) * this.svg.height;
+ _bars[index].x1 = this.svg.x + this.svg.barWidth / 2 + ((this.svg.barWidth + this.svg.margin) * index);
+ _bars[index].x2 = _bars[index].x1;
+ _bars[index].y1 = this.svg.y + this.svg.height;
+ _bars[index].y2 = _bars[index].y1 - this._bars[index].length;
+ _bars[index].dataLength = this._bars[index].length;
+ });
+ // HORIZONTAL
+ } else if (this.config.position.orientation === 'horizontal') {
+ if (this.dev.debug) console.log('bar is horizontal');
+ this._data.forEach((item, index) => {
+ if (!_bars[index]) _bars[index] = {};
+ // if (!item || isNaN(item)) item = this._scale.min;
+ _bars[index].length = (this._scale.size === 0) ? 0 : ((item - this._scale.min) / (this._scale.size)) * this.svg.width;
+ _bars[index].y1 = this.svg.y + this.svg.barWidth / 2 + ((this.svg.barWidth + this.svg.margin) * index);
+ _bars[index].y2 = _bars[index].y1;
+ _bars[index].x1 = this.svg.x;
+ _bars[index].x2 = _bars[index].x1 + this._bars[index].length;
+ _bars[index].dataLength = this._bars[index].length;
+ });
+ } else if (this.dev.debug) console.log('SparklineBarChartTool - unknown barchart orientation (horizontal or vertical)');
+ }
+
+ /** *****************************************************************************
+ * SparklineBarChartTool::_renderBars()
+ *
+ * Summary.
+ * Render all the bars. Number of bars depend on hours and barhours settings.
+ *
+ */
+ // _renderBars({ _bars } = this) {
+ _renderBars() {
+ const svgItems = [];
+
+ if (this._bars.length === 0) return;
+
+ if (this.dev.debug) console.log('_renderBars IN', this.toolId);
+
+ this._bars.forEach((item, index) => {
+ if (this.dev.debug) console.log('_renderBars - bars', item, index);
+
+ const stroke = this.getColorFromState(this._series[index]);
+
+ if (!this.stylesBar[index])
+ this.stylesBar[index] = { ...this.config.styles.bar };
+
+ // NOTE @ 2021.10.27
+ // Lijkt dat this.classes niet gevuld wordt. geen merge enzo. is dat een bug?
+ // Nu tijdelijk opgelost door this.config te gebruiken, maar hoort niet natuurlijk als je kijkt
+ // naar de andere tools...
+
+ // Safeguard...
+ if (!(this._bars[index].y2)) console.log('sparklebarchart y2 invalid', this._bars[index]);
+ svgItems.push(svg`
+
+ `);
+ });
+ if (this.dev.debug) console.log('_renderBars OUT', this.toolId);
+
+ return svg`${svgItems}`;
+ }
+
+ /** *****************************************************************************
+ * SparklineBarChartTool::render()
+ *
+ * Summary.
+ * The actual render() function called by the card for each tool.
+ *
+ */
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderBars()}
+
+ `;
+ }
+}
diff --git a/src/switch-tool.js b/src/switch-tool.js
new file mode 100644
index 0000000..768deca
--- /dev/null
+++ b/src/switch-tool.js
@@ -0,0 +1,251 @@
+import { svg } from 'lit-element';
+import { classMap } from 'lit-html/directives/class-map.js';
+import { styleMap } from 'lit-html/directives/style-map.js';
+
+import Merge from './merge';
+import Utils from './utils';
+import BaseTool from './base-tool';
+
+/** ****************************************************************************
+ * SwitchTool class
+ *
+ * Summary.
+ *
+ *
+ * NTS:
+ * - .mdc-switch__native-control uses:
+ * - width: 68px, 17em
+ * - height: 48px, 12em
+ * - and if checked (.mdc-switch--checked):
+ * - transform: translateX(-20px)
+ *
+ * .mdc-switch.mdc-switch--checked .mdc-switch__thumb {
+ * background-color: var(--switch-checked-button-color);
+ * border-color: var(--switch-checked-button-color);
+ *
+ */
+
+export default class SwitchTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_SWITCH_CONFIG = {
+ position: {
+ cx: 50,
+ cy: 50,
+ orientation: 'horizontal',
+ track: {
+ width: 16,
+ height: 7,
+ radius: 3.5,
+ },
+ thumb: {
+ width: 9,
+ height: 9,
+ radius: 4.5,
+ offset: 4.5,
+ },
+ },
+ classes: {
+ tool: {
+ 'sak-switch': true,
+ hover: true,
+ },
+ track: {
+ 'sak-switch__track': true,
+ },
+ thumb: {
+ 'sak-switch__thumb': true,
+ },
+ },
+ styles: {
+ tool: {
+ },
+ track: {
+ },
+ thumb: {
+ },
+ },
+ };
+
+ const HORIZONTAL_SWITCH_CONFIG = {
+ animations: [
+ {
+ state: 'on',
+ id: 1,
+ styles: {
+ track: {
+ fill: 'var(--switch-checked-track-color)',
+ 'pointer-events': 'auto',
+ },
+ thumb: {
+ fill: 'var(--switch-checked-button-color)',
+ transform: 'translateX(4.5em)',
+ 'pointer-events': 'auto',
+ },
+ },
+ },
+ {
+ state: 'off',
+ id: 0,
+ styles: {
+ track: {
+ fill: 'var(--switch-unchecked-track-color)',
+ 'pointer-events': 'auto',
+ },
+ thumb: {
+ fill: 'var(--switch-unchecked-button-color)',
+ transform: 'translateX(-4.5em)',
+ 'pointer-events': 'auto',
+ },
+ },
+ },
+ ],
+ };
+
+ const VERTICAL_SWITCH_CONFIG = {
+ animations: [
+ {
+ state: 'on',
+ id: 1,
+ styles: {
+ track: {
+ fill: 'var(--switch-checked-track-color)',
+ 'pointer-events': 'auto',
+ },
+ thumb: {
+ fill: 'var(--switch-checked-button-color)',
+ transform: 'translateY(-4.5em)',
+ 'pointer-events': 'auto',
+ },
+ },
+ },
+ {
+ state: 'off',
+ id: 0,
+ styles: {
+ track: {
+ fill: 'var(--switch-unchecked-track-color)',
+ 'pointer-events': 'auto',
+ },
+ thumb: {
+ fill: 'var(--switch-unchecked-button-color)',
+ transform: 'translateY(4.5em)',
+ 'pointer-events': 'auto',
+ },
+ },
+ },
+ ],
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_SWITCH_CONFIG, argConfig), argPos);
+
+ if (!['horizontal', 'vertical'].includes(this.config.position.orientation))
+ throw Error('SwitchTool::constructor - invalid orientation [vertical, horizontal] = ', this.config.position.orientation);
+
+ this.svg.track = {};
+ this.svg.track.radius = Utils.calculateSvgDimension(this.config.position.track.radius);
+
+ this.svg.thumb = {};
+ this.svg.thumb.radius = Utils.calculateSvgDimension(this.config.position.thumb.radius);
+ this.svg.thumb.offset = Utils.calculateSvgDimension(this.config.position.thumb.offset);
+
+ switch (this.config.position.orientation) {
+ // eslint-disable-next-line default-case-last
+ default:
+ case 'horizontal':
+ this.config = Merge.mergeDeep(DEFAULT_SWITCH_CONFIG, HORIZONTAL_SWITCH_CONFIG, argConfig);
+
+ this.svg.track.width = Utils.calculateSvgDimension(this.config.position.track.width);
+ this.svg.track.height = Utils.calculateSvgDimension(this.config.position.track.height);
+ this.svg.thumb.width = Utils.calculateSvgDimension(this.config.position.thumb.width);
+ this.svg.thumb.height = Utils.calculateSvgDimension(this.config.position.thumb.height);
+
+ this.svg.track.x1 = this.svg.cx - this.svg.track.width / 2;
+ this.svg.track.y1 = this.svg.cy - this.svg.track.height / 2;
+
+ this.svg.thumb.x1 = this.svg.cx - this.svg.thumb.width / 2;
+ this.svg.thumb.y1 = this.svg.cy - this.svg.thumb.height / 2;
+ break;
+
+ case 'vertical':
+ this.config = Merge.mergeDeep(DEFAULT_SWITCH_CONFIG, VERTICAL_SWITCH_CONFIG, argConfig);
+
+ this.svg.track.width = Utils.calculateSvgDimension(this.config.position.track.height);
+ this.svg.track.height = Utils.calculateSvgDimension(this.config.position.track.width);
+ this.svg.thumb.width = Utils.calculateSvgDimension(this.config.position.thumb.height);
+ this.svg.thumb.height = Utils.calculateSvgDimension(this.config.position.thumb.width);
+
+ this.svg.track.x1 = this.svg.cx - this.svg.track.width / 2;
+ this.svg.track.y1 = this.svg.cy - this.svg.track.height / 2;
+
+ this.svg.thumb.x1 = this.svg.cx - this.svg.thumb.width / 2;
+ this.svg.thumb.y1 = this.svg.cy - this.svg.thumb.height / 2;
+ break;
+ }
+
+ this.classes.track = {};
+ this.classes.thumb = {};
+
+ this.styles.track = {};
+ this.styles.thumb = {};
+ if (this.dev.debug) console.log('SwitchTool constructor config, svg', this.toolId, this.config, this.svg);
+ }
+
+ /** *****************************************************************************
+ * SwitchTool::value()
+ *
+ * Summary.
+ * Receive new state data for the entity this switch is linked to. Called from set hass;
+ *
+ */
+ set value(state) {
+ super.value = state;
+ }
+
+ /**
+ * SwitchTool::_renderSwitch()
+ *
+ * Summary.
+ * Renders the switch using precalculated coordinates and dimensions.
+ * Only the runtime style is calculated before rendering the switch
+ *
+ */
+
+ _renderSwitch() {
+ this.MergeAnimationClassIfChanged();
+ // this.MergeColorFromState(this.styles);
+ this.MergeAnimationStyleIfChanged(this.styles);
+ // this.MergeAnimationStyleIfChanged(this.styles.thumb);
+
+ return svg`
+
+
+
+
+ `;
+ }
+
+ /** *****************************************************************************
+ * SwitchTool::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ * https://codepen.io/joegaffey/pen/vrVZaN
+ *
+ */
+
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderSwitch()}
+
+ `;
+ }
+} // END of class
diff --git a/src/templates.js b/src/templates.js
new file mode 100644
index 0000000..8eb2a1b
--- /dev/null
+++ b/src/templates.js
@@ -0,0 +1,172 @@
+/** ****************************************************************************
+ * Templates class
+ *
+ * Summary.
+ *
+ */
+
+export default class Templates {
+ /** ****************************************************************************
+ * Templates::replaceVariables()
+ *
+ * Summary.
+ * A toolset defines a template. This template is found and passed as argToolsetTemplate.
+ * This is actually a set of tools, nothing else...
+ * Also passed is the list of variables that should be replaced:
+ * - The list defined in the toolset
+ * - The defaults defined in the template itself, which are defined in the argToolsetTemplate
+ *
+ */
+
+ static replaceVariables3(argVariables, argTemplate) {
+ // If no variables specified, return template contents, not the first object, but the contents!
+ // ie template.toolset or template.colorstops. The toolset and colorstops objects are removed...
+ //
+ // If not, one gets toolsets[1].toolset.position iso toolsets[1].position.
+ //
+ if (!argVariables && !argTemplate.template.defaults) {
+ return argTemplate[argTemplate.template.type];
+ }
+ let variableArray = argVariables?.slice(0) ?? [];
+
+ // Merge given variables and defaults...
+ if (argTemplate.template.defaults) {
+ variableArray = variableArray.concat(argTemplate.template.defaults);
+ }
+
+ let jsonConfig = JSON.stringify(argTemplate[argTemplate.template.type]);
+ variableArray.forEach((variable) => {
+ const key = Object.keys(variable)[0];
+ const value = Object.values(variable)[0];
+ if (typeof value === 'number' || typeof value === 'boolean') {
+ const rxp2 = new RegExp(`"\\[\\[${key}\\]\\]"`, 'gm');
+ jsonConfig = jsonConfig.replace(rxp2, value);
+ }
+ if (typeof value === 'object') {
+ const rxp2 = new RegExp(`"\\[\\[${key}\\]\\]"`, 'gm');
+ const valueString = JSON.stringify(value);
+ jsonConfig = jsonConfig.replace(rxp2, valueString);
+ } else {
+ const rxp = new RegExp(`\\[\\[${key}\\]\\]`, 'gm');
+ jsonConfig = jsonConfig.replace(rxp, value);
+ }
+ });
+
+ return (JSON.parse(jsonConfig));
+ }
+
+ static getJsTemplateOrValueConfig(argTool, argValue) {
+ // Check for 'undefined' or 'null'
+ if (!argValue) return argValue;
+
+ // Check for primitive data types
+ if (['number', 'boolean', 'bigint', 'symbol'].includes(typeof argValue)) return argValue;
+
+ // We might have an object.
+ // Beware of the fact that this recursive function overwrites the argValue object,
+ // so clone argValue if this is the tool configuration...
+ if (typeof argValue === 'object') {
+ Object.keys(argValue).forEach((key) => {
+ argValue[key] = Templates.getJsTemplateOrValueConfig(argTool, argValue[key]);
+ });
+ return argValue;
+ }
+
+ // typeof should be a string now.
+ // The string might be a Javascript template surrounded by [[[]]], or just a string.
+ const trimmedValue = argValue.trim();
+ if (trimmedValue.substring(0, 4) === '[[[[' && trimmedValue.slice(-4) === ']]]]') {
+ return Templates.evaluateJsTemplateConfig(argTool, trimmedValue.slice(4, -4));
+ } else {
+ // Just a plain string, return value.
+ return argValue;
+ }
+ }
+
+ static evaluateJsTemplateConfig(argTool, jsTemplate) {
+ try {
+ // eslint-disable-next-line no-new-func
+ return new Function('tool_config', `'use strict'; ${jsTemplate}`).call(
+ this,
+ argTool,
+ );
+ } catch (e) {
+ e.name = 'Sak-evaluateJsTemplateConfig-Error';
+ throw e;
+ }
+ }
+ /** *****************************************************************************
+ * Templates::evaluateJsTemplate()
+ *
+ * Summary.
+ * Runs the JavaScript template.
+ *
+ * The arguments passed to the function are:
+ * - state, state of the current entity
+ * - states, the full array of states provided by hass
+ * - entity, the current entity and its configuration
+ * - user, the currently logged in user
+ * - hass, the hass object...
+ * - tool_config, the YAML configuration of the current tool
+ * - entity_config, the YAML configuration of configured entity in this tool
+ *
+ */
+
+ static evaluateJsTemplate(argTool, state, jsTemplate) {
+ try {
+ // eslint-disable-next-line no-new-func
+ return new Function('state', 'states', 'entity', 'user', 'hass', 'tool_config', 'entity_config', `'use strict'; ${jsTemplate}`).call(
+ this,
+ state,
+ argTool._card._hass.states,
+ argTool.config.hasOwnProperty('entity_index') ? argTool._card.entities[argTool.config.entity_index] : undefined,
+ argTool._card._hass.user,
+ argTool._card._hass,
+ argTool.config,
+ argTool.config.hasOwnProperty('entity_index') ? argTool._card.config.entities[argTool.config.entity_index] : undefined,
+ );
+ } catch (e) {
+ e.name = 'Sak-evaluateJsTemplate-Error';
+ throw e;
+ }
+ }
+
+ /** *****************************************************************************
+ * Templates::getJsTemplateOrValue()
+ *
+ * Summary.
+ *
+ * References:
+ * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures
+ * - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof
+ *
+ */
+
+ static getJsTemplateOrValue(argTool, argState, argValue) {
+ // Check for 'undefined' or 'null'
+ if (!argValue) return argValue;
+
+ // Check for primitive data types
+ if (['number', 'boolean', 'bigint', 'symbol'].includes(typeof argValue)) return argValue;
+
+ // We might have an object.
+ // Beware of the fact that this recursive function overwrites the argValue object,
+ // so clone argValue if this is the tool configuration...
+ if (typeof argValue === 'object') {
+ Object.keys(argValue).forEach((key) => {
+ argValue[key] = Templates.getJsTemplateOrValue(argTool, argState, argValue[key]);
+ });
+ return argValue;
+ }
+
+ // typeof should be a string now.
+ // The string might be a Javascript template surrounded by [[[]]], or just a string.
+ const trimmedValue = argValue.trim();
+ if (trimmedValue.substring(0, 3) === '[[[' && trimmedValue.slice(-3) === ']]]') {
+ return Templates.evaluateJsTemplate(argTool, argState, trimmedValue.slice(3, -3));
+ } else {
+ // Just a plain string, return value.
+ return argValue;
+ }
+ }
+}
diff --git a/src/text-tool.js b/src/text-tool.js
new file mode 100644
index 0000000..d1fbf47
--- /dev/null
+++ b/src/text-tool.js
@@ -0,0 +1,79 @@
+import { svg } from 'lit-element';
+import { classMap } from 'lit-html/directives/class-map.js';
+import { styleMap } from 'lit-html/directives/style-map.js';
+
+import Merge from './merge';
+import BaseTool from './base-tool';
+
+/** ****************************************************************************
+ * TextTool class
+ *
+ * Summary.
+ *
+ */
+
+export default class TextTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_TEXT_CONFIG = {
+ classes: {
+ tool: {
+ 'sak-text': true,
+ },
+ text: {
+ 'sak-text__text': true,
+ hover: false,
+ },
+ },
+ styles: {
+ tool: {
+ },
+ text: {
+ },
+ },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_TEXT_CONFIG, argConfig), argPos);
+
+ this.EnableHoverForInteraction();
+ this.text = this.config.text;
+ this.styles.text = {};
+ if (this.dev.debug) console.log('TextTool constructor coords, dimensions', this.coords, this.dimensions, this.svg, this.config);
+ }
+
+ /** *****************************************************************************
+ * TextTool::_renderText()
+ *
+ * Summary.
+ * Renders the text using precalculated coordinates and dimensions.
+ * Only the runtime style is calculated before rendering the text
+ *
+ */
+
+ _renderText() {
+ this.MergeAnimationClassIfChanged();
+ this.MergeColorFromState(this.styles.text);
+ this.MergeAnimationStyleIfChanged();
+
+ return svg`
+
+ ${this.text}
+
+ `;
+ }
+
+ /** *****************************************************************************
+ * TextTool::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ */
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderText()}
+
+ `;
+ }
+} // END of class
diff --git a/src/toolset.js b/src/toolset.js
new file mode 100644
index 0000000..89262ab
--- /dev/null
+++ b/src/toolset.js
@@ -0,0 +1,345 @@
+import { svg } from 'lit-element';
+
+import { SVG_DEFAULT_DIMENSIONS, SVG_DEFAULT_DIMENSIONS_HALF } from './const';
+import Utils from './utils';
+
+import BadgeTool from './badge-tool';
+import CircleTool from './circle-tool';
+import CircularSliderTool from './circular-slider-tool';
+import EllipseTool from './ellipse-tool';
+import EntityAreaTool from './entity-area-tool';
+import EntityIconTool from './entity-icon-tool';
+import EntityNameTool from './entity-name-tool';
+import EntityStateTool from './entity-state-tool';
+import HorseshoeTool from './horseshoe-tool';
+import LineTool from './line-tool';
+import RangeSliderTool from './range-slider-tool';
+import RectangleTool from './rectangle-tool';
+import RectangleToolEx from './rectangle-ex-tool';
+import RegPolyTool from './regular-polygon-tool';
+import SegmentedArcTool from './segmented-arc-tool';
+import SparklineBarChartTool from './sparkline-barchart-tool';
+import SwitchTool from './switch-tool';
+import TextTool from './text-tool';
+import UserSvgTool from './user-svg-tool';
+
+/** ***************************************************************************
+ * Toolset class
+ *
+ * Summary.
+ *
+ */
+
+export default class Toolset {
+ constructor(argCard, argConfig) {
+ this.toolsetId = Math.random().toString(36).substr(2, 9);
+ this._card = argCard;
+ this.dev = { ...this._card.dev };
+ if (this.dev.performance) console.time(`--> ${this.toolsetId} PERFORMANCE Toolset::constructor`);
+
+ this.config = argConfig;
+ this.tools = [];
+
+ // Get SVG coordinates.
+ this.svg = {};
+ this.svg.cx = Utils.calculateSvgCoordinate(argConfig.position.cx, SVG_DEFAULT_DIMENSIONS_HALF);
+ this.svg.cy = Utils.calculateSvgCoordinate(argConfig.position.cy, SVG_DEFAULT_DIMENSIONS_HALF);
+
+ this.svg.x = (this.svg.cx) - (SVG_DEFAULT_DIMENSIONS_HALF);
+ this.svg.y = (this.svg.cy) - (SVG_DEFAULT_DIMENSIONS_HALF);
+
+ // Group scaling experiment. Calc translate values for SVG using the toolset scale value
+ this.transform = {};
+ this.transform.scale = {};
+ // eslint-disable-next-line no-multi-assign
+ this.transform.scale.x = this.transform.scale.y = 1;
+ this.transform.rotate = {};
+ // eslint-disable-next-line no-multi-assign
+ this.transform.rotate.x = this.transform.rotate.y = 0;
+ this.transform.skew = {};
+ // eslint-disable-next-line no-multi-assign
+ this.transform.skew.x = this.transform.skew.y = 0;
+
+ if (this.config.position.scale) {
+ // eslint-disable-next-line no-multi-assign
+ this.transform.scale.x = this.transform.scale.y = this.config.position.scale;
+ }
+ if (this.config.position.rotate) {
+ // eslint-disable-next-line no-multi-assign
+ this.transform.rotate.x = this.transform.rotate.y = this.config.position.rotate;
+ }
+
+ this.transform.scale.x = this.config.position.scale_x || this.config.position.scale || 1;
+ this.transform.scale.y = this.config.position.scale_y || this.config.position.scale || 1;
+
+ this.transform.rotate.x = this.config.position.rotate_x || this.config.position.rotate || 0;
+ this.transform.rotate.y = this.config.position.rotate_y || this.config.position.rotate || 0;
+
+ if (this.dev.debug) console.log('Toolset::constructor config/svg', this.toolsetId, this.config, this.svg);
+
+ // Create the tools configured in the toolset list.
+ const toolsNew = {
+ area: EntityAreaTool,
+ circslider: CircularSliderTool,
+ badge: BadgeTool,
+ bar: SparklineBarChartTool,
+ circle: CircleTool,
+ ellipse: EllipseTool,
+ horseshoe: HorseshoeTool,
+ icon: EntityIconTool,
+ line: LineTool,
+ name: EntityNameTool,
+ rectangle: RectangleTool,
+ rectex: RectangleToolEx,
+ regpoly: RegPolyTool,
+ segarc: SegmentedArcTool,
+ state: EntityStateTool,
+ slider: RangeSliderTool,
+ switch: SwitchTool,
+ text: TextTool,
+ usersvg: UserSvgTool,
+ };
+
+ this.config.tools.map((toolConfig) => {
+ const newConfig = { ...toolConfig };
+
+ const newPos = {
+ cx: 0 / 100 * SVG_DEFAULT_DIMENSIONS,
+ cy: 0 / 100 * SVG_DEFAULT_DIMENSIONS,
+ scale: this.config.position.scale ? this.config.position.scale : 1,
+ };
+
+ if (this.dev.debug) console.log('Toolset::constructor toolConfig', this.toolsetId, newConfig, newPos);
+
+ if (!toolConfig.disabled) {
+ const newTool = new toolsNew[toolConfig.type](this, newConfig, newPos);
+ // eslint-disable-next-line no-bitwise
+ this._card.entityHistory.needed |= (toolConfig.type === 'bar');
+ this.tools.push({ type: toolConfig.type, index: toolConfig.id, tool: newTool });
+ }
+ return true;
+ });
+
+ if (this.dev.performance) console.timeEnd(`--> ${this.toolsetId} PERFORMANCE Toolset::constructor`);
+ }
+
+ /** *****************************************************************************
+ * Toolset::updateValues()
+ *
+ * Summary.
+ * Called from set hass to update values for tools
+ *
+ */
+
+ // #TODO:
+ // Update only the changed entity_index, not all indexes. Now ALL tools are updated...
+ updateValues() {
+ if (this.dev.performance) console.time(`--> ${this.toolsetId} PERFORMANCE Toolset::updateValues`);
+ if (this.tools) {
+ this.tools.map((item, index) => {
+ // eslint-disable-next-line no-constant-condition
+ if (true || item.type === 'segarc') {
+ if ((item.tool.config.hasOwnProperty('entity_index'))) {
+ if (this.dev.debug) console.log('Toolset::updateValues', item, index);
+ // if (this.dev.debug) console.log('Toolset::updateValues', typeof item.tool._stateValue);
+
+ // #IDEA @2021.11.20
+ // What if for attribute and secondaryinfo the entity state itself is also passsed automatically
+ // In that case that state is always present and can be used in animations by default.
+ // No need to pass an extra entity_index.
+ // A tool using the light brightness can also use the state (on/off) in that case for styling.
+ //
+ // Test can be done on 'state', 'attr', or 'secinfo' for default entity_index.
+ //
+ // Should pass a record in here orso as value { state : xx, attr: yy }
+
+ item.tool.value = this._card.attributesStr[item.tool.config.entity_index]
+ ? this._card.attributesStr[item.tool.config.entity_index]
+ : this._card.secondaryInfoStr[item.tool.config.entity_index]
+ ? this._card.secondaryInfoStr[item.tool.config.entity_index]
+ : this._card.entitiesStr[item.tool.config.entity_index];
+ }
+
+ // Check for multiple entities specified, and pass them to the tool
+ if ((item.tool.config.hasOwnProperty('entity_indexes'))) {
+ // Update list of entities in single record and pass that to the tool
+ // The first entity is used as the state, additional entities can help with animations,
+ // (used for formatting classes/styles) or can be used in a derived entity
+
+ const valueList = [];
+ for (let i = 0; i < item.tool.config.entity_indexes.length; ++i) {
+ valueList[i] = this._card.attributesStr[item.tool.config.entity_indexes[i].entity_index]
+ ? this._card.attributesStr[item.tool.config.entity_indexes[i].entity_index]
+ : this._card.secondaryInfoStr[item.tool.config.entity_indexes[i].entity_index]
+ ? this._card.secondaryInfoStr[item.tool.config.entity_indexes[i].entity_index]
+ : this._card.entitiesStr[item.tool.config.entity_indexes[i].entity_index];
+ }
+
+ item.tool.values = valueList;
+ }
+ }
+ return true;
+ });
+ }
+ if (this.dev.performance) console.timeEnd(`--> ${this.toolsetId} PERFORMANCE Toolset::updateValues`);
+ }
+
+ /** *****************************************************************************
+ * Toolset::connectedCallback()
+ *
+ * Summary.
+ *
+ */
+ connectedCallback() {
+ if (this.dev.performance) console.time(`--> ${this.toolsetId} PERFORMANCE Toolset::connectedCallback`);
+
+ if (this.dev.debug) console.log('*****Event - connectedCallback', this.toolsetId, new Date().getTime());
+ if (this.dev.performance) console.timeEnd(`--> ${this.toolsetId} PERFORMANCE Toolset::connectedCallback`);
+ }
+
+ /** *****************************************************************************
+ * Toolset::disconnectedCallback()
+ *
+ * Summary.
+ *
+ */
+ disconnectedCallback() {
+ if (this.dev.performance) console.time(`--> ${this.cardId} PERFORMANCE Toolset::disconnectedCallback`);
+
+ if (this.dev.debug) console.log('*****Event - disconnectedCallback', this.toolsetId, new Date().getTime());
+ if (this.dev.performance) console.timeEnd(`--> ${this.cardId} PERFORMANCE Toolset::disconnectedCallback`);
+ }
+
+ /** *****************************************************************************
+ * Toolset::firstUpdated()
+ *
+ * Summary.
+ *
+ */
+ firstUpdated(changedProperties) {
+ if (this.dev.debug) console.log('*****Event - Toolset::firstUpdated', this.toolsetId, new Date().getTime());
+
+ if (this.tools) {
+ this.tools.map((item) => {
+ if (typeof item.tool.firstUpdated === 'function') {
+ item.tool.firstUpdated(changedProperties);
+ return true;
+ }
+ return false;
+ });
+ }
+ }
+
+ /** *****************************************************************************
+ * Toolset::updated()
+ *
+ * Summary.
+ *
+ */
+ updated(changedProperties) {
+ if (this.dev.debug) console.log('*****Event - Updated', this.toolsetId, new Date().getTime());
+
+ if (this.tools) {
+ this.tools.map((item) => {
+ if (typeof item.tool.updated === 'function') {
+ item.tool.updated(changedProperties);
+ return true;
+ }
+ return false;
+ });
+ }
+ }
+
+ /** *****************************************************************************
+ * Toolset::renderToolset()
+ *
+ * Summary.
+ *
+ */
+ renderToolset() {
+ if (this.dev.debug) console.log('*****Event - renderToolset', this.toolsetId, new Date().getTime());
+
+ const svgItems = this.tools.map((item) => svg`
+ ${item.tool.render()}
+ `);
+ return svg`${svgItems}`;
+ }
+
+ /** *****************************************************************************
+ * Toolset::render()
+ *
+ * Summary.
+ * The render() function for this toolset renders all the tools within this set.
+ *
+ * Important notes:
+ * - the toolset position is set on the svg. That one accepts x,y
+ * - scaling, rotating and skewing (and translating) is done on the parent group.
+ *
+ * The order of transformations are done from the child's perspective!!
+ * So, the child (tools) gets positioned FIRST, and then scaled/rotated.
+ *
+ * See comments for different render paths for Apple/Safari and any other browser...
+ *
+ */
+
+ render() {
+ // Note:
+ // Rotating a card can produce different results on several browsers.
+ // A 1:1 card / toolset gives the same results, but other aspect ratio's may give different results.
+
+ if (((this._card.isSafari) || (this._card.iOS)) && (!this._card.isSafari16)) {
+ //
+ // Render path for Safari if not Safari 16:
+ //
+ // Safari seems to ignore - although not always - the transform-box:fill-box setting.
+ // - It needs the explicit center point when rotating. So this is added to the rotate() command.
+ // - scale around center uses the "move object to 0,0 -> scale -> move object back to position" trick,
+ // where the second move takes scaling into account!
+ // - Does not apply transforms from the child's point of view.
+ // Transform of toolset_position MUST take scaling of one level higher into account!
+ //
+ // Note: rotate is done around the defined center (cx,cy) of the toolsets position!
+ //
+ // More:
+ // - Safari NEEDS the overflow:visible on the element, as it defaults to "svg:{overflow: hidden;}".
+ // Other browsers don't need that, they default to: "svg:not(:root) {overflow: hidden;}"
+ //
+ // Without this setting, objects are cut-off or become invisible while scaled!
+
+ return svg`
+
+
+
+ ${this.renderToolset()}
+
+
+
+ `;
+ } else {
+ //
+ // Render path for ANY other browser that usually follows the standards:
+ //
+ // - use transform-box:fill-box to make sure every transform is about the object itself!
+ // - applying the rules seen from the child's point of view.
+ // So the transform on the toolset_position is NOT scaled, as the scaling is done one level higher.
+ //
+ // Note: rotate is done around the center of the bounding box. This might NOT be the toolsets center (cx,cy) position!
+ //
+ return svg`
+
+
+
+ ${this.renderToolset()}
+
+
+
+ `;
+ }
+ }
+} // END of class
diff --git a/src/user-svg-tool.js b/src/user-svg-tool.js
new file mode 100644
index 0000000..0b447fb
--- /dev/null
+++ b/src/user-svg-tool.js
@@ -0,0 +1,249 @@
+import { svg } from 'lit-element';
+import { styleMap } from 'lit-html/directives/style-map.js';
+import { SVGInjector } from '@tanem/svg-injector';
+
+import Merge from './merge';
+import Utils from './utils';
+import BaseTool from './base-tool';
+import Templates from './templates';
+
+/** ****************************************************************************
+ * UserSvgTool class, UserSvgTool::constructor
+ *
+ * Summary.
+ *
+ */
+
+export default class UserSvgTool extends BaseTool {
+ constructor(argToolset, argConfig, argPos) {
+ const DEFAULT_USERSVG_CONFIG = {
+ position: {
+ cx: 50,
+ cy: 50,
+ height: 50,
+ width: 50,
+ },
+ options: {
+ svginject: true,
+ },
+ styles: {
+ usersvg: {
+ },
+ mask: {
+ fill: 'white',
+ },
+ },
+ };
+
+ super(argToolset, Merge.mergeDeep(DEFAULT_USERSVG_CONFIG, argConfig), argPos);
+
+ this.images = {};
+ this.images = Object.assign({}, ...this.config.images);
+
+ this.item = {};
+ this.item.image = 'default';
+ // Remember the SVG image to load, as we cache those SVG files
+ this.imageCur = 'none';
+ this.imagePrev = 'none';
+
+ this.injector = {};
+ this.injector.svg = null;
+ this.injector.cache = [];
+
+ this.clipPath = {};
+
+ if (this.config.clip_path) {
+ this.svg.cp_cx = Utils.calculateSvgCoordinate(this.config.clip_path.position.cx || this.config.position.cx, 0);
+ this.svg.cp_cy = Utils.calculateSvgCoordinate(this.config.clip_path.position.cy || this.config.position.cy, 0);
+ this.svg.cp_height = Utils.calculateSvgDimension(this.config.clip_path.position.height || this.config.position.height);
+ this.svg.cp_width = Utils.calculateSvgDimension(this.config.clip_path.position.width || this.config.position.width);
+
+ const maxRadius = Math.min(this.svg.cp_height, this.svg.cp_width) / 2;
+
+ this.svg.radiusTopLeft = +Math.min(maxRadius, Math.max(0, Utils.calculateSvgDimension(
+ this.config.clip_path.position.radius.top_left || this.config.clip_path.position.radius.left
+ || this.config.clip_path.position.radius.top || this.config.clip_path.position.radius.all,
+ ))) || 0;
+
+ this.svg.radiusTopRight = +Math.min(maxRadius, Math.max(0, Utils.calculateSvgDimension(
+ this.config.clip_path.position.radius.top_right || this.config.clip_path.position.radius.right
+ || this.config.clip_path.position.radius.top || this.config.clip_path.position.radius.all,
+ ))) || 0;
+
+ this.svg.radiusBottomLeft = +Math.min(maxRadius, Math.max(0, Utils.calculateSvgDimension(
+ this.config.clip_path.position.radius.bottom_left || this.config.clip_path.position.radius.left
+ || this.config.clip_path.position.radius.bottom || this.config.clip_path.position.radius.all,
+ ))) || 0;
+
+ this.svg.radiusBottomRight = +Math.min(maxRadius, Math.max(0, Utils.calculateSvgDimension(
+ this.config.clip_path.position.radius.bottom_right || this.config.clip_path.position.radius.right
+ || this.config.clip_path.position.radius.bottom || this.config.clip_path.position.radius.all,
+ ))) || 0;
+ }
+
+ if (this.dev.debug) console.log('UserSvgTool constructor config, svg', this.toolId, this.config, this.svg);
+ }
+
+ /** *****************************************************************************
+ * UserSvgTool::value()
+ *
+ * Summary.
+ * Receive new state data for the entity this usersvg is linked to. Called from set hass;
+ *
+ */
+ set value(state) {
+ super.value = state;
+ }
+
+ /**
+ * Summary.
+ * Use firstUpdated(). updated() gives a loop of updates of the SVG if more than one SVG
+ * is defined in the card: things start to blink, as each SVG is removed/rendered in a loop
+ * so it seems. Either a bug in the Injector, or the UserSvg tool...
+ *
+ * @param {()} changedProperties
+ * @returns
+ */
+ // eslint-disable-next-line no-unused-vars
+ updated(changedProperties) {
+ var myThis = this;
+
+ // No need to check SVG injection, if same image, and in cache
+ if ((!this.config.options.svginject) || this.injector.cache[this.imageCur]) {
+ return;
+ }
+
+ this.injector.elementsToInject = this._card.shadowRoot.getElementById(
+ 'usersvg-'.concat(this.toolId)).querySelectorAll('svg[data-src]:not(.injected-svg)');
+ if (this.injector.elementsToInject.length !== 0) {
+ SVGInjector(this.injector.elementsToInject, {
+ afterAll(elementsLoaded) {
+ // Request async update of card if all SVG files are loaded using async http request
+ setTimeout(() => { myThis._card.requestUpdate(); }, 0);
+ },
+ afterEach(err, svg) {
+ if (err) {
+ throw err;
+ }
+ myThis.injector.cache[myThis.imageCur] = svg;
+ },
+ beforeEach(svg) {
+ // Remove height and width attributes before injecting
+ svg.removeAttribute('height');
+ svg.removeAttribute('width');
+ },
+ cacheRequests: false,
+ evalScripts: 'once',
+ httpRequestWithCredentials: false,
+ renumerateIRIElements: false,
+ });
+ }
+ }
+
+ /** *****************************************************************************
+ * UserSvgTool::_renderUserSvg()
+ *
+ * Summary.
+ * Renders the usersvg using precalculated coordinates and dimensions.
+ * Only the runtime style is calculated before rendering the usersvg
+ *
+ */
+
+ _renderUserSvg() {
+ this.MergeAnimationStyleIfChanged();
+
+ const images = Templates.getJsTemplateOrValue(this, this._stateValue, Merge.mergeDeep(this.images));
+ this.imagePrev = this.imageCur;
+ this.imageCur = images[this.item.image];
+
+ // Render nothing if no image found
+ if (images[this.item.image] === 'none')
+ return svg``;
+
+ let cachedSvg = this.injector.cache[this.imageCur];
+
+ // construct clip path if specified
+ let clipPath = '';
+ if (this.config.clip_path) {
+ clipPath = svg`
+
+
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ // If jpg or png, use default image renderer...
+ if (['png', 'jpg'].includes((images[this.item.image].substring(images[this.item.image].lastIndexOf('.') + 1)))) {
+ // Render jpg or png
+ return svg`
+
+ "${clipPath}"
+
+
+ `;
+ // Must be svg. Render for the first time, if not in cache...
+ } else if ((!cachedSvg) || (!this.config.options.svginject)) {
+ return svg`
+
+ "${clipPath}"
+
+
+ `;
+ // Render from cache and pass clip path and mask as reference...
+ } else {
+ return svg`
+
+ "${clipPath}"
+ ${cachedSvg};
+
+ `;
+ }
+ }
+
+ /** *****************************************************************************
+ * UserSvgTool::render()
+ *
+ * Summary.
+ * The render() function for this object.
+ *
+ */
+ render() {
+ return svg`
+ this.handleTapEvent(e, this.config)}>
+ ${this._renderUserSvg()}
+
+ `;
+ }
+} // END of class
diff --git a/src/utils.js b/src/utils.js
new file mode 100644
index 0000000..fa034ef
--- /dev/null
+++ b/src/utils.js
@@ -0,0 +1,83 @@
+import {
+ SVG_DEFAULT_DIMENSIONS,
+ SVG_DEFAULT_DIMENSIONS_HALF,
+} from './const';
+
+/** ***************************************************************************
+ * Utils class
+ *
+ * Summary.
+ *
+ */
+
+export default class Utils {
+ /**
+ * Utils::calculateValueBetween()
+ *
+ * Summary.
+ * Clips the val value between start and end, and returns the between value ;-)
+ * Returned value is a fractional value between 0 and 1.
+ *
+ * Note 1:
+ * At start, state values are set to 'null' to make sure it has no value!
+ * If such a value is detected, return 0(%) as the relative value.
+ * In normal cases, this happens to be the _valuePrev, so 0% is ok!!!!
+ *
+ * Note 2:
+ * !xyz checks for "", null, undefined, false and number 0
+ * An extra check for NaN guards the result of this function ;-)
+ */
+
+ static calculateValueBetween(argStart, argEnd, argVal) {
+ // Check for valid argVal values and return 0 if invalid.
+ if (isNaN(argVal)) return 0;
+ if (!argVal) return 0;
+
+ // Valid argVal value: calculate fraction between 0 and 1
+ return (Math.min(Math.max(argVal, argStart), argEnd) - argStart) / (argEnd - argStart);
+ }
+
+ /**
+ * Utils::calculateSvgCoordinate()
+ *
+ * Summary.
+ * Calculate own (tool/tool) coordinates relative to centered toolset position.
+ * Tool coordinates are %
+ *
+ * Group is 50,40. Say SVG is 200x200. Group is 100,80 within 200x200.
+ * Tool is 10,50. 0.1 * 200 = 20 + (100 - 200/2) = 20 + 0.
+ */
+ static calculateSvgCoordinate(argOwn, argToolset) {
+ return (argOwn / 100) * (SVG_DEFAULT_DIMENSIONS)
+ + (argToolset - SVG_DEFAULT_DIMENSIONS_HALF);
+ }
+
+ /**
+ * Utils::calculateSvgDimension()
+ *
+ * Summary.
+ * Translate tool dimension like length or width to actual SVG dimension.
+ */
+
+ static calculateSvgDimension(argDimension) {
+ return (argDimension / 100) * (SVG_DEFAULT_DIMENSIONS);
+ }
+
+ static getLovelace() {
+ let root = window.document.querySelector('home-assistant');
+ root = root && root.shadowRoot;
+ root = root && root.querySelector('home-assistant-main');
+ root = root && root.shadowRoot;
+ root = root && root.querySelector('app-drawer-layout partial-panel-resolver, ha-drawer partial-panel-resolver');
+ root = (root && root.shadowRoot) || root;
+ root = root && root.querySelector('ha-panel-lovelace');
+ root = root && root.shadowRoot;
+ root = root && root.querySelector('hui-root');
+ if (root) {
+ const ll = root.lovelace;
+ ll.current_view = root.___curView;
+ return ll;
+ }
+ return null;
+ }
+}
diff --git a/srcjs/README.md b/srcjs/README.md
deleted file mode 100644
index 8b7a79b..0000000
--- a/srcjs/README.md
+++ /dev/null
@@ -1 +0,0 @@
-Split
diff --git a/workspaces.code-workspace b/workspaces.code-workspace
new file mode 100644
index 0000000..0158fe9
--- /dev/null
+++ b/workspaces.code-workspace
@@ -0,0 +1,15 @@
+{
+ "folders": [
+ {
+ "path": ".."
+ },
+ {
+ "name": "swiss-army-knife-card",
+ "path": "."
+ }
+ ],
+ "settings": {
+ "files.eol": "\n",
+ "editor.tabSize": 2,
+ }
+}
\ No newline at end of file