+21
| The MIT License (MIT) | ||
| Copyright (c) 2018-present Alberto Masuelli | ||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. |
@@ -1,2 +0,2 @@ | ||
| !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("Event 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=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[t])))}static resetUid(){h.uid=0}}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}}const c=["value","checked","selected"];let u=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),-1!==c.indexOf(t)&&t in this.ref&&(this.ref[t]="value"===t&&"")})),Object.keys(s).forEach((t=>{const e=s[t];this.ref.setAttribute(t,e),-1!==c.indexOf(t)&&t in this.ref&&(this.ref[t]="value"===t?e:!1!==e&&"false"!==e)}))}};class d{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}hasSingleComponent(){if(1!==this.tracked.size||1!==this.previous.size)return!1;const[t,e]=this.tracked.entries().next().value,[s,i]=this.previous.entries().next().value;return"0"===t&&"0"===s&&e===i}findRecyclable(t){const e=this.previous.get(this.getPath());return e&&!e.key&&e.constructor===t.constructor?e:null}}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 E(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 g{constructor(t){this.getStart=t.getStart,this.getEnd=t.getEnd,this.expression=t.expression,this.shouldSkipFind=t.shouldSkipFind,this.shouldSkipSync=t.shouldSkipSync,this.tracker=new d}hydrate(t){const e=E(t,this.getStart(),this.shouldSkipFind),s=E(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.parentNode.insertBefore(t,n):!o||1!==t.children.length||this.shouldSkipSync(r)||this.shouldSkipSync(t.firstChild)?(s=document.createComment(""),n.parentNode.insertBefore(s,n),n.parentNode.insertBefore(t,n)):f(r,t.firstChild),e(),s){if(this.ref[0].nextSibling===s)s.parentNode.removeChild(s);else{const t=document.createRange();t.setStartAfter(this.ref[0]),t.setEndAfter(s),t.deleteContents()}}}updateElement(t,e,s){const i=document.createComment("");t.parentNode.insertBefore(i,t.nextSibling),i.parentNode.insertBefore(e.firstChild,i.nextSibling),s(),t.nextSibling===i&&t.parentNode.removeChild(t),i.parentNode.removeChild(i)}}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,s)=>{try{return i(t,e,e)}catch(t){if(s&&!t.cause){let s;s=`Error in ${e.constructor.name}#${e.uid} expression`;const i=new Error(s,{cause:t});throw i.stack=t.stack,i}throw t}},v=t=>!!(t&&t.dataset&&t.dataset[_.DATASET_ELEMENT]&&t.dataset[_.DATASET_ELEMENT].endsWith("-1")),S=t=>!!(t&&t.dataset&&t.dataset[_.DATASET_ELEMENT])||!!t.querySelector(`[${_.ATTRIBUTE_ELEMENT}]`),A=(t,e)=>t.reduce(((t,s,i)=>(t.push(s),void 0!==e[i]&&t.push(_.PLACEHOLDER(i)),t)),[]).join(""),C=(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},R=(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}),{}),L=(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},$=(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,d;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=$(r,e,!0),s=O(t,e);d=C(s,e)}const p=w(u,e);return e.push((function(){const t=R(p,(t=>b(t,this,"children options")));return d&&(t.renderChildren=()=>new a(d.map((t=>b(t,this))))),c.mount(t)})),_.PLACEHOLDER(e.length-1)}))},k=(t,e)=>{const s=_.PLACEHOLDER("(?:\\d+)");return t.replace(new RegExp(`<(${s}|[a-z]+[1-6]?)(?:\\s*)((?:"[^"]*"|'[^']*'|[^>])*)(/?>)`,"gi"),e)},x=(t,e,s)=>{const n=_.PLACEHOLDER("(?:\\d+)");if(t.match(new RegExp(`^\\s*${n}\\s*$`)))return t;const r=t.match(new RegExp(`^\\s*<([a-z]+[1-6]?|${n})([^>]*)>([\\s\\S]*?)</(\\1|${n})>\\s*$|^\\s*<([a-z]+[1-6]?|${n})([^>]*)/>\\s*$`));if(!r){throw new Error("Invalid component template")}let h=0;return k(r[0],((t,r,o,a)=>{const l=0===h,c=++h;if(!l&&!o.match(new RegExp(n)))return t;const u=w(o,e),d=t=>`${t}-${c}`,p=s.length;s.push({getSelector:function(){return`[${_.ATTRIBUTE_ELEMENT}="${d(this.uid)}"]`},getAttributes:function(){const t=L(R(u,(t=>b(t,this,"element attribute"))),this.eventsManager,(t=>_.ATTRIBUTE_EVENT(t,this.uid)));return l&&this.attributes&&Object.assign(t,i(this.attributes,this)),t[_.ATTRIBUTE_ELEMENT]=d(this.uid),t}}),e.push((function(){const t=this.template.elements[p],e=t.getAttributes.call(this);return t.previousAttributes=e,_.markAsSafeHTML(T(e))}));return`<${r} ${_.PLACEHOLDER(e.length-1)}${a}`}))},O=(t,e)=>{const s=_.PLACEHOLDER("(?:\\d+)");return k(t,((t,i,n,r)=>{if(!n.match(new RegExp(s)))return t;const h=w(n,e),o=function(){return L(R(h,(t=>b(t,this,"partial element attribute"))),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}`}))},N=(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=!!n;let l=void 0!==t?e[parseInt(t,10)]:s,c=void 0!==h?e[parseInt(h,10)]:o;a&&void 0===c&&(c=""),void 0!==c?i.push([l,c,a]):i.push([l])}return i},M=["key","state","onCreate","onChange","onHydrate","onRecycle","onUpdate"];class _ extends h{constructor(t={}){super(...arguments),this.componentOptions=[],M.forEach((e=>{e in t&&(this[e]=t[e],this.componentOptions.push(e))}));const e={};Object.keys(t).forEach((s=>{-1===this.viewOptions.indexOf(s)&&-1===this.componentOptions.indexOf(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(){if(this.eventsManager=new l,this.template=i(this.template,this),this.el){if(this.el=i(this.el,this),!this.el.parentNode){const t=`Hydration failed in ${this.constructor.name}#${this.uid}`;throw new Error(t)}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):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.parentNode.removeChild(t);else{const s=document.activeElement;t.parentNode.insertBefore(e,t),t.parentNode.removeChild(t),s&&s!==document.activeElement&&e.contains(s)&&s.focus()}}(E(t,_.MARKER_RECYCLED(this.uid),v),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=C(O($(A(t,e).trim(),e),e),e).map((t=>b(t,this,"partial")));return new a(s)}renderTemplatePart(t,e,s){const i=b(t,this,"template part");if(null==i||!1===i||!0===i)return"";if(i instanceof o)return`${i}`;if(i instanceof _)return`${e(i,s)}`;if(i instanceof a){if(1===i.items.length)return this.renderTemplatePart(i.items[0],e,s);s.push();const t=i.items.map((t=>(s.increment(),this.renderTemplatePart(t,e,s)))).join("");return s.pop(),t}if(Array.isArray(i)){s.pause();const t=m(i).map((t=>this.renderTemplatePart(t,e,s))).join("");return s.resume(),t}if(i instanceof g){const t=i.tracker;t.reset();const s=this.isContainer()?"":`\x3c!--${i.getStart()}--\x3e`,n=this.isContainer()?"":`\x3c!--${i.getEnd()}--\x3e`;return`${s}${this.renderTemplatePart(i.expression,e,t)}${n}`}return`${_.sanitize(i)}`}toString(){this.destroyChildren(),this.eventsManager.reset();const t=(t,e)=>(e.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.template.elements.forEach((t=>t.update()));const t=this.children;return this.children=[],this.template.interpolations.forEach((e=>{const s=e.tracker;s.reset();const i=[],n=[],r=this.renderTemplatePart(e.expression,(e=>{let r=e,h=null;return h=e.key?t.find((t=>t.key===e.key)):s.findRecyclable(e),h?(r=h.getRecycledMarker(),n.push([h,e]),s.track(h)):(i.push(e),s.track(e)),r}),s),h=([t,e],s)=>{this.addChild(t).recycle(s,e.props.toJSON()),e.destroy()};if(s.hasSingleComponent())return void h(n[0],null);const o=y(r),a=t=>()=>{n.forEach((e=>h(e,t))),i.forEach((e=>this.addChild(e).hydrate(t)))};this.isContainer()?e.updateElement(this.el,o,a(this.el.parentNode)):e.update(o,a(this.el))})),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():e.append(y(i)),i.hydrate(e)),i}static create(t,...e){"function"==typeof t&&(e=[t],t=["",""]);const s=[],i=[],n=C(N(x($(A(t,e).trim(),e),e,s),e,i),e);return this.extend({source:null,template(){return{elements:s.map((t=>new u({getSelector:t.getSelector.bind(this),getAttributes:t.getAttributes.bind(this)}))),interpolations:i.map((t=>new g({getStart:t.getStart.bind(this),getEnd:t.getEnd.bind(this),expression:t.expression,shouldSkipFind:v,shouldSkipSync:S}))),parts:n}}})}}_.ATTRIBUTE_ELEMENT="data-rst-el",_.ATTRIBUTE_EVENT=(t,e)=>`data-rst-on-${t}-${e}`,_.DATASET_ELEMENT="rstEl",_.PLACEHOLDER=t=>`__RASTI_PLACEHOLDER_${t}__`,_.MARKER_RECYCLED=t=>`rst-r-${t}`,_.MARKER_START=t=>`rst-s-${t}`,_.MARKER_END=t=>`rst-e-${t}`;var j=_.create`<div></div>`;t.Component=j,t.Emitter=s,t.Model=n,t.View=h})); | ||
| !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("Event 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=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[t])))}static resetUid(){h.uid=0}}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}}const c=["value","checked","selected"];let u=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),-1!==c.indexOf(t)&&t in this.ref&&(this.ref[t]="value"===t&&"")})),Object.keys(s).forEach((t=>{const e=s[t];this.ref.setAttribute(t,e),-1!==c.indexOf(t)&&t in this.ref&&(this.ref[t]="value"===t?e:!1!==e&&"false"!==e)}))}};class d{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}hasSingleComponent(){if(1!==this.tracked.size||1!==this.previous.size)return!1;const[t,e]=this.tracked.entries().next().value,[s,i]=this.previous.entries().next().value;return"0"===t&&"0"===s&&e===i}findRecyclable(t){const e=this.previous.get(this.getPath());return e&&!e.key&&e.constructor===t.constructor?e:null}}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 E(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 g{constructor(t){this.getStart=t.getStart,this.getEnd=t.getEnd,this.expression=t.expression,this.shouldSkipFind=t.shouldSkipFind,this.shouldSkipSync=t.shouldSkipSync,this.tracker=new d}hydrate(t){const e=E(t,this.getStart(),this.shouldSkipFind),s=E(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.parentNode.insertBefore(t,n):!o||1!==t.children.length||this.shouldSkipSync(r)||this.shouldSkipSync(t.firstChild)?(s=document.createComment(""),n.parentNode.insertBefore(s,n),n.parentNode.insertBefore(t,n)):f(r,t.firstChild),e(),s){if(this.ref[0].nextSibling===s)s.parentNode.removeChild(s);else{const t=document.createRange();t.setStartAfter(this.ref[0]),t.setEndAfter(s),t.deleteContents()}}}updateElement(t,e,s){const i=document.createComment("");t.parentNode.insertBefore(i,t.nextSibling),i.parentNode.insertBefore(e.firstChild,i.nextSibling),s(),t.nextSibling===i&&t.parentNode.removeChild(t),i.parentNode.removeChild(i)}}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(" ")}let b,v,A,S;"undefined"!=typeof document&&(b=!!navigator.userAgent.match(/Chrome/),v=!!Element.prototype.moveBefore,A=!v||b,S=v&&b);const R=(t,e,s)=>{try{return"function"!=typeof t?t:t.call(e,e)}catch(t){if(s&&!t._rasti){let s;s=`Error in ${e.constructor.name}#${e.uid} expression`;const i=new Error(s,{cause:t});throw i._rasti=!0,i}throw t}},C=t=>!!(t&&t.dataset&&t.dataset[H.DATASET_ELEMENT]&&t.dataset[H.DATASET_ELEMENT].endsWith("-1")),L=t=>!!(t&&t.dataset&&t.dataset[H.DATASET_ELEMENT])||!!t.querySelector(`[${H.ATTRIBUTE_ELEMENT}]`),$=(t,e)=>t.reduce(((t,s,i)=>(t.push(s),void 0!==e[i]&&t.push(H.PLACEHOLDER(i)),t)),[]).join(""),x=(t,e)=>{const s=H.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(H.markAsSafeHTML(s),e[parseInt(h[1],10)]),o=h.index+h[0].length}return r.push(H.markAsSafeHTML(t.slice(o))),r},k=(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}),{}),O=(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},N=(t,e,s=!1)=>{const i=H.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 H){if(n.has(i))return n.get(i);n.set(i,t)}return t}))),t.replace(new RegExp(`<(${i})([^>]*)/>|<(${i})([^>]*)>([\\s\\S]*?)</\\4>`,"g"),((t,s,i,n,r,h,o,l)=>{let c,u,d;if(r?(c=e[h],u=o):(c=void 0!==i?e[i]:s,u=n),!(c.prototype instanceof H))return t;if(r){const t=N(l,e,!0),s=_(t,e);d=x(s,e)}const p=j(u,e);return e.push((function(){const t=k(p,(t=>R(t,this,"children options")));return d&&(t.renderChildren=()=>new a(d.map((t=>R(t,this,"children"))))),c.mount(t)})),H.PLACEHOLDER(e.length-1)}))},w=(t,e)=>{const s=H.PLACEHOLDER("(?:\\d+)");return t.replace(new RegExp(`<(${s}|[a-z]+[1-6]?)(?:\\s*)((?:"[^"]*"|'[^']*'|[^>])*)(/?>)`,"gi"),e)},M=(t,e,s)=>{const n=H.PLACEHOLDER("(?:\\d+)");if(t.match(new RegExp(`^\\s*${n}\\s*$`)))return t;const r=t.match(new RegExp(`^\\s*<([a-z]+[1-6]?|${n})([^>]*)>([\\s\\S]*?)</(\\1|${n})>\\s*$|^\\s*<([a-z]+[1-6]?|${n})([^>]*)/>\\s*$`));if(!r){throw new Error("Invalid component template")}let h=0;return w(r[0],((t,r,o,a)=>{const l=0===h,c=++h;if(!l&&!o.match(new RegExp(n)))return t;const u=j(o,e),d=t=>`${t}-${c}`,p=s.length;s.push({getSelector:function(){return`[${H.ATTRIBUTE_ELEMENT}="${d(this.uid)}"]`},getAttributes:function(){const t=O(k(u,(t=>R(t,this,"element attribute"))),this.eventsManager,(t=>H.ATTRIBUTE_EVENT(t,this.uid)));return l&&this.attributes&&Object.assign(t,i(this.attributes,this)),t[H.ATTRIBUTE_ELEMENT]=d(this.uid),t}}),e.push((function(){const t=this.template.elements[p],e=t.getAttributes.call(this);return t.previousAttributes=e,H.markAsSafeHTML(T(e))}));return`<${r} ${H.PLACEHOLDER(e.length-1)}${a}`}))},_=(t,e)=>{const s=H.PLACEHOLDER("(?:\\d+)");return w(t,((t,i,n,r)=>{if(!n.match(new RegExp(s)))return t;const h=j(n,e),o=function(){return O(k(h,(t=>R(t,this,"partial element attribute"))),this.eventsManager,(t=>H.ATTRIBUTE_EVENT(t,this.uid)))};e.push((function(){const t=o.call(this);return H.markAsSafeHTML(T(t))}));return`<${i} ${H.PLACEHOLDER(e.length-1)}${r}`}))},P=(t,e,s)=>{const i=H.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 H.MARKER_START(`${this.uid}-${a}`)},getEnd:function(){return H.MARKER_END(`${this.uid}-${a}`)},expression:e[r]}),e.push((function(){return this.template.interpolations[l]})),H.PLACEHOLDER(e.length-1)}))},j=(t,e)=>{const s=H.PLACEHOLDER("(\\d+)"),i=[],n=new RegExp(`(?:${s}|([\\w-]+))(?:=(["']?)(?:${s}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s*/>|\\s*[>"']))+.))?\\3)?`,"g");let r;for(;null!==(r=n.exec(t));){const[,t,s,n,h,o]=r,a=!!n;let l=void 0!==t?e[parseInt(t,10)]:s,c=void 0!==h?e[parseInt(h,10)]:o;a&&void 0===c&&(c=""),void 0!==c?i.push([l,c,a]):i.push([l])}return i},D=["key","state","onCreate","onChange","onHydrate","onBeforeRecycle","onRecycle","onBeforeUpdate","onUpdate"];class H extends h{constructor(t={}){super(...arguments),this.componentOptions=[],D.forEach((e=>{e in t&&(this[e]=t[e],this.componentOptions.push(e))}));const e={};Object.keys(t).forEach((s=>{-1===this.viewOptions.indexOf(s)&&-1===this.componentOptions.indexOf(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=H.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(){if(this.eventsManager=new l,this.template=i(this.template,this),this.el){if(this.el=i(this.el,this),!this.el.parentNode){const t=`Hydration failed in ${this.constructor.name}#${this.uid}`;throw new Error(t)}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):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){if(this.onBeforeRecycle.call(this),t){!function(t,e){const s=A&&document.activeElement&&e.contains(document.activeElement)?document.activeElement:null;s&&S&&s.blur(),t.parentNode[v?"moveBefore":"insertBefore"](e,t),t.parentNode.removeChild(t),s&&s!==document.activeElement&&e.contains(s)&&s.focus()}(E(t,H.MARKER_RECYCLED(this.uid),C),this.el)}return this}updateProps(t){return this.props.set(t),this.onRecycle.call(this),this}getRecycledMarker(){return`\x3c!--${H.MARKER_RECYCLED(this.uid)}--\x3e`}partial(t,...e){const s=x(_(N($(t,e).trim(),e),e),e).map((t=>R(t,this,"partial")));return new a(s)}renderTemplatePart(t,e,s){const i=R(t,this,"template part");if(null==i||!1===i||!0===i)return"";if(i instanceof o)return`${i}`;if(i instanceof H)return`${e(i,s)}`;if(i instanceof a){if(1===i.items.length)return this.renderTemplatePart(i.items[0],e,s);s.push();const t=i.items.map((t=>(s.increment(),this.renderTemplatePart(t,e,s)))).join("");return s.pop(),t}if(Array.isArray(i)){s.pause();const t=m(i).map((t=>this.renderTemplatePart(t,e,s))).join("");return s.resume(),t}if(i instanceof g){const t=i.tracker;t.reset();const s=this.isContainer()?"":`\x3c!--${i.getStart()}--\x3e`,n=this.isContainer()?"":`\x3c!--${i.getEnd()}--\x3e`;return`${s}${this.renderTemplatePart(i.expression,e,t)}${n}`}return`${H.sanitize(i)}`}toString(){this.destroyChildren(),this.eventsManager.reset();const t=(t,e)=>(e.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.onBeforeUpdate.call(this),this.eventsManager.reset();const t=this.children;this.children=[];const e=[];return this.template.interpolations.forEach((s=>{const i=s.tracker;i.reset();const n=[],r=[],h=this.renderTemplatePart(s.expression,(e=>{let s=e,h=null;return h=e.key?t.find((t=>t.key===e.key)):i.findRecyclable(e),h?(s=h.getRecycledMarker(),r.push([h,e]),i.track(h)):(n.push(e),i.track(e)),s}),i),o=([t,s],i)=>{e.push([t,s.props.toJSON()]),this.addChild(t).recycle(i),s.destroy()};if(i.hasSingleComponent())return void o(r[0],null);const a=y(h),l=t=>()=>{r.forEach((e=>o(e,t))),n.forEach((e=>this.addChild(e).hydrate(t)))};this.isContainer()?s.updateElement(this.el,a,l(this.el.parentNode)):s.update(a,l(this.el))})),t.forEach((t=>{this.children.indexOf(t)<0&&t.destroy()})),e.forEach((([t,e])=>{t.updateProps(e)})),this.isContainer()?this.el=this.children[0].el:(this.template.elements.forEach((t=>t.update())),this.eventsManager.hasPendingTypes()&&this.delegateEvents()),this.onUpdate.call(this),this}onCreate(){}onChange(){this.render()}onHydrate(){}onBeforeRecycle(){}onRecycle(){}onBeforeUpdate(){}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():e.append(y(i)),i.hydrate(e)),i}static create(t,...e){"function"==typeof t&&(e=[t],t=["",""]);const s=[],i=[],n=x(P(M(N($(t,e).trim(),e),e,s),e,i),e);return this.extend({source:null,template(){return{elements:s.map((t=>new u({getSelector:t.getSelector.bind(this),getAttributes:t.getAttributes.bind(this)}))),interpolations:i.map((t=>new g({getStart:t.getStart.bind(this),getEnd:t.getEnd.bind(this),expression:t.expression,shouldSkipFind:C,shouldSkipSync:L}))),parts:n}}})}}H.ATTRIBUTE_ELEMENT="data-rst-el",H.ATTRIBUTE_EVENT=(t,e)=>`data-rst-on-${t}-${e}`,H.DATASET_ELEMENT="rstEl",H.PLACEHOLDER=t=>`__RASTI_PLACEHOLDER_${t}__`,H.MARKER_RECYCLED=t=>`rst-r-${t}`,H.MARKER_START=t=>`rst-s-${t}`,H.MARKER_END=t=>`rst-e-${t}`;var B=H.create`<div></div>`;t.Component=B,t.Emitter=s,t.Model=n,t.View=h})); | ||
| //# sourceMappingURL=rasti.min.js.map |
+126
-38
@@ -38,5 +38,16 @@ import './Emitter.js'; | ||
| try { | ||
| return getResult(expression, context, context); | ||
| if (typeof expression !== 'function') return expression; | ||
| // In development, detect uninstantiated Component classes and provide a helpful error. | ||
| // This typically happens when a component tag is malformed and not properly expanded. | ||
| if (__DEV__ && expression.prototype instanceof Component) { | ||
| throw new Error( | ||
| `Received uninstantiated Component class "${expression.name || 'Anonymous'}". ` + | ||
| 'This usually happens when a component tag is malformed (e.g., missing closing tag or typo). ' + | ||
| 'If that\'s not the case, make sure to instantiate child components using a component tag, mount(), or new.' | ||
| ); | ||
| } | ||
| return expression.call(context, context); | ||
| } catch (error) { | ||
| if (meta && !error.cause) { | ||
| if (meta && !error._rasti) { | ||
| let message; | ||
@@ -46,10 +57,15 @@ | ||
| const formattedSource = formatTemplateSource(context.source, expression); | ||
| message = createDevelopmentErrorMessage(`Error in ${context.constructor.name}#${context.uid} (${meta})\n${error.message}\n\nTemplate source:\n\n${formattedSource}`); | ||
| message = createDevelopmentErrorMessage( | ||
| `Error in ${context.constructor.name}#${context.uid} (${meta})\n${error.message}\n\nTemplate source:\n\n${formattedSource}` | ||
| ); | ||
| } else { | ||
| message = createProductionErrorMessage(`Error in ${context.constructor.name}#${context.uid} expression`); | ||
| } | ||
| const enhancedError = new Error(message, { cause : error }); | ||
| enhancedError.stack = error.stack; | ||
| enhancedError._rasti = true; | ||
| throw enhancedError; | ||
| } | ||
| throw error; | ||
@@ -218,4 +234,4 @@ } | ||
| return main.replace( | ||
| new RegExp(`<(${PH})([^>]*)>([\\s\\S]*?)</\\1>|<(${PH})([^>]*)/>`,'g'), | ||
| (match, openTag, openIdx, nonVoidAttrs, inner, selfClosingTag, selfClosingIdx, selfClosingAttrs) => { | ||
| new RegExp(`<(${PH})([^>]*)/>|<(${PH})([^>]*)>([\\s\\S]*?)</\\4>`,'g'), | ||
| (match, selfClosingTag, selfClosingIdx, selfClosingAttrs, openTag, openIdx, nonVoidAttrs, inner) => { | ||
| let tag, attributesStr, innerList; | ||
@@ -250,3 +266,3 @@ | ||
| // Evaluate items in parent context and create Partial. | ||
| options.renderChildren = () => new Partial(innerList.map(item => getExpressionResult(item, this))); | ||
| options.renderChildren = () => new Partial(innerList.map(item => getExpressionResult(item, this, 'children'))); | ||
| } | ||
@@ -468,3 +484,3 @@ // Mount component. | ||
| // Parse attributes string with support for placeholders in both names and values. | ||
| const regExp = new RegExp(`(?:${PH}|([\\w-]+))(?:=(["']?)(?:${PH}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s*/?[>"']))+.))?\\3)?`, 'g'); | ||
| const regExp = new RegExp(`(?:${PH}|([\\w-]+))(?:=(["']?)(?:${PH}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s*/>|\\s*[>"']))+.))?\\3)?`, 'g'); | ||
@@ -497,3 +513,3 @@ let attributeMatch; | ||
| */ | ||
| const componentOptions = ['key', 'state', 'onCreate', 'onChange', 'onHydrate', 'onRecycle', 'onUpdate']; | ||
| const componentOptions = ['key', 'state', 'onCreate', 'onChange', 'onHydrate', 'onBeforeRecycle', 'onRecycle', 'onBeforeUpdate', 'onUpdate']; | ||
@@ -671,9 +687,11 @@ /** | ||
| * Reuse a `Component` by replacing the placeholder comment with the real nodes. | ||
| * Call `onRecycle` lifecycle method. | ||
| * @param parent {node} The parent node. | ||
| * @param props {object} The props to set on the recycled component. | ||
| * Calls `onBeforeRecycle` lifecycle method at the beginning, before any recycling operations occur. | ||
| * @param parent {node} The parent node. If not provided, the node is already in the correct position and won't be moved. | ||
| * @return {Component} The component instance. | ||
| * @private | ||
| */ | ||
| recycle(parent, props) { | ||
| recycle(parent) { | ||
| // Call `onBeforeRecycle` lifecycle method. | ||
| this.onBeforeRecycle.call(this); | ||
| // No parent means the node is already in the correct position. So we don't need to replace it. | ||
| if (parent) { | ||
@@ -684,6 +702,16 @@ // Locate the placeholder comment and replace it with the real nodes | ||
| } | ||
| // Update props. | ||
| if (props) { | ||
| this.props.set(props); | ||
| } | ||
| // Return `this` for chaining. | ||
| return this; | ||
| } | ||
| /** | ||
| * Update the component's props. | ||
| * Sets the props and calls the `onRecycle` lifecycle method. | ||
| * @param props {object} The props to set on the component. | ||
| * @return {Component} The component instance. | ||
| * @private | ||
| */ | ||
| updateProps(props) { | ||
| // Set the props. | ||
| this.props.set(props); | ||
| // Call `onRecycle` lifecycle method. | ||
@@ -719,3 +747,3 @@ this.onRecycle.call(this); | ||
| * const Title = Component.create` | ||
| * <h1>${({ props }) => props.children}</h1> | ||
| * <h1>${({ props }) => props.renderChildren()}</h1> | ||
| * `; | ||
@@ -854,10 +882,38 @@ * // Create Main component. | ||
| /** | ||
| * Render the `Component`. | ||
| * - If `this.el` is not present, the `Component` will be rendered as a string inside a `DocumentFragment` and hydrated, making `this.el` available. The `onHydrate` lifecycle method will be called. | ||
| * - If `this.el` is present, the method will update the attributes and inner HTML of the element, or recreate its child component in the case of a container. The `onUpdate` lifecycle method will be called. | ||
| * - When rendering child components, recycling happens in two ways: | ||
| * - Components with a `key` are recycled if a previous child with the same key exists. | ||
| * - Unkeyed components are recycled if they have the same type and position in the template or partial. | ||
| * A recycled `Component` will call the `onRecycle` lifecycle method. | ||
| * - If the active element is inside the component, it will retain focus after the render. | ||
| * Render the `Component`. | ||
| * | ||
| * **First render (when `this.el` is not present):** | ||
| * This is the initial render call. The component will be rendered as a string inside a `DocumentFragment` and hydrated, | ||
| * making `this.el` available. `this.el` is the root DOM element of the component that can be applied to the DOM. | ||
| * The `onHydrate` lifecycle method will be called. | ||
| * | ||
| * **Note:** Typically, you don't need to call `render()` directly for the first render. The static method `Component.mount()` | ||
| * handles this process automatically, creating the component instance, rendering it, and appending it to the DOM. | ||
| * | ||
| * **Update render (when `this.el` is present):** | ||
| * This indicates the component is being updated. The method will: | ||
| * - Update only the attributes of the root element and child elements | ||
| * - Update only the content of interpolations (the dynamic parts of the template) | ||
| * - For container components (components that render a single child component), update the single interpolation | ||
| * | ||
| * The `onBeforeUpdate` lifecycle method will be called at the beginning, followed by the `onUpdate` lifecycle method at the end. | ||
| * | ||
| * **Child component handling:** | ||
| * When rendering child components, they can be either recreated or recycled: | ||
| * | ||
| * - **Recreation:** A new component instance is created, running the constructor again. This happens when no matching component | ||
| * is found for recycling. | ||
| * | ||
| * - **Recycling:** The same component instance is reused. Recycling happens in two ways: | ||
| * - Components with a `key` are recycled if a previous child with the same key exists | ||
| * - Unkeyed components are recycled if they have the same type and position in the template or partial | ||
| * | ||
| * When a component is recycled: | ||
| * - The `onBeforeRecycle` lifecycle method is called when recycling starts | ||
| * - The component's `this.props` is updated with the new props from the parent | ||
| * - The `onRecycle` lifecycle method is called after props are updated | ||
| * | ||
| * A recycled component may not use props at all and remain unchanged, or it may be subscribed to a different model | ||
| * (or even the same model as the parent) and update independently in subsequent render cycles. | ||
| * | ||
| * @return {Component} The component instance. | ||
@@ -874,6 +930,6 @@ */ | ||
| } | ||
| // Call `onBeforeUpdate` lifecycle method. | ||
| this.onBeforeUpdate.call(this); | ||
| // Clear event listeners. | ||
| this.eventsManager.reset(); | ||
| // Update elements. | ||
| this.template.elements.forEach(element => element.update()); | ||
| // Store previous children. | ||
@@ -883,2 +939,4 @@ const previousChildren = this.children; | ||
| this.children = []; | ||
| // Store props to update. | ||
| const propsQueue = []; | ||
| // Update interpolations. | ||
@@ -927,4 +985,6 @@ this.template.interpolations.forEach(interpolation => { | ||
| const recycle = ([recycled, discarded], fragment) => { | ||
| // Add child, update props and recycle (move to new position if needed). | ||
| this.addChild(recycled).recycle(fragment, discarded.props.toJSON()); | ||
| // Store props to update. | ||
| propsQueue.push([recycled, discarded.props.toJSON()]); | ||
| // Add child and recycle (move to new position if needed). | ||
| this.addChild(recycled).recycle(fragment); | ||
| // Destroy discarded component. | ||
@@ -958,6 +1018,13 @@ discarded.destroy(); | ||
| }); | ||
| // If container, set el to the child element. | ||
| // Update recycled children props. | ||
| propsQueue.forEach(([recycled, props]) => { | ||
| recycled.updateProps(props); | ||
| }); | ||
| // If this component is a container, set el to the child element. | ||
| // Otherwise, update elements attributes and delegate events. | ||
| if (this.isContainer()) { | ||
| this.el = this.children[0].el; | ||
| } else { | ||
| // Update elements attributes. | ||
| this.template.elements.forEach(element => element.update()); | ||
| // If there are pending event types, delegate events again. | ||
@@ -1005,2 +1072,15 @@ if (this.eventsManager.hasPendingTypes()) { | ||
| /** | ||
| * Lifecycle method. Called before the component is recycled and reused between renders. | ||
| * This method is called at the beginning of the `recycle` method, before any recycling operations occur. | ||
| * | ||
| * A component is recycled when: | ||
| * - It has a `key` and a previous child with the same key exists | ||
| * - It doesn't have a `key` but has the same type and position in the template or partial | ||
| * | ||
| * Use this method to perform operations that need to happen before the component is recycled, | ||
| * such as storing previous state or preparing for the recycling. | ||
| */ | ||
| onBeforeRecycle() {} | ||
| /** | ||
| * Lifecycle method. Called when the component is recycled and reused between renders. | ||
@@ -1018,2 +1098,10 @@ * | ||
| /** | ||
| * Lifecycle method. Called before the component is updated or re-rendered. | ||
| * This method is called at the beginning of the `render` method when the component's state, model, or props change and trigger a re-render. | ||
| * Use this method to perform operations that need to happen before the component is updated, | ||
| * such as saving previous state or preparing for the update. | ||
| */ | ||
| onBeforeUpdate() {} | ||
| /** | ||
| * Lifecycle method. Called when the component is updated or re-rendered. | ||
@@ -1139,3 +1227,3 @@ * This method is called when the component's state, model, or props change and trigger a re-render. | ||
| * <button class="${({ props }) => props.className}"> | ||
| * ${({ props }) => props.children} | ||
| * ${({ props }) => props.renderChildren()} | ||
| * </button> | ||
@@ -1186,3 +1274,3 @@ * `; | ||
| * <button class="button"> | ||
| * ${({ props }) => props.children} | ||
| * ${({ props }) => props.renderChildren()} | ||
| * </button> | ||
@@ -1194,3 +1282,3 @@ * `; | ||
| * ${({ props }) => props.items.map( | ||
| * item => Button.mount({ children : item.label }) | ||
| * item => Button.mount({ renderChildren : () => item.label }) | ||
| * )} | ||
@@ -1211,3 +1299,3 @@ * </nav> | ||
| * <button class="button"> | ||
| * ${({ props }) => props.children} | ||
| * ${({ props }) => props.renderChildren()} | ||
| * </button> | ||
@@ -1235,3 +1323,3 @@ * `; | ||
| * <button class="${({ props }) => props.className}"> | ||
| * ${({ props }) => props.children} | ||
| * ${({ props }) => props.renderChildren()} | ||
| * </button> | ||
@@ -1246,3 +1334,3 @@ * `; | ||
| * className : 'cancel', | ||
| * children : 'Cancel' | ||
| * renderChildren : () => 'Cancel' | ||
| * })); | ||
@@ -1337,3 +1425,3 @@ * ``` | ||
| * @extends View | ||
| * @param {object} options Object containing options. The following keys will be merged to `this`: model, state, key, onDestroy, onHydrate, onRecycle, onUpdate, onCreate, onChange. Any additional options not in the component or view options list will be automatically extracted as props and stored as `this.props`. | ||
| * @param {object} options Object containing options. The following keys will be merged to `this`: model, state, key, onDestroy, onHydrate, onBeforeRecycle, onRecycle, onBeforeUpdate, onUpdate, onCreate, onChange. Any additional options not in the component or view options list will be automatically extracted as props and stored as `this.props`. | ||
| * @property {string} [key] A unique key to identify the component. Components with keys are recycled when the same key is found in the previous render. Unkeyed components are recycled based on type and position. | ||
@@ -1340,0 +1428,0 @@ * @property {Model} [model] A `Model` or any emitter object containing data and business logic. The component will listen to `change` events and call `onChange` lifecycle method. |
@@ -102,3 +102,3 @@ import repeat from './repeat.js'; | ||
| const fullSource = errorExpression.toString(); | ||
| if (fullSource.includes('\n')) { | ||
| if (fullSource.match(/\n/)) { | ||
| formattedLines.push(''); | ||
@@ -105,0 +105,0 @@ formattedLines.push(' | Expression details:'); |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"formatTemplateSource.js","sources":["../../src/utils/formatTemplateSource.js"],"sourcesContent":["import repeat from './repeat.js';\nimport padStart from './padStart.js';\n\n/**\n * Converts an expression to its string representation for display.\n * @param {any} expr The expression to convert.\n * @return {string} String representation of the expression.\n * @private\n */\nfunction expressionToString(expr) {\n if (typeof expr.toString === 'function') {\n const source = expr.toString();\n // Keep single line or show first line for multi-line.\n return '${' + (source.includes('\\n') ? source.split('\\n')[0] + '...' : source) + '}';\n }\n return '${' + String(expr) + '}';\n}\n\n/**\n * Formats the template source with line numbers and highlights the specific error expression.\n * @param {Object} source The original template source object with strings and expressions.\n * @param {any} errorExpression The expression that caused the error.\n * @return {string} Formatted template source.\n * @module\n * @private\n */\nexport default function formatTemplateSource(source, errorExpression) {\n if (!source || !source.strings || !source.expressions || !errorExpression) return '';\n\n const { strings, expressions } = source;\n\n // Find the index of the error expression in the expressions array.\n const expressionIndex = expressions.indexOf(errorExpression);\n if (expressionIndex === -1) return '';\n\n // Build the template source and calculate the error position directly.\n let templateSource = '';\n let errorPosition = -1;\n\n for (let i = 0; i < strings.length; i++) {\n templateSource += strings[i];\n\n if (i < expressions.length) {\n // Mark the position before adding the error expression.\n if (i === expressionIndex) {\n errorPosition = templateSource.length;\n }\n templateSource += expressionToString(expressions[i]);\n }\n }\n\n if (errorPosition === -1) return '';\n\n // Find the line and column of the error position.\n const lines = templateSource.split('\\n');\n const formattedLines = [];\n const maxLineNumWidth = String(lines.length).length;\n\n let errorLineNum = -1;\n let errorStartCol = -1;\n let charCount = 0;\n\n for (let i = 0; i < lines.length; i++) {\n const lineLength = lines[i].length + 1; // +1 for newline character.\n if (charCount + lineLength > errorPosition) {\n errorLineNum = i;\n errorStartCol = errorPosition - charCount;\n break;\n }\n charCount += lineLength;\n }\n\n if (errorLineNum === -1) return '';\n\n // Get the expression string to determine marker length.\n const expressionStr = expressionToString(errorExpression);\n\n // Format output with context.\n const contextStart = Math.max(0, errorLineNum - 2);\n const contextEnd = Math.min(lines.length - 1, errorLineNum + 2);\n\n for (let i = contextStart; i <= contextEnd; i++) {\n const lineNum = i + 1;\n const lineNumStr = padStart(String(lineNum), maxLineNumWidth, ' ');\n const isErrorLine = i === errorLineNum;\n const linePrefix = isErrorLine ? ` ${lineNumStr} > ` : ` ${lineNumStr} | `;\n\n formattedLines.push(linePrefix + lines[i]);\n\n // Add pointer on error line.\n if (isErrorLine) {\n const markerPos = linePrefix.length + errorStartCol;\n const markerLen = expressionStr.length;\n const pointerLine = repeat(' ', markerPos) + repeat('^', markerLen) + ' <-- Error here!';\n formattedLines.push(pointerLine);\n }\n }\n\n // If error expression is multi-line, show full details.\n if (typeof errorExpression === 'function') {\n const fullSource = errorExpression.toString();\n if (fullSource.includes('\\n')) {\n formattedLines.push('');\n formattedLines.push(' | Expression details:');\n fullSource.split('\\n').forEach(line => {\n formattedLines.push(' | ' + line);\n });\n }\n }\n\n return formattedLines.join('\\n');\n}\n"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"formatTemplateSource.js","sources":["../../src/utils/formatTemplateSource.js"],"sourcesContent":["import repeat from './repeat.js';\nimport padStart from './padStart.js';\n\n/**\n * Converts an expression to its string representation for display.\n * @param {any} expr The expression to convert.\n * @return {string} String representation of the expression.\n * @private\n */\nfunction expressionToString(expr) {\n if (typeof expr.toString === 'function') {\n const source = expr.toString();\n // Keep single line or show first line for multi-line.\n return '${' + (source.includes('\\n') ? source.split('\\n')[0] + '...' : source) + '}';\n }\n return '${' + String(expr) + '}';\n}\n\n/**\n * Formats the template source with line numbers and highlights the specific error expression.\n * @param {Object} source The original template source object with strings and expressions.\n * @param {any} errorExpression The expression that caused the error.\n * @return {string} Formatted template source.\n * @module\n * @private\n */\nexport default function formatTemplateSource(source, errorExpression) {\n if (!source || !source.strings || !source.expressions || !errorExpression) return '';\n\n const { strings, expressions } = source;\n\n // Find the index of the error expression in the expressions array.\n const expressionIndex = expressions.indexOf(errorExpression);\n if (expressionIndex === -1) return '';\n\n // Build the template source and calculate the error position directly.\n let templateSource = '';\n let errorPosition = -1;\n\n for (let i = 0; i < strings.length; i++) {\n templateSource += strings[i];\n\n if (i < expressions.length) {\n // Mark the position before adding the error expression.\n if (i === expressionIndex) {\n errorPosition = templateSource.length;\n }\n templateSource += expressionToString(expressions[i]);\n }\n }\n\n if (errorPosition === -1) return '';\n\n // Find the line and column of the error position.\n const lines = templateSource.split('\\n');\n const formattedLines = [];\n const maxLineNumWidth = String(lines.length).length;\n\n let errorLineNum = -1;\n let errorStartCol = -1;\n let charCount = 0;\n\n for (let i = 0; i < lines.length; i++) {\n const lineLength = lines[i].length + 1; // +1 for newline character.\n if (charCount + lineLength > errorPosition) {\n errorLineNum = i;\n errorStartCol = errorPosition - charCount;\n break;\n }\n charCount += lineLength;\n }\n\n if (errorLineNum === -1) return '';\n\n // Get the expression string to determine marker length.\n const expressionStr = expressionToString(errorExpression);\n\n // Format output with context.\n const contextStart = Math.max(0, errorLineNum - 2);\n const contextEnd = Math.min(lines.length - 1, errorLineNum + 2);\n\n for (let i = contextStart; i <= contextEnd; i++) {\n const lineNum = i + 1;\n const lineNumStr = padStart(String(lineNum), maxLineNumWidth, ' ');\n const isErrorLine = i === errorLineNum;\n const linePrefix = isErrorLine ? ` ${lineNumStr} > ` : ` ${lineNumStr} | `;\n\n formattedLines.push(linePrefix + lines[i]);\n\n // Add pointer on error line.\n if (isErrorLine) {\n const markerPos = linePrefix.length + errorStartCol;\n const markerLen = expressionStr.length;\n const pointerLine = repeat(' ', markerPos) + repeat('^', markerLen) + ' <-- Error here!';\n formattedLines.push(pointerLine);\n }\n }\n\n // If error expression is multi-line, show full details.\n if (typeof errorExpression === 'function') {\n const fullSource = errorExpression.toString();\n if (fullSource.match(/\\n/)) {\n formattedLines.push('');\n formattedLines.push(' | Expression details:');\n fullSource.split('\\n').forEach(line => {\n formattedLines.push(' | ' + line);\n });\n }\n }\n\n return formattedLines.join('\\n');\n}\n"],"names":[],"mappings":"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"} |
+28
-11
@@ -0,4 +1,19 @@ | ||
| let isChrome, moveBeforeSupported, preserveFocus, resetFocus; | ||
| // Browser compatibility notes (as of 2025): | ||
| // - Safari: Does not support moveBefore. | ||
| // - Firefox: moveBefore preserves focus but loses scroll position. | ||
| // - Chrome: moveBefore preserves scroll position but loses focus. | ||
| if (typeof document !== 'undefined') { | ||
| isChrome = !!navigator.userAgent.match(/Chrome/); | ||
| moveBeforeSupported = !!Element.prototype.moveBefore; | ||
| preserveFocus = !moveBeforeSupported || isChrome; | ||
| // When using moveBefore, Chrome resets the focus but preserves the active element. | ||
| // So we need to blur the active element before setting the focus again. | ||
| resetFocus = moveBeforeSupported && isChrome; | ||
| } | ||
| /** | ||
| * Replaces an existing DOM node with a new node, preserving internal DOM state. | ||
| * Uses moveBefore if available, otherwise falls back to before. | ||
| * Uses moveBefore if available, otherwise falls back to insertBefore. | ||
| * | ||
@@ -11,12 +26,14 @@ * @param {Node} oldNode The existing DOM node to replace. | ||
| function replaceNode(oldNode, newNode) { | ||
| if (Element.prototype.moveBefore) { | ||
| oldNode.parentNode.moveBefore(newNode, oldNode); | ||
| oldNode.parentNode.removeChild(oldNode); | ||
| } else { | ||
| const activeElement = document.activeElement; | ||
| oldNode.parentNode.insertBefore(newNode, oldNode); | ||
| oldNode.parentNode.removeChild(oldNode); | ||
| if (activeElement && activeElement !== document.activeElement && newNode.contains(activeElement)) { | ||
| activeElement.focus(); | ||
| } | ||
| const activeElement = preserveFocus && | ||
| document.activeElement && | ||
| newNode.contains(document.activeElement) ? | ||
| document.activeElement : null; | ||
| if (activeElement && resetFocus) activeElement.blur(); | ||
| oldNode.parentNode[moveBeforeSupported ? 'moveBefore' : 'insertBefore'](newNode, oldNode); | ||
| oldNode.parentNode.removeChild(oldNode); | ||
| if (activeElement && activeElement !== document.activeElement && newNode.contains(activeElement)) { | ||
| activeElement.focus(); | ||
| } | ||
@@ -23,0 +40,0 @@ } |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"replaceNode.js","sources":["../../src/utils/replaceNode.js"],"sourcesContent":["/**\n * Replaces an existing DOM node with a new node, preserving internal DOM state.\n * Uses moveBefore if available, otherwise falls back to before.\n * \n * @param {Node} oldNode The existing DOM node to replace.\n * @param {Node} newNode The new DOM node to replace the old node with.\n * @module\n * @private\n */\nexport default function replaceNode(oldNode, newNode) {\n if (Element.prototype.moveBefore) {\n oldNode.parentNode.moveBefore(newNode, oldNode);\n oldNode.parentNode.removeChild(oldNode);\n } else {\n const activeElement = document.activeElement;\n oldNode.parentNode.insertBefore(newNode, oldNode);\n oldNode.parentNode.removeChild(oldNode);\n if (activeElement && activeElement !== document.activeElement && newNode.contains(activeElement)) {\n activeElement.focus();\n }\n }\n}\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,SAAS,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE;AACtD,IAAI,IAAI,OAAO,CAAC,SAAS,CAAC,UAAU,EAAE;AACtC,QAAQ,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC;AACvD,QAAQ,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC;AAC/C,IAAI,CAAC,MAAM;AACX,QAAQ,MAAM,aAAa,GAAG,QAAQ,CAAC,aAAa;AACpD,QAAQ,OAAO,CAAC,UAAU,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC;AACzD,QAAQ,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC;AAC/C,QAAQ,IAAI,aAAa,IAAI,aAAa,KAAK,QAAQ,CAAC,aAAa,IAAI,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE;AAC1G,YAAY,aAAa,CAAC,KAAK,EAAE;AACjC,QAAQ;AACR,IAAI;AACJ;;;;"} | ||
| {"version":3,"file":"replaceNode.js","sources":["../../src/utils/replaceNode.js"],"sourcesContent":["let isChrome, moveBeforeSupported, preserveFocus, resetFocus;\n\n// Browser compatibility notes (as of 2025):\n// - Safari: Does not support moveBefore.\n// - Firefox: moveBefore preserves focus but loses scroll position.\n// - Chrome: moveBefore preserves scroll position but loses focus.\nif (typeof document !== 'undefined') {\n isChrome = !!navigator.userAgent.match(/Chrome/);\n moveBeforeSupported = !!Element.prototype.moveBefore;\n preserveFocus = !moveBeforeSupported || isChrome;\n // When using moveBefore, Chrome resets the focus but preserves the active element.\n // So we need to blur the active element before setting the focus again.\n resetFocus = moveBeforeSupported && isChrome;\n}\n\n/**\n * Replaces an existing DOM node with a new node, preserving internal DOM state.\n * Uses moveBefore if available, otherwise falls back to insertBefore.\n * \n * @param {Node} oldNode The existing DOM node to replace.\n * @param {Node} newNode The new DOM node to replace the old node with.\n * @module\n * @private\n */\nexport default function replaceNode(oldNode, newNode) {\n const activeElement = preserveFocus &&\n document.activeElement &&\n newNode.contains(document.activeElement) ?\n document.activeElement : null;\n\n if (activeElement && resetFocus) activeElement.blur();\n\n oldNode.parentNode[moveBeforeSupported ? 'moveBefore' : 'insertBefore'](newNode, oldNode);\n oldNode.parentNode.removeChild(oldNode);\n\n if (activeElement && activeElement !== document.activeElement && newNode.contains(activeElement)) {\n activeElement.focus();\n }\n}\n"],"names":[],"mappings":"AAAA,IAAI,QAAQ,EAAE,mBAAmB,EAAE,aAAa,EAAE,UAAU;;AAE5D;AACA;AACA;AACA;AACA,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE;AACrC,IAAI,QAAQ,GAAG,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC;AACpD,IAAI,mBAAmB,GAAG,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,UAAU;AACxD,IAAI,aAAa,GAAG,CAAC,mBAAmB,IAAI,QAAQ;AACpD;AACA;AACA,IAAI,UAAU,GAAG,mBAAmB,IAAI,QAAQ;AAChD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,SAAS,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE;AACtD,IAAI,MAAM,aAAa,GAAG,aAAa;AACvC,QAAQ,QAAQ,CAAC,aAAa;AAC9B,QAAQ,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,aAAa,CAAC;AAChD,QAAQ,QAAQ,CAAC,aAAa,GAAG,IAAI;;AAErC,IAAI,IAAI,aAAa,IAAI,UAAU,EAAE,aAAa,CAAC,IAAI,EAAE;;AAEzD,IAAI,OAAO,CAAC,UAAU,CAAC,mBAAmB,GAAG,YAAY,GAAG,cAAc,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC;AAC7F,IAAI,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC;;AAE3C,IAAI,IAAI,aAAa,IAAI,aAAa,KAAK,QAAQ,CAAC,aAAa,IAAI,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE;AACtG,QAAQ,aAAa,CAAC,KAAK,EAAE;AAC7B,IAAI;AACJ;;;;"} |
+126
-38
@@ -40,5 +40,16 @@ 'use strict'; | ||
| try { | ||
| return utils_getResult(expression, context, context); | ||
| if (typeof expression !== 'function') return expression; | ||
| // In development, detect uninstantiated Component classes and provide a helpful error. | ||
| // This typically happens when a component tag is malformed and not properly expanded. | ||
| if (utils_dev && expression.prototype instanceof Component) { | ||
| throw new Error( | ||
| `Received uninstantiated Component class "${expression.name || 'Anonymous'}". ` + | ||
| 'This usually happens when a component tag is malformed (e.g., missing closing tag or typo). ' + | ||
| 'If that\'s not the case, make sure to instantiate child components using a component tag, mount(), or new.' | ||
| ); | ||
| } | ||
| return expression.call(context, context); | ||
| } catch (error) { | ||
| if (meta && !error.cause) { | ||
| if (meta && !error._rasti) { | ||
| let message; | ||
@@ -48,10 +59,15 @@ | ||
| const formattedSource = utils_formatTemplateSource(context.source, expression); | ||
| message = utils_createDevelopmentErrorMessage(`Error in ${context.constructor.name}#${context.uid} (${meta})\n${error.message}\n\nTemplate source:\n\n${formattedSource}`); | ||
| message = utils_createDevelopmentErrorMessage( | ||
| `Error in ${context.constructor.name}#${context.uid} (${meta})\n${error.message}\n\nTemplate source:\n\n${formattedSource}` | ||
| ); | ||
| } else { | ||
| message = utils_createProductionErrorMessage(`Error in ${context.constructor.name}#${context.uid} expression`); | ||
| } | ||
| const enhancedError = new Error(message, { cause : error }); | ||
| enhancedError.stack = error.stack; | ||
| enhancedError._rasti = true; | ||
| throw enhancedError; | ||
| } | ||
| throw error; | ||
@@ -220,4 +236,4 @@ } | ||
| return main.replace( | ||
| new RegExp(`<(${PH})([^>]*)>([\\s\\S]*?)</\\1>|<(${PH})([^>]*)/>`,'g'), | ||
| (match, openTag, openIdx, nonVoidAttrs, inner, selfClosingTag, selfClosingIdx, selfClosingAttrs) => { | ||
| new RegExp(`<(${PH})([^>]*)/>|<(${PH})([^>]*)>([\\s\\S]*?)</\\4>`,'g'), | ||
| (match, selfClosingTag, selfClosingIdx, selfClosingAttrs, openTag, openIdx, nonVoidAttrs, inner) => { | ||
| let tag, attributesStr, innerList; | ||
@@ -252,3 +268,3 @@ | ||
| // Evaluate items in parent context and create Partial. | ||
| options.renderChildren = () => new core_Partial(innerList.map(item => getExpressionResult(item, this))); | ||
| options.renderChildren = () => new core_Partial(innerList.map(item => getExpressionResult(item, this, 'children'))); | ||
| } | ||
@@ -470,3 +486,3 @@ // Mount component. | ||
| // Parse attributes string with support for placeholders in both names and values. | ||
| const regExp = new RegExp(`(?:${PH}|([\\w-]+))(?:=(["']?)(?:${PH}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s*/?[>"']))+.))?\\3)?`, 'g'); | ||
| const regExp = new RegExp(`(?:${PH}|([\\w-]+))(?:=(["']?)(?:${PH}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s*/>|\\s*[>"']))+.))?\\3)?`, 'g'); | ||
@@ -499,3 +515,3 @@ let attributeMatch; | ||
| */ | ||
| const componentOptions = ['key', 'state', 'onCreate', 'onChange', 'onHydrate', 'onRecycle', 'onUpdate']; | ||
| const componentOptions = ['key', 'state', 'onCreate', 'onChange', 'onHydrate', 'onBeforeRecycle', 'onRecycle', 'onBeforeUpdate', 'onUpdate']; | ||
@@ -673,9 +689,11 @@ /** | ||
| * Reuse a `Component` by replacing the placeholder comment with the real nodes. | ||
| * Call `onRecycle` lifecycle method. | ||
| * @param parent {node} The parent node. | ||
| * @param props {object} The props to set on the recycled component. | ||
| * Calls `onBeforeRecycle` lifecycle method at the beginning, before any recycling operations occur. | ||
| * @param parent {node} The parent node. If not provided, the node is already in the correct position and won't be moved. | ||
| * @return {Component} The component instance. | ||
| * @private | ||
| */ | ||
| recycle(parent, props) { | ||
| recycle(parent) { | ||
| // Call `onBeforeRecycle` lifecycle method. | ||
| this.onBeforeRecycle.call(this); | ||
| // No parent means the node is already in the correct position. So we don't need to replace it. | ||
| if (parent) { | ||
@@ -686,6 +704,16 @@ // Locate the placeholder comment and replace it with the real nodes | ||
| } | ||
| // Update props. | ||
| if (props) { | ||
| this.props.set(props); | ||
| } | ||
| // Return `this` for chaining. | ||
| return this; | ||
| } | ||
| /** | ||
| * Update the component's props. | ||
| * Sets the props and calls the `onRecycle` lifecycle method. | ||
| * @param props {object} The props to set on the component. | ||
| * @return {Component} The component instance. | ||
| * @private | ||
| */ | ||
| updateProps(props) { | ||
| // Set the props. | ||
| this.props.set(props); | ||
| // Call `onRecycle` lifecycle method. | ||
@@ -721,3 +749,3 @@ this.onRecycle.call(this); | ||
| * const Title = Component.create` | ||
| * <h1>${({ props }) => props.children}</h1> | ||
| * <h1>${({ props }) => props.renderChildren()}</h1> | ||
| * `; | ||
@@ -856,10 +884,38 @@ * // Create Main component. | ||
| /** | ||
| * Render the `Component`. | ||
| * - If `this.el` is not present, the `Component` will be rendered as a string inside a `DocumentFragment` and hydrated, making `this.el` available. The `onHydrate` lifecycle method will be called. | ||
| * - If `this.el` is present, the method will update the attributes and inner HTML of the element, or recreate its child component in the case of a container. The `onUpdate` lifecycle method will be called. | ||
| * - When rendering child components, recycling happens in two ways: | ||
| * - Components with a `key` are recycled if a previous child with the same key exists. | ||
| * - Unkeyed components are recycled if they have the same type and position in the template or partial. | ||
| * A recycled `Component` will call the `onRecycle` lifecycle method. | ||
| * - If the active element is inside the component, it will retain focus after the render. | ||
| * Render the `Component`. | ||
| * | ||
| * **First render (when `this.el` is not present):** | ||
| * This is the initial render call. The component will be rendered as a string inside a `DocumentFragment` and hydrated, | ||
| * making `this.el` available. `this.el` is the root DOM element of the component that can be applied to the DOM. | ||
| * The `onHydrate` lifecycle method will be called. | ||
| * | ||
| * **Note:** Typically, you don't need to call `render()` directly for the first render. The static method `Component.mount()` | ||
| * handles this process automatically, creating the component instance, rendering it, and appending it to the DOM. | ||
| * | ||
| * **Update render (when `this.el` is present):** | ||
| * This indicates the component is being updated. The method will: | ||
| * - Update only the attributes of the root element and child elements | ||
| * - Update only the content of interpolations (the dynamic parts of the template) | ||
| * - For container components (components that render a single child component), update the single interpolation | ||
| * | ||
| * The `onBeforeUpdate` lifecycle method will be called at the beginning, followed by the `onUpdate` lifecycle method at the end. | ||
| * | ||
| * **Child component handling:** | ||
| * When rendering child components, they can be either recreated or recycled: | ||
| * | ||
| * - **Recreation:** A new component instance is created, running the constructor again. This happens when no matching component | ||
| * is found for recycling. | ||
| * | ||
| * - **Recycling:** The same component instance is reused. Recycling happens in two ways: | ||
| * - Components with a `key` are recycled if a previous child with the same key exists | ||
| * - Unkeyed components are recycled if they have the same type and position in the template or partial | ||
| * | ||
| * When a component is recycled: | ||
| * - The `onBeforeRecycle` lifecycle method is called when recycling starts | ||
| * - The component's `this.props` is updated with the new props from the parent | ||
| * - The `onRecycle` lifecycle method is called after props are updated | ||
| * | ||
| * A recycled component may not use props at all and remain unchanged, or it may be subscribed to a different model | ||
| * (or even the same model as the parent) and update independently in subsequent render cycles. | ||
| * | ||
| * @return {Component} The component instance. | ||
@@ -876,6 +932,6 @@ */ | ||
| } | ||
| // Call `onBeforeUpdate` lifecycle method. | ||
| this.onBeforeUpdate.call(this); | ||
| // Clear event listeners. | ||
| this.eventsManager.reset(); | ||
| // Update elements. | ||
| this.template.elements.forEach(element => element.update()); | ||
| // Store previous children. | ||
@@ -885,2 +941,4 @@ const previousChildren = this.children; | ||
| this.children = []; | ||
| // Store props to update. | ||
| const propsQueue = []; | ||
| // Update interpolations. | ||
@@ -929,4 +987,6 @@ this.template.interpolations.forEach(interpolation => { | ||
| const recycle = ([recycled, discarded], fragment) => { | ||
| // Add child, update props and recycle (move to new position if needed). | ||
| this.addChild(recycled).recycle(fragment, discarded.props.toJSON()); | ||
| // Store props to update. | ||
| propsQueue.push([recycled, discarded.props.toJSON()]); | ||
| // Add child and recycle (move to new position if needed). | ||
| this.addChild(recycled).recycle(fragment); | ||
| // Destroy discarded component. | ||
@@ -960,6 +1020,13 @@ discarded.destroy(); | ||
| }); | ||
| // If container, set el to the child element. | ||
| // Update recycled children props. | ||
| propsQueue.forEach(([recycled, props]) => { | ||
| recycled.updateProps(props); | ||
| }); | ||
| // If this component is a container, set el to the child element. | ||
| // Otherwise, update elements attributes and delegate events. | ||
| if (this.isContainer()) { | ||
| this.el = this.children[0].el; | ||
| } else { | ||
| // Update elements attributes. | ||
| this.template.elements.forEach(element => element.update()); | ||
| // If there are pending event types, delegate events again. | ||
@@ -1007,2 +1074,15 @@ if (this.eventsManager.hasPendingTypes()) { | ||
| /** | ||
| * Lifecycle method. Called before the component is recycled and reused between renders. | ||
| * This method is called at the beginning of the `recycle` method, before any recycling operations occur. | ||
| * | ||
| * A component is recycled when: | ||
| * - It has a `key` and a previous child with the same key exists | ||
| * - It doesn't have a `key` but has the same type and position in the template or partial | ||
| * | ||
| * Use this method to perform operations that need to happen before the component is recycled, | ||
| * such as storing previous state or preparing for the recycling. | ||
| */ | ||
| onBeforeRecycle() {} | ||
| /** | ||
| * Lifecycle method. Called when the component is recycled and reused between renders. | ||
@@ -1020,2 +1100,10 @@ * | ||
| /** | ||
| * Lifecycle method. Called before the component is updated or re-rendered. | ||
| * This method is called at the beginning of the `render` method when the component's state, model, or props change and trigger a re-render. | ||
| * Use this method to perform operations that need to happen before the component is updated, | ||
| * such as saving previous state or preparing for the update. | ||
| */ | ||
| onBeforeUpdate() {} | ||
| /** | ||
| * Lifecycle method. Called when the component is updated or re-rendered. | ||
@@ -1141,3 +1229,3 @@ * This method is called when the component's state, model, or props change and trigger a re-render. | ||
| * <button class="${({ props }) => props.className}"> | ||
| * ${({ props }) => props.children} | ||
| * ${({ props }) => props.renderChildren()} | ||
| * </button> | ||
@@ -1188,3 +1276,3 @@ * `; | ||
| * <button class="button"> | ||
| * ${({ props }) => props.children} | ||
| * ${({ props }) => props.renderChildren()} | ||
| * </button> | ||
@@ -1196,3 +1284,3 @@ * `; | ||
| * ${({ props }) => props.items.map( | ||
| * item => Button.mount({ children : item.label }) | ||
| * item => Button.mount({ renderChildren : () => item.label }) | ||
| * )} | ||
@@ -1213,3 +1301,3 @@ * </nav> | ||
| * <button class="button"> | ||
| * ${({ props }) => props.children} | ||
| * ${({ props }) => props.renderChildren()} | ||
| * </button> | ||
@@ -1237,3 +1325,3 @@ * `; | ||
| * <button class="${({ props }) => props.className}"> | ||
| * ${({ props }) => props.children} | ||
| * ${({ props }) => props.renderChildren()} | ||
| * </button> | ||
@@ -1248,3 +1336,3 @@ * `; | ||
| * className : 'cancel', | ||
| * children : 'Cancel' | ||
| * renderChildren : () => 'Cancel' | ||
| * })); | ||
@@ -1339,3 +1427,3 @@ * ``` | ||
| * @extends View | ||
| * @param {object} options Object containing options. The following keys will be merged to `this`: model, state, key, onDestroy, onHydrate, onRecycle, onUpdate, onCreate, onChange. Any additional options not in the component or view options list will be automatically extracted as props and stored as `this.props`. | ||
| * @param {object} options Object containing options. The following keys will be merged to `this`: model, state, key, onDestroy, onHydrate, onBeforeRecycle, onRecycle, onBeforeUpdate, onUpdate, onCreate, onChange. Any additional options not in the component or view options list will be automatically extracted as props and stored as `this.props`. | ||
| * @property {string} [key] A unique key to identify the component. Components with keys are recycled when the same key is found in the previous render. Unkeyed components are recycled based on type and position. | ||
@@ -1342,0 +1430,0 @@ * @property {Model} [model] A `Model` or any emitter object containing data and business logic. The component will listen to `change` events and call `onChange` lifecycle method. |
@@ -104,3 +104,3 @@ 'use strict'; | ||
| const fullSource = errorExpression.toString(); | ||
| if (fullSource.includes('\n')) { | ||
| if (fullSource.match(/\n/)) { | ||
| formattedLines.push(''); | ||
@@ -107,0 +107,0 @@ formattedLines.push(' | Expression details:'); |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"formatTemplateSource.cjs","sources":["../../src/utils/formatTemplateSource.js"],"sourcesContent":["import repeat from './repeat.js';\nimport padStart from './padStart.js';\n\n/**\n * Converts an expression to its string representation for display.\n * @param {any} expr The expression to convert.\n * @return {string} String representation of the expression.\n * @private\n */\nfunction expressionToString(expr) {\n if (typeof expr.toString === 'function') {\n const source = expr.toString();\n // Keep single line or show first line for multi-line.\n return '${' + (source.includes('\\n') ? source.split('\\n')[0] + '...' : source) + '}';\n }\n return '${' + String(expr) + '}';\n}\n\n/**\n * Formats the template source with line numbers and highlights the specific error expression.\n * @param {Object} source The original template source object with strings and expressions.\n * @param {any} errorExpression The expression that caused the error.\n * @return {string} Formatted template source.\n * @module\n * @private\n */\nexport default function formatTemplateSource(source, errorExpression) {\n if (!source || !source.strings || !source.expressions || !errorExpression) return '';\n\n const { strings, expressions } = source;\n\n // Find the index of the error expression in the expressions array.\n const expressionIndex = expressions.indexOf(errorExpression);\n if (expressionIndex === -1) return '';\n\n // Build the template source and calculate the error position directly.\n let templateSource = '';\n let errorPosition = -1;\n\n for (let i = 0; i < strings.length; i++) {\n templateSource += strings[i];\n\n if (i < expressions.length) {\n // Mark the position before adding the error expression.\n if (i === expressionIndex) {\n errorPosition = templateSource.length;\n }\n templateSource += expressionToString(expressions[i]);\n }\n }\n\n if (errorPosition === -1) return '';\n\n // Find the line and column of the error position.\n const lines = templateSource.split('\\n');\n const formattedLines = [];\n const maxLineNumWidth = String(lines.length).length;\n\n let errorLineNum = -1;\n let errorStartCol = -1;\n let charCount = 0;\n\n for (let i = 0; i < lines.length; i++) {\n const lineLength = lines[i].length + 1; // +1 for newline character.\n if (charCount + lineLength > errorPosition) {\n errorLineNum = i;\n errorStartCol = errorPosition - charCount;\n break;\n }\n charCount += lineLength;\n }\n\n if (errorLineNum === -1) return '';\n\n // Get the expression string to determine marker length.\n const expressionStr = expressionToString(errorExpression);\n\n // Format output with context.\n const contextStart = Math.max(0, errorLineNum - 2);\n const contextEnd = Math.min(lines.length - 1, errorLineNum + 2);\n\n for (let i = contextStart; i <= contextEnd; i++) {\n const lineNum = i + 1;\n const lineNumStr = padStart(String(lineNum), maxLineNumWidth, ' ');\n const isErrorLine = i === errorLineNum;\n const linePrefix = isErrorLine ? ` ${lineNumStr} > ` : ` ${lineNumStr} | `;\n\n formattedLines.push(linePrefix + lines[i]);\n\n // Add pointer on error line.\n if (isErrorLine) {\n const markerPos = linePrefix.length + errorStartCol;\n const markerLen = expressionStr.length;\n const pointerLine = repeat(' ', markerPos) + repeat('^', markerLen) + ' <-- Error here!';\n formattedLines.push(pointerLine);\n }\n }\n\n // If error expression is multi-line, show full details.\n if (typeof errorExpression === 'function') {\n const fullSource = errorExpression.toString();\n if (fullSource.includes('\\n')) {\n formattedLines.push('');\n formattedLines.push(' | Expression details:');\n fullSource.split('\\n').forEach(line => {\n formattedLines.push(' | ' + line);\n });\n }\n }\n\n return formattedLines.join('\\n');\n}\n"],"names":["padStart","repeat"],"mappings":"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"} | ||
| {"version":3,"file":"formatTemplateSource.cjs","sources":["../../src/utils/formatTemplateSource.js"],"sourcesContent":["import repeat from './repeat.js';\nimport padStart from './padStart.js';\n\n/**\n * Converts an expression to its string representation for display.\n * @param {any} expr The expression to convert.\n * @return {string} String representation of the expression.\n * @private\n */\nfunction expressionToString(expr) {\n if (typeof expr.toString === 'function') {\n const source = expr.toString();\n // Keep single line or show first line for multi-line.\n return '${' + (source.includes('\\n') ? source.split('\\n')[0] + '...' : source) + '}';\n }\n return '${' + String(expr) + '}';\n}\n\n/**\n * Formats the template source with line numbers and highlights the specific error expression.\n * @param {Object} source The original template source object with strings and expressions.\n * @param {any} errorExpression The expression that caused the error.\n * @return {string} Formatted template source.\n * @module\n * @private\n */\nexport default function formatTemplateSource(source, errorExpression) {\n if (!source || !source.strings || !source.expressions || !errorExpression) return '';\n\n const { strings, expressions } = source;\n\n // Find the index of the error expression in the expressions array.\n const expressionIndex = expressions.indexOf(errorExpression);\n if (expressionIndex === -1) return '';\n\n // Build the template source and calculate the error position directly.\n let templateSource = '';\n let errorPosition = -1;\n\n for (let i = 0; i < strings.length; i++) {\n templateSource += strings[i];\n\n if (i < expressions.length) {\n // Mark the position before adding the error expression.\n if (i === expressionIndex) {\n errorPosition = templateSource.length;\n }\n templateSource += expressionToString(expressions[i]);\n }\n }\n\n if (errorPosition === -1) return '';\n\n // Find the line and column of the error position.\n const lines = templateSource.split('\\n');\n const formattedLines = [];\n const maxLineNumWidth = String(lines.length).length;\n\n let errorLineNum = -1;\n let errorStartCol = -1;\n let charCount = 0;\n\n for (let i = 0; i < lines.length; i++) {\n const lineLength = lines[i].length + 1; // +1 for newline character.\n if (charCount + lineLength > errorPosition) {\n errorLineNum = i;\n errorStartCol = errorPosition - charCount;\n break;\n }\n charCount += lineLength;\n }\n\n if (errorLineNum === -1) return '';\n\n // Get the expression string to determine marker length.\n const expressionStr = expressionToString(errorExpression);\n\n // Format output with context.\n const contextStart = Math.max(0, errorLineNum - 2);\n const contextEnd = Math.min(lines.length - 1, errorLineNum + 2);\n\n for (let i = contextStart; i <= contextEnd; i++) {\n const lineNum = i + 1;\n const lineNumStr = padStart(String(lineNum), maxLineNumWidth, ' ');\n const isErrorLine = i === errorLineNum;\n const linePrefix = isErrorLine ? ` ${lineNumStr} > ` : ` ${lineNumStr} | `;\n\n formattedLines.push(linePrefix + lines[i]);\n\n // Add pointer on error line.\n if (isErrorLine) {\n const markerPos = linePrefix.length + errorStartCol;\n const markerLen = expressionStr.length;\n const pointerLine = repeat(' ', markerPos) + repeat('^', markerLen) + ' <-- Error here!';\n formattedLines.push(pointerLine);\n }\n }\n\n // If error expression is multi-line, show full details.\n if (typeof errorExpression === 'function') {\n const fullSource = errorExpression.toString();\n if (fullSource.match(/\\n/)) {\n formattedLines.push('');\n formattedLines.push(' | Expression details:');\n fullSource.split('\\n').forEach(line => {\n formattedLines.push(' | ' + line);\n });\n }\n }\n\n return formattedLines.join('\\n');\n}\n"],"names":["padStart","repeat"],"mappings":"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"} |
| 'use strict'; | ||
| let isChrome, moveBeforeSupported, preserveFocus, resetFocus; | ||
| // Browser compatibility notes (as of 2025): | ||
| // - Safari: Does not support moveBefore. | ||
| // - Firefox: moveBefore preserves focus but loses scroll position. | ||
| // - Chrome: moveBefore preserves scroll position but loses focus. | ||
| if (typeof document !== 'undefined') { | ||
| isChrome = !!navigator.userAgent.match(/Chrome/); | ||
| moveBeforeSupported = !!Element.prototype.moveBefore; | ||
| preserveFocus = !moveBeforeSupported || isChrome; | ||
| // When using moveBefore, Chrome resets the focus but preserves the active element. | ||
| // So we need to blur the active element before setting the focus again. | ||
| resetFocus = moveBeforeSupported && isChrome; | ||
| } | ||
| /** | ||
| * Replaces an existing DOM node with a new node, preserving internal DOM state. | ||
| * Uses moveBefore if available, otherwise falls back to before. | ||
| * Uses moveBefore if available, otherwise falls back to insertBefore. | ||
| * | ||
@@ -13,12 +28,14 @@ * @param {Node} oldNode The existing DOM node to replace. | ||
| function replaceNode(oldNode, newNode) { | ||
| if (Element.prototype.moveBefore) { | ||
| oldNode.parentNode.moveBefore(newNode, oldNode); | ||
| oldNode.parentNode.removeChild(oldNode); | ||
| } else { | ||
| const activeElement = document.activeElement; | ||
| oldNode.parentNode.insertBefore(newNode, oldNode); | ||
| oldNode.parentNode.removeChild(oldNode); | ||
| if (activeElement && activeElement !== document.activeElement && newNode.contains(activeElement)) { | ||
| activeElement.focus(); | ||
| } | ||
| const activeElement = preserveFocus && | ||
| document.activeElement && | ||
| newNode.contains(document.activeElement) ? | ||
| document.activeElement : null; | ||
| if (activeElement && resetFocus) activeElement.blur(); | ||
| oldNode.parentNode[moveBeforeSupported ? 'moveBefore' : 'insertBefore'](newNode, oldNode); | ||
| oldNode.parentNode.removeChild(oldNode); | ||
| if (activeElement && activeElement !== document.activeElement && newNode.contains(activeElement)) { | ||
| activeElement.focus(); | ||
| } | ||
@@ -25,0 +42,0 @@ } |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"replaceNode.cjs","sources":["../../src/utils/replaceNode.js"],"sourcesContent":["/**\n * Replaces an existing DOM node with a new node, preserving internal DOM state.\n * Uses moveBefore if available, otherwise falls back to before.\n * \n * @param {Node} oldNode The existing DOM node to replace.\n * @param {Node} newNode The new DOM node to replace the old node with.\n * @module\n * @private\n */\nexport default function replaceNode(oldNode, newNode) {\n if (Element.prototype.moveBefore) {\n oldNode.parentNode.moveBefore(newNode, oldNode);\n oldNode.parentNode.removeChild(oldNode);\n } else {\n const activeElement = document.activeElement;\n oldNode.parentNode.insertBefore(newNode, oldNode);\n oldNode.parentNode.removeChild(oldNode);\n if (activeElement && activeElement !== document.activeElement && newNode.contains(activeElement)) {\n activeElement.focus();\n }\n }\n}\n"],"names":[],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,SAAS,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE;AACtD,IAAI,IAAI,OAAO,CAAC,SAAS,CAAC,UAAU,EAAE;AACtC,QAAQ,OAAO,CAAC,UAAU,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC;AACvD,QAAQ,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC;AAC/C,IAAI,CAAC,MAAM;AACX,QAAQ,MAAM,aAAa,GAAG,QAAQ,CAAC,aAAa;AACpD,QAAQ,OAAO,CAAC,UAAU,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC;AACzD,QAAQ,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC;AAC/C,QAAQ,IAAI,aAAa,IAAI,aAAa,KAAK,QAAQ,CAAC,aAAa,IAAI,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE;AAC1G,YAAY,aAAa,CAAC,KAAK,EAAE;AACjC,QAAQ;AACR,IAAI;AACJ;;;;"} | ||
| {"version":3,"file":"replaceNode.cjs","sources":["../../src/utils/replaceNode.js"],"sourcesContent":["let isChrome, moveBeforeSupported, preserveFocus, resetFocus;\n\n// Browser compatibility notes (as of 2025):\n// - Safari: Does not support moveBefore.\n// - Firefox: moveBefore preserves focus but loses scroll position.\n// - Chrome: moveBefore preserves scroll position but loses focus.\nif (typeof document !== 'undefined') {\n isChrome = !!navigator.userAgent.match(/Chrome/);\n moveBeforeSupported = !!Element.prototype.moveBefore;\n preserveFocus = !moveBeforeSupported || isChrome;\n // When using moveBefore, Chrome resets the focus but preserves the active element.\n // So we need to blur the active element before setting the focus again.\n resetFocus = moveBeforeSupported && isChrome;\n}\n\n/**\n * Replaces an existing DOM node with a new node, preserving internal DOM state.\n * Uses moveBefore if available, otherwise falls back to insertBefore.\n * \n * @param {Node} oldNode The existing DOM node to replace.\n * @param {Node} newNode The new DOM node to replace the old node with.\n * @module\n * @private\n */\nexport default function replaceNode(oldNode, newNode) {\n const activeElement = preserveFocus &&\n document.activeElement &&\n newNode.contains(document.activeElement) ?\n document.activeElement : null;\n\n if (activeElement && resetFocus) activeElement.blur();\n\n oldNode.parentNode[moveBeforeSupported ? 'moveBefore' : 'insertBefore'](newNode, oldNode);\n oldNode.parentNode.removeChild(oldNode);\n\n if (activeElement && activeElement !== document.activeElement && newNode.contains(activeElement)) {\n activeElement.focus();\n }\n}\n"],"names":[],"mappings":";;AAAA,IAAI,QAAQ,EAAE,mBAAmB,EAAE,aAAa,EAAE,UAAU;;AAE5D;AACA;AACA;AACA;AACA,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE;AACrC,IAAI,QAAQ,GAAG,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC;AACpD,IAAI,mBAAmB,GAAG,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,UAAU;AACxD,IAAI,aAAa,GAAG,CAAC,mBAAmB,IAAI,QAAQ;AACpD;AACA;AACA,IAAI,UAAU,GAAG,mBAAmB,IAAI,QAAQ;AAChD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACe,SAAS,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE;AACtD,IAAI,MAAM,aAAa,GAAG,aAAa;AACvC,QAAQ,QAAQ,CAAC,aAAa;AAC9B,QAAQ,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,aAAa,CAAC;AAChD,QAAQ,QAAQ,CAAC,aAAa,GAAG,IAAI;;AAErC,IAAI,IAAI,aAAa,IAAI,UAAU,EAAE,aAAa,CAAC,IAAI,EAAE;;AAEzD,IAAI,OAAO,CAAC,UAAU,CAAC,mBAAmB,GAAG,YAAY,GAAG,cAAc,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC;AAC7F,IAAI,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,OAAO,CAAC;;AAE3C,IAAI,IAAI,aAAa,IAAI,aAAa,KAAK,QAAQ,CAAC,aAAa,IAAI,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE;AACtG,QAAQ,aAAa,CAAC,KAAK,EAAE;AAC7B,IAAI;AACJ;;;;"} |
+2
-2
| { | ||
| "name": "rasti", | ||
| "version": "4.0.0-alpha.9", | ||
| "version": "4.0.0-alpha.10", | ||
| "description": "Modern MVC for building user interfaces", | ||
@@ -33,3 +33,3 @@ "type": "module", | ||
| "test": "mocha --require jsdom-global/register --reporter nyan test/*.js", | ||
| "version": "node -e \"const fs=require('fs');const v=require('./package.json').version;const re=/@v\\d+\\.\\d+\\.\\d+(?:-[\\w.-]+)?/g;const t1=fs.readFileSync('README.md','utf8');fs.writeFileSync('README.md',t1.replace(re,'@v'+v));const t2=fs.readFileSync('example/todo/index.html','utf8');fs.writeFileSync('example/todo/index.html',t2.replace(re,'@v'+v));\" && git add README.md example/todo/index.html", | ||
| "version": "node -e \"const fs=require('fs');const {execSync}=require('child_process');const v=require('./package.json').version;const re=/rasti@(v)?\\d+\\.\\d+\\.\\d+(?:-[\\w.-]+)?/g;const files=['README.md', 'docs/AGENTS.md', 'example/todo/index.html'];files.forEach(f=>{const t=fs.readFileSync(f,'utf8');fs.writeFileSync(f,t.replace(re,(m,p)=>'rasti@'+(p||'')+v));});execSync('git add '+files.join(' '),{stdio:'inherit'});\"", | ||
| "docs:api": "jsdoc2md --module-index-format grouped --helper jsdoc2md/helper.cjs --partial jsdoc2md/header.hbs --partial jsdoc2md/sig-link.hbs --partial jsdoc2md/sig-link-html.hbs --partial jsdoc2md/sig-link-parent.hbs --partial jsdoc2md/link.hbs --files 'src/**/*.js' > docs/api.md" | ||
@@ -36,0 +36,0 @@ }, |
+11
-5
| <p align="center"> | ||
| <picture> | ||
| <source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/8tentaculos/rasti@v4.0.0-alpha.9/docs/logo-dark.svg"> | ||
| <img alt="Rasti.js" src="https://cdn.jsdelivr.net/gh/8tentaculos/rasti@v4.0.0-alpha.9/docs/logo.svg" height="120"> | ||
| <source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/8tentaculos/rasti@v4.0.0-alpha.10/docs/logo-dark.svg"> | ||
| <img alt="Rasti.js" src="https://cdn.jsdelivr.net/gh/8tentaculos/rasti@v4.0.0-alpha.10/docs/logo.svg" height="120"> | ||
| </picture> | ||
@@ -12,3 +12,5 @@ </p> | ||
| **Rasti** is a lightweight MVC library for building fast, reactive user interfaces. Inspired by **Backbone.js**, it retains a familiar API while removing non-essential features and introducing modern, declarative, and composable components to simplify complex UI development. | ||
| **Rasti is a lightweight MVC library for building fast, reactive user interfaces.** | ||
| It provides declarative, composable **components** for building state-driven UIs. | ||
| Its low-level MVC core, inspired by **Backbone.js**’s architecture, provides **models**, **views** and **event emitters** as the fundamental building blocks. | ||
@@ -34,3 +36,3 @@ [](https://app.travis-ci.com/8tentaculos/rasti) | ||
| - **Legacy Compatibility** 🕰️ | ||
| Seamlessly integrates into existing **Backbone.js** projects. | ||
| Seamlessly integrates into existing **Backbone.js** legacy projects. | ||
| - **Standards-Based** 📐 | ||
@@ -171,3 +173,3 @@ Built on modern web standards, no tooling required. | ||
| [Try it on CodePen](https://https://codepen.io/8tentaculos/pen/XJXVQOR?editors=0010) | ||
| [Try it on CodePen](https://codepen.io/8tentaculos/pen/XJXVQOR?editors=0010) | ||
@@ -193,2 +195,6 @@ ## Why Choose **Rasti**? | ||
| ## Working with LLMs | ||
| For those working with LLMs, there is an [AI Agents reference guide](/docs/AGENTS.md) that provides API patterns, lifecycle methods, and best practices, optimized for LLM context. You can share this guide with AI assistants to help them understand **Rasti**'s architecture and component APIs. | ||
| ## Version History | ||
@@ -195,0 +201,0 @@ |
+126
-38
@@ -31,5 +31,16 @@ import View from './View.js'; | ||
| try { | ||
| return getResult(expression, context, context); | ||
| if (typeof expression !== 'function') return expression; | ||
| // In development, detect uninstantiated Component classes and provide a helpful error. | ||
| // This typically happens when a component tag is malformed and not properly expanded. | ||
| if (__DEV__ && expression.prototype instanceof Component) { | ||
| throw new Error( | ||
| `Received uninstantiated Component class "${expression.name || 'Anonymous'}". ` + | ||
| 'This usually happens when a component tag is malformed (e.g., missing closing tag or typo). ' + | ||
| 'If that\'s not the case, make sure to instantiate child components using a component tag, mount(), or new.' | ||
| ); | ||
| } | ||
| return expression.call(context, context); | ||
| } catch (error) { | ||
| if (meta && !error.cause) { | ||
| if (meta && !error._rasti) { | ||
| let message; | ||
@@ -39,10 +50,15 @@ | ||
| const formattedSource = formatTemplateSource(context.source, expression); | ||
| message = createDevelopmentErrorMessage(`Error in ${context.constructor.name}#${context.uid} (${meta})\n${error.message}\n\nTemplate source:\n\n${formattedSource}`); | ||
| message = createDevelopmentErrorMessage( | ||
| `Error in ${context.constructor.name}#${context.uid} (${meta})\n${error.message}\n\nTemplate source:\n\n${formattedSource}` | ||
| ); | ||
| } else { | ||
| message = createProductionErrorMessage(`Error in ${context.constructor.name}#${context.uid} expression`); | ||
| } | ||
| const enhancedError = new Error(message, { cause : error }); | ||
| enhancedError.stack = error.stack; | ||
| enhancedError._rasti = true; | ||
| throw enhancedError; | ||
| } | ||
| throw error; | ||
@@ -211,4 +227,4 @@ } | ||
| return main.replace( | ||
| new RegExp(`<(${PH})([^>]*)>([\\s\\S]*?)</\\1>|<(${PH})([^>]*)/>`,'g'), | ||
| (match, openTag, openIdx, nonVoidAttrs, inner, selfClosingTag, selfClosingIdx, selfClosingAttrs) => { | ||
| new RegExp(`<(${PH})([^>]*)/>|<(${PH})([^>]*)>([\\s\\S]*?)</\\4>`,'g'), | ||
| (match, selfClosingTag, selfClosingIdx, selfClosingAttrs, openTag, openIdx, nonVoidAttrs, inner) => { | ||
| let tag, attributesStr, innerList; | ||
@@ -243,3 +259,3 @@ | ||
| // Evaluate items in parent context and create Partial. | ||
| options.renderChildren = () => new Partial(innerList.map(item => getExpressionResult(item, this))); | ||
| options.renderChildren = () => new Partial(innerList.map(item => getExpressionResult(item, this, 'children'))); | ||
| } | ||
@@ -461,3 +477,3 @@ // Mount component. | ||
| // Parse attributes string with support for placeholders in both names and values. | ||
| const regExp = new RegExp(`(?:${PH}|([\\w-]+))(?:=(["']?)(?:${PH}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s*/?[>"']))+.))?\\3)?`, 'g'); | ||
| const regExp = new RegExp(`(?:${PH}|([\\w-]+))(?:=(["']?)(?:${PH}|((?:.?(?!["']?\\s+(?:\\S+)=|\\s*/>|\\s*[>"']))+.))?\\3)?`, 'g'); | ||
@@ -490,3 +506,3 @@ let attributeMatch; | ||
| */ | ||
| const componentOptions = ['key', 'state', 'onCreate', 'onChange', 'onHydrate', 'onRecycle', 'onUpdate']; | ||
| const componentOptions = ['key', 'state', 'onCreate', 'onChange', 'onHydrate', 'onBeforeRecycle', 'onRecycle', 'onBeforeUpdate', 'onUpdate']; | ||
@@ -664,9 +680,11 @@ /** | ||
| * Reuse a `Component` by replacing the placeholder comment with the real nodes. | ||
| * Call `onRecycle` lifecycle method. | ||
| * @param parent {node} The parent node. | ||
| * @param props {object} The props to set on the recycled component. | ||
| * Calls `onBeforeRecycle` lifecycle method at the beginning, before any recycling operations occur. | ||
| * @param parent {node} The parent node. If not provided, the node is already in the correct position and won't be moved. | ||
| * @return {Component} The component instance. | ||
| * @private | ||
| */ | ||
| recycle(parent, props) { | ||
| recycle(parent) { | ||
| // Call `onBeforeRecycle` lifecycle method. | ||
| this.onBeforeRecycle.call(this); | ||
| // No parent means the node is already in the correct position. So we don't need to replace it. | ||
| if (parent) { | ||
@@ -677,6 +695,16 @@ // Locate the placeholder comment and replace it with the real nodes | ||
| } | ||
| // Update props. | ||
| if (props) { | ||
| this.props.set(props); | ||
| } | ||
| // Return `this` for chaining. | ||
| return this; | ||
| } | ||
| /** | ||
| * Update the component's props. | ||
| * Sets the props and calls the `onRecycle` lifecycle method. | ||
| * @param props {object} The props to set on the component. | ||
| * @return {Component} The component instance. | ||
| * @private | ||
| */ | ||
| updateProps(props) { | ||
| // Set the props. | ||
| this.props.set(props); | ||
| // Call `onRecycle` lifecycle method. | ||
@@ -712,3 +740,3 @@ this.onRecycle.call(this); | ||
| * const Title = Component.create` | ||
| * <h1>${({ props }) => props.children}</h1> | ||
| * <h1>${({ props }) => props.renderChildren()}</h1> | ||
| * `; | ||
@@ -847,10 +875,38 @@ * // Create Main component. | ||
| /** | ||
| * Render the `Component`. | ||
| * - If `this.el` is not present, the `Component` will be rendered as a string inside a `DocumentFragment` and hydrated, making `this.el` available. The `onHydrate` lifecycle method will be called. | ||
| * - If `this.el` is present, the method will update the attributes and inner HTML of the element, or recreate its child component in the case of a container. The `onUpdate` lifecycle method will be called. | ||
| * - When rendering child components, recycling happens in two ways: | ||
| * - Components with a `key` are recycled if a previous child with the same key exists. | ||
| * - Unkeyed components are recycled if they have the same type and position in the template or partial. | ||
| * A recycled `Component` will call the `onRecycle` lifecycle method. | ||
| * - If the active element is inside the component, it will retain focus after the render. | ||
| * Render the `Component`. | ||
| * | ||
| * **First render (when `this.el` is not present):** | ||
| * This is the initial render call. The component will be rendered as a string inside a `DocumentFragment` and hydrated, | ||
| * making `this.el` available. `this.el` is the root DOM element of the component that can be applied to the DOM. | ||
| * The `onHydrate` lifecycle method will be called. | ||
| * | ||
| * **Note:** Typically, you don't need to call `render()` directly for the first render. The static method `Component.mount()` | ||
| * handles this process automatically, creating the component instance, rendering it, and appending it to the DOM. | ||
| * | ||
| * **Update render (when `this.el` is present):** | ||
| * This indicates the component is being updated. The method will: | ||
| * - Update only the attributes of the root element and child elements | ||
| * - Update only the content of interpolations (the dynamic parts of the template) | ||
| * - For container components (components that render a single child component), update the single interpolation | ||
| * | ||
| * The `onBeforeUpdate` lifecycle method will be called at the beginning, followed by the `onUpdate` lifecycle method at the end. | ||
| * | ||
| * **Child component handling:** | ||
| * When rendering child components, they can be either recreated or recycled: | ||
| * | ||
| * - **Recreation:** A new component instance is created, running the constructor again. This happens when no matching component | ||
| * is found for recycling. | ||
| * | ||
| * - **Recycling:** The same component instance is reused. Recycling happens in two ways: | ||
| * - Components with a `key` are recycled if a previous child with the same key exists | ||
| * - Unkeyed components are recycled if they have the same type and position in the template or partial | ||
| * | ||
| * When a component is recycled: | ||
| * - The `onBeforeRecycle` lifecycle method is called when recycling starts | ||
| * - The component's `this.props` is updated with the new props from the parent | ||
| * - The `onRecycle` lifecycle method is called after props are updated | ||
| * | ||
| * A recycled component may not use props at all and remain unchanged, or it may be subscribed to a different model | ||
| * (or even the same model as the parent) and update independently in subsequent render cycles. | ||
| * | ||
| * @return {Component} The component instance. | ||
@@ -867,6 +923,6 @@ */ | ||
| } | ||
| // Call `onBeforeUpdate` lifecycle method. | ||
| this.onBeforeUpdate.call(this); | ||
| // Clear event listeners. | ||
| this.eventsManager.reset(); | ||
| // Update elements. | ||
| this.template.elements.forEach(element => element.update()); | ||
| // Store previous children. | ||
@@ -876,2 +932,4 @@ const previousChildren = this.children; | ||
| this.children = []; | ||
| // Store props to update. | ||
| const propsQueue = []; | ||
| // Update interpolations. | ||
@@ -920,4 +978,6 @@ this.template.interpolations.forEach(interpolation => { | ||
| const recycle = ([recycled, discarded], fragment) => { | ||
| // Add child, update props and recycle (move to new position if needed). | ||
| this.addChild(recycled).recycle(fragment, discarded.props.toJSON()); | ||
| // Store props to update. | ||
| propsQueue.push([recycled, discarded.props.toJSON()]); | ||
| // Add child and recycle (move to new position if needed). | ||
| this.addChild(recycled).recycle(fragment); | ||
| // Destroy discarded component. | ||
@@ -951,6 +1011,13 @@ discarded.destroy(); | ||
| }); | ||
| // If container, set el to the child element. | ||
| // Update recycled children props. | ||
| propsQueue.forEach(([recycled, props]) => { | ||
| recycled.updateProps(props); | ||
| }); | ||
| // If this component is a container, set el to the child element. | ||
| // Otherwise, update elements attributes and delegate events. | ||
| if (this.isContainer()) { | ||
| this.el = this.children[0].el; | ||
| } else { | ||
| // Update elements attributes. | ||
| this.template.elements.forEach(element => element.update()); | ||
| // If there are pending event types, delegate events again. | ||
@@ -998,2 +1065,15 @@ if (this.eventsManager.hasPendingTypes()) { | ||
| /** | ||
| * Lifecycle method. Called before the component is recycled and reused between renders. | ||
| * This method is called at the beginning of the `recycle` method, before any recycling operations occur. | ||
| * | ||
| * A component is recycled when: | ||
| * - It has a `key` and a previous child with the same key exists | ||
| * - It doesn't have a `key` but has the same type and position in the template or partial | ||
| * | ||
| * Use this method to perform operations that need to happen before the component is recycled, | ||
| * such as storing previous state or preparing for the recycling. | ||
| */ | ||
| onBeforeRecycle() {} | ||
| /** | ||
| * Lifecycle method. Called when the component is recycled and reused between renders. | ||
@@ -1011,2 +1091,10 @@ * | ||
| /** | ||
| * Lifecycle method. Called before the component is updated or re-rendered. | ||
| * This method is called at the beginning of the `render` method when the component's state, model, or props change and trigger a re-render. | ||
| * Use this method to perform operations that need to happen before the component is updated, | ||
| * such as saving previous state or preparing for the update. | ||
| */ | ||
| onBeforeUpdate() {} | ||
| /** | ||
| * Lifecycle method. Called when the component is updated or re-rendered. | ||
@@ -1132,3 +1220,3 @@ * This method is called when the component's state, model, or props change and trigger a re-render. | ||
| * <button class="${({ props }) => props.className}"> | ||
| * ${({ props }) => props.children} | ||
| * ${({ props }) => props.renderChildren()} | ||
| * </button> | ||
@@ -1179,3 +1267,3 @@ * `; | ||
| * <button class="button"> | ||
| * ${({ props }) => props.children} | ||
| * ${({ props }) => props.renderChildren()} | ||
| * </button> | ||
@@ -1187,3 +1275,3 @@ * `; | ||
| * ${({ props }) => props.items.map( | ||
| * item => Button.mount({ children : item.label }) | ||
| * item => Button.mount({ renderChildren : () => item.label }) | ||
| * )} | ||
@@ -1204,3 +1292,3 @@ * </nav> | ||
| * <button class="button"> | ||
| * ${({ props }) => props.children} | ||
| * ${({ props }) => props.renderChildren()} | ||
| * </button> | ||
@@ -1228,3 +1316,3 @@ * `; | ||
| * <button class="${({ props }) => props.className}"> | ||
| * ${({ props }) => props.children} | ||
| * ${({ props }) => props.renderChildren()} | ||
| * </button> | ||
@@ -1239,3 +1327,3 @@ * `; | ||
| * className : 'cancel', | ||
| * children : 'Cancel' | ||
| * renderChildren : () => 'Cancel' | ||
| * })); | ||
@@ -1330,3 +1418,3 @@ * ``` | ||
| * @extends View | ||
| * @param {object} options Object containing options. The following keys will be merged to `this`: model, state, key, onDestroy, onHydrate, onRecycle, onUpdate, onCreate, onChange. Any additional options not in the component or view options list will be automatically extracted as props and stored as `this.props`. | ||
| * @param {object} options Object containing options. The following keys will be merged to `this`: model, state, key, onDestroy, onHydrate, onBeforeRecycle, onRecycle, onBeforeUpdate, onUpdate, onCreate, onChange. Any additional options not in the component or view options list will be automatically extracted as props and stored as `this.props`. | ||
| * @property {string} [key] A unique key to identify the component. Components with keys are recycled when the same key is found in the previous render. Unkeyed components are recycled based on type and position. | ||
@@ -1333,0 +1421,0 @@ * @property {Model} [model] A `Model` or any emitter object containing data and business logic. The component will listen to `change` events and call `onChange` lifecycle method. |
@@ -102,3 +102,3 @@ import repeat from './repeat.js'; | ||
| const fullSource = errorExpression.toString(); | ||
| if (fullSource.includes('\n')) { | ||
| if (fullSource.match(/\n/)) { | ||
| formattedLines.push(''); | ||
@@ -105,0 +105,0 @@ formattedLines.push(' | Expression details:'); |
+28
-11
@@ -0,4 +1,19 @@ | ||
| let isChrome, moveBeforeSupported, preserveFocus, resetFocus; | ||
| // Browser compatibility notes (as of 2025): | ||
| // - Safari: Does not support moveBefore. | ||
| // - Firefox: moveBefore preserves focus but loses scroll position. | ||
| // - Chrome: moveBefore preserves scroll position but loses focus. | ||
| if (typeof document !== 'undefined') { | ||
| isChrome = !!navigator.userAgent.match(/Chrome/); | ||
| moveBeforeSupported = !!Element.prototype.moveBefore; | ||
| preserveFocus = !moveBeforeSupported || isChrome; | ||
| // When using moveBefore, Chrome resets the focus but preserves the active element. | ||
| // So we need to blur the active element before setting the focus again. | ||
| resetFocus = moveBeforeSupported && isChrome; | ||
| } | ||
| /** | ||
| * Replaces an existing DOM node with a new node, preserving internal DOM state. | ||
| * Uses moveBefore if available, otherwise falls back to before. | ||
| * Uses moveBefore if available, otherwise falls back to insertBefore. | ||
| * | ||
@@ -11,13 +26,15 @@ * @param {Node} oldNode The existing DOM node to replace. | ||
| export default function replaceNode(oldNode, newNode) { | ||
| if (Element.prototype.moveBefore) { | ||
| oldNode.parentNode.moveBefore(newNode, oldNode); | ||
| oldNode.parentNode.removeChild(oldNode); | ||
| } else { | ||
| const activeElement = document.activeElement; | ||
| oldNode.parentNode.insertBefore(newNode, oldNode); | ||
| oldNode.parentNode.removeChild(oldNode); | ||
| if (activeElement && activeElement !== document.activeElement && newNode.contains(activeElement)) { | ||
| activeElement.focus(); | ||
| } | ||
| const activeElement = preserveFocus && | ||
| document.activeElement && | ||
| newNode.contains(document.activeElement) ? | ||
| document.activeElement : null; | ||
| if (activeElement && resetFocus) activeElement.blur(); | ||
| oldNode.parentNode[moveBeforeSupported ? 'moveBefore' : 'insertBefore'](newNode, oldNode); | ||
| oldNode.parentNode.removeChild(oldNode); | ||
| if (activeElement && activeElement !== document.activeElement && newNode.contains(activeElement)) { | ||
| activeElement.focus(); | ||
| } | ||
| } |
-21
| The MIT License (MIT) | ||
| Copyright (c) 2018-present Alberto Masuelli | ||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
1959574
50.03%12704
3.11%213
2.9%