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.

1801 lines
44 KiB

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