decue 1.0.0 → 1.0.1

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/dist/decue.js CHANGED
@@ -195,64 +195,67 @@ var decue = (function() {
195
195
  throw `Invalid shadow DOM mode "${this._shadow}" for element "${elementName}". Must be one of "open", "closed" or "none".`;
196
196
  }
197
197
 
198
- const root = this._shadow === 'none' ? /** @type {HTMLElement} */ (this) : this.attachShadow({ mode: this._shadow, delegatesFocus: true });
199
- const content = /** @type {DocumentFragment} */ (this._template.content.cloneNode(true));
200
-
201
- const finalize = () => {
202
- dbg('Finalizing...');
198
+ if (!this._shadowRoot) {
199
+ this._shadowRoot = this._shadow === 'none' ? /** @type {HTMLElement} */ (this) : this.attachShadow({ mode: this._shadow, delegatesFocus: true });
200
+
201
+ const finalize = () => {
202
+ dbg('Finalizing...');
203
203
 
204
- if (this._shadow === 'none') {
205
- // Implement slotting manually when no shadow DOM in use
206
- lightDOMSlotting(root, content);
207
- dbg('Slots initialized');
208
- }
204
+ const content = /** @type {DocumentFragment} */ (this._template.content.cloneNode(true));
209
205
 
210
- // nodes having placeholder references, and thus need to be updated when attributes change.
211
- /** @type {[Node,string,string][]} */
212
- this._boundNodes = [];
213
-
214
- // Find all placeholders in the attributes and text of template or element content.
215
- [content, this].forEach(x =>
216
- forEachPlaceholder(x, (node,attributeName,_) => this._boundNodes.push(node instanceof HTMLElement
217
- ? [node, node.getAttribute(attributeName), attributeName]
218
- : [node, node.data, undefined])));
206
+ if (this._shadow === 'none') {
207
+ // Implement slotting manually when no shadow DOM in use
208
+ lightDOMSlotting(this._shadowRoot, content);
209
+ dbg('Slots initialized');
210
+ }
211
+
212
+ // nodes having placeholder references, and thus need to be updated when attributes change.
213
+ /** @type {[Node,string,string][]} */
214
+ this._boundNodes = [];
215
+
216
+ // Find all placeholders in the attributes and text of template or element content.
217
+ [content, this].forEach(x =>
218
+ forEachPlaceholder(x, (node,attributeName,_) => this._boundNodes.push(node instanceof HTMLElement
219
+ ? [node, node.getAttribute(attributeName), attributeName]
220
+ : [node, node.data, undefined])));
219
221
 
220
- const ths = this;
221
- // If there are attributes, which weren't yet observed statically, observe them dynamically with a MutationObserver.
222
- const unobservedAttributes = this.getAttributeNames().filter(isNotBuiltinAttribute).filter(x => !observedAttrs.includes(x));
223
- if (unobservedAttributes.length > 0 && this._boundNodes.length > 0) {
224
- new MutationObserver(recs => recs.forEach(rec => {
225
- if (unobservedAttributes.includes(rec.attributeName)) {
226
- ths.attributeChangedCallback(rec.attributeName, rec.oldValue, (/** @type {HTMLElement} */ (rec.target)).getAttribute(rec.attributeName));
222
+ const ths = this;
223
+ // If there are attributes, which weren't yet observed statically, observe them dynamically with a MutationObserver.
224
+ const unobservedAttributes = this.getAttributeNames().filter(isNotBuiltinAttribute).filter(x => !observedAttrs.includes(x));
225
+ if (unobservedAttributes.length > 0 && this._boundNodes.length > 0) {
226
+ new MutationObserver(recs => recs.forEach(rec => {
227
+ if (unobservedAttributes.includes(rec.attributeName)) {
228
+ ths.attributeChangedCallback(rec.attributeName, rec.oldValue, (/** @type {HTMLElement} */ (rec.target)).getAttribute(rec.attributeName));
229
+ }
230
+ })).observe(this, { attributes: true });
231
+ this._decueMutationObserved = true;
232
+ if (debug) {
233
+ dbg('Observing attributes with MutationObserver: ' + unobservedAttributes.join(' '));
234
+ this.setAttribute('data-decue-mutation-observed-attributes', unobservedAttributes.join(' '));
227
235
  }
228
- })).observe(this, { attributes: true });
229
- this._decueMutationObserved = true;
230
- if (debug) {
231
- dbg('Observing attributes with MutationObserver: ' + unobservedAttributes.join(' '));
232
- this.setAttribute('data-decue-mutation-observed-attributes', unobservedAttributes.join(' '));
233
236
  }
234
- }
235
- if (debug && observedAttrs.length > 0) {
236
- dbg('Observing attributes with observedAttributes: ' + observedAttrs.join(' '));
237
- this.setAttribute('data-decue-observed-attributes', observedAttrs.join(' '));
238
- }
239
-
240
- root.append(content);
241
- this._boundNodes.forEach(x => updatePlaceholders(ths, x));
237
+ if (debug && observedAttrs.length > 0) {
238
+ dbg('Observing attributes with observedAttributes: ' + observedAttrs.join(' '));
239
+ this.setAttribute('data-decue-observed-attributes', observedAttrs.join(' '));
240
+ }
241
+
242
+ this._shadowRoot.append(content);
243
+ this._boundNodes.forEach(x => updatePlaceholders(ths, x));
242
244
 
243
- fireEvent(this, 'connect');
244
- };
245
+ fireEvent(this, 'connect');
246
+ };
245
247
 
246
- if (template) {
247
- finalize();
248
- } else {
249
- // predefined element. Finalize only after the children are parsed.
250
- /** @type {MutationObserver} */
251
- const observer = new MutationObserver(() => {
252
- observer.disconnect();
248
+ if (template) {
253
249
  finalize();
254
- });
255
- observer.observe(this.parentElement, { childList: true });
250
+ } else {
251
+ // predefined element. Finalize only after the children are parsed.
252
+ /** @type {MutationObserver} */
253
+ const observer = new MutationObserver(() => {
254
+ observer.disconnect();
255
+ finalize();
256
+ });
257
+ observer.observe(this.parentElement, { childList: true });
258
+ }
256
259
  }
257
260
  }
258
261
 
@@ -321,7 +324,9 @@ var decue = (function() {
321
324
  .filter(obj => obj.getAttribute('type') === 'text/html')
322
325
  .forEach(obj => {
323
326
  obj.addEventListener('load', () => [...obj.contentDocument.getElementsByTagName('template')].forEach(processTemplate));
324
- [...obj.contentDocument.getElementsByTagName('template')].forEach(processTemplate);
327
+ if (obj.contentDocument) {
328
+ [...obj.contentDocument.getElementsByTagName('template')].forEach(processTemplate);
329
+ }
325
330
  });
326
331
  });
327
332
 
package/dist/decue.min.js CHANGED
@@ -1 +1 @@
1
- var decue=function(){const d=document.currentScript.getAttribute("shadow")||"none";if(!["open","closed","none"].includes(d)){throw`Invalid default shadow DOM mode "${d}". Must be one of "open", "closed" or "none".`}const a=/{([a-zA-Z_0-9]+)((?:\|[.]?[a-zA-Z_0-9]+)*)}/g;const c=t=>!(t==="shadow"||t.startsWith("decue-"));const u=t=>{var e=t;while(e&&e.parentNode){if(e.parentNode===document){return true}else if(e.parentNode instanceof ShadowRoot){e=e.parentNode.host}else{e=e.parentNode}}return false};const n=o=>(t,e)=>{const s=e.startsWith(".")?e.substring(1):undefined;const i=window[e];if(!s&&!i){throw`Global function "${e}" not found. Make sure to include it (not deferred) before this element is created.`}return s?typeof t[s]==="function"?t[s]():t[s]:i.apply(o,[t])};const r=(i,o,t)=>t.replaceAll(a,(t,e,s)=>o.hasAttribute(e)?s.split("|").slice(1).reduce(n(i),o.getAttribute(e)):t);const h=(t,[e,s,i])=>{const o=r(e,t,s);if(e instanceof Text){if(o!==e.data){e.data=o}}else if(e instanceof HTMLElement){if(o!==e.getAttribute(i)){e.setAttribute(i,o)}}};const f=(t,s)=>{const e=document.createTreeWalker(t,NodeFilter.SHOW_TEXT|NodeFilter.SHOW_ELEMENT);while(e.nextNode()){const i=e.currentNode;if(i instanceof Text){[...i.data.matchAll(a)].forEach(t=>s(i,undefined,t[1]))}else if(i instanceof HTMLElement){i.getAttributeNames().forEach(e=>[...i.getAttribute(e).matchAll(a)].forEach(t=>s(i,e,t[1])))}}};const b=(i,t)=>{t.querySelectorAll("slot[name]").forEach(t=>{const e=i.querySelectorAll(`:scope > [slot="${t.getAttribute("name")}"]`);const s=e.length>0?e:t.children;t.replaceWith.apply(t,s)});const e=t.querySelector("slot:not([name])");if(e){const s=[...i.childNodes].filter(t=>t.nodeType!==Node.ELEMENT_NODE||!t.hasAttribute("slot"));const o=s.length>0?s:e.children;e.replaceWith.apply(e,o)}};const i=(o,a,n,t,e)=>{const r=t=>o?console.log(a+": "+t):undefined;var l=e;if(n){f(n.content,(t,e,s)=>l.push(s))}l=[...new Set(l.filter(c))];window.customElements.define(a,class extends HTMLElement{static formAssociated=t;static observedAttributes=l;constructor(){super();this._template=n||[...document.getElementsByTagName("template")].find(t=>t.getAttribute("decue")===a);if(!this._template){throw`Template for "${a}" not found. Make sure it comes in the DOM before any corresponding custom element.`}if(!t&&this._template.hasAttribute("formAssociated")){throw`Cannot declare a predefined custom element "${a}" as formAssociated. Move it from elements="..." to formAssociated="...".`}const i=this;this.getAttributeNames().filter(t=>t.startsWith("decue-on:")).map(t=>t.substring("decue-on:".length)).forEach(t=>{const e=i.getAttribute("decue-on:"+t);const s=window[e];if(!s){throw`Global handler function "${e}" for event "${t}" not found.`}r("Registering event listener for event: "+t);i.addEventListener(t,s)});if(t){const o=this.attachInternals();var s=this.getAttribute("value");r("Making form-associated with value: "+s);Object.defineProperties(this,{internals:{value:o,writable:false},value:{get:()=>s,set:t=>{s=t;o.setFormValue(s);i.checkValidity()}},name:{get:()=>this.getAttribute("name")},form:{get:()=>o.form},labels:{get:()=>o.labels},validity:{get:()=>o.validity},validationMessage:{get:()=>o.validationMessage},willValidate:{get:()=>o.willValidate},setFormValue:{value:(t,e)=>o.setFormValue(s=t,e),writable:false},setValidity:{value:o.setValidity.bind(o),writable:false},checkValidity:{value:()=>{m(i,"checkvalidity");return o.checkValidity()},writable:false},reportValidity:{value:o.reportValidity.bind(o),writable:false}});this.value=s;if(!this.hasAttribute("tabindex")){this.tabIndex=0}}}connectedCallback(){this._shadow=this.getAttribute("shadow")||this._template.getAttribute("shadow")||d;if(!["open","closed","none"].includes(this._shadow)){throw`Invalid shadow DOM mode "${this._shadow}" for element "${a}". Must be one of "open", "closed" or "none".`}const t=this._shadow==="none"?this:this.attachShadow({mode:this._shadow,delegatesFocus:true});const i=this._template.content.cloneNode(true);const e=()=>{r("Finalizing...");if(this._shadow==="none"){b(t,i);r("Slots initialized")}this._boundNodes=[];[i,this].forEach(t=>f(t,(t,e,s)=>this._boundNodes.push(t instanceof HTMLElement?[t,t.getAttribute(e),e]:[t,t.data,undefined])));const e=this;const s=this.getAttributeNames().filter(c).filter(t=>!l.includes(t));if(s.length>0&&this._boundNodes.length>0){new MutationObserver(t=>t.forEach(t=>{if(s.includes(t.attributeName)){e.attributeChangedCallback(t.attributeName,t.oldValue,t.target.getAttribute(t.attributeName))}})).observe(this,{attributes:true});this._decueMutationObserved=true;if(o){r("Observing attributes with MutationObserver: "+s.join(" "));this.setAttribute("data-decue-mutation-observed-attributes",s.join(" "))}}if(o&&l.length>0){r("Observing attributes with observedAttributes: "+l.join(" "));this.setAttribute("data-decue-observed-attributes",l.join(" "))}t.append(i);this._boundNodes.forEach(t=>h(e,t));m(this,"connect")};if(n){e()}else{const s=new MutationObserver(()=>{s.disconnect();e()});s.observe(this.parentElement,{childList:true})}}attributeChangedCallback(t,e,s){if(e!==s){if(t==="value"&&this.value!==s){this.value=s}if(this._boundNodes){const i=this._boundNodes.filter(([t,e])=>u(t));if(i.length!==this._boundNodes.length){this._boundNodes=i}const o=this;this._boundNodes.forEach(t=>h(o,t))}m(this,"attributechange",{name:t,oldValue:e,newValue:s})}}disconnectedCallback(){m(this,"disconnect")}adoptedCallback(){m(this,"adopt")}formAssociatedCallback(t){m(this,"formassociate",{form:t})}formDisabledCallback(t){m(this,"formdisable",{disabled:t})}formResetCallback(){m(this,"formreset")}formStateRestoreCallback(t,e){m(this,"formstaterestore",{state:t,mode:e})}})};const m=(t,e,s)=>{t.dispatchEvent(new CustomEvent(e,{detail:s||{},bubbles:true}))};const o=document.currentScript.hasAttribute("debug");[{attr:"elements",formAssociated:false},{attr:"form-associated",formAssociated:true}].forEach(({attr:t,formAssociated:s})=>{if(document.currentScript.hasAttribute(t)){document.currentScript.getAttribute(t).split(/\s+/).map(t=>t.split(/\[|]/)).forEach(([t,e])=>i(o,t,undefined,s,e?e.split(","):[]))}});const e=t=>{const e=t.getAttribute("decue");if(e&&!window.customElements.get(e)){i(o,e,t,t.hasAttribute("form-associated"),[])}};window.addEventListener("DOMContentLoaded",()=>{[...document.getElementsByTagName("template")].forEach(e);[...document.getElementsByTagName("object")].filter(t=>t.getAttribute("type")==="text/html").forEach(t=>{t.addEventListener("load",()=>[...t.contentDocument.getElementsByTagName("template")].forEach(e));[...t.contentDocument.getElementsByTagName("template")].forEach(e)})});return{processTemplate:e,defineElement:i}}();
1
+ var decue=function(){const o=document.currentScript.getAttribute("shadow")||"none";if(!["open","closed","none"].includes(o)){throw`Invalid default shadow DOM mode "${o}". Must be one of "open", "closed" or "none".`}const a=/{([a-zA-Z_0-9]+)((?:\|[.]?[a-zA-Z_0-9]+)*)}/g;const l=t=>!(t==="shadow"||t.startsWith("decue-"));const d=t=>{var e=t;while(e&&e.parentNode){if(e.parentNode===document){return true}else if(e.parentNode instanceof ShadowRoot){e=e.parentNode.host}else{e=e.parentNode}}return false};const n=i=>(t,e)=>{const o=e.startsWith(".")?e.substring(1):undefined;const s=window[e];if(!o&&!s){throw`Global function "${e}" not found. Make sure to include it (not deferred) before this element is created.`}return o?typeof t[o]==="function"?t[o]():t[o]:s.apply(i,[t])};const r=(s,i,t)=>t.replaceAll(a,(t,e,o)=>i.hasAttribute(e)?o.split("|").slice(1).reduce(n(s),i.getAttribute(e)):t);const c=(t,[e,o,s])=>{const i=r(e,t,o);if(e instanceof Text){if(i!==e.data){e.data=i}}else if(e instanceof HTMLElement){if(i!==e.getAttribute(s)){e.setAttribute(s,i)}}};const u=(t,o)=>{const e=document.createTreeWalker(t,NodeFilter.SHOW_TEXT|NodeFilter.SHOW_ELEMENT);while(e.nextNode()){const s=e.currentNode;if(s instanceof Text){[...s.data.matchAll(a)].forEach(t=>o(s,undefined,t[1]))}else if(s instanceof HTMLElement){s.getAttributeNames().forEach(e=>[...s.getAttribute(e).matchAll(a)].forEach(t=>o(s,e,t[1])))}}};const h=(s,t)=>{t.querySelectorAll("slot[name]").forEach(t=>{const e=s.querySelectorAll(`:scope > [slot="${t.getAttribute("name")}"]`);const o=e.length>0?e:t.children;t.replaceWith.apply(t,o)});const e=t.querySelector("slot:not([name])");if(e){const o=[...s.childNodes].filter(t=>t.nodeType!==Node.ELEMENT_NODE||!t.hasAttribute("slot"));const i=o.length>0?o:e.children;e.replaceWith.apply(e,i)}};const s=(s,a,n,t,e)=>{const r=t=>s?console.log(a+": "+t):undefined;var i=e;if(n){u(n.content,(t,e,o)=>i.push(o))}i=[...new Set(i.filter(l))];window.customElements.define(a,class extends HTMLElement{static formAssociated=t;static observedAttributes=i;constructor(){super();this._template=n||[...document.getElementsByTagName("template")].find(t=>t.getAttribute("decue")===a);if(!this._template){throw`Template for "${a}" not found. Make sure it comes in the DOM before any corresponding custom element.`}if(!t&&this._template.hasAttribute("formAssociated")){throw`Cannot declare a predefined custom element "${a}" as formAssociated. Move it from elements="..." to formAssociated="...".`}const s=this;this.getAttributeNames().filter(t=>t.startsWith("decue-on:")).map(t=>t.substring("decue-on:".length)).forEach(t=>{const e=s.getAttribute("decue-on:"+t);const o=window[e];if(!o){throw`Global handler function "${e}" for event "${t}" not found.`}r("Registering event listener for event: "+t);s.addEventListener(t,o)});if(t){const i=this.attachInternals();var o=this.getAttribute("value");r("Making form-associated with value: "+o);Object.defineProperties(this,{internals:{value:i,writable:false},value:{get:()=>o,set:t=>{o=t;i.setFormValue(o);s.checkValidity()}},name:{get:()=>this.getAttribute("name")},form:{get:()=>i.form},labels:{get:()=>i.labels},validity:{get:()=>i.validity},validationMessage:{get:()=>i.validationMessage},willValidate:{get:()=>i.willValidate},setFormValue:{value:(t,e)=>i.setFormValue(o=t,e),writable:false},setValidity:{value:i.setValidity.bind(i),writable:false},checkValidity:{value:()=>{f(s,"checkvalidity");return i.checkValidity()},writable:false},reportValidity:{value:i.reportValidity.bind(i),writable:false}});this.value=o;if(!this.hasAttribute("tabindex")){this.tabIndex=0}}}connectedCallback(){this._shadow=this.getAttribute("shadow")||this._template.getAttribute("shadow")||o;if(!["open","closed","none"].includes(this._shadow)){throw`Invalid shadow DOM mode "${this._shadow}" for element "${a}". Must be one of "open", "closed" or "none".`}if(!this._shadowRoot){this._shadowRoot=this._shadow==="none"?this:this.attachShadow({mode:this._shadow,delegatesFocus:true});const t=()=>{r("Finalizing...");const t=this._template.content.cloneNode(true);if(this._shadow==="none"){h(this._shadowRoot,t);r("Slots initialized")}this._boundNodes=[];[t,this].forEach(t=>u(t,(t,e,o)=>this._boundNodes.push(t instanceof HTMLElement?[t,t.getAttribute(e),e]:[t,t.data,undefined])));const e=this;const o=this.getAttributeNames().filter(l).filter(t=>!i.includes(t));if(o.length>0&&this._boundNodes.length>0){new MutationObserver(t=>t.forEach(t=>{if(o.includes(t.attributeName)){e.attributeChangedCallback(t.attributeName,t.oldValue,t.target.getAttribute(t.attributeName))}})).observe(this,{attributes:true});this._decueMutationObserved=true;if(s){r("Observing attributes with MutationObserver: "+o.join(" "));this.setAttribute("data-decue-mutation-observed-attributes",o.join(" "))}}if(s&&i.length>0){r("Observing attributes with observedAttributes: "+i.join(" "));this.setAttribute("data-decue-observed-attributes",i.join(" "))}this._shadowRoot.append(t);this._boundNodes.forEach(t=>c(e,t));f(this,"connect")};if(n){t()}else{const e=new MutationObserver(()=>{e.disconnect();t()});e.observe(this.parentElement,{childList:true})}}}attributeChangedCallback(t,e,o){if(e!==o){if(t==="value"&&this.value!==o){this.value=o}if(this._boundNodes){const s=this._boundNodes.filter(([t,e])=>d(t));if(s.length!==this._boundNodes.length){this._boundNodes=s}const i=this;this._boundNodes.forEach(t=>c(i,t))}f(this,"attributechange",{name:t,oldValue:e,newValue:o})}}disconnectedCallback(){f(this,"disconnect")}adoptedCallback(){f(this,"adopt")}formAssociatedCallback(t){f(this,"formassociate",{form:t})}formDisabledCallback(t){f(this,"formdisable",{disabled:t})}formResetCallback(){f(this,"formreset")}formStateRestoreCallback(t,e){f(this,"formstaterestore",{state:t,mode:e})}})};const f=(t,e,o)=>{t.dispatchEvent(new CustomEvent(e,{detail:o||{},bubbles:true}))};const i=document.currentScript.hasAttribute("debug");[{attr:"elements",formAssociated:false},{attr:"form-associated",formAssociated:true}].forEach(({attr:t,formAssociated:o})=>{if(document.currentScript.hasAttribute(t)){document.currentScript.getAttribute(t).split(/\s+/).map(t=>t.split(/\[|]/)).forEach(([t,e])=>s(i,t,undefined,o,e?e.split(","):[]))}});const e=t=>{const e=t.getAttribute("decue");if(e&&!window.customElements.get(e)){s(i,e,t,t.hasAttribute("form-associated"),[])}};window.addEventListener("DOMContentLoaded",()=>{[...document.getElementsByTagName("template")].forEach(e);[...document.getElementsByTagName("object")].filter(t=>t.getAttribute("type")==="text/html").forEach(t=>{t.addEventListener("load",()=>[...t.contentDocument.getElementsByTagName("template")].forEach(e));if(t.contentDocument){[...t.contentDocument.getElementsByTagName("template")].forEach(e)}})});return{processTemplate:e,defineElement:s}}();
package/examples.html CHANGED
@@ -45,6 +45,18 @@
45
45
  document.querySelector('[data-modifyvalues]').addEventListener('click', () => {
46
46
  document.querySelectorAll('[value]').forEach(x => {x.value = 'hello ' + x.value; x.dispatchEvent(new Event('change'));});
47
47
  });
48
+
49
+ setTimeout(() => {
50
+ document.querySelectorAll('.remove-then-append nested-element-with-attribute').forEach(el => {
51
+ const parent = el.parentNode;
52
+ parent.removeChild(el);
53
+ parent.appendChild(el);
54
+ });
55
+
56
+ document.querySelectorAll('.move nested-element-with-attribute').forEach(el => {
57
+ el.parentNode.moveBefore(el, null);
58
+ });
59
+ }, 1000);
48
60
  });
49
61
  </script>
50
62
  <script src="src/decue.js" debug elements="predefined-element predefined-element-with-attributes predefined-element-with-slot"></script>
@@ -270,6 +282,48 @@
270
282
  </fieldset>
271
283
  </td>
272
284
  </tr>
285
+
286
+ <tr>
287
+ <td>
288
+ <fieldset class="remove-then-append">
289
+ <legend>remove-then-append</legend>
290
+ <nested-element-with-attribute greetings="World!"></nested-element-with-attribute>
291
+ </fieldset>
292
+ </td>
293
+ <td>
294
+ <fieldset class="remove-then-append">
295
+ <legend>remove-then-append</legend>
296
+ <nested-element-with-attribute greetings="World!" shadow="open"></nested-element-with-attribute>
297
+ </fieldset>
298
+ </td>
299
+ <td>
300
+ <fieldset class="remove-then-append">
301
+ <legend>remove-then-append</legend>
302
+ <nested-element-with-attribute greetings="World!" shadow="closed"></nested-element-with-attribute>
303
+ </fieldset>
304
+ </td>
305
+ </tr>
306
+
307
+ <tr>
308
+ <td>
309
+ <fieldset class="move">
310
+ <legend>move</legend>
311
+ <nested-element-with-attribute greetings="World!"></nested-element-with-attribute>
312
+ </fieldset>
313
+ </td>
314
+ <td>
315
+ <fieldset class="move">
316
+ <legend>move</legend>
317
+ <nested-element-with-attribute greetings="World!" shadow="open"></nested-element-with-attribute>
318
+ </fieldset>
319
+ </td>
320
+ <td>
321
+ <fieldset class="move">
322
+ <legend>move</legend>
323
+ <nested-element-with-attribute greetings="World!" shadow="closed"></nested-element-with-attribute>
324
+ </fieldset>
325
+ </td>
326
+ </tr>
273
327
  </table>
274
328
  </main>
275
329
 
package/external.html CHANGED
@@ -1,3 +1,4 @@
1
+ <!DOCTYPE html>
1
2
  <template decue="external-element">
2
3
  <slot><div title="an external element with default slot">default slot: Hello {greetings}</div></slot>
3
4
  </template>
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "decue",
3
- "version": "1.0.0",
4
- "description": "",
3
+ "version": "1.0.1",
4
+ "description": "Declarative Custom Elements. Create simple web components with just HTML, parameterized with slots and attributes. With or without shadow-dom.",
5
5
  "author": "Jyri-Matti Lähteenmäki <jyri-matti@lahteenmaki.net>",
6
- "keywords": [],
6
+ "keywords": [
7
+ "html",
8
+ "web-component",
9
+ "custom-element"
10
+ ],
7
11
  "license": "MIT",
8
12
  "scripts": {
9
13
  "test": "mocha-chrome test/index.html",
@@ -12,7 +16,7 @@
12
16
  },
13
17
  "repository": {
14
18
  "type": "git",
15
- "url": "git+https://codeberg.org/jyri-matti/decue.git"
19
+ "url": "https://codeberg.org/jyri-matti/decue.git"
16
20
  },
17
21
  "devDependencies": {
18
22
  "chai": "^4.3.6",
package/src/decue.js CHANGED
@@ -195,64 +195,67 @@ var decue = (function() {
195
195
  throw `Invalid shadow DOM mode "${this._shadow}" for element "${elementName}". Must be one of "open", "closed" or "none".`;
196
196
  }
197
197
 
198
- const root = this._shadow === 'none' ? /** @type {HTMLElement} */ (this) : this.attachShadow({ mode: this._shadow, delegatesFocus: true });
199
- const content = /** @type {DocumentFragment} */ (this._template.content.cloneNode(true));
200
-
201
- const finalize = () => {
202
- dbg('Finalizing...');
198
+ if (!this._shadowRoot) {
199
+ this._shadowRoot = this._shadow === 'none' ? /** @type {HTMLElement} */ (this) : this.attachShadow({ mode: this._shadow, delegatesFocus: true });
200
+
201
+ const finalize = () => {
202
+ dbg('Finalizing...');
203
203
 
204
- if (this._shadow === 'none') {
205
- // Implement slotting manually when no shadow DOM in use
206
- lightDOMSlotting(root, content);
207
- dbg('Slots initialized');
208
- }
204
+ const content = /** @type {DocumentFragment} */ (this._template.content.cloneNode(true));
209
205
 
210
- // nodes having placeholder references, and thus need to be updated when attributes change.
211
- /** @type {[Node,string,string][]} */
212
- this._boundNodes = [];
213
-
214
- // Find all placeholders in the attributes and text of template or element content.
215
- [content, this].forEach(x =>
216
- forEachPlaceholder(x, (node,attributeName,_) => this._boundNodes.push(node instanceof HTMLElement
217
- ? [node, node.getAttribute(attributeName), attributeName]
218
- : [node, node.data, undefined])));
206
+ if (this._shadow === 'none') {
207
+ // Implement slotting manually when no shadow DOM in use
208
+ lightDOMSlotting(this._shadowRoot, content);
209
+ dbg('Slots initialized');
210
+ }
211
+
212
+ // nodes having placeholder references, and thus need to be updated when attributes change.
213
+ /** @type {[Node,string,string][]} */
214
+ this._boundNodes = [];
215
+
216
+ // Find all placeholders in the attributes and text of template or element content.
217
+ [content, this].forEach(x =>
218
+ forEachPlaceholder(x, (node,attributeName,_) => this._boundNodes.push(node instanceof HTMLElement
219
+ ? [node, node.getAttribute(attributeName), attributeName]
220
+ : [node, node.data, undefined])));
219
221
 
220
- const ths = this;
221
- // If there are attributes, which weren't yet observed statically, observe them dynamically with a MutationObserver.
222
- const unobservedAttributes = this.getAttributeNames().filter(isNotBuiltinAttribute).filter(x => !observedAttrs.includes(x));
223
- if (unobservedAttributes.length > 0 && this._boundNodes.length > 0) {
224
- new MutationObserver(recs => recs.forEach(rec => {
225
- if (unobservedAttributes.includes(rec.attributeName)) {
226
- ths.attributeChangedCallback(rec.attributeName, rec.oldValue, (/** @type {HTMLElement} */ (rec.target)).getAttribute(rec.attributeName));
222
+ const ths = this;
223
+ // If there are attributes, which weren't yet observed statically, observe them dynamically with a MutationObserver.
224
+ const unobservedAttributes = this.getAttributeNames().filter(isNotBuiltinAttribute).filter(x => !observedAttrs.includes(x));
225
+ if (unobservedAttributes.length > 0 && this._boundNodes.length > 0) {
226
+ new MutationObserver(recs => recs.forEach(rec => {
227
+ if (unobservedAttributes.includes(rec.attributeName)) {
228
+ ths.attributeChangedCallback(rec.attributeName, rec.oldValue, (/** @type {HTMLElement} */ (rec.target)).getAttribute(rec.attributeName));
229
+ }
230
+ })).observe(this, { attributes: true });
231
+ this._decueMutationObserved = true;
232
+ if (debug) {
233
+ dbg('Observing attributes with MutationObserver: ' + unobservedAttributes.join(' '));
234
+ this.setAttribute('data-decue-mutation-observed-attributes', unobservedAttributes.join(' '));
227
235
  }
228
- })).observe(this, { attributes: true });
229
- this._decueMutationObserved = true;
230
- if (debug) {
231
- dbg('Observing attributes with MutationObserver: ' + unobservedAttributes.join(' '));
232
- this.setAttribute('data-decue-mutation-observed-attributes', unobservedAttributes.join(' '));
233
236
  }
234
- }
235
- if (debug && observedAttrs.length > 0) {
236
- dbg('Observing attributes with observedAttributes: ' + observedAttrs.join(' '));
237
- this.setAttribute('data-decue-observed-attributes', observedAttrs.join(' '));
238
- }
239
-
240
- root.append(content);
241
- this._boundNodes.forEach(x => updatePlaceholders(ths, x));
237
+ if (debug && observedAttrs.length > 0) {
238
+ dbg('Observing attributes with observedAttributes: ' + observedAttrs.join(' '));
239
+ this.setAttribute('data-decue-observed-attributes', observedAttrs.join(' '));
240
+ }
241
+
242
+ this._shadowRoot.append(content);
243
+ this._boundNodes.forEach(x => updatePlaceholders(ths, x));
242
244
 
243
- fireEvent(this, 'connect');
244
- };
245
+ fireEvent(this, 'connect');
246
+ };
245
247
 
246
- if (template) {
247
- finalize();
248
- } else {
249
- // predefined element. Finalize only after the children are parsed.
250
- /** @type {MutationObserver} */
251
- const observer = new MutationObserver(() => {
252
- observer.disconnect();
248
+ if (template) {
253
249
  finalize();
254
- });
255
- observer.observe(this.parentElement, { childList: true });
250
+ } else {
251
+ // predefined element. Finalize only after the children are parsed.
252
+ /** @type {MutationObserver} */
253
+ const observer = new MutationObserver(() => {
254
+ observer.disconnect();
255
+ finalize();
256
+ });
257
+ observer.observe(this.parentElement, { childList: true });
258
+ }
256
259
  }
257
260
  }
258
261
 
@@ -321,7 +324,9 @@ var decue = (function() {
321
324
  .filter(obj => obj.getAttribute('type') === 'text/html')
322
325
  .forEach(obj => {
323
326
  obj.addEventListener('load', () => [...obj.contentDocument.getElementsByTagName('template')].forEach(processTemplate));
324
- [...obj.contentDocument.getElementsByTagName('template')].forEach(processTemplate);
327
+ if (obj.contentDocument) {
328
+ [...obj.contentDocument.getElementsByTagName('template')].forEach(processTemplate);
329
+ }
325
330
  });
326
331
  });
327
332