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.

1815 lines
48 KiB

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