You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
8719 lines
382 KiB
8719 lines
382 KiB
(function (global, factory) {
|
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
|
typeof define === 'function' && define.amd ? define(factory) :
|
|
(global = global || self, global.GSTC = factory());
|
|
}(this, (function () { 'use strict';
|
|
|
|
/**
|
|
* @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
|
|
*/
|
|
/**
|
|
* Brands a function as a directive factory function so that lit-html will call
|
|
* the function during template rendering, rather than passing as a value.
|
|
*
|
|
* A _directive_ is a function that takes a Part as an argument. It has the
|
|
* signature: `(part: Part) => void`.
|
|
*
|
|
* A directive _factory_ is a function that takes arguments for data and
|
|
* configuration and returns a directive. Users of directive usually refer to
|
|
* the directive factory as the directive. For example, "The repeat directive".
|
|
*
|
|
* Usually a template author will invoke a directive factory in their template
|
|
* with relevant arguments, which will then return a directive function.
|
|
*
|
|
* Here's an example of using the `repeat()` directive factory that takes an
|
|
* array and a function to render an item:
|
|
*
|
|
* ```js
|
|
* html`<ul><${repeat(items, (item) => html`<li>${item}</li>`)}</ul>`
|
|
* ```
|
|
*
|
|
* When `repeat` is invoked, it returns a directive function that closes over
|
|
* `items` and the template function. When the outer template is rendered, the
|
|
* return directive function is called with the Part for the expression.
|
|
* `repeat` then performs it's custom logic to render multiple items.
|
|
*
|
|
* @param f The directive factory function. Must be a function that returns a
|
|
* function of the signature `(part: Part) => void`. The returned function will
|
|
* be called with the part object.
|
|
*
|
|
* @example
|
|
*
|
|
* import {directive, html} from 'lit-html';
|
|
*
|
|
* const immutable = directive((v) => (part) => {
|
|
* if (part.value !== v) {
|
|
* part.setValue(v)
|
|
* }
|
|
* });
|
|
*/
|
|
const directive = (f) => ((...args) => {
|
|
const d = f(...args);
|
|
// tslint:disable-next-line:no-any
|
|
d.isDirective = true;
|
|
return d;
|
|
});
|
|
class Directive {
|
|
constructor() {
|
|
this.isDirective = true;
|
|
this.isClass = true;
|
|
}
|
|
body(_part) {
|
|
// body of the directive
|
|
}
|
|
}
|
|
const isDirective = (o) => {
|
|
return o !== undefined && o !== null &&
|
|
// tslint:disable-next-line:no-any
|
|
typeof o.isDirective === 'boolean';
|
|
};
|
|
|
|
/**
|
|
* @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 :
|
|
false;
|
|
/**
|
|
* 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) 2018 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
/**
|
|
* A sentinel value that signals that a value was handled by a directive and
|
|
* should not be written to the DOM.
|
|
*/
|
|
const noChange = {};
|
|
/**
|
|
* A sentinel value that signals a NodePart to fully clear its content.
|
|
*/
|
|
const nothing = {};
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
/**
|
|
* An expression marker with embedded unique key to avoid collision with
|
|
* possible text in templates.
|
|
*/
|
|
const marker = `{{lit-${String(Math.random()).slice(2)}}}`;
|
|
/**
|
|
* An expression marker used text-positions, multi-binding attributes, and
|
|
* attributes with markup-like text values.
|
|
*/
|
|
const nodeMarker = `<!--${marker}-->`;
|
|
const markerRegex = new RegExp(`${marker}|${nodeMarker}`);
|
|
/**
|
|
* Suffix appended to all bound attribute names.
|
|
*/
|
|
const boundAttributeSuffix = '$lit$';
|
|
/**
|
|
* An 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,
|
|
sanitizer: undefined
|
|
});
|
|
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;
|
|
/**
|
|
* Used to clone existing node instead of each time creating new one which is
|
|
* slower
|
|
*/
|
|
const markerNode = document.createComment('');
|
|
// Allows `document.createComment('')` to be renamed for a
|
|
// small manual size-savings.
|
|
const createMarker = () => markerNode.cloneNode();
|
|
/**
|
|
* 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
|
|
*/
|
|
/**
|
|
* 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 textPart = this.processor.handleTextExpression(this.options, part);
|
|
textPart.insertAfterNode(node.previousSibling);
|
|
this.__parts.push(textPart);
|
|
}
|
|
else {
|
|
this.__parts.push(...this.processor.handleAttributeExpressions(node, part.name, part.strings, this.options, part));
|
|
}
|
|
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
|
|
*/
|
|
let policy;
|
|
/**
|
|
* Turns the value to trusted HTML. If the application uses Trusted Types the
|
|
* value is transformed into TrustedHTML, which can be assigned to execution
|
|
* sink. If the application doesn't use Trusted Types, the return value is the
|
|
* same as the argument.
|
|
*/
|
|
function convertConstantTemplateStringToTrustedHTML(value) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const w = window;
|
|
// TrustedTypes have been renamed to trustedTypes
|
|
// (https://github.com/WICG/trusted-types/issues/177)
|
|
const trustedTypes = (w.trustedTypes || w.TrustedTypes);
|
|
if (trustedTypes && !policy) {
|
|
policy = trustedTypes.createPolicy('lit-html', { createHTML: (s) => s });
|
|
}
|
|
return policy ? policy.createHTML(value) : value;
|
|
}
|
|
const commentMarker = ` ${marker} `;
|
|
/**
|
|
* Used to clone existing node instead of each time creating new one which is
|
|
* slower
|
|
*/
|
|
const emptyTemplateNode = document.createElement('template');
|
|
/**
|
|
* The return type of `html`, which holds a Template and the values from
|
|
* interpolated expressions.
|
|
*/
|
|
class TemplateResult {
|
|
constructor(strings, values, type, processor) {
|
|
this.strings = strings;
|
|
this.values = values;
|
|
this.type = type;
|
|
this.processor = processor;
|
|
}
|
|
/**
|
|
* Returns a string of HTML used to create a `<template>` element.
|
|
*/
|
|
getHTML() {
|
|
const l = this.strings.length - 1;
|
|
let html = '';
|
|
let isCommentBinding = false;
|
|
for (let i = 0; i < l; i++) {
|
|
const s = this.strings[i];
|
|
// For each binding we want to determine the kind of marker to insert
|
|
// into the template source before it's parsed by the browser's HTML
|
|
// parser. The marker type is based on whether the expression is in an
|
|
// attribute, text, or comment position.
|
|
// * For node-position bindings we insert a comment with the marker
|
|
// sentinel as its text content, like <!--{{lit-guid}}-->.
|
|
// * For attribute bindings we insert just the marker sentinel for the
|
|
// first binding, so that we support unquoted attribute bindings.
|
|
// Subsequent bindings can use a comment marker because multi-binding
|
|
// attributes must be quoted.
|
|
// * For comment bindings we insert just the marker sentinel so we don't
|
|
// close the comment.
|
|
//
|
|
// The following code scans the template source, but is *not* an HTML
|
|
// parser. We don't need to track the tree structure of the HTML, only
|
|
// whether a binding is inside a comment, and if not, if it appears to be
|
|
// the first binding in an attribute.
|
|
const commentOpen = s.lastIndexOf('<!--');
|
|
// We're in comment position if we have a comment open with no following
|
|
// comment close. Because <-- can appear in an attribute value there can
|
|
// be false positives.
|
|
isCommentBinding = (commentOpen > -1 || isCommentBinding) &&
|
|
s.indexOf('-->', commentOpen + 1) === -1;
|
|
// Check to see if we have an attribute-like sequence preceding the
|
|
// expression. This can match "name=value" like structures in text,
|
|
// comments, and attribute values, so there can be false-positives.
|
|
const attributeMatch = lastAttributeNameRegex.exec(s);
|
|
if (attributeMatch === null) {
|
|
// We're only in this branch if we don't have a attribute-like
|
|
// preceding sequence. For comments, this guards against unusual
|
|
// attribute values like <div foo="<!--${'bar'}">. Cases like
|
|
// <!-- foo=${'bar'}--> are handled correctly in the attribute branch
|
|
// below.
|
|
html += s + (isCommentBinding ? commentMarker : nodeMarker);
|
|
}
|
|
else {
|
|
// For attributes we use just a marker sentinel, and also append a
|
|
// $lit$ suffix to the name to opt-out of attribute-specific parsing
|
|
// that IE and Edge do for style and certain SVG attributes.
|
|
html += s.substr(0, attributeMatch.index) + attributeMatch[1] +
|
|
attributeMatch[2] + boundAttributeSuffix + attributeMatch[3] +
|
|
marker;
|
|
}
|
|
}
|
|
html += this.strings[l];
|
|
return html;
|
|
}
|
|
getTemplateElement() {
|
|
const template = emptyTemplateNode.cloneNode();
|
|
// this is secure because `this.strings` is a TemplateStringsArray.
|
|
// TODO: validate this when
|
|
// https://github.com/tc39/proposal-array-is-template-object is implemented.
|
|
template.innerHTML =
|
|
convertConstantTemplateStringToTrustedHTML(this.getHTML());
|
|
return template;
|
|
}
|
|
}
|
|
/**
|
|
* A TemplateResult for SVG fragments.
|
|
*
|
|
* This class wraps HTML in an `<svg>` tag in order to parse its contents in the
|
|
* SVG namespace, then modifies the template to remove the `<svg>` tag so that
|
|
* clones only container the original fragment.
|
|
*/
|
|
class SVGTemplateResult extends TemplateResult {
|
|
getHTML() {
|
|
return `<svg>${super.getHTML()}</svg>`;
|
|
}
|
|
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) ||
|
|
// tslint:disable-next-line: no-any
|
|
!!(value && value[Symbol.iterator]);
|
|
};
|
|
const identityFunction = (value) => value;
|
|
const noopSanitizer = (_node, _name, _type) => identityFunction;
|
|
/**
|
|
* A global callback used to get a sanitizer for a given field.
|
|
*/
|
|
let sanitizerFactory = noopSanitizer;
|
|
/** Sets the global sanitizer factory. */
|
|
const setSanitizerFactory = (newSanitizer) => {
|
|
if (sanitizerFactory !== noopSanitizer) {
|
|
throw new Error(`Attempted to overwrite existing lit-html security policy.` +
|
|
` setSanitizeDOMValueFactory should be called at most once.`);
|
|
}
|
|
sanitizerFactory = newSanitizer;
|
|
};
|
|
/**
|
|
* Used to clone text node instead of each time creating new one which is slower
|
|
*/
|
|
const emptyTextNode = document.createTextNode('');
|
|
/**
|
|
* 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,
|
|
// Next breaking change, consider making this param required.
|
|
templatePart, kind = 'attribute') {
|
|
this.dirty = true;
|
|
this.element = element;
|
|
this.name = name;
|
|
this.strings = strings;
|
|
this.parts = [];
|
|
let sanitizer = templatePart && templatePart.sanitizer;
|
|
if (sanitizer === undefined) {
|
|
sanitizer = sanitizerFactory(element, name, kind);
|
|
if (templatePart !== undefined) {
|
|
templatePart.sanitizer = sanitizer;
|
|
}
|
|
}
|
|
this.sanitizer = sanitizer;
|
|
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 parts = this.parts;
|
|
const l = strings.length - 1;
|
|
// 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] === '' &&
|
|
parts[0] !== undefined) {
|
|
const v = parts[0].value;
|
|
if (!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;
|
|
let value = this._getValue();
|
|
value = this.sanitizer(value);
|
|
if (typeof value === 'symbol') {
|
|
// Native Symbols throw if they're coerced to string.
|
|
value = String(value);
|
|
}
|
|
this.element.setAttribute(this.name, value);
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* 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;
|
|
// tslint:disable-next-line: no-any
|
|
if (directive.isClass) {
|
|
// tslint:disable-next-line: no-any
|
|
directive.body(this);
|
|
}
|
|
else {
|
|
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, templatePart) {
|
|
this.value = undefined;
|
|
this.__pendingValue = undefined;
|
|
/**
|
|
* The sanitizer to use when writing text contents into this NodePart.
|
|
*
|
|
* We have to initialize this here rather than at the template literal level
|
|
* because the security of text content depends on the context into which
|
|
* it's written. e.g. the same text has different security requirements
|
|
* when a child of a <script> vs a <style> vs a <div>.
|
|
*/
|
|
this.textSanitizer = undefined;
|
|
this.options = options;
|
|
this.templatePart = templatePart;
|
|
}
|
|
/**
|
|
* 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() {
|
|
while (isDirective(this.__pendingValue)) {
|
|
const directive = this.__pendingValue;
|
|
this.__pendingValue = noChange;
|
|
// tslint:disable-next-line: no-any
|
|
if (directive.isClass) {
|
|
// tslint:disable-next-line: no-any
|
|
directive.body(this);
|
|
}
|
|
else {
|
|
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 (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.
|
|
if (this.textSanitizer === undefined) {
|
|
this.textSanitizer = sanitizerFactory(node, 'data', 'property');
|
|
}
|
|
const renderedValue = this.textSanitizer(value);
|
|
node.data = typeof renderedValue === 'string' ?
|
|
renderedValue :
|
|
String(renderedValue);
|
|
}
|
|
else {
|
|
// When setting text content, for security purposes it matters a lot what
|
|
// the parent is. For example, <style> and <script> need to be handled
|
|
// with care, while <span> does not. So first we need to put a text node
|
|
// into the document, then we can sanitize its contentx.
|
|
const textNode = emptyTextNode.cloneNode();
|
|
this.__commitNode(textNode);
|
|
if (this.textSanitizer === undefined) {
|
|
this.textSanitizer = sanitizerFactory(textNode, 'data', 'property');
|
|
}
|
|
const renderedValue = this.textSanitizer(value);
|
|
textNode.data = typeof renderedValue === 'string' ? renderedValue :
|
|
String(renderedValue);
|
|
}
|
|
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 {
|
|
// `value` is a template result that was constructed without knowledge of
|
|
// the parent we're about to write it into. sanitizeDOMValue hasn't been
|
|
// made aware of this relationship, and for scripts and style specifically
|
|
// this is known to be unsafe. So in the case where the user is in
|
|
// "secure mode" (i.e. when there's a sanitizeDOMValue set), we just want
|
|
// to forbid this because it's not a use case we want to support.
|
|
// We only apply this policy when sanitizerFactory has been set to
|
|
// prevent this from being a breaking change to the library.
|
|
const parent = this.endNode.parentNode;
|
|
if (sanitizerFactory !== noopSanitizer && parent.nodeName === 'STYLE' ||
|
|
parent.nodeName === 'SCRIPT') {
|
|
this.__commitText('/* lit-html will not write ' +
|
|
'TemplateResults to scripts and styles */');
|
|
return;
|
|
}
|
|
// 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, this.templatePart);
|
|
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;
|
|
// tslint:disable-next-line: no-any
|
|
if (directive.isClass) {
|
|
// tslint:disable-next-line: no-any
|
|
directive.body(this);
|
|
}
|
|
else {
|
|
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,
|
|
// Next breaking change, consider making this param required.
|
|
templatePart) {
|
|
super(element, name, strings, templatePart, 'property');
|
|
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;
|
|
let value = this._getValue();
|
|
value = this.sanitizer(value);
|
|
// tslint:disable-next-line: no-any
|
|
this.element[this.name] = value;
|
|
}
|
|
}
|
|
}
|
|
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) {
|
|
// noop
|
|
}
|
|
})();
|
|
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;
|
|
// tslint:disable-next-line: no-any
|
|
if (directive.isClass) {
|
|
// tslint:disable-next-line: no-any
|
|
directive.body(this);
|
|
}
|
|
else {
|
|
directive(this);
|
|
}
|
|
}
|
|
if (this.__pendingValue === noChange) {
|
|
return;
|
|
}
|
|
const newListener = this.__pendingValue;
|
|
const oldListener = this.value;
|
|
const shouldRemoveListener = newListener == null ||
|
|
oldListener != null &&
|
|
(newListener.capture !== oldListener.capture ||
|
|
newListener.once !== oldListener.once ||
|
|
newListener.passive !== oldListener.passive);
|
|
const shouldAddListener = newListener != null && (oldListener == null || shouldRemoveListener);
|
|
if (shouldRemoveListener) {
|
|
this.element.removeEventListener(this.eventName, this.__boundHandleEvent, this.__options);
|
|
}
|
|
if (shouldAddListener) {
|
|
this.__options = getOptions(newListener);
|
|
this.element.addEventListener(this.eventName, this.__boundHandleEvent, this.__options);
|
|
}
|
|
this.value = newListener;
|
|
this.__pendingValue = noChange;
|
|
}
|
|
handleEvent(event) {
|
|
if (typeof this.value === 'function') {
|
|
this.value.call(this.eventContext || this.element, event);
|
|
}
|
|
else {
|
|
this.value.handleEvent(event);
|
|
}
|
|
}
|
|
}
|
|
// We copy options because of the inconsistent behavior of browsers when reading
|
|
// the third argument of add/removeEventListener. IE11 doesn't support options
|
|
// at all. Chrome 41 only reads `capture` if the argument is an object.
|
|
const getOptions = (o) => o &&
|
|
(eventOptionsSupported ?
|
|
{ capture: o.capture, passive: o.passive, once: o.once } :
|
|
o.capture);
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
/**
|
|
* Creates Parts when a template is instantiated.
|
|
*/
|
|
class DefaultTemplateProcessor {
|
|
/**
|
|
* Create parts for an attribute-position binding, given the event, attribute
|
|
* name, and string literals.
|
|
*
|
|
* @param element The element containing the binding
|
|
* @param name The attribute name
|
|
* @param strings The string literals. There are always at least two strings,
|
|
* event for fully-controlled bindings with a single expression.
|
|
*/
|
|
handleAttributeExpressions(element, name, strings, options, templatePart) {
|
|
const prefix = name[0];
|
|
if (prefix === '.') {
|
|
const committer = new PropertyCommitter(element, name.slice(1), strings, templatePart);
|
|
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, templatePart);
|
|
return committer.parts;
|
|
}
|
|
/**
|
|
* Create parts for a text-position binding.
|
|
* @param templateFactory
|
|
*/
|
|
handleTextExpression(options, nodeTemplatePart) {
|
|
return new NodePart(options, nodeTemplatePart);
|
|
}
|
|
}
|
|
const defaultTemplateProcessor = new DefaultTemplateProcessor();
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
/**
|
|
* The default TemplateFactory which caches Templates keyed on
|
|
* result.type and result.strings.
|
|
*/
|
|
function templateFactory(result) {
|
|
let templateCache = templateCaches.get(result.type);
|
|
if (templateCache === undefined) {
|
|
templateCache = {
|
|
stringsArray: new WeakMap(),
|
|
keyString: new Map()
|
|
};
|
|
templateCaches.set(result.type, templateCache);
|
|
}
|
|
let template = templateCache.stringsArray.get(result.strings);
|
|
if (template !== undefined) {
|
|
return template;
|
|
}
|
|
// If the TemplateStringsArray is new, generate a key from the strings
|
|
// This key is shared between all templates with identical content
|
|
const key = result.strings.join(marker);
|
|
// Check if we already have a Template for this key
|
|
template = templateCache.keyString.get(key);
|
|
if (template === undefined) {
|
|
// If we have not seen this key before, create a new Template
|
|
template = new Template(result, result.getTemplateElement());
|
|
// Cache the Template for this key
|
|
templateCache.keyString.set(key, template);
|
|
}
|
|
// Cache all future queries for this TemplateStringsArray
|
|
templateCache.stringsArray.set(result.strings, template);
|
|
return template;
|
|
}
|
|
const templateCaches = new Map();
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
const parts = new WeakMap();
|
|
/**
|
|
* Renders a template result or other value to a container.
|
|
*
|
|
* To update a container with new values, reevaluate the template literal and
|
|
* call `render` with the new result.
|
|
*
|
|
* @param result Any value renderable by NodePart - typically a TemplateResult
|
|
* created by evaluating a template tag like `html` or `svg`.
|
|
* @param container A DOM parent to render to. The entire contents are either
|
|
* replaced, or efficiently updated if the same result type was previous
|
|
* rendered there.
|
|
* @param options RenderOptions for the entire render tree rendered to this
|
|
* container. Render options must *not* change between renders to the same
|
|
* container, as those changes will not effect previously rendered DOM.
|
|
*/
|
|
const render = (result, container, options) => {
|
|
let part = parts.get(container);
|
|
if (part === undefined) {
|
|
removeNodes(container, container.firstChild);
|
|
parts.set(container, part = new NodePart(Object.assign({ templateFactory }, options), undefined));
|
|
part.appendInto(container);
|
|
}
|
|
part.setValue(result);
|
|
part.commit();
|
|
};
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
// IMPORTANT: do not change the property name or the assignment expression.
|
|
// This line will be used in regexes to search for lit-html usage.
|
|
// TODO(justinfagnani): inject version number at build time
|
|
const isBrowser = typeof window !== 'undefined';
|
|
if (isBrowser) {
|
|
// If we run in the browser set version
|
|
(window['litHtmlVersions'] || (window['litHtmlVersions'] = [])).push('1.1.7');
|
|
}
|
|
/**
|
|
* 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);
|
|
|
|
var lithtml = /*#__PURE__*/Object.freeze({
|
|
__proto__: null,
|
|
html: html,
|
|
svg: svg,
|
|
DefaultTemplateProcessor: DefaultTemplateProcessor,
|
|
defaultTemplateProcessor: defaultTemplateProcessor,
|
|
directive: directive,
|
|
Directive: Directive,
|
|
isDirective: isDirective,
|
|
removeNodes: removeNodes,
|
|
reparentNodes: reparentNodes,
|
|
noChange: noChange,
|
|
nothing: nothing,
|
|
AttributeCommitter: AttributeCommitter,
|
|
AttributePart: AttributePart,
|
|
BooleanAttributePart: BooleanAttributePart,
|
|
EventPart: EventPart,
|
|
isIterable: isIterable,
|
|
isPrimitive: isPrimitive,
|
|
NodePart: NodePart,
|
|
PropertyCommitter: PropertyCommitter,
|
|
PropertyPart: PropertyPart,
|
|
get sanitizerFactory () { return sanitizerFactory; },
|
|
setSanitizerFactory: setSanitizerFactory,
|
|
parts: parts,
|
|
render: render,
|
|
templateCaches: templateCaches,
|
|
templateFactory: templateFactory,
|
|
TemplateInstance: TemplateInstance,
|
|
SVGTemplateResult: SVGTemplateResult,
|
|
TemplateResult: TemplateResult,
|
|
createMarker: createMarker,
|
|
isTemplatePartActive: isTemplatePartActive,
|
|
Template: 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
|
|
*/
|
|
var __asyncValues = function (o) {
|
|
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
var m = o[Symbol.asyncIterator], i;
|
|
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
|
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
};
|
|
/**
|
|
* A directive that renders the items of an async iterable[1], appending new
|
|
* values after previous values, similar to the built-in support for iterables.
|
|
*
|
|
* Async iterables are objects with a [Symbol.asyncIterator] method, which
|
|
* returns an iterator who's `next()` method returns a Promise. When a new
|
|
* value is available, the Promise resolves and the value is appended to the
|
|
* Part controlled by the directive. If another value other than this
|
|
* directive has been set on the Part, the iterable will no longer be listened
|
|
* to and new values won't be written to the Part.
|
|
*
|
|
* [1]: https://github.com/tc39/proposal-async-iteration
|
|
*
|
|
* @param value An async iterable
|
|
* @param mapper An optional function that maps from (value, index) to another
|
|
* value. Useful for generating templates for each item in the iterable.
|
|
*/
|
|
const asyncAppend = directive((value, mapper) => async (part) => {
|
|
var e_1, _a;
|
|
if (!(part instanceof NodePart)) {
|
|
throw new Error('asyncAppend can only be used in text bindings');
|
|
}
|
|
// If we've already set up this particular iterable, we don't need
|
|
// to do anything.
|
|
if (value === part.value) {
|
|
return;
|
|
}
|
|
part.value = value;
|
|
// We keep track of item Parts across iterations, so that we can
|
|
// share marker nodes between consecutive Parts.
|
|
let itemPart;
|
|
let i = 0;
|
|
try {
|
|
for (var value_1 = __asyncValues(value), value_1_1; value_1_1 = await value_1.next(), !value_1_1.done;) {
|
|
let v = value_1_1.value;
|
|
// Check to make sure that value is the still the current value of
|
|
// the part, and if not bail because a new value owns this part
|
|
if (part.value !== value) {
|
|
break;
|
|
}
|
|
// When we get the first value, clear the part. This lets the
|
|
// previous value display until we can replace it.
|
|
if (i === 0) {
|
|
part.clear();
|
|
}
|
|
// As a convenience, because functional-programming-style
|
|
// transforms of iterables and async iterables requires a library,
|
|
// we accept a mapper function. This is especially convenient for
|
|
// rendering a template for each item.
|
|
if (mapper !== undefined) {
|
|
// This is safe because T must otherwise be treated as unknown by
|
|
// the rest of the system.
|
|
v = mapper(v, i);
|
|
}
|
|
// Like with sync iterables, each item induces a Part, so we need
|
|
// to keep track of start and end nodes for the Part.
|
|
// Note: Because these Parts are not updatable like with a sync
|
|
// iterable (if we render a new value, we always clear), it may
|
|
// be possible to optimize away the Parts and just re-use the
|
|
// Part.setValue() logic.
|
|
let itemStartNode = part.startNode;
|
|
// Check to see if we have a previous item and Part
|
|
if (itemPart !== undefined) {
|
|
// Create a new node to separate the previous and next Parts
|
|
itemStartNode = createMarker();
|
|
// itemPart is currently the Part for the previous item. Set
|
|
// it's endNode to the node we'll use for the next Part's
|
|
// startNode.
|
|
itemPart.endNode = itemStartNode;
|
|
part.endNode.parentNode.insertBefore(itemStartNode, part.endNode);
|
|
}
|
|
itemPart = new NodePart(part.options, part.templatePart);
|
|
itemPart.insertAfterNode(itemStartNode);
|
|
itemPart.setValue(v);
|
|
itemPart.commit();
|
|
i++;
|
|
}
|
|
}
|
|
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
finally {
|
|
try {
|
|
if (value_1_1 && !value_1_1.done && (_a = value_1.return)) await _a.call(value_1);
|
|
}
|
|
finally { if (e_1) throw e_1.error; }
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
var __asyncValues$1 = function (o) {
|
|
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
var m = o[Symbol.asyncIterator], i;
|
|
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
|
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
};
|
|
/**
|
|
* A directive that renders the items of an async iterable[1], replacing
|
|
* previous values with new values, so that only one value is ever rendered
|
|
* at a time.
|
|
*
|
|
* Async iterables are objects with a [Symbol.asyncIterator] method, which
|
|
* returns an iterator who's `next()` method returns a Promise. When a new
|
|
* value is available, the Promise resolves and the value is rendered to the
|
|
* Part controlled by the directive. If another value other than this
|
|
* directive has been set on the Part, the iterable will no longer be listened
|
|
* to and new values won't be written to the Part.
|
|
*
|
|
* [1]: https://github.com/tc39/proposal-async-iteration
|
|
*
|
|
* @param value An async iterable
|
|
* @param mapper An optional function that maps from (value, index) to another
|
|
* value. Useful for generating templates for each item in the iterable.
|
|
*/
|
|
const asyncReplace = directive((value, mapper) => async (part) => {
|
|
var e_1, _a;
|
|
if (!(part instanceof NodePart)) {
|
|
throw new Error('asyncReplace can only be used in text bindings');
|
|
}
|
|
// If we've already set up this particular iterable, we don't need
|
|
// to do anything.
|
|
if (value === part.value) {
|
|
return;
|
|
}
|
|
// We nest a new part to keep track of previous item values separately
|
|
// of the iterable as a value itself.
|
|
const itemPart = new NodePart(part.options, part.templatePart);
|
|
part.value = value;
|
|
let i = 0;
|
|
try {
|
|
for (var value_1 = __asyncValues$1(value), value_1_1; value_1_1 = await value_1.next(), !value_1_1.done;) {
|
|
let v = value_1_1.value;
|
|
// Check to make sure that value is the still the current value of
|
|
// the part, and if not bail because a new value owns this part
|
|
if (part.value !== value) {
|
|
break;
|
|
}
|
|
// When we get the first value, clear the part. This let's the
|
|
// previous value display until we can replace it.
|
|
if (i === 0) {
|
|
part.clear();
|
|
itemPart.appendIntoPart(part);
|
|
}
|
|
// As a convenience, because functional-programming-style
|
|
// transforms of iterables and async iterables requires a library,
|
|
// we accept a mapper function. This is especially convenient for
|
|
// rendering a template for each item.
|
|
if (mapper !== undefined) {
|
|
// This is safe because T must otherwise be treated as unknown by
|
|
// the rest of the system.
|
|
v = mapper(v, i);
|
|
}
|
|
itemPart.setValue(v);
|
|
itemPart.commit();
|
|
i++;
|
|
}
|
|
}
|
|
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
finally {
|
|
try {
|
|
if (value_1_1 && !value_1_1.done && (_a = value_1.return)) await _a.call(value_1);
|
|
}
|
|
finally { if (e_1) throw e_1.error; }
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @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
|
|
*/
|
|
const templateCaches$1 = new WeakMap();
|
|
/**
|
|
* Enables fast switching between multiple templates by caching the DOM nodes
|
|
* and TemplateInstances produced by the templates.
|
|
*
|
|
* Example:
|
|
*
|
|
* ```
|
|
* let checked = false;
|
|
*
|
|
* html`
|
|
* ${cache(checked ? html`input is checked` : html`input is not checked`)}
|
|
* `
|
|
* ```
|
|
*/
|
|
const cache = directive((value) => (part) => {
|
|
if (!(part instanceof NodePart)) {
|
|
throw new Error('cache can only be used in text bindings');
|
|
}
|
|
let templateCache = templateCaches$1.get(part);
|
|
if (templateCache === undefined) {
|
|
templateCache = new WeakMap();
|
|
templateCaches$1.set(part, templateCache);
|
|
}
|
|
const previousValue = part.value;
|
|
// First, can we update the current TemplateInstance, or do we need to move
|
|
// the current nodes into the cache?
|
|
if (previousValue instanceof TemplateInstance) {
|
|
if (value instanceof TemplateResult &&
|
|
previousValue.template === part.options.templateFactory(value)) {
|
|
// Same Template, just trigger an update of the TemplateInstance
|
|
part.setValue(value);
|
|
return;
|
|
}
|
|
else {
|
|
// Not the same Template, move the nodes from the DOM into the cache.
|
|
let cachedTemplate = templateCache.get(previousValue.template);
|
|
if (cachedTemplate === undefined) {
|
|
cachedTemplate = {
|
|
instance: previousValue,
|
|
nodes: document.createDocumentFragment(),
|
|
};
|
|
templateCache.set(previousValue.template, cachedTemplate);
|
|
}
|
|
reparentNodes(cachedTemplate.nodes, part.startNode.nextSibling, part.endNode);
|
|
}
|
|
}
|
|
// Next, can we reuse nodes from the cache?
|
|
if (value instanceof TemplateResult) {
|
|
const template = part.options.templateFactory(value);
|
|
const cachedTemplate = templateCache.get(template);
|
|
if (cachedTemplate !== undefined) {
|
|
// Move nodes out of cache
|
|
part.setValue(cachedTemplate.nodes);
|
|
part.commit();
|
|
// Set the Part value to the TemplateInstance so it'll update it.
|
|
part.value = cachedTemplate.instance;
|
|
}
|
|
}
|
|
part.setValue(value);
|
|
});
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2018 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
/**
|
|
* Stores the ClassInfo object applied to a given AttributePart.
|
|
* Used to unset existing values when a new ClassInfo object is applied.
|
|
*/
|
|
const previousClassesCache = new WeakMap();
|
|
/**
|
|
* A directive that applies CSS classes. This must be used in the `class`
|
|
* attribute and must be the only part used in the attribute. It takes each
|
|
* property in the `classInfo` argument and adds the property name to the
|
|
* element's `classList` if the property value is truthy; if the property value
|
|
* is falsey, the property name is removed from the element's `classList`. For
|
|
* example
|
|
* `{foo: bar}` applies the class `foo` if the value of `bar` is truthy.
|
|
* @param classInfo {ClassInfo}
|
|
*/
|
|
const classMap = directive((classInfo) => (part) => {
|
|
if (!(part instanceof AttributePart) || (part instanceof PropertyPart) ||
|
|
part.committer.name !== 'class' || part.committer.parts.length > 1) {
|
|
throw new Error('The `classMap` directive must be used in the `class` attribute ' +
|
|
'and must be the only part in the attribute.');
|
|
}
|
|
const { committer } = part;
|
|
const { element } = committer;
|
|
let previousClasses = previousClassesCache.get(part);
|
|
if (previousClasses === undefined) {
|
|
// Write static classes once
|
|
element.className = committer.strings.join(' ');
|
|
previousClassesCache.set(part, previousClasses = new Set());
|
|
}
|
|
const { classList } = element;
|
|
// Remove old classes that no longer apply
|
|
// We use forEach() instead of for-of so that re don't require down-level
|
|
// iteration.
|
|
previousClasses.forEach((name) => {
|
|
if (!(name in classInfo)) {
|
|
classList.remove(name);
|
|
previousClasses.delete(name);
|
|
}
|
|
});
|
|
// Add or remove classes based on their classMap value
|
|
for (const name in classInfo) {
|
|
const value = classInfo[name];
|
|
// We explicitly want a loose truthy check of `value` because it seems more
|
|
// convenient that '' and 0 are skipped.
|
|
if (value != previousClasses.has(name)) {
|
|
if (value) {
|
|
classList.add(name);
|
|
previousClasses.add(name);
|
|
}
|
|
else {
|
|
classList.remove(name);
|
|
previousClasses.delete(name);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @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
|
|
*/
|
|
const previousValues = new WeakMap();
|
|
/**
|
|
* Prevents re-render of a template function until a single value or an array of
|
|
* values changes.
|
|
*
|
|
* Example:
|
|
*
|
|
* ```js
|
|
* html`
|
|
* <div>
|
|
* ${guard([user.id, company.id], () => html`...`)}
|
|
* </div>
|
|
* ```
|
|
*
|
|
* In this case, the template only renders if either `user.id` or `company.id`
|
|
* changes.
|
|
*
|
|
* guard() is useful with immutable data patterns, by preventing expensive work
|
|
* until data updates.
|
|
*
|
|
* Example:
|
|
*
|
|
* ```js
|
|
* html`
|
|
* <div>
|
|
* ${guard([immutableItems], () => immutableItems.map(i => html`${i}`))}
|
|
* </div>
|
|
* ```
|
|
*
|
|
* In this case, items are mapped over only when the array reference changes.
|
|
*
|
|
* @param value the value to check before re-rendering
|
|
* @param f the template function
|
|
*/
|
|
const guard = directive((value, f) => (part) => {
|
|
const previousValue = previousValues.get(part);
|
|
if (Array.isArray(value)) {
|
|
// Dirty-check arrays by item
|
|
if (Array.isArray(previousValue) &&
|
|
previousValue.length === value.length &&
|
|
value.every((v, i) => v === previousValue[i])) {
|
|
return;
|
|
}
|
|
}
|
|
else if (previousValue === value &&
|
|
(value !== undefined || previousValues.has(part))) {
|
|
// Dirty-check non-arrays by identity
|
|
return;
|
|
}
|
|
part.setValue(f());
|
|
// Copy the value if it's an array so that if it's mutated we don't forget
|
|
// what the previous values were.
|
|
previousValues.set(part, Array.isArray(value) ? Array.from(value) : value);
|
|
});
|
|
|
|
/**
|
|
* @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
|
|
*/
|
|
/**
|
|
* For AttributeParts, sets the attribute if the value is defined and removes
|
|
* the attribute if the value is undefined.
|
|
*
|
|
* For other part types, this directive is a no-op.
|
|
*/
|
|
const ifDefined = directive((value) => (part) => {
|
|
if (value === undefined && part instanceof AttributePart) {
|
|
if (value !== part.value) {
|
|
const name = part.committer.name;
|
|
part.committer.element.removeAttribute(name);
|
|
}
|
|
}
|
|
else {
|
|
part.setValue(value);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @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
|
|
*/
|
|
// Helper functions for manipulating parts
|
|
// TODO(kschaaf): Refactor into Part API?
|
|
const createAndInsertPart = (containerPart, beforePart) => {
|
|
const container = containerPart.startNode.parentNode;
|
|
const beforeNode = beforePart == null ? containerPart.endNode : beforePart.startNode;
|
|
const startNode = container.insertBefore(createMarker(), beforeNode);
|
|
container.insertBefore(createMarker(), beforeNode);
|
|
const newPart = new NodePart(containerPart.options, undefined);
|
|
newPart.insertAfterNode(startNode);
|
|
return newPart;
|
|
};
|
|
const updatePart = (part, value) => {
|
|
part.setValue(value);
|
|
part.commit();
|
|
return part;
|
|
};
|
|
const insertPartBefore = (containerPart, part, ref) => {
|
|
const container = containerPart.startNode.parentNode;
|
|
const beforeNode = ref ? ref.startNode : containerPart.endNode;
|
|
const endNode = part.endNode.nextSibling;
|
|
if (endNode !== beforeNode) {
|
|
reparentNodes(container, part.startNode, endNode, beforeNode);
|
|
}
|
|
};
|
|
const removePart = (part) => {
|
|
removeNodes(part.startNode.parentNode, part.startNode, part.endNode.nextSibling);
|
|
};
|
|
// Helper for generating a map of array item to its index over a subset
|
|
// of an array (used to lazily generate `newKeyToIndexMap` and
|
|
// `oldKeyToIndexMap`)
|
|
const generateMap = (list, start, end) => {
|
|
const map = new Map();
|
|
for (let i = start; i <= end; i++) {
|
|
map.set(list[i], i);
|
|
}
|
|
return map;
|
|
};
|
|
// Stores previous ordered list of parts and map of key to index
|
|
const partListCache = new WeakMap();
|
|
const keyListCache = new WeakMap();
|
|
/**
|
|
* A directive that repeats a series of values (usually `TemplateResults`)
|
|
* generated from an iterable, and updates those items efficiently when the
|
|
* iterable changes based on user-provided `keys` associated with each item.
|
|
*
|
|
* Note that if a `keyFn` is provided, strict key-to-DOM mapping is maintained,
|
|
* meaning previous DOM for a given key is moved into the new position if
|
|
* needed, and DOM will never be reused with values for different keys (new DOM
|
|
* will always be created for new keys). This is generally the most efficient
|
|
* way to use `repeat` since it performs minimum unnecessary work for insertions
|
|
* and removals.
|
|
*
|
|
* IMPORTANT: If providing a `keyFn`, keys *must* be unique for all items in a
|
|
* given call to `repeat`. The behavior when two or more items have the same key
|
|
* is undefined.
|
|
*
|
|
* If no `keyFn` is provided, this directive will perform similar to mapping
|
|
* items to values, and DOM will be reused against potentially different items.
|
|
*/
|
|
const repeat = directive((items, keyFnOrTemplate, template) => {
|
|
let keyFn;
|
|
if (template === undefined) {
|
|
template = keyFnOrTemplate;
|
|
}
|
|
else if (keyFnOrTemplate !== undefined) {
|
|
keyFn = keyFnOrTemplate;
|
|
}
|
|
return (containerPart) => {
|
|
if (!(containerPart instanceof NodePart)) {
|
|
throw new Error('repeat can only be used in text bindings');
|
|
}
|
|
// Old part & key lists are retrieved from the last update
|
|
// (associated with the part for this instance of the directive)
|
|
const oldParts = partListCache.get(containerPart) || [];
|
|
const oldKeys = keyListCache.get(containerPart) || [];
|
|
// New part list will be built up as we go (either reused from
|
|
// old parts or created for new keys in this update). This is
|
|
// saved in the above cache at the end of the update.
|
|
const newParts = [];
|
|
// New value list is eagerly generated from items along with a
|
|
// parallel array indicating its key.
|
|
const newValues = [];
|
|
const newKeys = [];
|
|
let index = 0;
|
|
for (const item of items) {
|
|
newKeys[index] = keyFn ? keyFn(item, index) : index;
|
|
newValues[index] = template(item, index);
|
|
index++;
|
|
}
|
|
// Maps from key to index for current and previous update; these
|
|
// are generated lazily only when needed as a performance
|
|
// optimization, since they are only required for multiple
|
|
// non-contiguous changes in the list, which are less common.
|
|
let newKeyToIndexMap;
|
|
let oldKeyToIndexMap;
|
|
// Head and tail pointers to old parts and new values
|
|
let oldHead = 0;
|
|
let oldTail = oldParts.length - 1;
|
|
let newHead = 0;
|
|
let newTail = newValues.length - 1;
|
|
// Overview of O(n) reconciliation algorithm (general approach
|
|
// based on ideas found in ivi, vue, snabbdom, etc.):
|
|
//
|
|
// * We start with the list of old parts and new values (and
|
|
// arrays of their respective keys), head/tail pointers into
|
|
// each, and we build up the new list of parts by updating
|
|
// (and when needed, moving) old parts or creating new ones.
|
|
// The initial scenario might look like this (for brevity of
|
|
// the diagrams, the numbers in the array reflect keys
|
|
// associated with the old parts or new values, although keys
|
|
// and parts/values are actually stored in parallel arrays
|
|
// indexed using the same head/tail pointers):
|
|
//
|
|
// oldHead v v oldTail
|
|
// oldKeys: [0, 1, 2, 3, 4, 5, 6]
|
|
// newParts: [ , , , , , , ]
|
|
// newKeys: [0, 2, 1, 4, 3, 7, 6] <- reflects the user's new
|
|
// item order
|
|
// newHead ^ ^ newTail
|
|
//
|
|
// * Iterate old & new lists from both sides, updating,
|
|
// swapping, or removing parts at the head/tail locations
|
|
// until neither head nor tail can move.
|
|
//
|
|
// * Example below: keys at head pointers match, so update old
|
|
// part 0 in-place (no need to move it) and record part 0 in
|
|
// the `newParts` list. The last thing we do is advance the
|
|
// `oldHead` and `newHead` pointers (will be reflected in the
|
|
// next diagram).
|
|
//
|
|
// oldHead v v oldTail
|
|
// oldKeys: [0, 1, 2, 3, 4, 5, 6]
|
|
// newParts: [0, , , , , , ] <- heads matched: update 0
|
|
// newKeys: [0, 2, 1, 4, 3, 7, 6] and advance both oldHead
|
|
// & newHead
|
|
// newHead ^ ^ newTail
|
|
//
|
|
// * Example below: head pointers don't match, but tail
|
|
// pointers do, so update part 6 in place (no need to move
|
|
// it), and record part 6 in the `newParts` list. Last,
|
|
// advance the `oldTail` and `oldHead` pointers.
|
|
//
|
|
// oldHead v v oldTail
|
|
// oldKeys: [0, 1, 2, 3, 4, 5, 6]
|
|
// newParts: [0, , , , , , 6] <- tails matched: update 6
|
|
// newKeys: [0, 2, 1, 4, 3, 7, 6] and advance both oldTail
|
|
// & newTail
|
|
// newHead ^ ^ newTail
|
|
//
|
|
// * If neither head nor tail match; next check if one of the
|
|
// old head/tail items was removed. We first need to generate
|
|
// the reverse map of new keys to index (`newKeyToIndexMap`),
|
|
// which is done once lazily as a performance optimization,
|
|
// since we only hit this case if multiple non-contiguous
|
|
// changes were made. Note that for contiguous removal
|
|
// anywhere in the list, the head and tails would advance
|
|
// from either end and pass each other before we get to this
|
|
// case and removals would be handled in the final while loop
|
|
// without needing to generate the map.
|
|
//
|
|
// * Example below: The key at `oldTail` was removed (no longer
|
|
// in the `newKeyToIndexMap`), so remove that part from the
|
|
// DOM and advance just the `oldTail` pointer.
|
|
//
|
|
// oldHead v v oldTail
|
|
// oldKeys: [0, 1, 2, 3, 4, 5, 6]
|
|
// newParts: [0, , , , , , 6] <- 5 not in new map: remove
|
|
// newKeys: [0, 2, 1, 4, 3, 7, 6] 5 and advance oldTail
|
|
// newHead ^ ^ newTail
|
|
//
|
|
// * Once head and tail cannot move, any mismatches are due to
|
|
// either new or moved items; if a new key is in the previous
|
|
// "old key to old index" map, move the old part to the new
|
|
// location, otherwise create and insert a new part. Note
|
|
// that when moving an old part we null its position in the
|
|
// oldParts array if it lies between the head and tail so we
|
|
// know to skip it when the pointers get there.
|
|
//
|
|
// * Example below: neither head nor tail match, and neither
|
|
// were removed; so find the `newHead` key in the
|
|
// `oldKeyToIndexMap`, and move that old part's DOM into the
|
|
// next head position (before `oldParts[oldHead]`). Last,
|
|
// null the part in the `oldPart` array since it was
|
|
// somewhere in the remaining oldParts still to be scanned
|
|
// (between the head and tail pointers) so that we know to
|
|
// skip that old part on future iterations.
|
|
//
|
|
// oldHead v v oldTail
|
|
// oldKeys: [0, 1, -, 3, 4, 5, 6]
|
|
// newParts: [0, 2, , , , , 6] <- stuck: update & move 2
|
|
// newKeys: [0, 2, 1, 4, 3, 7, 6] into place and advance
|
|
// newHead
|
|
// newHead ^ ^ newTail
|
|
//
|
|
// * Note that for moves/insertions like the one above, a part
|
|
// inserted at the head pointer is inserted before the
|
|
// current `oldParts[oldHead]`, and a part inserted at the
|
|
// tail pointer is inserted before `newParts[newTail+1]`. The
|
|
// seeming asymmetry lies in the fact that new parts are
|
|
// moved into place outside in, so to the right of the head
|
|
// pointer are old parts, and to the right of the tail
|
|
// pointer are new parts.
|
|
//
|
|
// * We always restart back from the top of the algorithm,
|
|
// allowing matching and simple updates in place to
|
|
// continue...
|
|
//
|
|
// * Example below: the head pointers once again match, so
|
|
// simply update part 1 and record it in the `newParts`
|
|
// array. Last, advance both head pointers.
|
|
//
|
|
// oldHead v v oldTail
|
|
// oldKeys: [0, 1, -, 3, 4, 5, 6]
|
|
// newParts: [0, 2, 1, , , , 6] <- heads matched: update 1
|
|
// newKeys: [0, 2, 1, 4, 3, 7, 6] and advance both oldHead
|
|
// & newHead
|
|
// newHead ^ ^ newTail
|
|
//
|
|
// * As mentioned above, items that were moved as a result of
|
|
// being stuck (the final else clause in the code below) are
|
|
// marked with null, so we always advance old pointers over
|
|
// these so we're comparing the next actual old value on
|
|
// either end.
|
|
//
|
|
// * Example below: `oldHead` is null (already placed in
|
|
// newParts), so advance `oldHead`.
|
|
//
|
|
// oldHead v v oldTail
|
|
// oldKeys: [0, 1, -, 3, 4, 5, 6] <- old head already used:
|
|
// newParts: [0, 2, 1, , , , 6] advance oldHead
|
|
// newKeys: [0, 2, 1, 4, 3, 7, 6]
|
|
// newHead ^ ^ newTail
|
|
//
|
|
// * Note it's not critical to mark old parts as null when they
|
|
// are moved from head to tail or tail to head, since they
|
|
// will be outside the pointer range and never visited again.
|
|
//
|
|
// * Example below: Here the old tail key matches the new head
|
|
// key, so the part at the `oldTail` position and move its
|
|
// DOM to the new head position (before `oldParts[oldHead]`).
|
|
// Last, advance `oldTail` and `newHead` pointers.
|
|
//
|
|
// oldHead v v oldTail
|
|
// oldKeys: [0, 1, -, 3, 4, 5, 6]
|
|
// newParts: [0, 2, 1, 4, , , 6] <- old tail matches new
|
|
// newKeys: [0, 2, 1, 4, 3, 7, 6] head: update & move 4,
|
|
// advance oldTail & newHead
|
|
// newHead ^ ^ newTail
|
|
//
|
|
// * Example below: Old and new head keys match, so update the
|
|
// old head part in place, and advance the `oldHead` and
|
|
// `newHead` pointers.
|
|
//
|
|
// oldHead v oldTail
|
|
// oldKeys: [0, 1, -, 3, 4, 5, 6]
|
|
// newParts: [0, 2, 1, 4, 3, ,6] <- heads match: update 3
|
|
// newKeys: [0, 2, 1, 4, 3, 7, 6] and advance oldHead &
|
|
// newHead
|
|
// newHead ^ ^ newTail
|
|
//
|
|
// * Once the new or old pointers move past each other then all
|
|
// we have left is additions (if old list exhausted) or
|
|
// removals (if new list exhausted). Those are handled in the
|
|
// final while loops at the end.
|
|
//
|
|
// * Example below: `oldHead` exceeded `oldTail`, so we're done
|
|
// with the main loop. Create the remaining part and insert
|
|
// it at the new head position, and the update is complete.
|
|
//
|
|
// (oldHead > oldTail)
|
|
// oldKeys: [0, 1, -, 3, 4, 5, 6]
|
|
// newParts: [0, 2, 1, 4, 3, 7 ,6] <- create and insert 7
|
|
// newKeys: [0, 2, 1, 4, 3, 7, 6]
|
|
// newHead ^ newTail
|
|
//
|
|
// * Note that the order of the if/else clauses is not
|
|
// important to the algorithm, as long as the null checks
|
|
// come first (to ensure we're always working on valid old
|
|
// parts) and that the final else clause comes last (since
|
|
// that's where the expensive moves occur). The order of
|
|
// remaining clauses is is just a simple guess at which cases
|
|
// will be most common.
|
|
//
|
|
// * TODO(kschaaf) Note, we could calculate the longest
|
|
// increasing subsequence (LIS) of old items in new position,
|
|
// and only move those not in the LIS set. However that costs
|
|
// O(nlogn) time and adds a bit more code, and only helps
|
|
// make rare types of mutations require fewer moves. The
|
|
// above handles removes, adds, reversal, swaps, and single
|
|
// moves of contiguous items in linear time, in the minimum
|
|
// number of moves. As the number of multiple moves where LIS
|
|
// might help approaches a random shuffle, the LIS
|
|
// optimization becomes less helpful, so it seems not worth
|
|
// the code at this point. Could reconsider if a compelling
|
|
// case arises.
|
|
while (oldHead <= oldTail && newHead <= newTail) {
|
|
if (oldParts[oldHead] === null) {
|
|
// `null` means old part at head has already been used
|
|
// below; skip
|
|
oldHead++;
|
|
}
|
|
else if (oldParts[oldTail] === null) {
|
|
// `null` means old part at tail has already been used
|
|
// below; skip
|
|
oldTail--;
|
|
}
|
|
else if (oldKeys[oldHead] === newKeys[newHead]) {
|
|
// Old head matches new head; update in place
|
|
newParts[newHead] =
|
|
updatePart(oldParts[oldHead], newValues[newHead]);
|
|
oldHead++;
|
|
newHead++;
|
|
}
|
|
else if (oldKeys[oldTail] === newKeys[newTail]) {
|
|
// Old tail matches new tail; update in place
|
|
newParts[newTail] =
|
|
updatePart(oldParts[oldTail], newValues[newTail]);
|
|
oldTail--;
|
|
newTail--;
|
|
}
|
|
else if (oldKeys[oldHead] === newKeys[newTail]) {
|
|
// Old head matches new tail; update and move to new tail
|
|
newParts[newTail] =
|
|
updatePart(oldParts[oldHead], newValues[newTail]);
|
|
insertPartBefore(containerPart, oldParts[oldHead], newParts[newTail + 1]);
|
|
oldHead++;
|
|
newTail--;
|
|
}
|
|
else if (oldKeys[oldTail] === newKeys[newHead]) {
|
|
// Old tail matches new head; update and move to new head
|
|
newParts[newHead] =
|
|
updatePart(oldParts[oldTail], newValues[newHead]);
|
|
insertPartBefore(containerPart, oldParts[oldTail], oldParts[oldHead]);
|
|
oldTail--;
|
|
newHead++;
|
|
}
|
|
else {
|
|
if (newKeyToIndexMap === undefined) {
|
|
// Lazily generate key-to-index maps, used for removals &
|
|
// moves below
|
|
newKeyToIndexMap = generateMap(newKeys, newHead, newTail);
|
|
oldKeyToIndexMap = generateMap(oldKeys, oldHead, oldTail);
|
|
}
|
|
if (!newKeyToIndexMap.has(oldKeys[oldHead])) {
|
|
// Old head is no longer in new list; remove
|
|
removePart(oldParts[oldHead]);
|
|
oldHead++;
|
|
}
|
|
else if (!newKeyToIndexMap.has(oldKeys[oldTail])) {
|
|
// Old tail is no longer in new list; remove
|
|
removePart(oldParts[oldTail]);
|
|
oldTail--;
|
|
}
|
|
else {
|
|
// Any mismatches at this point are due to additions or
|
|
// moves; see if we have an old part we can reuse and move
|
|
// into place
|
|
const oldIndex = oldKeyToIndexMap.get(newKeys[newHead]);
|
|
const oldPart = oldIndex !== undefined ? oldParts[oldIndex] : null;
|
|
if (oldPart === null) {
|
|
// No old part for this value; create a new one and
|
|
// insert it
|
|
const newPart = createAndInsertPart(containerPart, oldParts[oldHead]);
|
|
updatePart(newPart, newValues[newHead]);
|
|
newParts[newHead] = newPart;
|
|
}
|
|
else {
|
|
// Reuse old part
|
|
newParts[newHead] =
|
|
updatePart(oldPart, newValues[newHead]);
|
|
insertPartBefore(containerPart, oldPart, oldParts[oldHead]);
|
|
// This marks the old part as having been used, so that
|
|
// it will be skipped in the first two checks above
|
|
oldParts[oldIndex] = null;
|
|
}
|
|
newHead++;
|
|
}
|
|
}
|
|
}
|
|
// Add parts for any remaining new values
|
|
while (newHead <= newTail) {
|
|
// For all remaining additions, we insert before last new
|
|
// tail, since old pointers are no longer valid
|
|
const newPart = createAndInsertPart(containerPart, newParts[newTail + 1]);
|
|
updatePart(newPart, newValues[newHead]);
|
|
newParts[newHead++] = newPart;
|
|
}
|
|
// Remove any remaining unused old parts
|
|
while (oldHead <= oldTail) {
|
|
const oldPart = oldParts[oldHead++];
|
|
if (oldPart !== null) {
|
|
removePart(oldPart);
|
|
}
|
|
}
|
|
// Save order of new parts for next round
|
|
partListCache.set(containerPart, newParts);
|
|
keyListCache.set(containerPart, newKeys);
|
|
};
|
|
});
|
|
|
|
/**
|
|
* @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
|
|
*/
|
|
// For each part, remember the value that was last rendered to the part by the
|
|
// unsafeHTML directive, and the DocumentFragment that was last set as a value.
|
|
// The DocumentFragment is used as a unique key to check if the last value
|
|
// rendered to the part was with unsafeHTML. If not, we'll always re-render the
|
|
// value passed to unsafeHTML.
|
|
const previousValues$1 = new WeakMap();
|
|
/**
|
|
* Used to clone existing node instead of each time creating new one which is
|
|
* slower
|
|
*/
|
|
const emptyTemplateNode$1 = document.createElement('template');
|
|
/**
|
|
* Renders the result as HTML, rather than text.
|
|
*
|
|
* Note, this is unsafe to use with any user-provided input that hasn't been
|
|
* sanitized or escaped, as it may lead to cross-site-scripting
|
|
* vulnerabilities.
|
|
*/
|
|
const unsafeHTML = directive((value) => (part) => {
|
|
if (!(part instanceof NodePart)) {
|
|
throw new Error('unsafeHTML can only be used in text bindings');
|
|
}
|
|
const previousValue = previousValues$1.get(part);
|
|
if (previousValue !== undefined && isPrimitive(value) &&
|
|
value === previousValue.value && part.value === previousValue.fragment) {
|
|
return;
|
|
}
|
|
const template = emptyTemplateNode$1.cloneNode();
|
|
template.innerHTML = value; // innerHTML casts to string internally
|
|
const fragment = document.importNode(template.content, true);
|
|
part.setValue(fragment);
|
|
previousValues$1.set(part, { value, fragment });
|
|
});
|
|
|
|
/**
|
|
* @license
|
|
* Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
|
* This code may only be used under the BSD style license found at
|
|
* http://polymer.github.io/LICENSE.txt
|
|
* The complete set of authors may be found at
|
|
* http://polymer.github.io/AUTHORS.txt
|
|
* The complete set of contributors may be found at
|
|
* http://polymer.github.io/CONTRIBUTORS.txt
|
|
* Code distributed by Google as part of the polymer project is also
|
|
* subject to an additional IP rights grant found at
|
|
* http://polymer.github.io/PATENTS.txt
|
|
*/
|
|
const _state = new WeakMap();
|
|
// Effectively infinity, but a SMI.
|
|
const _infinity = 0x7fffffff;
|
|
/**
|
|
* Renders one of a series of values, including Promises, to a Part.
|
|
*
|
|
* Values are rendered in priority order, with the first argument having the
|
|
* highest priority and the last argument having the lowest priority. If a
|
|
* value is a Promise, low-priority values will be rendered until it resolves.
|
|
*
|
|
* The priority of values can be used to create placeholder content for async
|
|
* data. For example, a Promise with pending content can be the first,
|
|
* highest-priority, argument, and a non_promise loading indicator template can
|
|
* be used as the second, lower-priority, argument. The loading indicator will
|
|
* render immediately, and the primary content will render when the Promise
|
|
* resolves.
|
|
*
|
|
* Example:
|
|
*
|
|
* const content = fetch('./content.txt').then(r => r.text());
|
|
* html`${until(content, html`<span>Loading...</span>`)}`
|
|
*/
|
|
const until = directive((...args) => (part) => {
|
|
let state = _state.get(part);
|
|
if (state === undefined) {
|
|
state = {
|
|
lastRenderedIndex: _infinity,
|
|
values: [],
|
|
};
|
|
_state.set(part, state);
|
|
}
|
|
const previousValues = state.values;
|
|
let previousLength = previousValues.length;
|
|
state.values = args;
|
|
for (let i = 0; i < args.length; i++) {
|
|
// If we've rendered a higher-priority value already, stop.
|
|
if (i > state.lastRenderedIndex) {
|
|
break;
|
|
}
|
|
const value = args[i];
|
|
// Render non-Promise values immediately
|
|
if (isPrimitive(value) ||
|
|
typeof value.then !== 'function') {
|
|
part.setValue(value);
|
|
state.lastRenderedIndex = i;
|
|
// Since a lower-priority value will never overwrite a higher-priority
|
|
// synchronous value, we can stop processing now.
|
|
break;
|
|
}
|
|
// If this is a Promise we've already handled, skip it.
|
|
if (i < previousLength && value === previousValues[i]) {
|
|
continue;
|
|
}
|
|
// We have a Promise that we haven't seen before, so priorities may have
|
|
// changed. Forget what we rendered before.
|
|
state.lastRenderedIndex = _infinity;
|
|
previousLength = 0;
|
|
Promise.resolve(value).then((resolvedValue) => {
|
|
const index = state.values.indexOf(value);
|
|
// If state.values doesn't contain the value, we've re-rendered without
|
|
// the value, so don't render it. Then, only render if the value is
|
|
// higher-priority than what's already been rendered.
|
|
if (index > -1 && index < state.lastRenderedIndex) {
|
|
state.lastRenderedIndex = index;
|
|
part.setValue(resolvedValue);
|
|
part.commit();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
const detached = new WeakMap();
|
|
class Detach extends Directive {
|
|
constructor(ifFn) {
|
|
super();
|
|
this.ifFn = ifFn;
|
|
}
|
|
body(part) {
|
|
const detach = this.ifFn();
|
|
const element = part.committer.element;
|
|
if (detach) {
|
|
if (!detached.has(part)) {
|
|
const nextSibling = element.nextSibling;
|
|
detached.set(part, { element, nextSibling });
|
|
}
|
|
element.remove();
|
|
}
|
|
else {
|
|
const data = detached.get(part);
|
|
if (typeof data !== 'undefined' && data !== null) {
|
|
data.nextSibling.parentNode.insertBefore(data.element, data.nextSibling);
|
|
detached.delete(part);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const toRemove = [], toUpdate = [];
|
|
class StyleMap extends Directive {
|
|
constructor(styleInfo, detach = false) {
|
|
super();
|
|
this.previous = {};
|
|
this.style = styleInfo;
|
|
this.detach = detach;
|
|
}
|
|
setStyle(styleInfo) {
|
|
this.style = styleInfo;
|
|
}
|
|
setDetach(detach) {
|
|
this.detach = detach;
|
|
}
|
|
body(part) {
|
|
toRemove.length = 0;
|
|
toUpdate.length = 0;
|
|
// @ts-ignore
|
|
const element = part.committer.element;
|
|
const style = element.style;
|
|
let previous = this.previous;
|
|
for (const name in previous) {
|
|
if (this.style[name] === undefined) {
|
|
toRemove.push(name);
|
|
}
|
|
}
|
|
for (const name in this.style) {
|
|
const value = this.style[name];
|
|
const prev = previous[name];
|
|
if (prev !== undefined && prev === value) {
|
|
continue;
|
|
}
|
|
toUpdate.push(name);
|
|
}
|
|
if (toRemove.length || toUpdate.length) {
|
|
let parent, nextSibling;
|
|
if (this.detach) {
|
|
parent = element.parentNode;
|
|
if (parent) {
|
|
nextSibling = element.nextSibling;
|
|
element.remove();
|
|
}
|
|
}
|
|
for (const name of toRemove) {
|
|
style.removeProperty(name);
|
|
}
|
|
for (const name of toUpdate) {
|
|
const value = this.style[name];
|
|
if (!name.includes('-')) {
|
|
style[name] = value;
|
|
}
|
|
else {
|
|
style.setProperty(name, value);
|
|
}
|
|
}
|
|
if (this.detach && parent) {
|
|
parent.insertBefore(element, nextSibling);
|
|
}
|
|
this.previous = Object.assign({}, this.style);
|
|
}
|
|
}
|
|
}
|
|
|
|
class Action {
|
|
constructor() {
|
|
this.isAction = true;
|
|
}
|
|
}
|
|
Action.prototype.isAction = true;
|
|
|
|
const defaultOptions = {
|
|
element: document.createTextNode(''),
|
|
axis: 'xy',
|
|
threshold: 10,
|
|
onDown(data) { },
|
|
onMove(data) { },
|
|
onUp(data) { },
|
|
onWheel(data) { }
|
|
};
|
|
const pointerEventsExists = typeof PointerEvent !== 'undefined';
|
|
let id = 0;
|
|
class PointerAction extends Action {
|
|
constructor(element, data) {
|
|
super();
|
|
this.moving = '';
|
|
this.initialX = 0;
|
|
this.initialY = 0;
|
|
this.lastY = 0;
|
|
this.lastX = 0;
|
|
this.onPointerDown = this.onPointerDown.bind(this);
|
|
this.onPointerMove = this.onPointerMove.bind(this);
|
|
this.onPointerUp = this.onPointerUp.bind(this);
|
|
this.onWheel = this.onWheel.bind(this);
|
|
this.element = element;
|
|
this.id = ++id;
|
|
this.options = Object.assign(Object.assign({}, defaultOptions), data.pointerOptions);
|
|
if (pointerEventsExists) {
|
|
element.addEventListener('pointerdown', this.onPointerDown);
|
|
document.addEventListener('pointermove', this.onPointerMove);
|
|
document.addEventListener('pointerup', this.onPointerUp);
|
|
}
|
|
else {
|
|
element.addEventListener('touchstart', this.onPointerDown);
|
|
document.addEventListener('touchmove', this.onPointerMove, { passive: false });
|
|
document.addEventListener('touchend', this.onPointerUp);
|
|
document.addEventListener('touchcancel', this.onPointerUp);
|
|
element.addEventListener('mousedown', this.onPointerDown);
|
|
document.addEventListener('mousemove', this.onPointerMove, { passive: false });
|
|
document.addEventListener('mouseup', this.onPointerUp);
|
|
}
|
|
}
|
|
normalizeMouseWheelEvent(event) {
|
|
// @ts-ignore
|
|
let x = event.deltaX || 0;
|
|
// @ts-ignore
|
|
let y = event.deltaY || 0;
|
|
// @ts-ignore
|
|
let z = event.deltaZ || 0;
|
|
// @ts-ignore
|
|
const mode = event.deltaMode;
|
|
// @ts-ignore
|
|
const lineHeight = parseInt(getComputedStyle(event.target).getPropertyValue('line-height'));
|
|
let scale = 1;
|
|
switch (mode) {
|
|
case 1:
|
|
scale = lineHeight;
|
|
break;
|
|
case 2:
|
|
// @ts-ignore
|
|
scale = window.height;
|
|
break;
|
|
}
|
|
x *= scale;
|
|
y *= scale;
|
|
z *= scale;
|
|
return { x, y, z, event };
|
|
}
|
|
onWheel(event) {
|
|
const normalized = this.normalizeMouseWheelEvent(event);
|
|
this.options.onWheel(normalized);
|
|
}
|
|
normalizePointerEvent(event) {
|
|
let result = { x: 0, y: 0, pageX: 0, pageY: 0, clientX: 0, clientY: 0, screenX: 0, screenY: 0, event };
|
|
switch (event.type) {
|
|
case 'wheel':
|
|
const wheel = this.normalizeMouseWheelEvent(event);
|
|
result.x = wheel.x;
|
|
result.y = wheel.y;
|
|
result.pageX = result.x;
|
|
result.pageY = result.y;
|
|
result.screenX = result.x;
|
|
result.screenY = result.y;
|
|
result.clientX = result.x;
|
|
result.clientY = result.y;
|
|
break;
|
|
case 'touchstart':
|
|
case 'touchmove':
|
|
case 'touchend':
|
|
case 'touchcancel':
|
|
result.x = event.changedTouches[0].screenX;
|
|
result.y = event.changedTouches[0].screenY;
|
|
result.pageX = event.changedTouches[0].pageX;
|
|
result.pageY = event.changedTouches[0].pageY;
|
|
result.screenX = event.changedTouches[0].screenX;
|
|
result.screenY = event.changedTouches[0].screenY;
|
|
result.clientX = event.changedTouches[0].clientX;
|
|
result.clientY = event.changedTouches[0].clientY;
|
|
break;
|
|
default:
|
|
result.x = event.x;
|
|
result.y = event.y;
|
|
result.pageX = event.pageX;
|
|
result.pageY = event.pageY;
|
|
result.screenX = event.screenX;
|
|
result.screenY = event.screenY;
|
|
result.clientX = event.clientX;
|
|
result.clientY = event.clientY;
|
|
break;
|
|
}
|
|
return result;
|
|
}
|
|
onPointerDown(event) {
|
|
if (event.type === 'mousedown' && event.button !== 0)
|
|
return;
|
|
this.moving = 'xy';
|
|
const normalized = this.normalizePointerEvent(event);
|
|
this.lastX = normalized.x;
|
|
this.lastY = normalized.y;
|
|
this.initialX = normalized.x;
|
|
this.initialY = normalized.y;
|
|
this.options.onDown(normalized);
|
|
}
|
|
handleX(normalized) {
|
|
let movementX = normalized.x - this.lastX;
|
|
this.lastY = normalized.y;
|
|
this.lastX = normalized.x;
|
|
return movementX;
|
|
}
|
|
handleY(normalized) {
|
|
let movementY = normalized.y - this.lastY;
|
|
this.lastY = normalized.y;
|
|
this.lastX = normalized.x;
|
|
return movementY;
|
|
}
|
|
onPointerMove(event) {
|
|
if (this.moving === '' || (event.type === 'mousemove' && event.button !== 0))
|
|
return;
|
|
const normalized = this.normalizePointerEvent(event);
|
|
if (this.options.axis === 'x|y') {
|
|
let movementX = 0, movementY = 0;
|
|
if (this.moving === 'x' ||
|
|
(this.moving === 'xy' && Math.abs(normalized.x - this.initialX) > this.options.threshold)) {
|
|
this.moving = 'x';
|
|
movementX = this.handleX(normalized);
|
|
}
|
|
if (this.moving === 'y' ||
|
|
(this.moving === 'xy' && Math.abs(normalized.y - this.initialY) > this.options.threshold)) {
|
|
this.moving = 'y';
|
|
movementY = this.handleY(normalized);
|
|
}
|
|
this.options.onMove({
|
|
movementX,
|
|
movementY,
|
|
x: normalized.x,
|
|
y: normalized.y,
|
|
initialX: this.initialX,
|
|
initialY: this.initialY,
|
|
lastX: this.lastX,
|
|
lastY: this.lastY,
|
|
event
|
|
});
|
|
}
|
|
else if (this.options.axis === 'xy') {
|
|
let movementX = 0, movementY = 0;
|
|
if (Math.abs(normalized.x - this.initialX) > this.options.threshold) {
|
|
movementX = this.handleX(normalized);
|
|
}
|
|
if (Math.abs(normalized.y - this.initialY) > this.options.threshold) {
|
|
movementY = this.handleY(normalized);
|
|
}
|
|
this.options.onMove({
|
|
movementX,
|
|
movementY,
|
|
x: normalized.x,
|
|
y: normalized.y,
|
|
initialX: this.initialX,
|
|
initialY: this.initialY,
|
|
lastX: this.lastX,
|
|
lastY: this.lastY,
|
|
event
|
|
});
|
|
}
|
|
else if (this.options.axis === 'x') {
|
|
if (this.moving === 'x' ||
|
|
(this.moving === 'xy' && Math.abs(normalized.x - this.initialX) > this.options.threshold)) {
|
|
this.moving = 'x';
|
|
this.options.onMove({
|
|
movementX: this.handleX(normalized),
|
|
movementY: 0,
|
|
initialX: this.initialX,
|
|
initialY: this.initialY,
|
|
lastX: this.lastX,
|
|
lastY: this.lastY,
|
|
event
|
|
});
|
|
}
|
|
}
|
|
else if (this.options.axis === 'y') {
|
|
let movementY = 0;
|
|
if (this.moving === 'y' ||
|
|
(this.moving === 'xy' && Math.abs(normalized.y - this.initialY) > this.options.threshold)) {
|
|
this.moving = 'y';
|
|
movementY = this.handleY(normalized);
|
|
}
|
|
this.options.onMove({
|
|
movementX: 0,
|
|
movementY,
|
|
x: normalized.x,
|
|
y: normalized.y,
|
|
initialX: this.initialX,
|
|
initialY: this.initialY,
|
|
lastX: this.lastX,
|
|
lastY: this.lastY,
|
|
event
|
|
});
|
|
}
|
|
}
|
|
onPointerUp(event) {
|
|
this.moving = '';
|
|
const normalized = this.normalizePointerEvent(event);
|
|
this.options.onUp({
|
|
movementX: 0,
|
|
movementY: 0,
|
|
x: normalized.x,
|
|
y: normalized.y,
|
|
initialX: this.initialX,
|
|
initialY: this.initialY,
|
|
lastX: this.lastX,
|
|
lastY: this.lastY,
|
|
event
|
|
});
|
|
this.lastY = 0;
|
|
this.lastX = 0;
|
|
}
|
|
destroy(element) {
|
|
if (pointerEventsExists) {
|
|
element.removeEventListener('pointerdown', this.onPointerDown);
|
|
document.removeEventListener('pointermove', this.onPointerMove);
|
|
document.removeEventListener('pointerup', this.onPointerUp);
|
|
}
|
|
else {
|
|
element.removeEventListener('mousedown', this.onPointerDown);
|
|
document.removeEventListener('mousemove', this.onPointerMove);
|
|
document.removeEventListener('mouseup', this.onPointerUp);
|
|
element.removeEventListener('touchstart', this.onPointerDown);
|
|
document.removeEventListener('touchmove', this.onPointerMove);
|
|
document.removeEventListener('touchend', this.onPointerUp);
|
|
document.removeEventListener('touchcancel', this.onPointerUp);
|
|
}
|
|
}
|
|
}
|
|
|
|
function getPublicComponentMethods(components, actionsByInstance, clone) {
|
|
return class PublicComponentMethods {
|
|
constructor(instance, vidoInstance, props = {}) {
|
|
this.instance = instance;
|
|
this.name = vidoInstance.name;
|
|
this.vidoInstance = vidoInstance;
|
|
this.props = props;
|
|
this.destroy = this.destroy.bind(this);
|
|
this.update = this.update.bind(this);
|
|
this.change = this.change.bind(this);
|
|
this.html = this.html.bind(this);
|
|
}
|
|
/**
|
|
* Destroy component
|
|
*/
|
|
destroy() {
|
|
if (this.vidoInstance.debug) {
|
|
console.groupCollapsed(`destroying component ${this.instance}`);
|
|
console.log(clone({ components: components.keys(), actionsByInstance }));
|
|
console.trace();
|
|
console.groupEnd();
|
|
}
|
|
return this.vidoInstance.destroyComponent(this.instance, this.vidoInstance);
|
|
}
|
|
/**
|
|
* Update template - trigger rendering process
|
|
*/
|
|
update() {
|
|
if (this.vidoInstance.debug) {
|
|
console.groupCollapsed(`updating component ${this.instance}`);
|
|
console.log(clone({ components: components.keys(), actionsByInstance }));
|
|
console.trace();
|
|
console.groupEnd();
|
|
}
|
|
return this.vidoInstance.updateTemplate(this.vidoInstance);
|
|
}
|
|
/**
|
|
* Change component input properties
|
|
* @param {any} newProps
|
|
*/
|
|
change(newProps, options) {
|
|
if (this.vidoInstance.debug) {
|
|
console.groupCollapsed(`changing component ${this.instance}`);
|
|
console.log(clone({ props: this.props, newProps: newProps, components: components.keys(), actionsByInstance }));
|
|
console.trace();
|
|
console.groupEnd();
|
|
}
|
|
components.get(this.instance).change(newProps, options);
|
|
}
|
|
/**
|
|
* Get component lit-html template
|
|
* @param {} templateProps
|
|
*/
|
|
html(templateProps = {}) {
|
|
const component = components.get(this.instance);
|
|
if (component) {
|
|
return component.update(templateProps, this.vidoInstance);
|
|
}
|
|
return undefined;
|
|
}
|
|
_getComponents() {
|
|
return components;
|
|
}
|
|
_getActions() {
|
|
return actionsByInstance;
|
|
}
|
|
};
|
|
}
|
|
|
|
function getActionsCollector(actionsByInstance) {
|
|
return class ActionsCollector extends Directive {
|
|
constructor(instance) {
|
|
super();
|
|
this.instance = instance;
|
|
}
|
|
set(actions, props) {
|
|
this.actions = actions;
|
|
this.props = props;
|
|
// props must be mutable! (do not do this -> {...props})
|
|
// because we will modify action props with onChange and can reuse existin instance
|
|
return this;
|
|
}
|
|
body(part) {
|
|
const element = part.committer.element;
|
|
for (const create of this.actions) {
|
|
if (typeof create !== 'undefined') {
|
|
let exists;
|
|
if (actionsByInstance.has(this.instance)) {
|
|
for (const action of actionsByInstance.get(this.instance)) {
|
|
if (action.componentAction.create === create && action.element === element) {
|
|
exists = action;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!exists) {
|
|
// @ts-ignore
|
|
if (typeof element.vido !== 'undefined')
|
|
delete element.vido;
|
|
const componentAction = {
|
|
create,
|
|
update() { },
|
|
destroy() { }
|
|
};
|
|
const action = { instance: this.instance, componentAction, element, props: this.props };
|
|
let byInstance = [];
|
|
if (actionsByInstance.has(this.instance)) {
|
|
byInstance = actionsByInstance.get(this.instance);
|
|
}
|
|
byInstance.push(action);
|
|
actionsByInstance.set(this.instance, byInstance);
|
|
}
|
|
else {
|
|
exists.props = this.props;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
function getInternalComponentMethods(components, actionsByInstance, clone) {
|
|
return class InternalComponentMethods {
|
|
constructor(instance, vidoInstance, renderFunction, content) {
|
|
this.instance = instance;
|
|
this.vidoInstance = vidoInstance;
|
|
this.renderFunction = renderFunction;
|
|
this.content = content;
|
|
}
|
|
destroy() {
|
|
var _a;
|
|
if (this.vidoInstance.debug) {
|
|
console.groupCollapsed(`component destroy method fired ${this.instance}`);
|
|
console.log(clone({
|
|
props: this.vidoInstance.props,
|
|
components: components.keys(),
|
|
destroyable: this.vidoInstance.destroyable,
|
|
actionsByInstance
|
|
}));
|
|
console.trace();
|
|
console.groupEnd();
|
|
}
|
|
if (typeof ((_a = this.content) === null || _a === void 0 ? void 0 : _a.destroy) === 'function') {
|
|
this.content.destroy();
|
|
}
|
|
for (const d of this.vidoInstance.destroyable) {
|
|
d();
|
|
}
|
|
this.vidoInstance.onChangeFunctions = [];
|
|
this.vidoInstance.destroyable = [];
|
|
this.vidoInstance.update();
|
|
}
|
|
update(props = {}) {
|
|
if (this.vidoInstance.debug) {
|
|
console.groupCollapsed(`component update method fired ${this.instance}`);
|
|
console.log(clone({ components: components.keys(), actionsByInstance }));
|
|
console.trace();
|
|
console.groupEnd();
|
|
}
|
|
return this.renderFunction(props);
|
|
}
|
|
change(changedProps, options = { leave: false }) {
|
|
const props = changedProps;
|
|
if (this.vidoInstance.debug) {
|
|
console.groupCollapsed(`component change method fired ${this.instance}`);
|
|
console.log(clone({
|
|
props,
|
|
components: components.keys(),
|
|
onChangeFunctions: this.vidoInstance.onChangeFunctions,
|
|
changedProps,
|
|
actionsByInstance
|
|
}));
|
|
console.trace();
|
|
console.groupEnd();
|
|
}
|
|
for (const fn of this.vidoInstance.onChangeFunctions) {
|
|
fn(changedProps, options);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Schedule - a throttle function that uses requestAnimationFrame to limit the rate at which a function is called.
|
|
*
|
|
* @param {function} fn
|
|
* @returns {function}
|
|
*/
|
|
function schedule(fn) {
|
|
let frameId = 0;
|
|
function wrapperFn(argument) {
|
|
if (frameId) {
|
|
return;
|
|
}
|
|
function executeFrame() {
|
|
frameId = 0;
|
|
fn.apply(undefined, [argument]);
|
|
}
|
|
frameId = requestAnimationFrame(executeFrame);
|
|
}
|
|
return wrapperFn;
|
|
}
|
|
/**
|
|
* Is object - helper function to determine if specified variable is an object
|
|
*
|
|
* @param {any} item
|
|
* @returns {boolean}
|
|
*/
|
|
function isObject(item) {
|
|
return item && typeof item === 'object' && !Array.isArray(item);
|
|
}
|
|
/**
|
|
* Merge deep - helper function which will merge objects recursively - creating brand new one - like clone
|
|
*
|
|
* @param {object} target
|
|
* @params {object} sources
|
|
* @returns {object}
|
|
*/
|
|
function mergeDeep(target, ...sources) {
|
|
const source = sources.shift();
|
|
if (isObject(target) && isObject(source)) {
|
|
for (const key in source) {
|
|
if (isObject(source[key])) {
|
|
if (typeof target[key] === 'undefined') {
|
|
target[key] = {};
|
|
}
|
|
target[key] = mergeDeep(target[key], source[key]);
|
|
}
|
|
else if (Array.isArray(source[key])) {
|
|
target[key] = [];
|
|
for (let item of source[key]) {
|
|
if (isObject(item)) {
|
|
target[key].push(mergeDeep({}, item));
|
|
continue;
|
|
}
|
|
target[key].push(item);
|
|
}
|
|
}
|
|
else {
|
|
target[key] = source[key];
|
|
}
|
|
}
|
|
}
|
|
if (!sources.length) {
|
|
return target;
|
|
}
|
|
return mergeDeep(target, ...sources);
|
|
}
|
|
/**
|
|
* Clone helper function
|
|
*
|
|
* @param source
|
|
* @returns {object} cloned source
|
|
*/
|
|
function clone(source) {
|
|
if (typeof source.actions !== 'undefined') {
|
|
const actns = source.actions.map((action) => {
|
|
const result = Object.assign({}, action);
|
|
const props = Object.assign({}, result.props);
|
|
delete props.state;
|
|
delete props.api;
|
|
delete result.element;
|
|
result.props = props;
|
|
return result;
|
|
});
|
|
source.actions = actns;
|
|
}
|
|
return mergeDeep({}, source);
|
|
}
|
|
|
|
/* dev imports
|
|
import { render, html, directive, svg, Part } from '../lit-html';
|
|
import { asyncAppend } from '../lit-html/directives/async-append';
|
|
import { asyncReplace } from '../lit-html/directives/async-replace';
|
|
import { cache } from '../lit-html/directives/cache';
|
|
import { classMap } from '../lit-html/directives/class-map';
|
|
import { guard } from '../lit-html/directives/guard';
|
|
import { ifDefined } from '../lit-html/directives/if-defined';
|
|
import { repeat } from '../lit-html/directives/repeat';
|
|
import { unsafeHTML } from '../lit-html/directives/unsafe-html';
|
|
import { until } from '../lit-html/directives/until';
|
|
import { Directive } from '../lit-html/lib/directive';
|
|
*/
|
|
/**
|
|
* Vido library
|
|
*
|
|
* @param {any} state - state management for the view (can be anything)
|
|
* @param {any} api - some api's or other globally available services
|
|
* @returns {object} vido instance
|
|
*/
|
|
function Vido(state, api) {
|
|
let componentId = 0;
|
|
const components = new Map();
|
|
let actionsByInstance = new Map();
|
|
let app, element;
|
|
let shouldUpdateCount = 0;
|
|
const resolved = Promise.resolve();
|
|
const additionalMethods = {};
|
|
const ActionsCollector = getActionsCollector(actionsByInstance);
|
|
class InstanceActionsCollector {
|
|
constructor(instance) {
|
|
this.instance = instance;
|
|
}
|
|
create(actions, props) {
|
|
const actionsInstance = new ActionsCollector(this.instance);
|
|
actionsInstance.set(actions, props);
|
|
return actionsInstance;
|
|
}
|
|
}
|
|
const PublicComponentMethods = getPublicComponentMethods(components, actionsByInstance, clone);
|
|
const InternalComponentMethods = getInternalComponentMethods(components, actionsByInstance, clone);
|
|
class vido {
|
|
constructor() {
|
|
this.destroyable = [];
|
|
this.onChangeFunctions = [];
|
|
this.debug = false;
|
|
this.state = state;
|
|
this.api = api;
|
|
this.lastProps = {};
|
|
this.html = html;
|
|
this.svg = svg;
|
|
this.directive = directive;
|
|
this.asyncAppend = asyncAppend;
|
|
this.asyncReplace = asyncReplace;
|
|
this.cache = cache;
|
|
this.classMap = classMap;
|
|
this.guard = guard;
|
|
this.ifDefined = ifDefined;
|
|
this.repeat = repeat;
|
|
this.unsafeHTML = unsafeHTML;
|
|
this.until = until;
|
|
this.schedule = schedule;
|
|
this.actionsByInstance = (componentActions, props) => { };
|
|
this.StyleMap = StyleMap;
|
|
this.Detach = Detach;
|
|
this.PointerAction = PointerAction;
|
|
this.Action = Action;
|
|
this._components = components;
|
|
this._actions = actionsByInstance;
|
|
this.reuseComponents = this.reuseComponents.bind(this);
|
|
this.onDestroy = this.onDestroy.bind(this);
|
|
this.onChange = this.onChange.bind(this);
|
|
this.update = this.update.bind(this);
|
|
for (const name in additionalMethods) {
|
|
this[name] = additionalMethods[name];
|
|
}
|
|
}
|
|
addMethod(name, body) {
|
|
additionalMethods[name] = body;
|
|
}
|
|
onDestroy(fn) {
|
|
this.destroyable.push(fn);
|
|
}
|
|
onChange(fn) {
|
|
this.onChangeFunctions.push(fn);
|
|
}
|
|
update(callback) {
|
|
return this.updateTemplate(callback);
|
|
}
|
|
/**
|
|
* Reuse existing components when your data was changed
|
|
*
|
|
* @param {array} currentComponents - array of components
|
|
* @param {array} dataArray - any data as array for each component
|
|
* @param {function} getProps - you can pass params to component from array item ( example: item=>({id:item.id}) )
|
|
* @param {function} component - what kind of components do you want to create?
|
|
* @param {boolean} leaveTail - leave last elements and do not destroy corresponding components
|
|
* @returns {array} of components (with updated/destroyed/created ones)
|
|
*/
|
|
reuseComponents(currentComponents, dataArray, getProps, component, leaveTail = true) {
|
|
const modified = [];
|
|
const currentLen = currentComponents.length;
|
|
const dataLen = dataArray.length;
|
|
let leave = false;
|
|
if (leaveTail && (dataArray === undefined || dataArray.length === 0)) {
|
|
leave = true;
|
|
}
|
|
let leaveStartingAt = 0;
|
|
if (currentLen < dataLen) {
|
|
let diff = dataLen - currentLen;
|
|
while (diff) {
|
|
const item = dataArray[dataLen - diff];
|
|
const newComponent = this.createComponent(component, getProps(item));
|
|
currentComponents.push(newComponent);
|
|
modified.push(newComponent.instance);
|
|
diff--;
|
|
}
|
|
}
|
|
else if (currentLen > dataLen) {
|
|
let diff = currentLen - dataLen;
|
|
if (leaveTail) {
|
|
leave = true;
|
|
leaveStartingAt = currentLen - diff;
|
|
}
|
|
while (diff) {
|
|
const index = currentLen - diff;
|
|
if (!leaveTail) {
|
|
modified.push(currentComponents[index].instance);
|
|
currentComponents[index].destroy();
|
|
}
|
|
diff--;
|
|
}
|
|
if (!leaveTail) {
|
|
currentComponents.length = dataLen;
|
|
}
|
|
}
|
|
let index = 0;
|
|
for (const component of currentComponents) {
|
|
const item = dataArray[index];
|
|
if (!modified.includes(component.instance)) {
|
|
component.change(getProps(item), { leave: leave && index >= leaveStartingAt });
|
|
}
|
|
index++;
|
|
}
|
|
}
|
|
createComponent(component, props = {}, content = null) {
|
|
const instance = component.name + ':' + componentId++;
|
|
let vidoInstance;
|
|
vidoInstance = new vido();
|
|
vidoInstance.instance = instance;
|
|
vidoInstance.name = component.name;
|
|
vidoInstance.Actions = new InstanceActionsCollector(instance);
|
|
const publicMethods = new PublicComponentMethods(instance, vidoInstance, props);
|
|
const internalMethods = new InternalComponentMethods(instance, vidoInstance, component(vidoInstance, props, content), content);
|
|
components.set(instance, internalMethods);
|
|
components.get(instance).change(props);
|
|
if (vidoInstance.debug) {
|
|
console.groupCollapsed(`component created ${instance}`);
|
|
console.log(clone({ props, components: components.keys(), actionsByInstance }));
|
|
console.trace();
|
|
console.groupEnd();
|
|
}
|
|
return publicMethods;
|
|
}
|
|
destroyComponent(instance, vidoInstance) {
|
|
if (vidoInstance.debug) {
|
|
console.groupCollapsed(`destroying component ${instance}...`);
|
|
console.log(clone({ components: components.keys(), actionsByInstance }));
|
|
console.trace();
|
|
console.groupEnd();
|
|
}
|
|
if (actionsByInstance.has(instance)) {
|
|
for (const action of actionsByInstance.get(instance)) {
|
|
if (typeof action.componentAction.destroy === 'function') {
|
|
action.componentAction.destroy(action.element, action.props);
|
|
}
|
|
}
|
|
}
|
|
actionsByInstance.delete(instance);
|
|
const component = components.get(instance);
|
|
component.update();
|
|
component.destroy();
|
|
components.delete(instance);
|
|
if (vidoInstance.debug) {
|
|
console.groupCollapsed(`component destroyed ${instance}`);
|
|
console.log(clone({ components: components.keys(), actionsByInstance }));
|
|
console.trace();
|
|
console.groupEnd();
|
|
}
|
|
}
|
|
executeActions() {
|
|
var _a, _b, _c;
|
|
for (const actions of actionsByInstance.values()) {
|
|
for (const action of actions) {
|
|
if (action.element.vido === undefined) {
|
|
const componentAction = action.componentAction;
|
|
const create = componentAction.create;
|
|
if (typeof create !== 'undefined') {
|
|
let result;
|
|
if (((_a = create.prototype) === null || _a === void 0 ? void 0 : _a.isAction) !== true &&
|
|
create.isAction === undefined &&
|
|
((_b = create.prototype) === null || _b === void 0 ? void 0 : _b.update) === undefined &&
|
|
((_c = create.prototype) === null || _c === void 0 ? void 0 : _c.destroy) === undefined) {
|
|
result = create(action.element, action.props);
|
|
}
|
|
else {
|
|
result = new create(action.element, action.props);
|
|
}
|
|
if (result !== undefined) {
|
|
if (typeof result === 'function') {
|
|
componentAction.destroy = result;
|
|
}
|
|
else {
|
|
if (typeof result.update === 'function') {
|
|
componentAction.update = result.update.bind(result);
|
|
}
|
|
if (typeof result.destroy === 'function') {
|
|
componentAction.destroy = result.destroy.bind(result);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
action.element.vido = action.props;
|
|
if (typeof action.componentAction.update === 'function') {
|
|
action.componentAction.update(action.element, action.props);
|
|
}
|
|
}
|
|
}
|
|
for (const action of actions) {
|
|
action.element.vido = action.props;
|
|
}
|
|
}
|
|
}
|
|
updateTemplate(callback) {
|
|
return new Promise((resolve) => {
|
|
const currentShouldUpdateCount = ++shouldUpdateCount;
|
|
const self = this;
|
|
function flush() {
|
|
if (currentShouldUpdateCount === shouldUpdateCount) {
|
|
shouldUpdateCount = 0;
|
|
self.render();
|
|
if (typeof callback === 'function')
|
|
callback();
|
|
resolve();
|
|
}
|
|
}
|
|
resolved.then(flush);
|
|
});
|
|
}
|
|
createApp(config) {
|
|
element = config.element;
|
|
const App = this.createComponent(config.component, config.props);
|
|
app = App.instance;
|
|
this.render();
|
|
return App;
|
|
}
|
|
render() {
|
|
const appComponent = components.get(app);
|
|
if (appComponent) {
|
|
render(appComponent.update(), element);
|
|
this.executeActions();
|
|
}
|
|
else if (element) {
|
|
element.remove();
|
|
}
|
|
}
|
|
}
|
|
return new vido();
|
|
}
|
|
Vido.prototype.lithtml = lithtml;
|
|
Vido.prototype.Action = Action;
|
|
Vido.prototype.Directive = Directive;
|
|
Vido.prototype.schedule = schedule;
|
|
Vido.prototype.Detach = Detach;
|
|
Vido.prototype.StyleMap = StyleMap;
|
|
Vido.prototype.PointerAction = PointerAction;
|
|
Vido.prototype.asyncAppend = asyncAppend;
|
|
Vido.prototype.asyncReplace = asyncReplace;
|
|
Vido.prototype.cache = cache;
|
|
Vido.prototype.classMap = classMap;
|
|
Vido.prototype.guard = guard;
|
|
Vido.prototype.ifDefined = ifDefined;
|
|
Vido.prototype.repeat = repeat;
|
|
Vido.prototype.unsafeHTML = unsafeHTML;
|
|
Vido.prototype.unti = until;
|
|
|
|
/**
|
|
* A collection of shims that provide minimal functionality of the ES6 collections.
|
|
*
|
|
* These implementations are not meant to be used outside of the ResizeObserver
|
|
* modules as they cover only a limited range of use cases.
|
|
*/
|
|
/* eslint-disable require-jsdoc, valid-jsdoc */
|
|
var MapShim = (function () {
|
|
if (typeof Map !== 'undefined') {
|
|
return Map;
|
|
}
|
|
/**
|
|
* Returns index in provided array that matches the specified key.
|
|
*
|
|
* @param {Array<Array>} arr
|
|
* @param {*} key
|
|
* @returns {number}
|
|
*/
|
|
function getIndex(arr, key) {
|
|
var result = -1;
|
|
arr.some(function (entry, index) {
|
|
if (entry[0] === key) {
|
|
result = index;
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
return result;
|
|
}
|
|
return /** @class */ (function () {
|
|
function class_1() {
|
|
this.__entries__ = [];
|
|
}
|
|
Object.defineProperty(class_1.prototype, "size", {
|
|
/**
|
|
* @returns {boolean}
|
|
*/
|
|
get: function () {
|
|
return this.__entries__.length;
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
/**
|
|
* @param {*} key
|
|
* @returns {*}
|
|
*/
|
|
class_1.prototype.get = function (key) {
|
|
var index = getIndex(this.__entries__, key);
|
|
var entry = this.__entries__[index];
|
|
return entry && entry[1];
|
|
};
|
|
/**
|
|
* @param {*} key
|
|
* @param {*} value
|
|
* @returns {void}
|
|
*/
|
|
class_1.prototype.set = function (key, value) {
|
|
var index = getIndex(this.__entries__, key);
|
|
if (~index) {
|
|
this.__entries__[index][1] = value;
|
|
}
|
|
else {
|
|
this.__entries__.push([key, value]);
|
|
}
|
|
};
|
|
/**
|
|
* @param {*} key
|
|
* @returns {void}
|
|
*/
|
|
class_1.prototype.delete = function (key) {
|
|
var entries = this.__entries__;
|
|
var index = getIndex(entries, key);
|
|
if (~index) {
|
|
entries.splice(index, 1);
|
|
}
|
|
};
|
|
/**
|
|
* @param {*} key
|
|
* @returns {void}
|
|
*/
|
|
class_1.prototype.has = function (key) {
|
|
return !!~getIndex(this.__entries__, key);
|
|
};
|
|
/**
|
|
* @returns {void}
|
|
*/
|
|
class_1.prototype.clear = function () {
|
|
this.__entries__.splice(0);
|
|
};
|
|
/**
|
|
* @param {Function} callback
|
|
* @param {*} [ctx=null]
|
|
* @returns {void}
|
|
*/
|
|
class_1.prototype.forEach = function (callback, ctx) {
|
|
if (ctx === void 0) { ctx = null; }
|
|
for (var _i = 0, _a = this.__entries__; _i < _a.length; _i++) {
|
|
var entry = _a[_i];
|
|
callback.call(ctx, entry[1], entry[0]);
|
|
}
|
|
};
|
|
return class_1;
|
|
}());
|
|
})();
|
|
|
|
/**
|
|
* Detects whether window and document objects are available in current environment.
|
|
*/
|
|
var isBrowser$1 = typeof window !== 'undefined' && typeof document !== 'undefined' && window.document === document;
|
|
|
|
// Returns global object of a current environment.
|
|
var global$1 = (function () {
|
|
if (typeof global !== 'undefined' && global.Math === Math) {
|
|
return global;
|
|
}
|
|
if (typeof self !== 'undefined' && self.Math === Math) {
|
|
return self;
|
|
}
|
|
if (typeof window !== 'undefined' && window.Math === Math) {
|
|
return window;
|
|
}
|
|
// eslint-disable-next-line no-new-func
|
|
return Function('return this')();
|
|
})();
|
|
|
|
/**
|
|
* A shim for the requestAnimationFrame which falls back to the setTimeout if
|
|
* first one is not supported.
|
|
*
|
|
* @returns {number} Requests' identifier.
|
|
*/
|
|
var requestAnimationFrame$1 = (function () {
|
|
if (typeof requestAnimationFrame === 'function') {
|
|
// It's required to use a bounded function because IE sometimes throws
|
|
// an "Invalid calling object" error if rAF is invoked without the global
|
|
// object on the left hand side.
|
|
return requestAnimationFrame.bind(global$1);
|
|
}
|
|
return function (callback) { return setTimeout(function () { return callback(Date.now()); }, 1000 / 60); };
|
|
})();
|
|
|
|
// Defines minimum timeout before adding a trailing call.
|
|
var trailingTimeout = 2;
|
|
/**
|
|
* Creates a wrapper function which ensures that provided callback will be
|
|
* invoked only once during the specified delay period.
|
|
*
|
|
* @param {Function} callback - Function to be invoked after the delay period.
|
|
* @param {number} delay - Delay after which to invoke callback.
|
|
* @returns {Function}
|
|
*/
|
|
function throttle (callback, delay) {
|
|
var leadingCall = false, trailingCall = false, lastCallTime = 0;
|
|
/**
|
|
* Invokes the original callback function and schedules new invocation if
|
|
* the "proxy" was called during current request.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
function resolvePending() {
|
|
if (leadingCall) {
|
|
leadingCall = false;
|
|
callback();
|
|
}
|
|
if (trailingCall) {
|
|
proxy();
|
|
}
|
|
}
|
|
/**
|
|
* Callback invoked after the specified delay. It will further postpone
|
|
* invocation of the original function delegating it to the
|
|
* requestAnimationFrame.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
function timeoutCallback() {
|
|
requestAnimationFrame$1(resolvePending);
|
|
}
|
|
/**
|
|
* Schedules invocation of the original function.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
function proxy() {
|
|
var timeStamp = Date.now();
|
|
if (leadingCall) {
|
|
// Reject immediately following calls.
|
|
if (timeStamp - lastCallTime < trailingTimeout) {
|
|
return;
|
|
}
|
|
// Schedule new call to be in invoked when the pending one is resolved.
|
|
// This is important for "transitions" which never actually start
|
|
// immediately so there is a chance that we might miss one if change
|
|
// happens amids the pending invocation.
|
|
trailingCall = true;
|
|
}
|
|
else {
|
|
leadingCall = true;
|
|
trailingCall = false;
|
|
setTimeout(timeoutCallback, delay);
|
|
}
|
|
lastCallTime = timeStamp;
|
|
}
|
|
return proxy;
|
|
}
|
|
|
|
// Minimum delay before invoking the update of observers.
|
|
var REFRESH_DELAY = 20;
|
|
// A list of substrings of CSS properties used to find transition events that
|
|
// might affect dimensions of observed elements.
|
|
var transitionKeys = ['top', 'right', 'bottom', 'left', 'width', 'height', 'size', 'weight'];
|
|
// Check if MutationObserver is available.
|
|
var mutationObserverSupported = typeof MutationObserver !== 'undefined';
|
|
/**
|
|
* Singleton controller class which handles updates of ResizeObserver instances.
|
|
*/
|
|
var ResizeObserverController = /** @class */ (function () {
|
|
/**
|
|
* Creates a new instance of ResizeObserverController.
|
|
*
|
|
* @private
|
|
*/
|
|
function ResizeObserverController() {
|
|
/**
|
|
* Indicates whether DOM listeners have been added.
|
|
*
|
|
* @private {boolean}
|
|
*/
|
|
this.connected_ = false;
|
|
/**
|
|
* Tells that controller has subscribed for Mutation Events.
|
|
*
|
|
* @private {boolean}
|
|
*/
|
|
this.mutationEventsAdded_ = false;
|
|
/**
|
|
* Keeps reference to the instance of MutationObserver.
|
|
*
|
|
* @private {MutationObserver}
|
|
*/
|
|
this.mutationsObserver_ = null;
|
|
/**
|
|
* A list of connected observers.
|
|
*
|
|
* @private {Array<ResizeObserverSPI>}
|
|
*/
|
|
this.observers_ = [];
|
|
this.onTransitionEnd_ = this.onTransitionEnd_.bind(this);
|
|
this.refresh = throttle(this.refresh.bind(this), REFRESH_DELAY);
|
|
}
|
|
/**
|
|
* Adds observer to observers list.
|
|
*
|
|
* @param {ResizeObserverSPI} observer - Observer to be added.
|
|
* @returns {void}
|
|
*/
|
|
ResizeObserverController.prototype.addObserver = function (observer) {
|
|
if (!~this.observers_.indexOf(observer)) {
|
|
this.observers_.push(observer);
|
|
}
|
|
// Add listeners if they haven't been added yet.
|
|
if (!this.connected_) {
|
|
this.connect_();
|
|
}
|
|
};
|
|
/**
|
|
* Removes observer from observers list.
|
|
*
|
|
* @param {ResizeObserverSPI} observer - Observer to be removed.
|
|
* @returns {void}
|
|
*/
|
|
ResizeObserverController.prototype.removeObserver = function (observer) {
|
|
var observers = this.observers_;
|
|
var index = observers.indexOf(observer);
|
|
// Remove observer if it's present in registry.
|
|
if (~index) {
|
|
observers.splice(index, 1);
|
|
}
|
|
// Remove listeners if controller has no connected observers.
|
|
if (!observers.length && this.connected_) {
|
|
this.disconnect_();
|
|
}
|
|
};
|
|
/**
|
|
* Invokes the update of observers. It will continue running updates insofar
|
|
* it detects changes.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
ResizeObserverController.prototype.refresh = function () {
|
|
var changesDetected = this.updateObservers_();
|
|
// Continue running updates if changes have been detected as there might
|
|
// be future ones caused by CSS transitions.
|
|
if (changesDetected) {
|
|
this.refresh();
|
|
}
|
|
};
|
|
/**
|
|
* Updates every observer from observers list and notifies them of queued
|
|
* entries.
|
|
*
|
|
* @private
|
|
* @returns {boolean} Returns "true" if any observer has detected changes in
|
|
* dimensions of it's elements.
|
|
*/
|
|
ResizeObserverController.prototype.updateObservers_ = function () {
|
|
// Collect observers that have active observations.
|
|
var activeObservers = this.observers_.filter(function (observer) {
|
|
return observer.gatherActive(), observer.hasActive();
|
|
});
|
|
// Deliver notifications in a separate cycle in order to avoid any
|
|
// collisions between observers, e.g. when multiple instances of
|
|
// ResizeObserver are tracking the same element and the callback of one
|
|
// of them changes content dimensions of the observed target. Sometimes
|
|
// this may result in notifications being blocked for the rest of observers.
|
|
activeObservers.forEach(function (observer) { return observer.broadcastActive(); });
|
|
return activeObservers.length > 0;
|
|
};
|
|
/**
|
|
* Initializes DOM listeners.
|
|
*
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
ResizeObserverController.prototype.connect_ = function () {
|
|
// Do nothing if running in a non-browser environment or if listeners
|
|
// have been already added.
|
|
if (!isBrowser$1 || this.connected_) {
|
|
return;
|
|
}
|
|
// Subscription to the "Transitionend" event is used as a workaround for
|
|
// delayed transitions. This way it's possible to capture at least the
|
|
// final state of an element.
|
|
document.addEventListener('transitionend', this.onTransitionEnd_);
|
|
window.addEventListener('resize', this.refresh);
|
|
if (mutationObserverSupported) {
|
|
this.mutationsObserver_ = new MutationObserver(this.refresh);
|
|
this.mutationsObserver_.observe(document, {
|
|
attributes: true,
|
|
childList: true,
|
|
characterData: true,
|
|
subtree: true
|
|
});
|
|
}
|
|
else {
|
|
document.addEventListener('DOMSubtreeModified', this.refresh);
|
|
this.mutationEventsAdded_ = true;
|
|
}
|
|
this.connected_ = true;
|
|
};
|
|
/**
|
|
* Removes DOM listeners.
|
|
*
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
ResizeObserverController.prototype.disconnect_ = function () {
|
|
// Do nothing if running in a non-browser environment or if listeners
|
|
// have been already removed.
|
|
if (!isBrowser$1 || !this.connected_) {
|
|
return;
|
|
}
|
|
document.removeEventListener('transitionend', this.onTransitionEnd_);
|
|
window.removeEventListener('resize', this.refresh);
|
|
if (this.mutationsObserver_) {
|
|
this.mutationsObserver_.disconnect();
|
|
}
|
|
if (this.mutationEventsAdded_) {
|
|
document.removeEventListener('DOMSubtreeModified', this.refresh);
|
|
}
|
|
this.mutationsObserver_ = null;
|
|
this.mutationEventsAdded_ = false;
|
|
this.connected_ = false;
|
|
};
|
|
/**
|
|
* "Transitionend" event handler.
|
|
*
|
|
* @private
|
|
* @param {TransitionEvent} event
|
|
* @returns {void}
|
|
*/
|
|
ResizeObserverController.prototype.onTransitionEnd_ = function (_a) {
|
|
var _b = _a.propertyName, propertyName = _b === void 0 ? '' : _b;
|
|
// Detect whether transition may affect dimensions of an element.
|
|
var isReflowProperty = transitionKeys.some(function (key) {
|
|
return !!~propertyName.indexOf(key);
|
|
});
|
|
if (isReflowProperty) {
|
|
this.refresh();
|
|
}
|
|
};
|
|
/**
|
|
* Returns instance of the ResizeObserverController.
|
|
*
|
|
* @returns {ResizeObserverController}
|
|
*/
|
|
ResizeObserverController.getInstance = function () {
|
|
if (!this.instance_) {
|
|
this.instance_ = new ResizeObserverController();
|
|
}
|
|
return this.instance_;
|
|
};
|
|
/**
|
|
* Holds reference to the controller's instance.
|
|
*
|
|
* @private {ResizeObserverController}
|
|
*/
|
|
ResizeObserverController.instance_ = null;
|
|
return ResizeObserverController;
|
|
}());
|
|
|
|
/**
|
|
* Defines non-writable/enumerable properties of the provided target object.
|
|
*
|
|
* @param {Object} target - Object for which to define properties.
|
|
* @param {Object} props - Properties to be defined.
|
|
* @returns {Object} Target object.
|
|
*/
|
|
var defineConfigurable = (function (target, props) {
|
|
for (var _i = 0, _a = Object.keys(props); _i < _a.length; _i++) {
|
|
var key = _a[_i];
|
|
Object.defineProperty(target, key, {
|
|
value: props[key],
|
|
enumerable: false,
|
|
writable: false,
|
|
configurable: true
|
|
});
|
|
}
|
|
return target;
|
|
});
|
|
|
|
/**
|
|
* Returns the global object associated with provided element.
|
|
*
|
|
* @param {Object} target
|
|
* @returns {Object}
|
|
*/
|
|
var getWindowOf = (function (target) {
|
|
// Assume that the element is an instance of Node, which means that it
|
|
// has the "ownerDocument" property from which we can retrieve a
|
|
// corresponding global object.
|
|
var ownerGlobal = target && target.ownerDocument && target.ownerDocument.defaultView;
|
|
// Return the local global object if it's not possible extract one from
|
|
// provided element.
|
|
return ownerGlobal || global$1;
|
|
});
|
|
|
|
// Placeholder of an empty content rectangle.
|
|
var emptyRect = createRectInit(0, 0, 0, 0);
|
|
/**
|
|
* Converts provided string to a number.
|
|
*
|
|
* @param {number|string} value
|
|
* @returns {number}
|
|
*/
|
|
function toFloat(value) {
|
|
return parseFloat(value) || 0;
|
|
}
|
|
/**
|
|
* Extracts borders size from provided styles.
|
|
*
|
|
* @param {CSSStyleDeclaration} styles
|
|
* @param {...string} positions - Borders positions (top, right, ...)
|
|
* @returns {number}
|
|
*/
|
|
function getBordersSize(styles) {
|
|
var positions = [];
|
|
for (var _i = 1; _i < arguments.length; _i++) {
|
|
positions[_i - 1] = arguments[_i];
|
|
}
|
|
return positions.reduce(function (size, position) {
|
|
var value = styles['border-' + position + '-width'];
|
|
return size + toFloat(value);
|
|
}, 0);
|
|
}
|
|
/**
|
|
* Extracts paddings sizes from provided styles.
|
|
*
|
|
* @param {CSSStyleDeclaration} styles
|
|
* @returns {Object} Paddings box.
|
|
*/
|
|
function getPaddings(styles) {
|
|
var positions = ['top', 'right', 'bottom', 'left'];
|
|
var paddings = {};
|
|
for (var _i = 0, positions_1 = positions; _i < positions_1.length; _i++) {
|
|
var position = positions_1[_i];
|
|
var value = styles['padding-' + position];
|
|
paddings[position] = toFloat(value);
|
|
}
|
|
return paddings;
|
|
}
|
|
/**
|
|
* Calculates content rectangle of provided SVG element.
|
|
*
|
|
* @param {SVGGraphicsElement} target - Element content rectangle of which needs
|
|
* to be calculated.
|
|
* @returns {DOMRectInit}
|
|
*/
|
|
function getSVGContentRect(target) {
|
|
var bbox = target.getBBox();
|
|
return createRectInit(0, 0, bbox.width, bbox.height);
|
|
}
|
|
/**
|
|
* Calculates content rectangle of provided HTMLElement.
|
|
*
|
|
* @param {HTMLElement} target - Element for which to calculate the content rectangle.
|
|
* @returns {DOMRectInit}
|
|
*/
|
|
function getHTMLElementContentRect(target) {
|
|
// Client width & height properties can't be
|
|
// used exclusively as they provide rounded values.
|
|
var clientWidth = target.clientWidth, clientHeight = target.clientHeight;
|
|
// By this condition we can catch all non-replaced inline, hidden and
|
|
// detached elements. Though elements with width & height properties less
|
|
// than 0.5 will be discarded as well.
|
|
//
|
|
// Without it we would need to implement separate methods for each of
|
|
// those cases and it's not possible to perform a precise and performance
|
|
// effective test for hidden elements. E.g. even jQuery's ':visible' filter
|
|
// gives wrong results for elements with width & height less than 0.5.
|
|
if (!clientWidth && !clientHeight) {
|
|
return emptyRect;
|
|
}
|
|
var styles = getWindowOf(target).getComputedStyle(target);
|
|
var paddings = getPaddings(styles);
|
|
var horizPad = paddings.left + paddings.right;
|
|
var vertPad = paddings.top + paddings.bottom;
|
|
// Computed styles of width & height are being used because they are the
|
|
// only dimensions available to JS that contain non-rounded values. It could
|
|
// be possible to utilize the getBoundingClientRect if only it's data wasn't
|
|
// affected by CSS transformations let alone paddings, borders and scroll bars.
|
|
var width = toFloat(styles.width), height = toFloat(styles.height);
|
|
// Width & height include paddings and borders when the 'border-box' box
|
|
// model is applied (except for IE).
|
|
if (styles.boxSizing === 'border-box') {
|
|
// Following conditions are required to handle Internet Explorer which
|
|
// doesn't include paddings and borders to computed CSS dimensions.
|
|
//
|
|
// We can say that if CSS dimensions + paddings are equal to the "client"
|
|
// properties then it's either IE, and thus we don't need to subtract
|
|
// anything, or an element merely doesn't have paddings/borders styles.
|
|
if (Math.round(width + horizPad) !== clientWidth) {
|
|
width -= getBordersSize(styles, 'left', 'right') + horizPad;
|
|
}
|
|
if (Math.round(height + vertPad) !== clientHeight) {
|
|
height -= getBordersSize(styles, 'top', 'bottom') + vertPad;
|
|
}
|
|
}
|
|
// Following steps can't be applied to the document's root element as its
|
|
// client[Width/Height] properties represent viewport area of the window.
|
|
// Besides, it's as well not necessary as the <html> itself neither has
|
|
// rendered scroll bars nor it can be clipped.
|
|
if (!isDocumentElement(target)) {
|
|
// In some browsers (only in Firefox, actually) CSS width & height
|
|
// include scroll bars size which can be removed at this step as scroll
|
|
// bars are the only difference between rounded dimensions + paddings
|
|
// and "client" properties, though that is not always true in Chrome.
|
|
var vertScrollbar = Math.round(width + horizPad) - clientWidth;
|
|
var horizScrollbar = Math.round(height + vertPad) - clientHeight;
|
|
// Chrome has a rather weird rounding of "client" properties.
|
|
// E.g. for an element with content width of 314.2px it sometimes gives
|
|
// the client width of 315px and for the width of 314.7px it may give
|
|
// 314px. And it doesn't happen all the time. So just ignore this delta
|
|
// as a non-relevant.
|
|
if (Math.abs(vertScrollbar) !== 1) {
|
|
width -= vertScrollbar;
|
|
}
|
|
if (Math.abs(horizScrollbar) !== 1) {
|
|
height -= horizScrollbar;
|
|
}
|
|
}
|
|
return createRectInit(paddings.left, paddings.top, width, height);
|
|
}
|
|
/**
|
|
* Checks whether provided element is an instance of the SVGGraphicsElement.
|
|
*
|
|
* @param {Element} target - Element to be checked.
|
|
* @returns {boolean}
|
|
*/
|
|
var isSVGGraphicsElement = (function () {
|
|
// Some browsers, namely IE and Edge, don't have the SVGGraphicsElement
|
|
// interface.
|
|
if (typeof SVGGraphicsElement !== 'undefined') {
|
|
return function (target) { return target instanceof getWindowOf(target).SVGGraphicsElement; };
|
|
}
|
|
// If it's so, then check that element is at least an instance of the
|
|
// SVGElement and that it has the "getBBox" method.
|
|
// eslint-disable-next-line no-extra-parens
|
|
return function (target) { return (target instanceof getWindowOf(target).SVGElement &&
|
|
typeof target.getBBox === 'function'); };
|
|
})();
|
|
/**
|
|
* Checks whether provided element is a document element (<html>).
|
|
*
|
|
* @param {Element} target - Element to be checked.
|
|
* @returns {boolean}
|
|
*/
|
|
function isDocumentElement(target) {
|
|
return target === getWindowOf(target).document.documentElement;
|
|
}
|
|
/**
|
|
* Calculates an appropriate content rectangle for provided html or svg element.
|
|
*
|
|
* @param {Element} target - Element content rectangle of which needs to be calculated.
|
|
* @returns {DOMRectInit}
|
|
*/
|
|
function getContentRect(target) {
|
|
if (!isBrowser$1) {
|
|
return emptyRect;
|
|
}
|
|
if (isSVGGraphicsElement(target)) {
|
|
return getSVGContentRect(target);
|
|
}
|
|
return getHTMLElementContentRect(target);
|
|
}
|
|
/**
|
|
* Creates rectangle with an interface of the DOMRectReadOnly.
|
|
* Spec: https://drafts.fxtf.org/geometry/#domrectreadonly
|
|
*
|
|
* @param {DOMRectInit} rectInit - Object with rectangle's x/y coordinates and dimensions.
|
|
* @returns {DOMRectReadOnly}
|
|
*/
|
|
function createReadOnlyRect(_a) {
|
|
var x = _a.x, y = _a.y, width = _a.width, height = _a.height;
|
|
// If DOMRectReadOnly is available use it as a prototype for the rectangle.
|
|
var Constr = typeof DOMRectReadOnly !== 'undefined' ? DOMRectReadOnly : Object;
|
|
var rect = Object.create(Constr.prototype);
|
|
// Rectangle's properties are not writable and non-enumerable.
|
|
defineConfigurable(rect, {
|
|
x: x, y: y, width: width, height: height,
|
|
top: y,
|
|
right: x + width,
|
|
bottom: height + y,
|
|
left: x
|
|
});
|
|
return rect;
|
|
}
|
|
/**
|
|
* Creates DOMRectInit object based on the provided dimensions and the x/y coordinates.
|
|
* Spec: https://drafts.fxtf.org/geometry/#dictdef-domrectinit
|
|
*
|
|
* @param {number} x - X coordinate.
|
|
* @param {number} y - Y coordinate.
|
|
* @param {number} width - Rectangle's width.
|
|
* @param {number} height - Rectangle's height.
|
|
* @returns {DOMRectInit}
|
|
*/
|
|
function createRectInit(x, y, width, height) {
|
|
return { x: x, y: y, width: width, height: height };
|
|
}
|
|
|
|
/**
|
|
* Class that is responsible for computations of the content rectangle of
|
|
* provided DOM element and for keeping track of it's changes.
|
|
*/
|
|
var ResizeObservation = /** @class */ (function () {
|
|
/**
|
|
* Creates an instance of ResizeObservation.
|
|
*
|
|
* @param {Element} target - Element to be observed.
|
|
*/
|
|
function ResizeObservation(target) {
|
|
/**
|
|
* Broadcasted width of content rectangle.
|
|
*
|
|
* @type {number}
|
|
*/
|
|
this.broadcastWidth = 0;
|
|
/**
|
|
* Broadcasted height of content rectangle.
|
|
*
|
|
* @type {number}
|
|
*/
|
|
this.broadcastHeight = 0;
|
|
/**
|
|
* Reference to the last observed content rectangle.
|
|
*
|
|
* @private {DOMRectInit}
|
|
*/
|
|
this.contentRect_ = createRectInit(0, 0, 0, 0);
|
|
this.target = target;
|
|
}
|
|
/**
|
|
* Updates content rectangle and tells whether it's width or height properties
|
|
* have changed since the last broadcast.
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
ResizeObservation.prototype.isActive = function () {
|
|
var rect = getContentRect(this.target);
|
|
this.contentRect_ = rect;
|
|
return (rect.width !== this.broadcastWidth ||
|
|
rect.height !== this.broadcastHeight);
|
|
};
|
|
/**
|
|
* Updates 'broadcastWidth' and 'broadcastHeight' properties with a data
|
|
* from the corresponding properties of the last observed content rectangle.
|
|
*
|
|
* @returns {DOMRectInit} Last observed content rectangle.
|
|
*/
|
|
ResizeObservation.prototype.broadcastRect = function () {
|
|
var rect = this.contentRect_;
|
|
this.broadcastWidth = rect.width;
|
|
this.broadcastHeight = rect.height;
|
|
return rect;
|
|
};
|
|
return ResizeObservation;
|
|
}());
|
|
|
|
var ResizeObserverEntry = /** @class */ (function () {
|
|
/**
|
|
* Creates an instance of ResizeObserverEntry.
|
|
*
|
|
* @param {Element} target - Element that is being observed.
|
|
* @param {DOMRectInit} rectInit - Data of the element's content rectangle.
|
|
*/
|
|
function ResizeObserverEntry(target, rectInit) {
|
|
var contentRect = createReadOnlyRect(rectInit);
|
|
// According to the specification following properties are not writable
|
|
// and are also not enumerable in the native implementation.
|
|
//
|
|
// Property accessors are not being used as they'd require to define a
|
|
// private WeakMap storage which may cause memory leaks in browsers that
|
|
// don't support this type of collections.
|
|
defineConfigurable(this, { target: target, contentRect: contentRect });
|
|
}
|
|
return ResizeObserverEntry;
|
|
}());
|
|
|
|
var ResizeObserverSPI = /** @class */ (function () {
|
|
/**
|
|
* Creates a new instance of ResizeObserver.
|
|
*
|
|
* @param {ResizeObserverCallback} callback - Callback function that is invoked
|
|
* when one of the observed elements changes it's content dimensions.
|
|
* @param {ResizeObserverController} controller - Controller instance which
|
|
* is responsible for the updates of observer.
|
|
* @param {ResizeObserver} callbackCtx - Reference to the public
|
|
* ResizeObserver instance which will be passed to callback function.
|
|
*/
|
|
function ResizeObserverSPI(callback, controller, callbackCtx) {
|
|
/**
|
|
* Collection of resize observations that have detected changes in dimensions
|
|
* of elements.
|
|
*
|
|
* @private {Array<ResizeObservation>}
|
|
*/
|
|
this.activeObservations_ = [];
|
|
/**
|
|
* Registry of the ResizeObservation instances.
|
|
*
|
|
* @private {Map<Element, ResizeObservation>}
|
|
*/
|
|
this.observations_ = new MapShim();
|
|
if (typeof callback !== 'function') {
|
|
throw new TypeError('The callback provided as parameter 1 is not a function.');
|
|
}
|
|
this.callback_ = callback;
|
|
this.controller_ = controller;
|
|
this.callbackCtx_ = callbackCtx;
|
|
}
|
|
/**
|
|
* Starts observing provided element.
|
|
*
|
|
* @param {Element} target - Element to be observed.
|
|
* @returns {void}
|
|
*/
|
|
ResizeObserverSPI.prototype.observe = function (target) {
|
|
if (!arguments.length) {
|
|
throw new TypeError('1 argument required, but only 0 present.');
|
|
}
|
|
// Do nothing if current environment doesn't have the Element interface.
|
|
if (typeof Element === 'undefined' || !(Element instanceof Object)) {
|
|
return;
|
|
}
|
|
if (!(target instanceof getWindowOf(target).Element)) {
|
|
throw new TypeError('parameter 1 is not of type "Element".');
|
|
}
|
|
var observations = this.observations_;
|
|
// Do nothing if element is already being observed.
|
|
if (observations.has(target)) {
|
|
return;
|
|
}
|
|
observations.set(target, new ResizeObservation(target));
|
|
this.controller_.addObserver(this);
|
|
// Force the update of observations.
|
|
this.controller_.refresh();
|
|
};
|
|
/**
|
|
* Stops observing provided element.
|
|
*
|
|
* @param {Element} target - Element to stop observing.
|
|
* @returns {void}
|
|
*/
|
|
ResizeObserverSPI.prototype.unobserve = function (target) {
|
|
if (!arguments.length) {
|
|
throw new TypeError('1 argument required, but only 0 present.');
|
|
}
|
|
// Do nothing if current environment doesn't have the Element interface.
|
|
if (typeof Element === 'undefined' || !(Element instanceof Object)) {
|
|
return;
|
|
}
|
|
if (!(target instanceof getWindowOf(target).Element)) {
|
|
throw new TypeError('parameter 1 is not of type "Element".');
|
|
}
|
|
var observations = this.observations_;
|
|
// Do nothing if element is not being observed.
|
|
if (!observations.has(target)) {
|
|
return;
|
|
}
|
|
observations.delete(target);
|
|
if (!observations.size) {
|
|
this.controller_.removeObserver(this);
|
|
}
|
|
};
|
|
/**
|
|
* Stops observing all elements.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
ResizeObserverSPI.prototype.disconnect = function () {
|
|
this.clearActive();
|
|
this.observations_.clear();
|
|
this.controller_.removeObserver(this);
|
|
};
|
|
/**
|
|
* Collects observation instances the associated element of which has changed
|
|
* it's content rectangle.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
ResizeObserverSPI.prototype.gatherActive = function () {
|
|
var _this = this;
|
|
this.clearActive();
|
|
this.observations_.forEach(function (observation) {
|
|
if (observation.isActive()) {
|
|
_this.activeObservations_.push(observation);
|
|
}
|
|
});
|
|
};
|
|
/**
|
|
* Invokes initial callback function with a list of ResizeObserverEntry
|
|
* instances collected from active resize observations.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
ResizeObserverSPI.prototype.broadcastActive = function () {
|
|
// Do nothing if observer doesn't have active observations.
|
|
if (!this.hasActive()) {
|
|
return;
|
|
}
|
|
var ctx = this.callbackCtx_;
|
|
// Create ResizeObserverEntry instance for every active observation.
|
|
var entries = this.activeObservations_.map(function (observation) {
|
|
return new ResizeObserverEntry(observation.target, observation.broadcastRect());
|
|
});
|
|
this.callback_.call(ctx, entries, ctx);
|
|
this.clearActive();
|
|
};
|
|
/**
|
|
* Clears the collection of active observations.
|
|
*
|
|
* @returns {void}
|
|
*/
|
|
ResizeObserverSPI.prototype.clearActive = function () {
|
|
this.activeObservations_.splice(0);
|
|
};
|
|
/**
|
|
* Tells whether observer has active observations.
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
ResizeObserverSPI.prototype.hasActive = function () {
|
|
return this.activeObservations_.length > 0;
|
|
};
|
|
return ResizeObserverSPI;
|
|
}());
|
|
|
|
// Registry of internal observers. If WeakMap is not available use current shim
|
|
// for the Map collection as it has all required methods and because WeakMap
|
|
// can't be fully polyfilled anyway.
|
|
var observers = typeof WeakMap !== 'undefined' ? new WeakMap() : new MapShim();
|
|
/**
|
|
* ResizeObserver API. Encapsulates the ResizeObserver SPI implementation
|
|
* exposing only those methods and properties that are defined in the spec.
|
|
*/
|
|
var ResizeObserver = /** @class */ (function () {
|
|
/**
|
|
* Creates a new instance of ResizeObserver.
|
|
*
|
|
* @param {ResizeObserverCallback} callback - Callback that is invoked when
|
|
* dimensions of the observed elements change.
|
|
*/
|
|
function ResizeObserver(callback) {
|
|
if (!(this instanceof ResizeObserver)) {
|
|
throw new TypeError('Cannot call a class as a function.');
|
|
}
|
|
if (!arguments.length) {
|
|
throw new TypeError('1 argument required, but only 0 present.');
|
|
}
|
|
var controller = ResizeObserverController.getInstance();
|
|
var observer = new ResizeObserverSPI(callback, controller, this);
|
|
observers.set(this, observer);
|
|
}
|
|
return ResizeObserver;
|
|
}());
|
|
// Expose public methods of ResizeObserver.
|
|
[
|
|
'observe',
|
|
'unobserve',
|
|
'disconnect'
|
|
].forEach(function (method) {
|
|
ResizeObserver.prototype[method] = function () {
|
|
var _a;
|
|
return (_a = observers.get(this))[method].apply(_a, arguments);
|
|
};
|
|
});
|
|
|
|
var index = (function () {
|
|
// Export existing implementation if available.
|
|
if (typeof global$1.ResizeObserver !== 'undefined') {
|
|
return global$1.ResizeObserver;
|
|
}
|
|
return ResizeObserver;
|
|
})();
|
|
|
|
/**
|
|
* Main component
|
|
*
|
|
* @copyright Rafal Pospiech <https://neuronet.io>
|
|
* @author Rafal Pospiech <neuronet.io@gmail.com>
|
|
* @package gantt-schedule-timeline-calendar
|
|
* @license AGPL-3.0 (https://github.com/neuronetio/gantt-schedule-timeline-calendar/blob/master/LICENSE)
|
|
* @link https://github.com/neuronetio/gantt-schedule-timeline-calendar
|
|
*/
|
|
function Main(vido, props = {}) {
|
|
const { api, state, onDestroy, Actions, update, createComponent, html, StyleMap, schedule } = vido;
|
|
const componentName = api.name;
|
|
// Initialize plugins
|
|
onDestroy(state.subscribe('config.plugins', plugins => {
|
|
if (typeof plugins !== 'undefined' && Array.isArray(plugins)) {
|
|
for (const initializePlugin of plugins) {
|
|
const destroyPlugin = initializePlugin(vido);
|
|
if (typeof destroyPlugin === 'function') {
|
|
onDestroy(destroyPlugin);
|
|
}
|
|
else if (destroyPlugin && destroyPlugin.hasOwnProperty('destroy')) {
|
|
destroyPlugin.destroy();
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
const componentSubs = [];
|
|
let ListComponent;
|
|
componentSubs.push(state.subscribe('config.components.List', value => (ListComponent = value)));
|
|
let ChartComponent;
|
|
componentSubs.push(state.subscribe('config.components.Chart', value => (ChartComponent = value)));
|
|
const List = createComponent(ListComponent);
|
|
onDestroy(List.destroy);
|
|
const Chart = createComponent(ChartComponent);
|
|
onDestroy(Chart.destroy);
|
|
onDestroy(() => {
|
|
componentSubs.forEach(unsub => unsub());
|
|
});
|
|
let wrapper;
|
|
onDestroy(state.subscribe('config.wrappers.Main', value => (wrapper = value)));
|
|
const componentActions = api.getActions('main');
|
|
let className, classNameVerticalScroll;
|
|
const styleMap = new StyleMap({}), verticalScrollStyleMap = new StyleMap({}), verticalScrollAreaStyleMap = new StyleMap({});
|
|
let verticalScrollBarElement;
|
|
let rowsHeight = 0;
|
|
let resizerActive = false;
|
|
/**
|
|
* Update class names
|
|
* @param {object} classNames
|
|
*/
|
|
const updateClassNames = classNames => {
|
|
const config = state.get('config');
|
|
className = api.getClass(componentName, { config });
|
|
if (resizerActive) {
|
|
className += ` ${componentName}__list-column-header-resizer--active`;
|
|
}
|
|
classNameVerticalScroll = api.getClass('vertical-scroll', { config });
|
|
update();
|
|
};
|
|
onDestroy(state.subscribe('config.classNames', updateClassNames));
|
|
/**
|
|
* Height change
|
|
*/
|
|
function heightChange() {
|
|
const config = state.get('config');
|
|
const scrollBarHeight = state.get('_internal.scrollBarHeight');
|
|
const height = config.height - config.headerHeight - scrollBarHeight;
|
|
state.update('_internal.height', height);
|
|
styleMap.style['--height'] = config.height + 'px';
|
|
verticalScrollStyleMap.style.height = height + 'px';
|
|
verticalScrollStyleMap.style.width = scrollBarHeight + 'px';
|
|
verticalScrollStyleMap.style['margin-top'] = config.headerHeight + 'px';
|
|
update();
|
|
}
|
|
onDestroy(state.subscribeAll(['config.height', 'config.headerHeight', '_internal.scrollBarHeight'], heightChange));
|
|
/**
|
|
* Resizer active change
|
|
* @param {boolean} active
|
|
*/
|
|
function resizerActiveChange(active) {
|
|
resizerActive = active;
|
|
className = api.getClass(api.name);
|
|
if (resizerActive) {
|
|
className += ` ${api.name}__list-column-header-resizer--active`;
|
|
}
|
|
update();
|
|
}
|
|
onDestroy(state.subscribe('_internal.list.columns.resizer.active', resizerActiveChange));
|
|
/**
|
|
* Generate tree
|
|
* @param {object} bulk
|
|
* @param {object} eventInfo
|
|
*/
|
|
function generateTree(bulk, eventInfo) {
|
|
if (state.get('_internal.flatTreeMap').length && eventInfo.type === 'subscribe') {
|
|
return;
|
|
}
|
|
const configRows = state.get('config.list.rows');
|
|
const rows = [];
|
|
for (const rowId in configRows) {
|
|
rows.push(configRows[rowId]);
|
|
}
|
|
api.fillEmptyRowValues(rows);
|
|
const configItems = state.get('config.chart.items');
|
|
const items = [];
|
|
for (const itemId in configItems) {
|
|
items.push(configItems[itemId]);
|
|
}
|
|
api.prepareItems(items);
|
|
const treeMap = api.makeTreeMap(rows, items);
|
|
state.update('_internal.treeMap', treeMap);
|
|
const flatTreeMapById = api.getFlatTreeMapById(treeMap);
|
|
state.update('_internal.flatTreeMapById', flatTreeMapById);
|
|
const flatTreeMap = api.flattenTreeMap(treeMap);
|
|
state.update('_internal.flatTreeMap', flatTreeMap);
|
|
update();
|
|
}
|
|
onDestroy(state.subscribeAll(['config.list.rows;', 'config.chart.items;'], generateTree));
|
|
onDestroy(state.subscribeAll(['config.list.rows.*.parentId', 'config.chart.items.*.rowId'], generateTree, { bulk: true }));
|
|
function prepareExpanded() {
|
|
const configRows = state.get('config.list.rows');
|
|
const rowsWithParentsExpanded = api.getRowsFromIds(api.getRowsWithParentsExpanded(state.get('_internal.flatTreeMap'), state.get('_internal.flatTreeMapById'), configRows), configRows);
|
|
rowsHeight = api.getRowsHeight(rowsWithParentsExpanded);
|
|
state.update('_internal.list.rowsHeight', rowsHeight);
|
|
state.update('_internal.list.rowsWithParentsExpanded', rowsWithParentsExpanded);
|
|
update();
|
|
}
|
|
onDestroy(state.subscribeAll(['config.list.rows.*.expanded', '_internal.treeMap;', 'config.list.rows.*.height'], prepareExpanded, { bulk: true }));
|
|
/**
|
|
* Generate visible rows
|
|
*/
|
|
function generateVisibleRowsAndItems() {
|
|
const { visibleRows, compensation } = api.getVisibleRowsAndCompensation(state.get('_internal.list.rowsWithParentsExpanded'));
|
|
const smoothScroll = state.get('config.scroll.smooth');
|
|
const currentVisibleRows = state.get('_internal.list.visibleRows');
|
|
let shouldUpdate = true;
|
|
state.update('config.scroll.compensation.y', smoothScroll ? -compensation : 0);
|
|
if (visibleRows.length !== currentVisibleRows.length) {
|
|
shouldUpdate = true;
|
|
}
|
|
else if (visibleRows.length) {
|
|
shouldUpdate = visibleRows.some((row, index) => {
|
|
if (typeof currentVisibleRows[index] === 'undefined') {
|
|
return true;
|
|
}
|
|
return row.id !== currentVisibleRows[index].id;
|
|
});
|
|
}
|
|
if (shouldUpdate) {
|
|
state.update('_internal.list.visibleRows', visibleRows);
|
|
}
|
|
const visibleItems = [];
|
|
for (const row of visibleRows) {
|
|
for (const item of row._internal.items) {
|
|
visibleItems.push(item);
|
|
}
|
|
}
|
|
state.update('_internal.chart.visibleItems', visibleItems);
|
|
update();
|
|
}
|
|
onDestroy(state.subscribeAll(['_internal.list.rowsWithParentsExpanded;', 'config.scroll.top', 'config.chart.items'], generateVisibleRowsAndItems, { bulk: true }));
|
|
let elementScrollTop = 0;
|
|
function onVisibleRowsChange() {
|
|
const top = state.get('config.scroll.top');
|
|
verticalScrollAreaStyleMap.style.width = '1px';
|
|
verticalScrollAreaStyleMap.style.height = rowsHeight + 'px';
|
|
if (elementScrollTop !== top && verticalScrollBarElement) {
|
|
elementScrollTop = top;
|
|
verticalScrollBarElement.scrollTop = top;
|
|
}
|
|
update();
|
|
}
|
|
onDestroy(state.subscribe('_internal.list.visibleRows;', onVisibleRowsChange));
|
|
/**
|
|
* Generate and add period dates
|
|
* @param {string} period
|
|
* @param {object} internalTime
|
|
*/
|
|
const generatePeriodDates = (period, internalTime) => {
|
|
const dates = [];
|
|
let leftGlobal = internalTime.leftGlobal;
|
|
const timePerPixel = internalTime.timePerPixel;
|
|
let startOfLeft = api.time
|
|
.date(leftGlobal)
|
|
.startOf(period)
|
|
.valueOf();
|
|
if (startOfLeft < leftGlobal)
|
|
startOfLeft = leftGlobal;
|
|
let sub = leftGlobal - startOfLeft;
|
|
let subPx = sub / timePerPixel;
|
|
let leftPx = 0;
|
|
const diff = Math.ceil(api.time
|
|
.date(internalTime.rightGlobal)
|
|
.endOf(period)
|
|
.diff(api.time.date(leftGlobal).startOf(period), period, true));
|
|
for (let i = 0; i < diff; i++) {
|
|
const date = {
|
|
sub,
|
|
subPx,
|
|
leftGlobal,
|
|
rightGlobal: api.time
|
|
.date(leftGlobal)
|
|
.endOf(period)
|
|
.valueOf(),
|
|
width: 0,
|
|
leftPx: 0,
|
|
rightPx: 0,
|
|
period
|
|
};
|
|
date.width = (date.rightGlobal - date.leftGlobal + sub) / timePerPixel;
|
|
date.leftPx = leftPx;
|
|
leftPx += date.width;
|
|
date.rightPx = leftPx;
|
|
dates.push(date);
|
|
leftGlobal = date.rightGlobal + 1;
|
|
sub = 0;
|
|
subPx = 0;
|
|
}
|
|
return dates;
|
|
};
|
|
function triggerLoadedEvent() {
|
|
if (state.get('_internal.loadedEventTriggered'))
|
|
return;
|
|
Promise.resolve().then(() => {
|
|
const element = state.get('_internal.elements.main');
|
|
const parent = element.parentNode;
|
|
const event = new Event('gstc-loaded');
|
|
element.dispatchEvent(event);
|
|
parent.dispatchEvent(event);
|
|
});
|
|
state.update('_internal.loadedEventTriggered', true);
|
|
}
|
|
function limitGlobalAndSetCenter(time) {
|
|
if (time.leftGlobal < time.finalFrom)
|
|
time.leftGlobal = time.finalFrom;
|
|
if (time.rightGlobal > time.finalTo)
|
|
time.rightGlobal = time.finalTo;
|
|
time.centerGlobal = time.leftGlobal + Math.round((time.rightGlobal - time.leftGlobal) / 2);
|
|
return time;
|
|
}
|
|
function guessPeriod(time, calendar) {
|
|
if (!time.zoom)
|
|
return time;
|
|
for (const level of calendar.levels) {
|
|
const formatting = level.formats.find(format => +time.zoom <= +format.zoomTo);
|
|
if (formatting && level.main) {
|
|
time.period = formatting.period;
|
|
}
|
|
}
|
|
return time;
|
|
}
|
|
function updateLevels(time, calendar) {
|
|
time.levels = [];
|
|
let index = 0;
|
|
for (const level of calendar.levels) {
|
|
const formatting = level.formats.find(format => +time.zoom <= +format.zoomTo);
|
|
if (level.main) {
|
|
time.format = formatting;
|
|
time.level = index;
|
|
}
|
|
if (formatting) {
|
|
time.levels.push(generatePeriodDates(formatting.period, time));
|
|
}
|
|
index++;
|
|
}
|
|
}
|
|
let working = false;
|
|
function recalculateTimes(reason) {
|
|
if (working)
|
|
return;
|
|
working = true;
|
|
const configTime = state.get('config.chart.time');
|
|
const chartWidth = state.get('_internal.chart.dimensions.width');
|
|
const calendar = state.get('config.chart.calendar');
|
|
const oldTime = Object.assign({}, state.get('_internal.chart.time'));
|
|
let time = api.mergeDeep({}, configTime);
|
|
if ((!time.from || !time.to) && !Object.keys(state.get('config.chart.items')).length) {
|
|
return;
|
|
}
|
|
let mainLevel = calendar.levels.find(level => level.main);
|
|
if (!mainLevel) {
|
|
throw new Error('Main calendar level not found (config.chart.calendar.levels).');
|
|
}
|
|
if (!time.calculatedZoomMode) {
|
|
if (time.period !== oldTime.period) {
|
|
let periodFormat = mainLevel.formats.find(format => format.period === time.period && format.default);
|
|
if (periodFormat) {
|
|
time.zoom = periodFormat.zoomTo;
|
|
}
|
|
}
|
|
guessPeriod(time, calendar);
|
|
}
|
|
// If _internal.chart.time (leftGlobal, centerGlobal, rightGlobal, from , to) was changed
|
|
// then we need to apply those values - no recalculation is needed (values form plugins etc)
|
|
const justApply = ['leftGlobal', 'centerGlobal', 'rightGlobal', 'from', 'to'].includes(reason.name);
|
|
if (justApply) {
|
|
time = Object.assign(Object.assign({}, time), { leftGlobal: configTime.leftGlobal, centerGlobal: configTime.centerGlobal, rightGlobal: configTime.rightGlobal, from: configTime.from, to: configTime.to });
|
|
}
|
|
let scrollLeft = 0;
|
|
// source of everything = time.timePerPixel
|
|
if (time.calculatedZoomMode && chartWidth) {
|
|
time.finalFrom = time.from;
|
|
time.finalTo = time.to;
|
|
time.totalViewDurationMs = api.time.date(time.finalTo).diff(time.finalFrom, 'milliseconds');
|
|
time.timePerPixel = time.totalViewDurationMs / chartWidth;
|
|
time.zoom = Math.log(time.timePerPixel) / Math.log(2);
|
|
guessPeriod(time, calendar);
|
|
time.totalViewDurationPx = Math.round(time.totalViewDurationMs / time.timePerPixel);
|
|
time.leftGlobal = time.from;
|
|
time.rightGlobal = time.to;
|
|
}
|
|
else {
|
|
time.timePerPixel = Math.pow(2, time.zoom);
|
|
time = api.time.recalculateFromTo(time);
|
|
time.totalViewDurationMs = api.time.date(time.finalTo).diff(time.finalFrom, 'milliseconds');
|
|
time.totalViewDurationPx = Math.round(time.totalViewDurationMs / time.timePerPixel);
|
|
scrollLeft = state.get('config.scroll.left');
|
|
}
|
|
if (!justApply && !time.calculatedZoomMode) {
|
|
// If time.zoom (or time.period) has been changed
|
|
// then we need to recalculate basing on time.centerGlobal
|
|
// and update scroll left
|
|
// if not then we need to calculate from scroll left
|
|
// because change was triggered by scroll
|
|
if (time.zoom !== oldTime.zoom && oldTime.centerGlobal) {
|
|
const chartWidthInMs = chartWidth * time.timePerPixel;
|
|
const halfChartInMs = Math.round(chartWidthInMs / 2);
|
|
time.leftGlobal = oldTime.centerGlobal - halfChartInMs;
|
|
time.rightGlobal = time.leftGlobal + chartWidthInMs;
|
|
scrollLeft = (time.leftGlobal - time.finalFrom) / time.timePerPixel;
|
|
scrollLeft = api.limitScrollLeft(time.totalViewDurationPx, chartWidth, scrollLeft);
|
|
}
|
|
else {
|
|
time.leftGlobal = scrollLeft * time.timePerPixel + time.finalFrom;
|
|
time.rightGlobal = time.leftGlobal + chartWidth * time.timePerPixel;
|
|
}
|
|
}
|
|
limitGlobalAndSetCenter(time);
|
|
time.leftInner = time.leftGlobal - time.finalFrom;
|
|
time.rightInner = time.rightGlobal - time.finalFrom;
|
|
time.leftPx = time.leftInner / time.timePerPixel;
|
|
time.rightPx = time.rightInner / time.timePerPixel;
|
|
updateLevels(time, calendar);
|
|
let xCompensation = 0;
|
|
if (time.levels[time.level] && time.levels[time.level].length !== 0) {
|
|
xCompensation = time.levels[time.level][0].subPx;
|
|
}
|
|
state.update(`_internal.chart.time`, time);
|
|
state.update('config.scroll.compensation.x', xCompensation);
|
|
state.update('config.chart.time', configTime => {
|
|
configTime.zoom = time.zoom;
|
|
configTime.period = time.format.period;
|
|
configTime.leftGlobal = time.leftGlobal;
|
|
configTime.centerGlobal = time.centerGlobal;
|
|
configTime.rightGlobal = time.rightGlobal;
|
|
configTime.from = time.from;
|
|
configTime.to = time.to;
|
|
configTime.finalFrom = time.finalFrom;
|
|
configTime.finalTo = time.finalTo;
|
|
return configTime;
|
|
});
|
|
state.update('config.scroll.left', scrollLeft);
|
|
update().then(() => {
|
|
if (!state.get('_internal.loaded.time')) {
|
|
state.update('_internal.loaded.time', true);
|
|
}
|
|
});
|
|
working = false;
|
|
}
|
|
const recalculationTriggerCache = {
|
|
initialized: false,
|
|
zoom: 0,
|
|
period: '',
|
|
scrollLeft: 0,
|
|
chartWidth: 0,
|
|
leftGlobal: 0,
|
|
centerGlobal: 0,
|
|
rightGlobal: 0,
|
|
from: 0,
|
|
to: 0
|
|
};
|
|
function recalculationIsNeeded() {
|
|
const configTime = state.get('config.chart.time');
|
|
const scrollLeft = state.get('config.scroll.left');
|
|
const chartWidth = state.get('_internal.chart.dimensions.width');
|
|
const cache = Object.assign({}, recalculationTriggerCache);
|
|
recalculationTriggerCache.zoom = configTime.zoom;
|
|
recalculationTriggerCache.period = configTime.period;
|
|
recalculationTriggerCache.leftGlobal = configTime.leftGlobal;
|
|
recalculationTriggerCache.centerGlobal = configTime.centerGlobal;
|
|
recalculationTriggerCache.rightGlobal = configTime.rightGlobal;
|
|
recalculationTriggerCache.from = configTime.from;
|
|
recalculationTriggerCache.to = configTime.to;
|
|
recalculationTriggerCache.scrollLeft = scrollLeft;
|
|
recalculationTriggerCache.chartWidth = chartWidth;
|
|
if (!recalculationTriggerCache.initialized) {
|
|
recalculationTriggerCache.initialized = true;
|
|
return { name: 'all' };
|
|
}
|
|
if (configTime.zoom !== cache.zoom)
|
|
return { name: 'zoom', oldValue: cache.zoom, newValue: configTime.zoom };
|
|
if (configTime.period !== cache.period)
|
|
return { name: 'period', oldValue: cache.period, newValue: configTime.period };
|
|
if (configTime.leftGlobal !== cache.leftGlobal)
|
|
return { name: 'leftGlobal', oldValue: cache.leftGlobal, newValue: configTime.leftGlobal };
|
|
if (configTime.centerGlobal !== cache.centerGlobal)
|
|
return { name: 'centerGlobal', oldValue: cache.centerGlobal, newValue: configTime.centerGlobal };
|
|
if (configTime.rightGlobal !== cache.rightGlobal)
|
|
return { name: 'rightGlobal', oldValue: cache.rightGlobal, newValue: configTime.rightGlobal };
|
|
if (configTime.from !== cache.from)
|
|
return { name: 'from', oldValue: cache.from, newValue: configTime.from };
|
|
if (configTime.to !== cache.to)
|
|
return { name: 'to', oldValue: cache.to, newValue: configTime.to };
|
|
if (scrollLeft !== cache.scrollLeft)
|
|
return { name: 'scroll', oldValue: cache.scrollLeft, newValue: scrollLeft };
|
|
if (chartWidth !== cache.chartWidth)
|
|
return { name: 'chartWidth', oldValue: cache.chartWidth, newValue: chartWidth };
|
|
return false;
|
|
}
|
|
onDestroy(state.subscribeAll(['config.chart.time', 'config.chart.calendar.levels', 'config.scroll.left', '_internal.chart.dimensions.width'], () => {
|
|
let reason = recalculationIsNeeded();
|
|
if (reason)
|
|
recalculateTimes(reason);
|
|
}, { bulk: true }));
|
|
// When time.from and time.to is not specified and items are reloaded;
|
|
// check if item is outside current time scope and extend it if needed
|
|
onDestroy(state.subscribe('config.chart.items.*.time', items => {
|
|
recalculateTimes({ name: 'items' });
|
|
}, { bulk: true }));
|
|
if (state.get('config.usageStatistics') === true && !localStorage.getItem('gstcus')) {
|
|
try {
|
|
fetch('https://gstc-us.neuronet.io/', {
|
|
method: 'POST',
|
|
cache: 'force-cache',
|
|
mode: 'cors',
|
|
credentials: 'omit',
|
|
redirect: 'follow',
|
|
body: JSON.stringify({ location: { href: location.href, host: location.host } })
|
|
}).catch(e => { });
|
|
localStorage.setItem('gstcus', 'true');
|
|
}
|
|
catch (e) { }
|
|
}
|
|
let scrollTop = 0;
|
|
let propagate = true;
|
|
onDestroy(state.subscribe('config.scroll.propagate', prpgt => (propagate = prpgt)));
|
|
/**
|
|
* Handle scroll Event
|
|
* @param {MouseEvent} event
|
|
*/
|
|
function handleEvent(event) {
|
|
if (!propagate) {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
}
|
|
if (event.type === 'scroll') {
|
|
// @ts-ignore
|
|
const top = event.target.scrollTop;
|
|
/**
|
|
* Handle on scroll event
|
|
* @param {object} scroll
|
|
* @returns {object} scroll
|
|
*/
|
|
const handleOnScroll = scroll => {
|
|
scroll.top = top;
|
|
scrollTop = scroll.top;
|
|
const scrollInner = state.get('_internal.elements.vertical-scroll-inner');
|
|
if (scrollInner) {
|
|
const scrollHeight = scrollInner.clientHeight;
|
|
scroll.percent.top = scroll.top / scrollHeight;
|
|
}
|
|
return scroll;
|
|
};
|
|
if (scrollTop !== top)
|
|
state.update('config.scroll', handleOnScroll, {
|
|
only: ['top', 'percent.top']
|
|
});
|
|
}
|
|
}
|
|
const onScroll = {
|
|
handleEvent,
|
|
passive: false,
|
|
capture: false
|
|
};
|
|
const dimensions = { width: 0, height: 0 };
|
|
let ro;
|
|
/**
|
|
* Resize action
|
|
* @param {Element} element
|
|
*/
|
|
class ResizeAction {
|
|
constructor(element) {
|
|
if (!ro) {
|
|
ro = new index((entries, observer) => {
|
|
const width = element.clientWidth;
|
|
const height = element.clientHeight;
|
|
if (dimensions.width !== width || dimensions.height !== height) {
|
|
dimensions.width = width;
|
|
dimensions.height = height;
|
|
state.update('_internal.dimensions', dimensions);
|
|
}
|
|
});
|
|
ro.observe(element);
|
|
state.update('_internal.elements.main', element);
|
|
}
|
|
}
|
|
update() { }
|
|
destroy(element) {
|
|
ro.unobserve(element);
|
|
}
|
|
}
|
|
if (!componentActions.includes(ResizeAction)) {
|
|
componentActions.push(ResizeAction);
|
|
}
|
|
onDestroy(() => {
|
|
ro.disconnect();
|
|
});
|
|
/**
|
|
* Bind scroll element
|
|
* @param {HTMLElement} element
|
|
*/
|
|
function bindScrollElement(element) {
|
|
if (!verticalScrollBarElement) {
|
|
verticalScrollBarElement = element;
|
|
state.update('_internal.elements.vertical-scroll', element);
|
|
}
|
|
}
|
|
onDestroy(state.subscribeAll(['_internal.loaded', '_internal.chart.time.totalViewDurationPx'], () => {
|
|
if (state.get('_internal.loadedEventTriggered'))
|
|
return;
|
|
const loaded = state.get('_internal.loaded');
|
|
if (loaded.main && loaded.chart && loaded.time && loaded['horizontal-scroll-inner']) {
|
|
const scroll = state.get('_internal.elements.horizontal-scroll-inner');
|
|
const width = state.get('_internal.chart.time.totalViewDurationPx');
|
|
if (scroll && scroll.clientWidth === Math.round(width)) {
|
|
setTimeout(triggerLoadedEvent, 0);
|
|
}
|
|
}
|
|
}));
|
|
function LoadedEventAction() {
|
|
state.update('_internal.loaded.main', true);
|
|
}
|
|
if (!componentActions.includes(LoadedEventAction))
|
|
componentActions.push(LoadedEventAction);
|
|
/**
|
|
* Bind scroll inner element
|
|
* @param {Element} element
|
|
*/
|
|
function bindScrollInnerElement(element) {
|
|
if (!state.get('_internal.elements.vertical-scroll-inner'))
|
|
state.update('_internal.elements.vertical-scroll-inner', element);
|
|
if (!state.get('_internal.loaded.vertical-scroll-inner'))
|
|
state.update('_internal.loaded.vertical-scroll-inner', true);
|
|
}
|
|
const actionProps = Object.assign(Object.assign({}, props), { api, state });
|
|
const mainActions = Actions.create(componentActions, actionProps);
|
|
const verticalScrollActions = Actions.create([bindScrollElement]);
|
|
const verticalScrollAreaActions = Actions.create([bindScrollInnerElement]);
|
|
return templateProps => wrapper(html `
|
|
<div
|
|
data-info-url="https://github.com/neuronetio/gantt-schedule-timeline-calendar"
|
|
class=${className}
|
|
style=${styleMap}
|
|
@scroll=${onScroll}
|
|
@wheel=${onScroll}
|
|
data-actions=${mainActions}
|
|
>
|
|
${List.html()}${Chart.html()}
|
|
<div
|
|
class=${classNameVerticalScroll}
|
|
style=${verticalScrollStyleMap}
|
|
@scroll=${onScroll}
|
|
@wheel=${onScroll}
|
|
data-actions=${verticalScrollActions}
|
|
>
|
|
<div style=${verticalScrollAreaStyleMap} data-actions=${verticalScrollAreaActions} />
|
|
</div>
|
|
</div>
|
|
`, { props, vido, templateProps });
|
|
}
|
|
|
|
/**
|
|
* List component
|
|
*
|
|
* @copyright Rafal Pospiech <https://neuronet.io>
|
|
* @author Rafal Pospiech <neuronet.io@gmail.com>
|
|
* @package gantt-schedule-timeline-calendar
|
|
* @license AGPL-3.0 (https://github.com/neuronetio/gantt-schedule-timeline-calendar/blob/master/LICENSE)
|
|
* @link https://github.com/neuronetio/gantt-schedule-timeline-calendar
|
|
*/
|
|
function List(vido, props = {}) {
|
|
const { api, state, onDestroy, Actions, update, reuseComponents, html, schedule, StyleMap, cache } = vido;
|
|
const componentName = 'list';
|
|
const componentActions = api.getActions(componentName);
|
|
let wrapper;
|
|
onDestroy(state.subscribe('config.wrappers.List', value => (wrapper = value)));
|
|
let ListColumnComponent;
|
|
const listColumnUnsub = state.subscribe('config.components.ListColumn', value => (ListColumnComponent = value));
|
|
function renderExpanderIcons() {
|
|
const icons = state.get('config.list.expander.icons');
|
|
const rendered = {};
|
|
for (const iconName in icons) {
|
|
const html = icons[iconName];
|
|
rendered[iconName] = api.getSVGIconSrc(html);
|
|
}
|
|
state.update('_internal.list.expander.icons', rendered);
|
|
}
|
|
renderExpanderIcons();
|
|
function renderToggleIcons() {
|
|
const toggleIconsSrc = {
|
|
open: '',
|
|
close: ''
|
|
};
|
|
const icons = state.get('config.list.toggle.icons');
|
|
for (const iconName in icons) {
|
|
const html = icons[iconName];
|
|
toggleIconsSrc[iconName] = api.getSVGIconSrc(html);
|
|
}
|
|
state.update('_internal.list.toggle.icons', toggleIconsSrc);
|
|
}
|
|
renderToggleIcons();
|
|
let className;
|
|
let list, percent;
|
|
function onListChange() {
|
|
list = state.get('config.list');
|
|
percent = list.columns.percent;
|
|
update();
|
|
}
|
|
onDestroy(state.subscribe('config.list', onListChange));
|
|
onDestroy(state.subscribe('config.classNames', () => {
|
|
className = api.getClass(componentName, { list });
|
|
update();
|
|
}));
|
|
let listColumns = [];
|
|
function onListColumnsDataChange(data) {
|
|
const destroy = reuseComponents(listColumns, Object.values(data), column => ({ columnId: column.id }), ListColumnComponent);
|
|
update();
|
|
return destroy;
|
|
}
|
|
onDestroy(state.subscribe('config.list.columns.data;', onListColumnsDataChange));
|
|
const styleMap = new StyleMap({
|
|
height: '',
|
|
'--expander-padding-width': '',
|
|
'--expander-size': ''
|
|
});
|
|
onDestroy(state.subscribeAll(['config.height', 'config.list.expander'], bulk => {
|
|
const expander = state.get('config.list.expander');
|
|
styleMap.style['height'] = state.get('config.height') + 'px';
|
|
styleMap.style['--expander-padding-width'] = expander.padding + 'px';
|
|
styleMap.style['--expander-size'] = expander.size + 'px';
|
|
update();
|
|
}));
|
|
onDestroy(() => {
|
|
listColumns.forEach(listColumn => listColumn.destroy());
|
|
listColumnUnsub();
|
|
});
|
|
function onScroll(event) {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
if (event.type === 'scroll') {
|
|
state.update('config.scroll.top', event.target.scrollTop);
|
|
}
|
|
else {
|
|
const wheel = api.normalizeMouseWheelEvent(event);
|
|
state.update('config.scroll.top', top => {
|
|
const rowsHeight = state.get('_internal.list.rowsHeight');
|
|
const internalHeight = state.get('_internal.height');
|
|
return api.limitScrollTop(rowsHeight, internalHeight, (top += wheel.y * state.get('config.scroll.yMultiplier')));
|
|
});
|
|
}
|
|
}
|
|
let width;
|
|
function getWidth(element) {
|
|
if (!width) {
|
|
width = element.clientWidth;
|
|
if (percent === 0) {
|
|
width = 0;
|
|
}
|
|
state.update('_internal.list.width', width);
|
|
}
|
|
}
|
|
class ListAction {
|
|
constructor(element, data) {
|
|
data.state.update('_internal.elements.list', element);
|
|
getWidth(element);
|
|
}
|
|
update(element) {
|
|
return getWidth(element);
|
|
}
|
|
}
|
|
componentActions.push(ListAction);
|
|
const actions = Actions.create(componentActions, Object.assign(Object.assign({}, props), { api, state }));
|
|
return templateProps => wrapper(cache(list.columns.percent > 0
|
|
? html `
|
|
<div class=${className} data-actions=${actions} style=${styleMap} @scroll=${onScroll} @wheel=${onScroll}>
|
|
${listColumns.map(c => c.html())}
|
|
</div>
|
|
`
|
|
: ''), { vido, props: {}, templateProps });
|
|
}
|
|
|
|
/**
|
|
* ListColumn component
|
|
*
|
|
* @copyright Rafal Pospiech <https://neuronet.io>
|
|
* @author Rafal Pospiech <neuronet.io@gmail.com>
|
|
* @package gantt-schedule-timeline-calendar
|
|
* @license AGPL-3.0 (https://github.com/neuronetio/gantt-schedule-timeline-calendar/blob/master/LICENSE)
|
|
* @link https://github.com/neuronetio/gantt-schedule-timeline-calendar
|
|
*/
|
|
/**
|
|
* Bind element action
|
|
*/
|
|
class BindElementAction {
|
|
constructor(element, data) {
|
|
let shouldUpdate = false;
|
|
let elements = data.state.get('_internal.elements.list-columns');
|
|
if (typeof elements === 'undefined') {
|
|
elements = [];
|
|
shouldUpdate = true;
|
|
}
|
|
if (!elements.includes(element)) {
|
|
elements.push(element);
|
|
shouldUpdate = true;
|
|
}
|
|
if (shouldUpdate)
|
|
data.state.update('_internal.elements.list-columns', elements);
|
|
}
|
|
destroy(element, data) {
|
|
data.state.update('_internal.elements.list-columns', elements => {
|
|
return elements.filter(el => el !== element);
|
|
});
|
|
}
|
|
}
|
|
function ListColumn(vido, props) {
|
|
const { api, state, onDestroy, onChange, Actions, update, createComponent, reuseComponents, html, StyleMap } = vido;
|
|
let wrapper;
|
|
onDestroy(state.subscribe('config.wrappers.ListColumn', value => (wrapper = value)));
|
|
const componentsSub = [];
|
|
let ListColumnRowComponent;
|
|
componentsSub.push(state.subscribe('config.components.ListColumnRow', value => (ListColumnRowComponent = value)));
|
|
let ListColumnHeaderComponent;
|
|
componentsSub.push(state.subscribe('config.components.ListColumnHeader', value => (ListColumnHeaderComponent = value)));
|
|
const actionProps = Object.assign(Object.assign({}, props), { api, state });
|
|
const componentName = 'list-column';
|
|
const rowsComponentName = componentName + '-rows';
|
|
const componentActions = api.getActions(componentName);
|
|
const rowsActions = api.getActions(rowsComponentName);
|
|
let className, classNameContainer, calculatedWidth;
|
|
const widthStyleMap = new StyleMap({ width: '', '--width': '' });
|
|
const containerStyleMap = new StyleMap({ width: '', height: '' });
|
|
const scrollCompensationStyleMap = new StyleMap({ width: '', height: '' });
|
|
let column, columnPath = `config.list.columns.data.${props.columnId}`;
|
|
let columnSub = state.subscribe(columnPath, function columnChanged(val) {
|
|
column = val;
|
|
update();
|
|
});
|
|
let width;
|
|
function calculateStyle() {
|
|
const list = state.get('config.list');
|
|
const compensationY = state.get('config.scroll.compensation.y');
|
|
calculatedWidth = list.columns.data[column.id].width * list.columns.percent * 0.01;
|
|
width = calculatedWidth;
|
|
const height = state.get('_internal.height');
|
|
widthStyleMap.style.width = width + 'px';
|
|
widthStyleMap.style['--width'] = width + 'px';
|
|
containerStyleMap.style.height = height + 'px';
|
|
scrollCompensationStyleMap.style.height = height + Math.abs(compensationY) + 'px';
|
|
scrollCompensationStyleMap.style.transform = `translate(0px, ${compensationY}px)`;
|
|
}
|
|
let styleSub = state.subscribeAll([
|
|
'config.list.columns.percent',
|
|
'config.list.columns.resizer.width',
|
|
`config.list.columns.data.${column.id}.width`,
|
|
'_internal.chart.dimensions.width',
|
|
'_internal.height',
|
|
'config.scroll.compensation.y',
|
|
'_internal.list.width'
|
|
], calculateStyle, { bulk: true });
|
|
const ListColumnHeader = createComponent(ListColumnHeaderComponent, { columnId: props.columnId });
|
|
onDestroy(ListColumnHeader.destroy);
|
|
onChange(changedProps => {
|
|
props = changedProps;
|
|
for (const prop in props) {
|
|
actionProps[prop] = props[prop];
|
|
}
|
|
if (columnSub)
|
|
columnSub();
|
|
ListColumnHeader.change({ columnId: props.columnId });
|
|
columnPath = `config.list.columns.data.${props.columnId}`;
|
|
columnSub = state.subscribe(columnPath, function columnChanged(val) {
|
|
column = val;
|
|
update();
|
|
});
|
|
if (styleSub)
|
|
styleSub();
|
|
styleSub = state.subscribeAll([
|
|
'config.list.columns.percent',
|
|
'config.list.columns.resizer.width',
|
|
`config.list.columns.data.${column.id}.width`,
|
|
'_internal.chart.dimensions.width',
|
|
'_internal.height',
|
|
'config.scroll.compensation.y',
|
|
'_internal.list.width'
|
|
], calculateStyle, { bulk: true });
|
|
ListColumnHeader.change(props);
|
|
});
|
|
onDestroy(() => {
|
|
columnSub();
|
|
styleSub();
|
|
});
|
|
onDestroy(state.subscribe('config.classNames', value => {
|
|
className = api.getClass(componentName);
|
|
classNameContainer = api.getClass(rowsComponentName);
|
|
update();
|
|
}));
|
|
const visibleRows = [];
|
|
const visibleRowsChange = val => {
|
|
const destroy = reuseComponents(visibleRows, val || [], row => row && { columnId: props.columnId, rowId: row.id, width }, ListColumnRowComponent);
|
|
update();
|
|
return destroy;
|
|
};
|
|
onDestroy(state.subscribe('_internal.list.visibleRows;', visibleRowsChange));
|
|
onDestroy(() => {
|
|
visibleRows.forEach(row => row.destroy());
|
|
componentsSub.forEach(unsub => unsub());
|
|
});
|
|
function getRowHtml(row) {
|
|
return row.html();
|
|
}
|
|
componentActions.push(BindElementAction);
|
|
const headerActions = Actions.create(componentActions, { column, state: state, api: api });
|
|
const rowActions = Actions.create(rowsActions, { api, state });
|
|
return templateProps => wrapper(html `
|
|
<div class=${className} data-actions=${headerActions} style=${widthStyleMap}>
|
|
${ListColumnHeader.html()}
|
|
<div class=${classNameContainer} style=${containerStyleMap} data-actions=${rowActions}>
|
|
<div class=${classNameContainer + '--scroll-compensation'} style=${scrollCompensationStyleMap}>
|
|
${visibleRows.map(getRowHtml)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`, { vido, props, templateProps });
|
|
}
|
|
|
|
/**
|
|
* ListColumnHeader component
|
|
*
|
|
* @copyright Rafal Pospiech <https://neuronet.io>
|
|
* @author Rafal Pospiech <neuronet.io@gmail.com>
|
|
* @package gantt-schedule-timeline-calendar
|
|
* @license AGPL-3.0 (https://github.com/neuronetio/gantt-schedule-timeline-calendar/blob/master/LICENSE)
|
|
* @link https://github.com/neuronetio/gantt-schedule-timeline-calendar
|
|
*/
|
|
function ListColumnHeader(vido, props) {
|
|
const { api, state, onDestroy, onChange, Actions, update, createComponent, html, cache, StyleMap } = vido;
|
|
const actionProps = Object.assign(Object.assign({}, props), { api, state });
|
|
let wrapper;
|
|
onDestroy(state.subscribe('config.wrappers.ListColumnHeader', value => (wrapper = value)));
|
|
const componentName = 'list-column-header';
|
|
const componentActions = api.getActions(componentName);
|
|
const componentsSubs = [];
|
|
let ListColumnHeaderResizerComponent;
|
|
componentsSubs.push(state.subscribe('config.components.ListColumnHeaderResizer', value => (ListColumnHeaderResizerComponent = value)));
|
|
const ListColumnHeaderResizer = createComponent(ListColumnHeaderResizerComponent, { columnId: props.columnId });
|
|
let ListColumnRowExpanderComponent;
|
|
componentsSubs.push(state.subscribe('config.components.ListColumnRowExpander', value => (ListColumnRowExpanderComponent = value)));
|
|
const ListColumnRowExpander = createComponent(ListColumnRowExpanderComponent, {});
|
|
onDestroy(() => {
|
|
ListColumnHeaderResizer.destroy();
|
|
ListColumnRowExpander.destroy();
|
|
componentsSubs.forEach(unsub => unsub());
|
|
});
|
|
let column;
|
|
let columnSub = state.subscribe(`config.list.columns.data.${props.columnId}`, val => {
|
|
column = val;
|
|
update();
|
|
});
|
|
onDestroy(columnSub);
|
|
onChange(changedProps => {
|
|
props = changedProps;
|
|
for (const prop in props) {
|
|
actionProps[prop] = props[prop];
|
|
}
|
|
if (columnSub)
|
|
columnSub();
|
|
columnSub = state.subscribe(`config.list.columns.data.${props.columnId}`, val => {
|
|
column = val;
|
|
update();
|
|
});
|
|
});
|
|
let className, contentClass;
|
|
onDestroy(state.subscribe('config.classNames', () => {
|
|
className = api.getClass(componentName);
|
|
contentClass = api.getClass(componentName + '-content');
|
|
}));
|
|
const styleMap = new StyleMap({
|
|
height: '',
|
|
'--height': '',
|
|
'--paddings-count': ''
|
|
});
|
|
onDestroy(state.subscribe('config.headerHeight', () => {
|
|
const value = state.get('config');
|
|
styleMap.style['height'] = value.headerHeight + 'px';
|
|
styleMap.style['--height'] = value.headerHeight + 'px';
|
|
styleMap.style['--paddings-count'] = '1';
|
|
update();
|
|
}));
|
|
function withExpander() {
|
|
return html `
|
|
<div class=${contentClass}>
|
|
${ListColumnRowExpander.html()}${ListColumnHeaderResizer.html(column)}
|
|
</div>
|
|
`;
|
|
}
|
|
function withoutExpander() {
|
|
return html `
|
|
<div class=${contentClass}>
|
|
${ListColumnHeaderResizer.html(column)}
|
|
</div>
|
|
`;
|
|
}
|
|
const actions = Actions.create(componentActions, actionProps);
|
|
return templateProps => wrapper(html `
|
|
<div class=${className} style=${styleMap} data-actions=${actions}>
|
|
${cache(column.expander ? withExpander() : withoutExpander())}
|
|
</div>
|
|
`, { vido, props, templateProps });
|
|
}
|
|
|
|
/**
|
|
* ListColumnHeaderResizer component
|
|
*
|
|
* @copyright Rafal Pospiech <https://neuronet.io>
|
|
* @author Rafal Pospiech <neuronet.io@gmail.com>
|
|
* @package gantt-schedule-timeline-calendar
|
|
* @license AGPL-3.0 (https://github.com/neuronetio/gantt-schedule-timeline-calendar/blob/master/LICENSE)
|
|
* @link https://github.com/neuronetio/gantt-schedule-timeline-calendar
|
|
*/
|
|
function ListColumnHeaderResizer(vido, props) {
|
|
const { api, state, onDestroy, update, html, schedule, Actions, PointerAction, cache, StyleMap } = vido;
|
|
const componentName = 'list-column-header-resizer';
|
|
const componentActions = api.getActions(componentName);
|
|
const componentDotsActions = api.getActions(componentName + '-dots');
|
|
let wrapper;
|
|
onDestroy(state.subscribe('config.wrappers.ListColumnHeaderResizer', value => (wrapper = value)));
|
|
let column;
|
|
onDestroy(state.subscribe(`config.list.columns.data.${props.columnId}`, val => {
|
|
column = val;
|
|
update();
|
|
}));
|
|
let className, containerClass, dotsClass, dotClass, calculatedWidth;
|
|
const dotsStyleMap = new StyleMap({ width: '' });
|
|
let inRealTime = false;
|
|
onDestroy(state.subscribe('config.classNames', value => {
|
|
className = api.getClass(componentName, { column });
|
|
containerClass = api.getClass(componentName + '-container', { column });
|
|
dotsClass = api.getClass(componentName + '-dots', { column });
|
|
dotClass = api.getClass(componentName + '-dots-dot', { column });
|
|
update();
|
|
}));
|
|
onDestroy(state.subscribeAll([
|
|
`config.list.columns.data.${column.id}.width`,
|
|
'config.list.columns.percent',
|
|
'config.list.columns.resizer.width',
|
|
'config.list.columns.resizer.inRealTime'
|
|
], (value, path) => {
|
|
const list = state.get('config.list');
|
|
calculatedWidth = column.width * list.columns.percent * 0.01;
|
|
dotsStyleMap.style['--width'] = list.columns.resizer.width + 'px';
|
|
inRealTime = list.columns.resizer.inRealTime;
|
|
state.update('_internal.list.width', calculatedWidth);
|
|
update();
|
|
}));
|
|
let dots = [1, 2, 3, 4, 5, 6, 7, 8];
|
|
onDestroy(state.subscribe('config.list.columns.resizer.dots', value => {
|
|
dots = [];
|
|
for (let i = 0; i < value; i++) {
|
|
dots.push(i);
|
|
}
|
|
update();
|
|
}));
|
|
/*
|
|
let isMoving = false;
|
|
const lineStyleMap = new StyleMap({
|
|
'--display': 'none',
|
|
'--left': left + 'px'
|
|
});*/
|
|
let left = calculatedWidth;
|
|
const columnWidthPath = `config.list.columns.data.${column.id}.width`;
|
|
const actionProps = {
|
|
column,
|
|
api,
|
|
state,
|
|
pointerOptions: {
|
|
axis: 'x',
|
|
onMove: function onMove({ movementX }) {
|
|
let minWidth = state.get('config.list.columns.minWidth');
|
|
if (typeof column.minWidth === 'number') {
|
|
minWidth = column.minWidth;
|
|
}
|
|
left += movementX;
|
|
if (left < minWidth) {
|
|
left = minWidth;
|
|
}
|
|
if (inRealTime) {
|
|
state.update(columnWidthPath, left);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
componentActions.push(PointerAction);
|
|
const actions = Actions.create(componentActions, actionProps);
|
|
const dotsActions = Actions.create(componentDotsActions, actionProps);
|
|
return templateProps => wrapper(html `
|
|
<div class=${className} data-actions=${actions}>
|
|
<div class=${containerClass}>
|
|
${cache(column.header.html
|
|
? html `
|
|
${column.header.html}
|
|
`
|
|
: column.header.content)}
|
|
</div>
|
|
<div class=${dotsClass} style=${dotsStyleMap} data-actions=${dotsActions}>
|
|
${dots.map(dot => html `
|
|
<div class=${dotClass} />
|
|
`)}
|
|
</div>
|
|
</div>
|
|
`, { vido, props, templateProps });
|
|
}
|
|
|
|
/**
|
|
* ListColumnRow component
|
|
*
|
|
* @copyright Rafal Pospiech <https://neuronet.io>
|
|
* @author Rafal Pospiech <neuronet.io@gmail.com>
|
|
* @package gantt-schedule-timeline-calendar
|
|
* @license AGPL-3.0 (https://github.com/neuronetio/gantt-schedule-timeline-calendar/blob/master/LICENSE)
|
|
* @link https://github.com/neuronetio/gantt-schedule-timeline-calendar
|
|
*/
|
|
/**
|
|
* Bind element action
|
|
*/
|
|
class BindElementAction$1 {
|
|
constructor(element, data) {
|
|
let elements = data.state.get('_internal.elements.list-column-rows');
|
|
let shouldUpdate = false;
|
|
if (typeof elements === 'undefined') {
|
|
shouldUpdate = true;
|
|
elements = [];
|
|
}
|
|
if (!elements.includes(element)) {
|
|
elements.push(element);
|
|
shouldUpdate = true;
|
|
}
|
|
if (shouldUpdate)
|
|
data.state.update('_internal.elements.list-column-rows', elements);
|
|
}
|
|
destroy(element, data) {
|
|
data.state.update('_internal.elements.list-column-rows', elements => {
|
|
return elements.filter(el => el !== element);
|
|
});
|
|
}
|
|
}
|
|
function ListColumnRow(vido, props) {
|
|
const { api, state, onDestroy, Detach, Actions, update, html, createComponent, onChange, StyleMap, unsafeHTML, PointerAction } = vido;
|
|
const actionProps = Object.assign(Object.assign({}, props), { api, state });
|
|
let shouldDetach = false;
|
|
const detach = new Detach(() => shouldDetach);
|
|
let wrapper;
|
|
onDestroy(state.subscribe('config.wrappers.ListColumnRow', value => (wrapper = value)));
|
|
let ListColumnRowExpanderComponent;
|
|
onDestroy(state.subscribe('config.components.ListColumnRowExpander', value => (ListColumnRowExpanderComponent = value)));
|
|
let rowPath = `_internal.flatTreeMapById.${props.rowId}`, row = state.get(rowPath);
|
|
let colPath = `config.list.columns.data.${props.columnId}`, column = state.get(colPath);
|
|
const styleMap = new StyleMap(column.expander
|
|
? {
|
|
height: '',
|
|
top: '',
|
|
'--height': '',
|
|
'--expander-padding-width': '',
|
|
'--expander-size': ''
|
|
}
|
|
: {
|
|
height: '',
|
|
top: '',
|
|
'--height': ''
|
|
}, true);
|
|
let rowSub, colSub;
|
|
const ListColumnRowExpander = createComponent(ListColumnRowExpanderComponent, { row });
|
|
const onPropsChange = (changedProps, options) => {
|
|
if (options.leave || changedProps.rowId === undefined || changedProps.columnId === undefined) {
|
|
shouldDetach = true;
|
|
if (rowSub)
|
|
rowSub();
|
|
if (colSub)
|
|
colSub();
|
|
update();
|
|
return;
|
|
}
|
|
shouldDetach = false;
|
|
props = changedProps;
|
|
for (const prop in props) {
|
|
actionProps[prop] = props[prop];
|
|
}
|
|
const rowId = props.rowId;
|
|
const columnId = props.columnId;
|
|
if (rowSub)
|
|
rowSub();
|
|
if (colSub)
|
|
colSub();
|
|
rowPath = `_internal.flatTreeMapById.${rowId}`;
|
|
colPath = `config.list.columns.data.${columnId}`;
|
|
rowSub = state.subscribeAll([rowPath, colPath, 'config.list.expander'], bulk => {
|
|
column = state.get(colPath);
|
|
row = state.get(rowPath);
|
|
if (column === undefined || row === undefined) {
|
|
shouldDetach = true;
|
|
update();
|
|
return;
|
|
}
|
|
if (column === undefined || row === undefined)
|
|
return;
|
|
const expander = state.get('config.list.expander');
|
|
// @ts-ignore
|
|
styleMap.setStyle({}); // we must reset style because of user specified styling
|
|
styleMap.style['height'] = row.height + 'px';
|
|
styleMap.style['--height'] = row.height + 'px';
|
|
if (column.expander) {
|
|
styleMap.style['--expander-padding-width'] = expander.padding * (row._internal.parents.length + 1) + 'px';
|
|
}
|
|
for (const parentId of row._internal.parents) {
|
|
const parent = state.get(`_internal.flatTreeMapById.${parentId}`);
|
|
if (typeof parent.style === 'object' && parent.style.constructor.name === 'Object') {
|
|
if (typeof parent.style.children === 'object') {
|
|
const childrenStyle = parent.style.children;
|
|
for (const name in childrenStyle) {
|
|
styleMap.style[name] = childrenStyle[name];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (typeof row.style === 'object' &&
|
|
row.style.constructor.name === 'Object' &&
|
|
typeof row.style.current === 'object') {
|
|
const rowCurrentStyle = row.style.current;
|
|
for (const name in rowCurrentStyle) {
|
|
styleMap.style[name] = rowCurrentStyle[name];
|
|
}
|
|
}
|
|
update();
|
|
}, { bulk: true });
|
|
if (ListColumnRowExpander) {
|
|
ListColumnRowExpander.change({ row });
|
|
}
|
|
colSub = state.subscribe(colPath, val => {
|
|
column = val;
|
|
update();
|
|
});
|
|
};
|
|
onChange(onPropsChange);
|
|
onDestroy(() => {
|
|
if (ListColumnRowExpander)
|
|
ListColumnRowExpander.destroy();
|
|
colSub();
|
|
rowSub();
|
|
});
|
|
const componentName = 'list-column-row';
|
|
const componentActions = api.getActions(componentName);
|
|
let className;
|
|
onDestroy(state.subscribe('config.classNames', value => {
|
|
className = api.getClass(componentName);
|
|
update();
|
|
}));
|
|
function getHtml() {
|
|
if (row === undefined)
|
|
return null;
|
|
if (typeof column.data === 'function')
|
|
return unsafeHTML(column.data(row));
|
|
return unsafeHTML(row[column.data]);
|
|
}
|
|
function getText() {
|
|
if (row === undefined)
|
|
return null;
|
|
if (typeof column.data === 'function')
|
|
return column.data(row);
|
|
return row[column.data];
|
|
}
|
|
if (!componentActions.includes(BindElementAction$1))
|
|
componentActions.push(BindElementAction$1);
|
|
actionProps.pointerOptions = {
|
|
axis: 'x|y',
|
|
onMove({ event, movementX, movementY }) {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
if (movementX) {
|
|
state.update('config.list.columns.percent', percent => {
|
|
percent += movementX * state.get('config.scroll.xMultiplier');
|
|
if (percent < 0)
|
|
percent = 0;
|
|
if (percent > 100)
|
|
percent = 100;
|
|
return percent;
|
|
});
|
|
}
|
|
else if (movementY) {
|
|
state.update('config.scroll.top', top => {
|
|
top -= movementY * state.get('config.scroll.yMultiplier');
|
|
const rowsHeight = state.get('_internal.list.rowsHeight');
|
|
const internalHeight = state.get('_internal.height');
|
|
top = api.limitScrollTop(rowsHeight, internalHeight, top);
|
|
return top;
|
|
});
|
|
}
|
|
}
|
|
};
|
|
componentActions.push(PointerAction);
|
|
const actions = Actions.create(componentActions, actionProps);
|
|
return templateProps => wrapper(html `
|
|
<div detach=${detach} class=${className} style=${styleMap} data-actions=${actions}>
|
|
${column.expander ? ListColumnRowExpander.html() : null}
|
|
<div class=${className + '-content'}>
|
|
${column.isHTML ? getHtml() : getText()}
|
|
</div>
|
|
</div>
|
|
`, { vido, props, templateProps });
|
|
}
|
|
|
|
/**
|
|
* ListColumnRowExpander component
|
|
*
|
|
* @copyright Rafal Pospiech <https://neuronet.io>
|
|
* @author Rafal Pospiech <neuronet.io@gmail.com>
|
|
* @package gantt-schedule-timeline-calendar
|
|
* @license AGPL-3.0 (https://github.com/neuronetio/gantt-schedule-timeline-calendar/blob/master/LICENSE)
|
|
* @link https://github.com/neuronetio/gantt-schedule-timeline-calendar
|
|
*/
|
|
function ListColumnRowExpander(vido, props) {
|
|
const { api, state, onDestroy, Actions, update, html, createComponent, onChange } = vido;
|
|
const componentName = 'list-column-row-expander';
|
|
const componentActions = api.getActions(componentName);
|
|
const actionProps = Object.assign(Object.assign({}, props), { api, state });
|
|
let className;
|
|
let ListColumnRowExpanderToggleComponent;
|
|
const toggleUnsub = state.subscribe('config.components.ListColumnRowExpanderToggle', value => (ListColumnRowExpanderToggleComponent = value));
|
|
const ListColumnRowExpanderToggle = createComponent(ListColumnRowExpanderToggleComponent, props.row ? { row: props.row } : {});
|
|
onDestroy(() => {
|
|
ListColumnRowExpanderToggle.destroy();
|
|
toggleUnsub();
|
|
});
|
|
let wrapper;
|
|
onDestroy(state.subscribe('config.wrappers.ListColumnRowExpander', value => (wrapper = value)));
|
|
onDestroy(state.subscribe('config.classNames', value => {
|
|
className = api.getClass(componentName);
|
|
update();
|
|
}));
|
|
if (props.row) {
|
|
function onPropsChange(changedProps) {
|
|
props = changedProps;
|
|
for (const prop in props) {
|
|
actionProps[prop] = props[prop];
|
|
}
|
|
ListColumnRowExpanderToggle.change(props);
|
|
}
|
|
onChange(onPropsChange);
|
|
}
|
|
const actions = Actions.create(componentActions, actionProps);
|
|
return templateProps => wrapper(html `
|
|
<div class=${className} data-action=${actions}>
|
|
${ListColumnRowExpanderToggle.html()}
|
|
</div>
|
|
`, { vido, props, templateProps });
|
|
}
|
|
|
|
/**
|
|
* ListColumnRowExpanderToggle component
|
|
*
|
|
* @copyright Rafal Pospiech <https://neuronet.io>
|
|
* @author Rafal Pospiech <neuronet.io@gmail.com>
|
|
* @package gantt-schedule-timeline-calendar
|
|
* @license AGPL-3.0 (https://github.com/neuronetio/gantt-schedule-timeline-calendar/blob/master/LICENSE)
|
|
* @link https://github.com/neuronetio/gantt-schedule-timeline-calendar
|
|
*/
|
|
function ListColumnRowExpanderToggle(vido, props) {
|
|
const { api, state, onDestroy, Actions, update, html, onChange, cache } = vido;
|
|
const componentName = 'list-column-row-expander-toggle';
|
|
const actionProps = Object.assign(Object.assign({}, props), { api, state });
|
|
let wrapper;
|
|
onDestroy(state.subscribe('config.wrappers.ListColumnRowExpanderToggle', value => (wrapper = value)));
|
|
const componentActions = api.getActions(componentName);
|
|
let className, classNameChild, classNameOpen, classNameClosed;
|
|
let expanded = false;
|
|
let iconChild, iconOpen, iconClosed;
|
|
onDestroy(state.subscribe('config.classNames', value => {
|
|
className = api.getClass(componentName);
|
|
classNameChild = className + '-child';
|
|
classNameOpen = className + '-open';
|
|
classNameClosed = className + '-closed';
|
|
update();
|
|
}));
|
|
onDestroy(state.subscribe('_internal.list.expander.icons', icons => {
|
|
if (icons) {
|
|
iconChild = icons.child;
|
|
iconOpen = icons.open;
|
|
iconClosed = icons.closed;
|
|
}
|
|
update();
|
|
}));
|
|
if (props.row) {
|
|
function expandedChange(isExpanded) {
|
|
expanded = isExpanded;
|
|
update();
|
|
}
|
|
let expandedSub;
|
|
function onPropsChange(changedProps) {
|
|
var _a, _b;
|
|
props = changedProps;
|
|
for (const prop in props) {
|
|
actionProps[prop] = props[prop];
|
|
}
|
|
if (expandedSub)
|
|
expandedSub();
|
|
if ((_b = (_a = props) === null || _a === void 0 ? void 0 : _a.row) === null || _b === void 0 ? void 0 : _b.id)
|
|
expandedSub = state.subscribe(`config.list.rows.${props.row.id}.expanded`, expandedChange);
|
|
}
|
|
onChange(onPropsChange);
|
|
onDestroy(function listToggleDestroy() {
|
|
if (expandedSub)
|
|
expandedSub();
|
|
});
|
|
}
|
|
else {
|
|
function expandedChange(bulk) {
|
|
for (const rowExpanded of bulk) {
|
|
if (rowExpanded.value) {
|
|
expanded = true;
|
|
break;
|
|
}
|
|
}
|
|
update();
|
|
}
|
|
onDestroy(state.subscribe('config.list.rows.*.expanded', expandedChange, { bulk: true }));
|
|
}
|
|
function toggle() {
|
|
expanded = !expanded;
|
|
if (props.row) {
|
|
state.update(`config.list.rows.${props.row.id}.expanded`, expanded);
|
|
}
|
|
else {
|
|
state.update(`config.list.rows`, rows => {
|
|
for (const rowId in rows) {
|
|
rows[rowId].expanded = expanded;
|
|
}
|
|
return rows;
|
|
}, { only: ['*.expanded'] });
|
|
}
|
|
}
|
|
const getIcon = () => {
|
|
var _a, _b, _c;
|
|
if (iconChild) {
|
|
if (((_c = (_b = (_a = props.row) === null || _a === void 0 ? void 0 : _a._internal) === null || _b === void 0 ? void 0 : _b.children) === null || _c === void 0 ? void 0 : _c.length) === 0) {
|
|
return html `
|
|
<img width="16" height="16" class=${classNameChild} src=${iconChild} />
|
|
`;
|
|
}
|
|
return expanded
|
|
? html `
|
|
<img width="16" height="16" class=${classNameOpen} src=${iconOpen} />
|
|
`
|
|
: html `
|
|
<img width="16" height="16" class=${classNameClosed} src=${iconClosed} />
|
|
`;
|
|
}
|
|
return '';
|
|
};
|
|
const actions = Actions.create(componentActions, actionProps);
|
|
return templateProps => wrapper(html `
|
|
<div class=${className} data-action=${actions} @click=${toggle}>
|
|
${cache(getIcon())}
|
|
</div>
|
|
`, { vido, props, templateProps });
|
|
}
|
|
|
|
/**
|
|
* ListToggle component
|
|
*
|
|
* @copyright Rafal Pospiech <https://neuronet.io>
|
|
* @author Rafal Pospiech <neuronet.io@gmail.com>
|
|
* @package gantt-schedule-timeline-calendar
|
|
* @license AGPL-3.0 (https://github.com/neuronetio/gantt-schedule-timeline-calendar/blob/master/LICENSE)
|
|
* @link https://github.com/neuronetio/gantt-schedule-timeline-calendar
|
|
*/
|
|
function ListToggle(vido, props = {}) {
|
|
const { html, onDestroy, api, state, update } = vido;
|
|
const componentName = 'list-toggle';
|
|
let className;
|
|
onDestroy(state.subscribe('config.classNames', classNames => {
|
|
className = api.getClass(componentName);
|
|
}));
|
|
let wrapper;
|
|
onDestroy(state.subscribe('config.wrappers.ListToggle', ListToggleWrapper => (wrapper = ListToggleWrapper)));
|
|
let toggleIconsSrc = {
|
|
open: '',
|
|
close: ''
|
|
};
|
|
onDestroy(state.subscribe('_internal.list.toggle.icons', value => {
|
|
if (value) {
|
|
toggleIconsSrc = value;
|
|
update();
|
|
}
|
|
}));
|
|
let open = true;
|
|
onDestroy(state.subscribe('config.list.columns.percent', percent => (percent === 0 ? (open = false) : (open = true))));
|
|
function toggle(ev) {
|
|
state.update('config.list.columns.percent', percent => {
|
|
return percent === 0 ? 100 : 0;
|
|
});
|
|
}
|
|
return templateProps => wrapper(html `
|
|
<div class=${className} @click=${toggle}><img src=${open ? toggleIconsSrc.close : toggleIconsSrc.open} /></div>
|
|
`, { props, vido, templateProps });
|
|
}
|
|
|
|
/**
|
|
* Chart component
|
|
*
|
|
* @copyright Rafal Pospiech <https://neuronet.io>
|
|
* @author Rafal Pospiech <neuronet.io@gmail.com>
|
|
* @package gantt-schedule-timeline-calendar
|
|
* @license AGPL-3.0 (https://github.com/neuronetio/gantt-schedule-timeline-calendar/blob/master/LICENSE)
|
|
* @link https://github.com/neuronetio/gantt-schedule-timeline-calendar
|
|
*/
|
|
function Chart(vido, props = {}) {
|
|
const { api, state, onDestroy, Actions, update, html, StyleMap, createComponent } = vido;
|
|
const componentName = 'chart';
|
|
const ChartCalendarComponent = state.get('config.components.ChartCalendar');
|
|
const ChartTimelineComponent = state.get('config.components.ChartTimeline');
|
|
let wrapper;
|
|
onDestroy(state.subscribe('config.wrappers.Chart', value => (wrapper = value)));
|
|
const Calendar = createComponent(ChartCalendarComponent);
|
|
onDestroy(Calendar.destroy);
|
|
const Timeline = createComponent(ChartTimelineComponent);
|
|
onDestroy(Timeline.destroy);
|
|
let className, classNameScroll, classNameScrollInner, scrollElement, scrollInnerElement;
|
|
const componentActions = api.getActions(componentName);
|
|
onDestroy(state.subscribe('config.classNames', value => {
|
|
className = api.getClass(componentName);
|
|
classNameScroll = api.getClass('horizontal-scroll');
|
|
classNameScrollInner = api.getClass('horizontal-scroll-inner');
|
|
update();
|
|
}));
|
|
onDestroy(state.subscribeAll(['_internal.chart.dimensions.width', '_internal.chart.time.totalViewDurationPx'], function horizontalScroll() {
|
|
if (scrollElement)
|
|
scrollElement.style.width = state.get('_internal.chart.dimensions.width') + 'px';
|
|
if (scrollInnerElement)
|
|
scrollInnerElement.style.width = state.get('_internal.chart.time.totalViewDurationPx') + 'px';
|
|
}));
|
|
onDestroy(state.subscribe('config.scroll.left', left => {
|
|
if (scrollElement) {
|
|
scrollElement.scrollLeft = left;
|
|
}
|
|
}));
|
|
function onScrollHandler(event) {
|
|
if (event.type === 'scroll') {
|
|
// @ts-ignore
|
|
const left = event.target.scrollLeft;
|
|
state.update('config.scroll.left', left);
|
|
}
|
|
}
|
|
const onScroll = {
|
|
handleEvent: onScrollHandler,
|
|
passive: true,
|
|
capture: false
|
|
};
|
|
function onWheelHandler(event) {
|
|
if (event.type === 'wheel') {
|
|
const wheel = api.normalizeMouseWheelEvent(event);
|
|
const xMultiplier = state.get('config.scroll.xMultiplier');
|
|
const yMultiplier = state.get('config.scroll.yMultiplier');
|
|
const currentScrollLeft = state.get('config.scroll.left');
|
|
const totalViewDurationPx = state.get('_internal.chart.time.totalViewDurationPx');
|
|
if (event.shiftKey && wheel.y) {
|
|
const newScrollLeft = api.limitScrollLeft(totalViewDurationPx, chartWidth, currentScrollLeft + wheel.y * xMultiplier);
|
|
state.update('config.scroll.left', newScrollLeft); // will trigger scrollbar to move which will trigger scroll event
|
|
}
|
|
else if (event.ctrlKey && wheel.y) {
|
|
event.preventDefault();
|
|
state.update('config.chart.time.zoom', currentZoom => {
|
|
if (wheel.y < 0) {
|
|
return currentZoom - 1;
|
|
}
|
|
return currentZoom + 1;
|
|
});
|
|
}
|
|
else if (wheel.x) {
|
|
const currentScrollLeft = state.get('config.scroll.left');
|
|
state.update('config.scroll.left', api.limitScrollLeft(totalViewDurationPx, chartWidth, currentScrollLeft + wheel.x * xMultiplier));
|
|
}
|
|
else {
|
|
state.update('config.scroll.top', top => {
|
|
const rowsHeight = state.get('_internal.list.rowsHeight');
|
|
const internalHeight = state.get('_internal.height');
|
|
return api.limitScrollTop(rowsHeight, internalHeight, (top += wheel.y * yMultiplier));
|
|
});
|
|
}
|
|
}
|
|
}
|
|
const onWheel = {
|
|
handleEvent: onWheelHandler,
|
|
passive: false,
|
|
capture: false
|
|
};
|
|
function bindElement(element) {
|
|
if (!scrollElement) {
|
|
scrollElement = element;
|
|
state.update('_internal.elements.horizontal-scroll', element);
|
|
}
|
|
}
|
|
function bindInnerScroll(element) {
|
|
scrollInnerElement = element;
|
|
const old = state.get('_internal.elements.horizontal-scroll-inner');
|
|
if (old !== element)
|
|
state.update('_internal.elements.horizontal-scroll-inner', element);
|
|
if (!state.get('_internal.loaded.horizontal-scroll-inner'))
|
|
state.update('_internal.loaded.horizontal-scroll-inner', true);
|
|
}
|
|
let chartWidth = 0;
|
|
let ro;
|
|
componentActions.push(function bindElement(element) {
|
|
if (!ro) {
|
|
ro = new index((entries, observer) => {
|
|
const width = element.clientWidth;
|
|
const height = element.clientHeight;
|
|
const innerWidth = width - state.get('_internal.scrollBarHeight');
|
|
if (chartWidth !== width) {
|
|
chartWidth = width;
|
|
state.update('_internal.chart.dimensions', { width, innerWidth, height });
|
|
}
|
|
});
|
|
ro.observe(element);
|
|
state.update('_internal.elements.chart', element);
|
|
state.update('_internal.loaded.chart', true);
|
|
}
|
|
});
|
|
onDestroy(() => {
|
|
ro.disconnect();
|
|
});
|
|
const actions = Actions.create(componentActions, { api, state });
|
|
const scrollActions = Actions.create([bindElement]);
|
|
const scrollAreaActions = Actions.create([bindInnerScroll]);
|
|
return templateProps => wrapper(html `
|
|
<div class=${className} data-actions=${actions} @wheel=${onWheel} @scroll=${onScroll}>
|
|
${Calendar.html()}${Timeline.html()}
|
|
<div class=${classNameScroll} data-actions=${scrollActions} @scroll=${onScroll}>
|
|
<div class=${classNameScrollInner} style="height: 1px" data-actions=${scrollAreaActions} />
|
|
</div>
|
|
</div>
|
|
`, { vido, props: {}, templateProps });
|
|
}
|
|
|
|
/**
|
|
* ChartCalendar component
|
|
*
|
|
* @copyright Rafal Pospiech <https://neuronet.io>
|
|
* @author Rafal Pospiech <neuronet.io@gmail.com>
|
|
* @package gantt-schedule-timeline-calendar
|
|
* @license AGPL-3.0 (https://github.com/neuronetio/gantt-schedule-timeline-calendar/blob/master/LICENSE)
|
|
* @link https://github.com/neuronetio/gantt-schedule-timeline-calendar
|
|
*/
|
|
function ChartCalendar(vido, props) {
|
|
const { api, state, onDestroy, Actions, update, reuseComponents, html, StyleMap } = vido;
|
|
const componentName = 'chart-calendar';
|
|
const componentActions = api.getActions(componentName);
|
|
const actionProps = Object.assign(Object.assign({}, props), { api, state });
|
|
const ChartCalendarDateComponent = state.get('config.components.ChartCalendarDate');
|
|
let wrapper;
|
|
onDestroy(state.subscribe('config.wrappers.ChartCalendar', value => (wrapper = value)));
|
|
let className;
|
|
onDestroy(state.subscribe('config.classNames', value => {
|
|
className = api.getClass(componentName);
|
|
update();
|
|
}));
|
|
let headerHeight;
|
|
const styleMap = new StyleMap({ height: '', '--headerHeight': '', 'margin-left': '' });
|
|
onDestroy(state.subscribe('config.headerHeight', value => {
|
|
headerHeight = value;
|
|
styleMap.style['height'] = headerHeight + 'px';
|
|
styleMap.style['--calendar-height'] = headerHeight + 'px';
|
|
update();
|
|
}));
|
|
onDestroy(state.subscribe('config.scroll.compensation.x', compensation => {
|
|
styleMap.style['margin-left'] = -compensation + 'px';
|
|
update();
|
|
}));
|
|
const components = [[], []];
|
|
onDestroy(state.subscribe(`_internal.chart.time.levels`, levels => {
|
|
let level = 0;
|
|
for (const dates of levels) {
|
|
if (!dates.length)
|
|
continue;
|
|
let currentDateFormat = 'YYYY-MM-DD HH'; // hour
|
|
switch (dates[0].period) {
|
|
case 'day':
|
|
currentDateFormat = 'YYYY-MM-DD';
|
|
break;
|
|
case 'week':
|
|
currentDateFormat = 'YYYY-MM-ww';
|
|
break;
|
|
case 'month':
|
|
currentDateFormat = 'YYYY-MM';
|
|
break;
|
|
case 'year':
|
|
currentDateFormat = 'YYYY';
|
|
break;
|
|
}
|
|
const currentDate = api.time.date().format(currentDateFormat);
|
|
reuseComponents(components[level], dates, date => date && { level, date, currentDate, currentDateFormat }, ChartCalendarDateComponent);
|
|
level++;
|
|
}
|
|
update();
|
|
}));
|
|
onDestroy(() => {
|
|
components.forEach(level => level.forEach(component => component.destroy()));
|
|
});
|
|
componentActions.push(element => {
|
|
state.update('_internal.elements.chart-calendar', element);
|
|
});
|
|
const actions = Actions.create(componentActions, actionProps);
|
|
return templateProps => wrapper(html `
|
|
<div class=${className} data-actions=${actions} style=${styleMap}>
|
|
${components.map((components, level) => html `
|
|
<div class=${className + '-dates ' + className + `-dates--level-${level}`}>
|
|
${components.map(m => m.html())}
|
|
</div>
|
|
`)}
|
|
</div>
|
|
`, { props, vido, templateProps });
|
|
}
|
|
|
|
class Action$1 {
|
|
constructor() {
|
|
this.isAction = true;
|
|
}
|
|
}
|
|
Action$1.prototype.isAction = true;
|
|
|
|
/**
|
|
* ChartCalendarDay component
|
|
*
|
|
* @copyright Rafal Pospiech <https://neuronet.io>
|
|
* @author Rafal Pospiech <neuronet.io@gmail.com>
|
|
* @package gantt-schedule-timeline-calendar
|
|
* @license AGPL-3.0 (https://github.com/neuronetio/gantt-schedule-timeline-calendar/blob/master/LICENSE)
|
|
* @link https://github.com/neuronetio/gantt-schedule-timeline-calendar
|
|
*/
|
|
/**
|
|
* Save element
|
|
* @param {HTMLElement} element
|
|
* @param {object} data
|
|
*/
|
|
class BindElementAction$2 extends Action$1 {
|
|
constructor(element, data) {
|
|
super();
|
|
data.state.update('_internal.elements.chart-calendar-dates', elements => {
|
|
if (typeof elements === 'undefined') {
|
|
elements = [];
|
|
}
|
|
if (!elements.includes(element)) {
|
|
elements.push(element);
|
|
}
|
|
return elements;
|
|
});
|
|
}
|
|
}
|
|
function ChartCalendarDay(vido, props) {
|
|
const { api, state, onDestroy, Actions, update, onChange, html, StyleMap, Detach } = vido;
|
|
const componentName = 'chart-calendar-date';
|
|
const componentActions = api.getActions(componentName);
|
|
let wrapper;
|
|
onDestroy(state.subscribe('config.wrappers.ChartCalendarDate', value => (wrapper = value)));
|
|
let className;
|
|
onDestroy(state.subscribe('config.classNames', () => {
|
|
className = api.getClass(componentName, props);
|
|
}));
|
|
let current = '';
|
|
let time, htmlFormatted;
|
|
const styleMap = new StyleMap({ width: '', visibility: 'visible' }), scrollStyleMap = new StyleMap({
|
|
overflow: 'hidden',
|
|
'text-align': 'left'
|
|
});
|
|
let formatClassName = '';
|
|
function updateDate() {
|
|
if (!props)
|
|
return;
|
|
const cache = state.get('_internal.cache.calendar');
|
|
const level = state.get(`config.chart.calendar.levels.${props.level}`);
|
|
styleMap.style.width = props.date.width + 'px';
|
|
styleMap.style.visibility = 'visible';
|
|
scrollStyleMap.style = { overflow: 'hidden', 'text-align': 'left', 'margin-left': props.date.subPx + 8 + 'px' };
|
|
time = state.get('_internal.chart.time');
|
|
const cacheKey = `${new Date(props.date.leftGlobal).toISOString()}-${props.date.period}-${props.level}-${time.zoom}`;
|
|
if (!cache[cacheKey]) {
|
|
cache[cacheKey] = {};
|
|
}
|
|
let timeStart, timeEnd;
|
|
{
|
|
timeStart = api.time.date(props.date.leftGlobal);
|
|
timeEnd = api.time.date(props.date.rightGlobal);
|
|
cache[cacheKey].timeStart = timeStart;
|
|
cache[cacheKey].timeEnd = timeEnd;
|
|
}
|
|
const formats = level.formats;
|
|
const formatting = formats.find(formatting => +time.zoom <= +formatting.zoomTo);
|
|
let format;
|
|
{
|
|
format = formatting ? formatting.format({ timeStart, timeEnd, className, vido, props }) : null;
|
|
cache[cacheKey].format = format;
|
|
}
|
|
{
|
|
if (timeStart.format(props.currentDateFormat) === props.currentDate) {
|
|
current = ' gstc-current';
|
|
}
|
|
else if (timeStart.subtract(1, props.date.period).format(props.currentDateFormat) === props.currentDate) {
|
|
current = ' gstc-next';
|
|
}
|
|
else if (timeStart.add(1, props.date.period).format(props.currentDateFormat) === props.currentDate) {
|
|
current = ' gstc-previous';
|
|
}
|
|
else {
|
|
current = '';
|
|
}
|
|
cache[cacheKey].current = current;
|
|
}
|
|
let finalClassName = className + '-content ' + className + `-content--${props.date.period}` + current;
|
|
if (formatting.className) {
|
|
finalClassName += ' ' + formatting.className;
|
|
formatClassName = ' ' + formatting.className;
|
|
}
|
|
else {
|
|
formatClassName = '';
|
|
}
|
|
// updating cache state is not necessary because it is object and nobody listen to cache
|
|
htmlFormatted = html `
|
|
<div class=${finalClassName}>
|
|
${format}
|
|
</div>
|
|
`;
|
|
update();
|
|
}
|
|
let shouldDetach = false;
|
|
const detach = new Detach(() => shouldDetach);
|
|
let timeSub;
|
|
const actionProps = { date: props.date, period: props.period, api, state };
|
|
onChange((changedProps, options) => {
|
|
if (options.leave) {
|
|
shouldDetach = true;
|
|
return update();
|
|
}
|
|
shouldDetach = false;
|
|
props = changedProps;
|
|
actionProps.date = props.date;
|
|
actionProps.period = props.period;
|
|
if (timeSub) {
|
|
timeSub();
|
|
}
|
|
timeSub = state.subscribeAll(['_internal.chart.time', 'config.chart.calendar.levels'], updateDate, {
|
|
bulk: true
|
|
});
|
|
});
|
|
onDestroy(() => {
|
|
timeSub();
|
|
});
|
|
if (!componentActions.includes(BindElementAction$2))
|
|
componentActions.push(BindElementAction$2);
|
|
const actions = Actions.create(componentActions, actionProps);
|
|
return templateProps => wrapper(html `
|
|
<div
|
|
detach=${detach}
|
|
class=${className +
|
|
' ' +
|
|
className +
|
|
`--${props.date.period}` +
|
|
' ' +
|
|
className +
|
|
`--level-${props.level}` +
|
|
current +
|
|
formatClassName}
|
|
style=${styleMap}
|
|
data-actions=${actions}
|
|
>
|
|
${htmlFormatted}
|
|
</div>
|
|
`, { props, vido, templateProps });
|
|
}
|
|
|
|
/**
|
|
* ChartTimeline component
|
|
*
|
|
* @copyright Rafal Pospiech <https://neuronet.io>
|
|
* @author Rafal Pospiech <neuronet.io@gmail.com>
|
|
* @package gantt-schedule-timeline-calendar
|
|
* @license AGPL-3.0 (https://github.com/neuronetio/gantt-schedule-timeline-calendar/blob/master/LICENSE)
|
|
* @link https://github.com/neuronetio/gantt-schedule-timeline-calendar
|
|
*/
|
|
function ChartTimeline(vido, props) {
|
|
const { api, state, onDestroy, Action, Actions, update, html, createComponent, StyleMap } = vido;
|
|
const componentName = 'chart-timeline';
|
|
const componentActions = api.getActions(componentName);
|
|
const actionProps = Object.assign(Object.assign({}, props), { api, state });
|
|
let wrapper;
|
|
onDestroy(state.subscribe('config.wrappers.ChartTimeline', value => (wrapper = value)));
|
|
const GridComponent = state.get('config.components.ChartTimelineGrid');
|
|
const ItemsComponent = state.get('config.components.ChartTimelineItems');
|
|
const ListToggleComponent = state.get('config.components.ListToggle');
|
|
const Grid = createComponent(GridComponent);
|
|
onDestroy(Grid.destroy);
|
|
const Items = createComponent(ItemsComponent);
|
|
onDestroy(Items.destroy);
|
|
const ListToggle = createComponent(ListToggleComponent);
|
|
onDestroy(ListToggle.destroy);
|
|
let className, classNameInner;
|
|
onDestroy(state.subscribe('config.classNames', () => {
|
|
className = api.getClass(componentName);
|
|
classNameInner = api.getClass(componentName + '-inner');
|
|
update();
|
|
}));
|
|
let showToggle;
|
|
onDestroy(state.subscribe('config.list.toggle.display', val => (showToggle = val)));
|
|
const styleMap = new StyleMap({}), innerStyleMap = new StyleMap({});
|
|
function calculateStyle() {
|
|
const xCompensation = api.getCompensationX();
|
|
const yCompensation = api.getCompensationY();
|
|
const width = state.get('_internal.chart.dimensions.width');
|
|
const height = state.get('_internal.list.rowsHeight');
|
|
styleMap.style.height = state.get('_internal.height') + 'px';
|
|
styleMap.style['--height'] = styleMap.style.height;
|
|
styleMap.style['--negative-compensation-x'] = xCompensation + 'px';
|
|
styleMap.style['--compensation-x'] = Math.round(Math.abs(xCompensation)) + 'px';
|
|
styleMap.style['--negative-compensation-y'] = yCompensation + 'px';
|
|
styleMap.style['--compensation-y'] = Math.abs(yCompensation) + 'px';
|
|
if (width) {
|
|
styleMap.style.width = width + 'px';
|
|
styleMap.style['--width'] = width + 'px';
|
|
}
|
|
else {
|
|
styleMap.style.width = '0px';
|
|
styleMap.style['--width'] = '0px';
|
|
}
|
|
innerStyleMap.style.height = height + 'px';
|
|
if (width) {
|
|
innerStyleMap.style.width = width + xCompensation + 'px';
|
|
}
|
|
else {
|
|
innerStyleMap.style.width = '0px';
|
|
}
|
|
//innerStyleMap.style.transform = `translate(-${xCompensation}px, ${yCompensation}px)`;
|
|
innerStyleMap.style['margin-left'] = -xCompensation + 'px';
|
|
update();
|
|
}
|
|
onDestroy(state.subscribeAll([
|
|
'_internal.height',
|
|
'_internal.chart.dimensions.width',
|
|
'_internal.list.rowsHeight',
|
|
'config.scroll.compensation',
|
|
'_internal.chart.time.dates.day'
|
|
], calculateStyle));
|
|
componentActions.push(class BindElementAction extends Action {
|
|
constructor(element) {
|
|
super();
|
|
const old = state.get('_internal.elements.chart-timeline');
|
|
if (old !== element)
|
|
state.update('_internal.elements.chart-timeline', element);
|
|
}
|
|
});
|
|
const actions = Actions.create(componentActions, actionProps);
|
|
return templateProps => wrapper(html `
|
|
<div class=${className} style=${styleMap} data-actions=${actions} @wheel=${api.onScroll}>
|
|
<div class=${classNameInner} style=${innerStyleMap}>
|
|
${Grid.html()}${Items.html()}${showToggle ? ListToggle.html() : ''}
|
|
</div>
|
|
</div>
|
|
`, { props, vido, templateProps });
|
|
}
|
|
|
|
/**
|
|
* ChartTimelineGrid component
|
|
*
|
|
* @copyright Rafal Pospiech <https://neuronet.io>
|
|
* @author Rafal Pospiech <neuronet.io@gmail.com>
|
|
* @package gantt-schedule-timeline-calendar
|
|
* @license AGPL-3.0 (https://github.com/neuronetio/gantt-schedule-timeline-calendar/blob/master/LICENSE)
|
|
* @link https://github.com/neuronetio/gantt-schedule-timeline-calendar
|
|
*/
|
|
/**
|
|
* Bind element action
|
|
*/
|
|
class BindElementAction$3 {
|
|
constructor(element, data) {
|
|
const old = data.state.get('_internal.elements.chart-timeline-grid');
|
|
if (old !== element)
|
|
data.state.update('_internal.elements.chart-timeline-grid', element);
|
|
}
|
|
destroy(element, data) {
|
|
data.state.update('_internal.elements', elements => {
|
|
delete elements['chart-timeline-grid'];
|
|
return elements;
|
|
});
|
|
}
|
|
}
|
|
function ChartTimelineGrid(vido, props) {
|
|
const { api, state, onDestroy, Actions, update, html, reuseComponents, StyleMap } = vido;
|
|
const componentName = 'chart-timeline-grid';
|
|
const componentActions = api.getActions(componentName);
|
|
const actionProps = { api, state };
|
|
let wrapper;
|
|
onDestroy(state.subscribe('config.wrappers.ChartTimelineGrid', value => (wrapper = value)));
|
|
const GridRowComponent = state.get('config.components.ChartTimelineGridRow');
|
|
let className;
|
|
onDestroy(state.subscribe('config.classNames', () => {
|
|
className = api.getClass(componentName);
|
|
update();
|
|
}));
|
|
let onBlockCreate;
|
|
onDestroy(state.subscribe('config.chart.grid.block.onCreate', onCreate => (onBlockCreate = onCreate)));
|
|
const rowsComponents = [];
|
|
const rowsWithBlocks = [];
|
|
const formatCache = new Map();
|
|
const styleMap = new StyleMap({});
|
|
/**
|
|
* Generate blocks
|
|
*/
|
|
function generateBlocks() {
|
|
const width = state.get('_internal.chart.dimensions.width');
|
|
const height = state.get('_internal.height');
|
|
const time = state.get('_internal.chart.time');
|
|
const periodDates = state.get(`_internal.chart.time.levels.${time.level}`);
|
|
if (!periodDates || periodDates.length === 0) {
|
|
state.update('_internal.chart.grid.rowsWithBlocks', []);
|
|
return;
|
|
}
|
|
const visibleRows = state.get('_internal.list.visibleRows');
|
|
const xCompensation = api.getCompensationX();
|
|
const yCompensation = api.getCompensationY();
|
|
styleMap.style.height = height + Math.abs(yCompensation) + 'px';
|
|
styleMap.style.width = width + xCompensation + 'px';
|
|
let top = 0;
|
|
rowsWithBlocks.length = 0;
|
|
for (const row of visibleRows) {
|
|
const blocks = [];
|
|
for (const time of periodDates) {
|
|
let format;
|
|
if (formatCache.has(time.leftGlobal)) {
|
|
format = formatCache.get(time.leftGlobal);
|
|
}
|
|
else {
|
|
format = api.time.date(time.leftGlobal).format('YYYY-MM-DD HH:mm');
|
|
formatCache.set(time.leftGlobal, format);
|
|
}
|
|
const id = row.id + ':' + format;
|
|
let block = { id, time, row, top };
|
|
for (const onCreate of onBlockCreate) {
|
|
block = onCreate(block);
|
|
}
|
|
blocks.push(block);
|
|
}
|
|
rowsWithBlocks.push({ row, blocks, top, width });
|
|
top += row.height;
|
|
}
|
|
state.update('_internal.chart.grid.rowsWithBlocks', rowsWithBlocks);
|
|
}
|
|
onDestroy(state.subscribeAll([
|
|
'_internal.list.visibleRows;',
|
|
`_internal.chart.time.levels`,
|
|
'_internal.height',
|
|
'_internal.chart.dimensions.width'
|
|
], generateBlocks, {
|
|
bulk: true
|
|
}));
|
|
/**
|
|
* Generate rows components
|
|
* @param {array} rowsWithBlocks
|
|
*/
|
|
function generateRowsComponents(rowsWithBlocks) {
|
|
reuseComponents(rowsComponents, rowsWithBlocks || [], row => row, GridRowComponent);
|
|
update();
|
|
}
|
|
onDestroy(state.subscribe('_internal.chart.grid.rowsWithBlocks', generateRowsComponents));
|
|
onDestroy(() => {
|
|
rowsComponents.forEach(row => row.destroy());
|
|
});
|
|
componentActions.push(BindElementAction$3);
|
|
const actions = Actions.create(componentActions, actionProps);
|
|
return templateProps => wrapper(html `
|
|
<div class=${className} data-actions=${actions} style=${styleMap}>
|
|
${rowsComponents.map(r => r.html())}
|
|
</div>
|
|
`, { props, vido, templateProps });
|
|
}
|
|
|
|
/**
|
|
* ChartTimelineGridRow component
|
|
*
|
|
* @copyright Rafal Pospiech <https://neuronet.io>
|
|
* @author Rafal Pospiech <neuronet.io@gmail.com>
|
|
* @package gantt-schedule-timeline-calendar
|
|
* @license AGPL-3.0 (https://github.com/neuronetio/gantt-schedule-timeline-calendar/blob/master/LICENSE)
|
|
* @link https://github.com/neuronetio/gantt-schedule-timeline-calendar
|
|
*/
|
|
/**
|
|
* Bind element action
|
|
*/
|
|
class BindElementAction$4 {
|
|
constructor(element, data) {
|
|
let shouldUpdate = false;
|
|
let rows = data.state.get('_internal.elements.chart-timeline-grid-rows');
|
|
if (typeof rows === 'undefined') {
|
|
rows = [];
|
|
shouldUpdate = true;
|
|
}
|
|
if (!rows.includes(element)) {
|
|
rows.push(element);
|
|
shouldUpdate = true;
|
|
}
|
|
if (shouldUpdate)
|
|
data.state.update('_internal.elements.chart-timeline-grid-rows', rows, { only: null });
|
|
}
|
|
destroy(element, data) {
|
|
data.state.update('_internal.elements.chart-timeline-grid-rows', rows => {
|
|
return rows.filter(el => el !== element);
|
|
});
|
|
}
|
|
}
|
|
function ChartTimelineGridRow(vido, props) {
|
|
const { api, state, onDestroy, Detach, Actions, update, html, reuseComponents, onChange, StyleMap } = vido;
|
|
const componentName = 'chart-timeline-grid-row';
|
|
const actionProps = Object.assign(Object.assign({}, props), { api,
|
|
state });
|
|
let wrapper;
|
|
onDestroy(state.subscribe('config.wrappers.ChartTimelineGridRow', value => {
|
|
wrapper = value;
|
|
update();
|
|
}));
|
|
const GridBlockComponent = state.get('config.components.ChartTimelineGridRowBlock');
|
|
const componentActions = api.getActions(componentName);
|
|
let className;
|
|
onDestroy(state.subscribe('config.classNames', () => {
|
|
className = api.getClass(componentName);
|
|
}));
|
|
const styleMap = new StyleMap({
|
|
width: props.width + 'px',
|
|
height: props.row.height + 'px',
|
|
overflow: 'hidden'
|
|
}, true);
|
|
let shouldDetach = false;
|
|
const detach = new Detach(() => shouldDetach);
|
|
const rowsBlocksComponents = [];
|
|
onChange(function onPropsChange(changedProps, options) {
|
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
if (options.leave || changedProps.row === undefined) {
|
|
shouldDetach = true;
|
|
reuseComponents(rowsBlocksComponents, [], block => block, GridBlockComponent);
|
|
update();
|
|
return;
|
|
}
|
|
shouldDetach = false;
|
|
props = changedProps;
|
|
reuseComponents(rowsBlocksComponents, props.blocks, block => block, GridBlockComponent);
|
|
styleMap.setStyle({});
|
|
styleMap.style.height = props.row.height + 'px';
|
|
styleMap.style.width = props.width + 'px';
|
|
const rows = state.get('config.list.rows');
|
|
for (const parentId of props.row._internal.parents) {
|
|
const parent = rows[parentId];
|
|
const childrenStyle = (_d = (_c = (_b = (_a = parent) === null || _a === void 0 ? void 0 : _a.style) === null || _b === void 0 ? void 0 : _b.grid) === null || _c === void 0 ? void 0 : _c.row) === null || _d === void 0 ? void 0 : _d.children;
|
|
if (childrenStyle)
|
|
for (const name in childrenStyle) {
|
|
styleMap.style[name] = childrenStyle[name];
|
|
}
|
|
}
|
|
const currentStyle = (_j = (_h = (_g = (_f = (_e = props) === null || _e === void 0 ? void 0 : _e.row) === null || _f === void 0 ? void 0 : _f.style) === null || _g === void 0 ? void 0 : _g.grid) === null || _h === void 0 ? void 0 : _h.row) === null || _j === void 0 ? void 0 : _j.current;
|
|
if (currentStyle)
|
|
for (const name in currentStyle) {
|
|
styleMap.style[name] = currentStyle[name];
|
|
}
|
|
for (const prop in props) {
|
|
actionProps[prop] = props[prop];
|
|
}
|
|
update();
|
|
});
|
|
onDestroy(function destroy() {
|
|
rowsBlocksComponents.forEach(rowBlock => rowBlock.destroy());
|
|
});
|
|
if (componentActions.indexOf(BindElementAction$4) === -1) {
|
|
componentActions.push(BindElementAction$4);
|
|
}
|
|
const actions = Actions.create(componentActions, actionProps);
|
|
return templateProps => {
|
|
return wrapper(html `
|
|
<div detach=${detach} class=${className} data-actions=${actions} style=${styleMap}>
|
|
${rowsBlocksComponents.map(r => r.html())}
|
|
</div>
|
|
`, { vido, props, templateProps });
|
|
};
|
|
}
|
|
|
|
/**
|
|
* ChartTimelineGridRowBlock component
|
|
*
|
|
* @copyright Rafal Pospiech <https://neuronet.io>
|
|
* @author Rafal Pospiech <neuronet.io@gmail.com>
|
|
* @package gantt-schedule-timeline-calendar
|
|
* @license AGPL-3.0 (https://github.com/neuronetio/gantt-schedule-timeline-calendar/blob/master/LICENSE)
|
|
* @link https://github.com/neuronetio/gantt-schedule-timeline-calendar
|
|
*/
|
|
/**
|
|
* Bind element action
|
|
* @param {Element} element
|
|
* @param {any} data
|
|
* @returns {object} with update and destroy
|
|
*/
|
|
class BindElementAction$5 {
|
|
constructor(element, data) {
|
|
let shouldUpdate = false;
|
|
let blocks = data.state.get('_internal.elements.chart-timeline-grid-row-blocks');
|
|
if (typeof blocks === 'undefined') {
|
|
blocks = [];
|
|
shouldUpdate = true;
|
|
}
|
|
if (!blocks.includes(element)) {
|
|
blocks.push(element);
|
|
shouldUpdate = true;
|
|
}
|
|
if (shouldUpdate)
|
|
data.state.update('_internal.elements.chart-timeline-grid-row-blocks', blocks, { only: null });
|
|
}
|
|
destroy(element, data) {
|
|
data.state.update('_internal.elements.chart-timeline-grid-row-blocks', blocks => {
|
|
return blocks.filter(el => el !== element);
|
|
}, { only: [''] });
|
|
}
|
|
}
|
|
const ChartTimelineGridRowBlock = (vido, props) => {
|
|
const { api, state, onDestroy, Detach, Actions, update, html, onChange, StyleMap } = vido;
|
|
const componentName = 'chart-timeline-grid-row-block';
|
|
const actionProps = Object.assign(Object.assign({}, props), { api,
|
|
state });
|
|
let shouldDetach = false;
|
|
const detach = new Detach(() => shouldDetach);
|
|
const componentActions = api.getActions(componentName);
|
|
let wrapper;
|
|
onDestroy(state.subscribe('config.wrappers.ChartTimelineGridRowBlock', value => {
|
|
wrapper = value;
|
|
update();
|
|
}));
|
|
let className;
|
|
function updateClassName(time) {
|
|
const currentTime = api.time
|
|
.date()
|
|
.startOf(time.period)
|
|
.valueOf();
|
|
className = api.getClass(componentName);
|
|
if (time.leftGlobal === currentTime) {
|
|
className += ' current';
|
|
}
|
|
}
|
|
updateClassName(props.time);
|
|
const styleMap = new StyleMap({ width: '', height: '' });
|
|
/**
|
|
* On props change
|
|
* @param {any} changedProps
|
|
*/
|
|
function onPropsChange(changedProps, options) {
|
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
if (options.leave || changedProps.row === undefined) {
|
|
shouldDetach = true;
|
|
return update();
|
|
}
|
|
shouldDetach = false;
|
|
props = changedProps;
|
|
for (const prop in props) {
|
|
actionProps[prop] = props[prop];
|
|
}
|
|
updateClassName(props.time);
|
|
styleMap.setStyle({});
|
|
styleMap.style.width = (((_b = (_a = props) === null || _a === void 0 ? void 0 : _a.time) === null || _b === void 0 ? void 0 : _b.width) || 0) + 'px';
|
|
styleMap.style.height = (((_d = (_c = props) === null || _c === void 0 ? void 0 : _c.row) === null || _d === void 0 ? void 0 : _d.height) || 0) + 'px';
|
|
const rows = state.get('config.list.rows');
|
|
for (const parentId of props.row._internal.parents) {
|
|
const parent = rows[parentId];
|
|
const childrenStyle = (_h = (_g = (_f = (_e = parent) === null || _e === void 0 ? void 0 : _e.style) === null || _f === void 0 ? void 0 : _f.grid) === null || _g === void 0 ? void 0 : _g.block) === null || _h === void 0 ? void 0 : _h.children;
|
|
if (childrenStyle)
|
|
styleMap.setStyle(Object.assign(Object.assign({}, styleMap.style), childrenStyle));
|
|
}
|
|
const currentStyle = (_o = (_m = (_l = (_k = (_j = props) === null || _j === void 0 ? void 0 : _j.row) === null || _k === void 0 ? void 0 : _k.style) === null || _l === void 0 ? void 0 : _l.grid) === null || _m === void 0 ? void 0 : _m.block) === null || _o === void 0 ? void 0 : _o.current;
|
|
if (currentStyle)
|
|
styleMap.setStyle(Object.assign(Object.assign({}, styleMap.style), currentStyle));
|
|
update();
|
|
}
|
|
onChange(onPropsChange);
|
|
componentActions.push(BindElementAction$5);
|
|
const actions = Actions.create(componentActions, actionProps);
|
|
return templateProps => {
|
|
return wrapper(html `
|
|
<div detach=${detach} class=${className} data-actions=${actions} style=${styleMap}></div>
|
|
`, { props, vido, templateProps });
|
|
};
|
|
};
|
|
|
|
/**
|
|
* ChartTimelineItems component
|
|
*
|
|
* @copyright Rafal Pospiech <https://neuronet.io>
|
|
* @author Rafal Pospiech <neuronet.io@gmail.com>
|
|
* @package gantt-schedule-timeline-calendar
|
|
* @license AGPL-3.0 (https://github.com/neuronetio/gantt-schedule-timeline-calendar/blob/master/LICENSE)
|
|
* @link https://github.com/neuronetio/gantt-schedule-timeline-calendar
|
|
*/
|
|
function ChartTimelineItems(vido, props = {}) {
|
|
const { api, state, onDestroy, Actions, update, html, reuseComponents, StyleMap } = vido;
|
|
const componentName = 'chart-timeline-items';
|
|
const componentActions = api.getActions(componentName);
|
|
let wrapper;
|
|
onDestroy(state.subscribe('config.wrappers.ChartTimelineItems', value => (wrapper = value)));
|
|
let ItemsRowComponent;
|
|
onDestroy(state.subscribe('config.components.ChartTimelineItemsRow', value => (ItemsRowComponent = value)));
|
|
let className;
|
|
onDestroy(state.subscribe('config.classNames', () => {
|
|
className = api.getClass(componentName);
|
|
update();
|
|
}));
|
|
const styleMap = new StyleMap({}, true);
|
|
function calculateStyle() {
|
|
const width = state.get('_internal.chart.dimensions.width');
|
|
const height = state.get('_internal.height');
|
|
const yCompensation = api.getCompensationY();
|
|
const xCompensation = api.getCompensationX();
|
|
styleMap.style.width = width + xCompensation + 'px';
|
|
styleMap.style.height = height + Math.abs(yCompensation) + 'px';
|
|
}
|
|
onDestroy(state.subscribeAll([
|
|
'_internal.height',
|
|
'_internal.chart.dimensions.width',
|
|
'config.scroll.compensation',
|
|
'_internal.chart.time.dates.day'
|
|
], calculateStyle));
|
|
const rowsComponents = [];
|
|
function createRowComponents() {
|
|
const visibleRows = state.get('_internal.list.visibleRows');
|
|
reuseComponents(rowsComponents, visibleRows || [], row => ({ row }), ItemsRowComponent);
|
|
update();
|
|
}
|
|
onDestroy(state.subscribeAll(['_internal.list.visibleRows;', 'config.chart.items'], createRowComponents));
|
|
onDestroy(() => {
|
|
rowsComponents.forEach(row => row.destroy());
|
|
});
|
|
const actions = Actions.create(componentActions, { api, state });
|
|
return templateProps => wrapper(html `
|
|
<div class=${className} style=${styleMap} data-actions=${actions}>
|
|
${rowsComponents.map(r => r.html())}
|
|
</div>
|
|
`, { props, vido, templateProps });
|
|
}
|
|
|
|
/**
|
|
* ChartTimelineItemsRow component
|
|
*
|
|
* @copyright Rafal Pospiech <https://neuronet.io>
|
|
* @author Rafal Pospiech <neuronet.io@gmail.com>
|
|
* @package gantt-schedule-timeline-calendar
|
|
* @license AGPL-3.0 (https://github.com/neuronetio/gantt-schedule-timeline-calendar/blob/master/LICENSE)
|
|
* @link https://github.com/neuronetio/gantt-schedule-timeline-calendar
|
|
*/
|
|
/**
|
|
* Bind element action
|
|
* @param {Element} element
|
|
* @param {any} data
|
|
*/
|
|
class BindElementAction$6 {
|
|
constructor(element, data) {
|
|
let shouldUpdate = false;
|
|
let rows = data.state.get('_internal.elements.chart-timeline-items-rows');
|
|
if (typeof rows === 'undefined') {
|
|
rows = [];
|
|
shouldUpdate = true;
|
|
}
|
|
if (!rows.includes(element)) {
|
|
rows.push(element);
|
|
shouldUpdate = true;
|
|
}
|
|
if (shouldUpdate)
|
|
data.state.update('_internal.elements.chart-timeline-items-rows', rows, { only: null });
|
|
}
|
|
destroy(element, data) {
|
|
data.state.update('_internal.elements.chart-timeline-items-rows', rows => {
|
|
return rows.filter(el => el !== element);
|
|
});
|
|
}
|
|
}
|
|
const ChartTimelineItemsRow = (vido, props) => {
|
|
const { api, state, onDestroy, Detach, Actions, update, html, onChange, reuseComponents, StyleMap } = vido;
|
|
const actionProps = Object.assign(Object.assign({}, props), { api, state });
|
|
let wrapper;
|
|
onDestroy(state.subscribe('config.wrappers.ChartTimelineItemsRow', value => (wrapper = value)));
|
|
let ItemComponent;
|
|
onDestroy(state.subscribe('config.components.ChartTimelineItemsRowItem', value => (ItemComponent = value)));
|
|
let itemsPath = `_internal.flatTreeMapById.${props.row.id}._internal.items`;
|
|
let rowSub, itemsSub;
|
|
const itemComponents = [], styleMap = new StyleMap({ width: '', height: '' }, true);
|
|
let shouldDetach = false;
|
|
const detach = new Detach(() => shouldDetach);
|
|
const updateDom = () => {
|
|
const chart = state.get('_internal.chart');
|
|
shouldDetach = false;
|
|
const xCompensation = api.getCompensationX();
|
|
styleMap.style.width = chart.dimensions.width + xCompensation + 'px';
|
|
if (!props) {
|
|
shouldDetach = true;
|
|
return;
|
|
}
|
|
styleMap.style.height = props.row.height + 'px';
|
|
styleMap.style['--row-height'] = props.row.height + 'px';
|
|
};
|
|
function updateRow(row) {
|
|
itemsPath = `_internal.flatTreeMapById.${row.id}._internal.items`;
|
|
if (typeof rowSub === 'function') {
|
|
rowSub();
|
|
}
|
|
if (typeof itemsSub === 'function') {
|
|
itemsSub();
|
|
}
|
|
rowSub = state.subscribe('_internal.chart', value => {
|
|
if (value === undefined) {
|
|
shouldDetach = true;
|
|
return update();
|
|
}
|
|
updateDom();
|
|
update();
|
|
});
|
|
itemsSub = state.subscribe(itemsPath, value => {
|
|
if (value === undefined) {
|
|
shouldDetach = true;
|
|
reuseComponents(itemComponents, [], item => ({ row, item }), ItemComponent);
|
|
return update();
|
|
}
|
|
reuseComponents(itemComponents, value, item => ({ row, item }), ItemComponent);
|
|
updateDom();
|
|
update();
|
|
});
|
|
}
|
|
/**
|
|
* On props change
|
|
* @param {any} changedProps
|
|
*/
|
|
onChange((changedProps, options) => {
|
|
if (options.leave || changedProps.row === undefined) {
|
|
shouldDetach = true;
|
|
return update();
|
|
}
|
|
props = changedProps;
|
|
for (const prop in props) {
|
|
actionProps[prop] = props[prop];
|
|
}
|
|
updateRow(props.row);
|
|
});
|
|
onDestroy(() => {
|
|
itemsSub();
|
|
rowSub();
|
|
itemComponents.forEach(item => item.destroy());
|
|
});
|
|
const componentName = 'chart-timeline-items-row';
|
|
const componentActions = api.getActions(componentName);
|
|
let className;
|
|
onDestroy(state.subscribe('config.classNames', () => {
|
|
className = api.getClass(componentName, props);
|
|
update();
|
|
}));
|
|
componentActions.push(BindElementAction$6);
|
|
const actions = Actions.create(componentActions, actionProps);
|
|
return templateProps => {
|
|
return wrapper(html `
|
|
<div detach=${detach} class=${className} data-actions=${actions} style=${styleMap}>
|
|
${itemComponents.map(i => i.html())}
|
|
</div>
|
|
`, { props, vido, templateProps });
|
|
};
|
|
};
|
|
|
|
/**
|
|
* ChartTimelineItemsRowItem component
|
|
*
|
|
* @copyright Rafal Pospiech <https://neuronet.io>
|
|
* @author Rafal Pospiech <neuronet.io@gmail.com>
|
|
* @package gantt-schedule-timeline-calendar
|
|
* @license AGPL-3.0 (https://github.com/neuronetio/gantt-schedule-timeline-calendar/blob/master/LICENSE)
|
|
* @link https://github.com/neuronetio/gantt-schedule-timeline-calendar
|
|
*/
|
|
/**
|
|
* Bind element action
|
|
*/
|
|
class BindElementAction$7 {
|
|
constructor(element, data) {
|
|
let shouldUpdate = false;
|
|
let items = data.state.get('_internal.elements.chart-timeline-items-row-items');
|
|
if (typeof items === 'undefined') {
|
|
items = [];
|
|
shouldUpdate = true;
|
|
}
|
|
if (!items.includes(element)) {
|
|
items.push(element);
|
|
shouldUpdate = true;
|
|
}
|
|
if (shouldUpdate)
|
|
data.state.update('_internal.elements.chart-timeline-items-row-items', items, { only: null });
|
|
}
|
|
destroy(element, data) {
|
|
data.state.update('_internal.elements.chart-timeline-items-row-items', items => {
|
|
return items.filter(el => el !== element);
|
|
});
|
|
}
|
|
}
|
|
function ChartTimelineItemsRowItem(vido, props) {
|
|
const { api, state, onDestroy, Detach, Actions, update, html, onChange, unsafeHTML, StyleMap } = vido;
|
|
let wrapper;
|
|
onDestroy(state.subscribe('config.wrappers.ChartTimelineItemsRowItem', value => (wrapper = value)));
|
|
let itemLeftPx = 0, itemWidthPx = 0, leave = false, cutLeft = false, cutRight = false;
|
|
const styleMap = new StyleMap({ width: '', height: '', left: '' }), leftCutStyleMap = new StyleMap({ 'margin-left': '0px' }), rightCutStyleMap = new StyleMap({ 'margin-right': '0px' }), actionProps = {
|
|
item: props.item,
|
|
row: props.row,
|
|
left: itemLeftPx,
|
|
width: itemWidthPx,
|
|
api,
|
|
state
|
|
};
|
|
let shouldDetach = false;
|
|
function updateItem() {
|
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
if (leave)
|
|
return;
|
|
const time = state.get('_internal.chart.time');
|
|
itemLeftPx = api.time.globalTimeToViewPixelOffset(props.item.time.start);
|
|
itemLeftPx = Math.round(itemLeftPx * 10) * 0.1;
|
|
itemWidthPx = (props.item.time.end - props.item.time.start) / time.timePerPixel;
|
|
itemWidthPx -= state.get('config.chart.spacing') || 0;
|
|
if (itemWidthPx) {
|
|
itemWidthPx = Math.round(itemWidthPx * 10) * 0.1;
|
|
}
|
|
if (props.item.time.start < time.leftGlobal) {
|
|
leftCutStyleMap.style['margin-left'] = (time.leftGlobal - props.item.time.start) / time.timePerPixel + 'px';
|
|
cutLeft = true;
|
|
}
|
|
else {
|
|
leftCutStyleMap.style['margin-left'] = '0px';
|
|
cutLeft = false;
|
|
}
|
|
if (props.item.time.end > time.rightGlobal) {
|
|
rightCutStyleMap.style['margin-right'] = (props.item.time.end - time.rightGlobal) / time.timePerPixel + 'px';
|
|
cutRight = true;
|
|
}
|
|
else {
|
|
cutRight = false;
|
|
rightCutStyleMap.style['margin-right'] = '0px';
|
|
}
|
|
const oldWidth = styleMap.style.width;
|
|
const oldLeft = styleMap.style.left;
|
|
const xCompensation = api.getCompensationX();
|
|
styleMap.setStyle({});
|
|
const inViewPort = api.isItemInViewport(props.item, time.leftGlobal, time.rightGlobal);
|
|
shouldDetach = !inViewPort;
|
|
if (inViewPort) {
|
|
// update style only when visible to prevent browser's recalculate style
|
|
styleMap.style.width = itemWidthPx + 'px';
|
|
styleMap.style.left = itemLeftPx + xCompensation + 'px';
|
|
}
|
|
else {
|
|
styleMap.style.width = oldWidth;
|
|
styleMap.style.left = oldLeft;
|
|
}
|
|
const rows = state.get('config.list.rows');
|
|
for (const parentId of props.row._internal.parents) {
|
|
const parent = rows[parentId];
|
|
const childrenStyle = (_d = (_c = (_b = (_a = parent) === null || _a === void 0 ? void 0 : _a.style) === null || _b === void 0 ? void 0 : _b.items) === null || _c === void 0 ? void 0 : _c.item) === null || _d === void 0 ? void 0 : _d.children;
|
|
if (childrenStyle)
|
|
styleMap.setStyle(Object.assign(Object.assign({}, styleMap.style), childrenStyle));
|
|
}
|
|
const currentRowItemsStyle = (_j = (_h = (_g = (_f = (_e = props) === null || _e === void 0 ? void 0 : _e.row) === null || _f === void 0 ? void 0 : _f.style) === null || _g === void 0 ? void 0 : _g.items) === null || _h === void 0 ? void 0 : _h.item) === null || _j === void 0 ? void 0 : _j.current;
|
|
if (currentRowItemsStyle)
|
|
styleMap.setStyle(Object.assign(Object.assign({}, styleMap.style), currentRowItemsStyle));
|
|
const currentStyle = (_l = (_k = props) === null || _k === void 0 ? void 0 : _k.item) === null || _l === void 0 ? void 0 : _l.style;
|
|
if (currentStyle)
|
|
styleMap.setStyle(Object.assign(Object.assign({}, styleMap.style), currentStyle));
|
|
actionProps.left = itemLeftPx + xCompensation;
|
|
actionProps.width = itemWidthPx;
|
|
update();
|
|
}
|
|
const componentName = 'chart-timeline-items-row-item';
|
|
const cutterName = api.getClass(componentName) + '-cut';
|
|
const cutterLeft = html `
|
|
<div class=${cutterName} style=${leftCutStyleMap}>
|
|
<svg xmlns="http://www.w3.org/2000/svg" height="16" viewBox="0 0 18 16" width="16">
|
|
<path fill-opacity="0.5" fill="#ffffff" d="m5,3l-5,5l5,5l0,-10z" />
|
|
</svg>
|
|
</div>
|
|
`;
|
|
const cutterRight = html `
|
|
<div class=${cutterName} style=${rightCutStyleMap}>
|
|
<svg xmlns="http://www.w3.org/2000/svg" height="16" viewBox="0 0 4 16" width="16">
|
|
<path transform="rotate(-180 2.5,8) " fill-opacity="0.5" fill="#ffffff" d="m5,3l-5,5l5,5l0,-10z" />
|
|
</svg>
|
|
</div>
|
|
`;
|
|
function onPropsChange(changedProps, options) {
|
|
if (options.leave || changedProps.row === undefined || changedProps.item === undefined) {
|
|
leave = true;
|
|
shouldDetach = true;
|
|
return update();
|
|
}
|
|
else {
|
|
shouldDetach = false;
|
|
leave = false;
|
|
}
|
|
props = changedProps;
|
|
actionProps.item = props.item;
|
|
actionProps.row = props.row;
|
|
updateItem();
|
|
}
|
|
onChange(onPropsChange);
|
|
const componentActions = api.getActions(componentName);
|
|
let className, labelClassName;
|
|
onDestroy(state.subscribe('config.classNames', () => {
|
|
className = api.getClass(componentName, props);
|
|
labelClassName = api.getClass(componentName + '-label', props);
|
|
update();
|
|
}));
|
|
onDestroy(state.subscribeAll(['_internal.chart.time', 'config.scroll.compensation.x'], updateItem));
|
|
componentActions.push(BindElementAction$7);
|
|
const actions = Actions.create(componentActions, actionProps);
|
|
const detach = new Detach(() => shouldDetach);
|
|
return templateProps => {
|
|
return wrapper(html `
|
|
<div detach=${detach} class=${className} data-actions=${actions} style=${styleMap}>
|
|
${cutLeft ? cutterLeft : ''}
|
|
<div class=${labelClassName}>
|
|
${props.item.isHtml ? unsafeHTML(props.item.label) : props.item.label}
|
|
</div>
|
|
${cutRight ? cutterRight : ''}
|
|
</div>
|
|
`, { vido, props, templateProps });
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Gantt-Schedule-Timeline-Calendar
|
|
*
|
|
* @copyright Rafal Pospiech <https://neuronet.io>
|
|
* @author Rafal Pospiech <neuronet.io@gmail.com>
|
|
* @package gantt-schedule-timeline-calendar
|
|
* @license AGPL-3.0
|
|
*/
|
|
const actionNames = [
|
|
'main',
|
|
'list',
|
|
'list-column',
|
|
'list-column-header',
|
|
'list-column-header-resizer',
|
|
'list-column-header-resizer-dots',
|
|
'list-column-row',
|
|
'list-column-row-expander',
|
|
'list-column-row-expander-toggle',
|
|
'list-toggle',
|
|
'chart',
|
|
'chart-calendar',
|
|
'chart-calendar-date',
|
|
'chart-timeline',
|
|
'chart-timeline-grid',
|
|
'chart-timeline-grid-row',
|
|
'chart-timeline-grid-row-block',
|
|
'chart-timeline-items',
|
|
'chart-timeline-items-row',
|
|
'chart-timeline-items-row-item'
|
|
];
|
|
function generateEmptyActions() {
|
|
const actions = {};
|
|
actionNames.forEach(name => (actions[name] = []));
|
|
return actions;
|
|
}
|
|
function generateEmptySlots() {
|
|
const slots = {};
|
|
actionNames.forEach(name => {
|
|
slots[name] = { before: [], after: [] };
|
|
});
|
|
return slots;
|
|
}
|
|
// default configuration
|
|
function defaultConfig() {
|
|
const actions = generateEmptyActions();
|
|
const slots = generateEmptySlots();
|
|
return {
|
|
plugins: [],
|
|
plugin: {},
|
|
height: 822,
|
|
headerHeight: 72,
|
|
components: {
|
|
Main,
|
|
List,
|
|
ListColumn,
|
|
ListColumnHeader,
|
|
ListColumnHeaderResizer,
|
|
ListColumnRow,
|
|
ListColumnRowExpander,
|
|
ListColumnRowExpanderToggle,
|
|
ListToggle,
|
|
Chart,
|
|
ChartCalendar,
|
|
ChartCalendarDate: ChartCalendarDay,
|
|
ChartTimeline,
|
|
ChartTimelineGrid,
|
|
ChartTimelineGridRow,
|
|
ChartTimelineGridRowBlock,
|
|
ChartTimelineItems,
|
|
ChartTimelineItemsRow,
|
|
ChartTimelineItemsRowItem
|
|
},
|
|
wrappers: {
|
|
Main(input) {
|
|
return input;
|
|
},
|
|
List(input) {
|
|
return input;
|
|
},
|
|
ListColumn(input) {
|
|
return input;
|
|
},
|
|
ListColumnHeader(input) {
|
|
return input;
|
|
},
|
|
ListColumnHeaderResizer(input) {
|
|
return input;
|
|
},
|
|
ListColumnRow(input) {
|
|
return input;
|
|
},
|
|
ListColumnRowExpander(input) {
|
|
return input;
|
|
},
|
|
ListColumnRowExpanderToggle(input) {
|
|
return input;
|
|
},
|
|
ListToggle(input) {
|
|
return input;
|
|
},
|
|
Chart(input) {
|
|
return input;
|
|
},
|
|
ChartCalendar(input) {
|
|
return input;
|
|
},
|
|
ChartCalendarDate(input) {
|
|
return input;
|
|
},
|
|
ChartTimeline(input) {
|
|
return input;
|
|
},
|
|
ChartTimelineGrid(input) {
|
|
return input;
|
|
},
|
|
ChartTimelineGridRow(input) {
|
|
return input;
|
|
},
|
|
ChartTimelineGridRowBlock(input) {
|
|
return input;
|
|
},
|
|
ChartTimelineItems(input) {
|
|
return input;
|
|
},
|
|
ChartTimelineItemsRow(input) {
|
|
return input;
|
|
},
|
|
ChartTimelineItemsRowItem(input) {
|
|
return input;
|
|
}
|
|
},
|
|
list: {
|
|
rows: {},
|
|
rowHeight: 40,
|
|
columns: {
|
|
percent: 100,
|
|
resizer: {
|
|
width: 10,
|
|
inRealTime: true,
|
|
dots: 6
|
|
},
|
|
minWidth: 50,
|
|
data: {}
|
|
},
|
|
expander: {
|
|
padding: 18,
|
|
size: 20,
|
|
icon: {
|
|
width: 16,
|
|
height: 16
|
|
},
|
|
icons: {
|
|
child: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><ellipse ry="4" rx="4" id="svg_1" cy="12" cx="12" fill="#000000B0"/></svg>',
|
|
open: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/><path fill="none" d="M0 0h24v24H0V0z"/></svg>',
|
|
closed: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"/><path fill="none" d="M0 0h24v24H0V0z"/></svg>'
|
|
}
|
|
},
|
|
toggle: {
|
|
display: true,
|
|
icons: {
|
|
open: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path stroke="null" d="m16.406954,16.012672l4.00393,-4.012673l-4.00393,-4.012673l1.232651,-1.232651l5.245324,5.245324l-5.245324,5.245324l-1.232651,-1.232651z"/><path stroke="null" d="m-0.343497,12.97734zm1.620144,0l11.341011,0l0,-1.954681l-11.341011,0l0,1.954681zm0,3.909362l11.341011,0l0,-1.954681l-11.341011,0l0,1.954681zm0,-9.773404l0,1.95468l11.341011,0l0,-1.95468l-11.341011,0z"/></svg>`,
|
|
close: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path transform="rotate(-180 4.392796516418457,12) " stroke="null" d="m1.153809,16.012672l4.00393,-4.012673l-4.00393,-4.012673l1.232651,-1.232651l5.245324,5.245324l-5.245324,5.245324l-1.232651,-1.232651z"/><path stroke="null" d="m9.773297,12.97734zm1.620144,0l11.341011,0l0,-1.954681l-11.341011,0l0,1.954681zm0,3.909362l11.341011,0l0,-1.954681l-11.341011,0l0,1.954681zm0,-9.773404l0,1.95468l11.341011,0l0,-1.95468l-11.341011,0z"/></svg>`
|
|
}
|
|
}
|
|
},
|
|
scroll: {
|
|
propagate: true,
|
|
smooth: false,
|
|
top: 0,
|
|
left: 0,
|
|
xMultiplier: 3,
|
|
yMultiplier: 3,
|
|
percent: {
|
|
top: 0,
|
|
left: 0
|
|
},
|
|
compensation: {
|
|
x: 0,
|
|
y: 0
|
|
}
|
|
},
|
|
chart: {
|
|
time: {
|
|
period: 'day',
|
|
from: 0,
|
|
to: 0,
|
|
finalFrom: 0,
|
|
finalTo: 0,
|
|
zoom: 21,
|
|
leftGlobal: 0,
|
|
centerGlobal: 0,
|
|
rightGlobal: 0,
|
|
levels: [],
|
|
calculatedZoomMode: false
|
|
},
|
|
calendar: {
|
|
expand: true,
|
|
levels: [
|
|
{
|
|
formats: [
|
|
{
|
|
zoomTo: 17,
|
|
period: 'day',
|
|
className: 'gstc-date-medium gstc-date-left',
|
|
format({ timeStart }) {
|
|
return timeStart.format('DD MMMM YYYY (dddd)');
|
|
}
|
|
},
|
|
{
|
|
zoomTo: 23,
|
|
period: 'month',
|
|
format({ timeStart }) {
|
|
return timeStart.format('MMMM YYYY');
|
|
}
|
|
},
|
|
{
|
|
zoomTo: 24,
|
|
period: 'month',
|
|
format({ timeStart, className, vido }) {
|
|
return timeStart.format("MMMM 'YY");
|
|
}
|
|
},
|
|
{
|
|
zoomTo: 25,
|
|
period: 'month',
|
|
format({ timeStart }) {
|
|
return timeStart.format('MMM YYYY');
|
|
}
|
|
},
|
|
{
|
|
zoomTo: 27,
|
|
period: 'year',
|
|
format({ timeStart }) {
|
|
return timeStart.format('YYYY');
|
|
}
|
|
},
|
|
{
|
|
zoomTo: 100,
|
|
period: 'year',
|
|
default: true,
|
|
format() {
|
|
return null;
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
main: true,
|
|
formats: [
|
|
{
|
|
zoomTo: 16,
|
|
period: 'hour',
|
|
format({ timeStart }) {
|
|
return timeStart.format('HH:mm');
|
|
}
|
|
},
|
|
{
|
|
zoomTo: 17,
|
|
period: 'hour',
|
|
default: true,
|
|
format({ timeStart }) {
|
|
return timeStart.format('HH');
|
|
}
|
|
},
|
|
{
|
|
zoomTo: 19,
|
|
period: 'day',
|
|
className: 'gstc-date-medium',
|
|
format({ timeStart, className, vido }) {
|
|
return vido.html `<span class="${className}-content gstc-date-bold">${timeStart.format('DD')}</span> <span class="${className}-content gstc-date-thin">${timeStart.format('dddd')}</span>`;
|
|
}
|
|
},
|
|
{
|
|
zoomTo: 20,
|
|
period: 'day',
|
|
default: true,
|
|
format({ timeStart, vido, className }) {
|
|
return vido.html `<div class="${className}-content gstc-date-top">${timeStart.format('DD')}</div><div class="${className}-content gstc-date-small">${timeStart.format('dddd')}</div>`;
|
|
}
|
|
},
|
|
{
|
|
zoomTo: 21,
|
|
period: 'day',
|
|
format({ timeStart, vido, className }) {
|
|
return vido.html `<div class="${className}-content gstc-date-top">${timeStart.format('DD')}</div><div class="${className}-content gstc-date-small">${timeStart.format('ddd')}</div>`;
|
|
}
|
|
},
|
|
{
|
|
zoomTo: 22,
|
|
period: 'day',
|
|
className: 'gstc-date-vertical',
|
|
format({ timeStart, className, vido }) {
|
|
return vido.html `<div class="${className}-content gstc-date-top">${timeStart.format('DD')}</div><div class="${className}-content gstc-date-extra-small">${timeStart.format('ddd')}</div>`;
|
|
}
|
|
},
|
|
{
|
|
zoomTo: 23,
|
|
period: 'week',
|
|
default: true,
|
|
format({ timeStart, timeEnd, className, vido }) {
|
|
return vido.html `<div class="${className}-content gstc-date-top">${timeStart.format('DD')} - ${timeEnd.format('DD')}</div><div class="${className}-content gstc-date-small gstc-date-thin">${timeStart.format('ddd')} - ${timeEnd.format('dd')}</div>`;
|
|
}
|
|
},
|
|
{
|
|
zoomTo: 25,
|
|
period: 'week',
|
|
className: 'gstc-date-vertical',
|
|
format({ timeStart, timeEnd, className, vido }) {
|
|
return vido.html `<div class="${className}-content gstc-date-top gstc-date-small gstc-date-normal">${timeStart.format('DD')}</div><div class="gstc-dash gstc-date-small">-</div><div class="${className}-content gstc-date-small gstc-date-normal">${timeEnd.format('DD')}</div>`;
|
|
}
|
|
},
|
|
{
|
|
zoomTo: 26,
|
|
period: 'month',
|
|
default: true,
|
|
className: 'gstc-date-month-level-1',
|
|
format({ timeStart, vido, className }) {
|
|
return vido.html `<div class="${className}-content gstc-date-top">${timeStart.format('MMM')}</div><div class="${className}-content gstc-date-small gstc-date-bottom">${timeStart.format('MM')}</div>`;
|
|
}
|
|
},
|
|
{
|
|
zoomTo: 27,
|
|
period: 'month',
|
|
className: 'gstc-date-vertical',
|
|
format({ timeStart, className, vido }) {
|
|
return vido.html `<div class="${className}-content gstc-date-top">${timeStart.format('MM')}</div><div class="${className}-content gstc-date-extra-small">${timeStart.format('MMM')}</div>`;
|
|
}
|
|
},
|
|
{
|
|
zoomTo: 28,
|
|
period: 'year',
|
|
default: true,
|
|
className: 'gstc-date-big',
|
|
format({ timeStart }) {
|
|
return timeStart.format('YYYY');
|
|
}
|
|
},
|
|
{
|
|
zoomTo: 29,
|
|
period: 'year',
|
|
className: 'gstc-date-medium',
|
|
format({ timeStart }) {
|
|
return timeStart.format('YYYY');
|
|
}
|
|
},
|
|
{
|
|
zoomTo: 30,
|
|
period: 'year',
|
|
className: 'gstc-date-medium',
|
|
format({ timeStart }) {
|
|
return timeStart.format('YY');
|
|
}
|
|
},
|
|
{
|
|
zoomTo: 100,
|
|
period: 'year',
|
|
default: true,
|
|
format() {
|
|
return null;
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
grid: {
|
|
block: {
|
|
onCreate: []
|
|
}
|
|
},
|
|
items: {},
|
|
spacing: 1
|
|
},
|
|
slots,
|
|
classNames: {},
|
|
actions,
|
|
locale: {
|
|
name: 'en',
|
|
weekdays: 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'),
|
|
weekdaysShort: 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'),
|
|
weekdaysMin: 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'),
|
|
months: 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'),
|
|
monthsShort: 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'),
|
|
weekStart: 1,
|
|
relativeTime: {
|
|
future: 'in %s',
|
|
past: '%s ago',
|
|
s: 'a few seconds',
|
|
m: 'a minute',
|
|
mm: '%d minutes',
|
|
h: 'an hour',
|
|
hh: '%d hours',
|
|
d: 'a day',
|
|
dd: '%d days',
|
|
M: 'a month',
|
|
MM: '%d months',
|
|
y: 'a year',
|
|
yy: '%d years'
|
|
},
|
|
formats: {
|
|
LT: 'HH:mm',
|
|
LTS: 'HH:mm:ss',
|
|
L: 'DD/MM/YYYY',
|
|
LL: 'D MMMM YYYY',
|
|
LLL: 'D MMMM YYYY HH:mm',
|
|
LLLL: 'dddd, D MMMM YYYY HH:mm'
|
|
},
|
|
ordinal: (n) => {
|
|
const s = ['th', 'st', 'nd', 'rd'];
|
|
const v = n % 100;
|
|
return `[${n}${s[(v - 20) % 10] || s[v] || s[0]}]`;
|
|
}
|
|
},
|
|
utcMode: false,
|
|
usageStatistics: true
|
|
};
|
|
}
|
|
|
|
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
|
|
|
function createCommonjsModule(fn, module) {
|
|
return module = { exports: {} }, fn(module, module.exports), module.exports;
|
|
}
|
|
|
|
var dayjs_min = createCommonjsModule(function (module, exports) {
|
|
!function(t,n){module.exports=n();}(commonjsGlobal,function(){var t="millisecond",n="second",e="minute",r="hour",i="day",s="week",u="month",o="quarter",a="year",h=/^(\d{4})-?(\d{1,2})-?(\d{0,2})[^0-9]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?.?(\d{1,3})?$/,f=/\[([^\]]+)]|Y{2,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,c=function(t,n,e){var r=String(t);return !r||r.length>=n?t:""+Array(n+1-r.length).join(e)+t},d={s:c,z:function(t){var n=-t.utcOffset(),e=Math.abs(n),r=Math.floor(e/60),i=e%60;return (n<=0?"+":"-")+c(r,2,"0")+":"+c(i,2,"0")},m:function(t,n){var e=12*(n.year()-t.year())+(n.month()-t.month()),r=t.clone().add(e,u),i=n-r<0,s=t.clone().add(e+(i?-1:1),u);return Number(-(e+(n-r)/(i?r-s:s-r))||0)},a:function(t){return t<0?Math.ceil(t)||0:Math.floor(t)},p:function(h){return {M:u,y:a,w:s,d:i,D:"date",h:r,m:e,s:n,ms:t,Q:o}[h]||String(h||"").toLowerCase().replace(/s$/,"")},u:function(t){return void 0===t}},$={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_")},l="en",m={};m[l]=$;var y=function(t){return t instanceof v},M=function(t,n,e){var r;if(!t)return l;if("string"==typeof t)m[t]&&(r=t),n&&(m[t]=n,r=t);else{var i=t.name;m[i]=t,r=i;}return !e&&r&&(l=r),r||!e&&l},g=function(t,n,e){if(y(t))return t.clone();var r=n?"string"==typeof n?{format:n,pl:e}:n:{};return r.date=t,new v(r)},D=d;D.l=M,D.i=y,D.w=function(t,n){return g(t,{locale:n.$L,utc:n.$u,$offset:n.$offset})};var v=function(){function c(t){this.$L=this.$L||M(t.locale,null,!0),this.parse(t);}var d=c.prototype;return d.parse=function(t){this.$d=function(t){var n=t.date,e=t.utc;if(null===n)return new Date(NaN);if(D.u(n))return new Date;if(n instanceof Date)return new Date(n);if("string"==typeof n&&!/Z$/i.test(n)){var r=n.match(h);if(r)return e?new Date(Date.UTC(r[1],r[2]-1,r[3]||1,r[4]||0,r[5]||0,r[6]||0,r[7]||0)):new Date(r[1],r[2]-1,r[3]||1,r[4]||0,r[5]||0,r[6]||0,r[7]||0)}return new Date(n)}(t),this.init();},d.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds();},d.$utils=function(){return D},d.isValid=function(){return !("Invalid Date"===this.$d.toString())},d.isSame=function(t,n){var e=g(t);return this.startOf(n)<=e&&e<=this.endOf(n)},d.isAfter=function(t,n){return g(t)<this.startOf(n)},d.isBefore=function(t,n){return this.endOf(n)<g(t)},d.$g=function(t,n,e){return D.u(t)?this[n]:this.set(e,t)},d.year=function(t){return this.$g(t,"$y",a)},d.month=function(t){return this.$g(t,"$M",u)},d.day=function(t){return this.$g(t,"$W",i)},d.date=function(t){return this.$g(t,"$D","date")},d.hour=function(t){return this.$g(t,"$H",r)},d.minute=function(t){return this.$g(t,"$m",e)},d.second=function(t){return this.$g(t,"$s",n)},d.millisecond=function(n){return this.$g(n,"$ms",t)},d.unix=function(){return Math.floor(this.valueOf()/1e3)},d.valueOf=function(){return this.$d.getTime()},d.startOf=function(t,o){var h=this,f=!!D.u(o)||o,c=D.p(t),d=function(t,n){var e=D.w(h.$u?Date.UTC(h.$y,n,t):new Date(h.$y,n,t),h);return f?e:e.endOf(i)},$=function(t,n){return D.w(h.toDate()[t].apply(h.toDate(),(f?[0,0,0,0]:[23,59,59,999]).slice(n)),h)},l=this.$W,m=this.$M,y=this.$D,M="set"+(this.$u?"UTC":"");switch(c){case a:return f?d(1,0):d(31,11);case u:return f?d(1,m):d(0,m+1);case s:var g=this.$locale().weekStart||0,v=(l<g?l+7:l)-g;return d(f?y-v:y+(6-v),m);case i:case"date":return $(M+"Hours",0);case r:return $(M+"Minutes",1);case e:return $(M+"Seconds",2);case n:return $(M+"Milliseconds",3);default:return this.clone()}},d.endOf=function(t){return this.startOf(t,!1)},d.$set=function(s,o){var h,f=D.p(s),c="set"+(this.$u?"UTC":""),d=(h={},h[i]=c+"Date",h.date=c+"Date",h[u]=c+"Month",h[a]=c+"FullYear",h[r]=c+"Hours",h[e]=c+"Minutes",h[n]=c+"Seconds",h[t]=c+"Milliseconds",h)[f],$=f===i?this.$D+(o-this.$W):o;if(f===u||f===a){var l=this.clone().set("date",1);l.$d[d]($),l.init(),this.$d=l.set("date",Math.min(this.$D,l.daysInMonth())).toDate();}else d&&this.$d[d]($);return this.init(),this},d.set=function(t,n){return this.clone().$set(t,n)},d.get=function(t){return this[D.p(t)]()},d.add=function(t,o){var h,f=this;t=Number(t);var c=D.p(o),d=function(n){var e=g(f);return D.w(e.date(e.date()+Math.round(n*t)),f)};if(c===u)return this.set(u,this.$M+t);if(c===a)return this.set(a,this.$y+t);if(c===i)return d(1);if(c===s)return d(7);var $=(h={},h[e]=6e4,h[r]=36e5,h[n]=1e3,h)[c]||1,l=this.$d.getTime()+t*$;return D.w(l,this)},d.subtract=function(t,n){return this.add(-1*t,n)},d.format=function(t){var n=this;if(!this.isValid())return "Invalid Date";var e=t||"YYYY-MM-DDTHH:mm:ssZ",r=D.z(this),i=this.$locale(),s=this.$H,u=this.$m,o=this.$M,a=i.weekdays,h=i.months,c=function(t,r,i,s){return t&&(t[r]||t(n,e))||i[r].substr(0,s)},d=function(t){return D.s(s%12||12,t,"0")},$=i.meridiem||function(t,n,e){var r=t<12?"AM":"PM";return e?r.toLowerCase():r},l={YY:String(this.$y).slice(-2),YYYY:this.$y,M:o+1,MM:D.s(o+1,2,"0"),MMM:c(i.monthsShort,o,h,3),MMMM:h[o]||h(this,e),D:this.$D,DD:D.s(this.$D,2,"0"),d:String(this.$W),dd:c(i.weekdaysMin,this.$W,a,2),ddd:c(i.weekdaysShort,this.$W,a,3),dddd:a[this.$W],H:String(s),HH:D.s(s,2,"0"),h:d(1),hh:d(2),a:$(s,u,!0),A:$(s,u,!1),m:String(u),mm:D.s(u,2,"0"),s:String(this.$s),ss:D.s(this.$s,2,"0"),SSS:D.s(this.$ms,3,"0"),Z:r};return e.replace(f,function(t,n){return n||l[t]||r.replace(":","")})},d.utcOffset=function(){return 15*-Math.round(this.$d.getTimezoneOffset()/15)},d.diff=function(t,h,f){var c,d=D.p(h),$=g(t),l=6e4*($.utcOffset()-this.utcOffset()),m=this-$,y=D.m(this,$);return y=(c={},c[a]=y/12,c[u]=y,c[o]=y/3,c[s]=(m-l)/6048e5,c[i]=(m-l)/864e5,c[r]=m/36e5,c[e]=m/6e4,c[n]=m/1e3,c)[d]||m,f?y:D.a(y)},d.daysInMonth=function(){return this.endOf(u).$D},d.$locale=function(){return m[this.$L]},d.locale=function(t,n){if(!t)return this.$L;var e=this.clone(),r=M(t,n,!0);return r&&(e.$L=r),e},d.clone=function(){return D.w(this.$d,this)},d.toDate=function(){return new Date(this.valueOf())},d.toJSON=function(){return this.isValid()?this.toISOString():null},d.toISOString=function(){return this.$d.toISOString()},d.toString=function(){return this.$d.toUTCString()},c}();return g.prototype=v.prototype,g.extend=function(t,n){return t(n,v,g),g},g.locale=M,g.isDayjs=y,g.unix=function(t){return g(1e3*t)},g.en=m[l],g.Ls=m,g});
|
|
});
|
|
|
|
var utc = createCommonjsModule(function (module, exports) {
|
|
!function(t,i){module.exports=i();}(commonjsGlobal,function(){return function(t,i,e){var s=(new Date).getTimezoneOffset(),n=i.prototype;e.utc=function(t,e){return new i({date:t,utc:!0,format:e})},n.utc=function(){return e(this.toDate(),{locale:this.$L,utc:!0})},n.local=function(){return e(this.toDate(),{locale:this.$L,utc:!1})};var u=n.parse;n.parse=function(t){t.utc&&(this.$u=!0),this.$utils().u(t.$offset)||(this.$offset=t.$offset),u.call(this,t);};var o=n.init;n.init=function(){if(this.$u){var t=this.$d;this.$y=t.getUTCFullYear(),this.$M=t.getUTCMonth(),this.$D=t.getUTCDate(),this.$W=t.getUTCDay(),this.$H=t.getUTCHours(),this.$m=t.getUTCMinutes(),this.$s=t.getUTCSeconds(),this.$ms=t.getUTCMilliseconds();}else o.call(this);};var f=n.utcOffset;n.utcOffset=function(t){var i=this.$utils().u;if(i(t))return this.$u?0:i(this.$offset)?f.call(this):this.$offset;var e,n=Math.abs(t)<=16?60*t:t;return 0!==t?(e=this.local().add(n+s,"minute")).$offset=n:e=this.utc(),e};var r=n.format;n.format=function(t){var i=t||(this.$u?"YYYY-MM-DDTHH:mm:ss[Z]":"");return r.call(this,i)},n.valueOf=function(){var t=this.$utils().u(this.$offset)?0:this.$offset+s;return this.$d.valueOf()-6e4*t},n.isUTC=function(){return !!this.$u},n.toISOString=function(){return this.toDate().toISOString()},n.toString=function(){return this.toDate().toUTCString()};}});
|
|
});
|
|
|
|
var advancedFormat = createCommonjsModule(function (module, exports) {
|
|
!function(e,t){module.exports=t();}(commonjsGlobal,function(){return function(e,t,r){var n=t.prototype,o=n.format;r.en.ordinal=function(e){var t=["th","st","nd","rd"],r=e%100;return "["+e+(t[(r-20)%10]||t[r]||t[0])+"]"},n.format=function(e){var t=this,r=this.$locale(),n=this.$utils(),a=(e||"YYYY-MM-DDTHH:mm:ssZ").replace(/\[([^\]]+)]|Q|wo|ww|w|gggg|Do|X|x|k{1,2}|S/g,function(e){switch(e){case"Q":return Math.ceil((t.$M+1)/3);case"Do":return r.ordinal(t.$D);case"gggg":return t.weekYear();case"wo":return r.ordinal(t.week(),"W");case"w":case"ww":return n.s(t.week(),"w"===e?1:2,"0");case"k":case"kk":return n.s(String(0===t.$H?24:t.$H),"k"===e?1:2,"0");case"X":return Math.floor(t.$d.getTime()/1e3);case"x":return t.$d.getTime();default:return e}});return o.bind(this)(a)};}});
|
|
});
|
|
|
|
var weekOfYear = createCommonjsModule(function (module, exports) {
|
|
!function(e,t){module.exports=t();}(commonjsGlobal,function(){var e="week",t="year";return function(i,n){var r=n.prototype;r.week=function(i){if(void 0===i&&(i=null),null!==i)return this.add(7*(i-this.week()),"day");var n=this.$locale().yearStart||1;if(11===this.month()&&this.date()>25){var r=this.startOf(t).add(1,t).date(n),f=this.endOf(e);if(r.isBefore(f))return 1}var s=this.startOf(t).date(n).startOf(e).subtract(1,"millisecond"),a=this.diff(s,e,!0);return a<0?this.startOf("week").week():Math.ceil(a)},r.weeks=function(e){return void 0===e&&(e=null),this.week(e)};}});
|
|
});
|
|
|
|
/**
|
|
* Gantt-Schedule-Timeline-Calendar
|
|
*
|
|
* @copyright Rafal Pospiech <https://neuronet.io>
|
|
* @author Rafal Pospiech <neuronet.io@gmail.com>
|
|
* @package gantt-schedule-timeline-calendar
|
|
* @license AGPL-3.0
|
|
*/
|
|
dayjs_min.extend(advancedFormat);
|
|
dayjs_min.extend(weekOfYear);
|
|
class TimeApi {
|
|
constructor(state) {
|
|
this.utcMode = false;
|
|
this.state = state;
|
|
this.locale = state.get('config.locale');
|
|
this.utcMode = state.get('config.utcMode');
|
|
if (this.utcMode) {
|
|
dayjs_min.extend(utc);
|
|
}
|
|
// @ts-ignore
|
|
dayjs_min.locale(this.locale, null, true);
|
|
}
|
|
date(time) {
|
|
const _dayjs = this.utcMode ? dayjs_min.utc : dayjs_min;
|
|
return time ? _dayjs(time).locale(this.locale.name) : _dayjs().locale(this.locale.name);
|
|
}
|
|
addAdditionalSpace(time) {
|
|
if (time.additionalSpaces && time.additionalSpaces[time.period]) {
|
|
const add = time.additionalSpaces[time.period];
|
|
if (add.before) {
|
|
time.finalFrom = this.date(time.from)
|
|
.subtract(add.before, add.period)
|
|
.valueOf();
|
|
}
|
|
if (add.after) {
|
|
time.finalTo = this.date(time.to)
|
|
.add(add.after, add.period)
|
|
.valueOf();
|
|
}
|
|
}
|
|
return time;
|
|
}
|
|
recalculateFromTo(time) {
|
|
const period = time.period;
|
|
time = Object.assign({}, time);
|
|
time.from = +time.from;
|
|
time.to = +time.to;
|
|
let from = Number.MAX_SAFE_INTEGER, to = 0;
|
|
const items = this.state.get('config.chart.items');
|
|
if (Object.keys(items).length === 0) {
|
|
return time;
|
|
}
|
|
if (time.from === 0 || time.to === 0) {
|
|
for (const itemId in items) {
|
|
const item = items[itemId];
|
|
if (item.time.start < from && item.time.start) {
|
|
from = item.time.start;
|
|
}
|
|
if (item.time.end > to) {
|
|
to = item.time.end;
|
|
}
|
|
}
|
|
if (time.from === 0) {
|
|
time.from = this.date(from)
|
|
.startOf(period)
|
|
.valueOf();
|
|
}
|
|
if (time.to === 0) {
|
|
time.to = this.date(to)
|
|
.endOf(period)
|
|
.valueOf();
|
|
}
|
|
}
|
|
time.finalFrom = time.from;
|
|
time.finalTo = time.to;
|
|
time = this.addAdditionalSpace(time);
|
|
return time;
|
|
}
|
|
getCenter(time) {
|
|
return time.leftGlobal + (time.rightGlobal - time.leftGlobal) / 2;
|
|
}
|
|
timeToPixelOffset(milliseconds) {
|
|
const timePerPixel = this.state.get('_internal.chart.time.timePerPixel') || 1;
|
|
return milliseconds / timePerPixel;
|
|
}
|
|
globalTimeToViewPixelOffset(milliseconds, withCompensation = false) {
|
|
const time = this.state.get('_internal.chart.time');
|
|
let xCompensation = this.state.get('config.scroll.compensation.x') || 0;
|
|
const viewPixelOffset = (milliseconds - time.leftGlobal) / time.timePerPixel;
|
|
if (withCompensation)
|
|
return viewPixelOffset + xCompensation;
|
|
return viewPixelOffset;
|
|
}
|
|
}
|
|
|
|
// forked from https://github.com/joonhocho/superwild
|
|
function Matcher(pattern, wchar = '*') {
|
|
this.wchar = wchar;
|
|
this.pattern = pattern;
|
|
this.segments = [];
|
|
this.starCount = 0;
|
|
this.minLength = 0;
|
|
this.maxLength = 0;
|
|
this.segStartIndex = 0;
|
|
for (let i = 0, len = pattern.length; i < len; i += 1) {
|
|
const char = pattern[i];
|
|
if (char === wchar) {
|
|
this.starCount += 1;
|
|
if (i > this.segStartIndex) {
|
|
this.segments.push(pattern.substring(this.segStartIndex, i));
|
|
}
|
|
this.segments.push(char);
|
|
this.segStartIndex = i + 1;
|
|
}
|
|
}
|
|
if (this.segStartIndex < pattern.length) {
|
|
this.segments.push(pattern.substring(this.segStartIndex));
|
|
}
|
|
if (this.starCount) {
|
|
this.minLength = pattern.length - this.starCount;
|
|
this.maxLength = Infinity;
|
|
}
|
|
else {
|
|
this.maxLength = this.minLength = pattern.length;
|
|
}
|
|
}
|
|
Matcher.prototype.match = function match(match) {
|
|
if (this.pattern === this.wchar) {
|
|
return true;
|
|
}
|
|
if (this.segments.length === 0) {
|
|
return this.pattern === match;
|
|
}
|
|
const { length } = match;
|
|
if (length < this.minLength || length > this.maxLength) {
|
|
return false;
|
|
}
|
|
let segLeftIndex = 0;
|
|
let segRightIndex = this.segments.length - 1;
|
|
let rightPos = match.length - 1;
|
|
let rightIsStar = false;
|
|
while (true) {
|
|
const segment = this.segments[segRightIndex];
|
|
segRightIndex -= 1;
|
|
if (segment === this.wchar) {
|
|
rightIsStar = true;
|
|
}
|
|
else {
|
|
const lastIndex = rightPos + 1 - segment.length;
|
|
const index = match.lastIndexOf(segment, lastIndex);
|
|
if (index === -1 || index > lastIndex) {
|
|
return false;
|
|
}
|
|
if (rightIsStar) {
|
|
rightPos = index - 1;
|
|
rightIsStar = false;
|
|
}
|
|
else {
|
|
if (index !== lastIndex) {
|
|
return false;
|
|
}
|
|
rightPos -= segment.length;
|
|
}
|
|
}
|
|
if (segLeftIndex > segRightIndex) {
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
function WildcardObject(obj, delimeter, wildcard) {
|
|
this.obj = obj;
|
|
this.delimeter = delimeter;
|
|
this.wildcard = wildcard;
|
|
}
|
|
WildcardObject.prototype.simpleMatch = function simpleMatch(first, second) {
|
|
if (first === second)
|
|
return true;
|
|
if (first === this.wildcard)
|
|
return true;
|
|
const index = first.indexOf(this.wildcard);
|
|
if (index > -1) {
|
|
const end = first.substr(index + 1);
|
|
if (index === 0 || second.substring(0, index) === first.substring(0, index)) {
|
|
const len = end.length;
|
|
if (len > 0) {
|
|
return second.substr(-len) === end;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
WildcardObject.prototype.match = function match(first, second) {
|
|
return (first === second ||
|
|
first === this.wildcard ||
|
|
second === this.wildcard ||
|
|
this.simpleMatch(first, second) ||
|
|
new Matcher(first).match(second));
|
|
};
|
|
WildcardObject.prototype.handleArray = function handleArray(wildcard, currentArr, partIndex, path, result = {}) {
|
|
let nextPartIndex = wildcard.indexOf(this.delimeter, partIndex);
|
|
let end = false;
|
|
if (nextPartIndex === -1) {
|
|
end = true;
|
|
nextPartIndex = wildcard.length;
|
|
}
|
|
const currentWildcardPath = wildcard.substring(partIndex, nextPartIndex);
|
|
let index = 0;
|
|
for (const item of currentArr) {
|
|
const key = index.toString();
|
|
const currentPath = path === '' ? key : path + this.delimeter + index;
|
|
if (currentWildcardPath === this.wildcard ||
|
|
currentWildcardPath === key ||
|
|
this.simpleMatch(currentWildcardPath, key)) {
|
|
end ? (result[currentPath] = item) : this.goFurther(wildcard, item, nextPartIndex + 1, currentPath, result);
|
|
}
|
|
index++;
|
|
}
|
|
return result;
|
|
};
|
|
WildcardObject.prototype.handleObject = function handleObject(wildcard, currentObj, partIndex, path, result = {}) {
|
|
let nextPartIndex = wildcard.indexOf(this.delimeter, partIndex);
|
|
let end = false;
|
|
if (nextPartIndex === -1) {
|
|
end = true;
|
|
nextPartIndex = wildcard.length;
|
|
}
|
|
const currentWildcardPath = wildcard.substring(partIndex, nextPartIndex);
|
|
for (let key in currentObj) {
|
|
key = key.toString();
|
|
const currentPath = path === '' ? key : path + this.delimeter + key;
|
|
if (currentWildcardPath === this.wildcard ||
|
|
currentWildcardPath === key ||
|
|
this.simpleMatch(currentWildcardPath, key)) {
|
|
end
|
|
? (result[currentPath] = currentObj[key])
|
|
: this.goFurther(wildcard, currentObj[key], nextPartIndex + 1, currentPath, result);
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
WildcardObject.prototype.goFurther = function goFurther(wildcard, currentObj, partIndex, currentPath, result = {}) {
|
|
if (Array.isArray(currentObj)) {
|
|
return this.handleArray(wildcard, currentObj, partIndex, currentPath, result);
|
|
}
|
|
return this.handleObject(wildcard, currentObj, partIndex, currentPath, result);
|
|
};
|
|
WildcardObject.prototype.get = function get(wildcard) {
|
|
return this.goFurther(wildcard, this.obj, 0, '');
|
|
};
|
|
|
|
class ObjectPath {
|
|
static get(path, obj, copiedPath = null) {
|
|
if (copiedPath === null) {
|
|
copiedPath = path.slice();
|
|
}
|
|
if (copiedPath.length === 0 || typeof obj === "undefined") {
|
|
return obj;
|
|
}
|
|
const currentPath = copiedPath.shift();
|
|
if (!obj.hasOwnProperty(currentPath)) {
|
|
return undefined;
|
|
}
|
|
if (copiedPath.length === 0) {
|
|
return obj[currentPath];
|
|
}
|
|
return ObjectPath.get(path, obj[currentPath], copiedPath);
|
|
}
|
|
static set(path, newValue, obj, copiedPath = null) {
|
|
if (copiedPath === null) {
|
|
copiedPath = path.slice();
|
|
}
|
|
if (copiedPath.length === 0) {
|
|
for (const key in obj) {
|
|
delete obj[key];
|
|
}
|
|
for (const key in newValue) {
|
|
obj[key] = newValue[key];
|
|
}
|
|
return;
|
|
}
|
|
const currentPath = copiedPath.shift();
|
|
if (copiedPath.length === 0) {
|
|
obj[currentPath] = newValue;
|
|
return;
|
|
}
|
|
if (!obj) {
|
|
obj = {};
|
|
}
|
|
if (!obj.hasOwnProperty(currentPath)) {
|
|
obj[currentPath] = {};
|
|
}
|
|
ObjectPath.set(path, newValue, obj[currentPath], copiedPath);
|
|
}
|
|
}
|
|
|
|
function log(message, info) {
|
|
console.debug(message, info);
|
|
}
|
|
const defaultOptions$1 = {
|
|
delimeter: `.`,
|
|
notRecursive: `;`,
|
|
param: `:`,
|
|
wildcard: `*`,
|
|
log
|
|
};
|
|
const defaultListenerOptions = {
|
|
bulk: false,
|
|
debug: false,
|
|
source: "",
|
|
data: undefined
|
|
};
|
|
const defaultUpdateOptions = {
|
|
only: [],
|
|
source: "",
|
|
debug: false,
|
|
data: undefined,
|
|
updateAfter: false
|
|
};
|
|
class DeepState {
|
|
constructor(data = {}, options = defaultOptions$1) {
|
|
this.listeners = new Map();
|
|
this.waitingListeners = new Map();
|
|
this.data = data;
|
|
this.options = Object.assign(Object.assign({}, defaultOptions$1), options);
|
|
this.id = 0;
|
|
this.pathGet = ObjectPath.get;
|
|
this.pathSet = ObjectPath.set;
|
|
this.scan = new WildcardObject(this.data, this.options.delimeter, this.options.wildcard);
|
|
}
|
|
getListeners() {
|
|
return this.listeners;
|
|
}
|
|
destroy() {
|
|
this.data = undefined;
|
|
this.listeners = new Map();
|
|
}
|
|
match(first, second) {
|
|
if (first === second)
|
|
return true;
|
|
if (first === this.options.wildcard || second === this.options.wildcard)
|
|
return true;
|
|
return this.scan.match(first, second);
|
|
}
|
|
getIndicesOf(searchStr, str) {
|
|
const searchStrLen = searchStr.length;
|
|
if (searchStrLen == 0) {
|
|
return [];
|
|
}
|
|
let startIndex = 0, index, indices = [];
|
|
while ((index = str.indexOf(searchStr, startIndex)) > -1) {
|
|
indices.push(index);
|
|
startIndex = index + searchStrLen;
|
|
}
|
|
return indices;
|
|
}
|
|
getIndicesCount(searchStr, str) {
|
|
const searchStrLen = searchStr.length;
|
|
if (searchStrLen == 0) {
|
|
return 0;
|
|
}
|
|
let startIndex = 0, index, indices = 0;
|
|
while ((index = str.indexOf(searchStr, startIndex)) > -1) {
|
|
indices++;
|
|
startIndex = index + searchStrLen;
|
|
}
|
|
return indices;
|
|
}
|
|
cutPath(longer, shorter) {
|
|
longer = this.cleanNotRecursivePath(longer);
|
|
shorter = this.cleanNotRecursivePath(shorter);
|
|
const shorterPartsLen = this.getIndicesCount(this.options.delimeter, shorter);
|
|
const longerParts = this.getIndicesOf(this.options.delimeter, longer);
|
|
return longer.substr(0, longerParts[shorterPartsLen]);
|
|
}
|
|
trimPath(path) {
|
|
path = this.cleanNotRecursivePath(path);
|
|
if (path.charAt(0) === this.options.delimeter) {
|
|
return path.substr(1);
|
|
}
|
|
return path;
|
|
}
|
|
split(path) {
|
|
return path === "" ? [] : path.split(this.options.delimeter);
|
|
}
|
|
isWildcard(path) {
|
|
return path.includes(this.options.wildcard);
|
|
}
|
|
isNotRecursive(path) {
|
|
return path.endsWith(this.options.notRecursive);
|
|
}
|
|
cleanNotRecursivePath(path) {
|
|
return this.isNotRecursive(path) ? path.substring(0, path.length - 1) : path;
|
|
}
|
|
hasParams(path) {
|
|
return path.includes(this.options.param);
|
|
}
|
|
getParamsInfo(path) {
|
|
let paramsInfo = { replaced: "", original: path, params: {} };
|
|
let partIndex = 0;
|
|
let fullReplaced = [];
|
|
for (const part of this.split(path)) {
|
|
paramsInfo.params[partIndex] = {
|
|
original: part,
|
|
replaced: "",
|
|
name: ""
|
|
};
|
|
const reg = new RegExp(`\\${this.options.param}([^\\${this.options.delimeter}\\${this.options.param}]+)`, "g");
|
|
let param = reg.exec(part);
|
|
if (param) {
|
|
paramsInfo.params[partIndex].name = param[1];
|
|
}
|
|
else {
|
|
delete paramsInfo.params[partIndex];
|
|
fullReplaced.push(part);
|
|
partIndex++;
|
|
continue;
|
|
}
|
|
reg.lastIndex = 0;
|
|
paramsInfo.params[partIndex].replaced = part.replace(reg, this.options.wildcard);
|
|
fullReplaced.push(paramsInfo.params[partIndex].replaced);
|
|
partIndex++;
|
|
}
|
|
paramsInfo.replaced = fullReplaced.join(this.options.delimeter);
|
|
return paramsInfo;
|
|
}
|
|
getParams(paramsInfo, path) {
|
|
if (!paramsInfo) {
|
|
return undefined;
|
|
}
|
|
const split = this.split(path);
|
|
const result = {};
|
|
for (const partIndex in paramsInfo.params) {
|
|
const param = paramsInfo.params[partIndex];
|
|
result[param.name] = split[partIndex];
|
|
}
|
|
return result;
|
|
}
|
|
waitForAll(userPaths, fn) {
|
|
const paths = {};
|
|
for (let path of userPaths) {
|
|
paths[path] = { dirty: false };
|
|
if (this.hasParams(path)) {
|
|
paths[path].paramsInfo = this.getParamsInfo(path);
|
|
}
|
|
paths[path].isWildcard = this.isWildcard(path);
|
|
paths[path].isRecursive = !this.isNotRecursive(path);
|
|
}
|
|
this.waitingListeners.set(userPaths, { fn, paths });
|
|
fn(paths);
|
|
return function unsubscribe() {
|
|
this.waitingListeners.delete(userPaths);
|
|
};
|
|
}
|
|
executeWaitingListeners(updatePath) {
|
|
for (const waitingListener of this.waitingListeners.values()) {
|
|
const { fn, paths } = waitingListener;
|
|
let dirty = 0;
|
|
let all = 0;
|
|
for (let path in paths) {
|
|
const pathInfo = paths[path];
|
|
let match = false;
|
|
if (pathInfo.isRecursive)
|
|
updatePath = this.cutPath(updatePath, path);
|
|
if (pathInfo.isWildcard && this.match(path, updatePath))
|
|
match = true;
|
|
if (updatePath === path)
|
|
match = true;
|
|
if (match) {
|
|
pathInfo.dirty = true;
|
|
}
|
|
if (pathInfo.dirty) {
|
|
dirty++;
|
|
}
|
|
all++;
|
|
}
|
|
if (dirty === all) {
|
|
fn(paths);
|
|
}
|
|
}
|
|
}
|
|
subscribeAll(userPaths, fn, options = defaultListenerOptions) {
|
|
let unsubscribers = [];
|
|
for (const userPath of userPaths) {
|
|
unsubscribers.push(this.subscribe(userPath, fn, options));
|
|
}
|
|
return function unsubscribe() {
|
|
for (const unsubscribe of unsubscribers) {
|
|
unsubscribe();
|
|
}
|
|
};
|
|
}
|
|
getCleanListenersCollection(values = {}) {
|
|
return Object.assign({ listeners: new Map(), isRecursive: false, isWildcard: false, hasParams: false, match: undefined, paramsInfo: undefined, path: undefined, count: 0 }, values);
|
|
}
|
|
getCleanListener(fn, options = defaultListenerOptions) {
|
|
return {
|
|
fn,
|
|
options: Object.assign(Object.assign({}, defaultListenerOptions), options)
|
|
};
|
|
}
|
|
getListenerCollectionMatch(listenerPath, isRecursive, isWildcard) {
|
|
listenerPath = this.cleanNotRecursivePath(listenerPath);
|
|
const self = this;
|
|
return function listenerCollectionMatch(path) {
|
|
if (isRecursive)
|
|
path = self.cutPath(path, listenerPath);
|
|
if (isWildcard && self.match(listenerPath, path))
|
|
return true;
|
|
return listenerPath === path;
|
|
};
|
|
}
|
|
getListenersCollection(listenerPath, listener) {
|
|
if (this.listeners.has(listenerPath)) {
|
|
let listenersCollection = this.listeners.get(listenerPath);
|
|
listenersCollection.listeners.set(++this.id, listener);
|
|
return listenersCollection;
|
|
}
|
|
let collCfg = {
|
|
isRecursive: true,
|
|
isWildcard: false,
|
|
hasParams: false,
|
|
paramsInfo: undefined,
|
|
originalPath: listenerPath,
|
|
path: listenerPath
|
|
};
|
|
if (this.hasParams(collCfg.path)) {
|
|
collCfg.paramsInfo = this.getParamsInfo(collCfg.path);
|
|
collCfg.path = collCfg.paramsInfo.replaced;
|
|
collCfg.hasParams = true;
|
|
}
|
|
collCfg.isWildcard = this.isWildcard(collCfg.path);
|
|
if (this.isNotRecursive(collCfg.path)) {
|
|
collCfg.isRecursive = false;
|
|
}
|
|
let listenersCollection = this.getCleanListenersCollection(Object.assign(Object.assign({}, collCfg), { match: this.getListenerCollectionMatch(collCfg.path, collCfg.isRecursive, collCfg.isWildcard) }));
|
|
this.id++;
|
|
listenersCollection.listeners.set(this.id, listener);
|
|
this.listeners.set(collCfg.path, listenersCollection);
|
|
return listenersCollection;
|
|
}
|
|
subscribe(listenerPath, fn, options = defaultListenerOptions, type = "subscribe") {
|
|
let listener = this.getCleanListener(fn, options);
|
|
const listenersCollection = this.getListenersCollection(listenerPath, listener);
|
|
listenersCollection.count++;
|
|
listenerPath = listenersCollection.path;
|
|
if (!listenersCollection.isWildcard) {
|
|
fn(this.pathGet(this.split(this.cleanNotRecursivePath(listenerPath)), this.data), {
|
|
type,
|
|
listener,
|
|
listenersCollection,
|
|
path: {
|
|
listener: listenerPath,
|
|
update: undefined,
|
|
resolved: this.cleanNotRecursivePath(listenerPath)
|
|
},
|
|
params: this.getParams(listenersCollection.paramsInfo, listenerPath),
|
|
options
|
|
});
|
|
}
|
|
else {
|
|
const paths = this.scan.get(this.cleanNotRecursivePath(listenerPath));
|
|
if (options.bulk) {
|
|
const bulkValue = [];
|
|
for (const path in paths) {
|
|
bulkValue.push({
|
|
path,
|
|
params: this.getParams(listenersCollection.paramsInfo, path),
|
|
value: paths[path]
|
|
});
|
|
}
|
|
fn(bulkValue, {
|
|
type,
|
|
listener,
|
|
listenersCollection,
|
|
path: {
|
|
listener: listenerPath,
|
|
update: undefined,
|
|
resolved: undefined
|
|
},
|
|
options,
|
|
params: undefined
|
|
});
|
|
}
|
|
else {
|
|
for (const path in paths) {
|
|
fn(paths[path], {
|
|
type,
|
|
listener,
|
|
listenersCollection,
|
|
path: {
|
|
listener: listenerPath,
|
|
update: undefined,
|
|
resolved: this.cleanNotRecursivePath(path)
|
|
},
|
|
params: this.getParams(listenersCollection.paramsInfo, path),
|
|
options
|
|
});
|
|
}
|
|
}
|
|
}
|
|
this.debugSubscribe(listener, listenersCollection, listenerPath);
|
|
return this.unsubscribe(listenerPath, this.id);
|
|
}
|
|
unsubscribe(path, id) {
|
|
const listeners = this.listeners;
|
|
const listenersCollection = listeners.get(path);
|
|
return function unsub() {
|
|
listenersCollection.listeners.delete(id);
|
|
listenersCollection.count--;
|
|
if (listenersCollection.count === 0) {
|
|
listeners.delete(path);
|
|
}
|
|
};
|
|
}
|
|
same(newValue, oldValue) {
|
|
return ((["number", "string", "undefined", "boolean"].includes(typeof newValue) || newValue === null) &&
|
|
oldValue === newValue);
|
|
}
|
|
notifyListeners(listeners, exclude = [], returnNotified = true) {
|
|
const alreadyNotified = [];
|
|
for (const path in listeners) {
|
|
let { single, bulk } = listeners[path];
|
|
for (const singleListener of single) {
|
|
if (exclude.includes(singleListener))
|
|
continue;
|
|
const time = this.debugTime(singleListener);
|
|
singleListener.listener.fn(singleListener.value(), singleListener.eventInfo);
|
|
if (returnNotified)
|
|
alreadyNotified.push(singleListener);
|
|
this.debugListener(time, singleListener);
|
|
}
|
|
for (const bulkListener of bulk) {
|
|
if (exclude.includes(bulkListener))
|
|
continue;
|
|
const time = this.debugTime(bulkListener);
|
|
const bulkValue = [];
|
|
for (const bulk of bulkListener.value) {
|
|
bulkValue.push(Object.assign(Object.assign({}, bulk), { value: bulk.value() }));
|
|
}
|
|
bulkListener.listener.fn(bulkValue, bulkListener.eventInfo);
|
|
if (returnNotified)
|
|
alreadyNotified.push(bulkListener);
|
|
this.debugListener(time, bulkListener);
|
|
}
|
|
}
|
|
return alreadyNotified;
|
|
}
|
|
getSubscribedListeners(updatePath, newValue, options, type = "update", originalPath = null) {
|
|
options = Object.assign(Object.assign({}, defaultUpdateOptions), options);
|
|
const listeners = {};
|
|
for (let [listenerPath, listenersCollection] of this.listeners) {
|
|
listeners[listenerPath] = { single: [], bulk: [], bulkData: [] };
|
|
if (listenersCollection.match(updatePath)) {
|
|
const params = listenersCollection.paramsInfo
|
|
? this.getParams(listenersCollection.paramsInfo, updatePath)
|
|
: undefined;
|
|
const value = listenersCollection.isRecursive || listenersCollection.isWildcard
|
|
? () => this.get(this.cutPath(updatePath, listenerPath))
|
|
: () => newValue;
|
|
const bulkValue = [{ value, path: updatePath, params }];
|
|
for (const listener of listenersCollection.listeners.values()) {
|
|
if (listener.options.bulk) {
|
|
listeners[listenerPath].bulk.push({
|
|
listener,
|
|
listenersCollection,
|
|
eventInfo: {
|
|
type,
|
|
listener,
|
|
path: {
|
|
listener: listenerPath,
|
|
update: originalPath ? originalPath : updatePath,
|
|
resolved: undefined
|
|
},
|
|
params,
|
|
options
|
|
},
|
|
value: bulkValue
|
|
});
|
|
}
|
|
else {
|
|
listeners[listenerPath].single.push({
|
|
listener,
|
|
listenersCollection,
|
|
eventInfo: {
|
|
type,
|
|
listener,
|
|
path: {
|
|
listener: listenerPath,
|
|
update: originalPath ? originalPath : updatePath,
|
|
resolved: this.cleanNotRecursivePath(updatePath)
|
|
},
|
|
params,
|
|
options
|
|
},
|
|
value
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return listeners;
|
|
}
|
|
notifySubscribedListeners(updatePath, newValue, options, type = "update", originalPath = null) {
|
|
return this.notifyListeners(this.getSubscribedListeners(updatePath, newValue, options, type, originalPath));
|
|
}
|
|
getNestedListeners(updatePath, newValue, options, type = "update", originalPath = null) {
|
|
const listeners = {};
|
|
for (let [listenerPath, listenersCollection] of this.listeners) {
|
|
listeners[listenerPath] = { single: [], bulk: [] };
|
|
const currentCuttedPath = this.cutPath(listenerPath, updatePath);
|
|
if (this.match(currentCuttedPath, updatePath)) {
|
|
const restPath = this.trimPath(listenerPath.substr(currentCuttedPath.length));
|
|
const values = new WildcardObject(newValue, this.options.delimeter, this.options.wildcard).get(restPath);
|
|
const params = listenersCollection.paramsInfo
|
|
? this.getParams(listenersCollection.paramsInfo, updatePath)
|
|
: undefined;
|
|
const bulk = [];
|
|
const bulkListeners = {};
|
|
for (const currentRestPath in values) {
|
|
const value = () => values[currentRestPath];
|
|
const fullPath = [updatePath, currentRestPath].join(this.options.delimeter);
|
|
for (const [listenerId, listener] of listenersCollection.listeners) {
|
|
const eventInfo = {
|
|
type,
|
|
listener,
|
|
listenersCollection,
|
|
path: {
|
|
listener: listenerPath,
|
|
update: originalPath ? originalPath : updatePath,
|
|
resolved: this.cleanNotRecursivePath(fullPath)
|
|
},
|
|
params,
|
|
options
|
|
};
|
|
if (listener.options.bulk) {
|
|
bulk.push({ value, path: fullPath, params });
|
|
bulkListeners[listenerId] = listener;
|
|
}
|
|
else {
|
|
listeners[listenerPath].single.push({
|
|
listener,
|
|
listenersCollection,
|
|
eventInfo,
|
|
value
|
|
});
|
|
}
|
|
}
|
|
}
|
|
for (const listenerId in bulkListeners) {
|
|
const listener = bulkListeners[listenerId];
|
|
const eventInfo = {
|
|
type,
|
|
listener,
|
|
listenersCollection,
|
|
path: {
|
|
listener: listenerPath,
|
|
update: updatePath,
|
|
resolved: undefined
|
|
},
|
|
options,
|
|
params
|
|
};
|
|
listeners[listenerPath].bulk.push({
|
|
listener,
|
|
listenersCollection,
|
|
eventInfo,
|
|
value: bulk
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return listeners;
|
|
}
|
|
notifyNestedListeners(updatePath, newValue, options, type = "update", alreadyNotified, originalPath = null) {
|
|
return this.notifyListeners(this.getNestedListeners(updatePath, newValue, options, type, originalPath), alreadyNotified, false);
|
|
}
|
|
getNotifyOnlyListeners(updatePath, newValue, options, type = "update", originalPath = null) {
|
|
const listeners = {};
|
|
if (typeof options.only !== "object" ||
|
|
!Array.isArray(options.only) ||
|
|
typeof options.only[0] === "undefined" ||
|
|
!this.canBeNested(newValue)) {
|
|
return listeners;
|
|
}
|
|
for (const notifyPath of options.only) {
|
|
const wildcardScan = new WildcardObject(newValue, this.options.delimeter, this.options.wildcard).get(notifyPath);
|
|
listeners[notifyPath] = { bulk: [], single: [] };
|
|
for (const wildcardPath in wildcardScan) {
|
|
const fullPath = updatePath + this.options.delimeter + wildcardPath;
|
|
for (const [listenerPath, listenersCollection] of this.listeners) {
|
|
const params = listenersCollection.paramsInfo
|
|
? this.getParams(listenersCollection.paramsInfo, fullPath)
|
|
: undefined;
|
|
if (this.match(listenerPath, fullPath)) {
|
|
const value = () => wildcardScan[wildcardPath];
|
|
const bulkValue = [{ value, path: fullPath, params }];
|
|
for (const listener of listenersCollection.listeners.values()) {
|
|
const eventInfo = {
|
|
type,
|
|
listener,
|
|
listenersCollection,
|
|
path: {
|
|
listener: listenerPath,
|
|
update: originalPath ? originalPath : updatePath,
|
|
resolved: this.cleanNotRecursivePath(fullPath)
|
|
},
|
|
params,
|
|
options
|
|
};
|
|
if (listener.options.bulk) {
|
|
if (!listeners[notifyPath].bulk.some(bulkListener => bulkListener.listener === listener)) {
|
|
listeners[notifyPath].bulk.push({
|
|
listener,
|
|
listenersCollection,
|
|
eventInfo,
|
|
value: bulkValue
|
|
});
|
|
}
|
|
}
|
|
else {
|
|
listeners[notifyPath].single.push({
|
|
listener,
|
|
listenersCollection,
|
|
eventInfo,
|
|
value
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return listeners;
|
|
}
|
|
notifyOnly(updatePath, newValue, options, type = "update", originalPath = "") {
|
|
return (typeof this.notifyListeners(this.getNotifyOnlyListeners(updatePath, newValue, options, type, originalPath))[0] !==
|
|
"undefined");
|
|
}
|
|
canBeNested(newValue) {
|
|
return typeof newValue === "object" && newValue !== null;
|
|
}
|
|
getUpdateValues(oldValue, split, fn) {
|
|
if (typeof oldValue === "object" && oldValue !== null) {
|
|
Array.isArray(oldValue) ? (oldValue = oldValue.slice()) : (oldValue = Object.assign({}, oldValue));
|
|
}
|
|
let newValue = fn;
|
|
if (typeof fn === "function") {
|
|
newValue = fn(this.pathGet(split, this.data));
|
|
}
|
|
return { newValue, oldValue };
|
|
}
|
|
wildcardUpdate(updatePath, fn, options = defaultUpdateOptions) {
|
|
options = Object.assign(Object.assign({}, defaultUpdateOptions), options);
|
|
const scanned = this.scan.get(updatePath);
|
|
const bulk = {};
|
|
for (const path in scanned) {
|
|
const split = this.split(path);
|
|
const { oldValue, newValue } = this.getUpdateValues(scanned[path], split, fn);
|
|
if (!this.same(newValue, oldValue))
|
|
bulk[path] = newValue;
|
|
}
|
|
const groupedListenersPack = [];
|
|
const waitingPaths = [];
|
|
for (const path in bulk) {
|
|
const newValue = bulk[path];
|
|
if (options.only.length) {
|
|
groupedListenersPack.push(this.getNotifyOnlyListeners(path, newValue, options, "update", updatePath));
|
|
}
|
|
else {
|
|
groupedListenersPack.push(this.getSubscribedListeners(path, newValue, options, "update", updatePath));
|
|
this.canBeNested(newValue) &&
|
|
groupedListenersPack.push(this.getNestedListeners(path, newValue, options, "update", updatePath));
|
|
}
|
|
options.debug && this.options.log("Wildcard update", { path, newValue });
|
|
this.pathSet(this.split(path), newValue, this.data);
|
|
waitingPaths.push(path);
|
|
}
|
|
let alreadyNotified = [];
|
|
for (const groupedListeners of groupedListenersPack) {
|
|
alreadyNotified = [...alreadyNotified, ...this.notifyListeners(groupedListeners, alreadyNotified)];
|
|
}
|
|
for (const path of waitingPaths) {
|
|
this.executeWaitingListeners(path);
|
|
}
|
|
}
|
|
update(updatePath, fn, options = defaultUpdateOptions) {
|
|
if (this.isWildcard(updatePath)) {
|
|
return this.wildcardUpdate(updatePath, fn, options);
|
|
}
|
|
const split = this.split(updatePath);
|
|
const { oldValue, newValue } = this.getUpdateValues(this.pathGet(split, this.data), split, fn);
|
|
if (options.debug) {
|
|
this.options.log(`Updating ${updatePath} ${options.source ? `from ${options.source}` : ""}`, {
|
|
oldValue,
|
|
newValue
|
|
});
|
|
}
|
|
if (this.same(newValue, oldValue)) {
|
|
return newValue;
|
|
}
|
|
if (!options.updateAfter) {
|
|
this.pathSet(split, newValue, this.data);
|
|
}
|
|
options = Object.assign(Object.assign({}, defaultUpdateOptions), options);
|
|
if (options.only === null) {
|
|
return newValue;
|
|
}
|
|
if (options.only.length) {
|
|
this.notifyOnly(updatePath, newValue, options);
|
|
this.executeWaitingListeners(updatePath);
|
|
return newValue;
|
|
}
|
|
const alreadyNotified = this.notifySubscribedListeners(updatePath, newValue, options);
|
|
if (this.canBeNested(newValue)) {
|
|
this.notifyNestedListeners(updatePath, newValue, options, "update", alreadyNotified);
|
|
}
|
|
this.executeWaitingListeners(updatePath);
|
|
if (options.updateAfter) {
|
|
this.pathSet(split, newValue, this.data);
|
|
}
|
|
return newValue;
|
|
}
|
|
get(userPath = undefined) {
|
|
if (typeof userPath === "undefined" || userPath === "") {
|
|
return this.data;
|
|
}
|
|
return this.pathGet(this.split(userPath), this.data);
|
|
}
|
|
debugSubscribe(listener, listenersCollection, listenerPath) {
|
|
if (listener.options.debug) {
|
|
this.options.log("listener subscribed", {
|
|
listenerPath,
|
|
listener,
|
|
listenersCollection
|
|
});
|
|
}
|
|
}
|
|
debugListener(time, groupedListener) {
|
|
if (groupedListener.eventInfo.options.debug || groupedListener.listener.options.debug) {
|
|
this.options.log("Listener fired", {
|
|
time: Date.now() - time,
|
|
info: groupedListener
|
|
});
|
|
}
|
|
}
|
|
debugTime(groupedListener) {
|
|
return groupedListener.listener.options.debug || groupedListener.eventInfo.options.debug ? Date.now() : 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedule - a throttle function that uses requestAnimationFrame to limit the rate at which a function is called.
|
|
*
|
|
* @param {function} fn
|
|
* @returns {function}
|
|
*/
|
|
/**
|
|
* Is object - helper function to determine if specified variable is an object
|
|
*
|
|
* @param {any} item
|
|
* @returns {boolean}
|
|
*/
|
|
function isObject$1(item) {
|
|
return item && typeof item === 'object' && !Array.isArray(item);
|
|
}
|
|
/**
|
|
* Merge deep - helper function which will merge objects recursively - creating brand new one - like clone
|
|
*
|
|
* @param {object} target
|
|
* @params {object} sources
|
|
* @returns {object}
|
|
*/
|
|
function mergeDeep$1(target, ...sources) {
|
|
const source = sources.shift();
|
|
if (isObject$1(target) && isObject$1(source)) {
|
|
for (const key in source) {
|
|
if (isObject$1(source[key])) {
|
|
if (typeof target[key] === 'undefined') {
|
|
target[key] = {};
|
|
}
|
|
target[key] = mergeDeep$1(target[key], source[key]);
|
|
}
|
|
else if (Array.isArray(source[key])) {
|
|
target[key] = [];
|
|
for (let item of source[key]) {
|
|
if (isObject$1(item)) {
|
|
target[key].push(mergeDeep$1({}, item));
|
|
continue;
|
|
}
|
|
target[key].push(item);
|
|
}
|
|
}
|
|
else {
|
|
target[key] = source[key];
|
|
}
|
|
}
|
|
}
|
|
if (!sources.length) {
|
|
return target;
|
|
}
|
|
return mergeDeep$1(target, ...sources);
|
|
}
|
|
|
|
/**
|
|
* Api functions
|
|
*
|
|
* @copyright Rafal Pospiech <https://neuronet.io>
|
|
* @author Rafal Pospiech <neuronet.io@gmail.com>
|
|
* @package gantt-schedule-timeline-calendar
|
|
* @license AGPL-3.0
|
|
*/
|
|
const lib = 'gantt-schedule-timeline-calendar';
|
|
function mergeActions(userConfig, defaultConfig) {
|
|
const defaultConfigActions = mergeDeep$1({}, defaultConfig.actions);
|
|
const userActions = mergeDeep$1({}, userConfig.actions);
|
|
let allActionNames = [...Object.keys(defaultConfigActions), ...Object.keys(userActions)];
|
|
allActionNames = allActionNames.filter(i => allActionNames.includes(i));
|
|
const actions = {};
|
|
for (const actionName of allActionNames) {
|
|
actions[actionName] = [];
|
|
if (typeof defaultConfigActions[actionName] !== 'undefined' && Array.isArray(defaultConfigActions[actionName])) {
|
|
actions[actionName] = [...defaultConfigActions[actionName]];
|
|
}
|
|
if (typeof userActions[actionName] !== 'undefined' && Array.isArray(userActions[actionName])) {
|
|
actions[actionName] = [...actions[actionName], ...userActions[actionName]];
|
|
}
|
|
}
|
|
delete userConfig.actions;
|
|
delete defaultConfig.actions;
|
|
return actions;
|
|
}
|
|
function stateFromConfig(userConfig) {
|
|
const defaultConfig$1 = defaultConfig();
|
|
const actions = mergeActions(userConfig, defaultConfig$1);
|
|
const state = { config: mergeDeep$1({}, defaultConfig$1, userConfig) };
|
|
state.config.actions = actions;
|
|
// @ts-ignore
|
|
return (this.state = new DeepState(state, { delimeter: '.' }));
|
|
}
|
|
const publicApi = {
|
|
name: lib,
|
|
stateFromConfig,
|
|
mergeDeep: mergeDeep$1,
|
|
date(time) {
|
|
return time ? dayjs_min(time) : dayjs_min();
|
|
},
|
|
setPeriod(period) {
|
|
this.state.update('config.chart.time.period', period);
|
|
return this.state.get('config.chart.time.zoom');
|
|
},
|
|
dayjs: dayjs_min
|
|
};
|
|
function getInternalApi(state) {
|
|
let $state = state.get();
|
|
let unsubscribes = [];
|
|
const iconsCache = {};
|
|
const api = {
|
|
name: lib,
|
|
debug: false,
|
|
setVido(Vido) {
|
|
},
|
|
log(...args) {
|
|
if (this.debug) {
|
|
console.log.call(console, ...args);
|
|
}
|
|
},
|
|
mergeDeep: mergeDeep$1,
|
|
getClass(name) {
|
|
let simple = `${lib}__${name}`;
|
|
if (name === this.name) {
|
|
simple = this.name;
|
|
}
|
|
return simple;
|
|
},
|
|
allActions: [],
|
|
getActions(name) {
|
|
if (!this.allActions.includes(name))
|
|
this.allActions.push(name);
|
|
let actions = state.get('config.actions.' + name);
|
|
if (typeof actions === 'undefined') {
|
|
actions = [];
|
|
}
|
|
return actions.slice();
|
|
},
|
|
isItemInViewport(item, left, right) {
|
|
return ((item.time.start >= left && item.time.start < right) ||
|
|
(item.time.end >= left && item.time.end < right) ||
|
|
(item.time.start <= left && item.time.end >= right));
|
|
},
|
|
prepareItems(items) {
|
|
for (const item of items) {
|
|
item.time.start = +item.time.start;
|
|
item.time.end = +item.time.end;
|
|
item.id = String(item.id);
|
|
}
|
|
return items;
|
|
},
|
|
fillEmptyRowValues(rows) {
|
|
let top = 0;
|
|
for (const rowId in rows) {
|
|
const row = rows[rowId];
|
|
row._internal = {
|
|
parents: [],
|
|
children: [],
|
|
items: []
|
|
};
|
|
if (typeof row.height !== 'number') {
|
|
row.height = $state.config.list.rowHeight;
|
|
}
|
|
if (typeof row.expanded !== 'boolean') {
|
|
row.expanded = false;
|
|
}
|
|
row.top = top;
|
|
top += row.height;
|
|
}
|
|
return rows;
|
|
},
|
|
generateParents(rows, parentName = 'parentId') {
|
|
const parents = {};
|
|
for (const row of rows) {
|
|
const parentId = row[parentName] !== undefined && row[parentName] !== null ? row[parentName] : '';
|
|
if (parents[parentId] === undefined) {
|
|
parents[parentId] = {};
|
|
}
|
|
parents[parentId][row.id] = row;
|
|
}
|
|
return parents;
|
|
},
|
|
fastTree(rowParents, node, parents = []) {
|
|
const children = rowParents[node.id];
|
|
node._internal.parents = parents;
|
|
if (typeof children === 'undefined') {
|
|
node._internal.children = [];
|
|
return node;
|
|
}
|
|
if (node.id !== '') {
|
|
parents = [...parents, node.id];
|
|
}
|
|
node._internal.children = Object.values(children);
|
|
for (const childrenId in children) {
|
|
const child = children[childrenId];
|
|
this.fastTree(rowParents, child, parents);
|
|
}
|
|
return node;
|
|
},
|
|
makeTreeMap(rows, items) {
|
|
const itemParents = this.generateParents(items, 'rowId');
|
|
for (const row of rows) {
|
|
row._internal.items = itemParents[row.id] !== undefined ? Object.values(itemParents[row.id]) : [];
|
|
}
|
|
const rowParents = this.generateParents(rows);
|
|
const tree = { id: '', _internal: { children: [], parents: [], items: [] } };
|
|
return this.fastTree(rowParents, tree);
|
|
},
|
|
getFlatTreeMapById(treeMap, flatTreeMapById = {}) {
|
|
for (const child of treeMap._internal.children) {
|
|
flatTreeMapById[child.id] = child;
|
|
this.getFlatTreeMapById(child, flatTreeMapById);
|
|
}
|
|
return flatTreeMapById;
|
|
},
|
|
flattenTreeMap(treeMap, rows = []) {
|
|
for (const child of treeMap._internal.children) {
|
|
rows.push(child.id);
|
|
this.flattenTreeMap(child, rows);
|
|
}
|
|
return rows;
|
|
},
|
|
getRowsFromMap(flatTreeMap, rows) {
|
|
return flatTreeMap.map(node => rows[node.id]);
|
|
},
|
|
getRowsFromIds(ids, rows) {
|
|
const result = [];
|
|
for (const id of ids) {
|
|
result.push(rows[id]);
|
|
}
|
|
return result;
|
|
},
|
|
getRowsWithParentsExpanded(flatTreeMap, flatTreeMapById, rows) {
|
|
if (!flatTreeMap ||
|
|
!flatTreeMapById ||
|
|
!rows ||
|
|
flatTreeMap.length === 0 ||
|
|
flatTreeMapById.length === 0 ||
|
|
Object.keys(rows).length === 0) {
|
|
return [];
|
|
}
|
|
const rowsWithParentsExpanded = [];
|
|
next: for (const rowId of flatTreeMap) {
|
|
for (const parentId of flatTreeMapById[rowId]._internal.parents) {
|
|
const parent = rows[parentId];
|
|
if (!parent || !parent.expanded) {
|
|
continue next;
|
|
}
|
|
}
|
|
rowsWithParentsExpanded.push(rowId);
|
|
}
|
|
return rowsWithParentsExpanded;
|
|
},
|
|
getRowsHeight(rows) {
|
|
let height = 0;
|
|
for (const row of rows) {
|
|
if (row)
|
|
height += row.height;
|
|
}
|
|
return height;
|
|
},
|
|
/**
|
|
* Get visible rows - get rows that are inside current viewport (height)
|
|
*
|
|
* @param {array} rowsWithParentsExpanded rows that have parent expanded- they are visible
|
|
*/
|
|
getVisibleRowsAndCompensation(rowsWithParentsExpanded) {
|
|
const visibleRows = [];
|
|
let currentRowsOffset = 0;
|
|
let rowOffset = 0;
|
|
const scrollTop = state.get('config.scroll.top');
|
|
const height = state.get('_internal.height');
|
|
let chartViewBottom = 0;
|
|
let compensation = 0;
|
|
for (const row of rowsWithParentsExpanded) {
|
|
if (row === undefined)
|
|
continue;
|
|
chartViewBottom = scrollTop + height;
|
|
if (currentRowsOffset + row.height >= scrollTop && currentRowsOffset <= chartViewBottom) {
|
|
row.top = rowOffset;
|
|
compensation = row.top + scrollTop - currentRowsOffset;
|
|
rowOffset += row.height;
|
|
visibleRows.push(row);
|
|
}
|
|
currentRowsOffset += row.height;
|
|
if (currentRowsOffset >= chartViewBottom) {
|
|
break;
|
|
}
|
|
}
|
|
return { visibleRows, compensation };
|
|
},
|
|
/**
|
|
* Normalize mouse wheel event to get proper scroll metrics
|
|
*
|
|
* @param {Event} event mouse wheel event
|
|
*/
|
|
normalizeMouseWheelEvent(event) {
|
|
// @ts-ignore
|
|
let x = event.deltaX || 0;
|
|
// @ts-ignore
|
|
let y = event.deltaY || 0;
|
|
// @ts-ignore
|
|
let z = event.deltaZ || 0;
|
|
// @ts-ignore
|
|
const mode = event.deltaMode;
|
|
const lineHeight = state.get('config.list.rowHeight');
|
|
let scale = 1;
|
|
switch (mode) {
|
|
case 1:
|
|
if (lineHeight) {
|
|
scale = lineHeight;
|
|
}
|
|
break;
|
|
case 2:
|
|
// @ts-ignore
|
|
scale = window.height;
|
|
break;
|
|
}
|
|
x *= scale;
|
|
y *= scale;
|
|
z *= scale;
|
|
return { x, y, z, event };
|
|
},
|
|
normalizePointerEvent(event) {
|
|
const result = { x: 0, y: 0, pageX: 0, pageY: 0, clientX: 0, clientY: 0, screenX: 0, screenY: 0 };
|
|
switch (event.type) {
|
|
case 'wheel':
|
|
const wheel = this.normalizeMouseWheelEvent(event);
|
|
result.x = wheel.x;
|
|
result.y = wheel.y;
|
|
result.pageX = result.x;
|
|
result.pageY = result.y;
|
|
result.screenX = result.x;
|
|
result.screenY = result.y;
|
|
result.clientX = result.x;
|
|
result.clientY = result.y;
|
|
break;
|
|
case 'touchstart':
|
|
case 'touchmove':
|
|
case 'touchend':
|
|
case 'touchcancel':
|
|
result.x = event.changedTouches[0].screenX;
|
|
result.y = event.changedTouches[0].screenY;
|
|
result.pageX = event.changedTouches[0].pageX;
|
|
result.pageY = event.changedTouches[0].pageY;
|
|
result.screenX = event.changedTouches[0].screenX;
|
|
result.screenY = event.changedTouches[0].screenY;
|
|
result.clientX = event.changedTouches[0].clientX;
|
|
result.clientY = event.changedTouches[0].clientY;
|
|
break;
|
|
default:
|
|
result.x = event.x;
|
|
result.y = event.y;
|
|
result.pageX = event.pageX;
|
|
result.pageY = event.pageY;
|
|
result.screenX = event.screenX;
|
|
result.screenY = event.screenY;
|
|
result.clientX = event.clientX;
|
|
result.clientY = event.clientY;
|
|
break;
|
|
}
|
|
return result;
|
|
},
|
|
limitScrollLeft(totalViewDurationPx, chartWidth, scrollLeft) {
|
|
const width = totalViewDurationPx - chartWidth;
|
|
if (scrollLeft < 0) {
|
|
scrollLeft = 0;
|
|
}
|
|
else if (scrollLeft > width) {
|
|
scrollLeft = width;
|
|
}
|
|
return Math.round(scrollLeft);
|
|
},
|
|
limitScrollTop(rowsHeight, internalHeight, scrollTop) {
|
|
const height = rowsHeight - internalHeight;
|
|
if (scrollTop < 0) {
|
|
scrollTop = 0;
|
|
}
|
|
else if (scrollTop > height) {
|
|
scrollTop = height;
|
|
}
|
|
return Math.round(scrollTop);
|
|
},
|
|
time: new TimeApi(state),
|
|
/**
|
|
* Get scrollbar height - compute it from element
|
|
*
|
|
* @returns {number}
|
|
*/
|
|
getScrollBarHeight(add = 0) {
|
|
const outer = document.createElement('div');
|
|
outer.style.visibility = 'hidden';
|
|
outer.style.height = '100px';
|
|
document.body.appendChild(outer);
|
|
const noScroll = outer.offsetHeight;
|
|
outer.style.msOverflowStyle = 'scrollbar';
|
|
outer.style.overflow = 'scroll';
|
|
const inner = document.createElement('div');
|
|
inner.style.height = '100%';
|
|
outer.appendChild(inner);
|
|
const withScroll = inner.offsetHeight;
|
|
outer.parentNode.removeChild(outer);
|
|
return noScroll - withScroll + add;
|
|
},
|
|
scrollToTime(toTime) {
|
|
const time = state.get('_internal.chart.time');
|
|
state.update('config.scroll', scroll => {
|
|
const chartWidth = state.get('_internal.chart.dimensions.width');
|
|
const halfTime = (chartWidth / 2) * time.timePerPixel;
|
|
const leftGlobal = toTime - halfTime - time.finalFrom;
|
|
scroll.left = this.limitScrollLeft(time.totalViewDurationPx, chartWidth, leftGlobal / time.timePerPixel);
|
|
return scroll;
|
|
});
|
|
},
|
|
/**
|
|
* Get grid blocks that are under specified rectangle
|
|
*
|
|
* @param {number} x beginging at chart-timeline bounding rect
|
|
* @param {number} y beginging at chart-timeline bounding rect
|
|
* @param {number} width
|
|
* @param {number} height
|
|
* @returns {array} array of {element, data}
|
|
*/
|
|
getGridBlocksUnderRect(x, y, width, height) {
|
|
const main = state.get('_internal.elements.main');
|
|
if (!main)
|
|
return [];
|
|
},
|
|
getCompensationX() {
|
|
return state.get('config.scroll.compensation.x') || 0;
|
|
},
|
|
getCompensationY() {
|
|
return state.get('config.scroll.compensation.y') || 0;
|
|
},
|
|
getSVGIconSrc(svg) {
|
|
if (typeof iconsCache[svg] === 'string')
|
|
return iconsCache[svg];
|
|
iconsCache[svg] = 'data:image/svg+xml;base64,' + btoa(svg);
|
|
return iconsCache[svg];
|
|
},
|
|
/**
|
|
* Destroy things to release memory
|
|
*/
|
|
destroy() {
|
|
$state = undefined;
|
|
for (const unsubscribe of unsubscribes) {
|
|
unsubscribe();
|
|
}
|
|
unsubscribes = [];
|
|
if (api.debug) {
|
|
// @ts-ignore
|
|
delete window.state;
|
|
}
|
|
}
|
|
};
|
|
if (api.debug) {
|
|
// @ts-ignore
|
|
window.state = state;
|
|
// @ts-ignore
|
|
window.api = api;
|
|
}
|
|
return api;
|
|
}
|
|
|
|
/**
|
|
* Gantt-Schedule-Timeline-Calendar
|
|
*
|
|
* @copyright Rafal Pospiech <https://neuronet.io>
|
|
* @author Rafal Pospiech <neuronet.io@gmail.com>
|
|
* @package gantt-schedule-timeline-calendar
|
|
* @license AGPL-3.0
|
|
*/
|
|
function GSTC(options) {
|
|
const state = options.state;
|
|
const api = getInternalApi(state);
|
|
const _internal = {
|
|
components: {
|
|
Main
|
|
},
|
|
scrollBarHeight: api.getScrollBarHeight(2),
|
|
height: 0,
|
|
treeMap: {},
|
|
flatTreeMap: [],
|
|
flatTreeMapById: {},
|
|
list: {
|
|
expandedHeight: 0,
|
|
visibleRows: [],
|
|
rows: {},
|
|
width: 0
|
|
},
|
|
dimensions: {
|
|
width: 0,
|
|
height: 0
|
|
},
|
|
chart: {
|
|
dimensions: {
|
|
width: 0,
|
|
innerWidth: 0
|
|
},
|
|
visibleItems: [],
|
|
time: {
|
|
levels: [],
|
|
timePerPixel: 0,
|
|
firstTaskTime: 0,
|
|
lastTaskTime: 0,
|
|
totalViewDurationMs: 0,
|
|
totalViewDurationPx: 0,
|
|
leftGlobal: 0,
|
|
rightGlobal: 0,
|
|
leftPx: 0,
|
|
rightPx: 0,
|
|
leftInner: 0,
|
|
rightInner: 0,
|
|
maxWidth: {}
|
|
}
|
|
},
|
|
elements: {},
|
|
cache: {
|
|
calendar: {}
|
|
},
|
|
loaded: {}
|
|
};
|
|
if (typeof options.debug === 'boolean' && options.debug) {
|
|
// @ts-ignore
|
|
window.state = state;
|
|
}
|
|
state.update('', oldValue => {
|
|
return {
|
|
config: oldValue.config,
|
|
_internal
|
|
};
|
|
});
|
|
// @ts-ignore
|
|
const vido = Vido(state, api);
|
|
api.setVido(vido);
|
|
const app = vido.createApp({ component: Main, props: {}, element: options.element });
|
|
const internalApi = app.vidoInstance.api;
|
|
return { state, app, api: internalApi };
|
|
}
|
|
GSTC.api = publicApi;
|
|
|
|
return GSTC;
|
|
|
|
})));
|
|
//# sourceMappingURL=index.umd.js.map
|