770 lines
35 KiB
JavaScript
770 lines
35 KiB
JavaScript
(function (root, factory) {
|
|
if (typeof define === 'function' && define.amd) {
|
|
// AMD. Register as an anonymous module.
|
|
define(['exports'], factory);
|
|
} else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
|
|
// CommonJS
|
|
factory(exports);
|
|
} else {
|
|
// Browser globals
|
|
factory(root.maquette = {});
|
|
}
|
|
}(this, function (exports) {
|
|
'use strict';
|
|
;
|
|
;
|
|
;
|
|
;
|
|
var NAMESPACE_W3 = 'http://www.w3.org/';
|
|
var NAMESPACE_SVG = NAMESPACE_W3 + '2000/svg';
|
|
var NAMESPACE_XLINK = NAMESPACE_W3 + '1999/xlink';
|
|
// Utilities
|
|
var emptyArray = [];
|
|
var extend = function (base, overrides) {
|
|
var result = {};
|
|
Object.keys(base).forEach(function (key) {
|
|
result[key] = base[key];
|
|
});
|
|
if (overrides) {
|
|
Object.keys(overrides).forEach(function (key) {
|
|
result[key] = overrides[key];
|
|
});
|
|
}
|
|
return result;
|
|
};
|
|
// Hyperscript helper functions
|
|
var same = function (vnode1, vnode2) {
|
|
if (vnode1.vnodeSelector !== vnode2.vnodeSelector) {
|
|
return false;
|
|
}
|
|
if (vnode1.properties && vnode2.properties) {
|
|
if (vnode1.properties.key !== vnode2.properties.key) {
|
|
return false;
|
|
}
|
|
return vnode1.properties.bind === vnode2.properties.bind;
|
|
}
|
|
return !vnode1.properties && !vnode2.properties;
|
|
};
|
|
var toTextVNode = function (data) {
|
|
return {
|
|
vnodeSelector: '',
|
|
properties: undefined,
|
|
children: undefined,
|
|
text: data.toString(),
|
|
domNode: null
|
|
};
|
|
};
|
|
var appendChildren = function (parentSelector, insertions, main) {
|
|
for (var i = 0; i < insertions.length; i++) {
|
|
var item = insertions[i];
|
|
if (Array.isArray(item)) {
|
|
appendChildren(parentSelector, item, main);
|
|
} else {
|
|
if (item !== null && item !== undefined) {
|
|
if (!item.hasOwnProperty('vnodeSelector')) {
|
|
item = toTextVNode(item);
|
|
}
|
|
main.push(item);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
// Render helper functions
|
|
var missingTransition = function () {
|
|
throw new Error('Provide a transitions object to the projectionOptions to do animations');
|
|
};
|
|
var DEFAULT_PROJECTION_OPTIONS = {
|
|
namespace: undefined,
|
|
eventHandlerInterceptor: undefined,
|
|
styleApplyer: function (domNode, styleName, value) {
|
|
// Provides a hook to add vendor prefixes for browsers that still need it.
|
|
domNode.style[styleName] = value;
|
|
},
|
|
transitions: {
|
|
enter: missingTransition,
|
|
exit: missingTransition
|
|
}
|
|
};
|
|
var applyDefaultProjectionOptions = function (projectorOptions) {
|
|
return extend(DEFAULT_PROJECTION_OPTIONS, projectorOptions);
|
|
};
|
|
var checkStyleValue = function (styleValue) {
|
|
if (typeof styleValue !== 'string') {
|
|
throw new Error('Style values must be strings');
|
|
}
|
|
};
|
|
var setProperties = function (domNode, properties, projectionOptions) {
|
|
if (!properties) {
|
|
return;
|
|
}
|
|
var eventHandlerInterceptor = projectionOptions.eventHandlerInterceptor;
|
|
var propNames = Object.keys(properties);
|
|
var propCount = propNames.length;
|
|
for (var i = 0; i < propCount; i++) {
|
|
var propName = propNames[i];
|
|
/* tslint:disable:no-var-keyword: edge case */
|
|
var propValue = properties[propName];
|
|
/* tslint:enable:no-var-keyword */
|
|
if (propName === 'className') {
|
|
throw new Error('Property "className" is not supported, use "class".');
|
|
} else if (propName === 'class') {
|
|
if (domNode.className) {
|
|
// May happen if classes is specified before class
|
|
domNode.className += ' ' + propValue;
|
|
} else {
|
|
domNode.className = propValue;
|
|
}
|
|
} else if (propName === 'classes') {
|
|
// object with string keys and boolean values
|
|
var classNames = Object.keys(propValue);
|
|
var classNameCount = classNames.length;
|
|
for (var j = 0; j < classNameCount; j++) {
|
|
var className = classNames[j];
|
|
if (propValue[className]) {
|
|
domNode.classList.add(className);
|
|
}
|
|
}
|
|
} else if (propName === 'styles') {
|
|
// object with string keys and string (!) values
|
|
var styleNames = Object.keys(propValue);
|
|
var styleCount = styleNames.length;
|
|
for (var j = 0; j < styleCount; j++) {
|
|
var styleName = styleNames[j];
|
|
var styleValue = propValue[styleName];
|
|
if (styleValue) {
|
|
checkStyleValue(styleValue);
|
|
projectionOptions.styleApplyer(domNode, styleName, styleValue);
|
|
}
|
|
}
|
|
} else if (propName === 'key') {
|
|
continue;
|
|
} else if (propValue === null || propValue === undefined) {
|
|
continue;
|
|
} else {
|
|
var type = typeof propValue;
|
|
if (type === 'function') {
|
|
if (propName.lastIndexOf('on', 0) === 0) {
|
|
if (eventHandlerInterceptor) {
|
|
propValue = eventHandlerInterceptor(propName, propValue, domNode, properties); // intercept eventhandlers
|
|
}
|
|
if (propName === 'oninput') {
|
|
(function () {
|
|
// record the evt.target.value, because IE and Edge sometimes do a requestAnimationFrame between changing value and running oninput
|
|
var oldPropValue = propValue;
|
|
propValue = function (evt) {
|
|
evt.target['oninput-value'] = evt.target.value;
|
|
// may be HTMLTextAreaElement as well
|
|
oldPropValue.apply(this, [evt]);
|
|
};
|
|
}());
|
|
}
|
|
domNode[propName] = propValue;
|
|
}
|
|
} else if (type === 'string' && propName !== 'value' && propName !== 'innerHTML') {
|
|
if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') {
|
|
domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue);
|
|
} else {
|
|
domNode.setAttribute(propName, propValue);
|
|
}
|
|
} else {
|
|
domNode[propName] = propValue;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
var updateProperties = function (domNode, previousProperties, properties, projectionOptions) {
|
|
if (!properties) {
|
|
return;
|
|
}
|
|
var propertiesUpdated = false;
|
|
var propNames = Object.keys(properties);
|
|
var propCount = propNames.length;
|
|
for (var i = 0; i < propCount; i++) {
|
|
var propName = propNames[i];
|
|
// assuming that properties will be nullified instead of missing is by design
|
|
var propValue = properties[propName];
|
|
var previousValue = previousProperties[propName];
|
|
if (propName === 'class') {
|
|
if (previousValue !== propValue) {
|
|
throw new Error('"class" property may not be updated. Use the "classes" property for conditional css classes.');
|
|
}
|
|
} else if (propName === 'classes') {
|
|
var classList = domNode.classList;
|
|
var classNames = Object.keys(propValue);
|
|
var classNameCount = classNames.length;
|
|
for (var j = 0; j < classNameCount; j++) {
|
|
var className = classNames[j];
|
|
var on = !!propValue[className];
|
|
var previousOn = !!previousValue[className];
|
|
if (on === previousOn) {
|
|
continue;
|
|
}
|
|
propertiesUpdated = true;
|
|
if (on) {
|
|
classList.add(className);
|
|
} else {
|
|
classList.remove(className);
|
|
}
|
|
}
|
|
} else if (propName === 'styles') {
|
|
var styleNames = Object.keys(propValue);
|
|
var styleCount = styleNames.length;
|
|
for (var j = 0; j < styleCount; j++) {
|
|
var styleName = styleNames[j];
|
|
var newStyleValue = propValue[styleName];
|
|
var oldStyleValue = previousValue[styleName];
|
|
if (newStyleValue === oldStyleValue) {
|
|
continue;
|
|
}
|
|
propertiesUpdated = true;
|
|
if (newStyleValue) {
|
|
checkStyleValue(newStyleValue);
|
|
projectionOptions.styleApplyer(domNode, styleName, newStyleValue);
|
|
} else {
|
|
projectionOptions.styleApplyer(domNode, styleName, '');
|
|
}
|
|
}
|
|
} else {
|
|
if (!propValue && typeof previousValue === 'string') {
|
|
propValue = '';
|
|
}
|
|
if (propName === 'value') {
|
|
if (domNode[propName] !== propValue && domNode['oninput-value'] !== propValue) {
|
|
domNode[propName] = propValue;
|
|
// Reset the value, even if the virtual DOM did not change
|
|
domNode['oninput-value'] = undefined;
|
|
}
|
|
// else do not update the domNode, otherwise the cursor position would be changed
|
|
if (propValue !== previousValue) {
|
|
propertiesUpdated = true;
|
|
}
|
|
} else if (propValue !== previousValue) {
|
|
var type = typeof propValue;
|
|
if (type === 'function') {
|
|
throw new Error('Functions may not be updated on subsequent renders (property: ' + propName + '). Hint: declare event handler functions outside the render() function.');
|
|
}
|
|
if (type === 'string' && propName !== 'innerHTML') {
|
|
if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') {
|
|
domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue);
|
|
} else {
|
|
domNode.setAttribute(propName, propValue);
|
|
}
|
|
} else {
|
|
if (domNode[propName] !== propValue) {
|
|
domNode[propName] = propValue;
|
|
}
|
|
}
|
|
propertiesUpdated = true;
|
|
}
|
|
}
|
|
}
|
|
return propertiesUpdated;
|
|
};
|
|
var findIndexOfChild = function (children, sameAs, start) {
|
|
if (sameAs.vnodeSelector !== '') {
|
|
// Never scan for text-nodes
|
|
for (var i = start; i < children.length; i++) {
|
|
if (same(children[i], sameAs)) {
|
|
return i;
|
|
}
|
|
}
|
|
}
|
|
return -1;
|
|
};
|
|
var nodeAdded = function (vNode, transitions) {
|
|
if (vNode.properties) {
|
|
var enterAnimation = vNode.properties.enterAnimation;
|
|
if (enterAnimation) {
|
|
if (typeof enterAnimation === 'function') {
|
|
enterAnimation(vNode.domNode, vNode.properties);
|
|
} else {
|
|
transitions.enter(vNode.domNode, vNode.properties, enterAnimation);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
var nodeToRemove = function (vNode, transitions) {
|
|
var domNode = vNode.domNode;
|
|
if (vNode.properties) {
|
|
var exitAnimation = vNode.properties.exitAnimation;
|
|
if (exitAnimation) {
|
|
domNode.style.pointerEvents = 'none';
|
|
var removeDomNode = function () {
|
|
if (domNode.parentNode) {
|
|
domNode.parentNode.removeChild(domNode);
|
|
}
|
|
};
|
|
if (typeof exitAnimation === 'function') {
|
|
exitAnimation(domNode, removeDomNode, vNode.properties);
|
|
return;
|
|
} else {
|
|
transitions.exit(vNode.domNode, vNode.properties, exitAnimation, removeDomNode);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
if (domNode.parentNode) {
|
|
domNode.parentNode.removeChild(domNode);
|
|
}
|
|
};
|
|
var checkDistinguishable = function (childNodes, indexToCheck, parentVNode, operation) {
|
|
var childNode = childNodes[indexToCheck];
|
|
if (childNode.vnodeSelector === '') {
|
|
return; // Text nodes need not be distinguishable
|
|
}
|
|
var properties = childNode.properties;
|
|
var key = properties ? properties.key === undefined ? properties.bind : properties.key : undefined;
|
|
if (!key) {
|
|
for (var i = 0; i < childNodes.length; i++) {
|
|
if (i !== indexToCheck) {
|
|
var node = childNodes[i];
|
|
if (same(node, childNode)) {
|
|
if (operation === 'added') {
|
|
throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'added, but there is now more than one. You must add unique key properties to make them distinguishable.');
|
|
} else {
|
|
throw new Error(parentVNode.vnodeSelector + ' had a ' + childNode.vnodeSelector + ' child ' + 'removed, but there were more than one. You must add unique key properties to make them distinguishable.');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
var createDom;
|
|
var updateDom;
|
|
var updateChildren = function (vnode, domNode, oldChildren, newChildren, projectionOptions) {
|
|
if (oldChildren === newChildren) {
|
|
return false;
|
|
}
|
|
oldChildren = oldChildren || emptyArray;
|
|
newChildren = newChildren || emptyArray;
|
|
var oldChildrenLength = oldChildren.length;
|
|
var newChildrenLength = newChildren.length;
|
|
var transitions = projectionOptions.transitions;
|
|
var oldIndex = 0;
|
|
var newIndex = 0;
|
|
var i;
|
|
var textUpdated = false;
|
|
while (newIndex < newChildrenLength) {
|
|
var oldChild = oldIndex < oldChildrenLength ? oldChildren[oldIndex] : undefined;
|
|
var newChild = newChildren[newIndex];
|
|
if (oldChild !== undefined && same(oldChild, newChild)) {
|
|
textUpdated = updateDom(oldChild, newChild, projectionOptions) || textUpdated;
|
|
oldIndex++;
|
|
} else {
|
|
var findOldIndex = findIndexOfChild(oldChildren, newChild, oldIndex + 1);
|
|
if (findOldIndex >= 0) {
|
|
// Remove preceding missing children
|
|
for (i = oldIndex; i < findOldIndex; i++) {
|
|
nodeToRemove(oldChildren[i], transitions);
|
|
checkDistinguishable(oldChildren, i, vnode, 'removed');
|
|
}
|
|
textUpdated = updateDom(oldChildren[findOldIndex], newChild, projectionOptions) || textUpdated;
|
|
oldIndex = findOldIndex + 1;
|
|
} else {
|
|
// New child
|
|
createDom(newChild, domNode, oldIndex < oldChildrenLength ? oldChildren[oldIndex].domNode : undefined, projectionOptions);
|
|
nodeAdded(newChild, transitions);
|
|
checkDistinguishable(newChildren, newIndex, vnode, 'added');
|
|
}
|
|
}
|
|
newIndex++;
|
|
}
|
|
if (oldChildrenLength > oldIndex) {
|
|
// Remove child fragments
|
|
for (i = oldIndex; i < oldChildrenLength; i++) {
|
|
nodeToRemove(oldChildren[i], transitions);
|
|
checkDistinguishable(oldChildren, i, vnode, 'removed');
|
|
}
|
|
}
|
|
return textUpdated;
|
|
};
|
|
var addChildren = function (domNode, children, projectionOptions) {
|
|
if (!children) {
|
|
return;
|
|
}
|
|
for (var i = 0; i < children.length; i++) {
|
|
createDom(children[i], domNode, undefined, projectionOptions);
|
|
}
|
|
};
|
|
var initPropertiesAndChildren = function (domNode, vnode, projectionOptions) {
|
|
addChildren(domNode, vnode.children, projectionOptions);
|
|
// children before properties, needed for value property of <select>.
|
|
if (vnode.text) {
|
|
domNode.textContent = vnode.text;
|
|
}
|
|
setProperties(domNode, vnode.properties, projectionOptions);
|
|
if (vnode.properties && vnode.properties.afterCreate) {
|
|
vnode.properties.afterCreate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children);
|
|
}
|
|
};
|
|
createDom = function (vnode, parentNode, insertBefore, projectionOptions) {
|
|
var domNode, i, c, start = 0, type, found;
|
|
var vnodeSelector = vnode.vnodeSelector;
|
|
if (vnodeSelector === '') {
|
|
domNode = vnode.domNode = document.createTextNode(vnode.text);
|
|
if (insertBefore !== undefined) {
|
|
parentNode.insertBefore(domNode, insertBefore);
|
|
} else {
|
|
parentNode.appendChild(domNode);
|
|
}
|
|
} else {
|
|
for (i = 0; i <= vnodeSelector.length; ++i) {
|
|
c = vnodeSelector.charAt(i);
|
|
if (i === vnodeSelector.length || c === '.' || c === '#') {
|
|
type = vnodeSelector.charAt(start - 1);
|
|
found = vnodeSelector.slice(start, i);
|
|
if (type === '.') {
|
|
domNode.classList.add(found);
|
|
} else if (type === '#') {
|
|
domNode.id = found;
|
|
} else {
|
|
if (found === 'svg') {
|
|
projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG });
|
|
}
|
|
if (projectionOptions.namespace !== undefined) {
|
|
domNode = vnode.domNode = document.createElementNS(projectionOptions.namespace, found);
|
|
} else {
|
|
domNode = vnode.domNode = document.createElement(found);
|
|
}
|
|
if (insertBefore !== undefined) {
|
|
parentNode.insertBefore(domNode, insertBefore);
|
|
} else {
|
|
parentNode.appendChild(domNode);
|
|
}
|
|
}
|
|
start = i + 1;
|
|
}
|
|
}
|
|
initPropertiesAndChildren(domNode, vnode, projectionOptions);
|
|
}
|
|
};
|
|
updateDom = function (previous, vnode, projectionOptions) {
|
|
var domNode = previous.domNode;
|
|
var textUpdated = false;
|
|
if (previous === vnode) {
|
|
return false; // By contract, VNode objects may not be modified anymore after passing them to maquette
|
|
}
|
|
var updated = false;
|
|
if (vnode.vnodeSelector === '') {
|
|
if (vnode.text !== previous.text) {
|
|
var newVNode = document.createTextNode(vnode.text);
|
|
domNode.parentNode.replaceChild(newVNode, domNode);
|
|
vnode.domNode = newVNode;
|
|
textUpdated = true;
|
|
return textUpdated;
|
|
}
|
|
} else {
|
|
if (vnode.vnodeSelector.lastIndexOf('svg', 0) === 0) {
|
|
projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG });
|
|
}
|
|
if (previous.text !== vnode.text) {
|
|
updated = true;
|
|
if (vnode.text === undefined) {
|
|
domNode.removeChild(domNode.firstChild); // the only textnode presumably
|
|
} else {
|
|
domNode.textContent = vnode.text;
|
|
}
|
|
}
|
|
updated = updateChildren(vnode, domNode, previous.children, vnode.children, projectionOptions) || updated;
|
|
updated = updateProperties(domNode, previous.properties, vnode.properties, projectionOptions) || updated;
|
|
if (vnode.properties && vnode.properties.afterUpdate) {
|
|
vnode.properties.afterUpdate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children);
|
|
}
|
|
}
|
|
if (updated && vnode.properties && vnode.properties.updateAnimation) {
|
|
vnode.properties.updateAnimation(domNode, vnode.properties, previous.properties);
|
|
}
|
|
vnode.domNode = previous.domNode;
|
|
return textUpdated;
|
|
};
|
|
var createProjection = function (vnode, projectionOptions) {
|
|
return {
|
|
update: function (updatedVnode) {
|
|
if (vnode.vnodeSelector !== updatedVnode.vnodeSelector) {
|
|
throw new Error('The selector for the root VNode may not be changed. (consider using dom.merge and add one extra level to the virtual DOM)');
|
|
}
|
|
updateDom(vnode, updatedVnode, projectionOptions);
|
|
vnode = updatedVnode;
|
|
},
|
|
domNode: vnode.domNode
|
|
};
|
|
};
|
|
;
|
|
// The other two parameters are not added here, because the Typescript compiler creates surrogate code for desctructuring 'children'.
|
|
exports.h = function (selector) {
|
|
var properties = arguments[1];
|
|
if (typeof selector !== 'string') {
|
|
throw new Error();
|
|
}
|
|
var childIndex = 1;
|
|
if (properties && !properties.hasOwnProperty('vnodeSelector') && !Array.isArray(properties) && typeof properties === 'object') {
|
|
childIndex = 2;
|
|
} else {
|
|
// Optional properties argument was omitted
|
|
properties = undefined;
|
|
}
|
|
var text = undefined;
|
|
var children = undefined;
|
|
var argsLength = arguments.length;
|
|
// Recognize a common special case where there is only a single text node
|
|
if (argsLength === childIndex + 1) {
|
|
var onlyChild = arguments[childIndex];
|
|
if (typeof onlyChild === 'string') {
|
|
text = onlyChild;
|
|
} else if (onlyChild !== undefined && onlyChild.length === 1 && typeof onlyChild[0] === 'string') {
|
|
text = onlyChild[0];
|
|
}
|
|
}
|
|
if (text === undefined) {
|
|
children = [];
|
|
for (; childIndex < arguments.length; childIndex++) {
|
|
var child = arguments[childIndex];
|
|
if (child === null || child === undefined) {
|
|
continue;
|
|
} else if (Array.isArray(child)) {
|
|
appendChildren(selector, child, children);
|
|
} else if (child.hasOwnProperty('vnodeSelector')) {
|
|
children.push(child);
|
|
} else {
|
|
children.push(toTextVNode(child));
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
vnodeSelector: selector,
|
|
properties: properties,
|
|
children: children,
|
|
text: text === '' ? undefined : text,
|
|
domNode: null
|
|
};
|
|
};
|
|
/**
|
|
* Contains simple low-level utility functions to manipulate the real DOM.
|
|
*/
|
|
exports.dom = {
|
|
/**
|
|
* Creates a real DOM tree from `vnode`. The [[Projection]] object returned will contain the resulting DOM Node in
|
|
* its [[Projection.domNode|domNode]] property.
|
|
* This is a low-level method. Users wil typically use a [[Projector]] instead.
|
|
* @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
|
|
* objects may only be rendered once.
|
|
* @param projectionOptions - Options to be used to create and update the projection.
|
|
* @returns The [[Projection]] which also contains the DOM Node that was created.
|
|
*/
|
|
create: function (vnode, projectionOptions) {
|
|
projectionOptions = applyDefaultProjectionOptions(projectionOptions);
|
|
createDom(vnode, document.createElement('div'), undefined, projectionOptions);
|
|
return createProjection(vnode, projectionOptions);
|
|
},
|
|
/**
|
|
* Appends a new childnode to the DOM which is generated from a [[VNode]].
|
|
* This is a low-level method. Users wil typically use a [[Projector]] instead.
|
|
* @param parentNode - The parent node for the new childNode.
|
|
* @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
|
|
* objects may only be rendered once.
|
|
* @param projectionOptions - Options to be used to create and update the [[Projection]].
|
|
* @returns The [[Projection]] that was created.
|
|
*/
|
|
append: function (parentNode, vnode, projectionOptions) {
|
|
projectionOptions = applyDefaultProjectionOptions(projectionOptions);
|
|
createDom(vnode, parentNode, undefined, projectionOptions);
|
|
return createProjection(vnode, projectionOptions);
|
|
},
|
|
/**
|
|
* Inserts a new DOM node which is generated from a [[VNode]].
|
|
* This is a low-level method. Users wil typically use a [[Projector]] instead.
|
|
* @param beforeNode - The node that the DOM Node is inserted before.
|
|
* @param vnode - The root of the virtual DOM tree that was created using the [[h]] function.
|
|
* NOTE: [[VNode]] objects may only be rendered once.
|
|
* @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]].
|
|
* @returns The [[Projection]] that was created.
|
|
*/
|
|
insertBefore: function (beforeNode, vnode, projectionOptions) {
|
|
projectionOptions = applyDefaultProjectionOptions(projectionOptions);
|
|
createDom(vnode, beforeNode.parentNode, beforeNode, projectionOptions);
|
|
return createProjection(vnode, projectionOptions);
|
|
},
|
|
/**
|
|
* Merges a new DOM node which is generated from a [[VNode]] with an existing DOM Node.
|
|
* This means that the virtual DOM and the real DOM will have one overlapping element.
|
|
* Therefore the selector for the root [[VNode]] will be ignored, but its properties and children will be applied to the Element provided.
|
|
* This is a low-level method. Users wil typically use a [[Projector]] instead.
|
|
* @param domNode - The existing element to adopt as the root of the new virtual DOM. Existing attributes and childnodes are preserved.
|
|
* @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]] objects
|
|
* may only be rendered once.
|
|
* @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]].
|
|
* @returns The [[Projection]] that was created.
|
|
*/
|
|
merge: function (element, vnode, projectionOptions) {
|
|
projectionOptions = applyDefaultProjectionOptions(projectionOptions);
|
|
vnode.domNode = element;
|
|
initPropertiesAndChildren(element, vnode, projectionOptions);
|
|
return createProjection(vnode, projectionOptions);
|
|
}
|
|
};
|
|
/**
|
|
* Creates a [[CalculationCache]] object, useful for caching [[VNode]] trees.
|
|
* In practice, caching of [[VNode]] trees is not needed, because achieving 60 frames per second is almost never a problem.
|
|
* For more information, see [[CalculationCache]].
|
|
*
|
|
* @param <Result> The type of the value that is cached.
|
|
*/
|
|
exports.createCache = function () {
|
|
var cachedInputs = undefined;
|
|
var cachedOutcome = undefined;
|
|
var result = {
|
|
invalidate: function () {
|
|
cachedOutcome = undefined;
|
|
cachedInputs = undefined;
|
|
},
|
|
result: function (inputs, calculation) {
|
|
if (cachedInputs) {
|
|
for (var i = 0; i < inputs.length; i++) {
|
|
if (cachedInputs[i] !== inputs[i]) {
|
|
cachedOutcome = undefined;
|
|
}
|
|
}
|
|
}
|
|
if (!cachedOutcome) {
|
|
cachedOutcome = calculation();
|
|
cachedInputs = inputs;
|
|
}
|
|
return cachedOutcome;
|
|
}
|
|
};
|
|
return result;
|
|
};
|
|
/**
|
|
* Creates a {@link Mapping} instance that keeps an array of result objects synchronized with an array of source objects.
|
|
* See {@link http://maquettejs.org/docs/arrays.html|Working with arrays}.
|
|
*
|
|
* @param <Source> The type of source items. A database-record for instance.
|
|
* @param <Target> The type of target items. A [[Component]] for instance.
|
|
* @param getSourceKey `function(source)` that must return a key to identify each source object. The result must either be a string or a number.
|
|
* @param createResult `function(source, index)` that must create a new result object from a given source. This function is identical
|
|
* to the `callback` argument in `Array.map(callback)`.
|
|
* @param updateResult `function(source, target, index)` that updates a result to an updated source.
|
|
*/
|
|
exports.createMapping = function (getSourceKey, createResult, updateResult) {
|
|
var keys = [];
|
|
var results = [];
|
|
return {
|
|
results: results,
|
|
map: function (newSources) {
|
|
var newKeys = newSources.map(getSourceKey);
|
|
var oldTargets = results.slice();
|
|
var oldIndex = 0;
|
|
for (var i = 0; i < newSources.length; i++) {
|
|
var source = newSources[i];
|
|
var sourceKey = newKeys[i];
|
|
if (sourceKey === keys[oldIndex]) {
|
|
results[i] = oldTargets[oldIndex];
|
|
updateResult(source, oldTargets[oldIndex], i);
|
|
oldIndex++;
|
|
} else {
|
|
var found = false;
|
|
for (var j = 1; j < keys.length; j++) {
|
|
var searchIndex = (oldIndex + j) % keys.length;
|
|
if (keys[searchIndex] === sourceKey) {
|
|
results[i] = oldTargets[searchIndex];
|
|
updateResult(newSources[i], oldTargets[searchIndex], i);
|
|
oldIndex = searchIndex + 1;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
results[i] = createResult(source, i);
|
|
}
|
|
}
|
|
}
|
|
results.length = newSources.length;
|
|
keys = newKeys;
|
|
}
|
|
};
|
|
};
|
|
/**
|
|
* Creates a [[Projector]] instance using the provided projectionOptions.
|
|
*
|
|
* For more information, see [[Projector]].
|
|
*
|
|
* @param projectionOptions Options that influence how the DOM is rendered and updated.
|
|
*/
|
|
exports.createProjector = function (projectorOptions) {
|
|
var projector;
|
|
var projectionOptions = applyDefaultProjectionOptions(projectorOptions);
|
|
projectionOptions.eventHandlerInterceptor = function (propertyName, eventHandler, domNode, properties) {
|
|
return function () {
|
|
// intercept function calls (event handlers) to do a render afterwards.
|
|
projector.scheduleRender();
|
|
return eventHandler.apply(properties.bind || this, arguments);
|
|
};
|
|
};
|
|
var renderCompleted = true;
|
|
var scheduled;
|
|
var stopped = false;
|
|
var projections = [];
|
|
var renderFunctions = [];
|
|
// matches the projections array
|
|
var doRender = function () {
|
|
scheduled = undefined;
|
|
if (!renderCompleted) {
|
|
return; // The last render threw an error, it should be logged in the browser console.
|
|
}
|
|
renderCompleted = false;
|
|
for (var i = 0; i < projections.length; i++) {
|
|
var updatedVnode = renderFunctions[i]();
|
|
projections[i].update(updatedVnode);
|
|
}
|
|
renderCompleted = true;
|
|
};
|
|
projector = {
|
|
scheduleRender: function () {
|
|
if (!scheduled && !stopped) {
|
|
scheduled = requestAnimationFrame(doRender);
|
|
}
|
|
},
|
|
stop: function () {
|
|
if (scheduled) {
|
|
cancelAnimationFrame(scheduled);
|
|
scheduled = undefined;
|
|
}
|
|
stopped = true;
|
|
},
|
|
resume: function () {
|
|
stopped = false;
|
|
renderCompleted = true;
|
|
projector.scheduleRender();
|
|
},
|
|
append: function (parentNode, renderMaquetteFunction) {
|
|
projections.push(exports.dom.append(parentNode, renderMaquetteFunction(), projectionOptions));
|
|
renderFunctions.push(renderMaquetteFunction);
|
|
},
|
|
insertBefore: function (beforeNode, renderMaquetteFunction) {
|
|
projections.push(exports.dom.insertBefore(beforeNode, renderMaquetteFunction(), projectionOptions));
|
|
renderFunctions.push(renderMaquetteFunction);
|
|
},
|
|
merge: function (domNode, renderMaquetteFunction) {
|
|
projections.push(exports.dom.merge(domNode, renderMaquetteFunction(), projectionOptions));
|
|
renderFunctions.push(renderMaquetteFunction);
|
|
},
|
|
replace: function (domNode, renderMaquetteFunction) {
|
|
var vnode = renderMaquetteFunction();
|
|
createDom(vnode, domNode.parentNode, domNode, projectionOptions);
|
|
domNode.parentNode.removeChild(domNode);
|
|
projections.push(createProjection(vnode, projectionOptions));
|
|
renderFunctions.push(renderMaquetteFunction);
|
|
},
|
|
detach: function (renderMaquetteFunction) {
|
|
for (var i = 0; i < renderFunctions.length; i++) {
|
|
if (renderFunctions[i] === renderMaquetteFunction) {
|
|
renderFunctions.splice(i, 1);
|
|
return projections.splice(i, 1)[0];
|
|
}
|
|
}
|
|
throw new Error('renderMaquetteFunction was not found');
|
|
}
|
|
};
|
|
return projector;
|
|
};
|
|
}));
|