You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1671 lines
43 KiB

5 years ago
  1. (function (global, factory) {
  2. typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
  3. typeof define === 'function' && define.amd ? define(['exports'], factory) :
  4. (global = global || self, factory(global.riotDOMBindings = {}));
  5. }(this, function (exports) { 'use strict';
  6. /**
  7. * Remove the child nodes from any DOM node
  8. * @param {HTMLElement} node - target node
  9. * @returns {undefined}
  10. */
  11. function cleanNode(node) {
  12. clearChildren(node.childNodes);
  13. }
  14. /**
  15. * Clear multiple children in a node
  16. * @param {HTMLElement[]} children - direct children nodes
  17. * @returns {undefined}
  18. */
  19. function clearChildren(children) {
  20. Array.from(children).forEach(n => n.parentNode && n.parentNode.removeChild(n));
  21. }
  22. const EACH = 0;
  23. const IF = 1;
  24. const SIMPLE = 2;
  25. const TAG = 3;
  26. const SLOT = 4;
  27. var bindingTypes = {
  28. EACH,
  29. IF,
  30. SIMPLE,
  31. TAG,
  32. SLOT
  33. };
  34. /**
  35. * Create the template meta object in case of <template> fragments
  36. * @param {TemplateChunk} componentTemplate - template chunk object
  37. * @returns {Object} the meta property that will be passed to the mount function of the TemplateChunk
  38. */
  39. function createTemplateMeta(componentTemplate) {
  40. const fragment = componentTemplate.dom.cloneNode(true);
  41. return {
  42. avoidDOMInjection: true,
  43. fragment,
  44. children: Array.from(fragment.childNodes)
  45. }
  46. }
  47. /* get rid of the @ungap/essential-map polyfill */
  48. const {indexOf: iOF} = [];
  49. const append = (get, parent, children, start, end, before) => {
  50. const isSelect = 'selectedIndex' in parent;
  51. let noSelection = isSelect;
  52. while (start < end) {
  53. const child = get(children[start], 1);
  54. parent.insertBefore(child, before);
  55. if (isSelect && noSelection && child.selected) {
  56. noSelection = !noSelection;
  57. let {selectedIndex} = parent;
  58. parent.selectedIndex = selectedIndex < 0 ?
  59. start :
  60. iOF.call(parent.querySelectorAll('option'), child);
  61. }
  62. start++;
  63. }
  64. };
  65. const eqeq = (a, b) => a == b;
  66. const identity = O => O;
  67. const indexOf = (
  68. moreNodes,
  69. moreStart,
  70. moreEnd,
  71. lessNodes,
  72. lessStart,
  73. lessEnd,
  74. compare
  75. ) => {
  76. const length = lessEnd - lessStart;
  77. /* istanbul ignore if */
  78. if (length < 1)
  79. return -1;
  80. while ((moreEnd - moreStart) >= length) {
  81. let m = moreStart;
  82. let l = lessStart;
  83. while (
  84. m < moreEnd &&
  85. l < lessEnd &&
  86. compare(moreNodes[m], lessNodes[l])
  87. ) {
  88. m++;
  89. l++;
  90. }
  91. if (l === lessEnd)
  92. return moreStart;
  93. moreStart = m + 1;
  94. }
  95. return -1;
  96. };
  97. const isReversed = (
  98. futureNodes,
  99. futureEnd,
  100. currentNodes,
  101. currentStart,
  102. currentEnd,
  103. compare
  104. ) => {
  105. while (
  106. currentStart < currentEnd &&
  107. compare(
  108. currentNodes[currentStart],
  109. futureNodes[futureEnd - 1]
  110. )) {
  111. currentStart++;
  112. futureEnd--;
  113. } return futureEnd === 0;
  114. };
  115. const next = (get, list, i, length, before) => i < length ?
  116. get(list[i], 0) :
  117. (0 < i ?
  118. get(list[i - 1], -0).nextSibling :
  119. before);
  120. const remove = (get, parent, children, start, end) => {
  121. if ((end - start) < 2)
  122. parent.removeChild(get(children[start], -1));
  123. else {
  124. const range = parent.ownerDocument.createRange();
  125. range.setStartBefore(get(children[start], -1));
  126. range.setEndAfter(get(children[end - 1], -1));
  127. range.deleteContents();
  128. }
  129. };
  130. // - - - - - - - - - - - - - - - - - - -
  131. // diff related constants and utilities
  132. // - - - - - - - - - - - - - - - - - - -
  133. const DELETION = -1;
  134. const INSERTION = 1;
  135. const SKIP = 0;
  136. const SKIP_OND = 50;
  137. const HS = (
  138. futureNodes,
  139. futureStart,
  140. futureEnd,
  141. futureChanges,
  142. currentNodes,
  143. currentStart,
  144. currentEnd,
  145. currentChanges
  146. ) => {
  147. let k = 0;
  148. /* istanbul ignore next */
  149. let minLen = futureChanges < currentChanges ? futureChanges : currentChanges;
  150. const link = Array(minLen++);
  151. const tresh = Array(minLen);
  152. tresh[0] = -1;
  153. for (let i = 1; i < minLen; i++)
  154. tresh[i] = currentEnd;
  155. const keymap = new Map;
  156. for (let i = currentStart; i < currentEnd; i++)
  157. keymap.set(currentNodes[i], i);
  158. for (let i = futureStart; i < futureEnd; i++) {
  159. const idxInOld = keymap.get(futureNodes[i]);
  160. if (idxInOld != null) {
  161. k = findK(tresh, minLen, idxInOld);
  162. /* istanbul ignore else */
  163. if (-1 < k) {
  164. tresh[k] = idxInOld;
  165. link[k] = {
  166. newi: i,
  167. oldi: idxInOld,
  168. prev: link[k - 1]
  169. };
  170. }
  171. }
  172. }
  173. k = --minLen;
  174. --currentEnd;
  175. while (tresh[k] > currentEnd) --k;
  176. minLen = currentChanges + futureChanges - k;
  177. const diff = Array(minLen);
  178. let ptr = link[k];
  179. --futureEnd;
  180. while (ptr) {
  181. const {newi, oldi} = ptr;
  182. while (futureEnd > newi) {
  183. diff[--minLen] = INSERTION;
  184. --futureEnd;
  185. }
  186. while (currentEnd > oldi) {
  187. diff[--minLen] = DELETION;
  188. --currentEnd;
  189. }
  190. diff[--minLen] = SKIP;
  191. --futureEnd;
  192. --currentEnd;
  193. ptr = ptr.prev;
  194. }
  195. while (futureEnd >= futureStart) {
  196. diff[--minLen] = INSERTION;
  197. --futureEnd;
  198. }
  199. while (currentEnd >= currentStart) {
  200. diff[--minLen] = DELETION;
  201. --currentEnd;
  202. }
  203. return diff;
  204. };
  205. // this is pretty much the same petit-dom code without the delete map part
  206. // https://github.com/yelouafi/petit-dom/blob/bd6f5c919b5ae5297be01612c524c40be45f14a7/src/vdom.js#L556-L561
  207. const OND = (
  208. futureNodes,
  209. futureStart,
  210. rows,
  211. currentNodes,
  212. currentStart,
  213. cols,
  214. compare
  215. ) => {
  216. const length = rows + cols;
  217. const v = [];
  218. let d, k, r, c, pv, cv, pd;
  219. outer: for (d = 0; d <= length; d++) {
  220. /* istanbul ignore if */
  221. if (d > SKIP_OND)
  222. return null;
  223. pd = d - 1;
  224. /* istanbul ignore next */
  225. pv = d ? v[d - 1] : [0, 0];
  226. cv = v[d] = [];
  227. for (k = -d; k <= d; k += 2) {
  228. if (k === -d || (k !== d && pv[pd + k - 1] < pv[pd + k + 1])) {
  229. c = pv[pd + k + 1];
  230. } else {
  231. c = pv[pd + k - 1] + 1;
  232. }
  233. r = c - k;
  234. while (
  235. c < cols &&
  236. r < rows &&
  237. compare(
  238. currentNodes[currentStart + c],
  239. futureNodes[futureStart + r]
  240. )
  241. ) {
  242. c++;
  243. r++;
  244. }
  245. if (c === cols && r === rows) {
  246. break outer;
  247. }
  248. cv[d + k] = c;
  249. }
  250. }
  251. const diff = Array(d / 2 + length / 2);
  252. let diffIdx = diff.length - 1;
  253. for (d = v.length - 1; d >= 0; d--) {
  254. while (
  255. c > 0 &&
  256. r > 0 &&
  257. compare(
  258. currentNodes[currentStart + c - 1],
  259. futureNodes[futureStart + r - 1]
  260. )
  261. ) {
  262. // diagonal edge = equality
  263. diff[diffIdx--] = SKIP;
  264. c--;
  265. r--;
  266. }
  267. if (!d)
  268. break;
  269. pd = d - 1;
  270. /* istanbul ignore next */
  271. pv = d ? v[d - 1] : [0, 0];
  272. k = c - r;
  273. if (k === -d || (k !== d && pv[pd + k - 1] < pv[pd + k + 1])) {
  274. // vertical edge = insertion
  275. r--;
  276. diff[diffIdx--] = INSERTION;
  277. } else {
  278. // horizontal edge = deletion
  279. c--;
  280. diff[diffIdx--] = DELETION;
  281. }
  282. }
  283. return diff;
  284. };
  285. const applyDiff = (
  286. diff,
  287. get,
  288. parentNode,
  289. futureNodes,
  290. futureStart,
  291. currentNodes,
  292. currentStart,
  293. currentLength,
  294. before
  295. ) => {
  296. const live = new Map;
  297. const length = diff.length;
  298. let currentIndex = currentStart;
  299. let i = 0;
  300. while (i < length) {
  301. switch (diff[i++]) {
  302. case SKIP:
  303. futureStart++;
  304. currentIndex++;
  305. break;
  306. case INSERTION:
  307. // TODO: bulk appends for sequential nodes
  308. live.set(futureNodes[futureStart], 1);
  309. append(
  310. get,
  311. parentNode,
  312. futureNodes,
  313. futureStart++,
  314. futureStart,
  315. currentIndex < currentLength ?
  316. get(currentNodes[currentIndex], 0) :
  317. before
  318. );
  319. break;
  320. case DELETION:
  321. currentIndex++;
  322. break;
  323. }
  324. }
  325. i = 0;
  326. while (i < length) {
  327. switch (diff[i++]) {
  328. case SKIP:
  329. currentStart++;
  330. break;
  331. case DELETION:
  332. // TODO: bulk removes for sequential nodes
  333. if (live.has(currentNodes[currentStart]))
  334. currentStart++;
  335. else
  336. remove(
  337. get,
  338. parentNode,
  339. currentNodes,
  340. currentStart++,
  341. currentStart
  342. );
  343. break;
  344. }
  345. }
  346. };
  347. const findK = (ktr, length, j) => {
  348. let lo = 1;
  349. let hi = length;
  350. while (lo < hi) {
  351. const mid = ((lo + hi) / 2) >>> 0;
  352. if (j < ktr[mid])
  353. hi = mid;
  354. else
  355. lo = mid + 1;
  356. }
  357. return lo;
  358. };
  359. const smartDiff = (
  360. get,
  361. parentNode,
  362. futureNodes,
  363. futureStart,
  364. futureEnd,
  365. futureChanges,
  366. currentNodes,
  367. currentStart,
  368. currentEnd,
  369. currentChanges,
  370. currentLength,
  371. compare,
  372. before
  373. ) => {
  374. applyDiff(
  375. OND(
  376. futureNodes,
  377. futureStart,
  378. futureChanges,
  379. currentNodes,
  380. currentStart,
  381. currentChanges,
  382. compare
  383. ) ||
  384. HS(
  385. futureNodes,
  386. futureStart,
  387. futureEnd,
  388. futureChanges,
  389. currentNodes,
  390. currentStart,
  391. currentEnd,
  392. currentChanges
  393. ),
  394. get,
  395. parentNode,
  396. futureNodes,
  397. futureStart,
  398. currentNodes,
  399. currentStart,
  400. currentLength,
  401. before
  402. );
  403. };
  404. /*! (c) 2018 Andrea Giammarchi (ISC) */
  405. const domdiff = (
  406. parentNode, // where changes happen
  407. currentNodes, // Array of current items/nodes
  408. futureNodes, // Array of future items/nodes
  409. options // optional object with one of the following properties
  410. // before: domNode
  411. // compare(generic, generic) => true if same generic
  412. // node(generic) => Node
  413. ) => {
  414. if (!options)
  415. options = {};
  416. const compare = options.compare || eqeq;
  417. const get = options.node || identity;
  418. const before = options.before == null ? null : get(options.before, 0);
  419. const currentLength = currentNodes.length;
  420. let currentEnd = currentLength;
  421. let currentStart = 0;
  422. let futureEnd = futureNodes.length;
  423. let futureStart = 0;
  424. // common prefix
  425. while (
  426. currentStart < currentEnd &&
  427. futureStart < futureEnd &&
  428. compare(currentNodes[currentStart], futureNodes[futureStart])
  429. ) {
  430. currentStart++;
  431. futureStart++;
  432. }
  433. // common suffix
  434. while (
  435. currentStart < currentEnd &&
  436. futureStart < futureEnd &&
  437. compare(currentNodes[currentEnd - 1], futureNodes[futureEnd - 1])
  438. ) {
  439. currentEnd--;
  440. futureEnd--;
  441. }
  442. const currentSame = currentStart === currentEnd;
  443. const futureSame = futureStart === futureEnd;
  444. // same list
  445. if (currentSame && futureSame)
  446. return futureNodes;
  447. // only stuff to add
  448. if (currentSame && futureStart < futureEnd) {
  449. append(
  450. get,
  451. parentNode,
  452. futureNodes,
  453. futureStart,
  454. futureEnd,
  455. next(get, currentNodes, currentStart, currentLength, before)
  456. );
  457. return futureNodes;
  458. }
  459. // only stuff to remove
  460. if (futureSame && currentStart < currentEnd) {
  461. remove(
  462. get,
  463. parentNode,
  464. currentNodes,
  465. currentStart,
  466. currentEnd
  467. );
  468. return futureNodes;
  469. }
  470. const currentChanges = currentEnd - currentStart;
  471. const futureChanges = futureEnd - futureStart;
  472. let i = -1;
  473. // 2 simple indels: the shortest sequence is a subsequence of the longest
  474. if (currentChanges < futureChanges) {
  475. i = indexOf(
  476. futureNodes,
  477. futureStart,
  478. futureEnd,
  479. currentNodes,
  480. currentStart,
  481. currentEnd,
  482. compare
  483. );
  484. // inner diff
  485. if (-1 < i) {
  486. append(
  487. get,
  488. parentNode,
  489. futureNodes,
  490. futureStart,
  491. i,
  492. get(currentNodes[currentStart], 0)
  493. );
  494. append(
  495. get,
  496. parentNode,
  497. futureNodes,
  498. i + currentChanges,
  499. futureEnd,
  500. next(get, currentNodes, currentEnd, currentLength, before)
  501. );
  502. return futureNodes;
  503. }
  504. }
  505. /* istanbul ignore else */
  506. else if (futureChanges < currentChanges) {
  507. i = indexOf(
  508. currentNodes,
  509. currentStart,
  510. currentEnd,
  511. futureNodes,
  512. futureStart,
  513. futureEnd,
  514. compare
  515. );
  516. // outer diff
  517. if (-1 < i) {
  518. remove(
  519. get,
  520. parentNode,
  521. currentNodes,
  522. currentStart,
  523. i
  524. );
  525. remove(
  526. get,
  527. parentNode,
  528. currentNodes,
  529. i + futureChanges,
  530. currentEnd
  531. );
  532. return futureNodes;
  533. }
  534. }
  535. // common case with one replacement for many nodes
  536. // or many nodes replaced for a single one
  537. /* istanbul ignore else */
  538. if ((currentChanges < 2 || futureChanges < 2)) {
  539. append(
  540. get,
  541. parentNode,
  542. futureNodes,
  543. futureStart,
  544. futureEnd,
  545. get(currentNodes[currentStart], 0)
  546. );
  547. remove(
  548. get,
  549. parentNode,
  550. currentNodes,
  551. currentStart,
  552. currentEnd
  553. );
  554. return futureNodes;
  555. }
  556. // the half match diff part has been skipped in petit-dom
  557. // https://github.com/yelouafi/petit-dom/blob/bd6f5c919b5ae5297be01612c524c40be45f14a7/src/vdom.js#L391-L397
  558. // accordingly, I think it's safe to skip in here too
  559. // if one day it'll come out like the speediest thing ever to do
  560. // then I might add it in here too
  561. // Extra: before going too fancy, what about reversed lists ?
  562. // This should bail out pretty quickly if that's not the case.
  563. if (
  564. currentChanges === futureChanges &&
  565. isReversed(
  566. futureNodes,
  567. futureEnd,
  568. currentNodes,
  569. currentStart,
  570. currentEnd,
  571. compare
  572. )
  573. ) {
  574. append(
  575. get,
  576. parentNode,
  577. futureNodes,
  578. futureStart,
  579. futureEnd,
  580. next(get, currentNodes, currentEnd, currentLength, before)
  581. );
  582. return futureNodes;
  583. }
  584. // last resort through a smart diff
  585. smartDiff(
  586. get,
  587. parentNode,
  588. futureNodes,
  589. futureStart,
  590. futureEnd,
  591. futureChanges,
  592. currentNodes,
  593. currentStart,
  594. currentEnd,
  595. currentChanges,
  596. currentLength,
  597. compare,
  598. before
  599. );
  600. return futureNodes;
  601. };
  602. /**
  603. * Check if a value is null or undefined
  604. * @param {*} value - anything
  605. * @returns {boolean} true only for the 'undefined' and 'null' types
  606. */
  607. function isNil(value) {
  608. return value === null || value === undefined
  609. }
  610. /**
  611. * Check if an element is a template tag
  612. * @param {HTMLElement} el - element to check
  613. * @returns {boolean} true if it's a <template>
  614. */
  615. function isTemplate(el) {
  616. return !isNil(el.content)
  617. }
  618. const EachBinding = Object.seal({
  619. // dynamic binding properties
  620. childrenMap: null,
  621. node: null,
  622. root: null,
  623. condition: null,
  624. evaluate: null,
  625. template: null,
  626. isTemplateTag: false,
  627. nodes: [],
  628. getKey: null,
  629. indexName: null,
  630. itemName: null,
  631. afterPlaceholder: null,
  632. placeholder: null,
  633. // API methods
  634. mount(scope, parentScope) {
  635. return this.update(scope, parentScope)
  636. },
  637. update(scope, parentScope) {
  638. const { placeholder } = this;
  639. const collection = this.evaluate(scope);
  640. const items = collection ? Array.from(collection) : [];
  641. const parent = placeholder.parentNode;
  642. // prepare the diffing
  643. const {
  644. newChildrenMap,
  645. batches,
  646. futureNodes
  647. } = createPatch(items, scope, parentScope, this);
  648. // patch the DOM only if there are new nodes
  649. if (futureNodes.length) {
  650. domdiff(parent, this.nodes, futureNodes, {
  651. before: placeholder,
  652. node: patch(
  653. Array.from(this.childrenMap.values()),
  654. parentScope
  655. )
  656. });
  657. } else {
  658. // remove all redundant templates
  659. unmountRedundant(this.childrenMap);
  660. }
  661. // trigger the mounts and the updates
  662. batches.forEach(fn => fn());
  663. // update the children map
  664. this.childrenMap = newChildrenMap;
  665. this.nodes = futureNodes;
  666. return this
  667. },
  668. unmount(scope, parentScope) {
  669. unmountRedundant(this.childrenMap, parentScope);
  670. this.childrenMap = new Map();
  671. this.nodes = [];
  672. return this
  673. }
  674. });
  675. /**
  676. * Patch the DOM while diffing
  677. * @param {TemplateChunk[]} redundant - redundant tepmplate chunks
  678. * @param {*} parentScope - scope of the parent template
  679. * @returns {Function} patch function used by domdiff
  680. */
  681. function patch(redundant, parentScope) {
  682. return (item, info) => {
  683. if (info < 0) {
  684. const {template, context} = redundant.pop();
  685. // notice that we pass null as last argument because
  686. // the root node and its children will be removed by domdiff
  687. template.unmount(context, parentScope, null);
  688. }
  689. return item
  690. }
  691. }
  692. /**
  693. * Unmount the remaining template instances
  694. * @param {Map} childrenMap - map containing the children template to unmount
  695. * @param {*} parentScope - scope of the parent template
  696. * @returns {TemplateChunk[]} collection containing the template chunks unmounted
  697. */
  698. function unmountRedundant(childrenMap, parentScope) {
  699. return Array
  700. .from(childrenMap.values())
  701. .map(({template, context}) => {
  702. return template.unmount(context, parentScope, true)
  703. })
  704. }
  705. /**
  706. * Check whether a template must be filtered from a loop
  707. * @param {Function} condition - filter function
  708. * @param {Object} context - argument passed to the filter function
  709. * @returns {boolean} true if this item should be skipped
  710. */
  711. function mustFilterItem(condition, context) {
  712. return condition ? Boolean(condition(context)) === false : false
  713. }
  714. /**
  715. * Extend the scope of the looped template
  716. * @param {Object} scope - current template scope
  717. * @param {string} options.itemName - key to identify the looped item in the new context
  718. * @param {string} options.indexName - key to identify the index of the looped item
  719. * @param {number} options.index - current index
  720. * @param {*} options.item - collection item looped
  721. * @returns {Object} enhanced scope object
  722. */
  723. function extendScope(scope, {itemName, indexName, index, item}) {
  724. scope[itemName] = item;
  725. if (indexName) scope[indexName] = index;
  726. return scope
  727. }
  728. /**
  729. * Loop the current template items
  730. * @param {Array} items - expression collection value
  731. * @param {*} scope - template scope
  732. * @param {*} parentScope - scope of the parent template
  733. * @param {EeachBinding} binding - each binding object instance
  734. * @returns {Object} data
  735. * @returns {Map} data.newChildrenMap - a Map containing the new children template structure
  736. * @returns {Array} data.batches - array containing the template lifecycle functions to trigger
  737. * @returns {Array} data.futureNodes - array containing the nodes we need to diff
  738. */
  739. function createPatch(items, scope, parentScope, binding) {
  740. const { condition, template, childrenMap, itemName, getKey, indexName, root, isTemplateTag } = binding;
  741. const newChildrenMap = new Map();
  742. const batches = [];
  743. const futureNodes = [];
  744. items.forEach((item, index) => {
  745. const context = extendScope(Object.create(scope), {itemName, indexName, index, item});
  746. const key = getKey ? getKey(context) : index;
  747. const oldItem = childrenMap.get(key);
  748. if (mustFilterItem(condition, context)) {
  749. return
  750. }
  751. const componentTemplate = oldItem ? oldItem.template : template.clone();
  752. const el = oldItem ? componentTemplate.el : root.cloneNode();
  753. const mustMount = !oldItem;
  754. const meta = isTemplateTag && mustMount ? createTemplateMeta(componentTemplate) : {};
  755. if (mustMount) {
  756. batches.push(() => componentTemplate.mount(el, context, parentScope, meta));
  757. } else {
  758. componentTemplate.update(context, parentScope);
  759. }
  760. // create the collection of nodes to update or to add
  761. // in case of template tags we need to add all its children nodes
  762. if (isTemplateTag) {
  763. futureNodes.push(...meta.children || componentTemplate.children);
  764. } else {
  765. futureNodes.push(el);
  766. }
  767. // delete the old item from the children map
  768. childrenMap.delete(key);
  769. // update the children map
  770. newChildrenMap.set(key, {
  771. template: componentTemplate,
  772. context,
  773. index
  774. });
  775. });
  776. return {
  777. newChildrenMap,
  778. batches,
  779. futureNodes
  780. }
  781. }
  782. function create(node, { evaluate, condition, itemName, indexName, getKey, template }) {
  783. const placeholder = document.createTextNode('');
  784. const parent = node.parentNode;
  785. const root = node.cloneNode();
  786. parent.insertBefore(placeholder, node);
  787. parent.removeChild(node);
  788. return {
  789. ...EachBinding,
  790. childrenMap: new Map(),
  791. node,
  792. root,
  793. condition,
  794. evaluate,
  795. isTemplateTag: isTemplate(root),
  796. template: template.createDOM(node),
  797. getKey,
  798. indexName,
  799. itemName,
  800. placeholder
  801. }
  802. }
  803. /**
  804. * Binding responsible for the `if` directive
  805. */
  806. const IfBinding = Object.seal({
  807. // dynamic binding properties
  808. node: null,
  809. evaluate: null,
  810. parent: null,
  811. isTemplateTag: false,
  812. placeholder: null,
  813. template: null,
  814. // API methods
  815. mount(scope, parentScope) {
  816. this.parent.insertBefore(this.placeholder, this.node);
  817. this.parent.removeChild(this.node);
  818. return this.update(scope, parentScope)
  819. },
  820. update(scope, parentScope) {
  821. const value = !!this.evaluate(scope);
  822. const mustMount = !this.value && value;
  823. const mustUnmount = this.value && !value;
  824. const mount = () => {
  825. const pristine = this.node.cloneNode();
  826. this.parent.insertBefore(pristine, this.placeholder);
  827. this.template = this.template.clone();
  828. this.template.mount(pristine, scope, parentScope);
  829. };
  830. switch (true) {
  831. case mustMount:
  832. mount();
  833. break
  834. case mustUnmount:
  835. this.unmount(scope);
  836. break
  837. default:
  838. if (value) this.template.update(scope, parentScope);
  839. }
  840. this.value = value;
  841. return this
  842. },
  843. unmount(scope, parentScope) {
  844. this.template.unmount(scope, parentScope, true);
  845. return this
  846. }
  847. });
  848. function create$1(node, { evaluate, template }) {
  849. return {
  850. ...IfBinding,
  851. node,
  852. evaluate,
  853. parent: node.parentNode,
  854. placeholder: document.createTextNode(''),
  855. template: template.createDOM(node)
  856. }
  857. }
  858. const ATTRIBUTE = 0;
  859. const EVENT = 1;
  860. const TEXT = 2;
  861. const VALUE = 3;
  862. var expressionTypes = {
  863. ATTRIBUTE,
  864. EVENT,
  865. TEXT,
  866. VALUE
  867. };
  868. /**
  869. * Check if a value is a Boolean
  870. * @param {*} value - anything
  871. * @returns {boolean} true only for the value is a boolean
  872. */
  873. function isBoolean(value) {
  874. return typeof value === 'boolean'
  875. }
  876. /**
  877. * Check if a value is an Object
  878. * @param {*} value - anything
  879. * @returns {boolean} true only for the value is an object
  880. */
  881. function isObject(value) {
  882. return typeof value === 'object'
  883. }
  884. const REMOVE_ATTRIBUTE = 'removeAttribute';
  885. const SET_ATTIBUTE = 'setAttribute';
  886. /**
  887. * Add all the attributes provided
  888. * @param {HTMLElement} node - target node
  889. * @param {Object} attributes - object containing the attributes names and values
  890. * @returns {undefined} sorry it's a void function :(
  891. */
  892. function setAllAttributes(node, attributes) {
  893. Object
  894. .entries(attributes)
  895. .forEach(([name, value]) => attributeExpression(node, { name }, value));
  896. }
  897. /**
  898. * Remove all the attributes provided
  899. * @param {HTMLElement} node - target node
  900. * @param {Object} attributes - object containing all the attribute names
  901. * @returns {undefined} sorry it's a void function :(
  902. */
  903. function removeAllAttributes(node, attributes) {
  904. Object
  905. .keys(attributes)
  906. .forEach(attribute => node.removeAttribute(attribute));
  907. }
  908. /**
  909. * This methods handles the DOM attributes updates
  910. * @param {HTMLElement} node - target node
  911. * @param {Object} expression - expression object
  912. * @param {string} expression.name - attribute name
  913. * @param {*} value - new expression value
  914. * @param {*} oldValue - the old expression cached value
  915. * @returns {undefined}
  916. */
  917. function attributeExpression(node, { name }, value, oldValue) {
  918. // is it a spread operator? {...attributes}
  919. if (!name) {
  920. // is the value still truthy?
  921. if (value) {
  922. setAllAttributes(node, value);
  923. } else if (oldValue) {
  924. // otherwise remove all the old attributes
  925. removeAllAttributes(node, oldValue);
  926. }
  927. return
  928. }
  929. // handle boolean attributes
  930. if (isBoolean(value) || isObject(value)) {
  931. node[name] = value;
  932. }
  933. node[getMethod(value)](name, normalizeValue(name, value));
  934. }
  935. /**
  936. * Get the attribute modifier method
  937. * @param {*} value - if truthy we return `setAttribute` othewise `removeAttribute`
  938. * @returns {string} the node attribute modifier method name
  939. */
  940. function getMethod(value) {
  941. return isNil(value) || value === false || value === '' || isObject(value) ?
  942. REMOVE_ATTRIBUTE :
  943. SET_ATTIBUTE
  944. }
  945. /**
  946. * Get the value as string
  947. * @param {string} name - attribute name
  948. * @param {*} value - user input value
  949. * @returns {string} input value as string
  950. */
  951. function normalizeValue(name, value) {
  952. // be sure that expressions like selected={ true } will be always rendered as selected='selected'
  953. if (value === true) return name
  954. return value
  955. }
  956. const RE_EVENTS_PREFIX = /^on/;
  957. /**
  958. * Set a new event listener
  959. * @param {HTMLElement} node - target node
  960. * @param {Object} expression - expression object
  961. * @param {string} expression.name - event name
  962. * @param {*} value - new expression value
  963. * @param {*} oldValue - old expression value
  964. * @returns {value} the callback just received
  965. */
  966. function eventExpression(node, { name }, value, oldValue) {
  967. const normalizedEventName = name.replace(RE_EVENTS_PREFIX, '');
  968. if (oldValue) {
  969. node.removeEventListener(normalizedEventName, oldValue);
  970. }
  971. if (value) {
  972. node.addEventListener(normalizedEventName, value, false);
  973. }
  974. return value
  975. }
  976. /**
  977. * This methods handles a simple text expression update
  978. * @param {HTMLElement} node - target node
  979. * @param {Object} expression - expression object
  980. * @param {number} expression.childNodeIndex - index to find the text node to update
  981. * @param {*} value - new expression value
  982. * @returns {undefined}
  983. */
  984. function textExpression(node, { childNodeIndex }, value) {
  985. const target = node.childNodes[childNodeIndex];
  986. const val = normalizeValue$1(value);
  987. // replace the target if it's a placeholder comment
  988. if (target.nodeType === Node.COMMENT_NODE) {
  989. const textNode = document.createTextNode(val);
  990. node.replaceChild(textNode, target);
  991. } else {
  992. target.data = normalizeValue$1(val);
  993. }
  994. }
  995. /**
  996. * Normalize the user value in order to render a empty string in case of falsy values
  997. * @param {*} value - user input value
  998. * @returns {string} hopefully a string
  999. */
  1000. function normalizeValue$1(value) {
  1001. return isNil(value) ? '' : value
  1002. }
  1003. /**
  1004. * This methods handles the input fileds value updates
  1005. * @param {HTMLElement} node - target node
  1006. * @param {Object} expression - expression object
  1007. * @param {*} value - new expression value
  1008. * @returns {undefined}
  1009. */
  1010. function valueExpression(node, expression, value) {
  1011. node.value = value;
  1012. }
  1013. var expressions = {
  1014. [ATTRIBUTE]: attributeExpression,
  1015. [EVENT]: eventExpression,
  1016. [TEXT]: textExpression,
  1017. [VALUE]: valueExpression
  1018. };
  1019. const Expression = Object.seal({
  1020. // Static props
  1021. node: null,
  1022. value: null,
  1023. // API methods
  1024. /**
  1025. * Mount the expression evaluating its initial value
  1026. * @param {*} scope - argument passed to the expression to evaluate its current values
  1027. * @returns {Expression} self
  1028. */
  1029. mount(scope) {
  1030. // hopefully a pure function
  1031. this.value = this.evaluate(scope);
  1032. // IO() DOM updates
  1033. apply(this, this.value);
  1034. return this
  1035. },
  1036. /**
  1037. * Update the expression if its value changed
  1038. * @param {*} scope - argument passed to the expression to evaluate its current values
  1039. * @returns {Expression} self
  1040. */
  1041. update(scope) {
  1042. // pure function
  1043. const value = this.evaluate(scope);
  1044. if (this.value !== value) {
  1045. // IO() DOM updates
  1046. apply(this, value);
  1047. this.value = value;
  1048. }
  1049. return this
  1050. },
  1051. /**
  1052. * Expression teardown method
  1053. * @returns {Expression} self
  1054. */
  1055. unmount() {
  1056. // unmount only the event handling expressions
  1057. if (this.type === EVENT) apply(this, null);
  1058. return this
  1059. }
  1060. });
  1061. /**
  1062. * IO() function to handle the DOM updates
  1063. * @param {Expression} expression - expression object
  1064. * @param {*} value - current expression value
  1065. * @returns {undefined}
  1066. */
  1067. function apply(expression, value) {
  1068. return expressions[expression.type](expression.node, expression, value, expression.value)
  1069. }
  1070. function create$2(node, data) {
  1071. return {
  1072. ...Expression,
  1073. ...data,
  1074. node
  1075. }
  1076. }
  1077. /**
  1078. * Create a flat object having as keys a list of methods that if dispatched will propagate
  1079. * on the whole collection
  1080. * @param {Array} collection - collection to iterate
  1081. * @param {Array<string>} methods - methods to execute on each item of the collection
  1082. * @param {*} context - context returned by the new methods created
  1083. * @returns {Object} a new object to simplify the the nested methods dispatching
  1084. */
  1085. function flattenCollectionMethods(collection, methods, context) {
  1086. return methods.reduce((acc, method) => {
  1087. return {
  1088. ...acc,
  1089. [method]: (scope) => {
  1090. return collection.map(item => item[method](scope)) && context
  1091. }
  1092. }
  1093. }, {})
  1094. }
  1095. function create$3(node, { expressions }) {
  1096. return {
  1097. ...flattenCollectionMethods(
  1098. expressions.map(expression => create$2(node, expression)),
  1099. ['mount', 'update', 'unmount']
  1100. )
  1101. }
  1102. }
  1103. const SlotBinding = Object.seal({
  1104. // dynamic binding properties
  1105. node: null,
  1106. name: null,
  1107. template: null,
  1108. // API methods
  1109. mount(scope, parentScope) {
  1110. const templateData = scope.slots ? scope.slots.find(({id}) => id === this.name) : false;
  1111. const {parentNode} = this.node;
  1112. this.template = templateData && create$6(
  1113. templateData.html,
  1114. templateData.bindings
  1115. ).createDOM(parentNode);
  1116. if (this.template) {
  1117. this.template.mount(this.node, parentScope);
  1118. moveSlotInnerContent(this.node);
  1119. }
  1120. parentNode.removeChild(this.node);
  1121. return this
  1122. },
  1123. update(scope, parentScope) {
  1124. if (this.template && parentScope) {
  1125. this.template.update(parentScope);
  1126. }
  1127. return this
  1128. },
  1129. unmount(scope, parentScope, mustRemoveRoot) {
  1130. if (this.template) {
  1131. this.template.unmount(parentScope, null, mustRemoveRoot);
  1132. }
  1133. return this
  1134. }
  1135. });
  1136. /**
  1137. * Move the inner content of the slots outside of them
  1138. * @param {HTMLNode} slot - slot node
  1139. * @returns {undefined} it's a void function
  1140. */
  1141. function moveSlotInnerContent(slot) {
  1142. if (slot.firstChild) {
  1143. slot.parentNode.insertBefore(slot.firstChild, slot);
  1144. moveSlotInnerContent(slot);
  1145. }
  1146. }
  1147. /**
  1148. * Create a single slot binding
  1149. * @param {HTMLElement} node - slot node
  1150. * @param {string} options.name - slot id
  1151. * @returns {Object} Slot binding object
  1152. */
  1153. function createSlot(node, { name }) {
  1154. return {
  1155. ...SlotBinding,
  1156. node,
  1157. name
  1158. }
  1159. }
  1160. /**
  1161. * Create a new tag object if it was registered before, otherwise fallback to the simple
  1162. * template chunk
  1163. * @param {Function} component - component factory function
  1164. * @param {Array<Object>} slots - array containing the slots markup
  1165. * @param {Array} attributes - dynamic attributes that will be received by the tag element
  1166. * @returns {TagImplementation|TemplateChunk} a tag implementation or a template chunk as fallback
  1167. */
  1168. function getTag(component, slots = [], attributes = []) {
  1169. // if this tag was registered before we will return its implementation
  1170. if (component) {
  1171. return component({ slots, attributes })
  1172. }
  1173. // otherwise we return a template chunk
  1174. return create$6(slotsToMarkup(slots), [
  1175. ...slotBindings(slots), {
  1176. // the attributes should be registered as binding
  1177. // if we fallback to a normal template chunk
  1178. expressions: attributes.map(attr => {
  1179. return {
  1180. type: ATTRIBUTE,
  1181. ...attr
  1182. }
  1183. })
  1184. }
  1185. ])
  1186. }
  1187. /**
  1188. * Merge all the slots bindings into a single array
  1189. * @param {Array<Object>} slots - slots collection
  1190. * @returns {Array<Bindings>} flatten bindings array
  1191. */
  1192. function slotBindings(slots) {
  1193. return slots.reduce((acc, { bindings }) => acc.concat(bindings), [])
  1194. }
  1195. /**
  1196. * Merge all the slots together in a single markup string
  1197. * @param {Array<Object>} slots - slots collection
  1198. * @returns {string} markup of all the slots in a single string
  1199. */
  1200. function slotsToMarkup(slots) {
  1201. return slots.reduce((acc, slot) => {
  1202. return acc + slot.html
  1203. }, '')
  1204. }
  1205. const TagBinding = Object.seal({
  1206. // dynamic binding properties
  1207. node: null,
  1208. evaluate: null,
  1209. name: null,
  1210. slots: null,
  1211. tag: null,
  1212. attributes: null,
  1213. getComponent: null,
  1214. mount(scope) {
  1215. return this.update(scope)
  1216. },
  1217. update(scope, parentScope) {
  1218. const name = this.evaluate(scope);
  1219. // simple update
  1220. if (name === this.name) {
  1221. this.tag.update(scope);
  1222. } else {
  1223. // unmount the old tag if it exists
  1224. this.unmount(scope, parentScope, true);
  1225. // mount the new tag
  1226. this.name = name;
  1227. this.tag = getTag(this.getComponent(name), this.slots, this.attributes);
  1228. this.tag.mount(this.node, scope);
  1229. }
  1230. return this
  1231. },
  1232. unmount(scope, parentScope, keepRootTag) {
  1233. if (this.tag) {
  1234. // keep the root tag
  1235. this.tag.unmount(keepRootTag);
  1236. }
  1237. return this
  1238. }
  1239. });
  1240. function create$4(node, { evaluate, getComponent, slots, attributes }) {
  1241. return {
  1242. ...TagBinding,
  1243. node,
  1244. evaluate,
  1245. slots,
  1246. attributes,
  1247. getComponent
  1248. }
  1249. }
  1250. var bindings = {
  1251. [IF]: create$1,
  1252. [SIMPLE]: create$3,
  1253. [EACH]: create,
  1254. [TAG]: create$4,
  1255. [SLOT]: createSlot
  1256. };
  1257. /**
  1258. * Bind a new expression object to a DOM node
  1259. * @param {HTMLElement} root - DOM node where to bind the expression
  1260. * @param {Object} binding - binding data
  1261. * @returns {Expression} Expression object
  1262. */
  1263. function create$5(root, binding) {
  1264. const { selector, type, redundantAttribute, expressions } = binding;
  1265. // find the node to apply the bindings
  1266. const node = selector ? root.querySelector(selector) : root;
  1267. // remove eventually additional attributes created only to select this node
  1268. if (redundantAttribute) node.removeAttribute(redundantAttribute);
  1269. // init the binding
  1270. return (bindings[type] || bindings[SIMPLE])(
  1271. node,
  1272. {
  1273. ...binding,
  1274. expressions: expressions || []
  1275. }
  1276. )
  1277. }
  1278. /**
  1279. * Check if an element is part of an svg
  1280. * @param {HTMLElement} el - element to check
  1281. * @returns {boolean} true if we are in an svg context
  1282. */
  1283. function isSvg(el) {
  1284. const owner = el.ownerSVGElement;
  1285. return !!owner || owner === null
  1286. }
  1287. // in this case a simple innerHTML is enough
  1288. function createHTMLTree(html, root) {
  1289. const template = isTemplate(root) ? root : document.createElement('template');
  1290. template.innerHTML = html;
  1291. return template.content
  1292. }
  1293. // for svg nodes we need a bit more work
  1294. function createSVGTree(html, container) {
  1295. // create the SVGNode
  1296. const svgNode = container.ownerDocument.importNode(
  1297. new window.DOMParser()
  1298. .parseFromString(
  1299. `<svg xmlns="http://www.w3.org/2000/svg">${html}</svg>`,
  1300. 'application/xml'
  1301. )
  1302. .documentElement,
  1303. true
  1304. );
  1305. return svgNode
  1306. }
  1307. /**
  1308. * Create the DOM that will be injected
  1309. * @param {Object} root - DOM node to find out the context where the fragment will be created
  1310. * @param {string} html - DOM to create as string
  1311. * @returns {HTMLDocumentFragment|HTMLElement} a new html fragment
  1312. */
  1313. function createDOMTree(root, html) {
  1314. if (isSvg(root)) return createSVGTree(html, root)
  1315. return createHTMLTree(html, root)
  1316. }
  1317. /**
  1318. * Move all the child nodes from a source tag to another
  1319. * @param {HTMLElement} source - source node
  1320. * @param {HTMLElement} target - target node
  1321. * @returns {undefined} it's a void method ¯\_()_/¯
  1322. */
  1323. // Ignore this helper because it's needed only for svg tags
  1324. /* istanbul ignore next */
  1325. function moveChildren(source, target) {
  1326. if (source.firstChild) {
  1327. target.appendChild(source.firstChild);
  1328. moveChildren(source, target);
  1329. }
  1330. }
  1331. /**
  1332. * Inject the DOM tree into a target node
  1333. * @param {HTMLElement} el - target element
  1334. * @param {HTMLFragment|SVGElement} dom - dom tree to inject
  1335. * @returns {undefined}
  1336. */
  1337. function injectDOM(el, dom) {
  1338. switch (true) {
  1339. case isSvg(el):
  1340. moveChildren(dom, el);
  1341. break
  1342. case isTemplate(el):
  1343. el.parentNode.replaceChild(dom, el);
  1344. break
  1345. default:
  1346. el.appendChild(dom);
  1347. }
  1348. }
  1349. /**
  1350. * Create the Template DOM skeleton
  1351. * @param {HTMLElement} el - root node where the DOM will be injected
  1352. * @param {string} html - markup that will be injected into the root node
  1353. * @returns {HTMLFragment} fragment that will be injected into the root node
  1354. */
  1355. function createTemplateDOM(el, html) {
  1356. return html && (typeof html === 'string' ?
  1357. createDOMTree(el, html) :
  1358. html)
  1359. }
  1360. /**
  1361. * Template Chunk model
  1362. * @type {Object}
  1363. */
  1364. const TemplateChunk = Object.freeze({
  1365. // Static props
  1366. bindings: null,
  1367. bindingsData: null,
  1368. html: null,
  1369. isTemplateTag: false,
  1370. fragment: null,
  1371. children: null,
  1372. dom: null,
  1373. el: null,
  1374. /**
  1375. * Create the template DOM structure that will be cloned on each mount
  1376. * @param {HTMLElement} el - the root node
  1377. * @returns {TemplateChunk} self
  1378. */
  1379. createDOM(el) {
  1380. // make sure that the DOM gets created before cloning the template
  1381. this.dom = this.dom || createTemplateDOM(el, this.html);
  1382. return this
  1383. },
  1384. // API methods
  1385. /**
  1386. * Attach the template to a DOM node
  1387. * @param {HTMLElement} el - target DOM node
  1388. * @param {*} scope - template data
  1389. * @param {*} parentScope - scope of the parent template tag
  1390. * @param {Object} meta - meta properties needed to handle the <template> tags in loops
  1391. * @returns {TemplateChunk} self
  1392. */
  1393. mount(el, scope, parentScope, meta = {}) {
  1394. if (!el) throw new Error('Please provide DOM node to mount properly your template')
  1395. if (this.el) this.unmount(scope);
  1396. // <template> tags require a bit more work
  1397. // the template fragment might be already created via meta outside of this call
  1398. const {fragment, children, avoidDOMInjection} = meta;
  1399. // <template> bindings of course can not have a root element
  1400. // so we check the parent node to set the query selector bindings
  1401. const {parentNode} = children ? children[0] : el;
  1402. this.isTemplateTag = isTemplate(el);
  1403. // create the DOM if it wasn't created before
  1404. this.createDOM(el);
  1405. if (this.dom) {
  1406. // create the new template dom fragment if it want already passed in via meta
  1407. this.fragment = fragment || this.dom.cloneNode(true);
  1408. }
  1409. // store root node
  1410. // notice that for template tags the root note will be the parent tag
  1411. this.el = this.isTemplateTag ? parentNode : el;
  1412. // create the children array only for the <template> fragments
  1413. this.children = this.isTemplateTag ? children || Array.from(this.fragment.childNodes) : null;
  1414. // inject the DOM into the el only if a fragment is available
  1415. if (!avoidDOMInjection && this.fragment) injectDOM(el, this.fragment);
  1416. // create the bindings
  1417. this.bindings = this.bindingsData.map(binding => create$5(this.el, binding));
  1418. this.bindings.forEach(b => b.mount(scope, parentScope));
  1419. return this
  1420. },
  1421. /**
  1422. * Update the template with fresh data
  1423. * @param {*} scope - template data
  1424. * @param {*} parentScope - scope of the parent template tag
  1425. * @returns {TemplateChunk} self
  1426. */
  1427. update(scope, parentScope) {
  1428. this.bindings.forEach(b => b.update(scope, parentScope));
  1429. return this
  1430. },
  1431. /**
  1432. * Remove the template from the node where it was initially mounted
  1433. * @param {*} scope - template data
  1434. * @param {*} parentScope - scope of the parent template tag
  1435. * @param {boolean|null} mustRemoveRoot - if true remove the root element,
  1436. * if false or undefined clean the root tag content, if null don't touch the DOM
  1437. * @returns {TemplateChunk} self
  1438. */
  1439. unmount(scope, parentScope, mustRemoveRoot) {
  1440. if (this.el) {
  1441. this.bindings.forEach(b => b.unmount(scope, parentScope, mustRemoveRoot));
  1442. switch (true) {
  1443. // <template> tags should be treated a bit differently
  1444. // we need to clear their children only if it's explicitly required by the caller
  1445. // via mustRemoveRoot !== null
  1446. case this.isTemplateTag === true && mustRemoveRoot !== null:
  1447. clearChildren(this.children);
  1448. break
  1449. // remove the root node only if the mustRemoveRoot === true
  1450. case mustRemoveRoot === true && this.el.parentNode !== null:
  1451. this.el.parentNode.removeChild(this.el);
  1452. break
  1453. // otherwise we clean the node children
  1454. case mustRemoveRoot !== null:
  1455. cleanNode(this.el);
  1456. break
  1457. }
  1458. this.el = null;
  1459. }
  1460. return this
  1461. },
  1462. /**
  1463. * Clone the template chunk
  1464. * @returns {TemplateChunk} a clone of this object resetting the this.el property
  1465. */
  1466. clone() {
  1467. return {
  1468. ...this,
  1469. el: null
  1470. }
  1471. }
  1472. });
  1473. /**
  1474. * Create a template chunk wiring also the bindings
  1475. * @param {string|HTMLElement} html - template string
  1476. * @param {Array} bindings - bindings collection
  1477. * @returns {TemplateChunk} a new TemplateChunk copy
  1478. */
  1479. function create$6(html, bindings = []) {
  1480. return {
  1481. ...TemplateChunk,
  1482. html,
  1483. bindingsData: bindings
  1484. }
  1485. }
  1486. exports.bindingTypes = bindingTypes;
  1487. exports.createBinding = create$5;
  1488. exports.createExpression = create$2;
  1489. exports.expressionTypes = expressionTypes;
  1490. exports.template = create$6;
  1491. Object.defineProperty(exports, '__esModule', { value: true });
  1492. }));