Forking what is left of ZeroNet and hopefully adding an AT Proto Frontend/Proxy
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 770 lines 36 kB view raw
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}));