rasti 4.0.0-alpha.5 → 4.0.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <p align="center">
2
2
  <picture>
3
- <source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/8tentaculos/rasti@v4.0.0-alpha.5/docs/logo-dark.svg">
4
- <img alt="Rasti.js" src="https://cdn.jsdelivr.net/gh/8tentaculos/rasti@v4.0.0-alpha.5/docs/logo.svg" height="120">
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/8tentaculos/rasti@v4.0.0-alpha.6/docs/logo-dark.svg">
4
+ <img alt="Rasti.js" src="https://cdn.jsdelivr.net/gh/8tentaculos/rasti@v4.0.0-alpha.6/docs/logo.svg" height="120">
5
5
  </picture>
6
6
  </p>
7
7
 
@@ -182,10 +182,12 @@ For detailed information on how to use **Rasti**, refer to the [API documentatio
182
182
 
183
183
  ## Version History
184
184
 
185
- - **[v4.0.0](https://github.com/8tentaculos/rasti/releases/tag/v4.0.0)** - Current version with simpler event handling, smarter component reuse, and granular DOM updates
186
- - **[v3.0.0](https://github.com/8tentaculos/rasti/releases/tag/v3.0.0)** - Introduced **Containers**, dynamic element tags, **Component** tags, `partial` method, and dropped ES5 support
187
- - **[v2.0.0](https://github.com/8tentaculos/rasti/releases/tag/v2.0.0)** - Introduced **Components**
188
- - **[v1.0.0](https://github.com/8tentaculos/rasti/releases/tag/v1.0.0)** - Initial release
185
+ We strive to minimize breaking changes between major versions. However, if you're migrating between major versions, please refer to the release notes below for details on any breaking changes and migration tips.
186
+
187
+ - **[v4.0.0](https://github.com/8tentaculos/rasti/releases/tag/v4.0.0)**
188
+ - **[v3.0.0](https://github.com/8tentaculos/rasti/releases/tag/v3.0.0)**
189
+ - **[v2.0.0](https://github.com/8tentaculos/rasti/releases/tag/v2.0.0)**
190
+ - **[v1.0.0](https://github.com/8tentaculos/rasti/releases/tag/v1.0.0)**
189
191
 
190
192
  ## License
191
193
 
package/dist/rasti.js CHANGED
@@ -830,7 +830,7 @@
830
830
  * Ensure that the view has a unique id at `this.uid`.
831
831
  */
832
832
  ensureUid() {
833
- if (!this.uid) this.uid = `r-${++View.uid}`;
833
+ if (!this.uid) this.uid = `r${++View.uid}`;
834
834
  }
835
835
 
836
836
  /**
@@ -879,7 +879,7 @@
879
879
  * @return {Rasti.View} Return `this` for chaining.
880
880
  */
881
881
  removeElement() {
882
- this.el.parentNode.removeChild(this.el);
882
+ this.el.remove();
883
883
  // Return `this` for chaining.
884
884
  return this;
885
885
  }
@@ -1258,7 +1258,7 @@
1258
1258
  * @param {Function} options.getAttributes Function that returns the attributes object for the element.
1259
1259
  * @private
1260
1260
  */
1261
- class Element {
1261
+ let Element$1 = class Element {
1262
1262
  constructor(options) {
1263
1263
  this.getSelector = options.getSelector;
1264
1264
  this.getAttributes = options.getAttributes;
@@ -1299,7 +1299,7 @@
1299
1299
  }
1300
1300
  });
1301
1301
  }
1302
- }
1302
+ };
1303
1303
 
1304
1304
  /**
1305
1305
  * Properties that should be mirrored from source to target.
@@ -1459,7 +1459,7 @@
1459
1459
  * @param {Function} options.getEnd Function that returns the end comment marker text.
1460
1460
  * @param {any} options.expression The expression to be evaluated for the interpolation.
1461
1461
  * @param {Function} options.isComponent Function that checks if an element is a component root element.
1462
- * @param {Function} options.isElement Function that checks if an element has the Rasti data attribute.
1462
+ * @param {Function} options.containsComponent Function that checks if an element contains a component.
1463
1463
  * @private
1464
1464
  */
1465
1465
  class Interpolation {
@@ -1467,8 +1467,8 @@
1467
1467
  this.getStart = options.getStart;
1468
1468
  this.getEnd = options.getEnd;
1469
1469
  this.expression = options.expression;
1470
- this.isComponent = options.isComponent;
1471
- this.isElement = options.isElement;
1470
+ this.shouldSkipFind = options.shouldSkipFind;
1471
+ this.shouldSkipSync = options.shouldSkipSync;
1472
1472
  this.previousItems = [];
1473
1473
  }
1474
1474
 
@@ -1478,12 +1478,12 @@
1478
1478
  * @param {Node} parent The parent node to search in.
1479
1479
  */
1480
1480
  hydrate(parent) {
1481
- const startComment = findComment(parent, this.getStart(), this.isComponent);
1482
- const endComment = findComment(parent, this.getEnd(), this.isComponent, startComment);
1481
+ const startMark = findComment(parent, this.getStart(), this.shouldSkipFind);
1482
+ const endMark = findComment(parent, this.getEnd(), this.shouldSkipFind, startMark);
1483
1483
 
1484
1484
  this.ref = [
1485
- startComment,
1486
- endComment
1485
+ startMark,
1486
+ endMark
1487
1487
  ];
1488
1488
  }
1489
1489
 
@@ -1493,39 +1493,67 @@
1493
1493
  * @param {DocumentFragment} fragment The new content fragment to insert.
1494
1494
  * @param {Element} targetElement The target element to replace with the new fragment.
1495
1495
  */
1496
- update(fragment, targetElement) {
1497
- if (targetElement) {
1498
- targetElement.replaceWith(fragment.firstChild);
1499
- return;
1500
- }
1501
-
1502
- const [startComment, endComment] = this.ref;
1496
+ update(fragment, handleComponents) {
1497
+ let divider;
1498
+ // Reference to marker elements.
1499
+ const [startMark, endMark] = this.ref;
1503
1500
 
1504
- const currentFirstElement = startComment.nextSibling;
1505
- const currentEmpty = currentFirstElement === endComment;
1506
- const currentSingleChildElement = !currentEmpty && currentFirstElement.nextSibling === endComment;
1507
- const fragmentChildren = fragment.children;
1501
+ const currentFirstElement = startMark.nextSibling;
1502
+ // Check if the interpolation is empty.
1503
+ const currentEmpty = currentFirstElement === endMark;
1504
+ // Check if the interpolation has a single child element.
1505
+ const currentSingleChildElement = !currentEmpty && currentFirstElement.nextSibling === endMark;
1508
1506
 
1509
1507
  if (currentEmpty) {
1510
- // Interpolation is empty. Insert the fragment.
1511
- endComment.parentNode.insertBefore(fragment, endComment);
1512
- } else if (currentSingleChildElement && fragmentChildren.length === 1) {
1513
- if (!this.isElement(currentFirstElement)) {
1514
- // There is a single child element that is not a component's root element. Sync node attributes and content.
1515
- syncNode(currentFirstElement, fragmentChildren[0]);
1508
+ // The interpolation is empty. Insert the fragment.
1509
+ endMark.before(fragment);
1510
+ } else if (
1511
+ currentSingleChildElement &&
1512
+ fragment.children.length === 1 &&
1513
+ !this.shouldSkipSync(currentFirstElement) &&
1514
+ !this.shouldSkipSync(fragment.firstChild)
1515
+ ) {
1516
+ // The interpolation has a single child element and the new fragment has a single child element.
1517
+ // The elements doesn't contain a component. Sync node attributes and content.
1518
+ syncNode(currentFirstElement, fragment.firstChild);
1519
+ } else {
1520
+ // The interpolation has multiple child elements or the new fragment has multiple child elements.
1521
+ // Insert a divider comment before the end marker and insert the new fragment after the divider.
1522
+ divider = document.createComment('');
1523
+ endMark.before(divider);
1524
+ endMark.before(fragment);
1525
+ }
1526
+ // Handle the components in the fragment. Execute the handler function.
1527
+ handleComponents();
1528
+
1529
+ if (divider) {
1530
+ // Remove old content and divider.
1531
+ const startMark = this.ref[0];
1532
+ if (startMark.nextSibling === divider) {
1533
+ divider.remove();
1516
1534
  } else {
1517
- // There is a single child element that is a component's root element. Replace it with the new child element.
1518
- currentFirstElement.replaceWith(fragmentChildren[0]);
1535
+ const range = document.createRange();
1536
+ range.setStartAfter(this.ref[0]);
1537
+ range.setEndAfter(divider);
1538
+ range.deleteContents();
1519
1539
  }
1520
- } else {
1521
- // Interpolation is not empty. Replace the interpolation content.
1522
- const range = document.createRange();
1523
- range.setStartAfter(startComment);
1524
- range.setEndBefore(endComment);
1525
- range.deleteContents();
1526
- range.insertNode(fragment);
1527
1540
  }
1528
1541
  }
1542
+
1543
+ /**
1544
+ * Update the interpolation content with a new fragment. This is used for container components.
1545
+ * @param {Element} element The element to replace with the new fragment.
1546
+ * @param {DocumentFragment} fragment The new content fragment to insert.
1547
+ * @param {Function} handleComponents Function to handle the components in the fragment before removing the old content.
1548
+ */
1549
+ updateElement(element, fragment, handleComponents) {
1550
+ const divider = document.createComment('');
1551
+ element.after(divider);
1552
+ divider.after(fragment.firstChild);
1553
+ handleComponents();
1554
+ element.remove();
1555
+ divider.remove();
1556
+ }
1529
1557
  }
1530
1558
 
1531
1559
  /**
@@ -1578,6 +1606,29 @@
1578
1606
  return html.join(' ');
1579
1607
  }
1580
1608
 
1609
+ /**
1610
+ * Replaces an existing DOM node with a new node, preserving internal DOM state.
1611
+ * Uses moveBefore if available, otherwise falls back to before.
1612
+ *
1613
+ * @param {Node} oldNode The existing DOM node to replace.
1614
+ * @param {Node} newNode The new DOM node to replace the old node with.
1615
+ * @module
1616
+ * @private
1617
+ */
1618
+ function replaceNode(oldNode, newNode) {
1619
+ if (Element.prototype.moveBefore) {
1620
+ oldNode.parentNode.moveBefore(newNode, oldNode);
1621
+ oldNode.remove();
1622
+ } else {
1623
+ const activeElement = document.activeElement;
1624
+ oldNode.before(newNode);
1625
+ oldNode.remove();
1626
+ if (activeElement && activeElement !== document.activeElement && newNode.contains(activeElement)) {
1627
+ activeElement.focus();
1628
+ }
1629
+ }
1630
+ }
1631
+
1581
1632
  /**
1582
1633
  * Same as getResult, but pass context as argument to the expression.
1583
1634
  * Used to evaluate expressions in the context of a component.
@@ -1598,13 +1649,13 @@
1598
1649
  const isComponent = (el) => !!(el && el.dataset && el.dataset[Component.DATASET_ELEMENT] && el.dataset[Component.DATASET_ELEMENT].endsWith('-1'));
1599
1650
 
1600
1651
  /**
1601
- * Check if an element has the Rasti data attribute.
1602
- * This includes both component root elements and regular tracked elements.
1652
+ * Check if an element contains a component.
1653
+ * It checks if the element is a component root element or if it contains a component.
1603
1654
  * @param {Element} el The element to check.
1604
- * @return {boolean} True if the element has the data attribute.
1655
+ * @return {boolean} True if the element contains a component.
1605
1656
  * @private
1606
1657
  */
1607
- const isElement = (el) => !!(el && el.dataset && el.dataset[Component.DATASET_ELEMENT]);
1658
+ const containsElement = (el) => !!(el && el.dataset && el.dataset[Component.DATASET_ELEMENT]) || !!el.querySelector(`[${Component.ATTRIBUTE_ELEMENT}]`);
1608
1659
 
1609
1660
  /**
1610
1661
  * Generate string with placeholders for interpolated expressions.
@@ -2175,9 +2226,8 @@
2175
2226
  recycle(parent, props) {
2176
2227
  if (parent) {
2177
2228
  // Locate the placeholder comment and replace it with the real nodes
2178
- const toBeReplaced = findComment(parent, Component.MARKER_RECYCLED(this.uid), isComponent);
2179
- // Replace it with this.el.
2180
- toBeReplaced.replaceWith(this.el);
2229
+ const placeholder = findComment(parent, Component.MARKER_RECYCLED(this.uid), isComponent);
2230
+ replaceNode(placeholder, this.el);
2181
2231
  }
2182
2232
  // Update props.
2183
2233
  if (props) {
@@ -2253,8 +2303,8 @@
2253
2303
  /**
2254
2304
  * Render a template part.
2255
2305
  * @param {any} part - The template part.
2256
- * @param {function} addChild - The addChild function.
2257
- * @param {Array} items - The items array.
2306
+ * @param {function} addChild - The addChild function. It should handle children and return a HTML string.
2307
+ * @param {Array} items - The items array. It is used to store the items that are parsed.
2258
2308
  * @return {string} The rendered template part.
2259
2309
  * @private
2260
2310
  */
@@ -2316,6 +2366,24 @@
2316
2366
  * Used internally on the render process.
2317
2367
  * Use it for server-side rendering or static site generation.
2318
2368
  * @return {string} The rendered component.
2369
+ * @example
2370
+ * import { Component } from 'rasti';
2371
+ * const Button = Component.create`
2372
+ * <button class="button">Click me</button>
2373
+ * `;
2374
+ * const App = Component.create`
2375
+ * <div>
2376
+ * <${Button}>Click me</${Button}>
2377
+ * </div>
2378
+ * `;
2379
+ *
2380
+ * const app = new App();
2381
+ *
2382
+ * console.log(app.toString());
2383
+ * // <div data-rasti-el="r1-1"><!--rasti-s-r1-1--><button class="button" data-rasti-el="r2-1">Click me</button><!--rasti-e-r1-1--></div>
2384
+ *
2385
+ * console.log(`${app}`);
2386
+ * // <div data-rasti-el="r1-1"><!--rasti-s-r1-1--><button class="button" data-rasti-el="r2-1">Click me</button><!--rasti-e-r1-1--></div>
2319
2387
  */
2320
2388
  toString() {
2321
2389
  // Normally there won't be any children, but if there are, destroy them.
@@ -2359,8 +2427,6 @@
2359
2427
  this.eventsManager.reset();
2360
2428
  // Reset position tracking.
2361
2429
  this.pathManager.reset();
2362
- // Store active element.
2363
- const activeElement = this.isContainer() ? null : document.activeElement;
2364
2430
  // Update elements.
2365
2431
  this.template.elements.forEach(element => element.update());
2366
2432
  // Store previous children.
@@ -2373,7 +2439,9 @@
2373
2439
  const recycledChildren = [];
2374
2440
 
2375
2441
  this.pathManager.increment();
2376
-
2442
+ // `addChild` handler is called from `renderTemplatePart` for every component.
2443
+ // It should handle children and return a HTML string.
2444
+ // In this case, where the component updates, it handles children recycling.
2377
2445
  const addChild = component => {
2378
2446
  let out = component;
2379
2447
  let found = null;
@@ -2422,13 +2490,19 @@
2422
2490
  }
2423
2491
  // Parse the rendered content and get the fragment.
2424
2492
  const fragment = parseHTML(rendered);
2425
- // Add recycled components to children and move them to the new template in the fragment.
2426
- recycledChildren.forEach(recycled => recycle(recycled, fragment));
2427
- // Add new children. Hydrate them and update the interpolation.
2428
- nextChildren.forEach(child => this.addChild(child).hydrate(fragment));
2429
- // Update the interpolation.
2430
- const targetElement = this.isContainer() ? this.el : null;
2431
- interpolation.update(fragment, targetElement);
2493
+
2494
+ const handleComponents = () => {
2495
+ // Add recycled components to children and move them to the new template in the fragment.
2496
+ recycledChildren.forEach(recycled => recycle(recycled, this.el));
2497
+ // Add new children. Hydrate them and update the interpolation.
2498
+ nextChildren.forEach(child => this.addChild(child).hydrate(this.el));
2499
+ };
2500
+
2501
+ if (this.isContainer()) {
2502
+ interpolation.updateElement(this.el, fragment, handleComponents);
2503
+ } else {
2504
+ interpolation.update(fragment, handleComponents);
2505
+ }
2432
2506
  });
2433
2507
  // Destroy unused children.
2434
2508
  previousChildren.forEach(prev => {
@@ -2443,10 +2517,6 @@
2443
2517
  this.delegateEvents();
2444
2518
  }
2445
2519
  }
2446
- // Restore focus.
2447
- if (activeElement && this.el.contains(activeElement)) {
2448
- activeElement.focus();
2449
- }
2450
2520
  // Call onUpdate lifecycle method.
2451
2521
  this.onUpdate.call(this);
2452
2522
  // Return this for chaining.
@@ -2723,7 +2793,7 @@
2723
2793
  return this.extend({
2724
2794
  template() {
2725
2795
  return {
2726
- elements : elements.map(element => new Element({
2796
+ elements : elements.map(element => new Element$1({
2727
2797
  getSelector : element.getSelector.bind(this),
2728
2798
  getAttributes : element.getAttributes.bind(this)
2729
2799
  })),
@@ -2731,8 +2801,8 @@
2731
2801
  getStart : interpolation.getStart.bind(this),
2732
2802
  getEnd : interpolation.getEnd.bind(this),
2733
2803
  expression : interpolation.expression,
2734
- isComponent,
2735
- isElement
2804
+ shouldSkipFind : isComponent,
2805
+ shouldSkipSync : containsElement
2736
2806
  })),
2737
2807
  parts,
2738
2808
  };
@@ -2755,14 +2825,14 @@
2755
2825
  /*
2756
2826
  * Placeholders used to temporarily replace expressions in the template.
2757
2827
  */
2758
- Component.PLACEHOLDER = (idx) => `__RASTI-${idx}__`;
2828
+ Component.PLACEHOLDER = (idx) => `__RASTI_PH_${idx}__`;
2759
2829
 
2760
2830
  /*
2761
2831
  * Markers used to identify interpolation and recycled components.
2762
2832
  */
2763
- Component.MARKER_RECYCLED = (uid) => `rasti-recycled-${uid}`;
2764
- Component.MARKER_START = (uid) => `rasti-start-${uid}`;
2765
- Component.MARKER_END = (uid) => `rasti-end-${uid}`;
2833
+ Component.MARKER_RECYCLED = (uid) => `rasti-r-${uid}`;
2834
+ Component.MARKER_START = (uid) => `rasti-s-${uid}`;
2835
+ Component.MARKER_END = (uid) => `rasti-e-${uid}`;
2766
2836
 
2767
2837
  /**
2768
2838
  * Components are a special kind of `View` that is designed to be easily composable,
package/dist/rasti.min.js CHANGED
@@ -1 +1 @@
1
- !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).Rasti={})}(this,(function(t){"use strict";function e(t){if("function"!=typeof t)throw new TypeError("Listener must be a function")}class s{on(t,s){return e(s),this.listeners||(this.listeners={}),this.listeners[t]||(this.listeners[t]=[]),this.listeners[t].push(s),()=>this.off(t,s)}once(t,s){e(s);const i=(...e)=>{s(...e),this.off(t,i)};return this.on(t,i)}off(t,e){this.listeners&&(t?this.listeners[t]&&(e?(this.listeners[t]=this.listeners[t].filter((t=>t!==e)),this.listeners[t].length||delete this.listeners[t]):delete this.listeners[t],Object.keys(this.listeners).length||delete this.listeners):delete this.listeners)}emit(t,...e){this.listeners&&this.listeners[t]&&this.listeners[t].slice().forEach((t=>t(...e)))}listenTo(t,e,s){return t.on(e,s),this.listeningTo||(this.listeningTo=[]),this.listeningTo.push({emitter:t,type:e,listener:s}),()=>this.stopListening(t,e,s)}listenToOnce(t,s,i){e(i);const n=(...e)=>{i(...e),this.stopListening(t,s,n)};return this.listenTo(t,s,n)}stopListening(t,e,s){this.listeningTo&&(this.listeningTo=this.listeningTo.filter((i=>!(!t||t===i.emitter&&!e||t===i.emitter&&e===i.type&&!s||t===i.emitter&&e===i.type&&s===i.listener)||(i.emitter.off(i.type,i.listener),!1))),this.listeningTo.length||delete this.listeningTo)}}const i=(t,e,...s)=>"function"!=typeof t?t:t.apply(e,s);class n extends s{constructor(){super(),this.preinitialize.apply(this,arguments),this.attributes=Object.assign({},i(this.defaults,this),this.parse.apply(this,arguments)),this.previous={},Object.keys(this.attributes).forEach(this.defineAttribute.bind(this))}preinitialize(){}defineAttribute(t){Object.defineProperty(this,`${this.constructor.attributePrefix}${t}`,{get:()=>this.get(t),set:e=>{this.set(t,e)}})}get(t){return this.attributes[t]}set(t,e,...s){let i,n;"object"==typeof t?(i=t,n=[e,...s]):(i={[t]:e},n=s);const r=this._changing;this._changing=!0;const h={};r||(this.previous=Object.assign({},this.attributes)),Object.keys(i).forEach((t=>{i[t]!==this.attributes[t]&&(h[t]=i[t],this.attributes[t]=i[t])}));const o=Object.keys(h);if(o.length&&(this._pending=["change",this,h,...n]),o.forEach((t=>{this.emit(`change:${t}`,this,i[t],...n)})),r)return this;for(;this._pending;){const t=this._pending;this._pending=null,this.emit.apply(this,t)}return this._pending=null,this._changing=!1,this}parse(t){return t}toJSON(){return Object.assign({},this.attributes)}}n.attributePrefix="";const r=["el","tag","attributes","events","model","template","onDestroy"];class h extends s{constructor(t={}){super(),this.preinitialize.apply(this,arguments),this.delegatedEventListeners=[],this.children=[],this.destroyQueue=[],this.viewOptions=[],r.forEach((e=>{e in t&&(this[e]=t[e],this.viewOptions.push(e))})),this.ensureUid(),this.ensureElement()}preinitialize(){}$(t){return this.el.querySelector(t)}$$(t){return this.el.querySelectorAll(t)}destroy(){return this.destroyChildren(),this.undelegateEvents(),this.stopListening(),this.off(),this.destroyQueue.forEach((t=>t())),this.destroyQueue=[],this.onDestroy.apply(this,arguments),this.destroyed=!0,this}onDestroy(){}addChild(t){return this.children.push(t),t}destroyChildren(){this.children.forEach((t=>t.destroy())),this.children=[]}ensureUid(){this.uid||(this.uid="r-"+ ++h.uid)}ensureElement(){if(this.el)this.el=i(this.el,this);else{const t=i(this.tag,this),e=i(this.attributes,this);this.el=this.createElement(t,e)}this.delegateEvents()}createElement(t="div",e={}){let s=document.createElement(t);return Object.keys(e).forEach((t=>s.setAttribute(t,e[t]))),s}removeElement(){return this.el.parentNode.removeChild(this.el),this}delegateEvents(t){if(t||(t=i(this.events,this)),!t)return this;this.delegatedEventListeners.length&&this.undelegateEvents();let s={};return Object.keys(t).forEach((i=>{const n=i.split(" "),r=n.shift(),h=n.join(" ");let o=t[i];"string"==typeof o&&(o=this[o]),e(o),s[r]||(s[r]=[]),s[r].push({selector:h,listener:o})})),Object.keys(s).forEach((t=>{const e=e=>{s[t].forEach((({selector:t,listener:s})=>{if(!t)return void s.call(this,e,this,this.el);let i=e.target;for(;i&&i!==this.el;)i.matches&&i.matches(t)&&s.call(this,e,this,i),i=i.parentElement}))};this.delegatedEventListeners.push({type:t,listener:e}),this.el.addEventListener(t,e)})),this}undelegateEvents(){return this.delegatedEventListeners.forEach((({type:t,listener:e})=>{this.el.removeEventListener(t,e)})),this.delegatedEventListeners=[],this}render(){return this.template&&(this.el.innerHTML=this.template(this.model)),this}static sanitize(t){return`${t}`.replace(/[&<>"']/g,(t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"}[t])))}}h.uid=0;class o{constructor(t){this.value=t}toString(){return this.value}}class a{constructor(t){this.items=t}}class l{constructor(){this.listeners=[],this.types=new Set,this.previousSize=0}addListener(t,e){return this.types.add(e),this.listeners.push(t),this.listeners.length-1}reset(){this.listeners=[],this.previousSize=this.types.size}hasPendingTypes(){return this.types.size>this.previousSize}}class c{constructor(){}reset(){this.paused=0,this.previous=this.tracked||new Map,this.tracked=new Map,this.positionStack=[0]}push(){this.positionStack.push(0)}pop(){this.positionStack.pop()}increment(){this.positionStack[this.positionStack.length-1]++}pause(){this.paused++}resume(){this.paused--}getPath(){return this.positionStack.join("-")}track(t){return 0===this.paused&&this.tracked.set(this.getPath(),t),t}findRecyclable(t){const e=this.previous.get(this.getPath());return e&&e.constructor===t&&!e.key?e:null}}const u=["value","checked","selected"];class p{constructor(t){this.getSelector=t.getSelector,this.getAttributes=t.getAttributes,this.previousAttributes={}}hydrate(t){this.ref=t.querySelector(this.getSelector())}update(){const t=this.getAttributes(),{remove:e,add:s}=function(t,e={}){const s={},i=[];return Object.keys(t).forEach((i=>{let n=t[i];n!==e[i]&&(!0===n?s[i]="":!1!==n&&(null==n&&(n=""),s[i]=n))})),Object.keys(e).forEach((s=>{(!(s in t)||e[s]!==t[s]&&!1===t[s])&&i.push(s)})),{add:s,remove:i}}(t,this.previousAttributes);this.previousAttributes=t,e.forEach((t=>{this.ref.removeAttribute(t),u.includes(t)&&t in this.ref&&(this.ref[t]="value"===t&&"")})),Object.keys(s).forEach((t=>{const e=s[t];this.ref.setAttribute(t,e),u.includes(t)&&t in this.ref&&(this.ref[t]="value"===t?e:!1!==e&&"false"!==e)}))}}const d=["value","checked","selected"];function f(t,e){t.nodeType===e.nodeType?t.nodeType!==Node.TEXT_NODE?t.tagName===e.tagName?(((t,e)=>{const s=e.attributes,i=t.attributes,n=new Set;for(let e=0,i=s.length;e<i;e++){const{name:i,value:r}=s[e];n.add(i),t.getAttribute(i)!==r&&t.setAttribute(i,r)}for(let e=i.length-1;e>=0;e--){const{name:s}=i[e];n.has(s)||t.removeAttribute(s)}for(let s=0,i=d.length;s<i;s++){const i=d[s];i in t&&t[i]!==e[i]&&(t[i]=e[i])}})(t,e),((t,e)=>{const s=t.childNodes,i=e.childNodes,n=s.length;if(n!==i.length)return!1;for(let t=0;t<n;t++)if(!s[t].isEqualNode(i[t]))return!1;return!0})(t,e)||((t,e)=>{const s=Array.from(e.childNodes);t.replaceChildren(...s)})(t,e)):t.replaceWith(e):t.nodeValue!==e.nodeValue&&(t.nodeValue=e.nodeValue):t.replaceWith(e)}function g(t,e,s=(()=>!1),i){let n=i||t.firstChild;for(;n;){if(n.nodeType===Node.COMMENT_NODE&&n.data.trim()===e)return n;if(n.nodeType!==Node.ELEMENT_NODE||s(n)||!n.firstChild){for(;n&&!n.nextSibling;)if(n=n.parentNode,!n||n===t)return null;n&&(n=n.nextSibling)}else n=n.firstChild}return null}class E{constructor(t){this.getStart=t.getStart,this.getEnd=t.getEnd,this.expression=t.expression,this.isComponent=t.isComponent,this.isElement=t.isElement,this.previousItems=[]}hydrate(t){const e=g(t,this.getStart(),this.isComponent),s=g(t,this.getEnd(),this.isComponent,e);this.ref=[e,s]}update(t,e){if(e)return void e.replaceWith(t.firstChild);const[s,i]=this.ref,n=s.nextSibling,r=n===i,h=!r&&n.nextSibling===i,o=t.children;if(r)i.parentNode.insertBefore(t,i);else if(h&&1===o.length)this.isElement(n)?n.replaceWith(o[0]):f(n,o[0]);else{const e=document.createRange();e.setStartAfter(s),e.setEndBefore(i),e.deleteContents(),e.insertNode(t)}}}const m=t=>t.reduce(((t,e)=>(Array.isArray(e)?t.push(...m(e)):t.push(e),t)),[]);function y(t){const e=document.createElement("template");return e.innerHTML=`${t}`.trim(),e.content}function T(t){const e=[];return Object.keys(t).forEach((s=>{let i=t[s];!0===i?e.push(s):!1!==i&&(null==i&&(i=""),e.push(`${s}="${i}"`))})),e.join(" ")}const b=(t,e)=>i(t,e,e),v=t=>!!(t&&t.dataset&&t.dataset[_.DATASET_ELEMENT]&&t.dataset[_.DATASET_ELEMENT].endsWith("-1")),A=t=>!!(t&&t.dataset&&t.dataset[_.DATASET_ELEMENT]),R=(t,e)=>t.reduce(((t,s,i)=>(t.push(s),void 0!==e[i]&&t.push(_.PLACEHOLDER(i)),t)),[]).join(""),L=(t,e)=>{const s=_.PLACEHOLDER("(\\d+)"),i=t.match(new RegExp(`^${s}$`));if(i)return[e[parseInt(i[1],10)]];const n=new RegExp(`${s}`,"g"),r=[];let h,o=0;for(;null!==(h=n.exec(t));){const s=t.slice(o,h.index);r.push(_.markAsSafeHTML(s),e[parseInt(h[1],10)]),o=h.index+h[0].length}return r.push(_.markAsSafeHTML(t.slice(o))),r},C=(t,e)=>t.reduce(((t,s)=>{const i=e(s[0]);if(1===s.length)"object"==typeof i?t=Object.assign(t,i):"string"==typeof i&&(t[i]=!0);else{const n=s[2]?e(s[1]):s[1];t[i]=n}return t}),{}),$=(t,e,s)=>{const i={};return Object.keys(t).forEach((n=>{const r=n.match(/on(([A-Z]{1}[a-z]+)+)/);if(r&&r[1]){const h=r[1].toLowerCase(),o=t[n];if(o){const t=e.addListener(o,h);i[s(h)]=t}}else i[n]=t[n]})),i},S=(t,e,s=!1)=>{const i=_.PLACEHOLDER("(\\d+)"),n=new Map;return s||(t=t.replace(new RegExp(i,"g"),((t,s)=>{const i=e[s];if(i&&i.prototype instanceof _){if(n.has(i))return n.get(i);n.set(i,t)}return t}))),t.replace(new RegExp(`<(${i})([^>]*)>([\\s\\S]*?)</\\1>|<(${i})([^>]*)/>`,"g"),((t,s,i,n,r,h,o,l)=>{let c,u,p;if(s?(c=e[i],u=n):(c=void 0!==o?e[o]:h,u=l),!(c.prototype instanceof _))return t;if(s){const t=S(r,e,!0),s=x(t,e);p=L(s,e)}const d=w(u,e);return e.push((function(){const t=C(d,(t=>b(t,this)));return p&&(t.renderChildren=()=>new a(p.map((t=>b(t,this))))),c.mount(t)})),_.PLACEHOLDER(e.length-1)}))},M=(t,e)=>{const s=_.PLACEHOLDER("(?:\\d+)");return t.replace(new RegExp(`<(${s}|[a-z]+[1-6]?)(?:\\s*)((?:"[^"]*"|'[^']*'|[^>])*)(/?>)`,"gi"),e)},O=(t,e,s)=>{const n=_.PLACEHOLDER("(?:\\d+)");if(t.match(new RegExp(`^\\s*${n}\\s*$`)))return t;if(!t.match(new RegExp(`^\\s*<([a-z]+[1-6]?|${n})([^>]*)>([\\s\\S]*?)</(\\1|${n})>\\s*$|^\\s*<([a-z]+[1-6]?|${n})([^>]*)/>\\s*$`)))throw new SyntaxError(`Template must have a single root element or be a container component: "${t.trim()}"`);let r=0;return M(t,((t,h,o,a)=>{const l=0===r,c=++r;if(!l&&!o.match(new RegExp(n)))return t;const u=w(o,e),p=t=>`${t}-${c}`,d=s.length;s.push({getSelector:function(){return`[${_.ATTRIBUTE_ELEMENT}="${p(this.uid)}"]`},getAttributes:function(){const t=$(C(u,(t=>b(t,this))),this.eventsManager,(t=>_.ATTRIBUTE_EVENT(t,this.uid)));return l&&this.attributes&&Object.assign(t,i(this.attributes,this)),t[_.ATTRIBUTE_ELEMENT]=p(this.uid),t}}),e.push((function(){const t=this.template.elements[d],e=t.getAttributes.call(this);return t.previousAttributes=e,_.markAsSafeHTML(T(e))}));return`<${h} ${_.PLACEHOLDER(e.length-1)}${a}`}))},x=(t,e)=>{const s=_.PLACEHOLDER("(?:\\d+)");return M(t,((t,i,n,r)=>{if(!n.match(new RegExp(s)))return t;const h=w(n,e),o=function(){return $(C(h,(t=>b(t,this))),this.eventsManager,(t=>_.ATTRIBUTE_EVENT(t,this.uid)))};e.push((function(){const t=o.call(this);return _.markAsSafeHTML(T(t))}));return`<${i} ${_.PLACEHOLDER(e.length-1)}${r}`}))},k=(t,e,s)=>{const i=_.PLACEHOLDER("(\\d+)");let n=0;return t.replace(new RegExp(i,"g"),(function(i,r,h){const o=t.substring(0,h);if(o.lastIndexOf("<")>o.lastIndexOf(">"))return i;const a=++n;const l=s.length;return s.push({getStart:function(){return _.MARKER_START(`${this.uid}-${a}`)},getEnd:function(){return _.MARKER_END(`${this.uid}-${a}`)},expression:e[r]}),e.push((function(){return this.template.interpolations[l]})),_.PLACEHOLDER(e.length-1)}))},w=(t,e)=>{const s=_.PLACEHOLDER("(\\d+)"),i=[],n=new RegExp(`(${s}|[\\w-]+)(?:=(["']?)(?:${s}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s*/?[>"']))+.))\\3)?`,"g");let r;for(;null!==(r=n.exec(t));){const[,t,s,n,h,o]=r,a=void 0!==s?e[parseInt(s,10)]:t,l=void 0!==h?e[parseInt(h,10)]:o;void 0!==l?i.push([a,l,!!n]):i.push([a])}return i},N=["key","state","onCreate","onChange","onHydrate","onRecycle","onUpdate"];class _ extends h{constructor(t={}){super(...arguments),this.componentOptions=[],N.forEach((e=>{e in t&&(this[e]=t[e],this.componentOptions.push(e))}));const e={};Object.keys(t).forEach((s=>{this.viewOptions.includes(s)||this.componentOptions.includes(s)||(e[s]=t[s])})),this.props=new n(e),this.options=t,this.partial=this.partial.bind(this),this.onChange=this.onChange.bind(this),this.onCreate.apply(this,arguments)}events(){const t={};return this.eventsManager.types.forEach((s=>{const i=_.ATTRIBUTE_EVENT(s,this.uid),n=function(t,s,n){const r=n.getAttribute(i);if(r){let i=this.eventsManager.listeners[parseInt(r,10)];"string"==typeof i&&(i=this[i]),e(i),i.call(this,t,s,n)}};t[`${s} [${i}]`]=n,t[s]=n})),t}ensureElement(){this.eventsManager=new l,this.pathManager=new c,this.template=i(this.template,this),this.el&&(this.el=i(this.el,this),this.toString(),this.hydrate(this.el.parentNode))}isContainer(){return 0===this.template.elements.length&&1===this.template.interpolations.length}subscribe(t,e="change",s=this.onChange){return t.on&&this.listenTo(t,e,s),this}hydrate(t){return["model","state","props"].forEach((t=>{this[t]&&this.subscribe(this[t])})),this.isContainer()?(this.children[0].hydrate(t),this.el=this.children[0].el):(this.template.elements.forEach(((e,s)=>{0===s?(e.hydrate(t),this.el?e.ref=this.el:this.el=e.ref):e.hydrate(this.el)})),this.delegateEvents(),this.template.interpolations.forEach((t=>t.hydrate(this.el))),this.children.forEach((t=>t.hydrate(this.el)))),this.onHydrate.call(this),this}recycle(t,e){if(t){g(t,_.MARKER_RECYCLED(this.uid),v).replaceWith(this.el)}return e&&this.props.set(e),this.onRecycle.call(this),this}getRecycledMarker(){return`\x3c!--${_.MARKER_RECYCLED(this.uid)}--\x3e`}partial(t,...e){const s=L(x(S(R(t,e).trim(),e),e),e).map((t=>b(t,this)));return new a(s)}renderTemplatePart(t,e,s=[]){const i=b(t,this),n=t=>{if(null==t||!1===t||!0===t)return"";if(t instanceof o)return s.push(t),t;if(t instanceof _)return s.push(t),e(t);if(t instanceof a){if(1===t.items.length)return n(t.items[0]);this.pathManager.push();const e=t.items.map((t=>(this.pathManager.increment(),n(t)))).join("");return this.pathManager.pop(),e}if(Array.isArray(t)){this.pathManager.pause();const e=m(t).map(n).join("");return this.pathManager.resume(),e}if(t instanceof E){this.pathManager.increment();const e=this.isContainer()?"":`\x3c!--${t.getStart()}--\x3e`,i=this.isContainer()?"":`\x3c!--${t.getEnd()}--\x3e`,r=n(b(t.expression,this));return t.previousItems=s,`${e}${r}${i}`}const i=_.sanitize(t);return s.push(i),i};return`${n(i)}`}toString(){this.destroyChildren(),this.eventsManager.reset(),this.pathManager.reset();const t=t=>(this.pathManager.track(t),this.addChild(t));return this.template.parts.map((e=>this.renderTemplatePart(e,t))).join("")}render(){if(this.destroyed)return this;if(!this.el){const t=y(this);return this.hydrate(t),this}this.eventsManager.reset(),this.pathManager.reset();const t=this.isContainer()?null:document.activeElement;this.template.elements.forEach((t=>t.update()));const e=this.children;return this.children=[],this.template.interpolations.forEach((t=>{const s=[],i=[];this.pathManager.increment();const n=t.previousItems,r=[],h=this.renderTemplatePart(t.expression,(t=>{let n=t,r=null;return r=t.key?e.find((e=>e.key===t.key)):this.pathManager.findRecyclable(t.constructor),r?(n=r.getRecycledMarker(),i.push([r,t]),r.key||this.pathManager.track(r)):(s.push(t),this.pathManager.track(t)),n}),r);t.previousItems=r;const o=([t,e],s)=>{this.addChild(t).recycle(s,e.props.toJSON()),e.destroy()};if(1===n.length&&n[0]instanceof _&&1===r.length&&r[0]instanceof _&&1===i.length)return void o(i[0],null);const a=y(h);i.forEach((t=>o(t,a))),s.forEach((t=>this.addChild(t).hydrate(a)));const l=this.isContainer()?this.el:null;t.update(a,l)})),e.forEach((t=>{this.children.indexOf(t)<0&&t.destroy()})),this.isContainer()?this.el=this.children[0].el:this.eventsManager.hasPendingTypes()&&this.delegateEvents(),t&&this.el.contains(t)&&t.focus(),this.onUpdate.call(this),this}onCreate(){}onChange(){this.render()}onHydrate(){}onRecycle(){}onUpdate(){}onDestroy(){}static markAsSafeHTML(t){return new o(t)}static extend(t){const e=this;class s extends e{}return Object.assign(s.prototype,"function"==typeof t?t(e.prototype):t),s}static mount(t,e,s){const i=new this(t);return e&&(s?(i.toString(),i.hydrate(e)):e.append(i.render().el)),i}static create(t,...e){"function"==typeof t&&(e=[t],t=["",""]);const s=[],i=[],n=L(k(O(S(R(t,e).trim(),e),e,s),e,i),e);return this.extend({template(){return{elements:s.map((t=>new p({getSelector:t.getSelector.bind(this),getAttributes:t.getAttributes.bind(this)}))),interpolations:i.map((t=>new E({getStart:t.getStart.bind(this),getEnd:t.getEnd.bind(this),expression:t.expression,isComponent:v,isElement:A}))),parts:n}}})}}_.ATTRIBUTE_ELEMENT="data-rasti-el",_.ATTRIBUTE_EVENT=(t,e)=>`data-rasti-on-${t}-${e}`,_.DATASET_ELEMENT="rastiEl",_.PLACEHOLDER=t=>`__RASTI-${t}__`,_.MARKER_RECYCLED=t=>`rasti-recycled-${t}`,_.MARKER_START=t=>`rasti-start-${t}`,_.MARKER_END=t=>`rasti-end-${t}`;var j=_.create`<div></div>`;t.Component=j,t.Emitter=s,t.Model=n,t.View=h}));
1
+ !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).Rasti={})}(this,(function(t){"use strict";function e(t){if("function"!=typeof t)throw new TypeError("Listener must be a function")}class s{on(t,s){return e(s),this.listeners||(this.listeners={}),this.listeners[t]||(this.listeners[t]=[]),this.listeners[t].push(s),()=>this.off(t,s)}once(t,s){e(s);const i=(...e)=>{s(...e),this.off(t,i)};return this.on(t,i)}off(t,e){this.listeners&&(t?this.listeners[t]&&(e?(this.listeners[t]=this.listeners[t].filter((t=>t!==e)),this.listeners[t].length||delete this.listeners[t]):delete this.listeners[t],Object.keys(this.listeners).length||delete this.listeners):delete this.listeners)}emit(t,...e){this.listeners&&this.listeners[t]&&this.listeners[t].slice().forEach((t=>t(...e)))}listenTo(t,e,s){return t.on(e,s),this.listeningTo||(this.listeningTo=[]),this.listeningTo.push({emitter:t,type:e,listener:s}),()=>this.stopListening(t,e,s)}listenToOnce(t,s,i){e(i);const n=(...e)=>{i(...e),this.stopListening(t,s,n)};return this.listenTo(t,s,n)}stopListening(t,e,s){this.listeningTo&&(this.listeningTo=this.listeningTo.filter((i=>!(!t||t===i.emitter&&!e||t===i.emitter&&e===i.type&&!s||t===i.emitter&&e===i.type&&s===i.listener)||(i.emitter.off(i.type,i.listener),!1))),this.listeningTo.length||delete this.listeningTo)}}const i=(t,e,...s)=>"function"!=typeof t?t:t.apply(e,s);class n extends s{constructor(){super(),this.preinitialize.apply(this,arguments),this.attributes=Object.assign({},i(this.defaults,this),this.parse.apply(this,arguments)),this.previous={},Object.keys(this.attributes).forEach(this.defineAttribute.bind(this))}preinitialize(){}defineAttribute(t){Object.defineProperty(this,`${this.constructor.attributePrefix}${t}`,{get:()=>this.get(t),set:e=>{this.set(t,e)}})}get(t){return this.attributes[t]}set(t,e,...s){let i,n;"object"==typeof t?(i=t,n=[e,...s]):(i={[t]:e},n=s);const r=this._changing;this._changing=!0;const h={};r||(this.previous=Object.assign({},this.attributes)),Object.keys(i).forEach((t=>{i[t]!==this.attributes[t]&&(h[t]=i[t],this.attributes[t]=i[t])}));const o=Object.keys(h);if(o.length&&(this._pending=["change",this,h,...n]),o.forEach((t=>{this.emit(`change:${t}`,this,i[t],...n)})),r)return this;for(;this._pending;){const t=this._pending;this._pending=null,this.emit.apply(this,t)}return this._pending=null,this._changing=!1,this}parse(t){return t}toJSON(){return Object.assign({},this.attributes)}}n.attributePrefix="";const r=["el","tag","attributes","events","model","template","onDestroy"];class h extends s{constructor(t={}){super(),this.preinitialize.apply(this,arguments),this.delegatedEventListeners=[],this.children=[],this.destroyQueue=[],this.viewOptions=[],r.forEach((e=>{e in t&&(this[e]=t[e],this.viewOptions.push(e))})),this.ensureUid(),this.ensureElement()}preinitialize(){}$(t){return this.el.querySelector(t)}$$(t){return this.el.querySelectorAll(t)}destroy(){return this.destroyChildren(),this.undelegateEvents(),this.stopListening(),this.off(),this.destroyQueue.forEach((t=>t())),this.destroyQueue=[],this.onDestroy.apply(this,arguments),this.destroyed=!0,this}onDestroy(){}addChild(t){return this.children.push(t),t}destroyChildren(){this.children.forEach((t=>t.destroy())),this.children=[]}ensureUid(){this.uid||(this.uid="r"+ ++h.uid)}ensureElement(){if(this.el)this.el=i(this.el,this);else{const t=i(this.tag,this),e=i(this.attributes,this);this.el=this.createElement(t,e)}this.delegateEvents()}createElement(t="div",e={}){let s=document.createElement(t);return Object.keys(e).forEach((t=>s.setAttribute(t,e[t]))),s}removeElement(){return this.el.remove(),this}delegateEvents(t){if(t||(t=i(this.events,this)),!t)return this;this.delegatedEventListeners.length&&this.undelegateEvents();let s={};return Object.keys(t).forEach((i=>{const n=i.split(" "),r=n.shift(),h=n.join(" ");let o=t[i];"string"==typeof o&&(o=this[o]),e(o),s[r]||(s[r]=[]),s[r].push({selector:h,listener:o})})),Object.keys(s).forEach((t=>{const e=e=>{s[t].forEach((({selector:t,listener:s})=>{if(!t)return void s.call(this,e,this,this.el);let i=e.target;for(;i&&i!==this.el;)i.matches&&i.matches(t)&&s.call(this,e,this,i),i=i.parentElement}))};this.delegatedEventListeners.push({type:t,listener:e}),this.el.addEventListener(t,e)})),this}undelegateEvents(){return this.delegatedEventListeners.forEach((({type:t,listener:e})=>{this.el.removeEventListener(t,e)})),this.delegatedEventListeners=[],this}render(){return this.template&&(this.el.innerHTML=this.template(this.model)),this}static sanitize(t){return`${t}`.replace(/[&<>"']/g,(t=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#039;"}[t])))}}h.uid=0;class o{constructor(t){this.value=t}toString(){return this.value}}class a{constructor(t){this.items=t}}class l{constructor(){this.listeners=[],this.types=new Set,this.previousSize=0}addListener(t,e){return this.types.add(e),this.listeners.push(t),this.listeners.length-1}reset(){this.listeners=[],this.previousSize=this.types.size}hasPendingTypes(){return this.types.size>this.previousSize}}class c{constructor(){}reset(){this.paused=0,this.previous=this.tracked||new Map,this.tracked=new Map,this.positionStack=[0]}push(){this.positionStack.push(0)}pop(){this.positionStack.pop()}increment(){this.positionStack[this.positionStack.length-1]++}pause(){this.paused++}resume(){this.paused--}getPath(){return this.positionStack.join("-")}track(t){return 0===this.paused&&this.tracked.set(this.getPath(),t),t}findRecyclable(t){const e=this.previous.get(this.getPath());return e&&e.constructor===t&&!e.key?e:null}}const u=["value","checked","selected"];let d=class{constructor(t){this.getSelector=t.getSelector,this.getAttributes=t.getAttributes,this.previousAttributes={}}hydrate(t){this.ref=t.querySelector(this.getSelector())}update(){const t=this.getAttributes(),{remove:e,add:s}=function(t,e={}){const s={},i=[];return Object.keys(t).forEach((i=>{let n=t[i];n!==e[i]&&(!0===n?s[i]="":!1!==n&&(null==n&&(n=""),s[i]=n))})),Object.keys(e).forEach((s=>{(!(s in t)||e[s]!==t[s]&&!1===t[s])&&i.push(s)})),{add:s,remove:i}}(t,this.previousAttributes);this.previousAttributes=t,e.forEach((t=>{this.ref.removeAttribute(t),u.includes(t)&&t in this.ref&&(this.ref[t]="value"===t&&"")})),Object.keys(s).forEach((t=>{const e=s[t];this.ref.setAttribute(t,e),u.includes(t)&&t in this.ref&&(this.ref[t]="value"===t?e:!1!==e&&"false"!==e)}))}};const p=["value","checked","selected"];function f(t,e){t.nodeType===e.nodeType?t.nodeType!==Node.TEXT_NODE?t.tagName===e.tagName?(((t,e)=>{const s=e.attributes,i=t.attributes,n=new Set;for(let e=0,i=s.length;e<i;e++){const{name:i,value:r}=s[e];n.add(i),t.getAttribute(i)!==r&&t.setAttribute(i,r)}for(let e=i.length-1;e>=0;e--){const{name:s}=i[e];n.has(s)||t.removeAttribute(s)}for(let s=0,i=p.length;s<i;s++){const i=p[s];i in t&&t[i]!==e[i]&&(t[i]=e[i])}})(t,e),((t,e)=>{const s=t.childNodes,i=e.childNodes,n=s.length;if(n!==i.length)return!1;for(let t=0;t<n;t++)if(!s[t].isEqualNode(i[t]))return!1;return!0})(t,e)||((t,e)=>{const s=Array.from(e.childNodes);t.replaceChildren(...s)})(t,e)):t.replaceWith(e):t.nodeValue!==e.nodeValue&&(t.nodeValue=e.nodeValue):t.replaceWith(e)}function g(t,e,s=(()=>!1),i){let n=i||t.firstChild;for(;n;){if(n.nodeType===Node.COMMENT_NODE&&n.data.trim()===e)return n;if(n.nodeType!==Node.ELEMENT_NODE||s(n)||!n.firstChild){for(;n&&!n.nextSibling;)if(n=n.parentNode,!n||n===t)return null;n&&(n=n.nextSibling)}else n=n.firstChild}return null}class E{constructor(t){this.getStart=t.getStart,this.getEnd=t.getEnd,this.expression=t.expression,this.shouldSkipFind=t.shouldSkipFind,this.shouldSkipSync=t.shouldSkipSync,this.previousItems=[]}hydrate(t){const e=g(t,this.getStart(),this.shouldSkipFind),s=g(t,this.getEnd(),this.shouldSkipFind,e);this.ref=[e,s]}update(t,e){let s;const[i,n]=this.ref,r=i.nextSibling,h=r===n,o=!h&&r.nextSibling===n;if(h?n.before(t):!o||1!==t.children.length||this.shouldSkipSync(r)||this.shouldSkipSync(t.firstChild)?(s=document.createComment(""),n.before(s),n.before(t)):f(r,t.firstChild),e(),s){if(this.ref[0].nextSibling===s)s.remove();else{const t=document.createRange();t.setStartAfter(this.ref[0]),t.setEndAfter(s),t.deleteContents()}}}updateElement(t,e,s){const i=document.createComment("");t.after(i),i.after(e.firstChild),s(),t.remove(),i.remove()}}const m=t=>t.reduce(((t,e)=>(Array.isArray(e)?t.push(...m(e)):t.push(e),t)),[]);function y(t){const e=document.createElement("template");return e.innerHTML=`${t}`.trim(),e.content}function T(t){const e=[];return Object.keys(t).forEach((s=>{let i=t[s];!0===i?e.push(s):!1!==i&&(null==i&&(i=""),e.push(`${s}="${i}"`))})),e.join(" ")}const b=(t,e)=>i(t,e,e),v=t=>!!(t&&t.dataset&&t.dataset[N.DATASET_ELEMENT]&&t.dataset[N.DATASET_ELEMENT].endsWith("-1")),A=t=>!!(t&&t.dataset&&t.dataset[N.DATASET_ELEMENT])||!!t.querySelector(`[${N.ATTRIBUTE_ELEMENT}]`),S=(t,e)=>t.reduce(((t,s,i)=>(t.push(s),void 0!==e[i]&&t.push(N.PLACEHOLDER(i)),t)),[]).join(""),R=(t,e)=>{const s=N.PLACEHOLDER("(\\d+)"),i=t.match(new RegExp(`^${s}$`));if(i)return[e[parseInt(i[1],10)]];const n=new RegExp(`${s}`,"g"),r=[];let h,o=0;for(;null!==(h=n.exec(t));){const s=t.slice(o,h.index);r.push(N.markAsSafeHTML(s),e[parseInt(h[1],10)]),o=h.index+h[0].length}return r.push(N.markAsSafeHTML(t.slice(o))),r},L=(t,e)=>t.reduce(((t,s)=>{const i=e(s[0]);if(1===s.length)"object"==typeof i?t=Object.assign(t,i):"string"==typeof i&&(t[i]=!0);else{const n=s[2]?e(s[1]):s[1];t[i]=n}return t}),{}),$=(t,e,s)=>{const i={};return Object.keys(t).forEach((n=>{const r=n.match(/on(([A-Z]{1}[a-z]+)+)/);if(r&&r[1]){const h=r[1].toLowerCase(),o=t[n];if(o){const t=e.addListener(o,h);i[s(h)]=t}}else i[n]=t[n]})),i},C=(t,e,s=!1)=>{const i=N.PLACEHOLDER("(\\d+)"),n=new Map;return s||(t=t.replace(new RegExp(i,"g"),((t,s)=>{const i=e[s];if(i&&i.prototype instanceof N){if(n.has(i))return n.get(i);n.set(i,t)}return t}))),t.replace(new RegExp(`<(${i})([^>]*)>([\\s\\S]*?)</\\1>|<(${i})([^>]*)/>`,"g"),((t,s,i,n,r,h,o,l)=>{let c,u,d;if(s?(c=e[i],u=n):(c=void 0!==o?e[o]:h,u=l),!(c.prototype instanceof N))return t;if(s){const t=C(r,e,!0),s=O(t,e);d=R(s,e)}const p=_(u,e);return e.push((function(){const t=L(p,(t=>b(t,this)));return d&&(t.renderChildren=()=>new a(d.map((t=>b(t,this))))),c.mount(t)})),N.PLACEHOLDER(e.length-1)}))},M=(t,e)=>{const s=N.PLACEHOLDER("(?:\\d+)");return t.replace(new RegExp(`<(${s}|[a-z]+[1-6]?)(?:\\s*)((?:"[^"]*"|'[^']*'|[^>])*)(/?>)`,"gi"),e)},k=(t,e,s)=>{const n=N.PLACEHOLDER("(?:\\d+)");if(t.match(new RegExp(`^\\s*${n}\\s*$`)))return t;if(!t.match(new RegExp(`^\\s*<([a-z]+[1-6]?|${n})([^>]*)>([\\s\\S]*?)</(\\1|${n})>\\s*$|^\\s*<([a-z]+[1-6]?|${n})([^>]*)/>\\s*$`)))throw new SyntaxError(`Template must have a single root element or be a container component: "${t.trim()}"`);let r=0;return M(t,((t,h,o,a)=>{const l=0===r,c=++r;if(!l&&!o.match(new RegExp(n)))return t;const u=_(o,e),d=t=>`${t}-${c}`,p=s.length;s.push({getSelector:function(){return`[${N.ATTRIBUTE_ELEMENT}="${d(this.uid)}"]`},getAttributes:function(){const t=$(L(u,(t=>b(t,this))),this.eventsManager,(t=>N.ATTRIBUTE_EVENT(t,this.uid)));return l&&this.attributes&&Object.assign(t,i(this.attributes,this)),t[N.ATTRIBUTE_ELEMENT]=d(this.uid),t}}),e.push((function(){const t=this.template.elements[p],e=t.getAttributes.call(this);return t.previousAttributes=e,N.markAsSafeHTML(T(e))}));return`<${h} ${N.PLACEHOLDER(e.length-1)}${a}`}))},O=(t,e)=>{const s=N.PLACEHOLDER("(?:\\d+)");return M(t,((t,i,n,r)=>{if(!n.match(new RegExp(s)))return t;const h=_(n,e),o=function(){return $(L(h,(t=>b(t,this))),this.eventsManager,(t=>N.ATTRIBUTE_EVENT(t,this.uid)))};e.push((function(){const t=o.call(this);return N.markAsSafeHTML(T(t))}));return`<${i} ${N.PLACEHOLDER(e.length-1)}${r}`}))},x=(t,e,s)=>{const i=N.PLACEHOLDER("(\\d+)");let n=0;return t.replace(new RegExp(i,"g"),(function(i,r,h){const o=t.substring(0,h);if(o.lastIndexOf("<")>o.lastIndexOf(">"))return i;const a=++n;const l=s.length;return s.push({getStart:function(){return N.MARKER_START(`${this.uid}-${a}`)},getEnd:function(){return N.MARKER_END(`${this.uid}-${a}`)},expression:e[r]}),e.push((function(){return this.template.interpolations[l]})),N.PLACEHOLDER(e.length-1)}))},_=(t,e)=>{const s=N.PLACEHOLDER("(\\d+)"),i=[],n=new RegExp(`(${s}|[\\w-]+)(?:=(["']?)(?:${s}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s*/?[>"']))+.))\\3)?`,"g");let r;for(;null!==(r=n.exec(t));){const[,t,s,n,h,o]=r,a=void 0!==s?e[parseInt(s,10)]:t,l=void 0!==h?e[parseInt(h,10)]:o;void 0!==l?i.push([a,l,!!n]):i.push([a])}return i},w=["key","state","onCreate","onChange","onHydrate","onRecycle","onUpdate"];class N extends h{constructor(t={}){super(...arguments),this.componentOptions=[],w.forEach((e=>{e in t&&(this[e]=t[e],this.componentOptions.push(e))}));const e={};Object.keys(t).forEach((s=>{this.viewOptions.includes(s)||this.componentOptions.includes(s)||(e[s]=t[s])})),this.props=new n(e),this.options=t,this.partial=this.partial.bind(this),this.onChange=this.onChange.bind(this),this.onCreate.apply(this,arguments)}events(){const t={};return this.eventsManager.types.forEach((s=>{const i=N.ATTRIBUTE_EVENT(s,this.uid),n=function(t,s,n){const r=n.getAttribute(i);if(r){let i=this.eventsManager.listeners[parseInt(r,10)];"string"==typeof i&&(i=this[i]),e(i),i.call(this,t,s,n)}};t[`${s} [${i}]`]=n,t[s]=n})),t}ensureElement(){this.eventsManager=new l,this.pathManager=new c,this.template=i(this.template,this),this.el&&(this.el=i(this.el,this),this.toString(),this.hydrate(this.el.parentNode))}isContainer(){return 0===this.template.elements.length&&1===this.template.interpolations.length}subscribe(t,e="change",s=this.onChange){return t.on&&this.listenTo(t,e,s),this}hydrate(t){return["model","state","props"].forEach((t=>{this[t]&&this.subscribe(this[t])})),this.isContainer()?(this.children[0].hydrate(t),this.el=this.children[0].el):(this.template.elements.forEach(((e,s)=>{0===s?(e.hydrate(t),this.el?e.ref=this.el:this.el=e.ref):e.hydrate(this.el)})),this.delegateEvents(),this.template.interpolations.forEach((t=>t.hydrate(this.el))),this.children.forEach((t=>t.hydrate(this.el)))),this.onHydrate.call(this),this}recycle(t,e){if(t){!function(t,e){if(Element.prototype.moveBefore)t.parentNode.moveBefore(e,t),t.remove();else{const s=document.activeElement;t.before(e),t.remove(),s&&s!==document.activeElement&&e.contains(s)&&s.focus()}}(g(t,N.MARKER_RECYCLED(this.uid),v),this.el)}return e&&this.props.set(e),this.onRecycle.call(this),this}getRecycledMarker(){return`\x3c!--${N.MARKER_RECYCLED(this.uid)}--\x3e`}partial(t,...e){const s=R(O(C(S(t,e).trim(),e),e),e).map((t=>b(t,this)));return new a(s)}renderTemplatePart(t,e,s=[]){const i=b(t,this),n=t=>{if(null==t||!1===t||!0===t)return"";if(t instanceof o)return s.push(t),t;if(t instanceof N)return s.push(t),e(t);if(t instanceof a){if(1===t.items.length)return n(t.items[0]);this.pathManager.push();const e=t.items.map((t=>(this.pathManager.increment(),n(t)))).join("");return this.pathManager.pop(),e}if(Array.isArray(t)){this.pathManager.pause();const e=m(t).map(n).join("");return this.pathManager.resume(),e}if(t instanceof E){this.pathManager.increment();const e=this.isContainer()?"":`\x3c!--${t.getStart()}--\x3e`,i=this.isContainer()?"":`\x3c!--${t.getEnd()}--\x3e`,r=n(b(t.expression,this));return t.previousItems=s,`${e}${r}${i}`}const i=N.sanitize(t);return s.push(i),i};return`${n(i)}`}toString(){this.destroyChildren(),this.eventsManager.reset(),this.pathManager.reset();const t=t=>(this.pathManager.track(t),this.addChild(t));return this.template.parts.map((e=>this.renderTemplatePart(e,t))).join("")}render(){if(this.destroyed)return this;if(!this.el){const t=y(this);return this.hydrate(t),this}this.eventsManager.reset(),this.pathManager.reset(),this.template.elements.forEach((t=>t.update()));const t=this.children;return this.children=[],this.template.interpolations.forEach((e=>{const s=[],i=[];this.pathManager.increment();const n=e.previousItems,r=[],h=this.renderTemplatePart(e.expression,(e=>{let n=e,r=null;return r=e.key?t.find((t=>t.key===e.key)):this.pathManager.findRecyclable(e.constructor),r?(n=r.getRecycledMarker(),i.push([r,e]),r.key||this.pathManager.track(r)):(s.push(e),this.pathManager.track(e)),n}),r);e.previousItems=r;const o=([t,e],s)=>{this.addChild(t).recycle(s,e.props.toJSON()),e.destroy()};if(1===n.length&&n[0]instanceof N&&1===r.length&&r[0]instanceof N&&1===i.length)return void o(i[0],null);const a=y(h),l=()=>{i.forEach((t=>o(t,this.el))),s.forEach((t=>this.addChild(t).hydrate(this.el)))};this.isContainer()?e.updateElement(this.el,a,l):e.update(a,l)})),t.forEach((t=>{this.children.indexOf(t)<0&&t.destroy()})),this.isContainer()?this.el=this.children[0].el:this.eventsManager.hasPendingTypes()&&this.delegateEvents(),this.onUpdate.call(this),this}onCreate(){}onChange(){this.render()}onHydrate(){}onRecycle(){}onUpdate(){}onDestroy(){}static markAsSafeHTML(t){return new o(t)}static extend(t){const e=this;class s extends e{}return Object.assign(s.prototype,"function"==typeof t?t(e.prototype):t),s}static mount(t,e,s){const i=new this(t);return e&&(s?(i.toString(),i.hydrate(e)):e.append(i.render().el)),i}static create(t,...e){"function"==typeof t&&(e=[t],t=["",""]);const s=[],i=[],n=R(x(k(C(S(t,e).trim(),e),e,s),e,i),e);return this.extend({template(){return{elements:s.map((t=>new d({getSelector:t.getSelector.bind(this),getAttributes:t.getAttributes.bind(this)}))),interpolations:i.map((t=>new E({getStart:t.getStart.bind(this),getEnd:t.getEnd.bind(this),expression:t.expression,shouldSkipFind:v,shouldSkipSync:A}))),parts:n}}})}}N.ATTRIBUTE_ELEMENT="data-rasti-el",N.ATTRIBUTE_EVENT=(t,e)=>`data-rasti-on-${t}-${e}`,N.DATASET_ELEMENT="rastiEl",N.PLACEHOLDER=t=>`__RASTI_PH_${t}__`,N.MARKER_RECYCLED=t=>`rasti-r-${t}`,N.MARKER_START=t=>`rasti-s-${t}`,N.MARKER_END=t=>`rasti-e-${t}`;var j=N.create`<div></div>`;t.Component=j,t.Emitter=s,t.Model=n,t.View=h}));
package/es/Component.js CHANGED
@@ -13,6 +13,7 @@ import deepFlat from './utils/deepFlat.js';
13
13
  import parseHTML from './utils/parseHTML.js';
14
14
  import findComment from './utils/findComment.js';
15
15
  import getAttributesHTML from './utils/getAttributesHTML.js';
16
+ import replaceNode from './utils/replaceNode.js';
16
17
  import './utils/getAttributesDiff.js';
17
18
  import './utils/syncNode.js';
18
19
 
@@ -36,13 +37,13 @@ const getExpressionResult = (expression, context) => getResult(expression, conte
36
37
  const isComponent = (el) => !!(el && el.dataset && el.dataset[Component.DATASET_ELEMENT] && el.dataset[Component.DATASET_ELEMENT].endsWith('-1'));
37
38
 
38
39
  /**
39
- * Check if an element has the Rasti data attribute.
40
- * This includes both component root elements and regular tracked elements.
40
+ * Check if an element contains a component.
41
+ * It checks if the element is a component root element or if it contains a component.
41
42
  * @param {Element} el The element to check.
42
- * @return {boolean} True if the element has the data attribute.
43
+ * @return {boolean} True if the element contains a component.
43
44
  * @private
44
45
  */
45
- const isElement = (el) => !!(el && el.dataset && el.dataset[Component.DATASET_ELEMENT]);
46
+ const containsElement = (el) => !!(el && el.dataset && el.dataset[Component.DATASET_ELEMENT]) || !!el.querySelector(`[${Component.ATTRIBUTE_ELEMENT}]`);
46
47
 
47
48
  /**
48
49
  * Generate string with placeholders for interpolated expressions.
@@ -613,9 +614,8 @@ class Component extends View {
613
614
  recycle(parent, props) {
614
615
  if (parent) {
615
616
  // Locate the placeholder comment and replace it with the real nodes
616
- const toBeReplaced = findComment(parent, Component.MARKER_RECYCLED(this.uid), isComponent);
617
- // Replace it with this.el.
618
- toBeReplaced.replaceWith(this.el);
617
+ const placeholder = findComment(parent, Component.MARKER_RECYCLED(this.uid), isComponent);
618
+ replaceNode(placeholder, this.el);
619
619
  }
620
620
  // Update props.
621
621
  if (props) {
@@ -691,8 +691,8 @@ class Component extends View {
691
691
  /**
692
692
  * Render a template part.
693
693
  * @param {any} part - The template part.
694
- * @param {function} addChild - The addChild function.
695
- * @param {Array} items - The items array.
694
+ * @param {function} addChild - The addChild function. It should handle children and return a HTML string.
695
+ * @param {Array} items - The items array. It is used to store the items that are parsed.
696
696
  * @return {string} The rendered template part.
697
697
  * @private
698
698
  */
@@ -754,6 +754,24 @@ class Component extends View {
754
754
  * Used internally on the render process.
755
755
  * Use it for server-side rendering or static site generation.
756
756
  * @return {string} The rendered component.
757
+ * @example
758
+ * import { Component } from 'rasti';
759
+ * const Button = Component.create`
760
+ * <button class="button">Click me</button>
761
+ * `;
762
+ * const App = Component.create`
763
+ * <div>
764
+ * <${Button}>Click me</${Button}>
765
+ * </div>
766
+ * `;
767
+ *
768
+ * const app = new App();
769
+ *
770
+ * console.log(app.toString());
771
+ * // <div data-rasti-el="r1-1"><!--rasti-s-r1-1--><button class="button" data-rasti-el="r2-1">Click me</button><!--rasti-e-r1-1--></div>
772
+ *
773
+ * console.log(`${app}`);
774
+ * // <div data-rasti-el="r1-1"><!--rasti-s-r1-1--><button class="button" data-rasti-el="r2-1">Click me</button><!--rasti-e-r1-1--></div>
757
775
  */
758
776
  toString() {
759
777
  // Normally there won't be any children, but if there are, destroy them.
@@ -797,8 +815,6 @@ class Component extends View {
797
815
  this.eventsManager.reset();
798
816
  // Reset position tracking.
799
817
  this.pathManager.reset();
800
- // Store active element.
801
- const activeElement = this.isContainer() ? null : document.activeElement;
802
818
  // Update elements.
803
819
  this.template.elements.forEach(element => element.update());
804
820
  // Store previous children.
@@ -811,7 +827,9 @@ class Component extends View {
811
827
  const recycledChildren = [];
812
828
 
813
829
  this.pathManager.increment();
814
-
830
+ // `addChild` handler is called from `renderTemplatePart` for every component.
831
+ // It should handle children and return a HTML string.
832
+ // In this case, where the component updates, it handles children recycling.
815
833
  const addChild = component => {
816
834
  let out = component;
817
835
  let found = null;
@@ -860,13 +878,19 @@ class Component extends View {
860
878
  }
861
879
  // Parse the rendered content and get the fragment.
862
880
  const fragment = parseHTML(rendered);
863
- // Add recycled components to children and move them to the new template in the fragment.
864
- recycledChildren.forEach(recycled => recycle(recycled, fragment));
865
- // Add new children. Hydrate them and update the interpolation.
866
- nextChildren.forEach(child => this.addChild(child).hydrate(fragment));
867
- // Update the interpolation.
868
- const targetElement = this.isContainer() ? this.el : null;
869
- interpolation.update(fragment, targetElement);
881
+
882
+ const handleComponents = () => {
883
+ // Add recycled components to children and move them to the new template in the fragment.
884
+ recycledChildren.forEach(recycled => recycle(recycled, this.el));
885
+ // Add new children. Hydrate them and update the interpolation.
886
+ nextChildren.forEach(child => this.addChild(child).hydrate(this.el));
887
+ };
888
+
889
+ if (this.isContainer()) {
890
+ interpolation.updateElement(this.el, fragment, handleComponents);
891
+ } else {
892
+ interpolation.update(fragment, handleComponents);
893
+ }
870
894
  });
871
895
  // Destroy unused children.
872
896
  previousChildren.forEach(prev => {
@@ -881,10 +905,6 @@ class Component extends View {
881
905
  this.delegateEvents();
882
906
  }
883
907
  }
884
- // Restore focus.
885
- if (activeElement && this.el.contains(activeElement)) {
886
- activeElement.focus();
887
- }
888
908
  // Call onUpdate lifecycle method.
889
909
  this.onUpdate.call(this);
890
910
  // Return this for chaining.
@@ -1169,8 +1189,8 @@ class Component extends View {
1169
1189
  getStart : interpolation.getStart.bind(this),
1170
1190
  getEnd : interpolation.getEnd.bind(this),
1171
1191
  expression : interpolation.expression,
1172
- isComponent,
1173
- isElement
1192
+ shouldSkipFind : isComponent,
1193
+ shouldSkipSync : containsElement
1174
1194
  })),
1175
1195
  parts,
1176
1196
  };
@@ -1193,14 +1213,14 @@ Component.DATASET_ELEMENT = 'rastiEl';
1193
1213
  /*
1194
1214
  * Placeholders used to temporarily replace expressions in the template.
1195
1215
  */
1196
- Component.PLACEHOLDER = (idx) => `__RASTI-${idx}__`;
1216
+ Component.PLACEHOLDER = (idx) => `__RASTI_PH_${idx}__`;
1197
1217
 
1198
1218
  /*
1199
1219
  * Markers used to identify interpolation and recycled components.
1200
1220
  */
1201
- Component.MARKER_RECYCLED = (uid) => `rasti-recycled-${uid}`;
1202
- Component.MARKER_START = (uid) => `rasti-start-${uid}`;
1203
- Component.MARKER_END = (uid) => `rasti-end-${uid}`;
1221
+ Component.MARKER_RECYCLED = (uid) => `rasti-r-${uid}`;
1222
+ Component.MARKER_START = (uid) => `rasti-s-${uid}`;
1223
+ Component.MARKER_END = (uid) => `rasti-e-${uid}`;
1204
1224
 
1205
1225
  /**
1206
1226
  * Components are a special kind of `View` that is designed to be easily composable,