decue 1.0.1 → 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
 
@@ -196,65 +227,108 @@ var decue = (function() {
196
227
  }
197
228
 
198
229
  if (!this._shadowRoot) {
199
- this._shadowRoot = this._shadow === 'none' ? /** @type {HTMLElement} */ (this) : this.attachShadow({ mode: this._shadow, delegatesFocus: true });
230
+ this._shadowRoot = this._shadow === 'none'
231
+ ? /** @type {HTMLElement} */ (this)
232
+ : this.attachShadow({ mode: this._shadow, delegatesFocus: true });
233
+
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
+ }
242
+ }
243
+
244
+ this._eventListeners = createEventListeners(this);
200
245
 
201
- const finalize = () => {
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() {
202
261
  dbg('Finalizing...');
203
262
 
204
- const content = /** @type {DocumentFragment} */ (this._template.content.cloneNode(true));
263
+ var content = /** @type {DocumentFragment} */ (ths._template.content.cloneNode(true));
205
264
 
206
- if (this._shadow === 'none') {
265
+ if (ths._shadow === 'none') {
207
266
  // Implement slotting manually when no shadow DOM in use
208
- lightDOMSlotting(this._shadowRoot, content);
267
+ lightDOMSlotting(ths._shadowRoot, content);
209
268
  dbg('Slots initialized');
210
269
  }
211
270
 
212
271
  // nodes having placeholder references, and thus need to be updated when attributes change.
213
272
  /** @type {[Node,string,string][]} */
214
- this._boundNodes = [];
273
+ ths._boundNodes = [];
215
274
 
216
275
  // 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])));
221
-
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));
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(' '));
229
305
  }
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(' '));
235
306
  }
236
307
  }
237
308
  if (debug && observedAttrs.length > 0) {
238
- dbg('Observing attributes with observedAttributes: ' + observedAttrs.join(' '));
239
- this.setAttribute('data-decue-observed-attributes', observedAttrs.join(' '));
309
+ dbg('Observing attributes with observedAttributes', observedAttrs);
310
+ ths.setAttribute('data-decue-observed-attributes', observedAttrs.join(' '));
240
311
  }
241
312
 
242
- this._shadowRoot.append(content);
243
- this._boundNodes.forEach(x => updatePlaceholders(ths, x));
313
+ ths._shadowRoot.append(content);
314
+ ths._boundNodes.forEach(function(x) { updatePlaceholdersFromAttributes(ths, x); });
244
315
 
245
- fireEvent(this, 'connect');
316
+ fireEvent(ths, 'connect');
246
317
  };
247
318
 
248
- if (template) {
319
+ if (template || ths.ownerDocument.readyState != 'loading') {
249
320
  finalize();
321
+ } else if (typeof MutationObserver === 'undefined') {
322
+ throw new Error(`No MutationObserver available, cannot use predefined elements`);
250
323
  } else {
251
324
  // predefined element. Finalize only after the children are parsed.
252
325
  /** @type {MutationObserver} */
253
- const observer = new MutationObserver(() => {
326
+ // eslint-disable-next-line no-undef
327
+ var observer = new MutationObserver(function() {
254
328
  observer.disconnect();
255
329
  finalize();
256
330
  });
257
- observer.observe(this.parentElement, { childList: true });
331
+ observer.observe(ths.parentElement, { childList: true });
258
332
  }
259
333
  }
260
334
  }
@@ -262,22 +336,30 @@ var decue = (function() {
262
336
  /** @type { (name: string, oldValue: string, newValue: string) => void } */
263
337
  attributeChangedCallback(name, oldValue, newValue) {
264
338
  if (oldValue !== newValue) {
265
- 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
266
341
  this.value = newValue;
267
342
  }
268
- if (this._boundNodes) {
269
- const stillInDocument = this._boundNodes.filter(([node,_]) => isInDocument(node));
270
- if (stillInDocument.length !== this._boundNodes.length) {
271
- this._boundNodes = stillInDocument;
272
- }
273
- const ths = this;
274
- this._boundNodes.forEach(x => updatePlaceholders(ths, x));
275
- }
343
+ this._boundNodes = updateBindings(this, this._boundNodes);
276
344
  fireEvent(this, 'attributechange', { name, oldValue, newValue })
277
345
  }
278
346
  }
279
347
 
280
- 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
+ }
281
363
  adoptedCallback() { fireEvent(this, 'adopt') }
282
364
  /** @type { (form:HTMLFormElement) => void } */
283
365
  formAssociatedCallback(form) { fireEvent(this, 'formassociate', { form }) }
@@ -290,48 +372,103 @@ var decue = (function() {
290
372
  }
291
373
 
292
374
  /** @type {(ths: HTMLElement, name: string, detail?: object) => void} */
293
- const fireEvent = (ths, name, detail) => {
294
- ths.dispatchEvent(new CustomEvent(name, { detail: detail || {}, bubbles: true }));
295
- }
296
-
297
- 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
+ };
298
382
 
299
383
  // predefine explicitly listed custom elements immediately before the DOM is parsed
300
- [{attr: 'elements', formAssociated: false},{attr: 'form-associated', formAssociated: true}].forEach(({attr, formAssociated}) => {
301
- if (document.currentScript.hasAttribute(attr)) {
302
- 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)
303
387
  .split(/\s+/)
304
- .map(x => x.split(/\[|]/))
305
- .forEach(([elementName,observedAttributes]) =>
306
- 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
+ });
307
392
  }
308
393
  });
309
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
+
310
423
  /** @type { (template:HTMLTemplateElement) => void } */
311
- const processTemplate = template => {
312
- const name = template.getAttribute('decue');
313
- if (name && !window.customElements.get(name)) {
424
+ var processTemplate = function(template) {
425
+ var name = template.getAttribute('decue');
426
+ if (name && !globalThis.customElements.get(name)) {
314
427
  defineElement(debug, name, template, template.hasAttribute('form-associated'), []);
315
428
  }
316
429
  };
317
430
 
318
- window.addEventListener('DOMContentLoaded', () => {
431
+ var initializer = function() {
319
432
  // define all custom elements not already defined
320
433
  [...document.getElementsByTagName('template')].forEach(processTemplate);
321
434
 
322
435
  // define all custom elements included in <object> tags
323
436
  [...document.getElementsByTagName('object')]
324
- .filter(obj => obj.getAttribute('type') === 'text/html')
325
- .forEach(obj => {
326
- obj.addEventListener('load', () => [...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
+ });
327
446
  if (obj.contentDocument) {
328
447
  [...obj.contentDocument.getElementsByTagName('template')].forEach(processTemplate);
329
448
  }
330
449
  });
331
- });
450
+ };
451
+ if (typeof globalThis !== 'undefined') {
452
+ globalThis.addEventListener('DOMContentLoaded', initializer);
453
+ } else if (typeof window !== 'undefined') {
454
+ window.addEventListener('DOMContentLoaded', initializer);
455
+ }
332
456
 
333
457
  return {
458
+ /** Process a <template> element */
334
459
  processTemplate: processTemplate,
335
- 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
336
466
  };
337
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
+ }