@jupyter/web-components
Version:
A component library for building extensions in Jupyter frontends.
1,565 lines (1,557 loc) • 913 kB
JavaScript
/**
* 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