Forking what is left of ZeroNet and hopefully adding an AT Proto Frontend/Proxy
1(function (root, factory) {
2 if (typeof define === 'function' && define.amd) {
3 // AMD. Register as an anonymous module.
4 define(['exports'], factory);
5 } else if (typeof exports === 'object' && typeof exports.nodeName !== 'string') {
6 // CommonJS
7 factory(exports);
8 } else {
9 // Browser globals
10 factory(root.maquette = {});
11 }
12}(this, function (exports) {
13 'use strict';
14 ;
15 ;
16 ;
17 ;
18 var NAMESPACE_W3 = 'http://www.w3.org/';
19 var NAMESPACE_SVG = NAMESPACE_W3 + '2000/svg';
20 var NAMESPACE_XLINK = NAMESPACE_W3 + '1999/xlink';
21 // Utilities
22 var emptyArray = [];
23 var extend = function (base, overrides) {
24 var result = {};
25 Object.keys(base).forEach(function (key) {
26 result[key] = base[key];
27 });
28 if (overrides) {
29 Object.keys(overrides).forEach(function (key) {
30 result[key] = overrides[key];
31 });
32 }
33 return result;
34 };
35 // Hyperscript helper functions
36 var same = function (vnode1, vnode2) {
37 if (vnode1.vnodeSelector !== vnode2.vnodeSelector) {
38 return false;
39 }
40 if (vnode1.properties && vnode2.properties) {
41 if (vnode1.properties.key !== vnode2.properties.key) {
42 return false;
43 }
44 return vnode1.properties.bind === vnode2.properties.bind;
45 }
46 return !vnode1.properties && !vnode2.properties;
47 };
48 var toTextVNode = function (data) {
49 return {
50 vnodeSelector: '',
51 properties: undefined,
52 children: undefined,
53 text: data.toString(),
54 domNode: null
55 };
56 };
57 var appendChildren = function (parentSelector, insertions, main) {
58 for (var i = 0; i < insertions.length; i++) {
59 var item = insertions[i];
60 if (Array.isArray(item)) {
61 appendChildren(parentSelector, item, main);
62 } else {
63 if (item !== null && item !== undefined) {
64 if (!item.hasOwnProperty('vnodeSelector')) {
65 item = toTextVNode(item);
66 }
67 main.push(item);
68 }
69 }
70 }
71 };
72 // Render helper functions
73 var missingTransition = function () {
74 throw new Error('Provide a transitions object to the projectionOptions to do animations');
75 };
76 var DEFAULT_PROJECTION_OPTIONS = {
77 namespace: undefined,
78 eventHandlerInterceptor: undefined,
79 styleApplyer: function (domNode, styleName, value) {
80 // Provides a hook to add vendor prefixes for browsers that still need it.
81 domNode.style[styleName] = value;
82 },
83 transitions: {
84 enter: missingTransition,
85 exit: missingTransition
86 }
87 };
88 var applyDefaultProjectionOptions = function (projectorOptions) {
89 return extend(DEFAULT_PROJECTION_OPTIONS, projectorOptions);
90 };
91 var checkStyleValue = function (styleValue) {
92 if (typeof styleValue !== 'string') {
93 throw new Error('Style values must be strings');
94 }
95 };
96 var setProperties = function (domNode, properties, projectionOptions) {
97 if (!properties) {
98 return;
99 }
100 var eventHandlerInterceptor = projectionOptions.eventHandlerInterceptor;
101 var propNames = Object.keys(properties);
102 var propCount = propNames.length;
103 for (var i = 0; i < propCount; i++) {
104 var propName = propNames[i];
105 /* tslint:disable:no-var-keyword: edge case */
106 var propValue = properties[propName];
107 /* tslint:enable:no-var-keyword */
108 if (propName === 'className') {
109 throw new Error('Property "className" is not supported, use "class".');
110 } else if (propName === 'class') {
111 if (domNode.className) {
112 // May happen if classes is specified before class
113 domNode.className += ' ' + propValue;
114 } else {
115 domNode.className = propValue;
116 }
117 } else if (propName === 'classes') {
118 // object with string keys and boolean values
119 var classNames = Object.keys(propValue);
120 var classNameCount = classNames.length;
121 for (var j = 0; j < classNameCount; j++) {
122 var className = classNames[j];
123 if (propValue[className]) {
124 domNode.classList.add(className);
125 }
126 }
127 } else if (propName === 'styles') {
128 // object with string keys and string (!) values
129 var styleNames = Object.keys(propValue);
130 var styleCount = styleNames.length;
131 for (var j = 0; j < styleCount; j++) {
132 var styleName = styleNames[j];
133 var styleValue = propValue[styleName];
134 if (styleValue) {
135 checkStyleValue(styleValue);
136 projectionOptions.styleApplyer(domNode, styleName, styleValue);
137 }
138 }
139 } else if (propName === 'key') {
140 continue;
141 } else if (propValue === null || propValue === undefined) {
142 continue;
143 } else {
144 var type = typeof propValue;
145 if (type === 'function') {
146 if (propName.lastIndexOf('on', 0) === 0) {
147 if (eventHandlerInterceptor) {
148 propValue = eventHandlerInterceptor(propName, propValue, domNode, properties); // intercept eventhandlers
149 }
150 if (propName === 'oninput') {
151 (function () {
152 // record the evt.target.value, because IE and Edge sometimes do a requestAnimationFrame between changing value and running oninput
153 var oldPropValue = propValue;
154 propValue = function (evt) {
155 evt.target['oninput-value'] = evt.target.value;
156 // may be HTMLTextAreaElement as well
157 oldPropValue.apply(this, [evt]);
158 };
159 }());
160 }
161 domNode[propName] = propValue;
162 }
163 } else if (type === 'string' && propName !== 'value' && propName !== 'innerHTML') {
164 if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') {
165 domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue);
166 } else {
167 domNode.setAttribute(propName, propValue);
168 }
169 } else {
170 domNode[propName] = propValue;
171 }
172 }
173 }
174 };
175 var updateProperties = function (domNode, previousProperties, properties, projectionOptions) {
176 if (!properties) {
177 return;
178 }
179 var propertiesUpdated = false;
180 var propNames = Object.keys(properties);
181 var propCount = propNames.length;
182 for (var i = 0; i < propCount; i++) {
183 var propName = propNames[i];
184 // assuming that properties will be nullified instead of missing is by design
185 var propValue = properties[propName];
186 var previousValue = previousProperties[propName];
187 if (propName === 'class') {
188 if (previousValue !== propValue) {
189 throw new Error('"class" property may not be updated. Use the "classes" property for conditional css classes.');
190 }
191 } else if (propName === 'classes') {
192 var classList = domNode.classList;
193 var classNames = Object.keys(propValue);
194 var classNameCount = classNames.length;
195 for (var j = 0; j < classNameCount; j++) {
196 var className = classNames[j];
197 var on = !!propValue[className];
198 var previousOn = !!previousValue[className];
199 if (on === previousOn) {
200 continue;
201 }
202 propertiesUpdated = true;
203 if (on) {
204 classList.add(className);
205 } else {
206 classList.remove(className);
207 }
208 }
209 } else if (propName === 'styles') {
210 var styleNames = Object.keys(propValue);
211 var styleCount = styleNames.length;
212 for (var j = 0; j < styleCount; j++) {
213 var styleName = styleNames[j];
214 var newStyleValue = propValue[styleName];
215 var oldStyleValue = previousValue[styleName];
216 if (newStyleValue === oldStyleValue) {
217 continue;
218 }
219 propertiesUpdated = true;
220 if (newStyleValue) {
221 checkStyleValue(newStyleValue);
222 projectionOptions.styleApplyer(domNode, styleName, newStyleValue);
223 } else {
224 projectionOptions.styleApplyer(domNode, styleName, '');
225 }
226 }
227 } else {
228 if (!propValue && typeof previousValue === 'string') {
229 propValue = '';
230 }
231 if (propName === 'value') {
232 if (domNode[propName] !== propValue && domNode['oninput-value'] !== propValue) {
233 domNode[propName] = propValue;
234 // Reset the value, even if the virtual DOM did not change
235 domNode['oninput-value'] = undefined;
236 }
237 // else do not update the domNode, otherwise the cursor position would be changed
238 if (propValue !== previousValue) {
239 propertiesUpdated = true;
240 }
241 } else if (propValue !== previousValue) {
242 var type = typeof propValue;
243 if (type === 'function') {
244 throw new Error('Functions may not be updated on subsequent renders (property: ' + propName + '). Hint: declare event handler functions outside the render() function.');
245 }
246 if (type === 'string' && propName !== 'innerHTML') {
247 if (projectionOptions.namespace === NAMESPACE_SVG && propName === 'href') {
248 domNode.setAttributeNS(NAMESPACE_XLINK, propName, propValue);
249 } else {
250 domNode.setAttribute(propName, propValue);
251 }
252 } else {
253 if (domNode[propName] !== propValue) {
254 domNode[propName] = propValue;
255 }
256 }
257 propertiesUpdated = true;
258 }
259 }
260 }
261 return propertiesUpdated;
262 };
263 var findIndexOfChild = function (children, sameAs, start) {
264 if (sameAs.vnodeSelector !== '') {
265 // Never scan for text-nodes
266 for (var i = start; i < children.length; i++) {
267 if (same(children[i], sameAs)) {
268 return i;
269 }
270 }
271 }
272 return -1;
273 };
274 var nodeAdded = function (vNode, transitions) {
275 if (vNode.properties) {
276 var enterAnimation = vNode.properties.enterAnimation;
277 if (enterAnimation) {
278 if (typeof enterAnimation === 'function') {
279 enterAnimation(vNode.domNode, vNode.properties);
280 } else {
281 transitions.enter(vNode.domNode, vNode.properties, enterAnimation);
282 }
283 }
284 }
285 };
286 var nodeToRemove = function (vNode, transitions) {
287 var domNode = vNode.domNode;
288 if (vNode.properties) {
289 var exitAnimation = vNode.properties.exitAnimation;
290 if (exitAnimation) {
291 domNode.style.pointerEvents = 'none';
292 var removeDomNode = function () {
293 if (domNode.parentNode) {
294 domNode.parentNode.removeChild(domNode);
295 }
296 };
297 if (typeof exitAnimation === 'function') {
298 exitAnimation(domNode, removeDomNode, vNode.properties);
299 return;
300 } else {
301 transitions.exit(vNode.domNode, vNode.properties, exitAnimation, removeDomNode);
302 return;
303 }
304 }
305 }
306 if (domNode.parentNode) {
307 domNode.parentNode.removeChild(domNode);
308 }
309 };
310 var checkDistinguishable = function (childNodes, indexToCheck, parentVNode, operation) {
311 var childNode = childNodes[indexToCheck];
312 if (childNode.vnodeSelector === '') {
313 return; // Text nodes need not be distinguishable
314 }
315 var properties = childNode.properties;
316 var key = properties ? properties.key === undefined ? properties.bind : properties.key : undefined;
317 if (!key) {
318 for (var i = 0; i < childNodes.length; i++) {
319 if (i !== indexToCheck) {
320 var node = childNodes[i];
321 if (same(node, childNode)) {
322 if (operation === 'added') {
323 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.');
324 } else {
325 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.');
326 }
327 }
328 }
329 }
330 }
331 };
332 var createDom;
333 var updateDom;
334 var updateChildren = function (vnode, domNode, oldChildren, newChildren, projectionOptions) {
335 if (oldChildren === newChildren) {
336 return false;
337 }
338 oldChildren = oldChildren || emptyArray;
339 newChildren = newChildren || emptyArray;
340 var oldChildrenLength = oldChildren.length;
341 var newChildrenLength = newChildren.length;
342 var transitions = projectionOptions.transitions;
343 var oldIndex = 0;
344 var newIndex = 0;
345 var i;
346 var textUpdated = false;
347 while (newIndex < newChildrenLength) {
348 var oldChild = oldIndex < oldChildrenLength ? oldChildren[oldIndex] : undefined;
349 var newChild = newChildren[newIndex];
350 if (oldChild !== undefined && same(oldChild, newChild)) {
351 textUpdated = updateDom(oldChild, newChild, projectionOptions) || textUpdated;
352 oldIndex++;
353 } else {
354 var findOldIndex = findIndexOfChild(oldChildren, newChild, oldIndex + 1);
355 if (findOldIndex >= 0) {
356 // Remove preceding missing children
357 for (i = oldIndex; i < findOldIndex; i++) {
358 nodeToRemove(oldChildren[i], transitions);
359 checkDistinguishable(oldChildren, i, vnode, 'removed');
360 }
361 textUpdated = updateDom(oldChildren[findOldIndex], newChild, projectionOptions) || textUpdated;
362 oldIndex = findOldIndex + 1;
363 } else {
364 // New child
365 createDom(newChild, domNode, oldIndex < oldChildrenLength ? oldChildren[oldIndex].domNode : undefined, projectionOptions);
366 nodeAdded(newChild, transitions);
367 checkDistinguishable(newChildren, newIndex, vnode, 'added');
368 }
369 }
370 newIndex++;
371 }
372 if (oldChildrenLength > oldIndex) {
373 // Remove child fragments
374 for (i = oldIndex; i < oldChildrenLength; i++) {
375 nodeToRemove(oldChildren[i], transitions);
376 checkDistinguishable(oldChildren, i, vnode, 'removed');
377 }
378 }
379 return textUpdated;
380 };
381 var addChildren = function (domNode, children, projectionOptions) {
382 if (!children) {
383 return;
384 }
385 for (var i = 0; i < children.length; i++) {
386 createDom(children[i], domNode, undefined, projectionOptions);
387 }
388 };
389 var initPropertiesAndChildren = function (domNode, vnode, projectionOptions) {
390 addChildren(domNode, vnode.children, projectionOptions);
391 // children before properties, needed for value property of <select>.
392 if (vnode.text) {
393 domNode.textContent = vnode.text;
394 }
395 setProperties(domNode, vnode.properties, projectionOptions);
396 if (vnode.properties && vnode.properties.afterCreate) {
397 vnode.properties.afterCreate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children);
398 }
399 };
400 createDom = function (vnode, parentNode, insertBefore, projectionOptions) {
401 var domNode, i, c, start = 0, type, found;
402 var vnodeSelector = vnode.vnodeSelector;
403 if (vnodeSelector === '') {
404 domNode = vnode.domNode = document.createTextNode(vnode.text);
405 if (insertBefore !== undefined) {
406 parentNode.insertBefore(domNode, insertBefore);
407 } else {
408 parentNode.appendChild(domNode);
409 }
410 } else {
411 for (i = 0; i <= vnodeSelector.length; ++i) {
412 c = vnodeSelector.charAt(i);
413 if (i === vnodeSelector.length || c === '.' || c === '#') {
414 type = vnodeSelector.charAt(start - 1);
415 found = vnodeSelector.slice(start, i);
416 if (type === '.') {
417 domNode.classList.add(found);
418 } else if (type === '#') {
419 domNode.id = found;
420 } else {
421 if (found === 'svg') {
422 projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG });
423 }
424 if (projectionOptions.namespace !== undefined) {
425 domNode = vnode.domNode = document.createElementNS(projectionOptions.namespace, found);
426 } else {
427 domNode = vnode.domNode = document.createElement(found);
428 }
429 if (insertBefore !== undefined) {
430 parentNode.insertBefore(domNode, insertBefore);
431 } else {
432 parentNode.appendChild(domNode);
433 }
434 }
435 start = i + 1;
436 }
437 }
438 initPropertiesAndChildren(domNode, vnode, projectionOptions);
439 }
440 };
441 updateDom = function (previous, vnode, projectionOptions) {
442 var domNode = previous.domNode;
443 var textUpdated = false;
444 if (previous === vnode) {
445 return false; // By contract, VNode objects may not be modified anymore after passing them to maquette
446 }
447 var updated = false;
448 if (vnode.vnodeSelector === '') {
449 if (vnode.text !== previous.text) {
450 var newVNode = document.createTextNode(vnode.text);
451 domNode.parentNode.replaceChild(newVNode, domNode);
452 vnode.domNode = newVNode;
453 textUpdated = true;
454 return textUpdated;
455 }
456 } else {
457 if (vnode.vnodeSelector.lastIndexOf('svg', 0) === 0) {
458 projectionOptions = extend(projectionOptions, { namespace: NAMESPACE_SVG });
459 }
460 if (previous.text !== vnode.text) {
461 updated = true;
462 if (vnode.text === undefined) {
463 domNode.removeChild(domNode.firstChild); // the only textnode presumably
464 } else {
465 domNode.textContent = vnode.text;
466 }
467 }
468 updated = updateChildren(vnode, domNode, previous.children, vnode.children, projectionOptions) || updated;
469 updated = updateProperties(domNode, previous.properties, vnode.properties, projectionOptions) || updated;
470 if (vnode.properties && vnode.properties.afterUpdate) {
471 vnode.properties.afterUpdate(domNode, projectionOptions, vnode.vnodeSelector, vnode.properties, vnode.children);
472 }
473 }
474 if (updated && vnode.properties && vnode.properties.updateAnimation) {
475 vnode.properties.updateAnimation(domNode, vnode.properties, previous.properties);
476 }
477 vnode.domNode = previous.domNode;
478 return textUpdated;
479 };
480 var createProjection = function (vnode, projectionOptions) {
481 return {
482 update: function (updatedVnode) {
483 if (vnode.vnodeSelector !== updatedVnode.vnodeSelector) {
484 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)');
485 }
486 updateDom(vnode, updatedVnode, projectionOptions);
487 vnode = updatedVnode;
488 },
489 domNode: vnode.domNode
490 };
491 };
492 ;
493 // The other two parameters are not added here, because the Typescript compiler creates surrogate code for desctructuring 'children'.
494 exports.h = function (selector) {
495 var properties = arguments[1];
496 if (typeof selector !== 'string') {
497 throw new Error();
498 }
499 var childIndex = 1;
500 if (properties && !properties.hasOwnProperty('vnodeSelector') && !Array.isArray(properties) && typeof properties === 'object') {
501 childIndex = 2;
502 } else {
503 // Optional properties argument was omitted
504 properties = undefined;
505 }
506 var text = undefined;
507 var children = undefined;
508 var argsLength = arguments.length;
509 // Recognize a common special case where there is only a single text node
510 if (argsLength === childIndex + 1) {
511 var onlyChild = arguments[childIndex];
512 if (typeof onlyChild === 'string') {
513 text = onlyChild;
514 } else if (onlyChild !== undefined && onlyChild.length === 1 && typeof onlyChild[0] === 'string') {
515 text = onlyChild[0];
516 }
517 }
518 if (text === undefined) {
519 children = [];
520 for (; childIndex < arguments.length; childIndex++) {
521 var child = arguments[childIndex];
522 if (child === null || child === undefined) {
523 continue;
524 } else if (Array.isArray(child)) {
525 appendChildren(selector, child, children);
526 } else if (child.hasOwnProperty('vnodeSelector')) {
527 children.push(child);
528 } else {
529 children.push(toTextVNode(child));
530 }
531 }
532 }
533 return {
534 vnodeSelector: selector,
535 properties: properties,
536 children: children,
537 text: text === '' ? undefined : text,
538 domNode: null
539 };
540 };
541 /**
542 * Contains simple low-level utility functions to manipulate the real DOM.
543 */
544 exports.dom = {
545 /**
546 * Creates a real DOM tree from `vnode`. The [[Projection]] object returned will contain the resulting DOM Node in
547 * its [[Projection.domNode|domNode]] property.
548 * This is a low-level method. Users wil typically use a [[Projector]] instead.
549 * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
550 * objects may only be rendered once.
551 * @param projectionOptions - Options to be used to create and update the projection.
552 * @returns The [[Projection]] which also contains the DOM Node that was created.
553 */
554 create: function (vnode, projectionOptions) {
555 projectionOptions = applyDefaultProjectionOptions(projectionOptions);
556 createDom(vnode, document.createElement('div'), undefined, projectionOptions);
557 return createProjection(vnode, projectionOptions);
558 },
559 /**
560 * Appends a new childnode to the DOM which is generated from a [[VNode]].
561 * This is a low-level method. Users wil typically use a [[Projector]] instead.
562 * @param parentNode - The parent node for the new childNode.
563 * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]]
564 * objects may only be rendered once.
565 * @param projectionOptions - Options to be used to create and update the [[Projection]].
566 * @returns The [[Projection]] that was created.
567 */
568 append: function (parentNode, vnode, projectionOptions) {
569 projectionOptions = applyDefaultProjectionOptions(projectionOptions);
570 createDom(vnode, parentNode, undefined, projectionOptions);
571 return createProjection(vnode, projectionOptions);
572 },
573 /**
574 * Inserts a new DOM node which is generated from a [[VNode]].
575 * This is a low-level method. Users wil typically use a [[Projector]] instead.
576 * @param beforeNode - The node that the DOM Node is inserted before.
577 * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function.
578 * NOTE: [[VNode]] objects may only be rendered once.
579 * @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]].
580 * @returns The [[Projection]] that was created.
581 */
582 insertBefore: function (beforeNode, vnode, projectionOptions) {
583 projectionOptions = applyDefaultProjectionOptions(projectionOptions);
584 createDom(vnode, beforeNode.parentNode, beforeNode, projectionOptions);
585 return createProjection(vnode, projectionOptions);
586 },
587 /**
588 * Merges a new DOM node which is generated from a [[VNode]] with an existing DOM Node.
589 * This means that the virtual DOM and the real DOM will have one overlapping element.
590 * Therefore the selector for the root [[VNode]] will be ignored, but its properties and children will be applied to the Element provided.
591 * This is a low-level method. Users wil typically use a [[Projector]] instead.
592 * @param domNode - The existing element to adopt as the root of the new virtual DOM. Existing attributes and childnodes are preserved.
593 * @param vnode - The root of the virtual DOM tree that was created using the [[h]] function. NOTE: [[VNode]] objects
594 * may only be rendered once.
595 * @param projectionOptions - Options to be used to create and update the projection, see [[createProjector]].
596 * @returns The [[Projection]] that was created.
597 */
598 merge: function (element, vnode, projectionOptions) {
599 projectionOptions = applyDefaultProjectionOptions(projectionOptions);
600 vnode.domNode = element;
601 initPropertiesAndChildren(element, vnode, projectionOptions);
602 return createProjection(vnode, projectionOptions);
603 }
604 };
605 /**
606 * Creates a [[CalculationCache]] object, useful for caching [[VNode]] trees.
607 * In practice, caching of [[VNode]] trees is not needed, because achieving 60 frames per second is almost never a problem.
608 * For more information, see [[CalculationCache]].
609 *
610 * @param <Result> The type of the value that is cached.
611 */
612 exports.createCache = function () {
613 var cachedInputs = undefined;
614 var cachedOutcome = undefined;
615 var result = {
616 invalidate: function () {
617 cachedOutcome = undefined;
618 cachedInputs = undefined;
619 },
620 result: function (inputs, calculation) {
621 if (cachedInputs) {
622 for (var i = 0; i < inputs.length; i++) {
623 if (cachedInputs[i] !== inputs[i]) {
624 cachedOutcome = undefined;
625 }
626 }
627 }
628 if (!cachedOutcome) {
629 cachedOutcome = calculation();
630 cachedInputs = inputs;
631 }
632 return cachedOutcome;
633 }
634 };
635 return result;
636 };
637 /**
638 * Creates a {@link Mapping} instance that keeps an array of result objects synchronized with an array of source objects.
639 * See {@link http://maquettejs.org/docs/arrays.html|Working with arrays}.
640 *
641 * @param <Source> The type of source items. A database-record for instance.
642 * @param <Target> The type of target items. A [[Component]] for instance.
643 * @param getSourceKey `function(source)` that must return a key to identify each source object. The result must either be a string or a number.
644 * @param createResult `function(source, index)` that must create a new result object from a given source. This function is identical
645 * to the `callback` argument in `Array.map(callback)`.
646 * @param updateResult `function(source, target, index)` that updates a result to an updated source.
647 */
648 exports.createMapping = function (getSourceKey, createResult, updateResult) {
649 var keys = [];
650 var results = [];
651 return {
652 results: results,
653 map: function (newSources) {
654 var newKeys = newSources.map(getSourceKey);
655 var oldTargets = results.slice();
656 var oldIndex = 0;
657 for (var i = 0; i < newSources.length; i++) {
658 var source = newSources[i];
659 var sourceKey = newKeys[i];
660 if (sourceKey === keys[oldIndex]) {
661 results[i] = oldTargets[oldIndex];
662 updateResult(source, oldTargets[oldIndex], i);
663 oldIndex++;
664 } else {
665 var found = false;
666 for (var j = 1; j < keys.length; j++) {
667 var searchIndex = (oldIndex + j) % keys.length;
668 if (keys[searchIndex] === sourceKey) {
669 results[i] = oldTargets[searchIndex];
670 updateResult(newSources[i], oldTargets[searchIndex], i);
671 oldIndex = searchIndex + 1;
672 found = true;
673 break;
674 }
675 }
676 if (!found) {
677 results[i] = createResult(source, i);
678 }
679 }
680 }
681 results.length = newSources.length;
682 keys = newKeys;
683 }
684 };
685 };
686 /**
687 * Creates a [[Projector]] instance using the provided projectionOptions.
688 *
689 * For more information, see [[Projector]].
690 *
691 * @param projectionOptions Options that influence how the DOM is rendered and updated.
692 */
693 exports.createProjector = function (projectorOptions) {
694 var projector;
695 var projectionOptions = applyDefaultProjectionOptions(projectorOptions);
696 projectionOptions.eventHandlerInterceptor = function (propertyName, eventHandler, domNode, properties) {
697 return function () {
698 // intercept function calls (event handlers) to do a render afterwards.
699 projector.scheduleRender();
700 return eventHandler.apply(properties.bind || this, arguments);
701 };
702 };
703 var renderCompleted = true;
704 var scheduled;
705 var stopped = false;
706 var projections = [];
707 var renderFunctions = [];
708 // matches the projections array
709 var doRender = function () {
710 scheduled = undefined;
711 if (!renderCompleted) {
712 return; // The last render threw an error, it should be logged in the browser console.
713 }
714 renderCompleted = false;
715 for (var i = 0; i < projections.length; i++) {
716 var updatedVnode = renderFunctions[i]();
717 projections[i].update(updatedVnode);
718 }
719 renderCompleted = true;
720 };
721 projector = {
722 scheduleRender: function () {
723 if (!scheduled && !stopped) {
724 scheduled = requestAnimationFrame(doRender);
725 }
726 },
727 stop: function () {
728 if (scheduled) {
729 cancelAnimationFrame(scheduled);
730 scheduled = undefined;
731 }
732 stopped = true;
733 },
734 resume: function () {
735 stopped = false;
736 renderCompleted = true;
737 projector.scheduleRender();
738 },
739 append: function (parentNode, renderMaquetteFunction) {
740 projections.push(exports.dom.append(parentNode, renderMaquetteFunction(), projectionOptions));
741 renderFunctions.push(renderMaquetteFunction);
742 },
743 insertBefore: function (beforeNode, renderMaquetteFunction) {
744 projections.push(exports.dom.insertBefore(beforeNode, renderMaquetteFunction(), projectionOptions));
745 renderFunctions.push(renderMaquetteFunction);
746 },
747 merge: function (domNode, renderMaquetteFunction) {
748 projections.push(exports.dom.merge(domNode, renderMaquetteFunction(), projectionOptions));
749 renderFunctions.push(renderMaquetteFunction);
750 },
751 replace: function (domNode, renderMaquetteFunction) {
752 var vnode = renderMaquetteFunction();
753 createDom(vnode, domNode.parentNode, domNode, projectionOptions);
754 domNode.parentNode.removeChild(domNode);
755 projections.push(createProjection(vnode, projectionOptions));
756 renderFunctions.push(renderMaquetteFunction);
757 },
758 detach: function (renderMaquetteFunction) {
759 for (var i = 0; i < renderFunctions.length; i++) {
760 if (renderFunctions[i] === renderMaquetteFunction) {
761 renderFunctions.splice(i, 1);
762 return projections.splice(i, 1)[0];
763 }
764 }
765 throw new Error('renderMaquetteFunction was not found');
766 }
767 };
768 return projector;
769 };
770}));