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/README.md +15 -1
- package/dist/decue.js +309 -172
- package/dist/decue.min.js +1 -1
- package/eslint.config.js +53 -0
- package/examples.html +29 -13
- package/npm.sh +1 -1
- package/package.json +17 -8
- package/src/decue.js +309 -172
- package/test/debug.test.js +201 -0
- package/test/decue.test.js +78 -0
- package/test/errors.test.js +212 -0
- package/test/eventHandlers.test.js +95 -0
- package/test/formAssociated.test.js +477 -0
- package/test/init.test.js +43 -0
- package/test/lifecycle.test.js +110 -0
- package/test/memory.test.js +283 -0
- package/test/piped.test.js +152 -0
- package/test/placeholders.test.js +396 -0
- package/test/predefined.test.js +131 -0
- package/test/scriptAttributes.test.js +464 -0
- package/test/slots.test.js +293 -0
- package/test/test-helpers.js +36 -0
- package/tsconfig.json +1 -1
- package/web-test-runner.config.mjs +30 -0
- package/serve.sh +0 -4
- package/test/decue-tests.js +0 -84
- package/test/index.html +0 -65
- package/test/util/util.js +0 -17
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
|
-
|
|
18
|
+
var defaultShadow = ('currentScript' in document && document['currentScript'] ? document['currentScript'].getAttribute('shadow') : undefined) || 'none';
|
|
17
19
|
if (!['open', 'closed', 'none'].includes(defaultShadow)) {
|
|
18
|
-
throw
|
|
20
|
+
throw new Error('Invalid default shadow DOM mode "' + defaultShadow + '". Must be one of "open", "closed" or "none".');
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
var placeholders = /{(\.?[a-zA-Z_0-9]+)((?:[.|][a-zA-Z_0-9]+)*)}/g;
|
|
22
24
|
|
|
23
25
|
/** @type {(a: string) => boolean} */
|
|
24
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
57
|
-
originalValue.replaceAll(placeholders, (placeholder, /** @type {string} */ attributeName, /** @type {string} */ pipes)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
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|
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
if (node
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
102
|
+
var lightDOMSlotting = function(root, content) {
|
|
93
103
|
// named slots
|
|
94
|
-
content.querySelectorAll('slot[name]').forEach(slot
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
110
|
+
// Only the first unnamed slot is the default slot
|
|
111
|
+
var unnamedSlot = content.querySelector('slot:not([name])');
|
|
101
112
|
if (unnamedSlot) {
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
/** @
|
|
114
|
-
|
|
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)
|
|
163
|
+
forEachPlaceholder(template.content, function(_node, _attributeName, placeholderName) { observedAttrs.push(placeholderName); });
|
|
120
164
|
}
|
|
121
|
-
observedAttrs =
|
|
122
|
-
|
|
123
|
-
static formAssociated
|
|
124
|
-
static observedAttributes
|
|
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 ||
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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: ()
|
|
162
|
-
|
|
163
|
-
internals.setFormValue(
|
|
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: ()
|
|
169
|
-
form: { get: ()
|
|
170
|
-
labels: { get: ()
|
|
171
|
-
validity: { get: ()
|
|
172
|
-
validationMessage: { get: ()
|
|
173
|
-
willValidate: { get: ()
|
|
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)
|
|
177
|
-
|
|
178
|
-
|
|
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),
|
|
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'
|
|
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
|
-
|
|
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
|
-
|
|
263
|
+
var content = /** @type {DocumentFragment} */ (ths._template.content.cloneNode(true));
|
|
205
264
|
|
|
206
|
-
if (
|
|
265
|
+
if (ths._shadow === 'none') {
|
|
207
266
|
// Implement slotting manually when no shadow DOM in use
|
|
208
|
-
lightDOMSlotting(
|
|
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
|
-
|
|
273
|
+
ths._boundNodes = [];
|
|
215
274
|
|
|
216
275
|
// Find all placeholders in the attributes and text of template or element content.
|
|
217
|
-
[content,
|
|
218
|
-
forEachPlaceholder(x, (node,attributeName
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
|
239
|
-
|
|
309
|
+
dbg('Observing attributes with observedAttributes', observedAttrs);
|
|
310
|
+
ths.setAttribute('data-decue-observed-attributes', observedAttrs.join(' '));
|
|
240
311
|
}
|
|
241
312
|
|
|
242
|
-
|
|
243
|
-
|
|
313
|
+
ths._shadowRoot.append(content);
|
|
314
|
+
ths._boundNodes.forEach(function(x) { updatePlaceholdersFromAttributes(ths, x); });
|
|
244
315
|
|
|
245
|
-
fireEvent(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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()
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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((
|
|
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
|
|
305
|
-
.forEach(([elementName,observedAttributes])
|
|
306
|
-
defineElement(debug, elementName,
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
if (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
|
-
|
|
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
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
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
|
+
}
|