decue 1.0.0 → 1.1.0

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/src/decue.js CHANGED
@@ -7,127 +7,171 @@
7
7
  // Declarative Custom Elements
8
8
  // https://codeberg.org/jyri-matti/decue
9
9
 
10
- /**
11
- * @typedef { ShadowRootMode | 'none' } ShadowMode
12
- */
13
-
14
10
  var decue = (function() {
11
+ /**
12
+ * @typedef { ShadowRootMode | 'none' } ShadowMode
13
+ */
14
+
15
+ var debug = ('currentScript' in document && document['currentScript'] ? document['currentScript'].hasAttribute('debug') : false) || false;
16
+
15
17
  /** @satisfy {ShadowMode} */
16
- const defaultShadow = document.currentScript.getAttribute('shadow') || 'none';
18
+ var defaultShadow = ('currentScript' in document && document['currentScript'] ? document['currentScript'].getAttribute('shadow') : undefined) || 'none';
17
19
  if (!['open', 'closed', 'none'].includes(defaultShadow)) {
18
- throw `Invalid default shadow DOM mode "${defaultShadow}". Must be one of "open", "closed" or "none".`;
20
+ throw new Error('Invalid default shadow DOM mode "' + defaultShadow + '". Must be one of "open", "closed" or "none".');
19
21
  }
20
22
 
21
- const placeholders = /{([a-zA-Z_0-9]+)((?:\|[.]?[a-zA-Z_0-9]+)*)}/g;
23
+ var placeholders = /{(\.?[a-zA-Z_0-9]+)((?:[.|][a-zA-Z_0-9]+)*)}/g;
22
24
 
23
25
  /** @type {(a: string) => boolean} */
24
- const isNotBuiltinAttribute = a => !(a === 'shadow' || a.startsWith('decue-'));
25
-
26
- // check if the element is still in the document
27
- /** @type {(element: Node) => boolean} */
28
- const isInDocument = element => {
29
- var currentElement = element;
30
- while (currentElement && currentElement.parentNode) {
31
- if (currentElement.parentNode === document) {
32
- return true;
33
- } else if (currentElement.parentNode instanceof ShadowRoot) {
34
- currentElement = currentElement.parentNode.host;
35
- } else {
36
- currentElement = currentElement.parentNode;
37
- }
38
- }
39
- return false;
40
- };
26
+ var isNotBuiltinAttribute = function(a) { return !(a === 'shadow' || a.startsWith('decue-')); };
41
27
 
42
28
  // evaluate a function or property on the given attribute value
43
- const evalFunc = (/** @type {Node} */ ths) => (/** @type {any} */ attrValue, /** @type {string} */ func) => {
44
- const method = func.startsWith('.') ? func.substring(1) : undefined;
45
- // @ts-ignore
46
- const globalFunc = window[func];
47
- if (!method && !globalFunc) {
48
- throw `Global function "${func}" not found. Make sure to include it (not deferred) before this element is created.`;
49
- }
50
- return method ? (typeof attrValue[method] === 'function' ? attrValue[method]() : attrValue[method])
51
- : globalFunc.apply(ths, [attrValue]);
29
+ var evalFunc = function(/** @type {Node} */ ths) {
30
+ return function(/** @type {any} */ attrValue, /** @type {string} */ func) {
31
+ var name = func.startsWith('.') ? func.substring(1) : undefined;
32
+ var isMethod = name && typeof attrValue[name] === 'function';
33
+ var registeredFunc = registeredFunctions[func];
34
+ if (!name && !registeredFunc) {
35
+ throw new Error('Global function "' + func + '" not found. Make sure to include it (not deferred) before this element is created.');
36
+ }
37
+ return name
38
+ ? (isMethod ? attrValue[name]() : attrValue[name])
39
+ : registeredFunc.apply(ths, [attrValue]);
40
+ };
52
41
  };
53
42
 
54
43
  // replace all placeholders in originalValue
55
44
  /** @type {(ths: Node, root: HTMLElement, originalValue: string) => string} */
56
- const replacePlaceholders = (ths, root, originalValue) =>
57
- originalValue.replaceAll(placeholders, (placeholder, /** @type {string} */ attributeName, /** @type {string} */ pipes) =>
58
- root.hasAttribute(attributeName)
59
- ? pipes.split('|').slice(1).reduce(evalFunc(ths), root.getAttribute(attributeName))
60
- : placeholder);
61
-
45
+ var replacePlaceholdersFromAttributes = function(ths, root, originalValue) {
46
+ return originalValue.replaceAll(placeholders, function(placeholder, /** @type {string} */ attributeName, /** @type {string} */ pipes) {
47
+ var parts = (attributeName+pipes).replaceAll(/([^|])\./g, '$1|.').split('|');
48
+ return root.hasAttribute(attributeName)
49
+ ? parts.slice(1).reduce(evalFunc(ths), root.getAttribute(attributeName))
50
+ : attributeName.startsWith('.')
51
+ ? parts.reduce(evalFunc(ths), root)
52
+ : placeholder;
53
+ });
54
+ };
55
+
62
56
  // update all placeholders in the given node
63
57
  /** @type {(root: HTMLElement, arr:[Node,string,string]) => void} */
64
- const updatePlaceholders = (root, [node,originalValue,attributeName]) => {
65
- const newval = replacePlaceholders(node, root, originalValue);
66
- if (node instanceof Text) {
67
- if (newval !== node.data) {
68
- node.data = newval;
58
+ var updatePlaceholdersFromAttributes = function(root, nodeOriginalValueAttributeName) {
59
+ var node = nodeOriginalValueAttributeName[0];
60
+ var originalValue = nodeOriginalValueAttributeName[1];
61
+ var attributeName = nodeOriginalValueAttributeName[2];
62
+ var newval = replacePlaceholdersFromAttributes(node, root, originalValue);
63
+ if (node.nodeType === Node.TEXT_NODE) {
64
+ var n = /** @type {Text} */ (node);
65
+ if (newval !== n.data) {
66
+ // only change text-node data, so the node itself remains
67
+ n.data = newval;
69
68
  }
70
- } else if (node instanceof HTMLElement) {
71
- if (newval !== node.getAttribute(attributeName)) {
72
- node.setAttribute(attributeName, newval);
69
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
70
+ var e = /** @type {Element} */ (node);
71
+ if (newval !== e.getAttribute(attributeName)) {
72
+ e.setAttribute(attributeName, newval);
73
73
  }
74
74
  }
75
75
  };
76
76
 
77
- /** @type {(elem: Node, matchHandler: (node: (Text|HTMLElement), attributeName: string?, placeholderName: string) => void) => void} */
78
- const forEachPlaceholder = (elem, matchHandler) => {
79
- const walker = document.createTreeWalker(elem, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT);
77
+ /** @type {(elem: Node, matchHandler: (node: (Text|Element), attributeName: string?, placeholderName: string) => void) => void} */
78
+ var forEachPlaceholder = function(elem, matchHandler) {
79
+ var walker = document.createTreeWalker(elem, 0x4 /* text */ | 0x1 /* element */);
80
80
  while (walker.nextNode()) {
81
- const node = walker.currentNode;
82
- if (node instanceof Text) {
83
- [...node.data.matchAll(placeholders)].forEach(m => matchHandler(node, undefined, m[1]));
84
- } else if (node instanceof HTMLElement) {
85
- node.getAttributeNames().forEach(a =>
86
- [...node.getAttribute(a).matchAll(placeholders)].forEach(m => matchHandler(node, a, m[1])));
81
+ var node = walker.currentNode;
82
+ if (node.nodeType === Node.TEXT_NODE) {
83
+ var n = /** @type {Text} */ (node);
84
+ var match;
85
+ while ((match = placeholders.exec(n.data)) !== null) {
86
+ matchHandler(n, null, match[1]);
87
+ }
88
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
89
+ var e = /** @type {Element} */ (node);
90
+ e.getAttributeNames().forEach(function(a) {
91
+ var attr = /** @type {string} */ (e.getAttribute(a));
92
+ var match;
93
+ while ((match = placeholders.exec(attr)) !== null) {
94
+ matchHandler(e, a, match[1]);
95
+ }
96
+ });
87
97
  }
88
98
  }
89
99
  };
90
100
 
91
101
  /** @type {(root: (HTMLElement|ShadowRoot), content: DocumentFragment) => void} */
92
- const lightDOMSlotting = (root, content) => {
102
+ var lightDOMSlotting = function(root, content) {
93
103
  // named slots
94
- content.querySelectorAll('slot[name]').forEach(slot => {
95
- const slotContents = root.querySelectorAll(`:scope > [slot="${slot.getAttribute('name')}"]`);
96
- const replacement = slotContents.length > 0 ? slotContents : slot.children;
104
+ content.querySelectorAll('slot[name]').forEach(function(slot) {
105
+ var slotContents = root.querySelectorAll(':scope > [slot="' + slot.getAttribute('name') + '"]');
106
+ var replacement = slotContents.length > 0 ? slotContents : slot.children;
97
107
  slot.replaceWith.apply(slot, replacement);
98
108
  });
99
109
 
100
- const unnamedSlot = content.querySelector('slot:not([name])');
110
+ // Only the first unnamed slot is the default slot
111
+ var unnamedSlot = content.querySelector('slot:not([name])');
101
112
  if (unnamedSlot) {
102
- const slotContents = [...root.childNodes].filter(x => x.nodeType !== Node.ELEMENT_NODE || ! /** @type HTMLElement */ (x).hasAttribute('slot'));
103
- const replacement = slotContents.length > 0 ? slotContents : unnamedSlot.children;
113
+ var slotContents = Array.prototype.slice.call(root.childNodes).filter(function(/** @type {Node} */x) { return x.nodeType !== Node.ELEMENT_NODE || ! /** @type HTMLElement */ (x).hasAttribute('slot'); });
114
+ var replacement = slotContents.length > 0 ? slotContents : unnamedSlot.children;
104
115
  unnamedSlot.replaceWith.apply(unnamedSlot, replacement);
105
116
  }
106
117
  };
118
+
119
+ /** @type {(ths: HTMLElement) => [string, EventListener][]} */
120
+ var createEventListeners = function(ths) {
121
+ return ths.getAttributeNames()
122
+ .filter(function(a) { return a.startsWith("decue-on:"); })
123
+ .map(function(a) { return a.substring("decue-on:".length); })
124
+ .map(function(eventName) {
125
+ var func = ths.getAttribute('decue-on:' + eventName);
126
+ // @ts-ignore
127
+ var handler = globalThis[func];
128
+ if (!handler) {
129
+ throw 'Global handler function "' + func + '" for event "' + eventName + '" not found.';
130
+ }
131
+ ths.addEventListener(eventName, handler);
132
+ return [eventName, handler];
133
+ });
134
+ }
135
+
136
+ /** @type {(ths: HTMLElement, boundNodes: [Node,string,string][]) => [Node,string,string][]} */
137
+ var updateBindings = function(ths, boundNodes) {
138
+ if (boundNodes) {
139
+ var stillInDocument = boundNodes.filter(function(nodes) { return nodes[0].ownerDocument !== undefined; });
140
+ if (stillInDocument.length !== boundNodes.length) {
141
+ boundNodes = stillInDocument;
142
+ }
143
+ boundNodes.forEach(function(x) { updatePlaceholdersFromAttributes(ths, x); });
144
+ }
145
+ return boundNodes;
146
+ }
107
147
 
108
- const defineElement = (/** @type {boolean} */ debug,
109
- /** @type {string} */ elementName,
110
- /** @type {HTMLTemplateElement?} */ template,
111
- /** @type {boolean} */ formAssociated,
112
- /** @type {string[]} */ attributesToObserve) => {
113
- /** @type { (msg: string) => void} */
114
- const dbg = msg => debug ? console.log(elementName + ': ' + msg) : undefined;
148
+ var defineElement = function(/** @type {boolean} */ debug,
149
+ /** @type {string} */ elementName,
150
+ /** @type {HTMLTemplateElement?} */ template,
151
+ /** @type {boolean} */ formAssociated,
152
+ /** @type {string[]} */ attributesToObserve) {
153
+ /** @param {...any} arguments */
154
+ var dbg = function() {
155
+ if (debug && console) {
156
+ console.log.apply(null, [elementName].concat(Array.prototype.slice.call(arguments)));
157
+ }
158
+ }
115
159
 
116
160
  /** @type {string[]} */
117
161
  var observedAttrs = attributesToObserve;
118
162
  if (template) {
119
- forEachPlaceholder(template.content, (_node, _attributeName, placeholderName) => observedAttrs.push(placeholderName))
163
+ forEachPlaceholder(template.content, function(_node, _attributeName, placeholderName) { observedAttrs.push(placeholderName); });
120
164
  }
121
- observedAttrs = [...new Set(observedAttrs.filter(isNotBuiltinAttribute))];
122
- window.customElements.define(elementName, class extends HTMLElement {
123
- static formAssociated = formAssociated;
124
- static observedAttributes = observedAttrs;
165
+ observedAttrs = observedAttrs.filter(isNotBuiltinAttribute).filter(function(v, i, arr) { return arr.indexOf(v) === i; });
166
+ globalThis.customElements.define(elementName, class extends HTMLElement {
167
+ static get formAssociated() { return formAssociated; }
168
+ static get observedAttributes() { return observedAttrs; }
125
169
 
126
170
  constructor() {
127
171
  super();
128
172
 
129
173
  /** @type {HTMLTemplateElement} */
130
- this._template = template || [...document.getElementsByTagName('template')].find(x => x.getAttribute('decue') === elementName);
174
+ this._template = template || Array.prototype.slice.call(document.getElementsByTagName('template')).filter(function(/** @type {HTMLTemplateElement} */ x) { return x.getAttribute('decue') === elementName; }).concat([undefined])[0];
131
175
  if (!this._template) {
132
176
  throw `Template for "${elementName}" not found. Make sure it comes in the DOM before any corresponding custom element.`;
133
177
  }
@@ -136,55 +180,42 @@ var decue = (function() {
136
180
  throw `Cannot declare a predefined custom element "${elementName}" as formAssociated. Move it from elements="..." to formAssociated="...".`;
137
181
  }
138
182
 
139
- const ths = this;
140
- this.getAttributeNames()
141
- .filter(a => a.startsWith("decue-on:"))
142
- .map(a => a.substring("decue-on:".length))
143
- .forEach(eventName => {
144
- const func = ths.getAttribute('decue-on:' + eventName);
145
- // @ts-ignore
146
- const handler = window[func];
147
- if (!handler) {
148
- throw `Global handler function "${func}" for event "${eventName}" not found.`;
149
- }
150
- dbg('Registering event listener for event: ' + eventName);
151
- ths.addEventListener(eventName, handler);
152
- });
153
-
154
183
  if (formAssociated) {
155
184
  // https://web.dev/articles/more-capable-form-controls
156
- const internals = this.attachInternals();
157
- var value = this.getAttribute('value');
158
- dbg('Making form-associated with value: ' + value);
185
+ var internals = this.attachInternals();
186
+
187
+ /** @type {any} */
188
+ var _value;
189
+ var ths = this;
159
190
  Object.defineProperties(this, {
160
191
  internals: { value: internals, writable: false },
161
- value: { get: () => value, set: newValue => {
162
- value = newValue;
163
- internals.setFormValue(value);
192
+ value: { get: function() { return _value; }, set: function(newValue) {
193
+ _value = newValue;
194
+ internals.setFormValue(_value);
164
195
  // @ts-ignore
165
196
  ths.checkValidity();
197
+ ths._boundNodes = updateBindings(ths, ths._boundNodes);
166
198
  }},
167
199
 
168
- name: { get: () => this.getAttribute('name') },
169
- form: { get: () => internals.form },
170
- labels: { get: () => internals.labels },
171
- validity: { get: () => internals.validity },
172
- validationMessage: { get: () => internals.validationMessage },
173
- willValidate: { get: () => internals.willValidate },
200
+ name: { get: function() { return this.getAttribute('name'); } },
201
+ form: { get: function() { return internals.form; } },
202
+ labels: { get: function() { return internals.labels; } },
203
+ validity: { get: function() { return internals.validity; } },
204
+ validationMessage: { get: function() { return internals.validationMessage; } },
205
+ willValidate: { get: function() { return internals.willValidate; } },
174
206
 
175
207
  // @ts-ignore
176
- setFormValue: { value: (n,s) => internals.setFormValue(value = n, s), writable: false },
177
- setValidity: { value: internals.setValidity.bind(internals), writable: false },
178
- checkValidity: { value: () => {
208
+ setFormValue: { value: function(n,s) {
209
+ internals.setFormValue(_value = n, s);
210
+ ths._boundNodes = updateBindings(ths, ths._boundNodes);
211
+ }, writable: false },
212
+ setValidity: { value: internals.setValidity.bind(internals), writable: false },
213
+ checkValidity: { value: function() {
179
214
  fireEvent(ths, 'checkvalidity');
180
215
  return internals.checkValidity();
181
216
  }, writable: false },
182
- reportValidity: { value: internals.reportValidity.bind(internals), writable: false }
217
+ reportValidity: { value: internals.reportValidity.bind(internals), writable: false }
183
218
  });
184
- this.value = value;
185
- if (!this.hasAttribute('tabindex')) {
186
- this.tabIndex = 0;
187
- }
188
219
  }
189
220
  }
190
221
 
@@ -195,86 +226,140 @@ var decue = (function() {
195
226
  throw `Invalid shadow DOM mode "${this._shadow}" for element "${elementName}". Must be one of "open", "closed" or "none".`;
196
227
  }
197
228
 
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...');
229
+ if (!this._shadowRoot) {
230
+ this._shadowRoot = this._shadow === 'none'
231
+ ? /** @type {HTMLElement} */ (this)
232
+ : this.attachShadow({ mode: this._shadow, delegatesFocus: true });
203
233
 
204
- if (this._shadow === 'none') {
205
- // Implement slotting manually when no shadow DOM in use
206
- lightDOMSlotting(root, content);
207
- dbg('Slots initialized');
234
+ if (formAssociated) {
235
+ // attributes must not be accessed in constructor, thus these are initalized here.
236
+ // @ts-ignore
237
+ this.value = this.getAttribute('value');
238
+ dbg('Making form-associated with value', this.value);
239
+ if (!this.hasAttribute('tabindex')) {
240
+ this.tabIndex = 0;
241
+ }
208
242
  }
209
243
 
210
- // nodes having placeholder references, and thus need to be updated when attributes change.
211
- /** @type {[Node,string,string][]} */
212
- this._boundNodes = [];
244
+ this._eventListeners = createEventListeners(this);
213
245
 
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])));
219
-
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));
246
+ /**
247
+ * @typedef { HTMLElement & {
248
+ _shadowRoot: (ShadowRoot|HTMLElement),
249
+ _template: HTMLTemplateElement,
250
+ _shadow: ShadowMode,
251
+ _boundNodes: [Node,string,string][],
252
+ _decueMutationObserved?: boolean,
253
+ _observer?: MutationObserver,
254
+ _eventListeners?: [string, EventListener][],
255
+ attributeChangedCallback: (name: string, oldValue: string, newValue: string) => void
256
+ }} Ths
257
+ */
258
+ /** @type {Ths} */
259
+ var ths = this;
260
+ var finalize = function() {
261
+ dbg('Finalizing...');
262
+
263
+ var content = /** @type {DocumentFragment} */ (ths._template.content.cloneNode(true));
264
+
265
+ if (ths._shadow === 'none') {
266
+ // Implement slotting manually when no shadow DOM in use
267
+ lightDOMSlotting(ths._shadowRoot, content);
268
+ dbg('Slots initialized');
269
+ }
270
+
271
+ // nodes having placeholder references, and thus need to be updated when attributes change.
272
+ /** @type {[Node,string,string][]} */
273
+ ths._boundNodes = [];
274
+
275
+ // Find all placeholders in the attributes and text of template or element content.
276
+ [content, ths].forEach(function(x) {
277
+ forEachPlaceholder(x, function(node,attributeName) {
278
+ ths._boundNodes.push(node.nodeType == Node.ELEMENT_NODE
279
+ ? [node, /** @type {HTMLElement} */ (node).getAttribute(attributeName), attributeName]
280
+ : [node, /** @type {Text} */ (node).data, undefined]);
281
+ });
282
+ });
283
+
284
+ // If there are attributes, which weren't yet observed statically, observe them dynamically with a MutationObserver, if present.
285
+ var unobservedAttributes = ths.getAttributeNames().filter(isNotBuiltinAttribute).filter(function(x) { return !observedAttrs.includes(x); });
286
+ if (unobservedAttributes.length > 0 && ths._boundNodes.length > 0) {
287
+ if (typeof MutationObserver === 'undefined') {
288
+ console.info(`Decue: Element "${elementName}" has attributes (${unobservedAttributes.join(' ')}) which are not declared in observedAttributes, but no MutationObserver is available. These attributes won't be observed.`);
289
+ } else {
290
+ // eslint-disable-next-line no-undef
291
+ ths._observer = new MutationObserver(function(recs) {
292
+ recs.forEach(function(rec) {
293
+ if (unobservedAttributes.includes(rec.attributeName)) {
294
+ var newVal = /** @type {HTMLElement} */ (rec.target).getAttribute(rec.attributeName);
295
+ dbg('Attribute observer hit', rec.attributeName, newVal, "(unobserved attributes: ", unobservedAttributes, ")");
296
+ ths.attributeChangedCallback(rec.attributeName, rec.oldValue, newVal);
297
+ }
298
+ });
299
+ });
300
+ ths._observer.observe(ths, { attributeOldValue: true });
301
+ ths._decueMutationObserved = true;
302
+ if (debug) {
303
+ dbg('Observing attributes with MutationObserver', unobservedAttributes);
304
+ ths.setAttribute('data-decue-mutation-observed-attributes', unobservedAttributes.join(' '));
305
+ }
227
306
  }
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
307
  }
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));
308
+ if (debug && observedAttrs.length > 0) {
309
+ dbg('Observing attributes with observedAttributes', observedAttrs);
310
+ ths.setAttribute('data-decue-observed-attributes', observedAttrs.join(' '));
311
+ }
312
+
313
+ ths._shadowRoot.append(content);
314
+ ths._boundNodes.forEach(function(x) { updatePlaceholdersFromAttributes(ths, x); });
242
315
 
243
- fireEvent(this, 'connect');
244
- };
316
+ fireEvent(ths, 'connect');
317
+ };
245
318
 
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();
319
+ if (template || ths.ownerDocument.readyState != 'loading') {
253
320
  finalize();
254
- });
255
- observer.observe(this.parentElement, { childList: true });
321
+ } else if (typeof MutationObserver === 'undefined') {
322
+ throw new Error(`No MutationObserver available, cannot use predefined elements`);
323
+ } else {
324
+ // predefined element. Finalize only after the children are parsed.
325
+ /** @type {MutationObserver} */
326
+ // eslint-disable-next-line no-undef
327
+ var observer = new MutationObserver(function() {
328
+ observer.disconnect();
329
+ finalize();
330
+ });
331
+ observer.observe(ths.parentElement, { childList: true });
332
+ }
256
333
  }
257
334
  }
258
335
 
259
336
  /** @type { (name: string, oldValue: string, newValue: string) => void } */
260
337
  attributeChangedCallback(name, oldValue, newValue) {
261
338
  if (oldValue !== newValue) {
262
- if (name === 'value' && this.value !== newValue) {
339
+ if (formAssociated && name === 'value' && this.value !== newValue) {
340
+ // if the value attribute changes for a form component, update the component 'value' property as well
263
341
  this.value = newValue;
264
342
  }
265
- if (this._boundNodes) {
266
- const stillInDocument = this._boundNodes.filter(([node,_]) => isInDocument(node));
267
- if (stillInDocument.length !== this._boundNodes.length) {
268
- this._boundNodes = stillInDocument;
269
- }
270
- const ths = this;
271
- this._boundNodes.forEach(x => updatePlaceholders(ths, x));
272
- }
343
+ this._boundNodes = updateBindings(this, this._boundNodes);
273
344
  fireEvent(this, 'attributechange', { name, oldValue, newValue })
274
345
  }
275
346
  }
276
347
 
277
- disconnectedCallback() { fireEvent(this, 'disconnect') }
348
+ disconnectedCallback() {
349
+ fireEvent(this, 'disconnect');
350
+ if (this._observer) {
351
+ dbg('Disconnecting MutationObserver');
352
+ this._observer.disconnect();
353
+ delete this._observer;
354
+ }
355
+ if (this._eventListeners) {
356
+ dbg('Removing event listeners');
357
+ this._eventListeners.forEach(function(/** @type {[string, EventListener]} */ x) {
358
+ this.removeEventListener(x[0], x[1]);
359
+ }, this);
360
+ delete this._eventListeners;
361
+ }
362
+ }
278
363
  adoptedCallback() { fireEvent(this, 'adopt') }
279
364
  /** @type { (form:HTMLFormElement) => void } */
280
365
  formAssociatedCallback(form) { fireEvent(this, 'formassociate', { form }) }
@@ -287,46 +372,103 @@ var decue = (function() {
287
372
  }
288
373
 
289
374
  /** @type {(ths: HTMLElement, name: string, detail?: object) => void} */
290
- const fireEvent = (ths, name, detail) => {
291
- ths.dispatchEvent(new CustomEvent(name, { detail: detail || {}, bubbles: true }));
292
- }
293
-
294
- const debug = document.currentScript.hasAttribute('debug');
375
+ var fireEvent = function(ths, name, detail) {
376
+ if (typeof CustomEvent !== 'undefined') {
377
+ ths.dispatchEvent(new CustomEvent(name, { detail: detail || {}, bubbles: true }));
378
+ } else {
379
+ ths.dispatchEvent(new Event(name, { detail: detail || {}, bubbles: true }));
380
+ }
381
+ };
295
382
 
296
383
  // predefine explicitly listed custom elements immediately before the DOM is parsed
297
- [{attr: 'elements', formAssociated: false},{attr: 'form-associated', formAssociated: true}].forEach(({attr, formAssociated}) => {
298
- if (document.currentScript.hasAttribute(attr)) {
299
- document.currentScript.getAttribute(attr)
384
+ [{attr: 'elements', formAssociated: false},{attr: 'form-associated', formAssociated: true}].forEach(function(x) {
385
+ if (document.currentScript.hasAttribute(x.attr)) {
386
+ document.currentScript.getAttribute(x.attr)
300
387
  .split(/\s+/)
301
- .map(x => x.split(/\[|]/))
302
- .forEach(([elementName,observedAttributes]) =>
303
- defineElement(debug, elementName, undefined, formAssociated, (observedAttributes ? observedAttributes.split(',') : [])));
388
+ .map(function(x) { return x.split(/\[|]/); })
389
+ .forEach(function([elementName,observedAttributes]) {
390
+ defineElement(debug, elementName, null, x.formAssociated, (observedAttributes ? observedAttributes.split(',') : []));
391
+ });
304
392
  }
305
393
  });
306
394
 
395
+ /** @type { {[key: string]: Function} } */
396
+ var registeredFunctions = {
397
+ };
398
+
399
+ if (document.currentScript && document.currentScript.hasAttribute('functions')) {
400
+ document.currentScript.getAttribute('functions').split(/\s+/).forEach(function(f) {
401
+ var name = f;
402
+ var location = f;
403
+ var named = f.indexOf(':');
404
+ if (named >= 0) {
405
+ name = f.substring(0, named);
406
+ location = f.substring(named+1);
407
+ }
408
+ var func = location.split('.').reduce(function(obj, prop) {
409
+ if (obj && prop in obj) {
410
+ // @ts-ignore
411
+ return obj[prop];
412
+ } else {
413
+ throw `Cannot find namespace "${prop}" while resolving function "${f}".`;
414
+ }
415
+ }, globalThis);
416
+ if (typeof func !== 'function') {
417
+ throw `Function "${f}" not found. Make sure to include it (not deferred) before Decue script.`;
418
+ }
419
+ registeredFunctions[name] = func;
420
+ });
421
+ }
422
+
307
423
  /** @type { (template:HTMLTemplateElement) => void } */
308
- const processTemplate = template => {
309
- const name = template.getAttribute('decue');
310
- if (name && !window.customElements.get(name)) {
424
+ var processTemplate = function(template) {
425
+ var name = template.getAttribute('decue');
426
+ if (name && !globalThis.customElements.get(name)) {
311
427
  defineElement(debug, name, template, template.hasAttribute('form-associated'), []);
312
428
  }
313
429
  };
314
430
 
315
- window.addEventListener('DOMContentLoaded', () => {
431
+ var initializer = function() {
316
432
  // define all custom elements not already defined
317
433
  [...document.getElementsByTagName('template')].forEach(processTemplate);
318
434
 
319
435
  // define all custom elements included in <object> tags
320
436
  [...document.getElementsByTagName('object')]
321
- .filter(obj => obj.getAttribute('type') === 'text/html')
322
- .forEach(obj => {
323
- obj.addEventListener('load', () => [...obj.contentDocument.getElementsByTagName('template')].forEach(processTemplate));
324
- [...obj.contentDocument.getElementsByTagName('template')].forEach(processTemplate);
437
+ .filter(function(obj) {
438
+ return obj.getAttribute('type') === 'text/html';
439
+ })
440
+ .forEach(function(obj) {
441
+ obj.addEventListener('load', function() {
442
+ if (obj.contentDocument) {
443
+ [...obj.contentDocument.getElementsByTagName('template')].forEach(processTemplate);
444
+ }
445
+ });
446
+ if (obj.contentDocument) {
447
+ [...obj.contentDocument.getElementsByTagName('template')].forEach(processTemplate);
448
+ }
325
449
  });
326
- });
450
+ };
451
+ if (typeof globalThis !== 'undefined') {
452
+ globalThis.addEventListener('DOMContentLoaded', initializer);
453
+ } else if (typeof window !== 'undefined') {
454
+ window.addEventListener('DOMContentLoaded', initializer);
455
+ }
327
456
 
328
457
  return {
458
+ /** Process a <template> element */
329
459
  processTemplate: processTemplate,
330
- defineElement: defineElement
460
+
461
+ /** Manually define a custom element */
462
+ defineElement: defineElement,
463
+
464
+ /** Add here all the function you want to refer to in pipings */
465
+ registeredFunctions: registeredFunctions
331
466
  };
332
467
  })();
468
+
469
+ // Attach to globalThis when available
470
+ if (typeof globalThis !== 'undefined') {
471
+ globalThis.decue = decue;
472
+ } else if (typeof window !== 'undefined') {
473
+ window.decue = decue;
474
+ }