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.

1657 lines
40 KiB

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