@endorphinjs/template-runtime
Advanced tools
Comparing version 0.1.27 to 0.3.0
@@ -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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
384023
27
9
4499
1