UNPKG

@jupyter/web-components

Version:

A component library for building extensions in Jupyter frontends.

1,565 lines (1,557 loc) 913 kB
/** * A reference to globalThis, with support * for browsers that don't yet support the spec. * @public */ const $global = function () { if (typeof globalThis !== "undefined") { // We're running in a modern environment. return globalThis; } if (typeof global !== "undefined") { // We're running in NodeJS return global; } if (typeof self !== "undefined") { // We're running in a worker. return self; } if (typeof window !== "undefined") { // We're running in the browser's main thread. return window; } try { // Hopefully we never get here... // Not all environments allow eval and Function. Use only as a last resort: // eslint-disable-next-line no-new-func return new Function("return this")(); } catch (_a) { // If all fails, give up and create an object. // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return {}; } }(); // API-only Polyfill for trustedTypes if ($global.trustedTypes === void 0) { $global.trustedTypes = { createPolicy: (n, r) => r }; } const propConfig = { configurable: false, enumerable: false, writable: false }; if ($global.FAST === void 0) { Reflect.defineProperty($global, "FAST", Object.assign({ value: Object.create(null) }, propConfig)); } /** * The FAST global. * @internal */ const FAST = $global.FAST; if (FAST.getById === void 0) { const storage = Object.create(null); Reflect.defineProperty(FAST, "getById", Object.assign({ value(id, initialize) { let found = storage[id]; if (found === void 0) { found = initialize ? storage[id] = initialize() : null; } return found; } }, propConfig)); } /** * A readonly, empty array. * @remarks * Typically returned by APIs that return arrays when there are * no actual items to return. * @internal */ const emptyArray = Object.freeze([]); /** * Creates a function capable of locating metadata associated with a type. * @returns A metadata locator function. * @internal */ function createMetadataLocator() { const metadataLookup = new WeakMap(); return function (target) { let metadata = metadataLookup.get(target); if (metadata === void 0) { let currentTarget = Reflect.getPrototypeOf(target); while (metadata === void 0 && currentTarget !== null) { metadata = metadataLookup.get(currentTarget); currentTarget = Reflect.getPrototypeOf(currentTarget); } metadata = metadata === void 0 ? [] : metadata.slice(0); metadataLookup.set(target, metadata); } return metadata; }; } const updateQueue = $global.FAST.getById(1 /* updateQueue */, () => { const tasks = []; const pendingErrors = []; function throwFirstError() { if (pendingErrors.length) { throw pendingErrors.shift(); } } function tryRunTask(task) { try { task.call(); } catch (error) { pendingErrors.push(error); setTimeout(throwFirstError, 0); } } function process() { const capacity = 1024; let index = 0; while (index < tasks.length) { tryRunTask(tasks[index]); index++; // Prevent leaking memory for long chains of recursive calls to `DOM.queueUpdate`. // If we call `DOM.queueUpdate` within a task scheduled by `DOM.queueUpdate`, the queue will // grow, but to avoid an O(n) walk for every task we execute, we don't // shift tasks off the queue after they have been executed. // Instead, we periodically shift 1024 tasks off the queue. if (index > capacity) { // Manually shift all values starting at the index back to the // beginning of the queue. for (let scan = 0, newLength = tasks.length - index; scan < newLength; scan++) { tasks[scan] = tasks[scan + index]; } tasks.length -= index; index = 0; } } tasks.length = 0; } function enqueue(callable) { if (tasks.length < 1) { $global.requestAnimationFrame(process); } tasks.push(callable); } return Object.freeze({ enqueue, process }); }); /* eslint-disable */ const fastHTMLPolicy = $global.trustedTypes.createPolicy("fast-html", { createHTML: html => html }); /* eslint-enable */ let htmlPolicy = fastHTMLPolicy; const marker = `fast-${Math.random().toString(36).substring(2, 8)}`; /** @internal */ const _interpolationStart = `${marker}{`; /** @internal */ const _interpolationEnd = `}${marker}`; /** * Common DOM APIs. * @public */ const DOM = Object.freeze({ /** * Indicates whether the DOM supports the adoptedStyleSheets feature. */ supportsAdoptedStyleSheets: Array.isArray(document.adoptedStyleSheets) && "replace" in CSSStyleSheet.prototype, /** * Sets the HTML trusted types policy used by the templating engine. * @param policy - The policy to set for HTML. * @remarks * This API can only be called once, for security reasons. It should be * called by the application developer at the start of their program. */ setHTMLPolicy(policy) { if (htmlPolicy !== fastHTMLPolicy) { throw new Error("The HTML policy can only be set once."); } htmlPolicy = policy; }, /** * Turns a string into trusted HTML using the configured trusted types policy. * @param html - The string to turn into trusted HTML. * @remarks * Used internally by the template engine when creating templates * and setting innerHTML. */ createHTML(html) { return htmlPolicy.createHTML(html); }, /** * Determines if the provided node is a template marker used by the runtime. * @param node - The node to test. */ isMarker(node) { return node && node.nodeType === 8 && node.data.startsWith(marker); }, /** * Given a marker node, extract the {@link HTMLDirective} index from the placeholder. * @param node - The marker node to extract the index from. */ extractDirectiveIndexFromMarker(node) { return parseInt(node.data.replace(`${marker}:`, "")); }, /** * Creates a placeholder string suitable for marking out a location *within* * an attribute value or HTML content. * @param index - The directive index to create the placeholder for. * @remarks * Used internally by binding directives. */ createInterpolationPlaceholder(index) { return `${_interpolationStart}${index}${_interpolationEnd}`; }, /** * Creates a placeholder that manifests itself as an attribute on an * element. * @param attributeName - The name of the custom attribute. * @param index - The directive index to create the placeholder for. * @remarks * Used internally by attribute directives such as `ref`, `slotted`, and `children`. */ createCustomAttributePlaceholder(attributeName, index) { return `${attributeName}="${this.createInterpolationPlaceholder(index)}"`; }, /** * Creates a placeholder that manifests itself as a marker within the DOM structure. * @param index - The directive index to create the placeholder for. * @remarks * Used internally by structural directives such as `repeat`. */ createBlockPlaceholder(index) { return `<!--${marker}:${index}-->`; }, /** * Schedules DOM update work in the next async batch. * @param callable - The callable function or object to queue. */ queueUpdate: updateQueue.enqueue, /** * Immediately processes all work previously scheduled * through queueUpdate. * @remarks * This also forces nextUpdate promises * to resolve. */ processUpdates: updateQueue.process, /** * Resolves with the next DOM update. */ nextUpdate() { return new Promise(updateQueue.enqueue); }, /** * Sets an attribute value on an element. * @param element - The element to set the attribute value on. * @param attributeName - The attribute name to set. * @param value - The value of the attribute to set. * @remarks * If the value is `null` or `undefined`, the attribute is removed, otherwise * it is set to the provided value using the standard `setAttribute` API. */ setAttribute(element, attributeName, value) { if (value === null || value === undefined) { element.removeAttribute(attributeName); } else { element.setAttribute(attributeName, value); } }, /** * Sets a boolean attribute value. * @param element - The element to set the boolean attribute value on. * @param attributeName - The attribute name to set. * @param value - The value of the attribute to set. * @remarks * If the value is true, the attribute is added; otherwise it is removed. */ setBooleanAttribute(element, attributeName, value) { value ? element.setAttribute(attributeName, "") : element.removeAttribute(attributeName); }, /** * Removes all the child nodes of the provided parent node. * @param parent - The node to remove the children from. */ removeChildNodes(parent) { for (let child = parent.firstChild; child !== null; child = parent.firstChild) { parent.removeChild(child); } }, /** * Creates a TreeWalker configured to walk a template fragment. * @param fragment - The fragment to walk. */ createTemplateWalker(fragment) { return document.createTreeWalker(fragment, 133, // element, text, comment null, false); } }); /** * An implementation of {@link Notifier} that efficiently keeps track of * subscribers interested in a specific change notification on an * observable source. * * @remarks * This set is optimized for the most common scenario of 1 or 2 subscribers. * With this in mind, it can store a subscriber in an internal field, allowing it to avoid Array#push operations. * If the set ever exceeds two subscribers, it upgrades to an array automatically. * @public */ class SubscriberSet { /** * Creates an instance of SubscriberSet for the specified source. * @param source - The object source that subscribers will receive notifications from. * @param initialSubscriber - An initial subscriber to changes. */ constructor(source, initialSubscriber) { this.sub1 = void 0; this.sub2 = void 0; this.spillover = void 0; this.source = source; this.sub1 = initialSubscriber; } /** * Checks whether the provided subscriber has been added to this set. * @param subscriber - The subscriber to test for inclusion in this set. */ has(subscriber) { return this.spillover === void 0 ? this.sub1 === subscriber || this.sub2 === subscriber : this.spillover.indexOf(subscriber) !== -1; } /** * Subscribes to notification of changes in an object's state. * @param subscriber - The object that is subscribing for change notification. */ subscribe(subscriber) { const spillover = this.spillover; if (spillover === void 0) { if (this.has(subscriber)) { return; } if (this.sub1 === void 0) { this.sub1 = subscriber; return; } if (this.sub2 === void 0) { this.sub2 = subscriber; return; } this.spillover = [this.sub1, this.sub2, subscriber]; this.sub1 = void 0; this.sub2 = void 0; } else { const index = spillover.indexOf(subscriber); if (index === -1) { spillover.push(subscriber); } } } /** * Unsubscribes from notification of changes in an object's state. * @param subscriber - The object that is unsubscribing from change notification. */ unsubscribe(subscriber) { const spillover = this.spillover; if (spillover === void 0) { if (this.sub1 === subscriber) { this.sub1 = void 0; } else if (this.sub2 === subscriber) { this.sub2 = void 0; } } else { const index = spillover.indexOf(subscriber); if (index !== -1) { spillover.splice(index, 1); } } } /** * Notifies all subscribers. * @param args - Data passed along to subscribers during notification. */ notify(args) { const spillover = this.spillover; const source = this.source; if (spillover === void 0) { const sub1 = this.sub1; const sub2 = this.sub2; if (sub1 !== void 0) { sub1.handleChange(source, args); } if (sub2 !== void 0) { sub2.handleChange(source, args); } } else { for (let i = 0, ii = spillover.length; i < ii; ++i) { spillover[i].handleChange(source, args); } } } } /** * An implementation of Notifier that allows subscribers to be notified * of individual property changes on an object. * @public */ class PropertyChangeNotifier { /** * Creates an instance of PropertyChangeNotifier for the specified source. * @param source - The object source that subscribers will receive notifications from. */ constructor(source) { this.subscribers = {}; this.sourceSubscribers = null; this.source = source; } /** * Notifies all subscribers, based on the specified property. * @param propertyName - The property name, passed along to subscribers during notification. */ notify(propertyName) { var _a; const subscribers = this.subscribers[propertyName]; if (subscribers !== void 0) { subscribers.notify(propertyName); } (_a = this.sourceSubscribers) === null || _a === void 0 ? void 0 : _a.notify(propertyName); } /** * Subscribes to notification of changes in an object's state. * @param subscriber - The object that is subscribing for change notification. * @param propertyToWatch - The name of the property that the subscriber is interested in watching for changes. */ subscribe(subscriber, propertyToWatch) { var _a; if (propertyToWatch) { let subscribers = this.subscribers[propertyToWatch]; if (subscribers === void 0) { this.subscribers[propertyToWatch] = subscribers = new SubscriberSet(this.source); } subscribers.subscribe(subscriber); } else { this.sourceSubscribers = (_a = this.sourceSubscribers) !== null && _a !== void 0 ? _a : new SubscriberSet(this.source); this.sourceSubscribers.subscribe(subscriber); } } /** * Unsubscribes from notification of changes in an object's state. * @param subscriber - The object that is unsubscribing from change notification. * @param propertyToUnwatch - The name of the property that the subscriber is no longer interested in watching. */ unsubscribe(subscriber, propertyToUnwatch) { var _a; if (propertyToUnwatch) { const subscribers = this.subscribers[propertyToUnwatch]; if (subscribers !== void 0) { subscribers.unsubscribe(subscriber); } } else { (_a = this.sourceSubscribers) === null || _a === void 0 ? void 0 : _a.unsubscribe(subscriber); } } } /** * Common Observable APIs. * @public */ const Observable = FAST.getById(2 /* observable */, () => { const volatileRegex = /(:|&&|\|\||if)/; const notifierLookup = new WeakMap(); const queueUpdate = DOM.queueUpdate; let watcher = void 0; let createArrayObserver = array => { throw new Error("Must call enableArrayObservation before observing arrays."); }; function getNotifier(source) { let found = source.$fastController || notifierLookup.get(source); if (found === void 0) { if (Array.isArray(source)) { found = createArrayObserver(source); } else { notifierLookup.set(source, found = new PropertyChangeNotifier(source)); } } return found; } const getAccessors = createMetadataLocator(); class DefaultObservableAccessor { constructor(name) { this.name = name; this.field = `_${name}`; this.callback = `${name}Changed`; } getValue(source) { if (watcher !== void 0) { watcher.watch(source, this.name); } return source[this.field]; } setValue(source, newValue) { const field = this.field; const oldValue = source[field]; if (oldValue !== newValue) { source[field] = newValue; const callback = source[this.callback]; if (typeof callback === "function") { callback.call(source, oldValue, newValue); } getNotifier(source).notify(this.name); } } } class BindingObserverImplementation extends SubscriberSet { constructor(binding, initialSubscriber, isVolatileBinding = false) { super(binding, initialSubscriber); this.binding = binding; this.isVolatileBinding = isVolatileBinding; this.needsRefresh = true; this.needsQueue = true; this.first = this; this.last = null; this.propertySource = void 0; this.propertyName = void 0; this.notifier = void 0; this.next = void 0; } observe(source, context) { if (this.needsRefresh && this.last !== null) { this.disconnect(); } const previousWatcher = watcher; watcher = this.needsRefresh ? this : void 0; this.needsRefresh = this.isVolatileBinding; const result = this.binding(source, context); watcher = previousWatcher; return result; } disconnect() { if (this.last !== null) { let current = this.first; while (current !== void 0) { current.notifier.unsubscribe(this, current.propertyName); current = current.next; } this.last = null; this.needsRefresh = this.needsQueue = true; } } watch(propertySource, propertyName) { const prev = this.last; const notifier = getNotifier(propertySource); const current = prev === null ? this.first : {}; current.propertySource = propertySource; current.propertyName = propertyName; current.notifier = notifier; notifier.subscribe(this, propertyName); if (prev !== null) { if (!this.needsRefresh) { // Declaring the variable prior to assignment below circumvents // a bug in Angular's optimization process causing infinite recursion // of this watch() method. Details https://github.com/microsoft/fast/issues/4969 let prevValue; watcher = void 0; /* eslint-disable-next-line */ prevValue = prev.propertySource[prev.propertyName]; /* eslint-disable-next-line @typescript-eslint/no-this-alias */ watcher = this; if (propertySource === prevValue) { this.needsRefresh = true; } } prev.next = current; } this.last = current; } handleChange() { if (this.needsQueue) { this.needsQueue = false; queueUpdate(this); } } call() { if (this.last !== null) { this.needsQueue = true; this.notify(this); } } records() { let next = this.first; return { next: () => { const current = next; if (current === undefined) { return { value: void 0, done: true }; } else { next = next.next; return { value: current, done: false }; } }, [Symbol.iterator]: function () { return this; } }; } } return Object.freeze({ /** * @internal * @param factory - The factory used to create array observers. */ setArrayObserverFactory(factory) { createArrayObserver = factory; }, /** * Gets a notifier for an object or Array. * @param source - The object or Array to get the notifier for. */ getNotifier, /** * Records a property change for a source object. * @param source - The object to record the change against. * @param propertyName - The property to track as changed. */ track(source, propertyName) { if (watcher !== void 0) { watcher.watch(source, propertyName); } }, /** * Notifies watchers that the currently executing property getter or function is volatile * with respect to its observable dependencies. */ trackVolatile() { if (watcher !== void 0) { watcher.needsRefresh = true; } }, /** * Notifies subscribers of a source object of changes. * @param source - the object to notify of changes. * @param args - The change args to pass to subscribers. */ notify(source, args) { getNotifier(source).notify(args); }, /** * Defines an observable property on an object or prototype. * @param target - The target object to define the observable on. * @param nameOrAccessor - The name of the property to define as observable; * or a custom accessor that specifies the property name and accessor implementation. */ defineProperty(target, nameOrAccessor) { if (typeof nameOrAccessor === "string") { nameOrAccessor = new DefaultObservableAccessor(nameOrAccessor); } getAccessors(target).push(nameOrAccessor); Reflect.defineProperty(target, nameOrAccessor.name, { enumerable: true, get: function () { return nameOrAccessor.getValue(this); }, set: function (newValue) { nameOrAccessor.setValue(this, newValue); } }); }, /** * Finds all the observable accessors defined on the target, * including its prototype chain. * @param target - The target object to search for accessor on. */ getAccessors, /** * Creates a {@link BindingObserver} that can watch the * provided {@link Binding} for changes. * @param binding - The binding to observe. * @param initialSubscriber - An initial subscriber to changes in the binding value. * @param isVolatileBinding - Indicates whether the binding's dependency list must be re-evaluated on every value evaluation. */ binding(binding, initialSubscriber, isVolatileBinding = this.isVolatileBinding(binding)) { return new BindingObserverImplementation(binding, initialSubscriber, isVolatileBinding); }, /** * Determines whether a binding expression is volatile and needs to have its dependency list re-evaluated * on every evaluation of the value. * @param binding - The binding to inspect. */ isVolatileBinding(binding) { return volatileRegex.test(binding.toString()); } }); }); /** * Decorator: Defines an observable property on the target. * @param target - The target to define the observable on. * @param nameOrAccessor - The property name or accessor to define the observable as. * @public */ function observable(target, nameOrAccessor) { Observable.defineProperty(target, nameOrAccessor); } /** * Decorator: Marks a property getter as having volatile observable dependencies. * @param target - The target that the property is defined on. * @param name - The property name. * @param name - The existing descriptor. * @public */ function volatile(target, name, descriptor) { return Object.assign({}, descriptor, { get: function () { Observable.trackVolatile(); return descriptor.get.apply(this); } }); } const contextEvent = FAST.getById(3 /* contextEvent */, () => { let current = null; return { get() { return current; }, set(event) { current = event; } }; }); /** * Provides additional contextual information available to behaviors and expressions. * @public */ class ExecutionContext { constructor() { /** * The index of the current item within a repeat context. */ this.index = 0; /** * The length of the current collection within a repeat context. */ this.length = 0; /** * The parent data object within a repeat context. */ this.parent = null; /** * The parent execution context when in nested context scenarios. */ this.parentContext = null; } /** * The current event within an event handler. */ get event() { return contextEvent.get(); } /** * Indicates whether the current item within a repeat context * has an even index. */ get isEven() { return this.index % 2 === 0; } /** * Indicates whether the current item within a repeat context * has an odd index. */ get isOdd() { return this.index % 2 !== 0; } /** * Indicates whether the current item within a repeat context * is the first item in the collection. */ get isFirst() { return this.index === 0; } /** * Indicates whether the current item within a repeat context * is somewhere in the middle of the collection. */ get isInMiddle() { return !this.isFirst && !this.isLast; } /** * Indicates whether the current item within a repeat context * is the last item in the collection. */ get isLast() { return this.index === this.length - 1; } /** * Sets the event for the current execution context. * @param event - The event to set. * @internal */ static setEvent(event) { contextEvent.set(event); } } Observable.defineProperty(ExecutionContext.prototype, "index"); Observable.defineProperty(ExecutionContext.prototype, "length"); /** * The default execution context used in binding expressions. * @public */ const defaultExecutionContext = Object.seal(new ExecutionContext()); /** * Instructs the template engine to apply behavior to a node. * @public */ class HTMLDirective { constructor() { /** * The index of the DOM node to which the created behavior will apply. */ this.targetIndex = 0; } } /** * A {@link HTMLDirective} that targets a named attribute or property on a node. * @public */ class TargetedHTMLDirective extends HTMLDirective { constructor() { super(...arguments); /** * Creates a placeholder string based on the directive's index within the template. * @param index - The index of the directive within the template. */ this.createPlaceholder = DOM.createInterpolationPlaceholder; } } /** * A directive that attaches special behavior to an element via a custom attribute. * @public */ class AttachedBehaviorHTMLDirective extends HTMLDirective { /** * * @param name - The name of the behavior; used as a custom attribute on the element. * @param behavior - The behavior to instantiate and attach to the element. * @param options - Options to pass to the behavior during creation. */ constructor(name, behavior, options) { super(); this.name = name; this.behavior = behavior; this.options = options; } /** * Creates a placeholder string based on the directive's index within the template. * @param index - The index of the directive within the template. * @remarks * Creates a custom attribute placeholder. */ createPlaceholder(index) { return DOM.createCustomAttributePlaceholder(this.name, index); } /** * Creates a behavior for the provided target node. * @param target - The node instance to create the behavior for. * @remarks * Creates an instance of the `behavior` type this directive was constructed with * and passes the target and options to that `behavior`'s constructor. */ createBehavior(target) { return new this.behavior(target, this.options); } } function normalBind(source, context) { this.source = source; this.context = context; if (this.bindingObserver === null) { this.bindingObserver = Observable.binding(this.binding, this, this.isBindingVolatile); } this.updateTarget(this.bindingObserver.observe(source, context)); } function triggerBind(source, context) { this.source = source; this.context = context; this.target.addEventListener(this.targetName, this); } function normalUnbind() { this.bindingObserver.disconnect(); this.source = null; this.context = null; } function contentUnbind() { this.bindingObserver.disconnect(); this.source = null; this.context = null; const view = this.target.$fastView; if (view !== void 0 && view.isComposed) { view.unbind(); view.needsBindOnly = true; } } function triggerUnbind() { this.target.removeEventListener(this.targetName, this); this.source = null; this.context = null; } function updateAttributeTarget(value) { DOM.setAttribute(this.target, this.targetName, value); } function updateBooleanAttributeTarget(value) { DOM.setBooleanAttribute(this.target, this.targetName, value); } function updateContentTarget(value) { // If there's no actual value, then this equates to the // empty string for the purposes of content bindings. if (value === null || value === undefined) { value = ""; } // If the value has a "create" method, then it's a template-like. if (value.create) { this.target.textContent = ""; let view = this.target.$fastView; // If there's no previous view that we might be able to // reuse then create a new view from the template. if (view === void 0) { view = value.create(); } else { // If there is a previous view, but it wasn't created // from the same template as the new value, then we // need to remove the old view if it's still in the DOM // and create a new view from the template. if (this.target.$fastTemplate !== value) { if (view.isComposed) { view.remove(); view.unbind(); } view = value.create(); } } // It's possible that the value is the same as the previous template // and that there's actually no need to compose it. if (!view.isComposed) { view.isComposed = true; view.bind(this.source, this.context); view.insertBefore(this.target); this.target.$fastView = view; this.target.$fastTemplate = value; } else if (view.needsBindOnly) { view.needsBindOnly = false; view.bind(this.source, this.context); } } else { const view = this.target.$fastView; // If there is a view and it's currently composed into // the DOM, then we need to remove it. if (view !== void 0 && view.isComposed) { view.isComposed = false; view.remove(); if (view.needsBindOnly) { view.needsBindOnly = false; } else { view.unbind(); } } this.target.textContent = value; } } function updatePropertyTarget(value) { this.target[this.targetName] = value; } function updateClassTarget(value) { const classVersions = this.classVersions || Object.create(null); const target = this.target; let version = this.version || 0; // Add the classes, tracking the version at which they were added. if (value !== null && value !== undefined && value.length) { const names = value.split(/\s+/); for (let i = 0, ii = names.length; i < ii; ++i) { const currentName = names[i]; if (currentName === "") { continue; } classVersions[currentName] = version; target.classList.add(currentName); } } this.classVersions = classVersions; this.version = version + 1; // If this is the first call to add classes, there's no need to remove old ones. if (version === 0) { return; } // Remove classes from the previous version. version -= 1; for (const name in classVersions) { if (classVersions[name] === version) { target.classList.remove(name); } } } /** * A directive that configures data binding to element content and attributes. * @public */ class HTMLBindingDirective extends TargetedHTMLDirective { /** * Creates an instance of BindingDirective. * @param binding - A binding that returns the data used to update the DOM. */ constructor(binding) { super(); this.binding = binding; this.bind = normalBind; this.unbind = normalUnbind; this.updateTarget = updateAttributeTarget; this.isBindingVolatile = Observable.isVolatileBinding(this.binding); } /** * Gets/sets the name of the attribute or property that this * binding is targeting. */ get targetName() { return this.originalTargetName; } set targetName(value) { this.originalTargetName = value; if (value === void 0) { return; } switch (value[0]) { case ":": this.cleanedTargetName = value.substr(1); this.updateTarget = updatePropertyTarget; if (this.cleanedTargetName === "innerHTML") { const binding = this.binding; this.binding = (s, c) => DOM.createHTML(binding(s, c)); } break; case "?": this.cleanedTargetName = value.substr(1); this.updateTarget = updateBooleanAttributeTarget; break; case "@": this.cleanedTargetName = value.substr(1); this.bind = triggerBind; this.unbind = triggerUnbind; break; default: this.cleanedTargetName = value; if (value === "class") { this.updateTarget = updateClassTarget; } break; } } /** * Makes this binding target the content of an element rather than * a particular attribute or property. */ targetAtContent() { this.updateTarget = updateContentTarget; this.unbind = contentUnbind; } /** * Creates the runtime BindingBehavior instance based on the configuration * information stored in the BindingDirective. * @param target - The target node that the binding behavior should attach to. */ createBehavior(target) { /* eslint-disable-next-line @typescript-eslint/no-use-before-define */ return new BindingBehavior(target, this.binding, this.isBindingVolatile, this.bind, this.unbind, this.updateTarget, this.cleanedTargetName); } } /** * A behavior that updates content and attributes based on a configured * BindingDirective. * @public */ class BindingBehavior { /** * Creates an instance of BindingBehavior. * @param target - The target of the data updates. * @param binding - The binding that returns the latest value for an update. * @param isBindingVolatile - Indicates whether the binding has volatile dependencies. * @param bind - The operation to perform during binding. * @param unbind - The operation to perform during unbinding. * @param updateTarget - The operation to perform when updating. * @param targetName - The name of the target attribute or property to update. */ constructor(target, binding, isBindingVolatile, bind, unbind, updateTarget, targetName) { /** @internal */ this.source = null; /** @internal */ this.context = null; /** @internal */ this.bindingObserver = null; this.target = target; this.binding = binding; this.isBindingVolatile = isBindingVolatile; this.bind = bind; this.unbind = unbind; this.updateTarget = updateTarget; this.targetName = targetName; } /** @internal */ handleChange() { this.updateTarget(this.bindingObserver.observe(this.source, this.context)); } /** @internal */ handleEvent(event) { ExecutionContext.setEvent(event); const result = this.binding(this.source, this.context); ExecutionContext.setEvent(null); if (result !== true) { event.preventDefault(); } } } let sharedContext = null; class CompilationContext { addFactory(factory) { factory.targetIndex = this.targetIndex; this.behaviorFactories.push(factory); } captureContentBinding(directive) { directive.targetAtContent(); this.addFactory(directive); } reset() { this.behaviorFactories = []; this.targetIndex = -1; } release() { /* eslint-disable-next-line @typescript-eslint/no-this-alias */ sharedContext = this; } static borrow(directives) { const shareable = sharedContext || new CompilationContext(); shareable.directives = directives; shareable.reset(); sharedContext = null; return shareable; } } function createAggregateBinding(parts) { if (parts.length === 1) { return parts[0]; } let targetName; const partCount = parts.length; const finalParts = parts.map(x => { if (typeof x === "string") { return () => x; } targetName = x.targetName || targetName; return x.binding; }); const binding = (scope, context) => { let output = ""; for (let i = 0; i < partCount; ++i) { output += finalParts[i](scope, context); } return output; }; const directive = new HTMLBindingDirective(binding); directive.targetName = targetName; return directive; } const interpolationEndLength = _interpolationEnd.length; function parseContent(context, value) { const valueParts = value.split(_interpolationStart); if (valueParts.length === 1) { return null; } const bindingParts = []; for (let i = 0, ii = valueParts.length; i < ii; ++i) { const current = valueParts[i]; const index = current.indexOf(_interpolationEnd); let literal; if (index === -1) { literal = current; } else { const directiveIndex = parseInt(current.substring(0, index)); bindingParts.push(context.directives[directiveIndex]); literal = current.substring(index + interpolationEndLength); } if (literal !== "") { bindingParts.push(literal); } } return bindingParts; } function compileAttributes(context, node, includeBasicValues = false) { const attributes = node.attributes; for (let i = 0, ii = attributes.length; i < ii; ++i) { const attr = attributes[i]; const attrValue = attr.value; const parseResult = parseContent(context, attrValue); let result = null; if (parseResult === null) { if (includeBasicValues) { result = new HTMLBindingDirective(() => attrValue); result.targetName = attr.name; } } else { result = createAggregateBinding(parseResult); } if (result !== null) { node.removeAttributeNode(attr); i--; ii--; context.addFactory(result); } } } function compileContent(context, node, walker) { const parseResult = parseContent(context, node.textContent); if (parseResult !== null) { let lastNode = node; for (let i = 0, ii = parseResult.length; i < ii; ++i) { const currentPart = parseResult[i]; const currentNode = i === 0 ? node : lastNode.parentNode.insertBefore(document.createTextNode(""), lastNode.nextSibling); if (typeof currentPart === "string") { currentNode.textContent = currentPart; } else { currentNode.textContent = " "; context.captureContentBinding(currentPart); } lastNode = currentNode; context.targetIndex++; if (currentNode !== node) { walker.nextNode(); } } context.targetIndex--; } } /** * Compiles a template and associated directives into a raw compilation * result which include a cloneable DocumentFragment and factories capable * of attaching runtime behavior to nodes within the fragment. * @param template - The template to compile. * @param directives - The directives referenced by the template. * @remarks * The template that is provided for compilation is altered in-place * and cannot be compiled again. If the original template must be preserved, * it is recommended that you clone the original and pass the clone to this API. * @public */ function compileTemplate(template, directives) { const fragment = template.content; // https://bugs.chromium.org/p/chromium/issues/detail?id=1111864 document.adoptNode(fragment); const context = CompilationContext.borrow(directives); compileAttributes(context, template, true); const hostBehaviorFactories = context.behaviorFactories; context.reset(); const walker = DOM.createTemplateWalker(fragment); let node; while (node = walker.nextNode()) { context.targetIndex++; switch (node.nodeType) { case 1: // element node compileAttributes(context, node); break; case 3: // text node compileContent(context, node, walker); break; case 8: // comment if (DOM.isMarker(node)) { context.addFactory(directives[DOM.extractDirectiveIndexFromMarker(node)]); } } } let targetOffset = 0; if ( // If the first node in a fragment is a marker, that means it's an unstable first node, // because something like a when, repeat, etc. could add nodes before the marker. // To mitigate this, we insert a stable first node. However, if we insert a node, // that will alter the result of the TreeWalker. So, we also need to offset the target index. DOM.isMarker(fragment.firstChild) || // Or if there is only one node and a directive, it means the template's content // is *only* the directive. In that case, HTMLView.dispose() misses any nodes inserted by // the directive. Inserting a new node ensures proper disposal of nodes added by the directive. fragment.childNodes.length === 1 && directives.length) { fragment.insertBefore(document.createComment(""), fragment.firstChild); targetOffset = -1; } const viewBehaviorFactories = context.behaviorFactories; context.release(); return { fragment, viewBehaviorFactories, hostBehaviorFactories, targetOffset }; } // A singleton Range instance used to efficiently remove ranges of DOM nodes. // See the implementation of HTMLView below for further details. const range = document.createRange(); /** * The standard View implementation, which also implements ElementView and SyntheticView. * @public */ class HTMLView { /** * Constructs an instance of HTMLView. * @param fragment - The html fragment that contains the nodes for this view. * @param behaviors - The behaviors to be applied to this view. */ constructor(fragment, behaviors) { this.fragment = fragment; this.behaviors = behaviors; /** * The data that the view is bound to. */ this.source = null; /** * The execution context the view is running within. */ this.context = null; this.firstChild = fragment.firstChild; this.lastChild = fragment.lastChild; } /** * Appends the view's DOM nodes to the referenced node. * @param node - The parent node to append the view's DOM nodes to. */ appendTo(node) { node.appendChild(this.fragment); } /** * Inserts the view's DOM nodes before the referenced node. * @param node - The node to insert the view's DOM before. */ insertBefore(node) { if (this.fragment.hasChildNodes()) { node.parentNode.insertBefore(this.fragment, node); } else { const end = this.lastChild; if (node.previousSibling === end) return; const parentNode = node.parentNode; let current = this.firstChild; let next; while (current !== end) { next = current.nextSibling; parentNode.insertBefore(current, node); current = next; } parentNode.insertBefore(end, node); } } /** * Removes the view's DOM nodes. * The nodes are not disposed and the view can later be re-inserted. */ remove() { const fragment = this.fragment; const end = this.lastChild; let current = this.firstChild; let next; while (current !== end) { next = current.nextSibling; fragment.appendChild(current); current = next; } fragment.appendChild(end); } /** * Removes the view and unbinds its behaviors, disposing of DOM nodes afterward. * Once a view has been disposed, it cannot be inserted or bound again. */ dispose() { const parent = this.firstChild.parentNode; const end = this.lastChild; let current = this.firstChild; let next; while (current !== end) { next = current.nextSibling; parent.removeChild(current); current = next; } parent.removeChild(end); const behaviors = this.behaviors; const oldSource = this.source; for (let i = 0, ii = behaviors.length; i < ii; ++i) { behaviors[i].unbind(oldSource); } } /** * Binds a view's behaviors to its binding source. * @param source - The binding source for the view's binding behaviors. * @param context - The execution context to run the behaviors within. */ bind(source, context) { const behaviors = this.behaviors; if (this.source === source) { return; } else if (this.source !== null) { const oldSource = this.source; this.source = source; this.context = context; for (let i = 0, ii = behaviors.length; i < ii; ++i) { const current = behaviors[i]; current.unbind(oldSource); current.bind(source, context); } } else { this.source = source; this.context = context; for (let i = 0, ii = behaviors.length; i < ii; ++i) { behaviors[i].bind(source, context); } } } /** * Unbinds a view's behaviors from its binding source. */ unbind() { if (this.source === null) { return; } const behaviors = this.behaviors; const oldSource = this.source; for (let i = 0, ii = behaviors.length; i < ii; ++i) { behaviors[i].unbind(oldSource); } this.source = null; } /** * Efficiently disposes of a contiguous range of synthetic view instances. * @param views - A contiguous range of views to be disposed. */ static disposeContiguousBatch(views) { if (views.length === 0) { return; } range.setStartBefore(views[0].firstChild); range.setEndAfter(views[views.length - 1].lastChild); range.deleteContents(); for (let i = 0, ii = views.length; i < ii; ++i) { const view = views[i]; const behaviors = view.behaviors; const oldSource = view.source; for (let j = 0, jj = behaviors.length; j < jj; ++j) { behaviors[j].unbind(oldSource); } } } } /** * A template capable of creating HTMLView instances or rendering directly to DOM. * @public */ /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ class ViewTemplate { /** * Creates an instance of ViewTemplate. * @param html - The html representing what this template will instantiate, including placeholders for directives. * @param directives - The directives that will be connected to placeholders in the html. */ constructor(html, directives) { this.behaviorCount = 0; this.hasHostBehaviors = false; this.fragment = null; this.targetOffset = 0; this.viewBehaviorFactories = null; this.hostBehaviorFactories = null; this.html = html; this.directives = directives; } /** * Creates an HTMLView instance based on this template definition. * @param hostBindingTarget - The element that host behaviors will be bound to. */ create(hostBindingTarget) { if (this.fragment === null) { let template; const html = this.html; if (typeof html === "string") { template = document.createElement("template"); template.innerHTML = DOM.createHTML(html); const fec = template.content.firstElementChild; if (fec !== null && fec.tagName === "TEMPLATE") { template = fec; } } else { template = html; } const result = compileTemplate(template, this.directives); this.fragment = result.fragment; this.viewBehaviorFactories = result.viewBehaviorFactories; this.hostBehaviorFactories = result.hostBehaviorFactories; this.targetOffset = result.targetOffset; this.behaviorCount = this.viewBehaviorFactories.length + this.hostBehaviorFactories.length; this.hasHostBehaviors = this.hostBehaviorFactories.length > 0; } const fragment = this.fragment.cloneNode(true); const viewFactories = this.viewBehaviorFactories; const behaviors = new Array(this.behaviorCount); const walker = DOM.createTemplateWalker(fragment); let behaviorIndex = 0; let targetIndex = this.targetOffset; let node = walker.nextNode(); for (let ii = viewFactories.length; behaviorIndex < ii; ++behaviorIndex) { const factory = viewFactories[behaviorIndex]; const factoryIndex = factory.targetIndex; while (node !== null) { if (targetIndex === factoryIndex) { behaviors[behaviorIndex] = factory.createBehavior(node); break; } else { node = walker.nextNode(); targetIndex++; } } } if (this.hasHostBehaviors) { const hostFactories = this.hostBehaviorFactories; for (let i = 0, ii = hostFactories.length; i < ii; ++i, ++behaviorIndex) { behaviors[behaviorIndex] = hostFactories[i].createBehavior(hostBindingTarget); } } return new HTMLView(fragment, behaviors); } /** * Creates an HTMLView from this template, binds it to the source, and then appends it to the host. * @param source - The data source to bind the template to. * @param host - The Element where the template will be rendered. * @param hostBindingTarget - An HTML element to target the host bindings at if different from the * host that the template is being attached to. */ render(source, host, hostBindingTarget) { if (typeof host === "string") { host = document.getElementById(host); } if (hostBindingTarget === void 0) { hostBindingTarget = host; } const view = this.create(hostBindingTarget); view.bind(source, defaultExecutionContext); view.appendTo(host); return view; } } // Much thanks to LitHTML for working this out! 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"'`<>=]*|"[^"]*|'[^']*))$/; /** * Transforms a template literal string into a renderable ViewTemplate. * @param strings - The string fragments that are interpolated with the values. * @param values - The values that are interpolated with the string fragments. * @remarks * The html he