@elenajs/core 1.0.0-rc.10 → 1.0.0-rc.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bundle.js +2 -2
- package/dist/common/props.d.ts +2 -2
- package/dist/common/props.d.ts.map +1 -1
- package/dist/common/render.d.ts +2 -2
- package/dist/common/utils.d.ts +3 -6
- package/dist/common/utils.d.ts.map +1 -1
- package/dist/elena.d.ts +4 -0
- package/dist/elena.d.ts.map +1 -1
- package/dist/elena.js +2 -2
- package/dist/render.js +1 -1
- package/dist/utils.js +1 -1
- package/package.json +2 -2
- package/src/common/props.js +1 -1
- package/src/common/render.js +103 -45
- package/src/common/utils.js +30 -21
- package/src/elena.js +49 -37
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
<br/>
|
|
24
24
|
|
|
25
|
-
<p align="center">Elena is a simple, tiny library
|
|
25
|
+
<p align="center">Elena is a simple, tiny library for building <a href="https://elenajs.com/">Progressive Web Components</a>. Unlike most web component libraries, Elena doesn’t force JavaScript for everything. You can load HTML and CSS first, then use JavaScript to progressively add interactivity.</p>
|
|
26
26
|
|
|
27
27
|
## Documentation
|
|
28
28
|
|
package/dist/bundle.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @elenajs/core v1.0.0-rc.
|
|
2
|
+
* @elenajs/core v1.0.0-rc.12
|
|
3
3
|
* (c) 2025-present Ariel Salminen and Elena contributors
|
|
4
4
|
* @license MIT
|
|
5
5
|
*/
|
|
6
|
-
const t="░█ [ELENA]: ",s=Array.isArray,i=s=>console.warn(t+s),e={"&":"&","<":"<",">":">",'"':""","'":"'"};function
|
|
6
|
+
const t="░█ [ELENA]: ",s=Array.isArray,i=Symbol("elena.raw"),n=s=>console.warn(t+s),e={"&":"&","<":"<",">":">",'"':""","'":"'"};function o(t){return s(t)?t.map(r).join(""):r(t)}function r(t){return t?.[i]?String(t):String(t??"").replace(/[&<>"']/g,t=>e[t])}class h{constructor(t,s){this.strings=t,this.values=s}toString(){return null==this.t&&(this.t=this.strings.reduce((t,s,i)=>t+s+o(this.values[i]),"")),this.t}}function c(t,...s){return new h(t,s)}function l(t){return{[i]:!0,toString:()=>t??""}}h.prototype[i]=!0;const u={[i]:!0,toString:()=>""},f=t=>s(t)?t.some(t=>t?.[i]):!!t?.[i],a=t=>s(t)?t.join(""):String(t??"");function d(t){return t.replace(/(>)\n\s*|\n\s*(<)/g,"$1$2").replace(/\n\s*/g," ").replace(/>\s+</g,"><")}function p(t,s,i){if(s="boolean"===t&&"boolean"!=typeof s?null!==s:s,!i)return s;if("toAttribute"===i)switch(t){case"object":case"array":return s&&JSON.stringify(s);case"boolean":return s?"":null;default:return""===s?null:s}else switch(t){case"object":case"array":if(!s)return s;try{return JSON.parse(s)}catch{return n("Invalid JSON: "+s),null}case"number":return null!==s?+s:s;default:return s??""}}function g(t,s,i){t?null===i?t.removeAttribute(s):t.setAttribute(s,i):n("Cannot sync attrs.")}const y=new WeakMap,b="e"+Math.random().toString(36).slice(2),S=()=>document.createElement("template"),m=t=>document.createTreeWalker(t,128);function _(t,s){const i=`\x3c!--${b}--\x3e`,n=[];let e="";for(let o=0;o<t.length;o++)if(e+=t[o],o<s){const s=t[o].match(/([^\s"'>/=]+)\s*=\s*["']$/);s?(n.push(s[1]),e+=b+"_"+o):(n.push(null),e+=i)}const o=S();o.innerHTML=e.trim();const r=m(o.content);let h=0;for(;r.nextNode();)r.currentNode.data===b&&h++;return h!==n.filter(t=>null===t).length?null:{i:o,o:n}}function w(t,s){const i=Array.from(t.childNodes),n=Array.from(s),e=Math.max(i.length,n.length);for(let s=0;s<e;s++){const e=i[s],o=n[s];e?o?e.nodeType!==o.nodeType||1===e.nodeType&&e.tagName!==o.tagName?t.replaceChild(o,e):3===e.nodeType?e.textContent!==o.textContent&&(e.textContent=o.textContent):1===e.nodeType&&(v(e,o),w(e,o.childNodes)):t.removeChild(e):t.appendChild(o)}}function v(t,s){for(let i=t.attributes.length-1;i>=0;i--){const{name:n}=t.attributes[i];s.hasAttribute(n)||t.removeAttribute(n)}for(let i=0;i<s.attributes.length;i++){const{name:n,value:e}=s.attributes[i];t.getAttribute(n)!==e&&t.setAttribute(n,e)}}const x=new WeakSet,C=(t,s)=>Object.prototype.hasOwnProperty.call(t,s);function A(i){return class extends i{element=null;attributeChangedCallback(t,s,i){if(super.attributeChangedCallback?.(t,s,i),"text"!==t){if(s!==i)if(this.h&&!this.l){const s=this.u.get(t),n=typeof s,e="string"===n?i??"":p(n,i,"toProp");e!==s&&this.u.set(t,e),this.p()}else this.S=!0,function(t,s,i,e){if(i!==e){const i=typeof t[s];"undefined"===i&&n(`Prop "${s}" has no default.`);const o=p(i,e,"toProp");t[s]=o}}(this,t,s,i),this.S=!1}else this.text=i??""}static get observedAttributes(){if(this.m)return this.m;const t=(this.props||[]).map(t=>"string"==typeof t?t:t.name);return this.m=[...t,"text"],this.m}connectedCallback(){super.connectedCallback?.(),this._(),this.v(),this.h||void 0!==this.C||(this.text=this.textContent.trim()),this.A(),this.k=this.P??this.shadowRoot??this,this.$??=()=>{try{this.M()}catch(s){console.error(t,s)}},this.willUpdate(),this.N(),this.O(),this.j(),this.h||(this.h=!0,this.setAttribute("hydrated",""),this.firstUpdated()),this.updated()}_(){const t=this.constructor;if(x.has(t))return;const s=new Set,i=[];if(t.props){for(const n of t.props)"string"==typeof n?i.push(n):(i.push(n.name),!1===n.reflect&&s.add(n.name));i.includes("text")&&n('"text" is reserved.'),function(t,s,i){for(const n of s){const s=!i||!i.has(n);Object.defineProperty(t,n,{configurable:!0,enumerable:!0,get(){return this.u?.get(n)},set(t){if(this.u||(this.u=new Map),t!==this.u.get(n)&&(this.u.set(n,t),this.isConnected))if(s){if(!this.S){const s=p(typeof t,t,"toAttribute");g(this,n,s)}}else this.h&&!this.l&&this.p()}})}}(t.prototype,i,s)}if(t.U=i,t.q=s,t.J=t.events||null,t.J)for(const s of t.J)C(t.prototype,s)||(t.prototype[s]=function(...t){return this.element[s](...t)});var e;t.R=(e=t.element)?t=>t.querySelector(e):t=>t.firstElementChild,x.add(t)}v(){this.S=!0;for(const t of this.constructor.U)if(C(this,t)){const s=this[t];delete this[t],this[t]=s}this.S=!1}A(){const t=this.constructor;if(!t.shadow)return;this.P||this.shadowRoot||(this.P=this.attachShadow({mode:t.shadow}));const s=this.P??this.shadowRoot;if(t.styles){if(!t.W){const s=[t.styles].flat();t.W=s.map(t=>{if("string"==typeof t){const s=new CSSStyleSheet;return s.replaceSync(t),s}return t})}s.adoptedStyleSheets=t.W}}N(){const t=this.constructor,i=this.k,e=this.render();if(e&&e.strings&&!function(t,i,n){if(t.D!==i||!t.F)return!1;const e=t.F,o=t.I;for(let t=0;t<n.length;t++){const i=n[t],r=s(i)?a(i):i;if(r===o[t])continue;if(f(i)&&i!==u)return!1;const h=e[t];if(!h)return!1;o[t]=r;const c=String(r??"");h.nodeType?h.textContent=c:h[0].setAttribute(h[1],c)}return!0}(r=i,h=e.strings,c=e.values)&&(function(t,i,n){let e=y.get(i);if(!e){const t=i.map(d);e={L:t,T:n.length>0?_(t,n.length):null},y.set(i,e)}if(e.T)t.F=function(t,i,n){const{i:e,o:r}=i,h=e.content.cloneNode(!0),c=m(h),l=Array(n.length),d=[];let p;for(;p=c.nextNode();)p.data===b&&d.push(p);let g=0;for(let t=0;t<n.length;t++){const i=r[t];if(i){const e=h.querySelector(`[${i}="${b+"_"+t}"]`);if(e){const o=n[t],r=String((s(o)?a(o):o)??"");e.setAttribute(i,r),l[t]=[e,i]}}else{const s=d[g++],i=n[t];if(f(i)&&i!==u){const t=S();t.innerHTML=o(i),s.parentNode.replaceChild(t.content,s)}else{const n=document.createTextNode(a(i));s.parentNode.replaceChild(n,s),l[t]=n}}}return t.D?(w(t,h.childNodes),null):(t.replaceChildren(h),l)}(t,e.T,n);else{const s=n.map(o),i=e.L.reduce((t,i,n)=>t+i+(s[n]??""),"").replace(/>\s+</g,"><").trim(),r=S();r.innerHTML=i,w(t,r.content.childNodes),t.F=null}t.D=i,t.I=n.map(t=>s(t)?a(t):t)}(r,h,c),1)){const s=this.element;if(this.element=t.R(i),this.B&&s&&this.element!==s){const i=t.J;for(const t of i)s.removeEventListener(t,this),this.element.addEventListener(t,this)}}var r,h,c;this.element||(this.element=t.R(i),this.element||(t.element&&n("Element not found."),this.element=i.firstElementChild))}O(){if(this.u){const t=this.constructor.q;for(const[s,i]of this.u){if(t.has(s))continue;const n=p(typeof i,i,"toAttribute");(null!==n||this.hasAttribute(s))&&g(this,s,n)}}}j(){const t=this.constructor.J;if(!this.B&&t?.length)if(this.element){this.B=!0;for(const s of t)this.element.addEventListener(s,this)}else n("Cannot add events.")}render(){}willUpdate(){}firstUpdated(){}updated(){}adoptedCallback(){super.adoptedCallback?.()}disconnectedCallback(){if(super.disconnectedCallback?.(),this.B){this.B=!1;for(const t of this.constructor.J)this.element?.removeEventListener(t,this)}}handleEvent(t){this.constructor.J?.includes(t.type)&&(t.bubbles&&(t.composed||this.k===this)||this.dispatchEvent(new Event(t.type,{bubbles:t.bubbles})))}get text(){return this.C??""}set text(t){const s=this.C;this.C=t,this.h&&s!==t&&!this.l&&this.p()}static define(){const t=this.tagName;t?function(t,s){const i=globalThis.customElements;i?.get(t)||i?.define(t,s)}(t,this):n("define() without a tagName.")}p(){this.l||this.G||(this.G=!0,queueMicrotask(this.$))}M(){this.G=!1;const t=this.H;this.H=null;try{try{this.willUpdate(),this.l=!0,this.N()}finally{this.l=!1}this.updated()}finally{this.K=null,t?.()}}get updateComplete(){return this.G?(this.K||(this.K=new Promise(t=>{this.H=t})),this.K):Promise.resolve()}requestUpdate(){this.h&&!this.l&&this.p()}}}export{A as Elena,c as html,u as nothing,l as unsafeHTML};
|
package/dist/common/props.d.ts
CHANGED
|
@@ -19,11 +19,11 @@ export function syncAttribute(element: Element, name: string, value: string | nu
|
|
|
19
19
|
* at class-creation time. Values are stored per-instance
|
|
20
20
|
* via a `_props` Map that is lazily created.
|
|
21
21
|
*
|
|
22
|
-
* @param {
|
|
22
|
+
* @param {Object} proto - The class prototype
|
|
23
23
|
* @param {string[]} propNames - Prop names to define
|
|
24
24
|
* @param {Set<string>} [noReflect] - Props that should not reflect to attributes
|
|
25
25
|
*/
|
|
26
|
-
export function setProps(proto:
|
|
26
|
+
export function setProps(proto: Object, propNames: string[], noReflect?: Set<string>): void;
|
|
27
27
|
/**
|
|
28
28
|
* We need to update the internals of the Elena Element
|
|
29
29
|
* when props on the host element are changed.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"props.d.ts","sourceRoot":"","sources":["../../src/common/props.js"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,mCAJW,MAAM,SACN,GAAG,cACH,aAAa,GAAG,QAAQ,OAqClC;AAED;;;;;;GAMG;AACH,uCAJW,OAAO,QACP,MAAM,SACN,MAAM,GAAG,IAAI,QAYvB;AAED;;;;;;;;GAQG;AACH,
|
|
1
|
+
{"version":3,"file":"props.d.ts","sourceRoot":"","sources":["../../src/common/props.js"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,mCAJW,MAAM,SACN,GAAG,cACH,aAAa,GAAG,QAAQ,OAqClC;AAED;;;;;;GAMG;AACH,uCAJW,OAAO,QACP,MAAM,SACN,MAAM,GAAG,IAAI,QAYvB;AAED;;;;;;;;GAQG;AACH,gCAJW,MAAM,aACN,MAAM,EAAE,cACR,GAAG,CAAC,MAAM,CAAC,QAsCrB;AAED;;;;;;;;GAQG;AACH,kCALW,MAAM,QACN,MAAM,YACN,GAAG,YACH,GAAG,QAWb"}
|
package/dist/common/render.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Render a tagged template into an Elena Element with DOM diffing.
|
|
3
|
-
* Returns true if the DOM was fully rebuilt, false if
|
|
4
|
-
*
|
|
3
|
+
* Returns true if the DOM was fully rebuilt, false if parts were
|
|
4
|
+
* patched in place.
|
|
5
5
|
*
|
|
6
6
|
* @param {HTMLElement} element
|
|
7
7
|
* @param {TemplateStringsArray} strings - Static parts of the tagged template
|
package/dist/common/utils.d.ts
CHANGED
|
@@ -20,10 +20,9 @@ export function resolveValue(value: any): string;
|
|
|
20
20
|
*
|
|
21
21
|
* @param {TemplateStringsArray} strings
|
|
22
22
|
* @param {...*} values
|
|
23
|
-
* @returns {{
|
|
23
|
+
* @returns {{ strings: TemplateStringsArray, values: Array, toString(): string }}
|
|
24
24
|
*/
|
|
25
25
|
export function html(strings: TemplateStringsArray, ...values: any[]): {
|
|
26
|
-
__raw: true;
|
|
27
26
|
strings: TemplateStringsArray;
|
|
28
27
|
values: any[];
|
|
29
28
|
toString(): string;
|
|
@@ -32,10 +31,9 @@ export function html(strings: TemplateStringsArray, ...values: any[]): {
|
|
|
32
31
|
* Renders a string as HTML rather than text.
|
|
33
32
|
*
|
|
34
33
|
* @param {string} str - The raw HTML string to trust.
|
|
35
|
-
* @returns {{
|
|
34
|
+
* @returns {{ toString(): string }}
|
|
36
35
|
*/
|
|
37
36
|
export function unsafeHTML(str: string): {
|
|
38
|
-
__raw: true;
|
|
39
37
|
toString(): string;
|
|
40
38
|
};
|
|
41
39
|
/**
|
|
@@ -50,10 +48,9 @@ export function warn(msg: string): void;
|
|
|
50
48
|
* A placeholder you can return from a conditional expression
|
|
51
49
|
* inside a template to render nothing.
|
|
52
50
|
*
|
|
53
|
-
* @type {{
|
|
51
|
+
* @type {{ toString(): string }}
|
|
54
52
|
*/
|
|
55
53
|
export const nothing: {
|
|
56
|
-
__raw: true;
|
|
57
54
|
toString(): string;
|
|
58
55
|
};
|
|
59
56
|
export function isRaw(value: any): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/common/utils.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/common/utils.js"],"names":[],"mappings":"AAWA;;;;;GAKG;AACH,uCAHW,MAAM,2BAMhB;AASD,6CAEC;AAED;;;;;;GAMG;AACH,oCAHW,GAAC,GACC,MAAM,CAOlB;AAkCD;;;;;;;GAOG;AACH,8BAJW,oBAAoB,aACjB,GAAC,EAAA,GACF;IAAE,OAAO,EAAE,oBAAoB,CAAC;IAAC,MAAM,QAAQ;IAAC,QAAQ,IAAI,MAAM,CAAA;CAAE,CAIhF;AAED;;;;;GAKG;AACH,gCAHW,MAAM,GACJ;IAAE,QAAQ,IAAI,MAAM,CAAA;CAAE,CAIlC;AA0BD;;;;;GAKG;AACH,2CAHW,MAAM,GACJ,MAAM,CAOlB;AAhIM,0BAHI,MAAM,QAGoC;AA6FrD;;;;;GAKG;AACH,sBAFU;IAAE,QAAQ,IAAI,MAAM,CAAA;CAAE,CAE2B;AAQpD,6BAHI,GAAC,GACC,OAAO,CAE6E;AAQ1F,mCAHI,GAAC,GACC,MAAM,CAEwE;AA3H3F,qBAAe,wBAAc,CAAC;AAC9B,iDAA8B"}
|
package/dist/elena.d.ts
CHANGED
|
@@ -13,12 +13,16 @@ export type ElenaConstructor = new (...args: any[]) => HTMLElement;
|
|
|
13
13
|
export type ElenaInstanceMembers = {
|
|
14
14
|
text: string;
|
|
15
15
|
element: HTMLElement | null;
|
|
16
|
+
updateComplete: Promise<void>;
|
|
16
17
|
render(): void;
|
|
17
18
|
willUpdate(): void;
|
|
18
19
|
firstUpdated(): void;
|
|
19
20
|
updated(): void;
|
|
21
|
+
requestUpdate(): void;
|
|
20
22
|
connectedCallback(): void;
|
|
21
23
|
disconnectedCallback(): void;
|
|
24
|
+
adoptedCallback(): void;
|
|
25
|
+
attributeChangedCallback(prop: string, oldValue: string | null, newValue: string | null): void;
|
|
22
26
|
};
|
|
23
27
|
export type ElenaPropObject = {
|
|
24
28
|
name: string;
|
package/dist/elena.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"elena.d.ts","sourceRoot":"","sources":["../src/elena.js"],"names":[],"mappings":"AAkEA;;;;;;;;;GASG;AACH,kCAHW,gBAAgB,GACd,uBAAuB,
|
|
1
|
+
{"version":3,"file":"elena.d.ts","sourceRoot":"","sources":["../src/elena.js"],"names":[],"mappings":"AAkEA;;;;;;;;;GASG;AACH,kCAHW,gBAAgB,GACd,uBAAuB,CAyenC;+BA7gBY,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,WAAW;mCAInC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;IAAC,cAAc,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAAC,MAAM,IAAI,IAAI,CAAC;IAAC,UAAU,IAAI,IAAI,CAAC;IAAC,YAAY,IAAI,IAAI,CAAC;IAAC,OAAO,IAAI,IAAI,CAAC;IAAC,aAAa,IAAI,IAAI,CAAC;IAAC,iBAAiB,IAAI,IAAI,CAAC;IAAC,oBAAoB,IAAI,IAAI,CAAC;IAAC,eAAe,IAAI,IAAI,CAAC;IAAC,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAA;CAAE;8BAIhW;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE;sCAInC,CAAC,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,WAAW,GAAG,oBAAoB,CAAC,GAAG;IACvE,MAAM,IAAI,IAAI,CAAC;IACnB,QAAY,CAAC,kBAAkB,EAAE,MAAM,EAAE,CAAC;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,CAAC,MAAM,GAAG,eAAe,CAAC,EAAE,CAAC;IACrC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC3B,MAAM,CAAC,EAAE,aAAa,GAAG,MAAM,GAAG,CAAC,aAAa,GAAG,MAAM,CAAC,EAAE,CAAC;CAC9D;qBA5CmE,mBAAmB;2BAAnB,mBAAmB;wBAAnB,mBAAmB"}
|
package/dist/elena.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @elenajs/core v1.0.0-rc.
|
|
2
|
+
* @elenajs/core v1.0.0-rc.12
|
|
3
3
|
* (c) 2025-present Ariel Salminen and Elena contributors
|
|
4
4
|
* @license MIT
|
|
5
5
|
*/
|
|
6
|
-
import{
|
|
6
|
+
import{getPropValue as t,getProps as e,setProps as s,syncAttribute as i}from"./props.js";import{prefix as n,warn as r,defineElement as o}from"./utils.js";export{html,nothing,unsafeHTML}from"./utils.js";import{renderTemplate as h}from"./render.js";const a=new WeakSet,d=(t,e)=>Object.prototype.hasOwnProperty.call(t,e);function l(l){return class extends l{element=null;attributeChangedCallback(s,i,n){if(super.attributeChangedCallback?.(s,i,n),"text"!==s){if(i!==n)if(this._hydrated&&!this._isRendering){const e=this._props.get(s),i=typeof e,r="string"===i?n??"":t(i,n,"toProp");r!==e&&this._props.set(s,r),this._safeRender()}else this._syncing=!0,e(this,s,i,n),this._syncing=!1}else this.text=n??""}static get observedAttributes(){if(this._observedAttrs)return this._observedAttrs;const t=(this.props||[]).map(t=>"string"==typeof t?t:t.name);return this._observedAttrs=[...t,"text"],this._observedAttrs}connectedCallback(){super.connectedCallback?.(),this._setupStaticProps(),this._captureClassFieldDefaults(),this._hydrated||void 0!==this._text||(this.text=this.textContent.trim()),this._attachShadow(),this._root=this._shadow??this.shadowRoot??this,this._runUpdate??=()=>{try{this._performUpdate()}catch(t){console.error(n,t)}},this.willUpdate(),this._applyRender(),this._syncProps(),this._delegateEvents(),this._hydrated||(this._hydrated=!0,this.setAttribute("hydrated",""),this.firstUpdated()),this.updated()}_setupStaticProps(){const t=this.constructor;if(a.has(t))return;const e=new Set,i=[];if(t.props){for(const s of t.props)"string"==typeof s?i.push(s):(i.push(s.name),!1===s.reflect&&e.add(s.name));i.includes("text")&&r('"text" is reserved.'),s(t.prototype,i,e)}if(t._propNames=i,t._noReflect=e,t._elenaEvents=t.events||null,t._elenaEvents)for(const e of t._elenaEvents)d(t.prototype,e)||(t.prototype[e]=function(...t){return this.element[e](...t)});var n;t._resolver=(n=t.element)?t=>t.querySelector(n):t=>t.firstElementChild,a.add(t)}_captureClassFieldDefaults(){this._syncing=!0;for(const t of this.constructor._propNames)if(d(this,t)){const e=this[t];delete this[t],this[t]=e}this._syncing=!1}_attachShadow(){const t=this.constructor;if(!t.shadow)return;this._shadow||this.shadowRoot||(this._shadow=this.attachShadow({mode:t.shadow}));const e=this._shadow??this.shadowRoot;if(t.styles){if(!t._adoptedSheets){const e=[t.styles].flat();t._adoptedSheets=e.map(t=>{if("string"==typeof t){const e=new CSSStyleSheet;return e.replaceSync(t),e}return t})}e.adoptedStyleSheets=t._adoptedSheets}}_applyRender(){const t=this.constructor,e=this._root,s=this.render();if(s&&s.strings&&h(e,s.strings,s.values)){const s=this.element;if(this.element=t._resolver(e),this._events&&s&&this.element!==s){const e=t._elenaEvents;for(const t of e)s.removeEventListener(t,this),this.element.addEventListener(t,this)}}this.element||(this.element=t._resolver(e),this.element||(t.element&&r("Element not found."),this.element=e.firstElementChild))}_syncProps(){if(this._props){const e=this.constructor._noReflect;for(const[s,n]of this._props){if(e.has(s))continue;const r=t(typeof n,n,"toAttribute");(null!==r||this.hasAttribute(s))&&i(this,s,r)}}}_delegateEvents(){const t=this.constructor._elenaEvents;if(!this._events&&t?.length)if(this.element){this._events=!0;for(const e of t)this.element.addEventListener(e,this)}else r("Cannot add events.")}render(){}willUpdate(){}firstUpdated(){}updated(){}adoptedCallback(){super.adoptedCallback?.()}disconnectedCallback(){if(super.disconnectedCallback?.(),this._events){this._events=!1;for(const t of this.constructor._elenaEvents)this.element?.removeEventListener(t,this)}}handleEvent(t){this.constructor._elenaEvents?.includes(t.type)&&(t.bubbles&&(t.composed||this._root===this)||this.dispatchEvent(new Event(t.type,{bubbles:t.bubbles})))}get text(){return this._text??""}set text(t){const e=this._text;this._text=t,this._hydrated&&e!==t&&!this._isRendering&&this._safeRender()}static define(){const t=this.tagName;t?o(t,this):r("define() without a tagName.")}_safeRender(){this._isRendering||this._renderPending||(this._renderPending=!0,queueMicrotask(this._runUpdate))}_performUpdate(){this._renderPending=!1;const t=this._resolveUpdate;this._resolveUpdate=null;try{try{this.willUpdate(),this._isRendering=!0,this._applyRender()}finally{this._isRendering=!1}this.updated()}finally{this._updateComplete=null,t?.()}}get updateComplete(){return this._renderPending?(this._updateComplete||(this._updateComplete=new Promise(t=>{this._resolveUpdate=t})),this._updateComplete):Promise.resolve()}requestUpdate(){this._hydrated&&!this._isRendering&&this._safeRender()}}}export{l as Elena};
|
package/dist/render.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isArray as t,toPlainText as e,isRaw as n,
|
|
1
|
+
import{isArray as t,toPlainText as e,isRaw as n,nothing as r,collapseWhitespace as o,resolveValue as l}from"./utils.js";const s=new WeakMap,a="e"+Math.random().toString(36).slice(2),c=()=>document.createElement("template"),i=t=>document.createTreeWalker(t,128);function u(u,m,f){return!function(o,l,s){if(o._templateStrings!==l||!o._templateParts)return!1;const a=o._templateParts,c=o._templateValues;for(let o=0;o<s.length;o++){const l=s[o],i=t(l)?e(l):l;if(i===c[o])continue;if(n(l)&&l!==r)return!1;const u=a[o];if(!u)return!1;c[o]=i;const p=String(i??"");u.nodeType?u.textContent=p:u[0].setAttribute(u[1],p)}return!0}(u,m,f)&&(function(u,m,f){let h=s.get(m);if(!h){const t=m.map(o);h={_strings:t,_template:f.length>0?p(t,f.length):null},s.set(m,h)}if(h._template)u._templateParts=function(o,s,u){const{_tpl:p,_attrs:m}=s,f=p.content.cloneNode(!0),h=i(f),g=Array(u.length),_=[];let N;for(;N=h.nextNode();)N.data===a&&_.push(N);let x=0;for(let o=0;o<u.length;o++){const s=m[o];if(s){const n=f.querySelector(`[${s}="${a+"_"+o}"]`);if(n){const r=u[o],l=String((t(r)?e(r):r)??"");n.setAttribute(s,l),g[o]=[n,s]}}else{const t=_[x++],s=u[o];if(n(s)&&s!==r){const e=c();e.innerHTML=l(s),t.parentNode.replaceChild(e.content,t)}else{const n=document.createTextNode(e(s));t.parentNode.replaceChild(n,t),g[o]=n}}}return o._templateStrings?(d(o,f.childNodes),null):(o.replaceChildren(f),g)}(u,h._template,f);else{const t=f.map(l),e=h._strings.reduce((e,n,r)=>e+n+(t[r]??""),"").replace(/>\s+</g,"><").trim(),n=c();n.innerHTML=e,d(u,n.content.childNodes),u._templateParts=null}u._templateStrings=m,u._templateValues=f.map(n=>t(n)?e(n):n)}(u,m,f),!0)}function p(t,e){const n=`\x3c!--${a}--\x3e`,r=[];let o="";for(let l=0;l<t.length;l++)if(o+=t[l],l<e){const e=t[l].match(/([^\s"'>/=]+)\s*=\s*["']$/);e?(r.push(e[1]),o+=a+"_"+l):(r.push(null),o+=n)}const l=c();l.innerHTML=o.trim();const s=i(l.content);let u=0;for(;s.nextNode();)s.currentNode.data===a&&u++;return u!==r.filter(t=>null===t).length?null:{_tpl:l,_attrs:r}}function d(t,e){const n=Array.from(t.childNodes),r=Array.from(e),o=Math.max(n.length,r.length);for(let e=0;e<o;e++){const o=n[e],l=r[e];o?l?o.nodeType!==l.nodeType||1===o.nodeType&&o.tagName!==l.tagName?t.replaceChild(l,o):3===o.nodeType?o.textContent!==l.textContent&&(o.textContent=l.textContent):1===o.nodeType&&(m(o,l),d(o,l.childNodes)):t.removeChild(o):t.appendChild(l)}}function m(t,e){for(let n=t.attributes.length-1;n>=0;n--){const{name:r}=t.attributes[n];e.hasAttribute(r)||t.removeAttribute(r)}for(let n=0;n<e.attributes.length;n++){const{name:r,value:o}=e.attributes[n];t.getAttribute(r)!==o&&t.setAttribute(r,o)}}export{u as renderTemplate};
|
package/dist/utils.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const
|
|
1
|
+
const t="░█ [ELENA]: ",n=Array.isArray,r=Symbol("elena.raw"),s=n=>console.warn(t+n);function e(t,n){const r=globalThis.customElements;r?.get(t)||r?.define(t,n)}const o={"&":"&","<":"<",">":">",'"':""","'":"'"};function i(t){return String(t).replace(/[&<>"']/g,t=>o[t])}function c(t){return n(t)?t.map(u).join(""):u(t)}function u(t){return t?.[r]?String(t):i(t??"")}class l{constructor(t,n){this.strings=t,this.values=n}toString(){return null==this._str&&(this._str=this.strings.reduce((t,n,r)=>t+n+c(this.values[r]),"")),this._str}}function a(t,...n){return new l(t,n)}function g(t){return{[r]:!0,toString:()=>t??""}}l.prototype[r]=!0;const p={[r]:!0,toString:()=>""},f=t=>n(t)?t.some(t=>t?.[r]):!!t?.[r],h=t=>n(t)?t.join(""):String(t??"");function S(t){return t.replace(/(>)\n\s*|\n\s*(<)/g,"$1$2").replace(/\n\s*/g," ").replace(/>\s+</g,"><")}export{S as collapseWhitespace,e as defineElement,i as escapeHtml,a as html,n as isArray,f as isRaw,p as nothing,t as prefix,c as resolveValue,h as toPlainText,g as unsafeHTML,s as warn};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elenajs/core",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.12",
|
|
4
4
|
"description": "Elena is a simple, tiny library for building Progressive Web Components.",
|
|
5
5
|
"author": "Elena <hi@elenajs.com>",
|
|
6
6
|
"homepage": "https://elenajs.com/",
|
|
@@ -56,5 +56,5 @@
|
|
|
56
56
|
"typescript": "6.0.2",
|
|
57
57
|
"vitest": "4.1.1"
|
|
58
58
|
},
|
|
59
|
-
"gitHead": "
|
|
59
|
+
"gitHead": "6e8de235d2bb9de8028c920b4213dc40505b4ef4"
|
|
60
60
|
}
|
package/src/common/props.js
CHANGED
|
@@ -68,7 +68,7 @@ export function syncAttribute(element, name, value) {
|
|
|
68
68
|
* at class-creation time. Values are stored per-instance
|
|
69
69
|
* via a `_props` Map that is lazily created.
|
|
70
70
|
*
|
|
71
|
-
* @param {
|
|
71
|
+
* @param {Object} proto - The class prototype
|
|
72
72
|
* @param {string[]} propNames - Prop names to define
|
|
73
73
|
* @param {Set<string>} [noReflect] - Props that should not reflect to attributes
|
|
74
74
|
*/
|
package/src/common/render.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { collapseWhitespace, isArray, isRaw, resolveValue, toPlainText } from "./utils.js";
|
|
1
|
+
import { collapseWhitespace, isArray, isRaw, nothing, resolveValue, toPlainText } from "./utils.js";
|
|
2
2
|
|
|
3
3
|
const stringsCache = new WeakMap();
|
|
4
|
-
const markerKey = "e" +
|
|
4
|
+
const markerKey = "e" + Math.random().toString(36).slice(2);
|
|
5
5
|
const SHOW_COMMENT = 128;
|
|
6
6
|
const ELEMENT_NODE = 1;
|
|
7
7
|
const TEXT_NODE = 3;
|
|
@@ -11,8 +11,8 @@ const treeWalker = node => document.createTreeWalker(node, SHOW_COMMENT);
|
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Render a tagged template into an Elena Element with DOM diffing.
|
|
14
|
-
* Returns true if the DOM was fully rebuilt, false if
|
|
15
|
-
*
|
|
14
|
+
* Returns true if the DOM was fully rebuilt, false if parts were
|
|
15
|
+
* patched in place.
|
|
16
16
|
*
|
|
17
17
|
* @param {HTMLElement} element
|
|
18
18
|
* @param {TemplateStringsArray} strings - Static parts of the tagged template
|
|
@@ -20,54 +20,69 @@ const treeWalker = node => document.createTreeWalker(node, SHOW_COMMENT);
|
|
|
20
20
|
* @returns {boolean}
|
|
21
21
|
*/
|
|
22
22
|
export function renderTemplate(element, strings, values) {
|
|
23
|
-
if (
|
|
23
|
+
if (patch(element, strings, values)) {
|
|
24
24
|
return false;
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
morph(element, strings, values);
|
|
27
27
|
return true;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
31
|
+
* Patch only changed text nodes and attribute values.
|
|
32
32
|
*
|
|
33
33
|
* @param {HTMLElement} element - The host element with cached template state
|
|
34
34
|
* @param {TemplateStringsArray} strings - Static parts of the tagged template
|
|
35
35
|
* @param {Array} values - Dynamic interpolated values
|
|
36
|
-
* @returns {boolean} Whether patching was sufficient (false =
|
|
36
|
+
* @returns {boolean} Whether patching was sufficient (false = do morph instead)
|
|
37
37
|
*/
|
|
38
|
-
function
|
|
38
|
+
function patch(element, strings, values) {
|
|
39
39
|
// Only works when re-rendering the same template shape
|
|
40
40
|
if (element._templateStrings !== strings || !element._templateParts) {
|
|
41
41
|
return false;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
const parts = element._templateParts;
|
|
45
|
+
const cached = element._templateValues;
|
|
46
|
+
|
|
44
47
|
for (let i = 0; i < values.length; i++) {
|
|
45
48
|
const v = values[i];
|
|
46
49
|
const comparable = isArray(v) ? toPlainText(v) : v;
|
|
47
50
|
|
|
48
|
-
if (comparable ===
|
|
51
|
+
if (comparable === cached[i]) {
|
|
49
52
|
continue;
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
if (isRaw(v)
|
|
55
|
+
if (isRaw(v) && v !== nothing) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const part = parts[i];
|
|
60
|
+
|
|
61
|
+
if (!part) {
|
|
53
62
|
return false;
|
|
54
63
|
}
|
|
55
64
|
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
cached[i] = comparable;
|
|
66
|
+
const str = String(comparable ?? "");
|
|
67
|
+
|
|
68
|
+
if (part.nodeType) {
|
|
69
|
+
part.textContent = str;
|
|
70
|
+
} else {
|
|
71
|
+
part[0].setAttribute(part[1], str);
|
|
72
|
+
}
|
|
58
73
|
}
|
|
59
74
|
|
|
60
75
|
return true;
|
|
61
76
|
}
|
|
62
77
|
|
|
63
78
|
/**
|
|
64
|
-
*
|
|
79
|
+
* Clone a cached <template> and morph in new structure.
|
|
65
80
|
*
|
|
66
81
|
* @param {HTMLElement} element - The host element to render into
|
|
67
82
|
* @param {TemplateStringsArray} strings - Static parts of the tagged template
|
|
68
83
|
* @param {Array} values - Dynamic interpolated values
|
|
69
84
|
*/
|
|
70
|
-
function
|
|
85
|
+
function morph(element, strings, values) {
|
|
71
86
|
let entry = stringsCache.get(strings);
|
|
72
87
|
|
|
73
88
|
if (!entry) {
|
|
@@ -82,7 +97,7 @@ function fullRender(element, strings, values) {
|
|
|
82
97
|
if (entry._template) {
|
|
83
98
|
element._templateParts = cloneAndPatch(element, entry._template, values);
|
|
84
99
|
} else {
|
|
85
|
-
// Fallback for
|
|
100
|
+
// Fallback for static templates or templates where marker detection failed.
|
|
86
101
|
// White space collapsing here protects against Vue SSR mismatches.
|
|
87
102
|
const renderedValues = values.map(resolveValue);
|
|
88
103
|
const markup = entry._strings
|
|
@@ -102,45 +117,66 @@ function fullRender(element, strings, values) {
|
|
|
102
117
|
}
|
|
103
118
|
|
|
104
119
|
/**
|
|
105
|
-
*
|
|
120
|
+
* Create a <template> element with comment markers and string placeholders.
|
|
106
121
|
*
|
|
107
122
|
* @param {string[]} _strings - Whitespace-collapsed static parts
|
|
108
123
|
* @param {number} valueCount - Number of dynamic values
|
|
109
|
-
* @returns {HTMLTemplateElement | null}
|
|
124
|
+
* @returns {{ _tpl: HTMLTemplateElement, _attrs: (string|null)[] } | null}
|
|
110
125
|
*/
|
|
111
126
|
function createTemplate(_strings, valueCount) {
|
|
112
127
|
const marker = `<!--${markerKey}-->`;
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
128
|
+
const attrs = [];
|
|
129
|
+
let markup = "";
|
|
130
|
+
|
|
131
|
+
for (let i = 0; i < _strings.length; i++) {
|
|
132
|
+
markup += _strings[i];
|
|
133
|
+
|
|
134
|
+
if (i < valueCount) {
|
|
135
|
+
const match = _strings[i].match(/([^\s"'>/=]+)\s*=\s*["']$/);
|
|
136
|
+
|
|
137
|
+
if (match) {
|
|
138
|
+
attrs.push(match[1]);
|
|
139
|
+
markup += markerKey + "_" + i;
|
|
140
|
+
} else {
|
|
141
|
+
attrs.push(null);
|
|
142
|
+
markup += marker;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
116
146
|
|
|
117
147
|
const template = newTemplate();
|
|
118
|
-
template.innerHTML = markup;
|
|
148
|
+
template.innerHTML = markup.trim();
|
|
119
149
|
|
|
120
150
|
// Mismatch means this template shape cannot use the clone path.
|
|
121
151
|
const walker = treeWalker(template.content);
|
|
122
|
-
let
|
|
152
|
+
let commentCount = 0;
|
|
123
153
|
|
|
124
154
|
while (walker.nextNode()) {
|
|
125
155
|
if (walker.currentNode.data === markerKey) {
|
|
126
|
-
|
|
156
|
+
commentCount++;
|
|
127
157
|
}
|
|
128
158
|
}
|
|
129
159
|
|
|
130
|
-
|
|
160
|
+
const expectedComments = attrs.filter(n => n === null).length;
|
|
161
|
+
|
|
162
|
+
if (commentCount !== expectedComments) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { _tpl: template, _attrs: attrs };
|
|
131
167
|
}
|
|
132
168
|
|
|
133
169
|
/**
|
|
134
|
-
* Clone a cached template and replace
|
|
135
|
-
* with actual content.
|
|
170
|
+
* Clone a cached template and replace markers with actual content.
|
|
136
171
|
*
|
|
137
172
|
* @param {HTMLElement} element - The host element to render into
|
|
138
|
-
* @param {
|
|
173
|
+
* @param {{ _tpl: HTMLTemplateElement, _attrs: (string|null)[] }} templateInfo
|
|
139
174
|
* @param {Array} values - Raw interpolated values
|
|
140
|
-
* @returns {Array<Text |
|
|
175
|
+
* @returns {Array<Text | [Element, string] | undefined> | null}
|
|
141
176
|
*/
|
|
142
|
-
function cloneAndPatch(element,
|
|
143
|
-
const
|
|
177
|
+
function cloneAndPatch(element, templateInfo, values) {
|
|
178
|
+
const { _tpl, _attrs } = templateInfo;
|
|
179
|
+
const clone = _tpl.content.cloneNode(true);
|
|
144
180
|
const walker = treeWalker(clone);
|
|
145
181
|
const parts = Array(values.length);
|
|
146
182
|
const markers = [];
|
|
@@ -153,24 +189,46 @@ function cloneAndPatch(element, template, values) {
|
|
|
153
189
|
}
|
|
154
190
|
}
|
|
155
191
|
|
|
156
|
-
|
|
157
|
-
const value = values[i];
|
|
192
|
+
let contentIdx = 0;
|
|
158
193
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
194
|
+
for (let i = 0; i < values.length; i++) {
|
|
195
|
+
const attr = _attrs[i];
|
|
196
|
+
|
|
197
|
+
if (attr) {
|
|
198
|
+
// Find the element with the placeholder value
|
|
199
|
+
const placeholder = markerKey + "_" + i;
|
|
200
|
+
const el = clone.querySelector(`[${attr}="${placeholder}"]`);
|
|
201
|
+
|
|
202
|
+
if (el) {
|
|
203
|
+
const value = values[i];
|
|
204
|
+
const str = String((isArray(value) ? toPlainText(value) : value) ?? "");
|
|
205
|
+
el.setAttribute(attr, str);
|
|
206
|
+
parts[i] = [el, attr];
|
|
207
|
+
}
|
|
166
208
|
} else {
|
|
167
|
-
//
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
209
|
+
// Replace comment marker with value
|
|
210
|
+
const marker = markers[contentIdx++];
|
|
211
|
+
const value = values[i];
|
|
212
|
+
|
|
213
|
+
// Parse and insert raw HTML as a fragment
|
|
214
|
+
if (isRaw(value) && value !== nothing) {
|
|
215
|
+
const tmp = newTemplate();
|
|
216
|
+
tmp.innerHTML = resolveValue(value);
|
|
217
|
+
marker.parentNode.replaceChild(tmp.content, marker);
|
|
218
|
+
|
|
219
|
+
// Create text node with unescaped content
|
|
220
|
+
} else {
|
|
221
|
+
const textNode = document.createTextNode(toPlainText(value));
|
|
222
|
+
marker.parentNode.replaceChild(textNode, marker);
|
|
223
|
+
parts[i] = textNode;
|
|
224
|
+
}
|
|
171
225
|
}
|
|
172
226
|
}
|
|
173
227
|
|
|
228
|
+
if (element._templateStrings) {
|
|
229
|
+
morphContent(element, clone.childNodes);
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
174
232
|
element.replaceChildren(clone);
|
|
175
233
|
return parts;
|
|
176
234
|
}
|
|
@@ -212,7 +270,7 @@ function morphContent(parent, nextNodes) {
|
|
|
212
270
|
}
|
|
213
271
|
|
|
214
272
|
/**
|
|
215
|
-
*
|
|
273
|
+
* Morph element’s attributes without rebuilding the DOM.
|
|
216
274
|
*
|
|
217
275
|
* @param {Element} current - The current existing DOM element
|
|
218
276
|
* @param {Element} next - The desired element from the new render
|
package/src/common/utils.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const prefix = "░█ [ELENA]: ";
|
|
2
2
|
const isArray = Array.isArray;
|
|
3
|
+
const RAW = Symbol("elena.raw");
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* @param {string} msg
|
|
@@ -52,51 +53,59 @@ export function resolveValue(value) {
|
|
|
52
53
|
* @returns {string}
|
|
53
54
|
*/
|
|
54
55
|
function resolveItem(value) {
|
|
55
|
-
return value?.
|
|
56
|
+
return value?.[RAW] ? String(value) : escapeHtml(value ?? "");
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Lightweight template result.
|
|
61
|
+
*
|
|
62
|
+
* @internal
|
|
63
|
+
*/
|
|
64
|
+
class HtmlResult {
|
|
65
|
+
constructor(strings, values) {
|
|
66
|
+
this.strings = strings;
|
|
67
|
+
this.values = values;
|
|
68
|
+
}
|
|
69
|
+
toString() {
|
|
70
|
+
if (this._str == null) {
|
|
71
|
+
this._str = this.strings.reduce((acc, s, i) => {
|
|
72
|
+
return acc + s + resolveValue(this.values[i]);
|
|
73
|
+
}, "");
|
|
74
|
+
}
|
|
75
|
+
return this._str;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
HtmlResult.prototype[RAW] = true;
|
|
79
|
+
|
|
58
80
|
/**
|
|
59
81
|
* Tagged template for trusted HTML. Use as the return value
|
|
60
82
|
* of render(), or for sub-fragments inside render methods.
|
|
61
83
|
*
|
|
62
84
|
* @param {TemplateStringsArray} strings
|
|
63
85
|
* @param {...*} values
|
|
64
|
-
* @returns {{
|
|
86
|
+
* @returns {{ strings: TemplateStringsArray, values: Array, toString(): string }}
|
|
65
87
|
*/
|
|
66
88
|
export function html(strings, ...values) {
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
__raw: true,
|
|
70
|
-
strings,
|
|
71
|
-
values,
|
|
72
|
-
toString: () => {
|
|
73
|
-
if (str == null) {
|
|
74
|
-
str = strings.reduce((acc, s, i) => {
|
|
75
|
-
return acc + s + resolveValue(values[i]);
|
|
76
|
-
}, "");
|
|
77
|
-
}
|
|
78
|
-
return str;
|
|
79
|
-
},
|
|
80
|
-
};
|
|
89
|
+
return new HtmlResult(strings, values);
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
/**
|
|
84
93
|
* Renders a string as HTML rather than text.
|
|
85
94
|
*
|
|
86
95
|
* @param {string} str - The raw HTML string to trust.
|
|
87
|
-
* @returns {{
|
|
96
|
+
* @returns {{ toString(): string }}
|
|
88
97
|
*/
|
|
89
98
|
export function unsafeHTML(str) {
|
|
90
|
-
return {
|
|
99
|
+
return { [RAW]: true, toString: () => str ?? "" };
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
/**
|
|
94
103
|
* A placeholder you can return from a conditional expression
|
|
95
104
|
* inside a template to render nothing.
|
|
96
105
|
*
|
|
97
|
-
* @type {{
|
|
106
|
+
* @type {{ toString(): string }}
|
|
98
107
|
*/
|
|
99
|
-
export const nothing = {
|
|
108
|
+
export const nothing = { [RAW]: true, toString: () => "" };
|
|
100
109
|
|
|
101
110
|
/**
|
|
102
111
|
* Check if a value contains trusted HTML fragments.
|
|
@@ -104,7 +113,7 @@ export const nothing = { __raw: true, toString: () => "" };
|
|
|
104
113
|
* @param {*} value
|
|
105
114
|
* @returns {boolean}
|
|
106
115
|
*/
|
|
107
|
-
export const isRaw = value => (isArray(value) ? value.some(item => item?.
|
|
116
|
+
export const isRaw = value => (isArray(value) ? value.some(item => item?.[RAW]) : !!value?.[RAW]);
|
|
108
117
|
|
|
109
118
|
/**
|
|
110
119
|
* Convert a value to its plain text string.
|
package/src/elena.js
CHANGED
|
@@ -40,7 +40,7 @@ function elementResolver(selector) {
|
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* @typedef {{ text: string, element: HTMLElement | null, render(): void, willUpdate(): void, firstUpdated(): void, updated(): void, connectedCallback(): void, disconnectedCallback(): void }} ElenaInstanceMembers
|
|
43
|
+
* @typedef {{ text: string, element: HTMLElement | null, updateComplete: Promise<void>, render(): void, willUpdate(): void, firstUpdated(): void, updated(): void, requestUpdate(): void, connectedCallback(): void, disconnectedCallback(): void, adoptedCallback(): void, attributeChangedCallback(prop: string, oldValue: string | null, newValue: string | null): void }} ElenaInstanceMembers
|
|
44
44
|
*/
|
|
45
45
|
|
|
46
46
|
/**
|
|
@@ -91,8 +91,8 @@ export function Elena(superClass) {
|
|
|
91
91
|
* Updates the matching prop and re-renders if needed.
|
|
92
92
|
*
|
|
93
93
|
* @param {string} prop
|
|
94
|
-
* @param {string} oldValue
|
|
95
|
-
* @param {string} newValue
|
|
94
|
+
* @param {string | null} oldValue
|
|
95
|
+
* @param {string | null} newValue
|
|
96
96
|
*/
|
|
97
97
|
attributeChangedCallback(prop, oldValue, newValue) {
|
|
98
98
|
super.attributeChangedCallback?.(prop, oldValue, newValue);
|
|
@@ -102,17 +102,29 @@ export function Elena(superClass) {
|
|
|
102
102
|
return;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
getProps(this, prop, oldValue, newValue);
|
|
109
|
-
this._syncing = false;
|
|
105
|
+
if (oldValue === newValue) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
110
108
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
109
|
+
if (this._hydrated && !this._isRendering) {
|
|
110
|
+
// The attribute is already set and we just need the coerced
|
|
111
|
+
// prop value stored for the next render.
|
|
112
|
+
const current = this._props.get(prop);
|
|
113
|
+
const type = typeof current;
|
|
114
|
+
const coerced =
|
|
115
|
+
type === "string" ? (newValue ?? "") : getPropValue(type, newValue, "toProp");
|
|
116
|
+
|
|
117
|
+
if (coerced !== current) {
|
|
118
|
+
this._props.set(prop, coerced);
|
|
119
|
+
}
|
|
115
120
|
this._safeRender();
|
|
121
|
+
|
|
122
|
+
// Runs pre-hydration or during render.
|
|
123
|
+
// Goes through the setter so _props is initialized correctly.
|
|
124
|
+
} else {
|
|
125
|
+
this._syncing = true;
|
|
126
|
+
getProps(this, prop, oldValue, newValue);
|
|
127
|
+
this._syncing = false;
|
|
116
128
|
}
|
|
117
129
|
}
|
|
118
130
|
|
|
@@ -141,6 +153,16 @@ export function Elena(superClass) {
|
|
|
141
153
|
this.text = this.textContent.trim();
|
|
142
154
|
}
|
|
143
155
|
this._attachShadow();
|
|
156
|
+
this._root = this._shadow ?? this.shadowRoot ?? this;
|
|
157
|
+
|
|
158
|
+
this._runUpdate ??= () => {
|
|
159
|
+
try {
|
|
160
|
+
this._performUpdate();
|
|
161
|
+
} catch (e) {
|
|
162
|
+
console.error(prefix, e);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
144
166
|
this.willUpdate();
|
|
145
167
|
this._applyRender();
|
|
146
168
|
this._syncProps();
|
|
@@ -228,16 +250,6 @@ export function Elena(superClass) {
|
|
|
228
250
|
this._syncing = false;
|
|
229
251
|
}
|
|
230
252
|
|
|
231
|
-
/**
|
|
232
|
-
* The root node to render into. Returns the shadow root when shadow mode
|
|
233
|
-
* is enabled, otherwise the host element itself.
|
|
234
|
-
*
|
|
235
|
-
* @type {ShadowRoot | HTMLElement}
|
|
236
|
-
*/
|
|
237
|
-
get _renderRoot() {
|
|
238
|
-
return this._shadow ?? this.shadowRoot ?? this;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
253
|
/**
|
|
242
254
|
* Attaches a shadow root and adopts styles on first connect.
|
|
243
255
|
* Only runs when `static shadow` is set on the component class.
|
|
@@ -290,14 +302,14 @@ export function Elena(superClass) {
|
|
|
290
302
|
*/
|
|
291
303
|
_applyRender() {
|
|
292
304
|
const constructor = this.constructor;
|
|
293
|
-
const root = this.
|
|
305
|
+
const root = this._root;
|
|
294
306
|
const result = this.render();
|
|
295
307
|
|
|
296
308
|
if (result && result.strings) {
|
|
297
309
|
const rebuilt = renderTemplate(root, result.strings, result.values);
|
|
298
310
|
|
|
299
311
|
// Re-resolve element ref when the DOM was fully rebuilt.
|
|
300
|
-
//
|
|
312
|
+
// patch() and morph() leave the DOM structure intact,
|
|
301
313
|
// so the existing ref is still valid.
|
|
302
314
|
if (rebuilt) {
|
|
303
315
|
const oldElement = this.element;
|
|
@@ -430,6 +442,7 @@ export function Elena(superClass) {
|
|
|
430
442
|
* events in Shadow DOM (change, submit, reset).
|
|
431
443
|
* Composed bubbling events (click, input) pass through on their own.
|
|
432
444
|
*
|
|
445
|
+
* @param {Event} event
|
|
433
446
|
* @internal
|
|
434
447
|
*/
|
|
435
448
|
handleEvent(event) {
|
|
@@ -437,7 +450,7 @@ export function Elena(superClass) {
|
|
|
437
450
|
return;
|
|
438
451
|
}
|
|
439
452
|
|
|
440
|
-
if (!event.bubbles || (!event.composed && this.
|
|
453
|
+
if (!event.bubbles || (!event.composed && this._root !== this)) {
|
|
441
454
|
/** @internal */
|
|
442
455
|
this.dispatchEvent(new Event(event.type, { bubbles: event.bubbles }));
|
|
443
456
|
}
|
|
@@ -489,16 +502,7 @@ export function Elena(superClass) {
|
|
|
489
502
|
}
|
|
490
503
|
if (!this._renderPending) {
|
|
491
504
|
this._renderPending = true;
|
|
492
|
-
this.
|
|
493
|
-
this._resolveUpdate = resolve;
|
|
494
|
-
});
|
|
495
|
-
queueMicrotask(() => {
|
|
496
|
-
try {
|
|
497
|
-
this._performUpdate();
|
|
498
|
-
} catch (e) {
|
|
499
|
-
console.error(prefix, e);
|
|
500
|
-
}
|
|
501
|
-
});
|
|
505
|
+
queueMicrotask(this._runUpdate);
|
|
502
506
|
}
|
|
503
507
|
}
|
|
504
508
|
|
|
@@ -523,7 +527,7 @@ export function Elena(superClass) {
|
|
|
523
527
|
this.updated();
|
|
524
528
|
} finally {
|
|
525
529
|
this._updateComplete = null;
|
|
526
|
-
resolve();
|
|
530
|
+
resolve?.();
|
|
527
531
|
}
|
|
528
532
|
}
|
|
529
533
|
|
|
@@ -534,7 +538,15 @@ export function Elena(superClass) {
|
|
|
534
538
|
* @type {Promise<void>}
|
|
535
539
|
*/
|
|
536
540
|
get updateComplete() {
|
|
537
|
-
|
|
541
|
+
if (!this._renderPending) {
|
|
542
|
+
return Promise.resolve();
|
|
543
|
+
}
|
|
544
|
+
if (!this._updateComplete) {
|
|
545
|
+
this._updateComplete = new Promise(resolve => {
|
|
546
|
+
this._resolveUpdate = resolve;
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
return this._updateComplete;
|
|
538
550
|
}
|
|
539
551
|
|
|
540
552
|
/**
|