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/README.md +15 -1
- package/dist/decue.js +333 -191
- package/dist/decue.min.js +1 -1
- package/eslint.config.js +53 -0
- package/examples.html +82 -12
- package/external.html +1 -0
- package/npm.sh +1 -1
- package/package.json +24 -11
- package/src/decue.js +333 -191
- 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/dist/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
|
|
|
@@ -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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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 (
|
|
205
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
211
|
-
/** @type {[Node,string,string][]} */
|
|
212
|
-
this._boundNodes = [];
|
|
244
|
+
this._eventListeners = createEventListeners(this);
|
|
213
245
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
244
|
-
|
|
316
|
+
fireEvent(ths, 'connect');
|
|
317
|
+
};
|
|
245
318
|
|
|
246
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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()
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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((
|
|
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
|
|
302
|
-
.forEach(([elementName,observedAttributes])
|
|
303
|
-
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
|
+
});
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
if (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
|
-
|
|
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
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
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
|
+
}
|