Join our webinar on Wednesday, June 26, at 1pm EDTHow Chia Mitigates Risk in the Crypto Industry.Register
Socket
Socket
Sign inDemoInstall

@endorphinjs/template-runtime

Package Overview
Dependencies
0
Maintainers
1
Versions
85
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.1.27 to 0.3.0

dist/animation.d.ts

3501

dist/runtime.cjs.js

@@ -6,123 +6,87 @@ 'use strict';

/**
* Creates linted list
* @return {LinkedList}
* Creates element with given tag name
* @param cssScope Scope for CSS isolation
*/
function createList() {
return { head: null };
function elem(tagName, cssScope) {
const el = document.createElement(tagName);
cssScope && el.setAttribute(cssScope, '');
return el;
}
/**
* Creates linked list item
* @template T
* @param {T} value
* @returns {LinkedListItem<T>}
* Creates element with given tag name under `ns` namespace
* @param cssScope Scope for CSS isolation
*/
function createListItem(value) {
return { value, next: null, prev: null };
function elemNS(tagName, ns, cssScope) {
const el = document.createElementNS(ns, tagName);
cssScope && el.setAttribute(cssScope, '');
return el;
}
/**
* Prepends given value to linked list
* @template T
* @param {LinkedList} list
* @param {T} value
* @return {LinkedListItem<T>}
* Creates element with given tag name and text
* @param cssScope Scope for CSS isolation
*/
function listPrependValue(list, value) {
const item = createListItem(value);
if (item.next = list.head) {
item.next.prev = item;
}
return list.head = item;
function elemWithText(tagName, value, cssScope) {
const el = elem(tagName, cssScope);
el.textContent = textValue(value);
return el;
}
/**
* Inserts given value after given `ref` item
* @template T
* @param {T} value
* @param {LinkedListItem<any>} ref
* @return {LinkedListItem<T>}
* Creates element with given tag name under `ns` namespace and text
* @param cssScope Scope for CSS isolation
*/
function listInsertValueAfter(value, ref) {
const item = createListItem(value);
const { next } = ref;
ref.next = item;
item.prev = ref;
if (item.next = next) {
next.prev = item;
}
return item;
function elemNSWithText(tagName, ns, value, cssScope) {
const el = elemNS(tagName, ns, cssScope);
el.textContent = textValue(value);
return el;
}
/**
* Moves list fragment with `start` and `end` bounds right after `ref` item
* @param {LinkedList} list
* @param {LinkedListItem} start
* @param {LinkedListItem} end
* @param {LinkedListItem} ref
* Creates text node with given value
*/
function listMoveFragmentAfter(list, start, end, ref) {
listDetachFragment(list, start, end);
if (end.next = ref.next) {
end.next.prev = end;
}
ref.next = start;
start.prev = ref;
function text(value) {
const node = document.createTextNode(textValue(value));
node.$value = value;
return node;
}
/**
* Moves list fragment with `start` and `end` to list head
* @param {LinkedList} list
* @param {LinkedListItem} start
* @param {LinkedListItem} end
* Updates given text node value, if required
* @returns Returns `1` if text was updated, `0` otherwise
*/
function listMoveFragmentFirst(list, start, end) {
listDetachFragment(list, start, end);
if (end.next = list.head) {
end.next.prev = end;
}
list.head = start;
function updateText(node, value) {
if (value !== node.$value) {
node.nodeValue = textValue(value);
node.$value = value;
return 1;
}
return 0;
}
/**
* Detaches list fragment with `start` and `end` from list
* @param {LinkedList} list
* @param {LinkedListItem} start
* @param {LinkedListItem} end
* @returns Inserted item
*/
function listDetachFragment(list, start, end) {
const { prev } = start;
const { next } = end;
if (prev) {
prev.next = next;
} else {
list.head = next;
}
if (next) {
next.prev = prev;
}
start.prev = end.next = null;
function domInsert(node, parent, anchor) {
return anchor
? parent.insertBefore(node, anchor)
: parent.appendChild(node);
}
/**
* Removes given DOM node from its tree
* @param {Node} node
*/
function domRemove(node) {
const { parentNode } = node;
parentNode && parentNode.removeChild(node);
}
/**
* Returns textual representation of given `value` object
*/
function textValue(value) {
return value != null ? value : '';
}
const animatingKey = '$$animating';
/**
* Creates fast object
* @param {Object} [proto]
* @returns {Object}
*/
function obj(proto = null) {
return Object.create(proto);
return Object.create(proto);
}
/**

@@ -134,5 +98,4 @@ * Check if given value id defined, e.g. not `null`, `undefined` or `NaN`

function isDefined(value) {
return value != null && value === value;
return value != null && value === value;
}
/**

@@ -146,106 +109,86 @@ * Finalizes updated items, defined in `items.prev` and `items.cur`

function finalizeItems(items, change, ctx) {
let updated = 0;
const { cur, prev } = items;
for (const name in cur) {
const curValue = cur[name], prevValue = prev[name];
if (curValue !== prevValue) {
updated = 1;
change(name, prevValue, prev[name] = curValue, ctx);
}
cur[name] = null;
}
return updated;
let updated = 0;
const { cur, prev } = items;
for (const name in cur) {
const curValue = cur[name];
const prevValue = prev[name];
if (curValue !== prevValue) {
updated = 1;
change(name, prevValue, prev[name] = curValue, ctx);
}
cur[name] = null;
}
return updated;
}
/**
* Creates object for storing change sets, e.g. current and previous values
* @returns {ChangeSet}
*/
function changeSet() {
return { prev: obj(), cur: obj() };
return { prev: obj(), cur: obj() };
}
/**
* Returns properties from `next` which were changed since `prev` state.
* Returns `null` if there are no changes
* @param {Object} next
* @param {Object} prev
* @return {Changes}
*/
function changed(next, prev, prefix = '') {
/** @type {Changes} */
const result = obj();
let dirty = false;
// Check if data was actually changed
for (const p in next) {
if (prev[p] !== next[p]) {
dirty = true;
result[prefix ? prefix + p : p] = {
prev: prev[p],
current: next[p]
};
}
}
return dirty ? result : null;
const result = obj();
let dirty = false;
// Check if data was actually changed
for (const p in next) {
if (prev[p] !== next[p]) {
dirty = true;
result[prefix ? prefix + p : p] = {
prev: prev[p],
current: next[p]
};
}
}
return dirty ? result : null;
}
/**
* Moves contents of given `from` element into `to` element
* @param {Element | DocumentFragment} from
* @param {Element} to
* @returns {Element} The `to` element
* @returns The `to` element
*/
function moveContents(from, to) {
if (from !== to) {
if (from.nodeType === from.DOCUMENT_FRAGMENT_NODE) {
to.appendChild(from);
} else {
let node;
while (node = from.firstChild) {
to.appendChild(node);
}
}
}
return to;
if (from !== to) {
if (from.nodeType === from.DOCUMENT_FRAGMENT_NODE) {
to.appendChild(from);
}
else {
let node;
while (node = from.firstChild) {
to.appendChild(node);
}
}
}
return to;
}
const assign = Object.assign || function(target) {
for (let i = 1, source; i < arguments.length; i++) {
source = arguments[i];
for (let p in source) {
if (source.hasOwnProperty(p)) {
target[p] = source[p];
}
}
}
return target;
// tslint:disable-next-line:only-arrow-functions
const assign = Object.assign || function (target) {
for (let i = 1, source; i < arguments.length; i++) {
source = arguments[i];
for (const p in source) {
if (source.hasOwnProperty(p)) {
target[p] = source[p];
}
}
}
return target;
};
/**
* Returns property descriptors from given object
* @param {Object} obj
* @return {Object}
*/
const getObjectDescriptors = Object.getOwnPropertyDescriptors || function(source) {
const descriptors = obj();
const props = Object.getOwnPropertyNames(source);
for (let i = 0, prop, descriptor; i < props.length; i++) {
prop = props[i];
descriptor = Object.getOwnPropertyDescriptor(source, prop);
if (descriptor != null) {
descriptors[prop] = descriptor;
}
}
return descriptors;
// tslint:disable-next-line:only-arrow-functions
const getObjectDescriptors = Object['getOwnPropertyDescriptors'] || function (source) {
const descriptors = obj();
const props = Object.getOwnPropertyNames(source);
for (let i = 0, prop, descriptor; i < props.length; i++) {
prop = props[i];
descriptor = Object.getOwnPropertyDescriptor(source, prop);
if (descriptor != null) {
descriptors[prop] = descriptor;
}
}
return descriptors;
};
/**

@@ -258,1790 +201,1309 @@ * Represents given attribute value in element

function representAttributeValue(elem, name, value) {
const type = typeof(value);
if (type === 'boolean') {
value = value ? '' : null;
} else if (type === 'function') {
value = '𝑓';
} else if (Array.isArray(value)) {
value = '[]';
} else if (isDefined(value) && type === 'object') {
value = '{}';
}
isDefined(value) ? elem.setAttribute(name, value) : elem.removeAttribute(name);
const type = typeof (value);
if (type === 'boolean') {
value = value ? '' : null;
}
else if (type === 'function') {
value = '𝑓';
}
else if (Array.isArray(value)) {
value = '[]';
}
else if (isDefined(value) && type === 'object') {
value = '{}';
}
isDefined(value) ? elem.setAttribute(name, value) : elem.removeAttribute(name);
}
/**
* Marks given item as explicitly disposable for given host
* @param {Component | Injector} host
* @param {DisposeCallback} callback
* @return {Component | Injector}
*/
function addDisposeCallback(host, callback) {
if (/** @type {Component} */ (host).componentModel) {
/** @type {Component} */ (host).componentModel.dispose = callback;
} else {
/** @type {Injector} */ (host).ctx.dispose = callback;
}
return host;
if ('componentModel' in host) {
host.componentModel.dispose = callback;
}
else if (host.ctx) {
host.ctx.dispose = callback;
}
return host;
}
function safeCall(fn, arg1, arg2) {
try {
return fn && fn(arg1, arg2);
}
catch (err) {
// tslint:disable-next-line:no-console
console.error(err);
}
}
/**
* @param {Function} fn
* @param {*} [arg1]
* @param {*} [arg2]
* Registers given event listener on `target` element and returns event binding
* object to unregister event
*/
function safeCall(fn, arg1, arg2) {
try {
return fn && fn(arg1, arg2);
} catch (err) {
console.error(err);
}
function addStaticEvent(target, type, handleEvent, host, scope) {
return registerBinding({ host, scope, type, handleEvent, target });
}
/**
* Creates element with given tag name
* @param {string} tagName
* @param {string} [cssScope] Scope for CSS isolation
* @return {Element}
* Unregister given event binding
*/
function elem(tagName, cssScope) {
const el = document.createElement(tagName);
cssScope && el.setAttribute(cssScope, '');
return el;
function removeStaticEvent(binding) {
binding.target.removeEventListener(binding.type, binding);
}
/**
* Creates element with given tag name under `ns` namespace
* @param {string} tagName
* @param {string} ns
* @param {string} [cssScope] Scope for CSS isolation
* @return {Element}
* Adds pending event `name` handler
*/
function elemNS(tagName, ns, cssScope) {
const el = document.createElementNS(ns, tagName);
cssScope && el.setAttribute(cssScope, '');
return el;
function addEvent(injector, type, handleEvent, host, scope) {
// We’ll use `ChangeSet` to bind and unbind events only: once binding is registered,
// we will mutate binding props
const { prev, cur } = injector.events;
const binding = cur[type] || prev[type];
if (binding) {
binding.scope = scope;
binding.handleEvent = handleEvent;
cur[type] = binding;
}
else {
cur[type] = { host, scope, type, handleEvent, target: injector.parentNode };
}
}
/**
* Creates element with given tag name and text
* @param {string} tagName
* @param {string} text
* @param {string} [cssScope] Scope for CSS isolation
* @return {Element}
* Finalizes events of given injector
*/
function elemWithText(tagName, text, cssScope) {
const el = elem(tagName, cssScope);
el.textContent = textValue(text);
return el;
function finalizeEvents(injector) {
return finalizeItems(injector.events, changeEvent, injector.parentNode);
}
function registerBinding(binding) {
binding.target.addEventListener(binding.type, binding);
return binding;
}
/**
* Creates element with given tag name under `ns` namespace and text
* @param {string} tagName
* @param {string} ns
* @param {string} text
* @param {string} [cssScope] Scope for CSS isolation
* @return {Element}
* Invoked when event handler was changed
*/
function elemNSWithText(tagName, ns, text, cssScope) {
const el = elemNS(tagName, ns, cssScope);
el.textContent = textValue(text);
return el;
function changeEvent(name, prevValue, newValue) {
if (!prevValue && newValue) {
// Should register new binding
registerBinding(newValue);
}
else if (prevValue && !newValue) {
removeStaticEvent(prevValue);
}
}
/**
* Creates text node with given value
* @param {String} value
* @returns {Text}
* Sets value of attribute `name` to `value`
* @return Update status. Always returns `0` since actual attribute value
* is defined in `finalizeAttributes()`
*/
function text(value) {
const node = document.createTextNode(textValue(value));
node['$value'] = value;
return node;
function setAttribute(injector, name, value) {
injector.attributes.cur[name] = value;
return 0;
}
/**
* Updates given text node value, if required
* @param {Text} node
* @param {*} value
* @returns {number} Returns `1` if text was updated, `0` otherwise
* Sets value of attribute `name` under namespace of `nsURI` to `value`
*/
function updateText(node, value) {
if (value !== node['$value']) {
node.nodeValue = textValue(value);
node['$value'] = value;
return 1;
}
return 0;
function setAttributeNS(injector, nsURI, name, value) {
if (!injector.attributesNS) {
injector.attributesNS = obj();
}
const { attributesNS } = injector;
if (!attributesNS[nsURI]) {
attributesNS[nsURI] = changeSet();
}
attributesNS[nsURI].cur[name] = value;
}
/**
* @param {Node} node
* @param {Node} parent
* @param {Node} anchor
* @returns {Node} Inserted item
* Updates `attrName` value in `elem`, if required
* @returns New attribute value
*/
function domInsert(node, parent, anchor) {
return anchor
? parent.insertBefore(node, anchor)
: parent.appendChild(node);
function updateAttribute(elem, attrName, value, prevValue) {
if (value !== prevValue) {
changeAttribute(attrName, prevValue, value, elem);
return value;
}
return prevValue;
}
/**
* Removes given DOM node from its tree
* @param {Node} node
* Updates props in given component, if required
* @return Returns `true` if value was updated
*/
function domRemove(node) {
const { parentNode } = node;
parentNode && parentNode.removeChild(node);
function updateProps(elem, data) {
const { props } = elem;
let updated;
for (const p in data) {
if (data.hasOwnProperty(p) && props[p] !== data[p]) {
if (!updated) {
updated = obj();
}
updated[p] = data[p];
}
}
if (updated) {
elem.setProps(data);
return true;
}
return false;
}
/**
* Adds given class name as pending attribute
*/
function addClass(injector, value) {
if (isDefined(value)) {
const className = injector.attributes.cur.class;
setAttribute(injector, 'class', isDefined(className) ? className + ' ' + value : value);
}
}
/**
* Applies pending attributes changes to injector’s host element
*/
function finalizeAttributes(injector) {
const { attributes, attributesNS } = injector;
if (isDefined(attributes.cur.class)) {
attributes.cur.class = normalizeClassName(attributes.cur.class);
}
let updated = finalizeItems(attributes, changeAttribute, injector.parentNode);
if (attributesNS) {
const ctx = { node: injector.parentNode, ns: null };
for (const ns in attributesNS) {
ctx.ns = ns;
updated |= finalizeItems(attributesNS[ns], changeAttributeNS, ctx);
}
}
return updated;
}
/**
* Normalizes given class value: removes duplicates and trims whitespace
*/
function normalizeClassName(str) {
const out = [];
const parts = String(str).split(/\s+/);
for (let i = 0, cl; i < parts.length; i++) {
cl = parts[i];
if (cl && out.indexOf(cl) === -1) {
out.push(cl);
}
}
return out.join(' ');
}
/**
* Callback for changing attribute value
*/
function changeAttribute(name, prevValue, newValue, elem) {
if (isDefined(newValue)) {
representAttributeValue(elem, name, newValue);
}
else if (isDefined(prevValue)) {
elem.removeAttribute(name);
}
}
/**
* Callback for changing attribute value
*/
function changeAttributeNS(name, prevValue, newValue, ctx) {
if (isDefined(newValue)) {
ctx.node.setAttributeNS(ctx.ns, name, newValue);
}
else if (isDefined(prevValue)) {
ctx.node.removeAttributeNS(ctx.ns, name);
}
}
/**
* Returns textual representation of given `value` object
* @param {*} value
* @returns {string}
* Creates linted list
*/
function textValue(value) {
return value != null ? value : '';
function createList() {
return { head: null };
}
/**
* Creates linked list item
*/
function createListItem(value) {
return { value, next: null, prev: null };
}
/**
* Prepends given value to linked list
*/
function listPrependValue(list, value) {
const item = createListItem(value);
if (item.next = list.head) {
item.next.prev = item;
}
return list.head = item;
}
/**
* Inserts given value after given `ref` item
*/
function listInsertValueAfter(value, ref) {
const item = createListItem(value);
const { next } = ref;
ref.next = item;
item.prev = ref;
if (item.next = next) {
next.prev = item;
}
return item;
}
/**
* Moves list fragment with `start` and `end` bounds right after `ref` item
*/
function listMoveFragmentAfter(list, start, end, ref) {
listDetachFragment(list, start, end);
if (end.next = ref.next) {
end.next.prev = end;
}
ref.next = start;
start.prev = ref;
}
/**
* Moves list fragment with `start` and `end` to list head
*/
function listMoveFragmentFirst(list, start, end) {
listDetachFragment(list, start, end);
if (end.next = list.head) {
end.next.prev = end;
}
list.head = start;
}
/**
* Detaches list fragment with `start` and `end` from list
*/
function listDetachFragment(list, start, end) {
const { prev } = start;
const { next } = end;
if (prev) {
prev.next = next;
}
else {
list.head = next;
}
if (next) {
next.prev = prev;
}
start.prev = end.next = null;
}
/**
* Creates injector instance for given target, if required
* @param {Element} target
* @returns {Injector}
*/
function createInjector(target) {
return {
parentNode: target,
items: createList(),
ctx: null,
ptr: null,
// NB create `slots` placeholder to promote object to hidden class.
// Do not use any additional function argument for adding value to `slots`
// to reduce runtime checks and keep functions in monomorphic state
slots: null,
attributes: changeSet(),
events: changeSet()
};
return {
parentNode: target,
items: createList(),
ctx: null,
ptr: null,
// NB create `slots` placeholder to promote object to hidden class.
// Do not use any additional function argument for adding value to `slots`
// to reduce runtime checks and keep functions in monomorphic state
slots: null,
attributes: changeSet(),
events: changeSet()
};
}
/**
* Inserts given node into current context
* @param {Injector} injector
* @param {Node} node
* @returns {Node}
*/
function insert(injector, node, slotName = '') {
let target;
const { items, slots, ptr } = injector;
if (slots) {
target = slots[slotName] || (slots[slotName] = document.createDocumentFragment());
} else {
target = injector.parentNode;
}
domInsert(node, target, ptr && getAnchorNode(ptr.next, target));
injector.ptr = ptr ? listInsertValueAfter(node, ptr) : listPrependValue(items, node);
return node;
let target;
const { items, slots, ptr } = injector;
if (slots) {
target = slots[slotName] || (slots[slotName] = document.createDocumentFragment());
}
else {
target = injector.parentNode;
}
domInsert(node, target, ptr ? getAnchorNode(ptr.next, target) : void 0);
injector.ptr = ptr ? listInsertValueAfter(node, ptr) : listPrependValue(items, node);
return node;
}
/**
* Injects given block
* @template {BaseBlock} T
* @param {Injector} injector
* @param {T} block
* @returns {T}
*/
function injectBlock(injector, block) {
const { items, ptr } = injector;
if (ptr) {
block.end = listInsertValueAfter(block, ptr);
block.start = listInsertValueAfter(block, ptr);
} else {
block.end = listPrependValue(items, block);
block.start = listPrependValue(items, block);
}
injector.ptr = block.end;
return block;
const { items, ptr } = injector;
if (ptr) {
block.end = listInsertValueAfter(block, ptr);
block.start = listInsertValueAfter(block, ptr);
}
else {
block.end = listPrependValue(items, block);
block.start = listPrependValue(items, block);
}
block.$$block = true;
injector.ptr = block.end;
return block;
}
/**
* Runs `fn` template function in context of given `block`
* @param {BaseBlock} block
* @param {Function} fn
* @param {*} data
* @returns {*} Result of `fn` function call
*/
function run(block, fn, data) {
const { host, injector } = block;
const { ctx } = injector;
injector.ctx = block;
injector.ptr = block.start;
const result = fn(host, injector, data);
injector.ptr = block.end;
injector.ctx = ctx;
return result;
const { host, injector } = block;
const { ctx } = injector;
injector.ctx = block;
injector.ptr = block.start;
const result = fn(host, injector, data);
injector.ptr = block.end;
injector.ctx = ctx;
return result;
}
/**
* Empties content of given block
* @param {BaseBlock} block
*/
function emptyBlockContent(block) {
if (block.dispose) {
block.dispose(block.scope);
block.dispose = null;
}
let item = block.start.next;
while (item && item !== block.end) {
let { value, next, prev } = item;
if (isBlock(value)) {
next = value.end.next;
disposeBlock(value);
} else if (!value[animatingKey]) {
domRemove(value);
}
prev.next = next;
next.prev = prev;
item = next;
}
if (block.dispose) {
block.dispose(block.scope);
block.dispose = null;
}
let item = block.start.next;
while (item && item !== block.end) {
// tslint:disable-next-line:prefer-const
let { value, next, prev } = item;
if (isBlock(value)) {
next = value.end.next;
disposeBlock(value);
}
else if (!value[animatingKey]) {
domRemove(value);
}
// NB: Block always contains `.next` and `.prev` items which are block
// bounds so we can safely skip null check here
prev.next = next;
next.prev = prev;
item = next;
}
}
/**
* Moves contents of `block` after `ref` list item
* @param {Injector} injector
* @param {BaseBlock} block
* @param {LinkedListItem<any>} [ref]
*/
function move(injector, block, ref) {
if (ref && ref.next && ref.next.value === block) {
return;
}
// Update linked list
const { start, end } = block;
if (ref) {
listMoveFragmentAfter(injector.items, start, end, ref);
} else {
listMoveFragmentFirst(injector.items, start, end);
}
// Move block contents in DOM
let item = start.next, node;
while (item !== end) {
if (!isBlock(item.value)) {
/** @type {Node} */
node = item.value;
// NB it’s possible that a single block contains nodes from different
// slots so we have to find anchor for each node individually
domInsert(node, node.parentNode, getAnchorNode(end.next, node.parentNode));
}
item = item.next;
}
if (ref && ref.next && ref.next.value === block) {
return;
}
// Update linked list
const { start, end } = block;
if (ref) {
listMoveFragmentAfter(injector.items, start, end, ref);
}
else {
listMoveFragmentFirst(injector.items, start, end);
}
// Move block contents in DOM
let item = start.next;
let node;
while (item && item !== end) {
if (!isBlock(item.value)) {
node = item.value;
// NB it’s possible that a single block contains nodes from different
// slots so we have to find anchor for each node individually
domInsert(node, node.parentNode, getAnchorNode(end.next, node.parentNode));
}
item = item.next;
}
}
/**
* Disposes given block
* @param {BaseBlock} block
*/
function disposeBlock(block) {
emptyBlockContent(block);
listDetachFragment(block.injector.items, block.start, block.end);
block.start = block.end = null;
emptyBlockContent(block);
listDetachFragment(block.injector.items, block.start, block.end);
// @ts-ignore: Nulling disposed object
block.start = block.end = null;
}
/**
* Check if given value is a block
* @param {*} obj
* @returns {boolean}
*/
function isBlock(obj$$1) {
return '$$block' in obj$$1;
function isBlock(obj) {
return '$$block' in obj;
}
/**
* Get DOM node nearest to given position of items list
* @param {LinkedListItem} item
* @param {Node} parent Ensure element has given element as parent node
* @returns {Node}
*/
function getAnchorNode(item, parent) {
while (item) {
if (item.value.parentNode === parent) {
return item.value;
}
while (item) {
if (item.value.parentNode === parent) {
return item.value;
}
item = item.next;
}
}
item = item.next;
}
/**
* Walks over each definition (including given one) and runs callback on it
*/
function walkDefinitions(definition, fn) {
safeCall(fn, definition);
const { plugins } = definition;
if (plugins) {
for (let i = 0; i < plugins.length; i++) {
walkDefinitions(plugins[i], fn);
}
}
}
/**
* Same as `walkDefinitions` but runs in reverse order
*/
function reverseWalkDefinitions(definition, fn) {
const { plugins } = definition;
if (plugins) {
let i = plugins.length;
while (i--) {
walkDefinitions(plugins[i], fn);
}
}
safeCall(fn, definition);
}
/**
* Invokes `name` hook for given component definition
*/
function runHook(elem, name, arg1, arg2) {
walkDefinitions(elem.componentModel.definition, dfn => {
const hook = dfn[name];
if (typeof hook === 'function') {
hook(elem, arg1, arg2);
}
});
}
/**
* Enters new variable scope context
* @param {Component} host
* @param {object} incoming
* @return {Object}
*/
function enterScope(host, incoming) {
return setScope(host, createScope(host, incoming));
return setScope(host, createScope(host, incoming));
}
/**
* Exit from current variable scope
* @param {Component} host
* @returns {Object}
*/
function exitScope(host) {
return setScope(host, Object.getPrototypeOf(host.componentModel.vars));
return setScope(host, Object.getPrototypeOf(host.componentModel.vars));
}
/**
* Creates new scope from given component state
* @param {Component} host
* @param {Object} [incoming]
* @return {Object}
*/
function createScope(host, incoming) {
return assign(obj(host.componentModel.vars), incoming);
return assign(obj(host.componentModel.vars), incoming);
}
/**
* Sets given object as current component scope
* @param {Component} host
* @param {Object} scope
* @returns {Object}
*/
function setScope(host, scope) {
return host.componentModel.vars = scope;
return host.componentModel.vars = scope;
}
/**
* Returns current variable scope
* @param {Component} elem
* @returns {object}
*/
function getScope(elem) {
return elem.componentModel.vars;
return elem.componentModel.vars;
}
/**
* Returns property with given name from component
* @param {Component} elem
* @param {string} name
* @return {*}
*/
function getProp(elem, name) {
return elem.props[name];
return elem.props[name];
}
/**
* Returns state value with given name from component
* @param {Component} elem
* @param {string} name
* @return {*}
*/
function getState(elem, name) {
return elem.state[name];
return elem.state[name];
}
/**
* Returns value of given runtime variable from component
* @param {Component} elem
* @param {string} name
* @returns {*}
*/
function getVar(elem, name) {
return elem.componentModel.vars[name];
return elem.componentModel.vars[name];
}
/**
* Sets value of given runtime variable for component
* @param {Component} elem
* @param {string} name
* @param {*} value
*/
function setVar(elem, name, value) {
elem.componentModel.vars[name] = value;
elem.componentModel.vars[name] = value;
}
/**
* @param {Component} host
* @param {Injector} injector
* @param {Function} get
* @returns {FunctionBlock}
*/
function mountBlock(host, injector, get) {
/** @type {FunctionBlock} */
const block = injectBlock(injector, {
$$block: true,
host,
injector,
scope: getScope(host),
dispose: null,
get,
fn: undefined,
update: undefined,
start: null,
end: null
});
updateBlock(block);
return block;
const block = injectBlock(injector, {
host,
injector,
scope: getScope(host),
dispose: null,
get,
fn: undefined,
update: undefined
});
updateBlock(block);
return block;
}
/**
* Updated block, described in `ctx` object
* @param {FunctionBlock} block
* @returns {number} Returns `1` if block was updated, `0` otherwise
* @returns Returns `1` if block was updated, `0` otherwise
*/
function updateBlock(block) {
let updated = 0;
const { scope } = block;
const fn = block.get(block.host, scope);
if (block.fn !== fn) {
updated = 1;
// Unmount previously rendered content
block.fn && emptyBlockContent(block);
// Mount new block content
block.update = fn && run(block, fn, scope);
block.fn = fn;
} else if (block.update) {
// Update rendered result
updated = run(block, block.update, scope) ? 1 : 0;
}
block.injector.ptr = block.end;
return updated;
let updated = 0;
const { scope } = block;
const fn = block.get(block.host, scope);
if (block.fn !== fn) {
updated = 1;
// Unmount previously rendered content
block.fn && emptyBlockContent(block);
// Mount new block content
block.update = fn && run(block, fn, scope);
block.fn = fn;
}
else if (block.update) {
// Update rendered result
updated = run(block, block.update, scope) ? 1 : 0;
}
block.injector.ptr = block.end;
return updated;
}
/**
* @param {FunctionBlock} block
*/
function unmountBlock(block) {
disposeBlock(block);
disposeBlock(block);
}
/**
* Mounts iterator block
* @param {Component} host
* @param {Injector} injector
* @param {Function} get A function that returns collection to iterate
* @param {Function} body A function that renders item of iterated collection
* @returns {IteratorBlock}
* Registers given element as output slot for `host` component
* @param defaultContent Function for rendering default slot content
*/
function mountIterator(host, injector, get, body) {
/** @type {IteratorBlock} */
const block = injectBlock(injector, {
$$block: true,
host,
injector,
scope: getScope(host),
dispose: null,
get,
body,
index: 0,
updated: 0,
start: null,
end: null
});
updateIterator(block);
return block;
function mountSlot(host, name, elem, defaultContent) {
const ctx = { host, name, defaultContent, isDefault: false };
const { slots } = host.componentModel;
const injector = createInjector(elem);
const blockEntry = () => {
ctx.isDefault = !renderSlot(host, injector);
return ctx.isDefault ? ctx.defaultContent : void 0;
};
slots[name] = mountBlock(host, injector, blockEntry);
return ctx;
}
/**
* Updates iterator block defined in `ctx`
* @param {IteratorBlock} block
* @returns {number} Returns `1` if iterator was updated, `0` otherwise
* Unmounts given slot
* @param {SlotContext} ctx
*/
function updateIterator(block) {
run(block, iteratorHost, block);
return block.updated;
function unmountSlot(ctx) {
const { host, name } = ctx;
const { slots } = host.componentModel;
if (ctx.isDefault) {
unmountBlock(slots[name]);
}
ctx.defaultContent = void 0;
delete slots[name];
}
/**
* @param {IteratorBlock} block
* Sync slot content if necessary
*/
function unmountIterator(block) {
disposeBlock(block);
function updateSlots(host) {
const { slots, slotStatus, input } = host.componentModel;
for (const name in slots) {
updateBlock(slots[name]);
}
for (const name in slotStatus) {
if (slotStatus[name]) {
runHook(host, 'didSlotUpdate', name, input.slots[name]);
slotStatus[name] = 0;
}
}
}
/**
*
* @param {Component} host
* @param {Injector} injector
* @param {IteratorBlock} block
* Renders incoming contents of given slot
* @returns Returns `true` if slot content was filled with incoming data,
* `false` otherwise
*/
function iteratorHost(host, injector, block) {
block.index = 0;
block.updated = 0;
const collection = block.get(host, block.scope);
if (collection && typeof collection.forEach === 'function') {
collection.forEach(iterator, block);
}
trimIteratorItems(block);
function renderSlot(host, target) {
const { parentNode } = target;
const name = parentNode.getAttribute('name') || '';
const slotted = parentNode.hasAttribute('slotted');
const { input } = host.componentModel;
const source = input.slots[name];
if (source && source.childNodes.length) {
// There’s incoming slot content
if (!slotted) {
parentNode.setAttribute('slotted', '');
input.slots[name] = moveContents(source, parentNode);
}
return true;
}
if (slotted) {
// Parent renderer removed incoming data
parentNode.removeAttribute('slotted');
input[name] = null;
}
return false;
}
/**
* @param {*} scope
* @param {number} index
* @param {*} key
* @param {*} value
* Marks slot update status
*/
function prepareScope(scope, index, key, value) {
scope.index = index;
scope.key = key;
scope.value = value;
return scope;
function markSlotUpdate(component, slotName, status) {
const { slotStatus } = component.componentModel;
if (slotName in slotStatus) {
slotStatus[slotName] |= status;
}
else {
slotStatus[slotName] = status;
}
}
let renderQueue = null;
/**
* Removes remaining iterator items from current context
* @param {IteratorBlock} block
* Creates internal lightweight Endorphin component with given definition
*/
function trimIteratorItems(block) {
/** @type {LinkedListItem<IteratorItemBlock>} */
let item = block.injector.ptr.next, listItem;
while (item.value.owner === block) {
block.updated = 1;
listItem = item.value;
item = listItem.end.next;
disposeBlock(listItem);
}
function createComponent(name, definition, host) {
let cssScope;
let root;
if (host && 'componentModel' in host) {
cssScope = host.componentModel.definition.cssScope;
root = host.root || host;
}
const element = elem(name, cssScope);
// Add host scope marker: we can’t rely on tag name since component
// definition is bound to element in runtime, not compile time
if (definition.cssScope) {
element.setAttribute(definition.cssScope + '-host', '');
}
const { props, state, extend, events } = prepare(element, definition);
element.refs = {};
element.props = obj(props);
element.state = state;
element.componentView = element; // XXX Should point to Shadow Root in Web Components
root && (element.root = root);
addPropsState(element);
if (extend) {
Object.defineProperties(element, extend);
}
if (definition.store) {
element.store = definition.store();
}
else if (root && root.store) {
element.store = root.store;
}
// Create slotted input
const input = createInjector(element.componentView);
input.slots = obj();
element.componentModel = {
definition,
input,
vars: obj(),
refs: changeSet(),
slots: obj(),
slotStatus: obj(),
mounted: false,
rendering: false,
finalizing: false,
update: void 0,
queued: false,
events,
dispose: void 0,
defaultProps: props
};
runHook(element, 'init');
return element;
}
/**
* @this {IteratorBlock}
* @param {*} value
* @param {*} key
* Mounts given component
*/
function iterator(value, key) {
const { host, injector, index } = this;
const { ptr } = injector;
const prevScope = getScope(host);
/** @type {IteratorItemBlock} */
let rendered = ptr.next.value;
if (rendered.owner === this) {
// We have rendered item, update it
if (rendered.update) {
const scope = prepareScope(rendered.scope, index, key, value);
setScope(host, scope);
if (run(rendered, rendered.update, scope)) {
this.updated = 1;
}
setScope(host, prevScope);
}
} else {
// Create & render new block
const scope = prepareScope(obj(prevScope), index, key, value);
/** @type {IteratorItemBlock} */
rendered = injectBlock(injector, {
$$block: true,
host,
injector,
scope,
dispose: null,
update: undefined,
owner: this,
start: null,
end: null
});
setScope(host, scope);
rendered.update = run(rendered, this.body, scope);
setScope(host, prevScope);
this.updated = 1;
}
injector.ptr = rendered.end;
this.index++;
function mountComponent(component, initialProps) {
const { componentModel } = component;
const { input, definition, defaultProps } = componentModel;
let changes = setPropsInternal(component, obj(), assign(obj(defaultProps), initialProps));
const runtimeChanges = setPropsInternal(component, input.attributes.prev, input.attributes.cur);
if (changes && runtimeChanges) {
assign(changes, runtimeChanges);
}
else if (runtimeChanges) {
changes = runtimeChanges;
}
const arg = changes || {};
finalizeEvents(input);
componentModel.rendering = true;
// Notify slot status
for (const p in input.slots) {
runHook(component, 'didSlotUpdate', p, input.slots[p]);
}
if (changes) {
runHook(component, 'didChange', arg);
}
runHook(component, 'willMount', arg);
runHook(component, 'willRender', arg);
componentModel.update = safeCall(definition.default, component, getScope(component));
componentModel.mounted = true;
componentModel.rendering = false;
componentModel.finalizing = true;
runHook(component, 'didRender', arg);
runHook(component, 'didMount', arg);
componentModel.finalizing = false;
}
/**
* Renders key iterator block
* @param {Component} host
* @param {Injector} injector
* @param {Function} get
* @param {Function} keyExpr
* @param {Function} body
* @returns {KeyIteratorBlock}
* Updates given mounted component
*/
function mountKeyIterator(host, injector, get, keyExpr, body) {
const parentScope = getScope(host);
/** @type {KeyIteratorBlock} */
const block = injectBlock(injector, {
$$block: true,
host,
injector,
scope: obj(parentScope),
dispose: null,
get,
body,
keyExpr,
index: 0,
updated: 0,
rendered: null,
needReorder: false,
parentScope,
order: [],
used: null,
start: null,
end: null
});
updateKeyIterator(block);
return block;
function updateComponent(component) {
const { input } = component.componentModel;
const changes = setPropsInternal(component, input.attributes.prev, input.attributes.cur);
finalizeEvents(input);
updateSlots(component);
if (changes || component.componentModel.queued) {
renderNext(component, changes);
}
}
/**
* Updates iterator block defined in `ctx`
* @param {KeyIteratorBlock} block
* @returns {number} Returns `1` if iterator was updated, `0` otherwise
* Destroys given component: removes static event listeners and cleans things up
* @returns Should return nothing since function result will be used
* as shorthand to reset cached value
*/
function updateKeyIterator(block) {
run(block, keyIteratorHost, block);
return block.updated;
function unmountComponent(component) {
const { componentModel } = component;
const { slots, dispose, events } = componentModel;
const scope = getScope(component);
runHook(component, 'willUnmount');
componentModel.mounted = false;
if (events) {
detachStaticEvents(component, events);
}
if (component.store) {
component.store.unwatch(component);
}
safeCall(dispose, scope);
for (const slotName in slots) {
disposeBlock(slots[slotName]);
}
runHook(component, 'didUnmount');
// @ts-ignore: Nulling disposed object
component.componentModel = null;
}
/**
* @param {KeyIteratorBlock} ctx
* Subscribes to store updates of given component
*/
function unmountKeyIterator(ctx) {
disposeBlock(ctx);
function subscribeStore(component, keys) {
if (!component.store) {
throw new Error(`Store is not defined for ${component.nodeName} component`);
}
component.store.watch(component, keys);
}
/**
*
* @param {Component} host
* @param {Injector} injector
* @param {KeyIteratorBlock} block
* Queues next component render
*/
function keyIteratorHost(host, injector, block) {
block.used = obj();
block.index = 0;
block.updated = 0;
block.needReorder = false;
const collection = block.get(host, block.parentScope);
if (collection && typeof collection.forEach === 'function') {
collection.forEach(iterator$1, block);
}
const { rendered } = block;
for (let p in rendered) {
for (let i = 0, items = rendered[p]; i < items.length; i++) {
block.updated = 1;
disposeBlock(items[i]);
}
}
if (block.needReorder) {
reorder(block);
}
block.order.length = 0;
block.rendered = block.used;
function renderNext(component, changes) {
if (!component.componentModel.rendering) {
renderComponent(component, changes);
}
else {
scheduleRender(component, changes);
}
}
/**
* @param {IteratorItemBlock} expected
* @param {KeyIteratorBlock} owner
* @returns {IteratorItemBlock | null}
* Schedules render of given component on next tick
*/
function getItem(expected, owner) {
return expected.owner === owner ? expected : null;
function scheduleRender(component, changes) {
if (!component.componentModel.queued) {
component.componentModel.queued = true;
if (renderQueue) {
renderQueue.push(component, changes);
}
else {
renderQueue = [component, changes];
requestAnimationFrame(drainQueue);
}
}
}
/**
* @this {KeyIteratorBlock}
* @param {*} value
* @param {*} key
* Renders given component
*/
function iterator$1(value, key) {
const { host, injector, index, rendered } = this;
const id = getId(this, index, key, value);
// TODO make `rendered` a linked list for faster insert and remove
let entry = rendered && id in rendered ? rendered[id].shift() : null;
const prevScope = getScope(host);
const scope = prepareScope(entry ? entry.scope : obj(this.scope), index, key, value);
setScope(host, scope);
if (!entry) {
entry = injector.ctx = createItem(this, scope);
injector.ptr = entry.start;
entry.update = this.body(host, injector, scope);
this.updated = 1;
} else if (entry.update) {
if (entry.start.prev !== injector.ptr) {
this.needReorder = true;
}
if (entry.update(host, injector, scope)) {
this.updated = 1;
}
}
setScope(host, prevScope);
markUsed(this, id, entry);
injector.ptr = entry.end;
this.index++;
function renderComponent(component, changes) {
const { componentModel } = component;
const arg = changes || {};
componentModel.queued = false;
componentModel.rendering = true;
if (changes) {
runHook(component, 'didChange', arg);
}
runHook(component, 'willUpdate', arg);
runHook(component, 'willRender', arg);
safeCall(componentModel.update, component, getScope(component));
componentModel.rendering = false;
componentModel.finalizing = true;
runHook(component, 'didRender', arg);
runHook(component, 'didUpdate', arg);
componentModel.finalizing = false;
}
/**
* @param {KeyIteratorBlock} block
* Removes attached events from given map
*/
function reorder(block) {
const { injector, order } = block;
let actualPrev, actualNext;
let expectedPrev, expectedNext;
for (let i = 0, maxIx = order.length - 1, item; i <= maxIx; i++) {
item = order[i];
expectedPrev = i > 0 ? order[i - 1] : null;
expectedNext = i < maxIx ? order[i + 1] : null;
actualPrev = getItem(item.start.prev.value, block);
actualNext = getItem(item.end.next.value, block);
if (expectedPrev !== actualPrev && expectedNext !== actualNext) {
// Blocks must be reordered
move(injector, item, expectedPrev ? expectedPrev.end : block.start);
}
}
function detachStaticEvents(component, eventMap) {
const { listeners, handler } = eventMap;
for (const p in listeners) {
component.removeEventListener(p, handler);
}
}
/**
* @param {KeyIteratorBlock} iterator
* @param {string} id
* @param {IteratorItemBlock} block
*/
function markUsed(iterator, id, block) {
const { used } = iterator;
// We allow multiple items key in case of poorly prepared data.
if (id in used) {
used[id].push(block);
} else {
used[id] = [block];
}
iterator.order.push(block);
function kebabCase(ch) {
return '-' + ch.toLowerCase();
}
/**
* @param {KeyIteratorBlock} iterator
* @param {number} index
* @param {*} key
* @param {*} value
* @return {string}
*/
function getId(iterator, index, key, value) {
return iterator.keyExpr(value, prepareScope(iterator.scope, index, key, value));
function setPropsInternal(component, prevProps, nextProps) {
const changes = {};
let didChanged = false;
const { props } = component;
const { defaultProps } = component.componentModel;
for (const p in nextProps) {
const prev = prevProps[p];
let current = nextProps[p];
if (current == null) {
current = defaultProps[p];
}
if (p === 'class' && current != null) {
current = normalizeClassName(current);
}
if (current !== prev) {
didChanged = true;
props[p] = prevProps[p] = current;
changes[p] = { current, prev };
if (!/^partial:/.test(p)) {
representAttributeValue(component, p.replace(/[A-Z]/g, kebabCase), current);
}
}
nextProps[p] = null;
}
return didChanged ? changes : null;
}
/**
* @param {KeyIteratorBlock} iterator
* @param {Object} scope
* @returns {IteratorItemBlock}
* Check if `next` contains value that differs from one in `prev`
*/
function createItem(iterator, scope) {
return injectBlock(iterator.injector, {
$$block: true,
host: iterator.host,
injector: iterator.injector,
scope,
dispose: null,
update: undefined,
owner: iterator,
start: null,
end: null
});
function hasChanges(prev, next) {
for (const p in next) {
if (next[p] !== prev[p]) {
return true;
}
}
return false;
}
/**
* Sets value of attribute `name` to `value`
* @param {Injector} injector
* @param {string} name
* @param {*} value
* @return {number} Update status. Always returns `0` since actual attribute value
* is defined in `finalizeAttributes()`
* Prepares internal data for given component
*/
function setAttribute(injector, name, value) {
injector.attributes.cur[name] = value;
return 0;
function prepare(component, definition) {
const props = obj();
const state = obj();
let events;
let extend;
reverseWalkDefinitions(definition, dfn => {
dfn.props && assign(props, dfn.props(component));
dfn.state && assign(state, dfn.state(component));
// NB: backward compatibility with previous implementation
if (dfn.methods) {
extend = getDescriptors(dfn.methods, extend);
}
if (dfn.extend) {
extend = getDescriptors(dfn.extend, extend);
}
if (dfn.events) {
if (!events) {
events = createEventsMap(component);
}
attachEventHandlers(component, dfn.events, events);
}
});
return { props, state, extend, events };
}
/**
* Sets value of attribute `name` under namespace of `nsURI` to `value`
*
* @param {Injector} injector
* @param {string} nsURI
* @param {string} name
* @param {*} value
* Extracts property descriptors from given source object and merges it with `prev`
* descriptor map, if given
*/
function setAttributeNS(injector, nsURI, name, value) {
if (!injector.attributesNS) {
injector.attributesNS = obj();
}
const { attributesNS } = injector;
if (!attributesNS[nsURI]) {
attributesNS[nsURI] = changeSet();
}
attributesNS[nsURI].cur[name] = value;
function getDescriptors(source, prev) {
const descriptors = getObjectDescriptors(source);
return prev ? assign(prev, descriptors) : descriptors;
}
/**
* Updates `attrName` value in `elem`, if required
* @param {HTMLElement} elem
* @param {string} attrName
* @param {*} value
* @param {*} prevValue
* @returns {*} New attribute value
*/
function updateAttribute(elem, attrName, value, prevValue) {
if (value !== prevValue) {
changeAttribute(attrName, prevValue, value, elem);
return value;
}
return prevValue;
function createEventsMap(component) {
const listeners = obj();
const handler = function (evt) {
if (component.componentModel) {
const handlers = listeners[evt.type];
for (let i = 0; i < handlers.length; i++) {
handlers[i](component, evt, this);
}
}
};
return { handler, listeners };
}
/**
* Updates props in given component, if required
* @param {Component} elem
* @param {object} data
* @return {boolean} Returns `true` if value was updated
*/
function updateProps(elem, data) {
const { props } = elem;
let updated;
for (let p in data) {
if (data.hasOwnProperty(p) && props[p] !== data[p]) {
if (!updated) {
updated = obj();
}
updated[p] = data[p];
}
}
if (updated) {
elem.setProps(data);
return true;
}
return false;
function attachEventHandlers(component, events, eventMap) {
const names = Object.keys(events);
const { listeners } = eventMap;
for (let i = 0, name; i < names.length; i++) {
name = names[i];
if (name in listeners) {
listeners[name].push(events[name]);
}
else {
component.addEventListener(name, eventMap.handler);
listeners[name] = [events[name]];
}
}
}
/**
* Adds given class name as pending attribute
* @param {Injector} injector
* @param {string} value
*/
function addClass(injector, value) {
if (isDefined(value)) {
const className = injector.attributes.cur['class'];
setAttribute(injector, 'class', isDefined(className) ? className + ' ' + value : value);
}
function addPropsState(element) {
element.setProps = function setProps(value) {
const { componentModel } = element;
// In case of calling `setProps` after component was unmounted,
// check if `componentModel` is available
if (value != null && componentModel && componentModel.mounted) {
const changes = setPropsInternal(element, element.props, obj(value));
changes && renderNext(element, changes);
}
};
element.setState = function setState(value) {
const { componentModel } = element;
// In case of calling `setState` after component was unmounted,
// check if `componentModel` is available
if (value != null && componentModel && hasChanges(element.state, value)) {
assign(element.state, value);
// If we’re in rendering state than current `setState()` is caused by
// one of the `will*` hooks, which means applied changes will be automatically
// applied during rendering stage.
// If called outside of rendering state we should schedule render
// on next tick
if (componentModel.mounted && !componentModel.rendering) {
scheduleRender(element);
}
}
};
}
/**
* Applies pending attributes changes to injector’s host element
* @param {Injector} injector
* @return {number}
*/
function finalizeAttributes(injector) {
const { attributes, attributesNS } = injector;
if (isDefined(attributes.cur['class'])) {
attributes.cur['class'] = normalizeClassName(attributes.cur['class']);
}
let updated = finalizeItems(attributes, changeAttribute, injector.parentNode);
if (attributesNS) {
const ctx = { node: injector.parentNode, ns: null };
for (let ns in attributesNS) {
ctx.ns = ns;
updated |= finalizeItems(attributesNS[ns], changeAttributeNS, ctx);
}
}
return updated;
function drainQueue() {
const pending = renderQueue;
renderQueue = null;
for (let i = 0, component; i < pending.length; i += 2) {
component = pending[i];
// It’s possible that a component can be rendered before next tick
// (for example, if parent node updated component props).
// Check if it’s still queued then render.
// Also, component can be unmounted after it’s rendering was scheduled
if (component.componentModel && component.componentModel.queued) {
renderComponent(component, pending[i + 1]);
}
}
}
/**
* Normalizes given class value: removes duplicates and trims whitespace
* @param {string} str
* @returns {string}
* Mounts iterator block
* @param get A function that returns collection to iterate
* @param body A function that renders item of iterated collection
*/
function normalizeClassName(str) {
/** @type {string[]} */
const out = [];
const parts = String(str).split(/\s+/);
for (let i = 0, cl; i < parts.length; i++) {
cl = parts[i];
if (cl && out.indexOf(cl) === -1) {
out.push(cl);
}
}
return out.join(' ');
function mountIterator(host, injector, get, body) {
const block = injectBlock(injector, {
host,
injector,
scope: getScope(host),
dispose: null,
get,
body,
index: 0,
updated: 0
});
updateIterator(block);
return block;
}
/**
* Callback for changing attribute value
* @param {string} name
* @param {*} prevValue
* @param {*} newValue
* @param {Element} elem
* Updates iterator block defined in `ctx`
* @returns Returns `1` if iterator was updated, `0` otherwise
*/
function changeAttribute(name, prevValue, newValue, elem) {
if (isDefined(newValue)) {
representAttributeValue(elem, name, newValue);
} else if (isDefined(prevValue)) {
elem.removeAttribute(name);
}
function updateIterator(block) {
run(block, iteratorHost, block);
return block.updated;
}
/**
* Callback for changing attribute value
* @param {string} name
* @param {*} prevValue
* @param {*} newValue
* @param {{node: Element, ns: string}} ctx
*/
function changeAttributeNS(name, prevValue, newValue, ctx) {
if (isDefined(newValue)) {
ctx.node.setAttributeNS(ctx.ns, name, newValue);
} else if (isDefined(prevValue)) {
ctx.node.removeAttributeNS(ctx.ns, name);
}
function unmountIterator(block) {
disposeBlock(block);
}
/**
* Adds pending event `name` handler
* @param {Injector} injector
* @param {string} name
* @param {function} handler
*/
function addEvent(injector, name, handler) {
injector.events.cur[name] = handler;
function iteratorHost(host, injector, block) {
block.index = 0;
block.updated = 0;
const collection = block.get(host, block.scope);
if (collection && typeof collection.forEach === 'function') {
collection.forEach(iterator, block);
}
trimIteratorItems(block);
}
/**
* Adds given `handler` as event `name` listener
* @param {Element} elem
* @param {string} name
* @param {EventListener} handler
*/
function addStaticEvent(elem, name, handler) {
handler && elem.addEventListener(name, handler);
function prepareScope(scope, index, key, value) {
scope.index = index;
scope.key = key;
scope.value = value;
return scope;
}
/**
* Finalizes events of given injector
* @param {Injector} injector
* @returns {number} Update status
* Removes remaining iterator items from current context
*/
function finalizeEvents(injector) {
return finalizeItems(injector.events, changeEvent, injector.parentNode);
function trimIteratorItems(block) {
let item = block.injector.ptr.next;
let listItem;
while (item && item.value.owner === block) {
block.updated = 1;
listItem = item.value;
item = listItem.end.next;
disposeBlock(listItem);
}
}
/**
* Returns function that must be invoked as event handler for given component
* @param {Component} component
* @param {string} name Method name
* @param {HTMLElement} ctx Context element where event listener was added
* @returns {function?}
*/
function getEventHandler(component, name, ctx) {
let fn;
if (typeof component[name] === 'function') {
fn = component[name].bind(component);
} else {
const handler = component.componentModel.definition[name];
if (typeof handler === 'function') {
fn = handler.bind(ctx);
}
}
if (fn) {
fn.displayName = name;
}
return fn;
function iterator(value, key) {
const { host, injector, index } = this;
const { ptr } = injector;
const prevScope = getScope(host);
let rendered = ptr.next.value;
if (rendered.owner === this) {
// We have rendered item, update it
if (rendered.update) {
const scope = prepareScope(rendered.scope, index, key, value);
setScope(host, scope);
if (run(rendered, rendered.update, scope)) {
this.updated = 1;
}
setScope(host, prevScope);
}
}
else {
// Create & render new block
const scope = prepareScope(obj(prevScope), index, key, value);
rendered = injectBlock(injector, {
host,
injector,
scope,
dispose: null,
update: undefined,
owner: this
});
setScope(host, scope);
rendered.update = run(rendered, this.body, scope);
setScope(host, prevScope);
this.updated = 1;
}
injector.ptr = rendered.end;
this.index++;
}
/**
* Invoked when event handler was changed
* @param {string} name
* @param {EventListener} prevValue
* @param {EventListener} newValue
* @param {Element} elem
* Renders key iterator block
*/
function changeEvent(name, prevValue, newValue, elem) {
prevValue && elem.removeEventListener(name, prevValue);
addStaticEvent(elem, name, newValue);
function mountKeyIterator(host, injector, get, keyExpr, body) {
const parentScope = getScope(host);
const block = injectBlock(injector, {
host,
injector,
scope: obj(parentScope),
dispose: null,
get,
body,
keyExpr,
index: 0,
updated: 0,
rendered: null,
needReorder: false,
parentScope,
order: [],
used: null
});
updateKeyIterator(block);
return block;
}
/**
* Walks over each definition (including given one) and runs callback on it
* @param {ComponentDefinition} definition
* @param {(dfn: ComponentDefinition) => void} fn
* Updates iterator block defined in `ctx`
* @returns Returns `1` if iterator was updated, `0` otherwise
*/
function walkDefinitions(definition, fn) {
safeCall(fn, definition);
const { plugins } = definition;
if (plugins) {
for (let i = 0; i < plugins.length; i++) {
walkDefinitions(plugins[i], fn);
}
}
function updateKeyIterator(block) {
run(block, keyIteratorHost, block);
return block.updated;
}
/**
* Same as `walkDefinitions` but runs in reverse order
* @param {ComponentDefinition} definition
* @param {(dfn: ComponentDefinition) => void} fn
*/
function reverseWalkDefinitions(definition, fn) {
const { plugins } = definition;
if (plugins) {
let i = plugins.length;
while (i--) {
walkDefinitions(plugins[i], fn);
}
}
safeCall(fn, definition);
function unmountKeyIterator(ctx) {
disposeBlock(ctx);
}
/**
* Invokes `name` hook for given component definition
* @param {Component} elem
* @param {string} name
* @param {*} [arg1]
* @param {*} [arg2]
*/
function runHook(elem, name, arg1, arg2) {
walkDefinitions(elem.componentModel.definition, dfn => {
const hook = dfn[name];
if (typeof hook === 'function') {
hook(elem, arg1, arg2);
}
});
function keyIteratorHost(host, injector, block) {
block.used = obj();
block.index = 0;
block.updated = 0;
block.needReorder = false;
const collection = block.get(host, block.parentScope);
if (collection && typeof collection.forEach === 'function') {
collection.forEach(iterator$1, block);
}
const { rendered } = block;
for (const p in rendered) {
for (let i = 0, items = rendered[p]; i < items.length; i++) {
block.updated = 1;
disposeBlock(items[i]);
}
}
if (block.needReorder) {
reorder(block);
}
block.order.length = 0;
block.rendered = block.used;
}
/**
* Registers given element as output slot for `host` component
* @param {Component} host
* @param {string} name
* @param {HTMLElement} elem
* @param {Function} [defaultContent] Function for rendering default slot content
* @return {SlotContext}
* @param {KeyIteratorItemBlock} expected
* @param {KeyIteratorBlock} owner
* @returns {KeyIteratorItemBlock | null}
*/
function mountSlot(host, name, elem, defaultContent) {
/** @type {SlotContext} */
const ctx = { host, name, defaultContent, isDefault: false };
const { slots } = host.componentModel;
const injector = createInjector(elem);
function blockEntry() {
ctx.isDefault = !renderSlot(host, injector);
return ctx.isDefault ? ctx.defaultContent : null;
}
slots[name] = mountBlock(host, injector, blockEntry);
return ctx;
function getItem(expected, owner) {
return expected.owner === owner ? expected : null;
}
/**
* Unmounts given slot
* @param {SlotContext} ctx
*/
function unmountSlot(ctx) {
const { host, name } = ctx;
const { slots } = host.componentModel;
if (ctx.isDefault) {
unmountBlock(slots[name]);
}
ctx.defaultContent = null;
delete slots[name];
function iterator$1(value, key) {
const { host, injector, index, rendered } = this;
const id = getId(this, index, key, value);
// TODO make `rendered` a linked list for faster insert and remove
let entry = rendered && id in rendered ? rendered[id].shift() : null;
const prevScope = getScope(host);
const scope = prepareScope(entry ? entry.scope : obj(this.scope), index, key, value);
setScope(host, scope);
if (!entry) {
entry = injector.ctx = createItem(this, scope);
injector.ptr = entry.start;
entry.update = this.body(host, injector, scope);
this.updated = 1;
}
else if (entry.update) {
if (entry.start.prev !== injector.ptr) {
this.needReorder = true;
}
if (entry.update(host, injector, scope)) {
this.updated = 1;
}
}
setScope(host, prevScope);
markUsed(this, id, entry);
injector.ptr = entry.end;
this.index++;
}
/**
* Sync slot content if necessary
* @param {Component} host
*/
function updateSlots(host) {
const { slots, slotStatus, input } = host.componentModel;
for (const name in slots) {
updateBlock(slots[name]);
}
for (const name in slotStatus) {
if (slotStatus[name]) {
runHook(host, 'didSlotUpdate', name, input.slots[name]);
slotStatus[name] = 0;
}
}
function reorder(block) {
const { injector, order } = block;
let actualPrev;
let actualNext;
let expectedPrev;
let expectedNext;
for (let i = 0, maxIx = order.length - 1, item; i <= maxIx; i++) {
item = order[i];
expectedPrev = i > 0 ? order[i - 1] : null;
expectedNext = i < maxIx ? order[i + 1] : null;
actualPrev = getItem(item.start.prev.value, block);
actualNext = getItem(item.end.next.value, block);
if (expectedPrev !== actualPrev && expectedNext !== actualNext) {
// Blocks must be reordered
move(injector, item, expectedPrev ? expectedPrev.end : block.start);
}
}
}
/**
* Renders incoming contents of given slot
* @param {Component} host
* @param {Injector} target
* @returns {boolean} Returns `true` if slot content was filled with incoming data,
* `false` otherwise
*/
function renderSlot(host, target) {
const { parentNode } = target;
const name = parentNode.getAttribute('name') || '';
const slotted = parentNode.hasAttribute('slotted');
const { input } = host.componentModel;
/** @type {Element | DocumentFragment} */
const source = input.slots[name];
if (source && source.childNodes.length) {
// There’s incoming slot content
if (!slotted) {
parentNode.setAttribute('slotted', '');
input.slots[name] = moveContents(source, parentNode);
}
return true;
}
if (slotted) {
// Parent renderer removed incoming data
parentNode.removeAttribute('slotted');
input[name] = null;
}
return false;
function markUsed(iter, id, block) {
const { used } = iter;
// We allow multiple items key in case of poorly prepared data.
if (id in used) {
used[id].push(block);
}
else {
used[id] = [block];
}
iter.order.push(block);
}
/**
* Marks slot update status
* @param {Component} component
* @param {string} slotName
* @param {number} status
*/
function markSlotUpdate(component, slotName, status) {
const { slotStatus } = component.componentModel;
if (slotName in slotStatus) {
slotStatus[slotName] |= status;
} else {
slotStatus[slotName] = status;
}
function getId(iter, index, key, value) {
return iter.keyExpr(value, prepareScope(iter.scope, index, key, value));
}
function createItem(iter, scope) {
return injectBlock(iter.injector, {
$$block: true,
host: iter.host,
injector: iter.injector,
scope,
dispose: null,
update: undefined,
owner: iter
});
}
/**
* Sets runtime ref (e.g. ref which will be changed over time) to given host
* @param {Component} host
* @param {string} name
* @param {HTMLElement} elem
* @returns {number} Update status. Refs must be explicitly finalized, thus
* @returns Update status. Refs must be explicitly finalized, thus
* we always return `0` as nothing was changed
*/
function setRef(host, name, elem) {
host.componentModel.refs.cur[name] = elem;
return 0;
host.componentModel.refs.cur[name] = elem;
return 0;
}
/**
* Sets static ref (e.g. ref which won’t be changed over time) to given host
* @param {Component} host
* @param {string} name
* @param {Element} value
*/
function setStaticRef(host, name, value) {
value && value.setAttribute(getRefAttr(name, host), '');
host.refs[name] = value;
value && value.setAttribute(getRefAttr(name, host), '');
host.refs[name] = value;
}
/**
* Finalizes refs on given scope
* @param {Component} host
* @returns {number} Update status
* @returns Update status
*/
function finalizeRefs(host) {
return finalizeItems(host.componentModel.refs, changeRef, host);
return finalizeItems(host.componentModel.refs, changeRef, host);
}
/**
* Invoked when element reference was changed
* @param {string} name
* @param {Element} prevValue
* @param {Element} newValue
* @param {Component} host
*/
function changeRef(name, prevValue, newValue, host) {
prevValue && prevValue.removeAttribute(getRefAttr(name, host));
setStaticRef(host, name, newValue);
prevValue && prevValue.removeAttribute(getRefAttr(name, host));
setStaticRef(host, name, newValue);
}
/**
* Returns attribute name to identify element in CSS
* @param {String} name
* @param {Component} host
*/
function getRefAttr(name, host) {
const cssScope$$1 = host.componentModel.definition.cssScope;
return 'ref-' + name + (cssScope$$1 ? '-' + cssScope$$1 : '');
const cssScope = host.componentModel.definition.cssScope;
return 'ref-' + name + (cssScope ? '-' + cssScope : '');
}
/** @type {Array} */
let renderQueue = null;
/**
* Creates internal lightweight Endorphin component with given definition
* @param {string} name
* @param {ComponentDefinition} definition
* @param {Component} [host]
* @returns {Component}
*/
function createComponent(name, definition, host) {
const element = /** @type {Component} */ (elem(name, host && host.componentModel && host.componentModel.definition.cssScope));
// Add host scope marker: we can’t rely on tag name since component
// definition is bound to element in runtime, not compile time
const { cssScope: cssScope$$1 } = definition;
if (cssScope$$1) {
element.setAttribute(cssScope$$1 + '-host', '');
}
if (host && host.componentModel) {
// Passed component as parent: detect app root
element.root = host.root || host;
}
// XXX Should point to Shadow Root in Web Components
element.componentView = element;
const { props, state, extend, methods, events } = prepare(element, definition);
element.refs = {};
element.props = obj(props);
element.state = state;
element.setProps = function setProps(value) {
const { componentModel } = element;
// In case of calling `setProps` after component was unmounted,
// check if `componentModel` is available
if (value != null && componentModel && componentModel.mounted) {
const changes = setPropsInternal(element, element.props, obj(value));
changes && renderNext(element, changes);
}
};
element.setState = function setState(value) {
const { componentModel } = element;
// In case of calling `setState` after component was unmounted,
// check if `componentModel` is available
if (value != null && componentModel && hasChanges(element.state, value)) {
assign(element.state, value);
// If we’re in rendering state than current `setState()` is caused by
// one of the `will*` hooks, which means applied changes will be automatically
// applied during rendering stage.
// If called outside of rendering state we should schedule render
// on next tick
if (componentModel.mounted && !componentModel.rendering) {
scheduleRender(element);
}
}
};
assign(element, methods);
if (extend) {
Object.defineProperties(element, extend);
}
if (definition.store) {
element.store = definition.store();
} else if (element.root && element.root.store) {
element.store = element.root.store;
}
// Create slotted input
const input = createInjector(element.componentView);
input.slots = obj();
element.componentModel = {
definition,
input,
vars: obj(),
refs: changeSet(),
slots: obj(),
slotStatus: obj(),
mounted: false,
rendering: false,
finalizing: false,
update: null,
queued: false,
events,
dispose: null,
defaultProps: props
};
runHook(element, 'init');
return element;
}
/**
* Mounts given component
* @param {Component} elem
* @param {object} [initialProps]
*/
function mountComponent(elem$$1, initialProps) {
const { componentModel } = elem$$1;
const { input, definition, defaultProps } = componentModel;
let changes = setPropsInternal(elem$$1, obj(), assign(obj(defaultProps), initialProps));
const runtimeChanges = setPropsInternal(elem$$1, input.attributes.prev, input.attributes.cur);
if (changes && runtimeChanges) {
assign(changes, runtimeChanges);
} else if (runtimeChanges) {
changes = runtimeChanges;
}
const arg = changes || {};
finalizeEvents(input);
componentModel.rendering = true;
// Notify slot status
for (const p in input.slots) {
runHook(elem$$1, 'didSlotUpdate', p, input.slots[p]);
}
if (changes) {
runHook(elem$$1, 'didChange', arg);
}
runHook(elem$$1, 'willMount', arg);
runHook(elem$$1, 'willRender', arg);
componentModel.update = safeCall(definition.default, elem$$1, getScope(elem$$1));
componentModel.mounted = true;
componentModel.rendering = false;
componentModel.finalizing = true;
runHook(elem$$1, 'didRender', arg);
runHook(elem$$1, 'didMount', arg);
componentModel.finalizing = false;
}
/**
* Updates given mounted component
* @param {Component} elem
*/
function updateComponent(elem$$1) {
const { input } = elem$$1.componentModel;
const changes = setPropsInternal(elem$$1, input.attributes.prev, input.attributes.cur);
finalizeEvents(input);
updateSlots(elem$$1);
if (changes || elem$$1.componentModel.queued) {
renderNext(elem$$1, changes);
}
}
/**
* Destroys given component: removes static event listeners and cleans things up
* @param {Component} elem
* @returns {void} Should return nothing since function result will be used
* as shorthand to reset cached value
*/
function unmountComponent(elem$$1) {
const { componentModel } = elem$$1;
const { slots, input, dispose, events } = componentModel;
const scope = getScope(elem$$1);
runHook(elem$$1, 'willUnmount');
componentModel.mounted = false;
if (events) {
detachStaticEvents(elem$$1, events);
}
if (elem$$1.store) {
elem$$1.store.unwatch(elem$$1);
}
// Detach own handlers
// XXX doesn’t remove static events (via direct call of `addStaticEvent()`)
const ownHandlers = input.events.prev;
for (let p in ownHandlers) {
elem$$1.removeEventListener(p, ownHandlers[p]);
}
safeCall(dispose, scope);
for (const slotName in slots) {
disposeBlock(slots[slotName]);
}
runHook(elem$$1, 'didUnmount');
elem$$1.componentModel = null;
}
/**
* Subscribes to store updates of given component
* @param {Component} component
* @param {string[]} [keys]
*/
function subscribeStore(component, keys) {
if (!component.store) {
throw new Error(`Store is not defined for ${component.nodeName} component`);
}
component.store.watch(component, keys);
}
/**
* Queues next component render
* @param {Component} elem
* @param {Object} [changes]
*/
function renderNext(elem$$1, changes) {
if (!elem$$1.componentModel.rendering) {
renderComponent(elem$$1, changes);
} else {
scheduleRender(elem$$1, changes);
}
}
/**
* Schedules render of given component on next tick
* @param {Component} elem
* @param {Changes} [changes]
*/
function scheduleRender(elem$$1, changes) {
if (!elem$$1.componentModel.queued) {
elem$$1.componentModel.queued = true;
if (renderQueue) {
renderQueue.push(elem$$1, changes);
} else {
renderQueue = [elem$$1, changes];
requestAnimationFrame(drainQueue);
}
}
}
/**
* Renders given component
* @param {Component} elem
* @param {Changes} [changes]
*/
function renderComponent(elem$$1, changes) {
const { componentModel } = elem$$1;
const arg = changes || {};
componentModel.queued = false;
componentModel.rendering = true;
if (changes) {
runHook(elem$$1, 'didChange', arg);
}
// TODO prepare data for hooks in `mountComponent`?
runHook(elem$$1, 'willUpdate', arg);
runHook(elem$$1, 'willRender', arg);
safeCall(componentModel.update, elem$$1, getScope(elem$$1));
componentModel.rendering = false;
componentModel.finalizing = true;
runHook(elem$$1, 'didRender', arg);
runHook(elem$$1, 'didUpdate', arg);
componentModel.finalizing = false;
}
/**
* Removes attached events from given map
* @param {Component} component
* @param {AttachedStaticEvents} eventMap
*/
function detachStaticEvents(component, eventMap) {
const { listeners, handler } = eventMap;
for (let p in listeners) {
component.removeEventListener(p, handler);
}
}
/**
* @param {string} ch
* @returns {string}
*/
function kebabCase(ch) {
return '-' + ch.toLowerCase();
}
/**
* @param {Component} component
* @param {Object} prevProps
* @param {Object} nextProps
* @returns {Changes}
*/
function setPropsInternal(component, prevProps, nextProps) {
/** @type {Changes} */
const changes = {};
let hasChanges = false;
const { props } = component;
const { defaultProps } = component.componentModel;
for (const p in nextProps) {
const prev = prevProps[p];
let current = nextProps[p];
if (current == null) {
current = defaultProps[p];
}
if (p === 'class' && current != null) {
current = normalizeClassName(current);
}
if (current !== prev) {
hasChanges = true;
props[p] = prevProps[p] = current;
changes[p] = { current, prev };
if (!/^partial:/.test(p)) {
representAttributeValue(component, p.replace(/[A-Z]/g, kebabCase), current);
}
}
nextProps[p] = null;
}
return hasChanges ? changes : null;
}
/**
* Check if `next` contains value that differs from one in `prev`
* @param {Object} prev
* @param {Object} next
* @returns {boolean}
*/
function hasChanges(prev, next) {
for (const p in next) {
if (next[p] !== prev[p]) {
return true;
}
}
}
/**
* Prepares internal data for given component
* @param {Component} component
* @param {ComponentDefinition} definition
*/
function prepare(component, definition) {
const props = obj();
const state = obj();
const methods = obj();
/** @type {AttachedStaticEvents} */
let events;
let extend;
reverseWalkDefinitions(definition, dfn => {
dfn.props && assign(props, dfn.props(component));
dfn.state && assign(state, dfn.state(component));
dfn.methods && assign(methods, dfn.methods);
if (dfn.extend) {
const descriptors = getObjectDescriptors(dfn.extend);
extend = extend ? assign(extend, descriptors) : descriptors;
}
if (dfn.events) {
if (!events) {
events = createEventsMap(component);
}
attachEventHandlers(component, dfn.events, events);
}
});
return { props, state, extend, methods, events };
}
/**
* @param {Component} component
* @returns {AttachedStaticEvents}
*/
function createEventsMap(component) {
/** @type {{[event: string]: ComponentEventHandler[]}} */
const listeners = obj();
/** @type {StaticEventHandler} */
const handler = function (evt) {
if (component.componentModel) {
const handlers = listeners[evt.type];
for (let i = 0; i < handlers.length; i++) {
handlers[i](component, evt, this);
}
}
};
return { handler, listeners };
}
/**
* @param {Component} component
* @param {{[name: string]: ComponentEventHandler}} events
* @param {AttachedStaticEvents} eventMap
*/
function attachEventHandlers(component, events, eventMap) {
const names = Object.keys(events);
const { listeners } = eventMap;
for (let i = 0, name; i < names.length; i++) {
name = names[i];
if (name in listeners) {
listeners[name].push(events[name]);
} else {
component.addEventListener(name, eventMap.handler);
listeners[name] = [events[name]];
}
}
}
function drainQueue() {
const pending = renderQueue;
renderQueue = null;
for (let i = 0, component; i < pending.length; i += 2) {
component = pending[i];
// It’s possible that a component can be rendered before next tick
// (for example, if parent node updated component props).
// Check if it’s still queued then render.
// Also, component can be unmounted after it’s rendering was scheduled
if (component.componentModel && component.componentModel.queued) {
renderComponent(component, pending[i + 1]);
}
}
}
/**
* Renders code, returned from `get` function, as HTML
* @param {Component} host
* @param {Injector} injector
* @param {Function} get
* @param {string} slotName
* @returns {InnerHtmlBlock}
*/
function mountInnerHTML(host, injector, get, slotName) {
/** @type {InnerHtmlBlock} */
const block = injectBlock(injector, {
$$block: true,
host,
injector,
scope: getScope(host),
dispose: null,
get,
code: null,
slotName,
start: null,
end: null
});
updateInnerHTML(block);
return block;
const block = injectBlock(injector, {
$$block: true,
host,
injector,
scope: getScope(host),
dispose: null,
get,
code: null,
slotName
});
updateInnerHTML(block);
return block;
}
/**
* Updates inner HTML of block, defined in `ctx`
* @param {InnerHtmlBlock} block
* @returns {number} Returns `1` if inner HTML was updated, `0` otherwise
* @returns Returns `1` if inner HTML was updated, `0` otherwise
*/
function updateInnerHTML(block) {
const code = block.get(block.host, block.scope);
if (code !== block.code) {
emptyBlockContent(block);
if (isDefined(block.code = code)) {
run(block, renderHTML, block);
}
block.injector.ptr = block.end;
return 1;
}
return 0;
const code = block.get(block.host, block.scope);
if (code !== block.code) {
emptyBlockContent(block);
if (isDefined(block.code = code)) {
run(block, renderHTML, block);
}
block.injector.ptr = block.end;
return 1;
}
return 0;
}
/**
* @param {InnerHtmlBlock} ctx
*/
function unmountInnerHTML(ctx) {
disposeBlock(ctx);
disposeBlock(ctx);
}
/**
* @param {Component} host
* @param {Injector} injector
* @param {InnerHtmlBlock} ctx
*/
function renderHTML(host, injector, ctx) {
const { code } = ctx;
const { cssScope: cssScope$$1 } = host.componentModel.definition;
if (code && code.nodeType) {
// Insert as DOM element
cssScope$$1 && scopeDOM(code, cssScope$$1);
insert(injector, code, ctx.slotName);
} else {
// Render as HTML
const div = document.createElement('div');
div.innerHTML = ctx.code;
cssScope$$1 && scopeDOM(div, cssScope$$1);
while (div.firstChild) {
insert(injector, div.firstChild, ctx.slotName);
}
}
const { code } = ctx;
const { cssScope } = host.componentModel.definition;
if (code && code.nodeType) {
// Insert as DOM element
cssScope && scopeDOM(code, cssScope);
insert(injector, code, ctx.slotName);
}
else {
// Render as HTML
const div = document.createElement('div');
div.innerHTML = ctx.code;
cssScope && scopeDOM(div, cssScope);
while (div.firstChild) {
insert(injector, div.firstChild, ctx.slotName);
}
}
}
/**
* Scopes CSS of all elements in given node
* @param {Element} node
* @param {string} cssScope
*/
function scopeDOM(node, cssScope$$1) {
node = /** @type {Element} */ (node.firstChild);
while (node) {
if (node.nodeType === node.ELEMENT_NODE) {
node.setAttribute(cssScope$$1, '');
scopeDOM(node, cssScope$$1);
}
node = /** @type {Element} */ (node.nextSibling);
}
function scopeDOM(node, cssScope) {
node = node.firstChild;
while (node) {
if (node.nodeType === node.ELEMENT_NODE) {
node.setAttribute(cssScope, '');
scopeDOM(node, cssScope);
}
node = node.nextSibling;
}
}

@@ -2051,174 +1513,136 @@

* Mounts given partial into injector context
* @param {Component} host
* @param {Injector} injector
* @param {PartialDefinition} partial
* @param {Object} args
* @return {PartialBlock}
*/
function mountPartial(host, injector, partial, args) {
/** @type {PartialBlock} */
const block = injectBlock(injector, {
$$block: true,
host,
injector,
scope: getScope(host),
dispose: null,
update: null,
partial: null,
start: null,
end: null
});
updatePartial(block, partial, args);
return block;
const block = injectBlock(injector, {
$$block: true,
host,
injector,
scope: getScope(host),
dispose: null,
update: void 0,
partial: null
});
updatePartial(block, partial, args);
return block;
}
/**
* Updates mounted partial
* @param {PartialBlock} ctx
* @param {PartialDefinition} partial
* @param {Object} args
* @returns {number} Returns `1` if partial was updated, `0` otherwise
* @returns Returns `1` if partial was updated, `0` otherwise
*/
function updatePartial(ctx, partial, args) {
const host = partial.host || ctx.host;
const { injector } = ctx;
const prevHost = ctx.host;
const prevScope = getScope(host);
let updated = 0;
ctx.host = host;
if (ctx.partial !== partial) {
// Unmount previously rendered partial
ctx.partial && emptyBlockContent(ctx);
// Mount new partial
const scope = ctx.scope = assign(obj(prevScope), partial.defaults, args);
setScope(host, scope);
ctx.update = partial ? run(ctx, partial.body, scope) : null;
ctx.partial = partial;
setScope(host, prevScope);
updated = 1;
} else if (ctx.update) {
// Update rendered partial
const scope = setScope(host, assign(ctx.scope, args));
if (run(ctx, ctx.update, scope)) {
updated = 1;
}
setScope(host, prevScope);
}
ctx.host = prevHost;
injector.ptr = ctx.end;
return updated;
const host = partial.host || ctx.host;
const { injector } = ctx;
const prevHost = ctx.host;
const prevScope = getScope(host);
let updated = 0;
ctx.host = host;
if (ctx.partial !== partial) {
// Unmount previously rendered partial
ctx.partial && emptyBlockContent(ctx);
// Mount new partial
const scope = ctx.scope = assign(obj(prevScope), partial.defaults, args);
setScope(host, scope);
ctx.update = partial ? run(ctx, partial.body, scope) : void 0;
ctx.partial = partial;
setScope(host, prevScope);
updated = 1;
}
else if (ctx.update) {
// Update rendered partial
const scope = setScope(host, assign(ctx.scope, args));
if (run(ctx, ctx.update, scope)) {
updated = 1;
}
setScope(host, prevScope);
}
ctx.host = prevHost;
injector.ptr = ctx.end;
return updated;
}
/**
* @param {PartialBlock} ctx
*/
function unmountPartial(ctx) {
disposeBlock(ctx);
disposeBlock(ctx);
}
const prefix = '$';
class Store {
constructor(data = {}) {
this.data = assign({}, data);
/** @type {StoreUpdateEntry[]} */
this._listeners = [];
// For unit tests
this.sync = false;
}
/**
* Returns current store data
* @returns {Object}
*/
get() {
return this.data;
}
/**
* Updates data in store
* @param {Object} data
*/
set(data) {
const updated = changed(data, this.data, prefix);
const render = this.sync ? renderComponent : scheduleRender;
if (updated) {
const next = this.data = assign(this.data, data);
// Notify listeners.
// Run in reverse order for listener safety (in case if handler decides
// to unsubscribe during notification)
for (let i = this._listeners.length - 1, item; i >= 0; i--) {
item = this._listeners[i];
if (!item.keys || !item.keys.length || hasChange(item.keys, updated)) {
if ('component' in item) {
render(item.component, updated);
} else if ('handler' in item) {
item.handler(next, updated);
}
}
}
}
}
/**
* Subscribes to changes in given store
* @param {StoreUpdateHandler} handler Function to invoke when store changes
* @param {string[]} keys Run handler only if given top-level keys are changed
* @returns {Object} Object that should be used to unsubscribe from updates
*/
subscribe(handler, keys) {
/** @type {StoreUpdateEntry} */
const obj$$1 = {
handler,
keys: scopeKeys(keys, prefix)
};
this._listeners.push(obj$$1);
return obj$$1;
}
/**
* Unsubscribes from further updates
* @param {Object} obj
*/
unsubscribe(obj$$1) {
const ix = this._listeners.indexOf(obj$$1);
if (ix !== -1) {
this._listeners.splice(ix, 1);
}
}
/**
* Watches for updates of given `keys` in store and runs `component` render on change
* @param {Component} component
* @param {string[]} keys
*/
watch(component, keys) {
this._listeners.push({
component,
keys: scopeKeys(keys, prefix)
});
}
/**
* Stops watching for store updates for given component
* @param {Component} component
*/
unwatch(component) {
for (let i = 0; i < this._listeners.length; i++) {
if (this._listeners[i].component === component) {
this._listeners.splice(i, 1);
return;
}
}
}
constructor(data) {
this.sync = false;
this.listeners = [];
this.data = assign({}, data || {});
}
/**
* Returns current store data
*/
get() {
return this.data;
}
/**
* Updates data in store
*/
set(data) {
const updated = changed(data, this.data, prefix);
const render = this.sync ? renderComponent : scheduleRender;
if (updated) {
const next = this.data = assign(this.data, data);
// Notify listeners.
// Run in reverse order for listener safety (in case if handler decides
// to unsubscribe during notification)
for (let i = this.listeners.length - 1, item; i >= 0; i--) {
item = this.listeners[i];
if (!item.keys || !item.keys.length || hasChange(item.keys, updated)) {
if ('component' in item) {
render(item.component, updated);
}
else if ('handler' in item) {
item.handler(next, updated);
}
}
}
}
}
/**
* Subscribes to changes in given store
* @param handler Function to invoke when store changes
* @param keys Run handler only if given top-level keys are changed
* @returns Object that should be used to unsubscribe from updates
*/
subscribe(handler, keys) {
const obj = {
handler,
keys: scopeKeys(keys, prefix)
};
this.listeners.push(obj);
return obj;
}
/**
* Unsubscribes from further updates
*/
unsubscribe(obj) {
const ix = this.listeners.indexOf(obj);
if (ix !== -1) {
this.listeners.splice(ix, 1);
}
}
/**
* Watches for updates of given `keys` in store and runs `component` render on change
*/
watch(component, keys) {
this.listeners.push({
component,
keys: scopeKeys(keys, prefix)
});
}
/**
* Stops watching for store updates for given component
* @param {Component} component
*/
unwatch(component) {
for (let i = 0; i < this.listeners.length; i++) {
if (this.listeners[i].component === component) {
this.listeners.splice(i, 1);
return;
}
}
}
}
/**

@@ -2231,19 +1655,14 @@ * Check if any of `keys` was changed in `next` object since `prev` state

function hasChange(keys, updated) {
for (let i = 0; i < keys.length; i++) {
if (keys[i] in updated) {
return true;
}
}
return false;
for (let i = 0; i < keys.length; i++) {
if (keys[i] in updated) {
return true;
}
}
return false;
}
/**
* Adds given prefix to keys
* @param {string[]} keys
* @param {string} prefix
* @returns {string[]}
*/
function scopeKeys(keys, prefix) {
return keys && prefix ? keys.map(key => prefix + key) : keys;
function scopeKeys(keys, pfx) {
return keys && pfx ? keys.map(key => pfx + key) : keys;
}

@@ -2253,78 +1672,57 @@

* Animates element appearance
* @param {HTMLElement | Component} elem
* @param {string} animation
* @param {string} [cssScope]
*/
function animateIn(elem$$1, animation, cssScope$$1) {
if (animation = createAnimation(animation, cssScope$$1)) {
elem$$1.style.animation = animation;
}
function animateIn(elem, animation, cssScope) {
if (animation = createAnimation(animation, cssScope)) {
elem.style.animation = animation;
}
}
/**
* Animates element disappearance
* @param {HTMLElement | Component} elem
* @param {string} animation
* @param {Object} [scope]
* @param {Function} [callback]
* @param {string} [cssScope]
*/
function animateOut(elem$$1, animation, scope, callback, cssScope$$1) {
if (typeof scope === 'string') {
cssScope$$1 = scope;
scope = callback = null;
}
if (animation = createAnimation(animation, cssScope$$1)) {
// Create a copy of scope and pass it to callback function.
// It’s required for proper clean-up in case if the same element
// (with the same scope references) will be created during animation
if (scope) {
scope = assign(obj(), scope);
}
/** @param {AnimationEvent} evt */
const handler = evt => {
if (evt.target === elem$$1) {
elem$$1[animatingKey] = false;
elem$$1.removeEventListener('animationend', handler);
elem$$1.removeEventListener('animationcancel', handler);
dispose(elem$$1, () => callback && callback(scope));
}
};
elem$$1[animatingKey] = true;
elem$$1.addEventListener('animationend', handler);
elem$$1.addEventListener('animationcancel', handler);
elem$$1.style.animation = animation;
} else {
dispose(elem$$1, callback);
}
function animateOut(elem, animation, scope, callback, cssScope) {
if (typeof scope === 'string') {
cssScope = scope;
scope = callback = undefined;
}
if (animation = createAnimation(animation, cssScope)) {
// Create a copy of scope and pass it to callback function.
// It’s required for proper clean-up in case if the same element
// (with the same scope references) will be created during animation
if (scope) {
scope = assign(obj(), scope);
}
/** @param {AnimationEvent} evt */
const handler = (evt) => {
if (evt.target === elem) {
elem[animatingKey] = false;
elem.removeEventListener('animationend', handler);
elem.removeEventListener('animationcancel', handler);
dispose(elem, () => callback && callback(scope));
}
};
elem[animatingKey] = true;
elem.addEventListener('animationend', handler);
elem.addEventListener('animationcancel', handler);
elem.style.animation = animation;
}
else {
dispose(elem, () => callback && callback(scope));
}
}
/**
* Creates animation CSS value with scoped animation name
* @param {string} animation
* @param {string} [cssScope]
* @returns {string}
*/
function createAnimation(animation, cssScope$$1) {
if (animation == null) {
return '';
}
const parts = String(animation).split(' ');
let name = parts[0].trim();
const globalPrefix = 'global:';
if (name.indexOf(globalPrefix) === 0) {
// Do not scope animation name, use globally defined animation name
parts[0] = name.slice(globalPrefix.length);
} else if (cssScope$$1) {
parts[0] = concat(name, cssScope$$1);
}
return parts.join(' ').trim();
function createAnimation(animation, cssScope) {
if (animation == null) {
return '';
}
const parts = String(animation).split(' ');
const name = parts[0].trim();
const globalPrefix = 'global:';
if (name.indexOf(globalPrefix) === 0) {
// Do not scope animation name, use globally defined animation name
parts[0] = name.slice(globalPrefix.length);
}
else if (cssScope) {
parts[0] = concat(name, cssScope);
}
return parts.join(' ').trim();
}
/**

@@ -2336,22 +1734,29 @@ * Concatenates two strings with optional separator

function concat(name, suffix) {
const sep = suffix[0] === '_' || suffix[0] === '-' ? '' : '-';
return name + sep + suffix;
const sep = suffix[0] === '_' || suffix[0] === '-' ? '' : '-';
return name + sep + suffix;
}
function dispose(elem, callback) {
if ('componentModel' in elem) {
unmountComponent(elem);
}
if (callback) {
callback();
}
domRemove(elem);
}
/**
* @param {HTMLElement | Component} elem
* @param {Function} [callback]
* Creates Endorphin component and mounts it into given `options.target` container
*/
function dispose(elem$$1, callback) {
if (/** @type {Component} */ (elem$$1).componentModel) {
unmountComponent(/** @type {Component} */(elem$$1));
}
if (callback) {
callback();
}
domRemove(elem$$1);
function endorphin(name, definition, options = {}) {
const component = createComponent(name, definition, options.target);
if (options.store) {
component.store = options.store;
}
if (options.target && !options.detached) {
options.target.appendChild(component);
}
mountComponent(component, options.props);
return component;
}
/**

@@ -2364,122 +1769,140 @@ * Safe property getter

function get(ctx) {
const hasMap = typeof Map !== 'undefined';
for (let i = 1, il = arguments.length, arg; ctx != null && i < il; i++) {
arg = arguments[i];
if (hasMap && ctx instanceof Map) {
ctx = ctx.get(arg);
} else {
ctx = ctx[arg];
}
}
return ctx;
const hasMap = typeof Map !== 'undefined';
for (let i = 1, il = arguments.length, arg; ctx != null && i < il; i++) {
arg = arguments[i];
if (hasMap && ctx instanceof Map) {
ctx = ctx.get(arg);
}
else {
ctx = ctx[arg];
}
}
return ctx;
}
/**
* Invokes `methodName` of `ctx` object with given args
*/
function call(ctx, methodName, args) {
const method = ctx != null && ctx[methodName];
if (typeof method === 'function') {
return args ? method.apply(ctx, args) : method.call(ctx);
}
}
/**
* Filter items from given collection that matches `fn` criteria and returns
* matched items
* @param {Component} host
* @param {Iterable} collection
* @param {Function} fn
* @returns {Array}
*/
function filter(host, collection, fn) {
const result = [];
if (collection && collection.forEach) {
collection.forEach((value, key) => {
if (fn(host, value, key)) {
result.push(value);
}
});
}
return result;
const result = [];
if (collection && collection.forEach) {
collection.forEach((value, key) => {
if (fn(host, value, key)) {
result.push(value);
}
});
}
return result;
}
/**
* Invokes `methodName` of `ctx` object with given args
* @param {Object} ctx
* @param {string} methodName
* @param {Array} [args]
* Finds first item in given `collection` that matches truth test of `fn`
*/
function call(ctx, methodName, args) {
const method = ctx != null && ctx[methodName];
if (typeof method === 'function') {
return args ? method.apply(ctx, args) : method.call(ctx);
}
function find(host, collection, fn) {
if (Array.isArray(collection)) {
// Fast path: find item in array
for (let i = 0, item; i < collection.length; i++) {
item = collection[i];
if (fn(host, item, i)) {
return item;
}
}
}
else if (collection && collection.forEach) {
// Iterate over collection
let found = false;
let result = null;
collection.forEach((value, key) => {
if (!found && fn(host, value, key)) {
found = true;
result = value;
}
});
return result;
}
}
exports.get = get;
exports.filter = filter;
exports.call = call;
exports.Store = Store;
exports.addClass = addClass;
exports.addDisposeCallback = addDisposeCallback;
exports.addEvent = addEvent;
exports.addStaticEvent = addStaticEvent;
exports.animateIn = animateIn;
exports.animateOut = animateOut;
exports.assign = assign;
exports.mountBlock = mountBlock;
exports.updateBlock = updateBlock;
exports.unmountBlock = unmountBlock;
exports.mountIterator = mountIterator;
exports.updateIterator = updateIterator;
exports.unmountIterator = unmountIterator;
exports.prepareScope = prepareScope;
exports.mountKeyIterator = mountKeyIterator;
exports.updateKeyIterator = updateKeyIterator;
exports.unmountKeyIterator = unmountKeyIterator;
exports.call = call;
exports.createComponent = createComponent;
exports.createInjector = createInjector;
exports.insert = insert;
exports.injectBlock = injectBlock;
exports.run = run;
exports.createScope = createScope;
exports.default = endorphin;
exports.disposeBlock = disposeBlock;
exports.domInsert = domInsert;
exports.domRemove = domRemove;
exports.elem = elem;
exports.elemNS = elemNS;
exports.elemNSWithText = elemNSWithText;
exports.elemWithText = elemWithText;
exports.emptyBlockContent = emptyBlockContent;
exports.move = move;
exports.disposeBlock = disposeBlock;
exports.enterScope = enterScope;
exports.exitScope = exitScope;
exports.createScope = createScope;
exports.setScope = setScope;
exports.filter = filter;
exports.finalizeAttributes = finalizeAttributes;
exports.finalizeEvents = finalizeEvents;
exports.finalizeRefs = finalizeRefs;
exports.find = find;
exports.get = get;
exports.getProp = getProp;
exports.getScope = getScope;
exports.getProp = getProp;
exports.getState = getState;
exports.getVar = getVar;
exports.setVar = setVar;
exports.injectBlock = injectBlock;
exports.insert = insert;
exports.markSlotUpdate = markSlotUpdate;
exports.mountBlock = mountBlock;
exports.mountComponent = mountComponent;
exports.mountInnerHTML = mountInnerHTML;
exports.mountIterator = mountIterator;
exports.mountKeyIterator = mountKeyIterator;
exports.mountPartial = mountPartial;
exports.mountSlot = mountSlot;
exports.move = move;
exports.normalizeClassName = normalizeClassName;
exports.prepareScope = prepareScope;
exports.removeStaticEvent = removeStaticEvent;
exports.renderComponent = renderComponent;
exports.run = run;
exports.scheduleRender = scheduleRender;
exports.setAttribute = setAttribute;
exports.setAttributeNS = setAttributeNS;
exports.updateAttribute = updateAttribute;
exports.updateProps = updateProps;
exports.addClass = addClass;
exports.finalizeAttributes = finalizeAttributes;
exports.normalizeClassName = normalizeClassName;
exports.addEvent = addEvent;
exports.addStaticEvent = addStaticEvent;
exports.finalizeEvents = finalizeEvents;
exports.getEventHandler = getEventHandler;
exports.mountSlot = mountSlot;
exports.unmountSlot = unmountSlot;
exports.updateSlots = updateSlots;
exports.markSlotUpdate = markSlotUpdate;
exports.setRef = setRef;
exports.setScope = setScope;
exports.setStaticRef = setStaticRef;
exports.finalizeRefs = finalizeRefs;
exports.createComponent = createComponent;
exports.mountComponent = mountComponent;
exports.setVar = setVar;
exports.subscribeStore = subscribeStore;
exports.text = text;
exports.unmountBlock = unmountBlock;
exports.unmountComponent = unmountComponent;
exports.unmountInnerHTML = unmountInnerHTML;
exports.unmountIterator = unmountIterator;
exports.unmountKeyIterator = unmountKeyIterator;
exports.unmountPartial = unmountPartial;
exports.unmountSlot = unmountSlot;
exports.updateAttribute = updateAttribute;
exports.updateBlock = updateBlock;
exports.updateComponent = updateComponent;
exports.unmountComponent = unmountComponent;
exports.subscribeStore = subscribeStore;
exports.scheduleRender = scheduleRender;
exports.renderComponent = renderComponent;
exports.mountInnerHTML = mountInnerHTML;
exports.updateInnerHTML = updateInnerHTML;
exports.unmountInnerHTML = unmountInnerHTML;
exports.elem = elem;
exports.elemNS = elemNS;
exports.elemWithText = elemWithText;
exports.elemNSWithText = elemNSWithText;
exports.text = text;
exports.updateIterator = updateIterator;
exports.updateKeyIterator = updateKeyIterator;
exports.updatePartial = updatePartial;
exports.updateProps = updateProps;
exports.updateSlots = updateSlots;
exports.updateText = updateText;
exports.domInsert = domInsert;
exports.domRemove = domRemove;
exports.mountPartial = mountPartial;
exports.updatePartial = updatePartial;
exports.unmountPartial = unmountPartial;
exports.Store = Store;
exports.animateIn = animateIn;
exports.animateOut = animateOut;
//# sourceMappingURL=runtime.cjs.js.map
/**
* Creates linted list
* @return {LinkedList}
* Creates element with given tag name
* @param cssScope Scope for CSS isolation
*/
function createList() {
return { head: null };
function elem(tagName, cssScope) {
const el = document.createElement(tagName);
cssScope && el.setAttribute(cssScope, '');
return el;
}
/**
* Creates linked list item
* @template T
* @param {T} value
* @returns {LinkedListItem<T>}
* Creates element with given tag name under `ns` namespace
* @param cssScope Scope for CSS isolation
*/
function createListItem(value) {
return { value, next: null, prev: null };
function elemNS(tagName, ns, cssScope) {
const el = document.createElementNS(ns, tagName);
cssScope && el.setAttribute(cssScope, '');
return el;
}
/**
* Prepends given value to linked list
* @template T
* @param {LinkedList} list
* @param {T} value
* @return {LinkedListItem<T>}
* Creates element with given tag name and text
* @param cssScope Scope for CSS isolation
*/
function listPrependValue(list, value) {
const item = createListItem(value);
if (item.next = list.head) {
item.next.prev = item;
}
return list.head = item;
function elemWithText(tagName, value, cssScope) {
const el = elem(tagName, cssScope);
el.textContent = textValue(value);
return el;
}
/**
* Inserts given value after given `ref` item
* @template T
* @param {T} value
* @param {LinkedListItem<any>} ref
* @return {LinkedListItem<T>}
* Creates element with given tag name under `ns` namespace and text
* @param cssScope Scope for CSS isolation
*/
function listInsertValueAfter(value, ref) {
const item = createListItem(value);
const { next } = ref;
ref.next = item;
item.prev = ref;
if (item.next = next) {
next.prev = item;
}
return item;
function elemNSWithText(tagName, ns, value, cssScope) {
const el = elemNS(tagName, ns, cssScope);
el.textContent = textValue(value);
return el;
}
/**
* Moves list fragment with `start` and `end` bounds right after `ref` item
* @param {LinkedList} list
* @param {LinkedListItem} start
* @param {LinkedListItem} end
* @param {LinkedListItem} ref
* Creates text node with given value
*/
function listMoveFragmentAfter(list, start, end, ref) {
listDetachFragment(list, start, end);
if (end.next = ref.next) {
end.next.prev = end;
}
ref.next = start;
start.prev = ref;
function text(value) {
const node = document.createTextNode(textValue(value));
node.$value = value;
return node;
}
/**
* Moves list fragment with `start` and `end` to list head
* @param {LinkedList} list
* @param {LinkedListItem} start
* @param {LinkedListItem} end
* Updates given text node value, if required
* @returns Returns `1` if text was updated, `0` otherwise
*/
function listMoveFragmentFirst(list, start, end) {
listDetachFragment(list, start, end);
if (end.next = list.head) {
end.next.prev = end;
}
list.head = start;
function updateText(node, value) {
if (value !== node.$value) {
node.nodeValue = textValue(value);
node.$value = value;
return 1;
}
return 0;
}
/**
* Detaches list fragment with `start` and `end` from list
* @param {LinkedList} list
* @param {LinkedListItem} start
* @param {LinkedListItem} end
* @returns Inserted item
*/
function listDetachFragment(list, start, end) {
const { prev } = start;
const { next } = end;
if (prev) {
prev.next = next;
} else {
list.head = next;
}
if (next) {
next.prev = prev;
}
start.prev = end.next = null;
function domInsert(node, parent, anchor) {
return anchor
? parent.insertBefore(node, anchor)
: parent.appendChild(node);
}
/**
* Removes given DOM node from its tree
* @param {Node} node
*/
function domRemove(node) {
const { parentNode } = node;
parentNode && parentNode.removeChild(node);
}
/**
* Returns textual representation of given `value` object
*/
function textValue(value) {
return value != null ? value : '';
}
const animatingKey = '$$animating';
/**
* Creates fast object
* @param {Object} [proto]
* @returns {Object}
*/
function obj(proto = null) {
return Object.create(proto);
return Object.create(proto);
}
/**

@@ -129,5 +93,4 @@ * Check if given value id defined, e.g. not `null`, `undefined` or `NaN`

function isDefined(value) {
return value != null && value === value;
return value != null && value === value;
}
/**

@@ -141,106 +104,86 @@ * Finalizes updated items, defined in `items.prev` and `items.cur`

function finalizeItems(items, change, ctx) {
let updated = 0;
const { cur, prev } = items;
for (const name in cur) {
const curValue = cur[name], prevValue = prev[name];
if (curValue !== prevValue) {
updated = 1;
change(name, prevValue, prev[name] = curValue, ctx);
}
cur[name] = null;
}
return updated;
let updated = 0;
const { cur, prev } = items;
for (const name in cur) {
const curValue = cur[name];
const prevValue = prev[name];
if (curValue !== prevValue) {
updated = 1;
change(name, prevValue, prev[name] = curValue, ctx);
}
cur[name] = null;
}
return updated;
}
/**
* Creates object for storing change sets, e.g. current and previous values
* @returns {ChangeSet}
*/
function changeSet() {
return { prev: obj(), cur: obj() };
return { prev: obj(), cur: obj() };
}
/**
* Returns properties from `next` which were changed since `prev` state.
* Returns `null` if there are no changes
* @param {Object} next
* @param {Object} prev
* @return {Changes}
*/
function changed(next, prev, prefix = '') {
/** @type {Changes} */
const result = obj();
let dirty = false;
// Check if data was actually changed
for (const p in next) {
if (prev[p] !== next[p]) {
dirty = true;
result[prefix ? prefix + p : p] = {
prev: prev[p],
current: next[p]
};
}
}
return dirty ? result : null;
const result = obj();
let dirty = false;
// Check if data was actually changed
for (const p in next) {
if (prev[p] !== next[p]) {
dirty = true;
result[prefix ? prefix + p : p] = {
prev: prev[p],
current: next[p]
};
}
}
return dirty ? result : null;
}
/**
* Moves contents of given `from` element into `to` element
* @param {Element | DocumentFragment} from
* @param {Element} to
* @returns {Element} The `to` element
* @returns The `to` element
*/
function moveContents(from, to) {
if (from !== to) {
if (from.nodeType === from.DOCUMENT_FRAGMENT_NODE) {
to.appendChild(from);
} else {
let node;
while (node = from.firstChild) {
to.appendChild(node);
}
}
}
return to;
if (from !== to) {
if (from.nodeType === from.DOCUMENT_FRAGMENT_NODE) {
to.appendChild(from);
}
else {
let node;
while (node = from.firstChild) {
to.appendChild(node);
}
}
}
return to;
}
const assign = Object.assign || function(target) {
for (let i = 1, source; i < arguments.length; i++) {
source = arguments[i];
for (let p in source) {
if (source.hasOwnProperty(p)) {
target[p] = source[p];
}
}
}
return target;
// tslint:disable-next-line:only-arrow-functions
const assign = Object.assign || function (target) {
for (let i = 1, source; i < arguments.length; i++) {
source = arguments[i];
for (const p in source) {
if (source.hasOwnProperty(p)) {
target[p] = source[p];
}
}
}
return target;
};
/**
* Returns property descriptors from given object
* @param {Object} obj
* @return {Object}
*/
const getObjectDescriptors = Object.getOwnPropertyDescriptors || function(source) {
const descriptors = obj();
const props = Object.getOwnPropertyNames(source);
for (let i = 0, prop, descriptor; i < props.length; i++) {
prop = props[i];
descriptor = Object.getOwnPropertyDescriptor(source, prop);
if (descriptor != null) {
descriptors[prop] = descriptor;
}
}
return descriptors;
// tslint:disable-next-line:only-arrow-functions
const getObjectDescriptors = Object['getOwnPropertyDescriptors'] || function (source) {
const descriptors = obj();
const props = Object.getOwnPropertyNames(source);
for (let i = 0, prop, descriptor; i < props.length; i++) {
prop = props[i];
descriptor = Object.getOwnPropertyDescriptor(source, prop);
if (descriptor != null) {
descriptors[prop] = descriptor;
}
}
return descriptors;
};
/**

@@ -253,1790 +196,1309 @@ * Represents given attribute value in element

function representAttributeValue(elem, name, value) {
const type = typeof(value);
if (type === 'boolean') {
value = value ? '' : null;
} else if (type === 'function') {
value = '𝑓';
} else if (Array.isArray(value)) {
value = '[]';
} else if (isDefined(value) && type === 'object') {
value = '{}';
}
isDefined(value) ? elem.setAttribute(name, value) : elem.removeAttribute(name);
const type = typeof (value);
if (type === 'boolean') {
value = value ? '' : null;
}
else if (type === 'function') {
value = '𝑓';
}
else if (Array.isArray(value)) {
value = '[]';
}
else if (isDefined(value) && type === 'object') {
value = '{}';
}
isDefined(value) ? elem.setAttribute(name, value) : elem.removeAttribute(name);
}
/**
* Marks given item as explicitly disposable for given host
* @param {Component | Injector} host
* @param {DisposeCallback} callback
* @return {Component | Injector}
*/
function addDisposeCallback(host, callback) {
if (/** @type {Component} */ (host).componentModel) {
/** @type {Component} */ (host).componentModel.dispose = callback;
} else {
/** @type {Injector} */ (host).ctx.dispose = callback;
}
return host;
if ('componentModel' in host) {
host.componentModel.dispose = callback;
}
else if (host.ctx) {
host.ctx.dispose = callback;
}
return host;
}
function safeCall(fn, arg1, arg2) {
try {
return fn && fn(arg1, arg2);
}
catch (err) {
// tslint:disable-next-line:no-console
console.error(err);
}
}
/**
* @param {Function} fn
* @param {*} [arg1]
* @param {*} [arg2]
* Registers given event listener on `target` element and returns event binding
* object to unregister event
*/
function safeCall(fn, arg1, arg2) {
try {
return fn && fn(arg1, arg2);
} catch (err) {
console.error(err);
}
function addStaticEvent(target, type, handleEvent, host, scope) {
return registerBinding({ host, scope, type, handleEvent, target });
}
/**
* Creates element with given tag name
* @param {string} tagName
* @param {string} [cssScope] Scope for CSS isolation
* @return {Element}
* Unregister given event binding
*/
function elem(tagName, cssScope) {
const el = document.createElement(tagName);
cssScope && el.setAttribute(cssScope, '');
return el;
function removeStaticEvent(binding) {
binding.target.removeEventListener(binding.type, binding);
}
/**
* Creates element with given tag name under `ns` namespace
* @param {string} tagName
* @param {string} ns
* @param {string} [cssScope] Scope for CSS isolation
* @return {Element}
* Adds pending event `name` handler
*/
function elemNS(tagName, ns, cssScope) {
const el = document.createElementNS(ns, tagName);
cssScope && el.setAttribute(cssScope, '');
return el;
function addEvent(injector, type, handleEvent, host, scope) {
// We’ll use `ChangeSet` to bind and unbind events only: once binding is registered,
// we will mutate binding props
const { prev, cur } = injector.events;
const binding = cur[type] || prev[type];
if (binding) {
binding.scope = scope;
binding.handleEvent = handleEvent;
cur[type] = binding;
}
else {
cur[type] = { host, scope, type, handleEvent, target: injector.parentNode };
}
}
/**
* Creates element with given tag name and text
* @param {string} tagName
* @param {string} text
* @param {string} [cssScope] Scope for CSS isolation
* @return {Element}
* Finalizes events of given injector
*/
function elemWithText(tagName, text, cssScope) {
const el = elem(tagName, cssScope);
el.textContent = textValue(text);
return el;
function finalizeEvents(injector) {
return finalizeItems(injector.events, changeEvent, injector.parentNode);
}
function registerBinding(binding) {
binding.target.addEventListener(binding.type, binding);
return binding;
}
/**
* Creates element with given tag name under `ns` namespace and text
* @param {string} tagName
* @param {string} ns
* @param {string} text
* @param {string} [cssScope] Scope for CSS isolation
* @return {Element}
* Invoked when event handler was changed
*/
function elemNSWithText(tagName, ns, text, cssScope) {
const el = elemNS(tagName, ns, cssScope);
el.textContent = textValue(text);
return el;
function changeEvent(name, prevValue, newValue) {
if (!prevValue && newValue) {
// Should register new binding
registerBinding(newValue);
}
else if (prevValue && !newValue) {
removeStaticEvent(prevValue);
}
}
/**
* Creates text node with given value
* @param {String} value
* @returns {Text}
* Sets value of attribute `name` to `value`
* @return Update status. Always returns `0` since actual attribute value
* is defined in `finalizeAttributes()`
*/
function text(value) {
const node = document.createTextNode(textValue(value));
node['$value'] = value;
return node;
function setAttribute(injector, name, value) {
injector.attributes.cur[name] = value;
return 0;
}
/**
* Updates given text node value, if required
* @param {Text} node
* @param {*} value
* @returns {number} Returns `1` if text was updated, `0` otherwise
* Sets value of attribute `name` under namespace of `nsURI` to `value`
*/
function updateText(node, value) {
if (value !== node['$value']) {
node.nodeValue = textValue(value);
node['$value'] = value;
return 1;
}
return 0;
function setAttributeNS(injector, nsURI, name, value) {
if (!injector.attributesNS) {
injector.attributesNS = obj();
}
const { attributesNS } = injector;
if (!attributesNS[nsURI]) {
attributesNS[nsURI] = changeSet();
}
attributesNS[nsURI].cur[name] = value;
}
/**
* @param {Node} node
* @param {Node} parent
* @param {Node} anchor
* @returns {Node} Inserted item
* Updates `attrName` value in `elem`, if required
* @returns New attribute value
*/
function domInsert(node, parent, anchor) {
return anchor
? parent.insertBefore(node, anchor)
: parent.appendChild(node);
function updateAttribute(elem, attrName, value, prevValue) {
if (value !== prevValue) {
changeAttribute(attrName, prevValue, value, elem);
return value;
}
return prevValue;
}
/**
* Removes given DOM node from its tree
* @param {Node} node
* Updates props in given component, if required
* @return Returns `true` if value was updated
*/
function domRemove(node) {
const { parentNode } = node;
parentNode && parentNode.removeChild(node);
function updateProps(elem, data) {
const { props } = elem;
let updated;
for (const p in data) {
if (data.hasOwnProperty(p) && props[p] !== data[p]) {
if (!updated) {
updated = obj();
}
updated[p] = data[p];
}
}
if (updated) {
elem.setProps(data);
return true;
}
return false;
}
/**
* Adds given class name as pending attribute
*/
function addClass(injector, value) {
if (isDefined(value)) {
const className = injector.attributes.cur.class;
setAttribute(injector, 'class', isDefined(className) ? className + ' ' + value : value);
}
}
/**
* Applies pending attributes changes to injector’s host element
*/
function finalizeAttributes(injector) {
const { attributes, attributesNS } = injector;
if (isDefined(attributes.cur.class)) {
attributes.cur.class = normalizeClassName(attributes.cur.class);
}
let updated = finalizeItems(attributes, changeAttribute, injector.parentNode);
if (attributesNS) {
const ctx = { node: injector.parentNode, ns: null };
for (const ns in attributesNS) {
ctx.ns = ns;
updated |= finalizeItems(attributesNS[ns], changeAttributeNS, ctx);
}
}
return updated;
}
/**
* Normalizes given class value: removes duplicates and trims whitespace
*/
function normalizeClassName(str) {
const out = [];
const parts = String(str).split(/\s+/);
for (let i = 0, cl; i < parts.length; i++) {
cl = parts[i];
if (cl && out.indexOf(cl) === -1) {
out.push(cl);
}
}
return out.join(' ');
}
/**
* Callback for changing attribute value
*/
function changeAttribute(name, prevValue, newValue, elem) {
if (isDefined(newValue)) {
representAttributeValue(elem, name, newValue);
}
else if (isDefined(prevValue)) {
elem.removeAttribute(name);
}
}
/**
* Callback for changing attribute value
*/
function changeAttributeNS(name, prevValue, newValue, ctx) {
if (isDefined(newValue)) {
ctx.node.setAttributeNS(ctx.ns, name, newValue);
}
else if (isDefined(prevValue)) {
ctx.node.removeAttributeNS(ctx.ns, name);
}
}
/**
* Returns textual representation of given `value` object
* @param {*} value
* @returns {string}
* Creates linted list
*/
function textValue(value) {
return value != null ? value : '';
function createList() {
return { head: null };
}
/**
* Creates linked list item
*/
function createListItem(value) {
return { value, next: null, prev: null };
}
/**
* Prepends given value to linked list
*/
function listPrependValue(list, value) {
const item = createListItem(value);
if (item.next = list.head) {
item.next.prev = item;
}
return list.head = item;
}
/**
* Inserts given value after given `ref` item
*/
function listInsertValueAfter(value, ref) {
const item = createListItem(value);
const { next } = ref;
ref.next = item;
item.prev = ref;
if (item.next = next) {
next.prev = item;
}
return item;
}
/**
* Moves list fragment with `start` and `end` bounds right after `ref` item
*/
function listMoveFragmentAfter(list, start, end, ref) {
listDetachFragment(list, start, end);
if (end.next = ref.next) {
end.next.prev = end;
}
ref.next = start;
start.prev = ref;
}
/**
* Moves list fragment with `start` and `end` to list head
*/
function listMoveFragmentFirst(list, start, end) {
listDetachFragment(list, start, end);
if (end.next = list.head) {
end.next.prev = end;
}
list.head = start;
}
/**
* Detaches list fragment with `start` and `end` from list
*/
function listDetachFragment(list, start, end) {
const { prev } = start;
const { next } = end;
if (prev) {
prev.next = next;
}
else {
list.head = next;
}
if (next) {
next.prev = prev;
}
start.prev = end.next = null;
}
/**
* Creates injector instance for given target, if required
* @param {Element} target
* @returns {Injector}
*/
function createInjector(target) {
return {
parentNode: target,
items: createList(),
ctx: null,
ptr: null,
// NB create `slots` placeholder to promote object to hidden class.
// Do not use any additional function argument for adding value to `slots`
// to reduce runtime checks and keep functions in monomorphic state
slots: null,
attributes: changeSet(),
events: changeSet()
};
return {
parentNode: target,
items: createList(),
ctx: null,
ptr: null,
// NB create `slots` placeholder to promote object to hidden class.
// Do not use any additional function argument for adding value to `slots`
// to reduce runtime checks and keep functions in monomorphic state
slots: null,
attributes: changeSet(),
events: changeSet()
};
}
/**
* Inserts given node into current context
* @param {Injector} injector
* @param {Node} node
* @returns {Node}
*/
function insert(injector, node, slotName = '') {
let target;
const { items, slots, ptr } = injector;
if (slots) {
target = slots[slotName] || (slots[slotName] = document.createDocumentFragment());
} else {
target = injector.parentNode;
}
domInsert(node, target, ptr && getAnchorNode(ptr.next, target));
injector.ptr = ptr ? listInsertValueAfter(node, ptr) : listPrependValue(items, node);
return node;
let target;
const { items, slots, ptr } = injector;
if (slots) {
target = slots[slotName] || (slots[slotName] = document.createDocumentFragment());
}
else {
target = injector.parentNode;
}
domInsert(node, target, ptr ? getAnchorNode(ptr.next, target) : void 0);
injector.ptr = ptr ? listInsertValueAfter(node, ptr) : listPrependValue(items, node);
return node;
}
/**
* Injects given block
* @template {BaseBlock} T
* @param {Injector} injector
* @param {T} block
* @returns {T}
*/
function injectBlock(injector, block) {
const { items, ptr } = injector;
if (ptr) {
block.end = listInsertValueAfter(block, ptr);
block.start = listInsertValueAfter(block, ptr);
} else {
block.end = listPrependValue(items, block);
block.start = listPrependValue(items, block);
}
injector.ptr = block.end;
return block;
const { items, ptr } = injector;
if (ptr) {
block.end = listInsertValueAfter(block, ptr);
block.start = listInsertValueAfter(block, ptr);
}
else {
block.end = listPrependValue(items, block);
block.start = listPrependValue(items, block);
}
block.$$block = true;
injector.ptr = block.end;
return block;
}
/**
* Runs `fn` template function in context of given `block`
* @param {BaseBlock} block
* @param {Function} fn
* @param {*} data
* @returns {*} Result of `fn` function call
*/
function run(block, fn, data) {
const { host, injector } = block;
const { ctx } = injector;
injector.ctx = block;
injector.ptr = block.start;
const result = fn(host, injector, data);
injector.ptr = block.end;
injector.ctx = ctx;
return result;
const { host, injector } = block;
const { ctx } = injector;
injector.ctx = block;
injector.ptr = block.start;
const result = fn(host, injector, data);
injector.ptr = block.end;
injector.ctx = ctx;
return result;
}
/**
* Empties content of given block
* @param {BaseBlock} block
*/
function emptyBlockContent(block) {
if (block.dispose) {
block.dispose(block.scope);
block.dispose = null;
}
let item = block.start.next;
while (item && item !== block.end) {
let { value, next, prev } = item;
if (isBlock(value)) {
next = value.end.next;
disposeBlock(value);
} else if (!value[animatingKey]) {
domRemove(value);
}
prev.next = next;
next.prev = prev;
item = next;
}
if (block.dispose) {
block.dispose(block.scope);
block.dispose = null;
}
let item = block.start.next;
while (item && item !== block.end) {
// tslint:disable-next-line:prefer-const
let { value, next, prev } = item;
if (isBlock(value)) {
next = value.end.next;
disposeBlock(value);
}
else if (!value[animatingKey]) {
domRemove(value);
}
// NB: Block always contains `.next` and `.prev` items which are block
// bounds so we can safely skip null check here
prev.next = next;
next.prev = prev;
item = next;
}
}
/**
* Moves contents of `block` after `ref` list item
* @param {Injector} injector
* @param {BaseBlock} block
* @param {LinkedListItem<any>} [ref]
*/
function move(injector, block, ref) {
if (ref && ref.next && ref.next.value === block) {
return;
}
// Update linked list
const { start, end } = block;
if (ref) {
listMoveFragmentAfter(injector.items, start, end, ref);
} else {
listMoveFragmentFirst(injector.items, start, end);
}
// Move block contents in DOM
let item = start.next, node;
while (item !== end) {
if (!isBlock(item.value)) {
/** @type {Node} */
node = item.value;
// NB it’s possible that a single block contains nodes from different
// slots so we have to find anchor for each node individually
domInsert(node, node.parentNode, getAnchorNode(end.next, node.parentNode));
}
item = item.next;
}
if (ref && ref.next && ref.next.value === block) {
return;
}
// Update linked list
const { start, end } = block;
if (ref) {
listMoveFragmentAfter(injector.items, start, end, ref);
}
else {
listMoveFragmentFirst(injector.items, start, end);
}
// Move block contents in DOM
let item = start.next;
let node;
while (item && item !== end) {
if (!isBlock(item.value)) {
node = item.value;
// NB it’s possible that a single block contains nodes from different
// slots so we have to find anchor for each node individually
domInsert(node, node.parentNode, getAnchorNode(end.next, node.parentNode));
}
item = item.next;
}
}
/**
* Disposes given block
* @param {BaseBlock} block
*/
function disposeBlock(block) {
emptyBlockContent(block);
listDetachFragment(block.injector.items, block.start, block.end);
block.start = block.end = null;
emptyBlockContent(block);
listDetachFragment(block.injector.items, block.start, block.end);
// @ts-ignore: Nulling disposed object
block.start = block.end = null;
}
/**
* Check if given value is a block
* @param {*} obj
* @returns {boolean}
*/
function isBlock(obj$$1) {
return '$$block' in obj$$1;
function isBlock(obj) {
return '$$block' in obj;
}
/**
* Get DOM node nearest to given position of items list
* @param {LinkedListItem} item
* @param {Node} parent Ensure element has given element as parent node
* @returns {Node}
*/
function getAnchorNode(item, parent) {
while (item) {
if (item.value.parentNode === parent) {
return item.value;
}
while (item) {
if (item.value.parentNode === parent) {
return item.value;
}
item = item.next;
}
}
item = item.next;
}
/**
* Walks over each definition (including given one) and runs callback on it
*/
function walkDefinitions(definition, fn) {
safeCall(fn, definition);
const { plugins } = definition;
if (plugins) {
for (let i = 0; i < plugins.length; i++) {
walkDefinitions(plugins[i], fn);
}
}
}
/**
* Same as `walkDefinitions` but runs in reverse order
*/
function reverseWalkDefinitions(definition, fn) {
const { plugins } = definition;
if (plugins) {
let i = plugins.length;
while (i--) {
walkDefinitions(plugins[i], fn);
}
}
safeCall(fn, definition);
}
/**
* Invokes `name` hook for given component definition
*/
function runHook(elem, name, arg1, arg2) {
walkDefinitions(elem.componentModel.definition, dfn => {
const hook = dfn[name];
if (typeof hook === 'function') {
hook(elem, arg1, arg2);
}
});
}
/**
* Enters new variable scope context
* @param {Component} host
* @param {object} incoming
* @return {Object}
*/
function enterScope(host, incoming) {
return setScope(host, createScope(host, incoming));
return setScope(host, createScope(host, incoming));
}
/**
* Exit from current variable scope
* @param {Component} host
* @returns {Object}
*/
function exitScope(host) {
return setScope(host, Object.getPrototypeOf(host.componentModel.vars));
return setScope(host, Object.getPrototypeOf(host.componentModel.vars));
}
/**
* Creates new scope from given component state
* @param {Component} host
* @param {Object} [incoming]
* @return {Object}
*/
function createScope(host, incoming) {
return assign(obj(host.componentModel.vars), incoming);
return assign(obj(host.componentModel.vars), incoming);
}
/**
* Sets given object as current component scope
* @param {Component} host
* @param {Object} scope
* @returns {Object}
*/
function setScope(host, scope) {
return host.componentModel.vars = scope;
return host.componentModel.vars = scope;
}
/**
* Returns current variable scope
* @param {Component} elem
* @returns {object}
*/
function getScope(elem) {
return elem.componentModel.vars;
return elem.componentModel.vars;
}
/**
* Returns property with given name from component
* @param {Component} elem
* @param {string} name
* @return {*}
*/
function getProp(elem, name) {
return elem.props[name];
return elem.props[name];
}
/**
* Returns state value with given name from component
* @param {Component} elem
* @param {string} name
* @return {*}
*/
function getState(elem, name) {
return elem.state[name];
return elem.state[name];
}
/**
* Returns value of given runtime variable from component
* @param {Component} elem
* @param {string} name
* @returns {*}
*/
function getVar(elem, name) {
return elem.componentModel.vars[name];
return elem.componentModel.vars[name];
}
/**
* Sets value of given runtime variable for component
* @param {Component} elem
* @param {string} name
* @param {*} value
*/
function setVar(elem, name, value) {
elem.componentModel.vars[name] = value;
elem.componentModel.vars[name] = value;
}
/**
* @param {Component} host
* @param {Injector} injector
* @param {Function} get
* @returns {FunctionBlock}
*/
function mountBlock(host, injector, get) {
/** @type {FunctionBlock} */
const block = injectBlock(injector, {
$$block: true,
host,
injector,
scope: getScope(host),
dispose: null,
get,
fn: undefined,
update: undefined,
start: null,
end: null
});
updateBlock(block);
return block;
const block = injectBlock(injector, {
host,
injector,
scope: getScope(host),
dispose: null,
get,
fn: undefined,
update: undefined
});
updateBlock(block);
return block;
}
/**
* Updated block, described in `ctx` object
* @param {FunctionBlock} block
* @returns {number} Returns `1` if block was updated, `0` otherwise
* @returns Returns `1` if block was updated, `0` otherwise
*/
function updateBlock(block) {
let updated = 0;
const { scope } = block;
const fn = block.get(block.host, scope);
if (block.fn !== fn) {
updated = 1;
// Unmount previously rendered content
block.fn && emptyBlockContent(block);
// Mount new block content
block.update = fn && run(block, fn, scope);
block.fn = fn;
} else if (block.update) {
// Update rendered result
updated = run(block, block.update, scope) ? 1 : 0;
}
block.injector.ptr = block.end;
return updated;
let updated = 0;
const { scope } = block;
const fn = block.get(block.host, scope);
if (block.fn !== fn) {
updated = 1;
// Unmount previously rendered content
block.fn && emptyBlockContent(block);
// Mount new block content
block.update = fn && run(block, fn, scope);
block.fn = fn;
}
else if (block.update) {
// Update rendered result
updated = run(block, block.update, scope) ? 1 : 0;
}
block.injector.ptr = block.end;
return updated;
}
/**
* @param {FunctionBlock} block
*/
function unmountBlock(block) {
disposeBlock(block);
disposeBlock(block);
}
/**
* Mounts iterator block
* @param {Component} host
* @param {Injector} injector
* @param {Function} get A function that returns collection to iterate
* @param {Function} body A function that renders item of iterated collection
* @returns {IteratorBlock}
* Registers given element as output slot for `host` component
* @param defaultContent Function for rendering default slot content
*/
function mountIterator(host, injector, get, body) {
/** @type {IteratorBlock} */
const block = injectBlock(injector, {
$$block: true,
host,
injector,
scope: getScope(host),
dispose: null,
get,
body,
index: 0,
updated: 0,
start: null,
end: null
});
updateIterator(block);
return block;
function mountSlot(host, name, elem, defaultContent) {
const ctx = { host, name, defaultContent, isDefault: false };
const { slots } = host.componentModel;
const injector = createInjector(elem);
const blockEntry = () => {
ctx.isDefault = !renderSlot(host, injector);
return ctx.isDefault ? ctx.defaultContent : void 0;
};
slots[name] = mountBlock(host, injector, blockEntry);
return ctx;
}
/**
* Updates iterator block defined in `ctx`
* @param {IteratorBlock} block
* @returns {number} Returns `1` if iterator was updated, `0` otherwise
* Unmounts given slot
* @param {SlotContext} ctx
*/
function updateIterator(block) {
run(block, iteratorHost, block);
return block.updated;
function unmountSlot(ctx) {
const { host, name } = ctx;
const { slots } = host.componentModel;
if (ctx.isDefault) {
unmountBlock(slots[name]);
}
ctx.defaultContent = void 0;
delete slots[name];
}
/**
* @param {IteratorBlock} block
* Sync slot content if necessary
*/
function unmountIterator(block) {
disposeBlock(block);
function updateSlots(host) {
const { slots, slotStatus, input } = host.componentModel;
for (const name in slots) {
updateBlock(slots[name]);
}
for (const name in slotStatus) {
if (slotStatus[name]) {
runHook(host, 'didSlotUpdate', name, input.slots[name]);
slotStatus[name] = 0;
}
}
}
/**
*
* @param {Component} host
* @param {Injector} injector
* @param {IteratorBlock} block
* Renders incoming contents of given slot
* @returns Returns `true` if slot content was filled with incoming data,
* `false` otherwise
*/
function iteratorHost(host, injector, block) {
block.index = 0;
block.updated = 0;
const collection = block.get(host, block.scope);
if (collection && typeof collection.forEach === 'function') {
collection.forEach(iterator, block);
}
trimIteratorItems(block);
function renderSlot(host, target) {
const { parentNode } = target;
const name = parentNode.getAttribute('name') || '';
const slotted = parentNode.hasAttribute('slotted');
const { input } = host.componentModel;
const source = input.slots[name];
if (source && source.childNodes.length) {
// There’s incoming slot content
if (!slotted) {
parentNode.setAttribute('slotted', '');
input.slots[name] = moveContents(source, parentNode);
}
return true;
}
if (slotted) {
// Parent renderer removed incoming data
parentNode.removeAttribute('slotted');
input[name] = null;
}
return false;
}
/**
* @param {*} scope
* @param {number} index
* @param {*} key
* @param {*} value
* Marks slot update status
*/
function prepareScope(scope, index, key, value) {
scope.index = index;
scope.key = key;
scope.value = value;
return scope;
function markSlotUpdate(component, slotName, status) {
const { slotStatus } = component.componentModel;
if (slotName in slotStatus) {
slotStatus[slotName] |= status;
}
else {
slotStatus[slotName] = status;
}
}
let renderQueue = null;
/**
* Removes remaining iterator items from current context
* @param {IteratorBlock} block
* Creates internal lightweight Endorphin component with given definition
*/
function trimIteratorItems(block) {
/** @type {LinkedListItem<IteratorItemBlock>} */
let item = block.injector.ptr.next, listItem;
while (item.value.owner === block) {
block.updated = 1;
listItem = item.value;
item = listItem.end.next;
disposeBlock(listItem);
}
function createComponent(name, definition, host) {
let cssScope;
let root;
if (host && 'componentModel' in host) {
cssScope = host.componentModel.definition.cssScope;
root = host.root || host;
}
const element = elem(name, cssScope);
// Add host scope marker: we can’t rely on tag name since component
// definition is bound to element in runtime, not compile time
if (definition.cssScope) {
element.setAttribute(definition.cssScope + '-host', '');
}
const { props, state, extend, events } = prepare(element, definition);
element.refs = {};
element.props = obj(props);
element.state = state;
element.componentView = element; // XXX Should point to Shadow Root in Web Components
root && (element.root = root);
addPropsState(element);
if (extend) {
Object.defineProperties(element, extend);
}
if (definition.store) {
element.store = definition.store();
}
else if (root && root.store) {
element.store = root.store;
}
// Create slotted input
const input = createInjector(element.componentView);
input.slots = obj();
element.componentModel = {
definition,
input,
vars: obj(),
refs: changeSet(),
slots: obj(),
slotStatus: obj(),
mounted: false,
rendering: false,
finalizing: false,
update: void 0,
queued: false,
events,
dispose: void 0,
defaultProps: props
};
runHook(element, 'init');
return element;
}
/**
* @this {IteratorBlock}
* @param {*} value
* @param {*} key
* Mounts given component
*/
function iterator(value, key) {
const { host, injector, index } = this;
const { ptr } = injector;
const prevScope = getScope(host);
/** @type {IteratorItemBlock} */
let rendered = ptr.next.value;
if (rendered.owner === this) {
// We have rendered item, update it
if (rendered.update) {
const scope = prepareScope(rendered.scope, index, key, value);
setScope(host, scope);
if (run(rendered, rendered.update, scope)) {
this.updated = 1;
}
setScope(host, prevScope);
}
} else {
// Create & render new block
const scope = prepareScope(obj(prevScope), index, key, value);
/** @type {IteratorItemBlock} */
rendered = injectBlock(injector, {
$$block: true,
host,
injector,
scope,
dispose: null,
update: undefined,
owner: this,
start: null,
end: null
});
setScope(host, scope);
rendered.update = run(rendered, this.body, scope);
setScope(host, prevScope);
this.updated = 1;
}
injector.ptr = rendered.end;
this.index++;
function mountComponent(component, initialProps) {
const { componentModel } = component;
const { input, definition, defaultProps } = componentModel;
let changes = setPropsInternal(component, obj(), assign(obj(defaultProps), initialProps));
const runtimeChanges = setPropsInternal(component, input.attributes.prev, input.attributes.cur);
if (changes && runtimeChanges) {
assign(changes, runtimeChanges);
}
else if (runtimeChanges) {
changes = runtimeChanges;
}
const arg = changes || {};
finalizeEvents(input);
componentModel.rendering = true;
// Notify slot status
for (const p in input.slots) {
runHook(component, 'didSlotUpdate', p, input.slots[p]);
}
if (changes) {
runHook(component, 'didChange', arg);
}
runHook(component, 'willMount', arg);
runHook(component, 'willRender', arg);
componentModel.update = safeCall(definition.default, component, getScope(component));
componentModel.mounted = true;
componentModel.rendering = false;
componentModel.finalizing = true;
runHook(component, 'didRender', arg);
runHook(component, 'didMount', arg);
componentModel.finalizing = false;
}
/**
* Renders key iterator block
* @param {Component} host
* @param {Injector} injector
* @param {Function} get
* @param {Function} keyExpr
* @param {Function} body
* @returns {KeyIteratorBlock}
* Updates given mounted component
*/
function mountKeyIterator(host, injector, get, keyExpr, body) {
const parentScope = getScope(host);
/** @type {KeyIteratorBlock} */
const block = injectBlock(injector, {
$$block: true,
host,
injector,
scope: obj(parentScope),
dispose: null,
get,
body,
keyExpr,
index: 0,
updated: 0,
rendered: null,
needReorder: false,
parentScope,
order: [],
used: null,
start: null,
end: null
});
updateKeyIterator(block);
return block;
function updateComponent(component) {
const { input } = component.componentModel;
const changes = setPropsInternal(component, input.attributes.prev, input.attributes.cur);
finalizeEvents(input);
updateSlots(component);
if (changes || component.componentModel.queued) {
renderNext(component, changes);
}
}
/**
* Updates iterator block defined in `ctx`
* @param {KeyIteratorBlock} block
* @returns {number} Returns `1` if iterator was updated, `0` otherwise
* Destroys given component: removes static event listeners and cleans things up
* @returns Should return nothing since function result will be used
* as shorthand to reset cached value
*/
function updateKeyIterator(block) {
run(block, keyIteratorHost, block);
return block.updated;
function unmountComponent(component) {
const { componentModel } = component;
const { slots, dispose, events } = componentModel;
const scope = getScope(component);
runHook(component, 'willUnmount');
componentModel.mounted = false;
if (events) {
detachStaticEvents(component, events);
}
if (component.store) {
component.store.unwatch(component);
}
safeCall(dispose, scope);
for (const slotName in slots) {
disposeBlock(slots[slotName]);
}
runHook(component, 'didUnmount');
// @ts-ignore: Nulling disposed object
component.componentModel = null;
}
/**
* @param {KeyIteratorBlock} ctx
* Subscribes to store updates of given component
*/
function unmountKeyIterator(ctx) {
disposeBlock(ctx);
function subscribeStore(component, keys) {
if (!component.store) {
throw new Error(`Store is not defined for ${component.nodeName} component`);
}
component.store.watch(component, keys);
}
/**
*
* @param {Component} host
* @param {Injector} injector
* @param {KeyIteratorBlock} block
* Queues next component render
*/
function keyIteratorHost(host, injector, block) {
block.used = obj();
block.index = 0;
block.updated = 0;
block.needReorder = false;
const collection = block.get(host, block.parentScope);
if (collection && typeof collection.forEach === 'function') {
collection.forEach(iterator$1, block);
}
const { rendered } = block;
for (let p in rendered) {
for (let i = 0, items = rendered[p]; i < items.length; i++) {
block.updated = 1;
disposeBlock(items[i]);
}
}
if (block.needReorder) {
reorder(block);
}
block.order.length = 0;
block.rendered = block.used;
function renderNext(component, changes) {
if (!component.componentModel.rendering) {
renderComponent(component, changes);
}
else {
scheduleRender(component, changes);
}
}
/**
* @param {IteratorItemBlock} expected
* @param {KeyIteratorBlock} owner
* @returns {IteratorItemBlock | null}
* Schedules render of given component on next tick
*/
function getItem(expected, owner) {
return expected.owner === owner ? expected : null;
function scheduleRender(component, changes) {
if (!component.componentModel.queued) {
component.componentModel.queued = true;
if (renderQueue) {
renderQueue.push(component, changes);
}
else {
renderQueue = [component, changes];
requestAnimationFrame(drainQueue);
}
}
}
/**
* @this {KeyIteratorBlock}
* @param {*} value
* @param {*} key
* Renders given component
*/
function iterator$1(value, key) {
const { host, injector, index, rendered } = this;
const id = getId(this, index, key, value);
// TODO make `rendered` a linked list for faster insert and remove
let entry = rendered && id in rendered ? rendered[id].shift() : null;
const prevScope = getScope(host);
const scope = prepareScope(entry ? entry.scope : obj(this.scope), index, key, value);
setScope(host, scope);
if (!entry) {
entry = injector.ctx = createItem(this, scope);
injector.ptr = entry.start;
entry.update = this.body(host, injector, scope);
this.updated = 1;
} else if (entry.update) {
if (entry.start.prev !== injector.ptr) {
this.needReorder = true;
}
if (entry.update(host, injector, scope)) {
this.updated = 1;
}
}
setScope(host, prevScope);
markUsed(this, id, entry);
injector.ptr = entry.end;
this.index++;
function renderComponent(component, changes) {
const { componentModel } = component;
const arg = changes || {};
componentModel.queued = false;
componentModel.rendering = true;
if (changes) {
runHook(component, 'didChange', arg);
}
runHook(component, 'willUpdate', arg);
runHook(component, 'willRender', arg);
safeCall(componentModel.update, component, getScope(component));
componentModel.rendering = false;
componentModel.finalizing = true;
runHook(component, 'didRender', arg);
runHook(component, 'didUpdate', arg);
componentModel.finalizing = false;
}
/**
* @param {KeyIteratorBlock} block
* Removes attached events from given map
*/
function reorder(block) {
const { injector, order } = block;
let actualPrev, actualNext;
let expectedPrev, expectedNext;
for (let i = 0, maxIx = order.length - 1, item; i <= maxIx; i++) {
item = order[i];
expectedPrev = i > 0 ? order[i - 1] : null;
expectedNext = i < maxIx ? order[i + 1] : null;
actualPrev = getItem(item.start.prev.value, block);
actualNext = getItem(item.end.next.value, block);
if (expectedPrev !== actualPrev && expectedNext !== actualNext) {
// Blocks must be reordered
move(injector, item, expectedPrev ? expectedPrev.end : block.start);
}
}
function detachStaticEvents(component, eventMap) {
const { listeners, handler } = eventMap;
for (const p in listeners) {
component.removeEventListener(p, handler);
}
}
/**
* @param {KeyIteratorBlock} iterator
* @param {string} id
* @param {IteratorItemBlock} block
*/
function markUsed(iterator, id, block) {
const { used } = iterator;
// We allow multiple items key in case of poorly prepared data.
if (id in used) {
used[id].push(block);
} else {
used[id] = [block];
}
iterator.order.push(block);
function kebabCase(ch) {
return '-' + ch.toLowerCase();
}
/**
* @param {KeyIteratorBlock} iterator
* @param {number} index
* @param {*} key
* @param {*} value
* @return {string}
*/
function getId(iterator, index, key, value) {
return iterator.keyExpr(value, prepareScope(iterator.scope, index, key, value));
function setPropsInternal(component, prevProps, nextProps) {
const changes = {};
let didChanged = false;
const { props } = component;
const { defaultProps } = component.componentModel;
for (const p in nextProps) {
const prev = prevProps[p];
let current = nextProps[p];
if (current == null) {
current = defaultProps[p];
}
if (p === 'class' && current != null) {
current = normalizeClassName(current);
}
if (current !== prev) {
didChanged = true;
props[p] = prevProps[p] = current;
changes[p] = { current, prev };
if (!/^partial:/.test(p)) {
representAttributeValue(component, p.replace(/[A-Z]/g, kebabCase), current);
}
}
nextProps[p] = null;
}
return didChanged ? changes : null;
}
/**
* @param {KeyIteratorBlock} iterator
* @param {Object} scope
* @returns {IteratorItemBlock}
* Check if `next` contains value that differs from one in `prev`
*/
function createItem(iterator, scope) {
return injectBlock(iterator.injector, {
$$block: true,
host: iterator.host,
injector: iterator.injector,
scope,
dispose: null,
update: undefined,
owner: iterator,
start: null,
end: null
});
function hasChanges(prev, next) {
for (const p in next) {
if (next[p] !== prev[p]) {
return true;
}
}
return false;
}
/**
* Sets value of attribute `name` to `value`
* @param {Injector} injector
* @param {string} name
* @param {*} value
* @return {number} Update status. Always returns `0` since actual attribute value
* is defined in `finalizeAttributes()`
* Prepares internal data for given component
*/
function setAttribute(injector, name, value) {
injector.attributes.cur[name] = value;
return 0;
function prepare(component, definition) {
const props = obj();
const state = obj();
let events;
let extend;
reverseWalkDefinitions(definition, dfn => {
dfn.props && assign(props, dfn.props(component));
dfn.state && assign(state, dfn.state(component));
// NB: backward compatibility with previous implementation
if (dfn.methods) {
extend = getDescriptors(dfn.methods, extend);
}
if (dfn.extend) {
extend = getDescriptors(dfn.extend, extend);
}
if (dfn.events) {
if (!events) {
events = createEventsMap(component);
}
attachEventHandlers(component, dfn.events, events);
}
});
return { props, state, extend, events };
}
/**
* Sets value of attribute `name` under namespace of `nsURI` to `value`
*
* @param {Injector} injector
* @param {string} nsURI
* @param {string} name
* @param {*} value
* Extracts property descriptors from given source object and merges it with `prev`
* descriptor map, if given
*/
function setAttributeNS(injector, nsURI, name, value) {
if (!injector.attributesNS) {
injector.attributesNS = obj();
}
const { attributesNS } = injector;
if (!attributesNS[nsURI]) {
attributesNS[nsURI] = changeSet();
}
attributesNS[nsURI].cur[name] = value;
function getDescriptors(source, prev) {
const descriptors = getObjectDescriptors(source);
return prev ? assign(prev, descriptors) : descriptors;
}
/**
* Updates `attrName` value in `elem`, if required
* @param {HTMLElement} elem
* @param {string} attrName
* @param {*} value
* @param {*} prevValue
* @returns {*} New attribute value
*/
function updateAttribute(elem, attrName, value, prevValue) {
if (value !== prevValue) {
changeAttribute(attrName, prevValue, value, elem);
return value;
}
return prevValue;
function createEventsMap(component) {
const listeners = obj();
const handler = function (evt) {
if (component.componentModel) {
const handlers = listeners[evt.type];
for (let i = 0; i < handlers.length; i++) {
handlers[i](component, evt, this);
}
}
};
return { handler, listeners };
}
/**
* Updates props in given component, if required
* @param {Component} elem
* @param {object} data
* @return {boolean} Returns `true` if value was updated
*/
function updateProps(elem, data) {
const { props } = elem;
let updated;
for (let p in data) {
if (data.hasOwnProperty(p) && props[p] !== data[p]) {
if (!updated) {
updated = obj();
}
updated[p] = data[p];
}
}
if (updated) {
elem.setProps(data);
return true;
}
return false;
function attachEventHandlers(component, events, eventMap) {
const names = Object.keys(events);
const { listeners } = eventMap;
for (let i = 0, name; i < names.length; i++) {
name = names[i];
if (name in listeners) {
listeners[name].push(events[name]);
}
else {
component.addEventListener(name, eventMap.handler);
listeners[name] = [events[name]];
}
}
}
/**
* Adds given class name as pending attribute
* @param {Injector} injector
* @param {string} value
*/
function addClass(injector, value) {
if (isDefined(value)) {
const className = injector.attributes.cur['class'];
setAttribute(injector, 'class', isDefined(className) ? className + ' ' + value : value);
}
function addPropsState(element) {
element.setProps = function setProps(value) {
const { componentModel } = element;
// In case of calling `setProps` after component was unmounted,
// check if `componentModel` is available
if (value != null && componentModel && componentModel.mounted) {
const changes = setPropsInternal(element, element.props, obj(value));
changes && renderNext(element, changes);
}
};
element.setState = function setState(value) {
const { componentModel } = element;
// In case of calling `setState` after component was unmounted,
// check if `componentModel` is available
if (value != null && componentModel && hasChanges(element.state, value)) {
assign(element.state, value);
// If we’re in rendering state than current `setState()` is caused by
// one of the `will*` hooks, which means applied changes will be automatically
// applied during rendering stage.
// If called outside of rendering state we should schedule render
// on next tick
if (componentModel.mounted && !componentModel.rendering) {
scheduleRender(element);
}
}
};
}
/**
* Applies pending attributes changes to injector’s host element
* @param {Injector} injector
* @return {number}
*/
function finalizeAttributes(injector) {
const { attributes, attributesNS } = injector;
if (isDefined(attributes.cur['class'])) {
attributes.cur['class'] = normalizeClassName(attributes.cur['class']);
}
let updated = finalizeItems(attributes, changeAttribute, injector.parentNode);
if (attributesNS) {
const ctx = { node: injector.parentNode, ns: null };
for (let ns in attributesNS) {
ctx.ns = ns;
updated |= finalizeItems(attributesNS[ns], changeAttributeNS, ctx);
}
}
return updated;
function drainQueue() {
const pending = renderQueue;
renderQueue = null;
for (let i = 0, component; i < pending.length; i += 2) {
component = pending[i];
// It’s possible that a component can be rendered before next tick
// (for example, if parent node updated component props).
// Check if it’s still queued then render.
// Also, component can be unmounted after it’s rendering was scheduled
if (component.componentModel && component.componentModel.queued) {
renderComponent(component, pending[i + 1]);
}
}
}
/**
* Normalizes given class value: removes duplicates and trims whitespace
* @param {string} str
* @returns {string}
* Mounts iterator block
* @param get A function that returns collection to iterate
* @param body A function that renders item of iterated collection
*/
function normalizeClassName(str) {
/** @type {string[]} */
const out = [];
const parts = String(str).split(/\s+/);
for (let i = 0, cl; i < parts.length; i++) {
cl = parts[i];
if (cl && out.indexOf(cl) === -1) {
out.push(cl);
}
}
return out.join(' ');
function mountIterator(host, injector, get, body) {
const block = injectBlock(injector, {
host,
injector,
scope: getScope(host),
dispose: null,
get,
body,
index: 0,
updated: 0
});
updateIterator(block);
return block;
}
/**
* Callback for changing attribute value
* @param {string} name
* @param {*} prevValue
* @param {*} newValue
* @param {Element} elem
* Updates iterator block defined in `ctx`
* @returns Returns `1` if iterator was updated, `0` otherwise
*/
function changeAttribute(name, prevValue, newValue, elem) {
if (isDefined(newValue)) {
representAttributeValue(elem, name, newValue);
} else if (isDefined(prevValue)) {
elem.removeAttribute(name);
}
function updateIterator(block) {
run(block, iteratorHost, block);
return block.updated;
}
/**
* Callback for changing attribute value
* @param {string} name
* @param {*} prevValue
* @param {*} newValue
* @param {{node: Element, ns: string}} ctx
*/
function changeAttributeNS(name, prevValue, newValue, ctx) {
if (isDefined(newValue)) {
ctx.node.setAttributeNS(ctx.ns, name, newValue);
} else if (isDefined(prevValue)) {
ctx.node.removeAttributeNS(ctx.ns, name);
}
function unmountIterator(block) {
disposeBlock(block);
}
/**
* Adds pending event `name` handler
* @param {Injector} injector
* @param {string} name
* @param {function} handler
*/
function addEvent(injector, name, handler) {
injector.events.cur[name] = handler;
function iteratorHost(host, injector, block) {
block.index = 0;
block.updated = 0;
const collection = block.get(host, block.scope);
if (collection && typeof collection.forEach === 'function') {
collection.forEach(iterator, block);
}
trimIteratorItems(block);
}
/**
* Adds given `handler` as event `name` listener
* @param {Element} elem
* @param {string} name
* @param {EventListener} handler
*/
function addStaticEvent(elem, name, handler) {
handler && elem.addEventListener(name, handler);
function prepareScope(scope, index, key, value) {
scope.index = index;
scope.key = key;
scope.value = value;
return scope;
}
/**
* Finalizes events of given injector
* @param {Injector} injector
* @returns {number} Update status
* Removes remaining iterator items from current context
*/
function finalizeEvents(injector) {
return finalizeItems(injector.events, changeEvent, injector.parentNode);
function trimIteratorItems(block) {
let item = block.injector.ptr.next;
let listItem;
while (item && item.value.owner === block) {
block.updated = 1;
listItem = item.value;
item = listItem.end.next;
disposeBlock(listItem);
}
}
/**
* Returns function that must be invoked as event handler for given component
* @param {Component} component
* @param {string} name Method name
* @param {HTMLElement} ctx Context element where event listener was added
* @returns {function?}
*/
function getEventHandler(component, name, ctx) {
let fn;
if (typeof component[name] === 'function') {
fn = component[name].bind(component);
} else {
const handler = component.componentModel.definition[name];
if (typeof handler === 'function') {
fn = handler.bind(ctx);
}
}
if (fn) {
fn.displayName = name;
}
return fn;
function iterator(value, key) {
const { host, injector, index } = this;
const { ptr } = injector;
const prevScope = getScope(host);
let rendered = ptr.next.value;
if (rendered.owner === this) {
// We have rendered item, update it
if (rendered.update) {
const scope = prepareScope(rendered.scope, index, key, value);
setScope(host, scope);
if (run(rendered, rendered.update, scope)) {
this.updated = 1;
}
setScope(host, prevScope);
}
}
else {
// Create & render new block
const scope = prepareScope(obj(prevScope), index, key, value);
rendered = injectBlock(injector, {
host,
injector,
scope,
dispose: null,
update: undefined,
owner: this
});
setScope(host, scope);
rendered.update = run(rendered, this.body, scope);
setScope(host, prevScope);
this.updated = 1;
}
injector.ptr = rendered.end;
this.index++;
}
/**
* Invoked when event handler was changed
* @param {string} name
* @param {EventListener} prevValue
* @param {EventListener} newValue
* @param {Element} elem
* Renders key iterator block
*/
function changeEvent(name, prevValue, newValue, elem) {
prevValue && elem.removeEventListener(name, prevValue);
addStaticEvent(elem, name, newValue);
function mountKeyIterator(host, injector, get, keyExpr, body) {
const parentScope = getScope(host);
const block = injectBlock(injector, {
host,
injector,
scope: obj(parentScope),
dispose: null,
get,
body,
keyExpr,
index: 0,
updated: 0,
rendered: null,
needReorder: false,
parentScope,
order: [],
used: null
});
updateKeyIterator(block);
return block;
}
/**
* Walks over each definition (including given one) and runs callback on it
* @param {ComponentDefinition} definition
* @param {(dfn: ComponentDefinition) => void} fn
* Updates iterator block defined in `ctx`
* @returns Returns `1` if iterator was updated, `0` otherwise
*/
function walkDefinitions(definition, fn) {
safeCall(fn, definition);
const { plugins } = definition;
if (plugins) {
for (let i = 0; i < plugins.length; i++) {
walkDefinitions(plugins[i], fn);
}
}
function updateKeyIterator(block) {
run(block, keyIteratorHost, block);
return block.updated;
}
/**
* Same as `walkDefinitions` but runs in reverse order
* @param {ComponentDefinition} definition
* @param {(dfn: ComponentDefinition) => void} fn
*/
function reverseWalkDefinitions(definition, fn) {
const { plugins } = definition;
if (plugins) {
let i = plugins.length;
while (i--) {
walkDefinitions(plugins[i], fn);
}
}
safeCall(fn, definition);
function unmountKeyIterator(ctx) {
disposeBlock(ctx);
}
/**
* Invokes `name` hook for given component definition
* @param {Component} elem
* @param {string} name
* @param {*} [arg1]
* @param {*} [arg2]
*/
function runHook(elem, name, arg1, arg2) {
walkDefinitions(elem.componentModel.definition, dfn => {
const hook = dfn[name];
if (typeof hook === 'function') {
hook(elem, arg1, arg2);
}
});
function keyIteratorHost(host, injector, block) {
block.used = obj();
block.index = 0;
block.updated = 0;
block.needReorder = false;
const collection = block.get(host, block.parentScope);
if (collection && typeof collection.forEach === 'function') {
collection.forEach(iterator$1, block);
}
const { rendered } = block;
for (const p in rendered) {
for (let i = 0, items = rendered[p]; i < items.length; i++) {
block.updated = 1;
disposeBlock(items[i]);
}
}
if (block.needReorder) {
reorder(block);
}
block.order.length = 0;
block.rendered = block.used;
}
/**
* Registers given element as output slot for `host` component
* @param {Component} host
* @param {string} name
* @param {HTMLElement} elem
* @param {Function} [defaultContent] Function for rendering default slot content
* @return {SlotContext}
* @param {KeyIteratorItemBlock} expected
* @param {KeyIteratorBlock} owner
* @returns {KeyIteratorItemBlock | null}
*/
function mountSlot(host, name, elem, defaultContent) {
/** @type {SlotContext} */
const ctx = { host, name, defaultContent, isDefault: false };
const { slots } = host.componentModel;
const injector = createInjector(elem);
function blockEntry() {
ctx.isDefault = !renderSlot(host, injector);
return ctx.isDefault ? ctx.defaultContent : null;
}
slots[name] = mountBlock(host, injector, blockEntry);
return ctx;
function getItem(expected, owner) {
return expected.owner === owner ? expected : null;
}
/**
* Unmounts given slot
* @param {SlotContext} ctx
*/
function unmountSlot(ctx) {
const { host, name } = ctx;
const { slots } = host.componentModel;
if (ctx.isDefault) {
unmountBlock(slots[name]);
}
ctx.defaultContent = null;
delete slots[name];
function iterator$1(value, key) {
const { host, injector, index, rendered } = this;
const id = getId(this, index, key, value);
// TODO make `rendered` a linked list for faster insert and remove
let entry = rendered && id in rendered ? rendered[id].shift() : null;
const prevScope = getScope(host);
const scope = prepareScope(entry ? entry.scope : obj(this.scope), index, key, value);
setScope(host, scope);
if (!entry) {
entry = injector.ctx = createItem(this, scope);
injector.ptr = entry.start;
entry.update = this.body(host, injector, scope);
this.updated = 1;
}
else if (entry.update) {
if (entry.start.prev !== injector.ptr) {
this.needReorder = true;
}
if (entry.update(host, injector, scope)) {
this.updated = 1;
}
}
setScope(host, prevScope);
markUsed(this, id, entry);
injector.ptr = entry.end;
this.index++;
}
/**
* Sync slot content if necessary
* @param {Component} host
*/
function updateSlots(host) {
const { slots, slotStatus, input } = host.componentModel;
for (const name in slots) {
updateBlock(slots[name]);
}
for (const name in slotStatus) {
if (slotStatus[name]) {
runHook(host, 'didSlotUpdate', name, input.slots[name]);
slotStatus[name] = 0;
}
}
function reorder(block) {
const { injector, order } = block;
let actualPrev;
let actualNext;
let expectedPrev;
let expectedNext;
for (let i = 0, maxIx = order.length - 1, item; i <= maxIx; i++) {
item = order[i];
expectedPrev = i > 0 ? order[i - 1] : null;
expectedNext = i < maxIx ? order[i + 1] : null;
actualPrev = getItem(item.start.prev.value, block);
actualNext = getItem(item.end.next.value, block);
if (expectedPrev !== actualPrev && expectedNext !== actualNext) {
// Blocks must be reordered
move(injector, item, expectedPrev ? expectedPrev.end : block.start);
}
}
}
/**
* Renders incoming contents of given slot
* @param {Component} host
* @param {Injector} target
* @returns {boolean} Returns `true` if slot content was filled with incoming data,
* `false` otherwise
*/
function renderSlot(host, target) {
const { parentNode } = target;
const name = parentNode.getAttribute('name') || '';
const slotted = parentNode.hasAttribute('slotted');
const { input } = host.componentModel;
/** @type {Element | DocumentFragment} */
const source = input.slots[name];
if (source && source.childNodes.length) {
// There’s incoming slot content
if (!slotted) {
parentNode.setAttribute('slotted', '');
input.slots[name] = moveContents(source, parentNode);
}
return true;
}
if (slotted) {
// Parent renderer removed incoming data
parentNode.removeAttribute('slotted');
input[name] = null;
}
return false;
function markUsed(iter, id, block) {
const { used } = iter;
// We allow multiple items key in case of poorly prepared data.
if (id in used) {
used[id].push(block);
}
else {
used[id] = [block];
}
iter.order.push(block);
}
/**
* Marks slot update status
* @param {Component} component
* @param {string} slotName
* @param {number} status
*/
function markSlotUpdate(component, slotName, status) {
const { slotStatus } = component.componentModel;
if (slotName in slotStatus) {
slotStatus[slotName] |= status;
} else {
slotStatus[slotName] = status;
}
function getId(iter, index, key, value) {
return iter.keyExpr(value, prepareScope(iter.scope, index, key, value));
}
function createItem(iter, scope) {
return injectBlock(iter.injector, {
$$block: true,
host: iter.host,
injector: iter.injector,
scope,
dispose: null,
update: undefined,
owner: iter
});
}
/**
* Sets runtime ref (e.g. ref which will be changed over time) to given host
* @param {Component} host
* @param {string} name
* @param {HTMLElement} elem
* @returns {number} Update status. Refs must be explicitly finalized, thus
* @returns Update status. Refs must be explicitly finalized, thus
* we always return `0` as nothing was changed
*/
function setRef(host, name, elem) {
host.componentModel.refs.cur[name] = elem;
return 0;
host.componentModel.refs.cur[name] = elem;
return 0;
}
/**
* Sets static ref (e.g. ref which won’t be changed over time) to given host
* @param {Component} host
* @param {string} name
* @param {Element} value
*/
function setStaticRef(host, name, value) {
value && value.setAttribute(getRefAttr(name, host), '');
host.refs[name] = value;
value && value.setAttribute(getRefAttr(name, host), '');
host.refs[name] = value;
}
/**
* Finalizes refs on given scope
* @param {Component} host
* @returns {number} Update status
* @returns Update status
*/
function finalizeRefs(host) {
return finalizeItems(host.componentModel.refs, changeRef, host);
return finalizeItems(host.componentModel.refs, changeRef, host);
}
/**
* Invoked when element reference was changed
* @param {string} name
* @param {Element} prevValue
* @param {Element} newValue
* @param {Component} host
*/
function changeRef(name, prevValue, newValue, host) {
prevValue && prevValue.removeAttribute(getRefAttr(name, host));
setStaticRef(host, name, newValue);
prevValue && prevValue.removeAttribute(getRefAttr(name, host));
setStaticRef(host, name, newValue);
}
/**
* Returns attribute name to identify element in CSS
* @param {String} name
* @param {Component} host
*/
function getRefAttr(name, host) {
const cssScope$$1 = host.componentModel.definition.cssScope;
return 'ref-' + name + (cssScope$$1 ? '-' + cssScope$$1 : '');
const cssScope = host.componentModel.definition.cssScope;
return 'ref-' + name + (cssScope ? '-' + cssScope : '');
}
/** @type {Array} */
let renderQueue = null;
/**
* Creates internal lightweight Endorphin component with given definition
* @param {string} name
* @param {ComponentDefinition} definition
* @param {Component} [host]
* @returns {Component}
*/
function createComponent(name, definition, host) {
const element = /** @type {Component} */ (elem(name, host && host.componentModel && host.componentModel.definition.cssScope));
// Add host scope marker: we can’t rely on tag name since component
// definition is bound to element in runtime, not compile time
const { cssScope: cssScope$$1 } = definition;
if (cssScope$$1) {
element.setAttribute(cssScope$$1 + '-host', '');
}
if (host && host.componentModel) {
// Passed component as parent: detect app root
element.root = host.root || host;
}
// XXX Should point to Shadow Root in Web Components
element.componentView = element;
const { props, state, extend, methods, events } = prepare(element, definition);
element.refs = {};
element.props = obj(props);
element.state = state;
element.setProps = function setProps(value) {
const { componentModel } = element;
// In case of calling `setProps` after component was unmounted,
// check if `componentModel` is available
if (value != null && componentModel && componentModel.mounted) {
const changes = setPropsInternal(element, element.props, obj(value));
changes && renderNext(element, changes);
}
};
element.setState = function setState(value) {
const { componentModel } = element;
// In case of calling `setState` after component was unmounted,
// check if `componentModel` is available
if (value != null && componentModel && hasChanges(element.state, value)) {
assign(element.state, value);
// If we’re in rendering state than current `setState()` is caused by
// one of the `will*` hooks, which means applied changes will be automatically
// applied during rendering stage.
// If called outside of rendering state we should schedule render
// on next tick
if (componentModel.mounted && !componentModel.rendering) {
scheduleRender(element);
}
}
};
assign(element, methods);
if (extend) {
Object.defineProperties(element, extend);
}
if (definition.store) {
element.store = definition.store();
} else if (element.root && element.root.store) {
element.store = element.root.store;
}
// Create slotted input
const input = createInjector(element.componentView);
input.slots = obj();
element.componentModel = {
definition,
input,
vars: obj(),
refs: changeSet(),
slots: obj(),
slotStatus: obj(),
mounted: false,
rendering: false,
finalizing: false,
update: null,
queued: false,
events,
dispose: null,
defaultProps: props
};
runHook(element, 'init');
return element;
}
/**
* Mounts given component
* @param {Component} elem
* @param {object} [initialProps]
*/
function mountComponent(elem$$1, initialProps) {
const { componentModel } = elem$$1;
const { input, definition, defaultProps } = componentModel;
let changes = setPropsInternal(elem$$1, obj(), assign(obj(defaultProps), initialProps));
const runtimeChanges = setPropsInternal(elem$$1, input.attributes.prev, input.attributes.cur);
if (changes && runtimeChanges) {
assign(changes, runtimeChanges);
} else if (runtimeChanges) {
changes = runtimeChanges;
}
const arg = changes || {};
finalizeEvents(input);
componentModel.rendering = true;
// Notify slot status
for (const p in input.slots) {
runHook(elem$$1, 'didSlotUpdate', p, input.slots[p]);
}
if (changes) {
runHook(elem$$1, 'didChange', arg);
}
runHook(elem$$1, 'willMount', arg);
runHook(elem$$1, 'willRender', arg);
componentModel.update = safeCall(definition.default, elem$$1, getScope(elem$$1));
componentModel.mounted = true;
componentModel.rendering = false;
componentModel.finalizing = true;
runHook(elem$$1, 'didRender', arg);
runHook(elem$$1, 'didMount', arg);
componentModel.finalizing = false;
}
/**
* Updates given mounted component
* @param {Component} elem
*/
function updateComponent(elem$$1) {
const { input } = elem$$1.componentModel;
const changes = setPropsInternal(elem$$1, input.attributes.prev, input.attributes.cur);
finalizeEvents(input);
updateSlots(elem$$1);
if (changes || elem$$1.componentModel.queued) {
renderNext(elem$$1, changes);
}
}
/**
* Destroys given component: removes static event listeners and cleans things up
* @param {Component} elem
* @returns {void} Should return nothing since function result will be used
* as shorthand to reset cached value
*/
function unmountComponent(elem$$1) {
const { componentModel } = elem$$1;
const { slots, input, dispose, events } = componentModel;
const scope = getScope(elem$$1);
runHook(elem$$1, 'willUnmount');
componentModel.mounted = false;
if (events) {
detachStaticEvents(elem$$1, events);
}
if (elem$$1.store) {
elem$$1.store.unwatch(elem$$1);
}
// Detach own handlers
// XXX doesn’t remove static events (via direct call of `addStaticEvent()`)
const ownHandlers = input.events.prev;
for (let p in ownHandlers) {
elem$$1.removeEventListener(p, ownHandlers[p]);
}
safeCall(dispose, scope);
for (const slotName in slots) {
disposeBlock(slots[slotName]);
}
runHook(elem$$1, 'didUnmount');
elem$$1.componentModel = null;
}
/**
* Subscribes to store updates of given component
* @param {Component} component
* @param {string[]} [keys]
*/
function subscribeStore(component, keys) {
if (!component.store) {
throw new Error(`Store is not defined for ${component.nodeName} component`);
}
component.store.watch(component, keys);
}
/**
* Queues next component render
* @param {Component} elem
* @param {Object} [changes]
*/
function renderNext(elem$$1, changes) {
if (!elem$$1.componentModel.rendering) {
renderComponent(elem$$1, changes);
} else {
scheduleRender(elem$$1, changes);
}
}
/**
* Schedules render of given component on next tick
* @param {Component} elem
* @param {Changes} [changes]
*/
function scheduleRender(elem$$1, changes) {
if (!elem$$1.componentModel.queued) {
elem$$1.componentModel.queued = true;
if (renderQueue) {
renderQueue.push(elem$$1, changes);
} else {
renderQueue = [elem$$1, changes];
requestAnimationFrame(drainQueue);
}
}
}
/**
* Renders given component
* @param {Component} elem
* @param {Changes} [changes]
*/
function renderComponent(elem$$1, changes) {
const { componentModel } = elem$$1;
const arg = changes || {};
componentModel.queued = false;
componentModel.rendering = true;
if (changes) {
runHook(elem$$1, 'didChange', arg);
}
// TODO prepare data for hooks in `mountComponent`?
runHook(elem$$1, 'willUpdate', arg);
runHook(elem$$1, 'willRender', arg);
safeCall(componentModel.update, elem$$1, getScope(elem$$1));
componentModel.rendering = false;
componentModel.finalizing = true;
runHook(elem$$1, 'didRender', arg);
runHook(elem$$1, 'didUpdate', arg);
componentModel.finalizing = false;
}
/**
* Removes attached events from given map
* @param {Component} component
* @param {AttachedStaticEvents} eventMap
*/
function detachStaticEvents(component, eventMap) {
const { listeners, handler } = eventMap;
for (let p in listeners) {
component.removeEventListener(p, handler);
}
}
/**
* @param {string} ch
* @returns {string}
*/
function kebabCase(ch) {
return '-' + ch.toLowerCase();
}
/**
* @param {Component} component
* @param {Object} prevProps
* @param {Object} nextProps
* @returns {Changes}
*/
function setPropsInternal(component, prevProps, nextProps) {
/** @type {Changes} */
const changes = {};
let hasChanges = false;
const { props } = component;
const { defaultProps } = component.componentModel;
for (const p in nextProps) {
const prev = prevProps[p];
let current = nextProps[p];
if (current == null) {
current = defaultProps[p];
}
if (p === 'class' && current != null) {
current = normalizeClassName(current);
}
if (current !== prev) {
hasChanges = true;
props[p] = prevProps[p] = current;
changes[p] = { current, prev };
if (!/^partial:/.test(p)) {
representAttributeValue(component, p.replace(/[A-Z]/g, kebabCase), current);
}
}
nextProps[p] = null;
}
return hasChanges ? changes : null;
}
/**
* Check if `next` contains value that differs from one in `prev`
* @param {Object} prev
* @param {Object} next
* @returns {boolean}
*/
function hasChanges(prev, next) {
for (const p in next) {
if (next[p] !== prev[p]) {
return true;
}
}
}
/**
* Prepares internal data for given component
* @param {Component} component
* @param {ComponentDefinition} definition
*/
function prepare(component, definition) {
const props = obj();
const state = obj();
const methods = obj();
/** @type {AttachedStaticEvents} */
let events;
let extend;
reverseWalkDefinitions(definition, dfn => {
dfn.props && assign(props, dfn.props(component));
dfn.state && assign(state, dfn.state(component));
dfn.methods && assign(methods, dfn.methods);
if (dfn.extend) {
const descriptors = getObjectDescriptors(dfn.extend);
extend = extend ? assign(extend, descriptors) : descriptors;
}
if (dfn.events) {
if (!events) {
events = createEventsMap(component);
}
attachEventHandlers(component, dfn.events, events);
}
});
return { props, state, extend, methods, events };
}
/**
* @param {Component} component
* @returns {AttachedStaticEvents}
*/
function createEventsMap(component) {
/** @type {{[event: string]: ComponentEventHandler[]}} */
const listeners = obj();
/** @type {StaticEventHandler} */
const handler = function (evt) {
if (component.componentModel) {
const handlers = listeners[evt.type];
for (let i = 0; i < handlers.length; i++) {
handlers[i](component, evt, this);
}
}
};
return { handler, listeners };
}
/**
* @param {Component} component
* @param {{[name: string]: ComponentEventHandler}} events
* @param {AttachedStaticEvents} eventMap
*/
function attachEventHandlers(component, events, eventMap) {
const names = Object.keys(events);
const { listeners } = eventMap;
for (let i = 0, name; i < names.length; i++) {
name = names[i];
if (name in listeners) {
listeners[name].push(events[name]);
} else {
component.addEventListener(name, eventMap.handler);
listeners[name] = [events[name]];
}
}
}
function drainQueue() {
const pending = renderQueue;
renderQueue = null;
for (let i = 0, component; i < pending.length; i += 2) {
component = pending[i];
// It’s possible that a component can be rendered before next tick
// (for example, if parent node updated component props).
// Check if it’s still queued then render.
// Also, component can be unmounted after it’s rendering was scheduled
if (component.componentModel && component.componentModel.queued) {
renderComponent(component, pending[i + 1]);
}
}
}
/**
* Renders code, returned from `get` function, as HTML
* @param {Component} host
* @param {Injector} injector
* @param {Function} get
* @param {string} slotName
* @returns {InnerHtmlBlock}
*/
function mountInnerHTML(host, injector, get, slotName) {
/** @type {InnerHtmlBlock} */
const block = injectBlock(injector, {
$$block: true,
host,
injector,
scope: getScope(host),
dispose: null,
get,
code: null,
slotName,
start: null,
end: null
});
updateInnerHTML(block);
return block;
const block = injectBlock(injector, {
$$block: true,
host,
injector,
scope: getScope(host),
dispose: null,
get,
code: null,
slotName
});
updateInnerHTML(block);
return block;
}
/**
* Updates inner HTML of block, defined in `ctx`
* @param {InnerHtmlBlock} block
* @returns {number} Returns `1` if inner HTML was updated, `0` otherwise
* @returns Returns `1` if inner HTML was updated, `0` otherwise
*/
function updateInnerHTML(block) {
const code = block.get(block.host, block.scope);
if (code !== block.code) {
emptyBlockContent(block);
if (isDefined(block.code = code)) {
run(block, renderHTML, block);
}
block.injector.ptr = block.end;
return 1;
}
return 0;
const code = block.get(block.host, block.scope);
if (code !== block.code) {
emptyBlockContent(block);
if (isDefined(block.code = code)) {
run(block, renderHTML, block);
}
block.injector.ptr = block.end;
return 1;
}
return 0;
}
/**
* @param {InnerHtmlBlock} ctx
*/
function unmountInnerHTML(ctx) {
disposeBlock(ctx);
disposeBlock(ctx);
}
/**
* @param {Component} host
* @param {Injector} injector
* @param {InnerHtmlBlock} ctx
*/
function renderHTML(host, injector, ctx) {
const { code } = ctx;
const { cssScope: cssScope$$1 } = host.componentModel.definition;
if (code && code.nodeType) {
// Insert as DOM element
cssScope$$1 && scopeDOM(code, cssScope$$1);
insert(injector, code, ctx.slotName);
} else {
// Render as HTML
const div = document.createElement('div');
div.innerHTML = ctx.code;
cssScope$$1 && scopeDOM(div, cssScope$$1);
while (div.firstChild) {
insert(injector, div.firstChild, ctx.slotName);
}
}
const { code } = ctx;
const { cssScope } = host.componentModel.definition;
if (code && code.nodeType) {
// Insert as DOM element
cssScope && scopeDOM(code, cssScope);
insert(injector, code, ctx.slotName);
}
else {
// Render as HTML
const div = document.createElement('div');
div.innerHTML = ctx.code;
cssScope && scopeDOM(div, cssScope);
while (div.firstChild) {
insert(injector, div.firstChild, ctx.slotName);
}
}
}
/**
* Scopes CSS of all elements in given node
* @param {Element} node
* @param {string} cssScope
*/
function scopeDOM(node, cssScope$$1) {
node = /** @type {Element} */ (node.firstChild);
while (node) {
if (node.nodeType === node.ELEMENT_NODE) {
node.setAttribute(cssScope$$1, '');
scopeDOM(node, cssScope$$1);
}
node = /** @type {Element} */ (node.nextSibling);
}
function scopeDOM(node, cssScope) {
node = node.firstChild;
while (node) {
if (node.nodeType === node.ELEMENT_NODE) {
node.setAttribute(cssScope, '');
scopeDOM(node, cssScope);
}
node = node.nextSibling;
}
}

@@ -2046,174 +1508,136 @@

* Mounts given partial into injector context
* @param {Component} host
* @param {Injector} injector
* @param {PartialDefinition} partial
* @param {Object} args
* @return {PartialBlock}
*/
function mountPartial(host, injector, partial, args) {
/** @type {PartialBlock} */
const block = injectBlock(injector, {
$$block: true,
host,
injector,
scope: getScope(host),
dispose: null,
update: null,
partial: null,
start: null,
end: null
});
updatePartial(block, partial, args);
return block;
const block = injectBlock(injector, {
$$block: true,
host,
injector,
scope: getScope(host),
dispose: null,
update: void 0,
partial: null
});
updatePartial(block, partial, args);
return block;
}
/**
* Updates mounted partial
* @param {PartialBlock} ctx
* @param {PartialDefinition} partial
* @param {Object} args
* @returns {number} Returns `1` if partial was updated, `0` otherwise
* @returns Returns `1` if partial was updated, `0` otherwise
*/
function updatePartial(ctx, partial, args) {
const host = partial.host || ctx.host;
const { injector } = ctx;
const prevHost = ctx.host;
const prevScope = getScope(host);
let updated = 0;
ctx.host = host;
if (ctx.partial !== partial) {
// Unmount previously rendered partial
ctx.partial && emptyBlockContent(ctx);
// Mount new partial
const scope = ctx.scope = assign(obj(prevScope), partial.defaults, args);
setScope(host, scope);
ctx.update = partial ? run(ctx, partial.body, scope) : null;
ctx.partial = partial;
setScope(host, prevScope);
updated = 1;
} else if (ctx.update) {
// Update rendered partial
const scope = setScope(host, assign(ctx.scope, args));
if (run(ctx, ctx.update, scope)) {
updated = 1;
}
setScope(host, prevScope);
}
ctx.host = prevHost;
injector.ptr = ctx.end;
return updated;
const host = partial.host || ctx.host;
const { injector } = ctx;
const prevHost = ctx.host;
const prevScope = getScope(host);
let updated = 0;
ctx.host = host;
if (ctx.partial !== partial) {
// Unmount previously rendered partial
ctx.partial && emptyBlockContent(ctx);
// Mount new partial
const scope = ctx.scope = assign(obj(prevScope), partial.defaults, args);
setScope(host, scope);
ctx.update = partial ? run(ctx, partial.body, scope) : void 0;
ctx.partial = partial;
setScope(host, prevScope);
updated = 1;
}
else if (ctx.update) {
// Update rendered partial
const scope = setScope(host, assign(ctx.scope, args));
if (run(ctx, ctx.update, scope)) {
updated = 1;
}
setScope(host, prevScope);
}
ctx.host = prevHost;
injector.ptr = ctx.end;
return updated;
}
/**
* @param {PartialBlock} ctx
*/
function unmountPartial(ctx) {
disposeBlock(ctx);
disposeBlock(ctx);
}
const prefix = '$';
class Store {
constructor(data = {}) {
this.data = assign({}, data);
/** @type {StoreUpdateEntry[]} */
this._listeners = [];
// For unit tests
this.sync = false;
}
/**
* Returns current store data
* @returns {Object}
*/
get() {
return this.data;
}
/**
* Updates data in store
* @param {Object} data
*/
set(data) {
const updated = changed(data, this.data, prefix);
const render = this.sync ? renderComponent : scheduleRender;
if (updated) {
const next = this.data = assign(this.data, data);
// Notify listeners.
// Run in reverse order for listener safety (in case if handler decides
// to unsubscribe during notification)
for (let i = this._listeners.length - 1, item; i >= 0; i--) {
item = this._listeners[i];
if (!item.keys || !item.keys.length || hasChange(item.keys, updated)) {
if ('component' in item) {
render(item.component, updated);
} else if ('handler' in item) {
item.handler(next, updated);
}
}
}
}
}
/**
* Subscribes to changes in given store
* @param {StoreUpdateHandler} handler Function to invoke when store changes
* @param {string[]} keys Run handler only if given top-level keys are changed
* @returns {Object} Object that should be used to unsubscribe from updates
*/
subscribe(handler, keys) {
/** @type {StoreUpdateEntry} */
const obj$$1 = {
handler,
keys: scopeKeys(keys, prefix)
};
this._listeners.push(obj$$1);
return obj$$1;
}
/**
* Unsubscribes from further updates
* @param {Object} obj
*/
unsubscribe(obj$$1) {
const ix = this._listeners.indexOf(obj$$1);
if (ix !== -1) {
this._listeners.splice(ix, 1);
}
}
/**
* Watches for updates of given `keys` in store and runs `component` render on change
* @param {Component} component
* @param {string[]} keys
*/
watch(component, keys) {
this._listeners.push({
component,
keys: scopeKeys(keys, prefix)
});
}
/**
* Stops watching for store updates for given component
* @param {Component} component
*/
unwatch(component) {
for (let i = 0; i < this._listeners.length; i++) {
if (this._listeners[i].component === component) {
this._listeners.splice(i, 1);
return;
}
}
}
constructor(data) {
this.sync = false;
this.listeners = [];
this.data = assign({}, data || {});
}
/**
* Returns current store data
*/
get() {
return this.data;
}
/**
* Updates data in store
*/
set(data) {
const updated = changed(data, this.data, prefix);
const render = this.sync ? renderComponent : scheduleRender;
if (updated) {
const next = this.data = assign(this.data, data);
// Notify listeners.
// Run in reverse order for listener safety (in case if handler decides
// to unsubscribe during notification)
for (let i = this.listeners.length - 1, item; i >= 0; i--) {
item = this.listeners[i];
if (!item.keys || !item.keys.length || hasChange(item.keys, updated)) {
if ('component' in item) {
render(item.component, updated);
}
else if ('handler' in item) {
item.handler(next, updated);
}
}
}
}
}
/**
* Subscribes to changes in given store
* @param handler Function to invoke when store changes
* @param keys Run handler only if given top-level keys are changed
* @returns Object that should be used to unsubscribe from updates
*/
subscribe(handler, keys) {
const obj = {
handler,
keys: scopeKeys(keys, prefix)
};
this.listeners.push(obj);
return obj;
}
/**
* Unsubscribes from further updates
*/
unsubscribe(obj) {
const ix = this.listeners.indexOf(obj);
if (ix !== -1) {
this.listeners.splice(ix, 1);
}
}
/**
* Watches for updates of given `keys` in store and runs `component` render on change
*/
watch(component, keys) {
this.listeners.push({
component,
keys: scopeKeys(keys, prefix)
});
}
/**
* Stops watching for store updates for given component
* @param {Component} component
*/
unwatch(component) {
for (let i = 0; i < this.listeners.length; i++) {
if (this.listeners[i].component === component) {
this.listeners.splice(i, 1);
return;
}
}
}
}
/**

@@ -2226,19 +1650,14 @@ * Check if any of `keys` was changed in `next` object since `prev` state

function hasChange(keys, updated) {
for (let i = 0; i < keys.length; i++) {
if (keys[i] in updated) {
return true;
}
}
return false;
for (let i = 0; i < keys.length; i++) {
if (keys[i] in updated) {
return true;
}
}
return false;
}
/**
* Adds given prefix to keys
* @param {string[]} keys
* @param {string} prefix
* @returns {string[]}
*/
function scopeKeys(keys, prefix) {
return keys && prefix ? keys.map(key => prefix + key) : keys;
function scopeKeys(keys, pfx) {
return keys && pfx ? keys.map(key => pfx + key) : keys;
}

@@ -2248,78 +1667,57 @@

* Animates element appearance
* @param {HTMLElement | Component} elem
* @param {string} animation
* @param {string} [cssScope]
*/
function animateIn(elem$$1, animation, cssScope$$1) {
if (animation = createAnimation(animation, cssScope$$1)) {
elem$$1.style.animation = animation;
}
function animateIn(elem, animation, cssScope) {
if (animation = createAnimation(animation, cssScope)) {
elem.style.animation = animation;
}
}
/**
* Animates element disappearance
* @param {HTMLElement | Component} elem
* @param {string} animation
* @param {Object} [scope]
* @param {Function} [callback]
* @param {string} [cssScope]
*/
function animateOut(elem$$1, animation, scope, callback, cssScope$$1) {
if (typeof scope === 'string') {
cssScope$$1 = scope;
scope = callback = null;
}
if (animation = createAnimation(animation, cssScope$$1)) {
// Create a copy of scope and pass it to callback function.
// It’s required for proper clean-up in case if the same element
// (with the same scope references) will be created during animation
if (scope) {
scope = assign(obj(), scope);
}
/** @param {AnimationEvent} evt */
const handler = evt => {
if (evt.target === elem$$1) {
elem$$1[animatingKey] = false;
elem$$1.removeEventListener('animationend', handler);
elem$$1.removeEventListener('animationcancel', handler);
dispose(elem$$1, () => callback && callback(scope));
}
};
elem$$1[animatingKey] = true;
elem$$1.addEventListener('animationend', handler);
elem$$1.addEventListener('animationcancel', handler);
elem$$1.style.animation = animation;
} else {
dispose(elem$$1, callback);
}
function animateOut(elem, animation, scope, callback, cssScope) {
if (typeof scope === 'string') {
cssScope = scope;
scope = callback = undefined;
}
if (animation = createAnimation(animation, cssScope)) {
// Create a copy of scope and pass it to callback function.
// It’s required for proper clean-up in case if the same element
// (with the same scope references) will be created during animation
if (scope) {
scope = assign(obj(), scope);
}
/** @param {AnimationEvent} evt */
const handler = (evt) => {
if (evt.target === elem) {
elem[animatingKey] = false;
elem.removeEventListener('animationend', handler);
elem.removeEventListener('animationcancel', handler);
dispose(elem, () => callback && callback(scope));
}
};
elem[animatingKey] = true;
elem.addEventListener('animationend', handler);
elem.addEventListener('animationcancel', handler);
elem.style.animation = animation;
}
else {
dispose(elem, () => callback && callback(scope));
}
}
/**
* Creates animation CSS value with scoped animation name
* @param {string} animation
* @param {string} [cssScope]
* @returns {string}
*/
function createAnimation(animation, cssScope$$1) {
if (animation == null) {
return '';
}
const parts = String(animation).split(' ');
let name = parts[0].trim();
const globalPrefix = 'global:';
if (name.indexOf(globalPrefix) === 0) {
// Do not scope animation name, use globally defined animation name
parts[0] = name.slice(globalPrefix.length);
} else if (cssScope$$1) {
parts[0] = concat(name, cssScope$$1);
}
return parts.join(' ').trim();
function createAnimation(animation, cssScope) {
if (animation == null) {
return '';
}
const parts = String(animation).split(' ');
const name = parts[0].trim();
const globalPrefix = 'global:';
if (name.indexOf(globalPrefix) === 0) {
// Do not scope animation name, use globally defined animation name
parts[0] = name.slice(globalPrefix.length);
}
else if (cssScope) {
parts[0] = concat(name, cssScope);
}
return parts.join(' ').trim();
}
/**

@@ -2331,22 +1729,29 @@ * Concatenates two strings with optional separator

function concat(name, suffix) {
const sep = suffix[0] === '_' || suffix[0] === '-' ? '' : '-';
return name + sep + suffix;
const sep = suffix[0] === '_' || suffix[0] === '-' ? '' : '-';
return name + sep + suffix;
}
function dispose(elem, callback) {
if ('componentModel' in elem) {
unmountComponent(elem);
}
if (callback) {
callback();
}
domRemove(elem);
}
/**
* @param {HTMLElement | Component} elem
* @param {Function} [callback]
* Creates Endorphin component and mounts it into given `options.target` container
*/
function dispose(elem$$1, callback) {
if (/** @type {Component} */ (elem$$1).componentModel) {
unmountComponent(/** @type {Component} */(elem$$1));
}
if (callback) {
callback();
}
domRemove(elem$$1);
function endorphin(name, definition, options = {}) {
const component = createComponent(name, definition, options.target);
if (options.store) {
component.store = options.store;
}
if (options.target && !options.detached) {
options.target.appendChild(component);
}
mountComponent(component, options.props);
return component;
}
/**

@@ -2359,50 +1764,67 @@ * Safe property getter

function get(ctx) {
const hasMap = typeof Map !== 'undefined';
for (let i = 1, il = arguments.length, arg; ctx != null && i < il; i++) {
arg = arguments[i];
if (hasMap && ctx instanceof Map) {
ctx = ctx.get(arg);
} else {
ctx = ctx[arg];
}
}
return ctx;
const hasMap = typeof Map !== 'undefined';
for (let i = 1, il = arguments.length, arg; ctx != null && i < il; i++) {
arg = arguments[i];
if (hasMap && ctx instanceof Map) {
ctx = ctx.get(arg);
}
else {
ctx = ctx[arg];
}
}
return ctx;
}
/**
* Invokes `methodName` of `ctx` object with given args
*/
function call(ctx, methodName, args) {
const method = ctx != null && ctx[methodName];
if (typeof method === 'function') {
return args ? method.apply(ctx, args) : method.call(ctx);
}
}
/**
* Filter items from given collection that matches `fn` criteria and returns
* matched items
* @param {Component} host
* @param {Iterable} collection
* @param {Function} fn
* @returns {Array}
*/
function filter(host, collection, fn) {
const result = [];
if (collection && collection.forEach) {
collection.forEach((value, key) => {
if (fn(host, value, key)) {
result.push(value);
}
});
}
return result;
const result = [];
if (collection && collection.forEach) {
collection.forEach((value, key) => {
if (fn(host, value, key)) {
result.push(value);
}
});
}
return result;
}
/**
* Invokes `methodName` of `ctx` object with given args
* @param {Object} ctx
* @param {string} methodName
* @param {Array} [args]
* Finds first item in given `collection` that matches truth test of `fn`
*/
function call(ctx, methodName, args) {
const method = ctx != null && ctx[methodName];
if (typeof method === 'function') {
return args ? method.apply(ctx, args) : method.call(ctx);
}
function find(host, collection, fn) {
if (Array.isArray(collection)) {
// Fast path: find item in array
for (let i = 0, item; i < collection.length; i++) {
item = collection[i];
if (fn(host, item, i)) {
return item;
}
}
}
else if (collection && collection.forEach) {
// Iterate over collection
let found = false;
let result = null;
collection.forEach((value, key) => {
if (!found && fn(host, value, key)) {
found = true;
result = value;
}
});
return result;
}
}
export { get, filter, call, addDisposeCallback, assign, mountBlock, updateBlock, unmountBlock, mountIterator, updateIterator, unmountIterator, prepareScope, mountKeyIterator, updateKeyIterator, unmountKeyIterator, createInjector, insert, injectBlock, run, emptyBlockContent, move, disposeBlock, enterScope, exitScope, createScope, setScope, getScope, getProp, getState, getVar, setVar, setAttribute, setAttributeNS, updateAttribute, updateProps, addClass, finalizeAttributes, normalizeClassName, addEvent, addStaticEvent, finalizeEvents, getEventHandler, mountSlot, unmountSlot, updateSlots, markSlotUpdate, setRef, setStaticRef, finalizeRefs, createComponent, mountComponent, updateComponent, unmountComponent, subscribeStore, scheduleRender, renderComponent, mountInnerHTML, updateInnerHTML, unmountInnerHTML, elem, elemNS, elemWithText, elemNSWithText, text, updateText, domInsert, domRemove, mountPartial, updatePartial, unmountPartial, Store, animateIn, animateOut };
export default endorphin;
export { Store, addClass, addDisposeCallback, addEvent, addStaticEvent, animateIn, animateOut, assign, call, createComponent, createInjector, createScope, disposeBlock, domInsert, domRemove, elem, elemNS, elemNSWithText, elemWithText, emptyBlockContent, enterScope, exitScope, filter, finalizeAttributes, finalizeEvents, finalizeRefs, find, get, getProp, getScope, getState, getVar, injectBlock, insert, markSlotUpdate, mountBlock, mountComponent, mountInnerHTML, mountIterator, mountKeyIterator, mountPartial, mountSlot, move, normalizeClassName, prepareScope, removeStaticEvent, renderComponent, run, scheduleRender, setAttribute, setAttributeNS, setRef, setScope, setStaticRef, setVar, subscribeStore, text, unmountBlock, unmountComponent, unmountInnerHTML, unmountIterator, unmountKeyIterator, unmountPartial, unmountSlot, updateAttribute, updateBlock, updateComponent, updateInnerHTML, updateIterator, updateKeyIterator, updatePartial, updateProps, updateSlots, updateText };
//# sourceMappingURL=runtime.es.js.map
{
"name": "@endorphinjs/template-runtime",
"version": "0.1.27",
"version": "0.3.0",
"description": "EndorphinJS template runtime, embedded with template bundles",
"main": "./dist/runtime.cjs.js",
"module": "./dist/runtime.es.js",
"types": "./types.d.ts",
"types": "./dist/runtime.d.ts",
"scripts": {
"build": "rollup -c",
"watch": "rollup -wc",
"test": "mocha",
"prepare": "npm test && npm run build"
"lint": "tslint ./src/**/*.ts",
"build": "rollup -c && npm run types",
"types": "tsc -p ./tsconfig.declaration.json",
"clean": "rm -rf ./dist",
"prepare": "npm run lint && npm test && npm run clean && npm run build"
},

@@ -23,6 +25,11 @@ "keywords": [

"devDependencies": {
"@endorphinjs/template-compiler": "^0.1.10",
"mocha": "^5.2.0",
"reify": "^0.18.1",
"rollup": "^1.1.2"
"@endorphinjs/template-compiler": "^0.3.0",
"@types/mocha": "^5.2.6",
"@types/node": "^11.13.10",
"mocha": "^6.1.4",
"rollup": "^1.11.3",
"rollup-plugin-typescript": "^1.0.1",
"ts-node": "^8.1.0",
"tslint": "^5.16.0",
"typescript": "^3.4.5"
},

@@ -41,3 +48,7 @@ "directories": {

},
"homepage": "https://github.com/endorphinjs/template-runtime#readme"
"homepage": "https://github.com/endorphinjs/template-runtime#readme",
"mocha": {
"require": "./test/register",
"spec": "./test/*.ts"
}
}

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc