decue 1.0.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/.forgejo/workflows/publish.yaml +18 -0
- package/LICENSE +21 -0
- package/README.md +95 -0
- package/dist/decue.js +332 -0
- package/dist/decue.min.js +1 -0
- package/examples.html +278 -0
- package/external.html +3 -0
- package/npm.sh +4 -0
- package/package.json +25 -0
- package/serve.sh +4 -0
- package/src/decue.js +332 -0
- package/test/decue-tests.js +84 -0
- package/test/index.html +65 -0
- package/test/util/util.js +17 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: Publish Package to npmjs
|
|
2
|
+
on:
|
|
3
|
+
release:
|
|
4
|
+
types: [published]
|
|
5
|
+
jobs:
|
|
6
|
+
build:
|
|
7
|
+
runs-on: codeberg-tiny-lazy
|
|
8
|
+
steps:
|
|
9
|
+
- uses: actions/checkout@v4
|
|
10
|
+
# Setup .npmrc file to publish to npm
|
|
11
|
+
- uses: actions/setup-node@v4
|
|
12
|
+
with:
|
|
13
|
+
node-version: '15.x'
|
|
14
|
+
registry-url: 'https://registry.npmjs.org'
|
|
15
|
+
- run: npm ci
|
|
16
|
+
- run: npm publish
|
|
17
|
+
env:
|
|
18
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Jyri-Matti Lähteenmäki
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# DecuE
|
|
2
|
+
|
|
3
|
+
Declarative Custom Elements
|
|
4
|
+
|
|
5
|
+
DeCuE is a lightweight framework for defining custom elements declaratively using HTML `<template>` tags.
|
|
6
|
+
It supports Shadow DOM, slotting, attribute observation, and form-associated custom elements.
|
|
7
|
+
|
|
8
|
+
Based on ideas in https://github.com/kgscialdone/facet (Thank you very much!) though with a bit different mindset.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- Automatically registers all `<template decue="...">` as custom elements at DOMContentLoaded.
|
|
13
|
+
- also from inside `<object>`s, thus supporting elements defined in external files.
|
|
14
|
+
- Supports Shadow DOM modes: `none`, `open`, `closed` (default is `none`).
|
|
15
|
+
- Allows giving parameter data to elements.
|
|
16
|
+
- as slots for structured data: named and unnamed, also without shadow DOM (though mind the differences to native slots).
|
|
17
|
+
- as attributes for textual data: referenced by {placeholders} in template content.
|
|
18
|
+
- Supports function and method piping in placeholders, e.g. `{name|.toUpperCase|someGlobalFunc}`.
|
|
19
|
+
- Supports form-associated custom elements.
|
|
20
|
+
- I currently don't have actual use cases. Please let me know if you need some functionality!
|
|
21
|
+
- Allows event handler binding via `decue-on:eventname` attributes.
|
|
22
|
+
- No dependencies. Rather small.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
Download from https://unpkg.com/decue/dist/decue.min.js
|
|
27
|
+
and include in your HTML:
|
|
28
|
+
```
|
|
29
|
+
<script src="decue.min.js"></script>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
You can add `defer` unless you want to use predefined elements.
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
By default, all templates having the attribute `decue` are automatically defined as custom elements at `DOMContentLoaded`. The template content is automatically searched for placeholder references, which are then included as observedAttributes to monitor for changes.
|
|
37
|
+
|
|
38
|
+
By default, all elements are defined without a shadow DOM. You can choose the shadow mode (`none`, `open` or `closed`) by the following ways:
|
|
39
|
+
```
|
|
40
|
+
<!-- global default shadow DOM mode -->
|
|
41
|
+
<script shadow="...">
|
|
42
|
+
|
|
43
|
+
<!-- my-element default shadow DOM mode -->
|
|
44
|
+
<template decue="my-element" shadow="...">
|
|
45
|
+
|
|
46
|
+
<!-- specific my-element instance shadow DOM mode -->
|
|
47
|
+
<my-element shadow="...">
|
|
48
|
+
|
|
49
|
+
<!-- make my-element form-associated -->
|
|
50
|
+
<template decue="my-element" form-associated>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Predefined elements
|
|
54
|
+
|
|
55
|
+
If you want to have elements already defined when the DOM is parsed to remove flicker, you can use the script tag with the attribute `elements` (or `form-associated` for form associated elements). In this case the template is not available yet, so attributes will be monitored for changes using a MutationObserver. You can declare attributes explicitly to make them observed as `observedAttributes` instead. Make sure the template appears before the using elements in the DOM.
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
<!-- predefined custom element names, separated by spaces, -->
|
|
59
|
+
<!-- can include attributes to observe, separated by a comma -->
|
|
60
|
+
<script elements="my-element my-other-element[observed1,observed2]">
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### External elements
|
|
64
|
+
|
|
65
|
+
You can include elements defined in external files using an `<object>` tag:
|
|
66
|
+
```
|
|
67
|
+
<object data="external.html" type="text/html" style="position:absolute; left: -99999px"></object>
|
|
68
|
+
```
|
|
69
|
+
or by including them into the DOM as you wish and calling `decue.processTemplate(...)` for each of them.
|
|
70
|
+
|
|
71
|
+
### Events
|
|
72
|
+
|
|
73
|
+
Fires `connect`, `disconnect`, `adopt`, `attributechange`, `formassociate`, `formdisable`, `formreset`, `formstaterestore` and `checkvalidity` custom events.
|
|
74
|
+
|
|
75
|
+
Allows binding global functions as event handlers via `decue-on:eventname` attributes:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
<script>
|
|
79
|
+
function validateme(ev) {
|
|
80
|
+
...
|
|
81
|
+
}
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<form-associated-element decue-on:checkvalidity="validateme">...</form-associated-element>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
This form doesn't require using eval. Please let me know if you need the old fashioned (and not recommended) inline-javascript way.
|
|
88
|
+
|
|
89
|
+
### Examples
|
|
90
|
+
|
|
91
|
+
See https://lahteenmaki.net/decue/examples.html for some examples.
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
[MIT](https://choosealicense.com/licenses/mit/)
|
package/dist/decue.js
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
// ---------------------------
|
|
4
|
+
// ---------- DeCuE ----------
|
|
5
|
+
// ---------------------------
|
|
6
|
+
//
|
|
7
|
+
// Declarative Custom Elements
|
|
8
|
+
// https://codeberg.org/jyri-matti/decue
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef { ShadowRootMode | 'none' } ShadowMode
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
var decue = (function() {
|
|
15
|
+
/** @satisfy {ShadowMode} */
|
|
16
|
+
const defaultShadow = document.currentScript.getAttribute('shadow') || 'none';
|
|
17
|
+
if (!['open', 'closed', 'none'].includes(defaultShadow)) {
|
|
18
|
+
throw `Invalid default shadow DOM mode "${defaultShadow}". Must be one of "open", "closed" or "none".`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const placeholders = /{([a-zA-Z_0-9]+)((?:\|[.]?[a-zA-Z_0-9]+)*)}/g;
|
|
22
|
+
|
|
23
|
+
/** @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
|
+
};
|
|
41
|
+
|
|
42
|
+
// 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]);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// replace all placeholders in originalValue
|
|
55
|
+
/** @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
|
+
|
|
62
|
+
// update all placeholders in the given node
|
|
63
|
+
/** @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;
|
|
69
|
+
}
|
|
70
|
+
} else if (node instanceof HTMLElement) {
|
|
71
|
+
if (newval !== node.getAttribute(attributeName)) {
|
|
72
|
+
node.setAttribute(attributeName, newval);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
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);
|
|
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])));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** @type {(root: (HTMLElement|ShadowRoot), content: DocumentFragment) => void} */
|
|
92
|
+
const lightDOMSlotting = (root, content) => {
|
|
93
|
+
// 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;
|
|
97
|
+
slot.replaceWith.apply(slot, replacement);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const unnamedSlot = content.querySelector('slot:not([name])');
|
|
101
|
+
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;
|
|
104
|
+
unnamedSlot.replaceWith.apply(unnamedSlot, replacement);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
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;
|
|
115
|
+
|
|
116
|
+
/** @type {string[]} */
|
|
117
|
+
var observedAttrs = attributesToObserve;
|
|
118
|
+
if (template) {
|
|
119
|
+
forEachPlaceholder(template.content, (_node, _attributeName, placeholderName) => observedAttrs.push(placeholderName))
|
|
120
|
+
}
|
|
121
|
+
observedAttrs = [...new Set(observedAttrs.filter(isNotBuiltinAttribute))];
|
|
122
|
+
window.customElements.define(elementName, class extends HTMLElement {
|
|
123
|
+
static formAssociated = formAssociated;
|
|
124
|
+
static observedAttributes = observedAttrs;
|
|
125
|
+
|
|
126
|
+
constructor() {
|
|
127
|
+
super();
|
|
128
|
+
|
|
129
|
+
/** @type {HTMLTemplateElement} */
|
|
130
|
+
this._template = template || [...document.getElementsByTagName('template')].find(x => x.getAttribute('decue') === elementName);
|
|
131
|
+
if (!this._template) {
|
|
132
|
+
throw `Template for "${elementName}" not found. Make sure it comes in the DOM before any corresponding custom element.`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!formAssociated && this._template.hasAttribute('formAssociated')) {
|
|
136
|
+
throw `Cannot declare a predefined custom element "${elementName}" as formAssociated. Move it from elements="..." to formAssociated="...".`;
|
|
137
|
+
}
|
|
138
|
+
|
|
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
|
+
if (formAssociated) {
|
|
155
|
+
// 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);
|
|
159
|
+
Object.defineProperties(this, {
|
|
160
|
+
internals: { value: internals, writable: false },
|
|
161
|
+
value: { get: () => value, set: newValue => {
|
|
162
|
+
value = newValue;
|
|
163
|
+
internals.setFormValue(value);
|
|
164
|
+
// @ts-ignore
|
|
165
|
+
ths.checkValidity();
|
|
166
|
+
}},
|
|
167
|
+
|
|
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 },
|
|
174
|
+
|
|
175
|
+
// @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: () => {
|
|
179
|
+
fireEvent(ths, 'checkvalidity');
|
|
180
|
+
return internals.checkValidity();
|
|
181
|
+
}, writable: false },
|
|
182
|
+
reportValidity: { value: internals.reportValidity.bind(internals), writable: false }
|
|
183
|
+
});
|
|
184
|
+
this.value = value;
|
|
185
|
+
if (!this.hasAttribute('tabindex')) {
|
|
186
|
+
this.tabIndex = 0;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
connectedCallback() {
|
|
192
|
+
/** @type {ShadowMode} */
|
|
193
|
+
this._shadow = /** @type {ShadowMode} */ (this.getAttribute('shadow') || this._template.getAttribute('shadow') || defaultShadow);
|
|
194
|
+
if (!['open', 'closed', 'none'].includes(this._shadow)) {
|
|
195
|
+
throw `Invalid shadow DOM mode "${this._shadow}" for element "${elementName}". Must be one of "open", "closed" or "none".`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const root = this._shadow === 'none' ? /** @type {HTMLElement} */ (this) : this.attachShadow({ mode: this._shadow, delegatesFocus: true });
|
|
199
|
+
const content = /** @type {DocumentFragment} */ (this._template.content.cloneNode(true));
|
|
200
|
+
|
|
201
|
+
const finalize = () => {
|
|
202
|
+
dbg('Finalizing...');
|
|
203
|
+
|
|
204
|
+
if (this._shadow === 'none') {
|
|
205
|
+
// Implement slotting manually when no shadow DOM in use
|
|
206
|
+
lightDOMSlotting(root, content);
|
|
207
|
+
dbg('Slots initialized');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// nodes having placeholder references, and thus need to be updated when attributes change.
|
|
211
|
+
/** @type {[Node,string,string][]} */
|
|
212
|
+
this._boundNodes = [];
|
|
213
|
+
|
|
214
|
+
// Find all placeholders in the attributes and text of template or element content.
|
|
215
|
+
[content, this].forEach(x =>
|
|
216
|
+
forEachPlaceholder(x, (node,attributeName,_) => this._boundNodes.push(node instanceof HTMLElement
|
|
217
|
+
? [node, node.getAttribute(attributeName), attributeName]
|
|
218
|
+
: [node, node.data, undefined])));
|
|
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));
|
|
227
|
+
}
|
|
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
|
+
}
|
|
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));
|
|
242
|
+
|
|
243
|
+
fireEvent(this, 'connect');
|
|
244
|
+
};
|
|
245
|
+
|
|
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();
|
|
253
|
+
finalize();
|
|
254
|
+
});
|
|
255
|
+
observer.observe(this.parentElement, { childList: true });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** @type { (name: string, oldValue: string, newValue: string) => void } */
|
|
260
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
261
|
+
if (oldValue !== newValue) {
|
|
262
|
+
if (name === 'value' && this.value !== newValue) {
|
|
263
|
+
this.value = newValue;
|
|
264
|
+
}
|
|
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
|
+
}
|
|
273
|
+
fireEvent(this, 'attributechange', { name, oldValue, newValue })
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
disconnectedCallback() { fireEvent(this, 'disconnect') }
|
|
278
|
+
adoptedCallback() { fireEvent(this, 'adopt') }
|
|
279
|
+
/** @type { (form:HTMLFormElement) => void } */
|
|
280
|
+
formAssociatedCallback(form) { fireEvent(this, 'formassociate', { form }) }
|
|
281
|
+
/** @type { (disabled:boolean) => void } */
|
|
282
|
+
formDisabledCallback(disabled) { fireEvent(this, 'formdisable', { disabled }) }
|
|
283
|
+
formResetCallback() { fireEvent(this, 'formreset') }
|
|
284
|
+
/** @type { (state: any, mode: "autocomplete"|"restore") => void } */
|
|
285
|
+
formStateRestoreCallback(state, mode) { fireEvent(this, 'formstaterestore', { state, mode }) }
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** @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');
|
|
295
|
+
|
|
296
|
+
// 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)
|
|
300
|
+
.split(/\s+/)
|
|
301
|
+
.map(x => x.split(/\[|]/))
|
|
302
|
+
.forEach(([elementName,observedAttributes]) =>
|
|
303
|
+
defineElement(debug, elementName, undefined, formAssociated, (observedAttributes ? observedAttributes.split(',') : [])));
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
/** @type { (template:HTMLTemplateElement) => void } */
|
|
308
|
+
const processTemplate = template => {
|
|
309
|
+
const name = template.getAttribute('decue');
|
|
310
|
+
if (name && !window.customElements.get(name)) {
|
|
311
|
+
defineElement(debug, name, template, template.hasAttribute('form-associated'), []);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
window.addEventListener('DOMContentLoaded', () => {
|
|
316
|
+
// define all custom elements not already defined
|
|
317
|
+
[...document.getElementsByTagName('template')].forEach(processTemplate);
|
|
318
|
+
|
|
319
|
+
// define all custom elements included in <object> tags
|
|
320
|
+
[...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);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
processTemplate: processTemplate,
|
|
330
|
+
defineElement: defineElement
|
|
331
|
+
};
|
|
332
|
+
})();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var decue=function(){const d=document.currentScript.getAttribute("shadow")||"none";if(!["open","closed","none"].includes(d)){throw`Invalid default shadow DOM mode "${d}". Must be one of "open", "closed" or "none".`}const a=/{([a-zA-Z_0-9]+)((?:\|[.]?[a-zA-Z_0-9]+)*)}/g;const c=t=>!(t==="shadow"||t.startsWith("decue-"));const u=t=>{var e=t;while(e&&e.parentNode){if(e.parentNode===document){return true}else if(e.parentNode instanceof ShadowRoot){e=e.parentNode.host}else{e=e.parentNode}}return false};const n=o=>(t,e)=>{const s=e.startsWith(".")?e.substring(1):undefined;const i=window[e];if(!s&&!i){throw`Global function "${e}" not found. Make sure to include it (not deferred) before this element is created.`}return s?typeof t[s]==="function"?t[s]():t[s]:i.apply(o,[t])};const r=(i,o,t)=>t.replaceAll(a,(t,e,s)=>o.hasAttribute(e)?s.split("|").slice(1).reduce(n(i),o.getAttribute(e)):t);const h=(t,[e,s,i])=>{const o=r(e,t,s);if(e instanceof Text){if(o!==e.data){e.data=o}}else if(e instanceof HTMLElement){if(o!==e.getAttribute(i)){e.setAttribute(i,o)}}};const f=(t,s)=>{const e=document.createTreeWalker(t,NodeFilter.SHOW_TEXT|NodeFilter.SHOW_ELEMENT);while(e.nextNode()){const i=e.currentNode;if(i instanceof Text){[...i.data.matchAll(a)].forEach(t=>s(i,undefined,t[1]))}else if(i instanceof HTMLElement){i.getAttributeNames().forEach(e=>[...i.getAttribute(e).matchAll(a)].forEach(t=>s(i,e,t[1])))}}};const b=(i,t)=>{t.querySelectorAll("slot[name]").forEach(t=>{const e=i.querySelectorAll(`:scope > [slot="${t.getAttribute("name")}"]`);const s=e.length>0?e:t.children;t.replaceWith.apply(t,s)});const e=t.querySelector("slot:not([name])");if(e){const s=[...i.childNodes].filter(t=>t.nodeType!==Node.ELEMENT_NODE||!t.hasAttribute("slot"));const o=s.length>0?s:e.children;e.replaceWith.apply(e,o)}};const i=(o,a,n,t,e)=>{const r=t=>o?console.log(a+": "+t):undefined;var l=e;if(n){f(n.content,(t,e,s)=>l.push(s))}l=[...new Set(l.filter(c))];window.customElements.define(a,class extends HTMLElement{static formAssociated=t;static observedAttributes=l;constructor(){super();this._template=n||[...document.getElementsByTagName("template")].find(t=>t.getAttribute("decue")===a);if(!this._template){throw`Template for "${a}" not found. Make sure it comes in the DOM before any corresponding custom element.`}if(!t&&this._template.hasAttribute("formAssociated")){throw`Cannot declare a predefined custom element "${a}" as formAssociated. Move it from elements="..." to formAssociated="...".`}const i=this;this.getAttributeNames().filter(t=>t.startsWith("decue-on:")).map(t=>t.substring("decue-on:".length)).forEach(t=>{const e=i.getAttribute("decue-on:"+t);const s=window[e];if(!s){throw`Global handler function "${e}" for event "${t}" not found.`}r("Registering event listener for event: "+t);i.addEventListener(t,s)});if(t){const o=this.attachInternals();var s=this.getAttribute("value");r("Making form-associated with value: "+s);Object.defineProperties(this,{internals:{value:o,writable:false},value:{get:()=>s,set:t=>{s=t;o.setFormValue(s);i.checkValidity()}},name:{get:()=>this.getAttribute("name")},form:{get:()=>o.form},labels:{get:()=>o.labels},validity:{get:()=>o.validity},validationMessage:{get:()=>o.validationMessage},willValidate:{get:()=>o.willValidate},setFormValue:{value:(t,e)=>o.setFormValue(s=t,e),writable:false},setValidity:{value:o.setValidity.bind(o),writable:false},checkValidity:{value:()=>{m(i,"checkvalidity");return o.checkValidity()},writable:false},reportValidity:{value:o.reportValidity.bind(o),writable:false}});this.value=s;if(!this.hasAttribute("tabindex")){this.tabIndex=0}}}connectedCallback(){this._shadow=this.getAttribute("shadow")||this._template.getAttribute("shadow")||d;if(!["open","closed","none"].includes(this._shadow)){throw`Invalid shadow DOM mode "${this._shadow}" for element "${a}". Must be one of "open", "closed" or "none".`}const t=this._shadow==="none"?this:this.attachShadow({mode:this._shadow,delegatesFocus:true});const i=this._template.content.cloneNode(true);const e=()=>{r("Finalizing...");if(this._shadow==="none"){b(t,i);r("Slots initialized")}this._boundNodes=[];[i,this].forEach(t=>f(t,(t,e,s)=>this._boundNodes.push(t instanceof HTMLElement?[t,t.getAttribute(e),e]:[t,t.data,undefined])));const e=this;const s=this.getAttributeNames().filter(c).filter(t=>!l.includes(t));if(s.length>0&&this._boundNodes.length>0){new MutationObserver(t=>t.forEach(t=>{if(s.includes(t.attributeName)){e.attributeChangedCallback(t.attributeName,t.oldValue,t.target.getAttribute(t.attributeName))}})).observe(this,{attributes:true});this._decueMutationObserved=true;if(o){r("Observing attributes with MutationObserver: "+s.join(" "));this.setAttribute("data-decue-mutation-observed-attributes",s.join(" "))}}if(o&&l.length>0){r("Observing attributes with observedAttributes: "+l.join(" "));this.setAttribute("data-decue-observed-attributes",l.join(" "))}t.append(i);this._boundNodes.forEach(t=>h(e,t));m(this,"connect")};if(n){e()}else{const s=new MutationObserver(()=>{s.disconnect();e()});s.observe(this.parentElement,{childList:true})}}attributeChangedCallback(t,e,s){if(e!==s){if(t==="value"&&this.value!==s){this.value=s}if(this._boundNodes){const i=this._boundNodes.filter(([t,e])=>u(t));if(i.length!==this._boundNodes.length){this._boundNodes=i}const o=this;this._boundNodes.forEach(t=>h(o,t))}m(this,"attributechange",{name:t,oldValue:e,newValue:s})}}disconnectedCallback(){m(this,"disconnect")}adoptedCallback(){m(this,"adopt")}formAssociatedCallback(t){m(this,"formassociate",{form:t})}formDisabledCallback(t){m(this,"formdisable",{disabled:t})}formResetCallback(){m(this,"formreset")}formStateRestoreCallback(t,e){m(this,"formstaterestore",{state:t,mode:e})}})};const m=(t,e,s)=>{t.dispatchEvent(new CustomEvent(e,{detail:s||{},bubbles:true}))};const o=document.currentScript.hasAttribute("debug");[{attr:"elements",formAssociated:false},{attr:"form-associated",formAssociated:true}].forEach(({attr:t,formAssociated:s})=>{if(document.currentScript.hasAttribute(t)){document.currentScript.getAttribute(t).split(/\s+/).map(t=>t.split(/\[|]/)).forEach(([t,e])=>i(o,t,undefined,s,e?e.split(","):[]))}});const e=t=>{const e=t.getAttribute("decue");if(e&&!window.customElements.get(e)){i(o,e,t,t.hasAttribute("form-associated"),[])}};window.addEventListener("DOMContentLoaded",()=>{[...document.getElementsByTagName("template")].forEach(e);[...document.getElementsByTagName("object")].filter(t=>t.getAttribute("type")==="text/html").forEach(t=>{t.addEventListener("load",()=>[...t.contentDocument.getElementsByTagName("template")].forEach(e));[...t.contentDocument.getElementsByTagName("template")].forEach(e)})});return{processTemplate:e,defineElement:i}}();
|