p-elements-core 1.2.30 → 1.2.32-rc-10
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/.editorconfig +17 -17
- package/.gitlab-ci.yml +18 -18
- package/CHANGELOG.md +201 -0
- package/demo/sample.js +1 -1
- package/demo/screen.css +16 -16
- package/demo/theme.css +1 -0
- package/dist/p-elements-core-modern.js +1 -1
- package/dist/p-elements-core.js +1 -1
- package/docs/package-lock.json +6897 -6897
- package/docs/package.json +27 -27
- package/docs/src/404.md +8 -8
- package/docs/src/_data/demos/hello-world/hello-world.tsx +35 -35
- package/docs/src/_data/demos/hello-world/index.html +10 -10
- package/docs/src/_data/demos/hello-world/project.json +7 -7
- package/docs/src/_data/demos/timer/demo-timer.tsx +120 -120
- package/docs/src/_data/demos/timer/icons.tsx +62 -62
- package/docs/src/_data/demos/timer/index.html +12 -12
- package/docs/src/_data/demos/timer/project.json +8 -8
- package/docs/src/_data/global.js +13 -13
- package/docs/src/_data/helpers.js +19 -19
- package/docs/src/_includes/layouts/base.njk +30 -30
- package/docs/src/_includes/layouts/playground.njk +40 -40
- package/docs/src/_includes/partials/app-header.njk +8 -8
- package/docs/src/_includes/partials/head.njk +14 -14
- package/docs/src/_includes/partials/nav.njk +19 -19
- package/docs/src/_includes/partials/top-nav.njk +51 -51
- package/docs/src/documentation/custom-element.md +221 -221
- package/docs/src/documentation/decorators/bind.md +71 -71
- package/docs/src/documentation/decorators/custom-element-config.md +63 -63
- package/docs/src/documentation/decorators/property.md +83 -83
- package/docs/src/documentation/decorators/query.md +66 -66
- package/docs/src/documentation/decorators/render-property-on-set.md +60 -60
- package/docs/src/documentation/decorators.md +9 -9
- package/docs/src/documentation/reactive-properties.md +53 -53
- package/docs/src/index.d.ts +25 -25
- package/docs/src/index.md +3 -3
- package/docs/src/scripts/components/app-mode-switch/app-mode-switch.css +78 -78
- package/docs/src/scripts/components/app-mode-switch/app-mode-switch.tsx +166 -166
- package/docs/src/scripts/components/app-playground/app-playground.tsx +189 -189
- package/docs/tsconfig.json +22 -22
- package/index.html +15 -2
- package/p-elements-core.d.ts +12 -3
- package/package.json +11 -4
- package/readme.md +206 -206
- package/src/custom-element-controller.test.ts +226 -0
- package/src/custom-element-controller.ts +31 -31
- package/src/custom-element.test.ts +906 -0
- package/src/custom-element.ts +471 -188
- package/src/custom-style-element.ts +4 -1
- package/src/decorators/bind.test.ts +163 -0
- package/src/decorators/bind.ts +46 -46
- package/src/decorators/custom-element-config.ts +17 -17
- package/src/decorators/property.test.ts +279 -0
- package/src/decorators/property.ts +822 -150
- package/src/decorators/query.test.ts +146 -0
- package/src/decorators/query.ts +12 -12
- package/src/decorators/render-property-on-set.ts +3 -3
- package/src/helpers/css.test.ts +150 -0
- package/src/helpers/css.ts +71 -71
- package/src/maquette/cache.test.ts +150 -0
- package/src/maquette/cache.ts +35 -35
- package/src/maquette/dom.test.ts +263 -0
- package/src/maquette/dom.ts +115 -115
- package/src/maquette/h.test.ts +165 -0
- package/src/maquette/h.ts +100 -100
- package/src/maquette/index.ts +12 -12
- package/src/maquette/interfaces.ts +536 -536
- package/src/maquette/jsx.ts +61 -61
- package/src/maquette/mapping.test.ts +294 -0
- package/src/maquette/mapping.ts +56 -56
- package/src/maquette/maquette.test.ts +493 -0
- package/src/maquette/projection.test.ts +366 -0
- package/src/maquette/projection.ts +666 -666
- package/src/maquette/projector.test.ts +351 -0
- package/src/maquette/projector.ts +200 -200
- package/src/sample/mixin/highlight.tsx +33 -32
- package/src/sample/sample.tsx +167 -7
- package/src/test-setup.ts +85 -0
- package/src/test-utils.ts +223 -0
- package/tsconfig.json +1 -0
- package/vitest.config.ts +41 -0
- package/webpack.config.js +1 -1
package/src/custom-element.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
type PropertyOptionsWithName,
|
|
3
|
+
isSettingAttribute,
|
|
4
|
+
processPendingUpdates,
|
|
5
|
+
} from "./decorators/property";
|
|
6
|
+
|
|
3
7
|
import { ICustomElementController } from "./custom-element-controller";
|
|
4
8
|
import { Projector, VNode } from "./maquette/interfaces";
|
|
9
|
+
import { replaceApplyToCssVars } from "./helpers/css";
|
|
5
10
|
|
|
6
11
|
export type ElementProjectorMode = "append" | "merge" | "replace";
|
|
7
12
|
|
|
@@ -10,22 +15,34 @@ declare var HTMLElement: {
|
|
|
10
15
|
new (param?): HTMLElement;
|
|
11
16
|
};
|
|
12
17
|
|
|
18
|
+
interface ComponentConstructor {
|
|
19
|
+
readonly css?: string;
|
|
20
|
+
readonly formAssociated?: boolean;
|
|
21
|
+
readonly delegatesFocus?: boolean;
|
|
22
|
+
_propertyInfo?: Map<string, PropertyOptionsWithName>;
|
|
23
|
+
observedAttributes?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
13
26
|
const documentAdoptedStyleSheets: number[] = [];
|
|
14
27
|
|
|
15
28
|
export abstract class CustomElement extends HTMLElement {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Map of reactive property metadata, indexed by property name
|
|
31
|
+
* Used by @property decorator and attributeChangedCallback
|
|
32
|
+
*
|
|
33
|
+
* @static
|
|
34
|
+
* @type {Map<string, PropertyOptionsWithName>}
|
|
35
|
+
*/
|
|
36
|
+
static readonly _propertyInfo: Map<string, PropertyOptionsWithName> =
|
|
37
|
+
new Map();
|
|
24
38
|
|
|
25
39
|
#projector: Projector;
|
|
26
40
|
|
|
27
41
|
#projectorMode: ElementProjectorMode;
|
|
28
42
|
|
|
43
|
+
/** Whether the component is currently connected to the DOM */
|
|
44
|
+
#connected = false;
|
|
45
|
+
|
|
29
46
|
#cssText: string;
|
|
30
47
|
|
|
31
48
|
#cssSheet: CSSStyleSheet;
|
|
@@ -34,159 +51,262 @@ export abstract class CustomElement extends HTMLElement {
|
|
|
34
51
|
|
|
35
52
|
#isSheetAdopted = false;
|
|
36
53
|
|
|
37
|
-
#useShadowRoot;
|
|
54
|
+
#useShadowRoot = false;
|
|
55
|
+
|
|
56
|
+
#delegatesFocus = false;
|
|
57
|
+
|
|
58
|
+
#isFirstRenderStart = true;
|
|
59
|
+
|
|
60
|
+
#isFirstRenderDone = true;
|
|
38
61
|
|
|
39
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Form API internals for form-associated custom elements
|
|
64
|
+
*
|
|
65
|
+
* @type {ElementInternals | null}
|
|
66
|
+
* @description Provides access to form APIs when formAssociated is true.
|
|
67
|
+
* Available for form-associated custom elements to interact with forms.
|
|
68
|
+
* Null if element is not form-associated.
|
|
69
|
+
*/
|
|
70
|
+
#internals: ElementInternals | null = null;
|
|
40
71
|
|
|
41
|
-
#
|
|
72
|
+
#internalsObjectUntilAttached: object | null;
|
|
42
73
|
|
|
43
|
-
#controllers
|
|
74
|
+
readonly #controllers: ICustomElementController[] = [];
|
|
44
75
|
|
|
45
|
-
|
|
76
|
+
/** Promise that resolves when the current update is complete */
|
|
77
|
+
#updatePromise: Promise<void> | null = null;
|
|
78
|
+
|
|
79
|
+
/** Resolve function for the current update promise */
|
|
80
|
+
#updateResolve: (() => void) | null = null;
|
|
81
|
+
|
|
82
|
+
constructor(self?: any) {
|
|
83
|
+
super();
|
|
46
84
|
|
|
47
|
-
|
|
85
|
+
const warn = (fn: string) => {
|
|
86
|
+
console.warn(
|
|
87
|
+
`ElementInternals.${fn} called before element was connected. Call will be ignored.`,
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
this.#internalsObjectUntilAttached = {
|
|
91
|
+
setValidity: (
|
|
92
|
+
_flags?: ValidityStateFlags,
|
|
93
|
+
_message?: string,
|
|
94
|
+
_anchor?: HTMLElement,
|
|
95
|
+
) => {
|
|
96
|
+
warn("setValidity");
|
|
97
|
+
},
|
|
98
|
+
reportValidity: () => {
|
|
99
|
+
warn("reportValidity");
|
|
100
|
+
return false;
|
|
101
|
+
},
|
|
102
|
+
checkValidity: () => {
|
|
103
|
+
warn("checkValidity");
|
|
104
|
+
return true;
|
|
105
|
+
},
|
|
106
|
+
setFormValue: (
|
|
107
|
+
_value: File | string | FormData | null,
|
|
108
|
+
_state?: File | string | FormData | null,
|
|
109
|
+
) => {
|
|
110
|
+
warn("setFormValue");
|
|
111
|
+
},
|
|
112
|
+
};
|
|
48
113
|
|
|
49
|
-
|
|
50
|
-
|
|
114
|
+
this.#init();
|
|
115
|
+
this.#callInitFunction();
|
|
116
|
+
return self;
|
|
51
117
|
}
|
|
52
118
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
119
|
+
/**
|
|
120
|
+
* Whether the component is currently connected to the DOM.
|
|
121
|
+
*
|
|
122
|
+
* @returns {boolean} True if connected, false otherwise
|
|
123
|
+
*/
|
|
124
|
+
get isConnected(): boolean {
|
|
125
|
+
return this.#connected;
|
|
126
|
+
}
|
|
56
127
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Array of reactive property metadata for the component, indexed by property name.
|
|
130
|
+
* Used by @property decorator and attributeChangedCallback
|
|
131
|
+
*
|
|
132
|
+
* @returns {readonly PropertyOptionsWithName[]} Array of property metadata
|
|
133
|
+
*/
|
|
134
|
+
get properties(): readonly PropertyOptionsWithName[] {
|
|
135
|
+
const ctor = this.constructor as ComponentConstructor;
|
|
136
|
+
if (!ctor._propertyInfo) {
|
|
137
|
+
return [];
|
|
61
138
|
}
|
|
139
|
+
return Array.from(ctor._propertyInfo.values());
|
|
62
140
|
}
|
|
63
141
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Promise that resolves when the component has finished updating
|
|
144
|
+
* and rendering to the DOM.
|
|
145
|
+
*
|
|
146
|
+
* @returns {Promise<void>} A promise that resolves after the next render
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```typescript
|
|
150
|
+
* component.name = 'new value';
|
|
151
|
+
* await component.updateComplete;
|
|
152
|
+
* // DOM is now updated
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
get updateComplete(): Promise<void> {
|
|
156
|
+
if (!this.#updatePromise) {
|
|
157
|
+
return Promise.resolve();
|
|
70
158
|
}
|
|
159
|
+
return this.#updatePromise;
|
|
71
160
|
}
|
|
72
161
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
this.#
|
|
162
|
+
/**
|
|
163
|
+
* Gets the ElementInternals for form-associated custom elements.
|
|
164
|
+
* Initializes internals on first access if the component is form-associated.
|
|
165
|
+
*
|
|
166
|
+
* @returns {ElementInternals | null} The element internals or null if not form-associated
|
|
167
|
+
*/
|
|
168
|
+
get internals(): ElementInternals | null {
|
|
169
|
+
if (
|
|
170
|
+
!this.#connected &&
|
|
171
|
+
typeof this.#internalsObjectUntilAttached === "object"
|
|
172
|
+
) {
|
|
173
|
+
return this.#internalsObjectUntilAttached as any;
|
|
82
174
|
}
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
this.#
|
|
87
|
-
this.#
|
|
88
|
-
|
|
89
|
-
|
|
175
|
+
const ctor = this.constructor as ComponentConstructor;
|
|
176
|
+
if (this.#internals === null && ctor.formAssociated) {
|
|
177
|
+
const tempInternals = this.#internalsObjectUntilAttached;
|
|
178
|
+
this.#internalsObjectUntilAttached = null;
|
|
179
|
+
this.#internals = this.attachInternals();
|
|
180
|
+
for (const key in tempInternals) {
|
|
181
|
+
if (typeof (this.#internals as any)[key] !== "function") {
|
|
182
|
+
(this.#internals as any)[key] = (tempInternals as any)[key];
|
|
183
|
+
}
|
|
90
184
|
}
|
|
91
|
-
this.#initStylesheet(css);
|
|
92
|
-
const div = document.createElement("div");
|
|
93
|
-
this.shadowRoot.appendChild(div);
|
|
94
|
-
requestAnimationFrame(() => {
|
|
95
|
-
this.createProjector(div, (this as any).render);
|
|
96
|
-
});
|
|
97
|
-
window.addEventListener("updatecssapply", () => {
|
|
98
|
-
this.#polyfillCssApply();
|
|
99
|
-
});
|
|
100
185
|
}
|
|
101
|
-
|
|
186
|
+
return this.#internals;
|
|
102
187
|
}
|
|
103
188
|
|
|
104
|
-
|
|
105
|
-
this.#
|
|
106
|
-
if (p.type === "string" && this[p.name] !== undefined && this[p.name] !== null) {
|
|
107
|
-
this.setAttribute(p.attribute, this[p.name]);
|
|
108
|
-
} else if (p.type === "number" && this[p.name] !== undefined && this[p.name] !== null) {
|
|
109
|
-
this.setAttribute(p.attribute, this[p.name].toString());
|
|
110
|
-
} else if (p.type === "boolean" && this[p.name] === true) {
|
|
111
|
-
if (this[p.name] === true && !this.hasAttribute(p.attribute)) {
|
|
112
|
-
this.setAttribute(p.attribute, "");
|
|
113
|
-
}
|
|
114
|
-
} else if (p.type === "boolean" && this[p.name] === false) {
|
|
115
|
-
if (this.hasAttribute(p.attribute)) {
|
|
116
|
-
this.removeAttribute(p.attribute);
|
|
117
|
-
}
|
|
118
|
-
} else if (p.type === "object" && this[p.name] !== undefined && this[p.name] !== null) {
|
|
119
|
-
if (p.converter) {
|
|
120
|
-
this.setAttribute(p.attribute, p.converter.toAttribute(this[p.name]));
|
|
121
|
-
} else {
|
|
122
|
-
this.setAttribute(p.attribute, JSON.stringify(this[p.name]));
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
});
|
|
189
|
+
protected set internals(elementInternals: ElementInternals) {
|
|
190
|
+
this.#internals = elementInternals;
|
|
126
191
|
}
|
|
127
192
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Request an update and re-render of the component.
|
|
195
|
+
* Creates a promise that resolves when the update is complete.
|
|
196
|
+
*
|
|
197
|
+
* @returns {Promise<void>} A promise that resolves after rendering
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```typescript
|
|
201
|
+
* await component.requestUpdate();
|
|
202
|
+
* // DOM is now updated
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
requestUpdate(): Promise<void> {
|
|
206
|
+
if (!this.#updatePromise) {
|
|
207
|
+
this.#updatePromise = new Promise((resolve) => {
|
|
208
|
+
this.#updateResolve = resolve;
|
|
209
|
+
});
|
|
210
|
+
this.scheduleRender();
|
|
133
211
|
}
|
|
134
|
-
this.#
|
|
135
|
-
return { ...this[`__property_${propertyName}__`], name: propertyName };
|
|
136
|
-
});
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
get #hasAdoptedStyleSheetsSupport(): boolean {
|
|
140
|
-
return (
|
|
141
|
-
Array.isArray(document.adoptedStyleSheets) &&
|
|
142
|
-
"replace" in CSSStyleSheet.prototype
|
|
143
|
-
);
|
|
212
|
+
return this.#updatePromise;
|
|
144
213
|
}
|
|
145
214
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (this.#
|
|
149
|
-
|
|
150
|
-
if (this.#hasAdoptedStyleSheetsSupport && this.#cssSheet) {
|
|
151
|
-
this.#cssSheet.replaceSync(style);
|
|
152
|
-
} else if (!this.#hasAdoptedStyleSheetsSupport) {
|
|
153
|
-
if (this.#linkElement) {
|
|
154
|
-
URL.revokeObjectURL(this.#linkElement.href);
|
|
155
|
-
}
|
|
156
|
-
this.#linkElement.href = URL.createObjectURL(
|
|
157
|
-
new Blob([style], { type: "text/css" })
|
|
158
|
-
);
|
|
159
|
-
}
|
|
215
|
+
addController(controller: ICustomElementController) {
|
|
216
|
+
this.#controllers.push(controller);
|
|
217
|
+
if (this.#connected && controller.connected) {
|
|
218
|
+
controller.connected();
|
|
160
219
|
}
|
|
161
|
-
return style;
|
|
162
220
|
}
|
|
163
221
|
|
|
164
|
-
|
|
165
|
-
this.#
|
|
166
|
-
if (this.#useShadowRoot && this.shadowRoot) {
|
|
167
|
-
this.addStylesheetToRootNode(style, this.shadowRoot);
|
|
168
|
-
} else if (!this.#useShadowRoot) {
|
|
169
|
-
this.addStylesheetToRootNode(style, document);
|
|
170
|
-
}
|
|
222
|
+
scheduleRender(): void {
|
|
223
|
+
this.#projector?.scheduleRender();
|
|
171
224
|
}
|
|
172
225
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
226
|
+
/**
|
|
227
|
+
* Immediately render the component to its shadow DOM
|
|
228
|
+
* Calls the abstract render() method and updates DOM via uhtml
|
|
229
|
+
* Re-injects style elements if necessary
|
|
230
|
+
*
|
|
231
|
+
* @public
|
|
232
|
+
*/
|
|
233
|
+
renderNow(): void {
|
|
234
|
+
if (this.#useShadowRoot && !this.shadowRoot) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Create update promise if it doesn't exist (for synchronous renderNow calls)
|
|
239
|
+
if (!this.#updatePromise) {
|
|
240
|
+
this.#updatePromise = new Promise((resolve) => {
|
|
241
|
+
this.#updateResolve = resolve;
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
this.#projector?.renderNow();
|
|
245
|
+
|
|
246
|
+
// Call updated() lifecycle hook after DOM updates are complete
|
|
247
|
+
this.updated("", null, null);
|
|
248
|
+
|
|
249
|
+
// Resolve the update promise after rendering and updated() hook
|
|
250
|
+
if (this.#updateResolve) {
|
|
251
|
+
const resolve = this.#updateResolve;
|
|
252
|
+
this.#updateResolve = null;
|
|
253
|
+
this.#updatePromise = null;
|
|
254
|
+
|
|
255
|
+
// Resolve on next microtask to ensure DOM updates are complete
|
|
256
|
+
resolve();
|
|
257
|
+
}
|
|
177
258
|
}
|
|
178
259
|
|
|
179
|
-
|
|
180
|
-
|
|
260
|
+
/**
|
|
261
|
+
* Lifecycle hook called when a property is updated
|
|
262
|
+
* Override to perform side effects when properties change
|
|
263
|
+
*
|
|
264
|
+
* @param {string} _propertyName - The name of the updated property
|
|
265
|
+
* @param {any} _oldValue - The previous property value
|
|
266
|
+
* @param {any} _newValue - The new property value
|
|
267
|
+
*
|
|
268
|
+
* @virtual
|
|
269
|
+
* @example
|
|
270
|
+
* ```typescript
|
|
271
|
+
* updated(propertyName: string, oldValue: any, newValue: any) {
|
|
272
|
+
* if (propertyName === 'count') {
|
|
273
|
+
* console.log(`Count changed from ${oldValue} to ${newValue}`);
|
|
274
|
+
* }
|
|
275
|
+
* }
|
|
276
|
+
* ```
|
|
277
|
+
*/
|
|
278
|
+
updated(_propertyName: string, _oldValue: any, _newValue: any): void {
|
|
279
|
+
// Default: no-op. Override in subclass to perform side effects.
|
|
181
280
|
}
|
|
182
281
|
|
|
183
|
-
|
|
184
|
-
|
|
282
|
+
/**
|
|
283
|
+
* Determine if component should update when a property changes
|
|
284
|
+
* Override to add custom logic for skipping updates
|
|
285
|
+
*
|
|
286
|
+
* @param {string} _name - The name of the property that changed
|
|
287
|
+
* @param {any} _oldValue - The previous property value
|
|
288
|
+
* @param {any} _newValue - The new property value
|
|
289
|
+
* @returns {boolean} True if the component should update, false otherwise
|
|
290
|
+
*
|
|
291
|
+
* @virtual
|
|
292
|
+
* @default true
|
|
293
|
+
* @example
|
|
294
|
+
* ```typescript
|
|
295
|
+
* shouldUpdate(name: string, oldValue: any, newValue: any): boolean {
|
|
296
|
+
* if (name === 'internal') {
|
|
297
|
+
* return false; // Skip updates for 'internal' property
|
|
298
|
+
* }
|
|
299
|
+
* return true;
|
|
300
|
+
* }
|
|
301
|
+
* ```
|
|
302
|
+
*/
|
|
303
|
+
shouldUpdate(_name: string, _oldValue: any, _newValue: any): boolean {
|
|
304
|
+
return true;
|
|
185
305
|
}
|
|
186
306
|
|
|
187
307
|
protected addStylesheetToRootNode(
|
|
188
308
|
style: string,
|
|
189
|
-
rootNode: ShadowRoot | Document
|
|
309
|
+
rootNode: ShadowRoot | Document,
|
|
190
310
|
) {
|
|
191
311
|
if (this.#hasAdoptedStyleSheetsSupport) {
|
|
192
312
|
if (!this.#cssSheet) {
|
|
@@ -216,7 +336,7 @@ export abstract class CustomElement extends HTMLElement {
|
|
|
216
336
|
this.#linkElement.rel = "stylesheet";
|
|
217
337
|
style = this.#polyfillCssApply();
|
|
218
338
|
this.#linkElement.href = URL.createObjectURL(
|
|
219
|
-
new Blob([style], { type: "text/css" })
|
|
339
|
+
new Blob([style], { type: "text/css" }),
|
|
220
340
|
);
|
|
221
341
|
if (rootNode instanceof Document) {
|
|
222
342
|
const styleHash = this.#getHashCode(style);
|
|
@@ -236,35 +356,44 @@ export abstract class CustomElement extends HTMLElement {
|
|
|
236
356
|
): DocumentFragment {
|
|
237
357
|
const templateElement = document.createElement("template");
|
|
238
358
|
templateElement.innerHTML = template;
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
const styleElement =
|
|
359
|
+
const fragment = document.createDocumentFragment();
|
|
360
|
+
fragment.appendChild(templateElement.content);
|
|
361
|
+
const styleElement = fragment.querySelector("style");
|
|
242
362
|
if (useShadowRoot) {
|
|
243
363
|
this.#useShadowRoot = true;
|
|
244
364
|
if (!this.shadowRoot) {
|
|
245
|
-
this.attachShadow({
|
|
365
|
+
this.attachShadow({
|
|
366
|
+
mode: "open",
|
|
367
|
+
delegatesFocus: this.#delegatesFocus,
|
|
368
|
+
});
|
|
246
369
|
}
|
|
247
370
|
}
|
|
248
|
-
|
|
249
|
-
|
|
371
|
+
if (styleElement) {
|
|
372
|
+
this.#initStylesheet(styleElement.textContent);
|
|
373
|
+
styleElement.remove();
|
|
374
|
+
}
|
|
250
375
|
window.addEventListener("updatecssapply", () => {
|
|
251
376
|
this.#polyfillCssApply();
|
|
252
377
|
});
|
|
253
|
-
|
|
254
|
-
return
|
|
378
|
+
|
|
379
|
+
return fragment;
|
|
255
380
|
}
|
|
256
381
|
|
|
257
382
|
protected adoptStyle(
|
|
258
383
|
root: Document | ShadowRoot,
|
|
259
|
-
css: string
|
|
384
|
+
css: string,
|
|
260
385
|
): string | void {
|
|
261
386
|
this.addStylesheetToRootNode(css, root);
|
|
262
387
|
}
|
|
263
388
|
|
|
264
|
-
protected createProjector(
|
|
389
|
+
protected async createProjector(
|
|
265
390
|
element: Element,
|
|
266
|
-
render: () => VNode
|
|
391
|
+
render: () => VNode,
|
|
267
392
|
): Promise<Projector> {
|
|
393
|
+
return this.#createProjector(element, render);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#createProjector(element: Element, render: () => VNode): Promise<Projector> {
|
|
268
397
|
return new Promise<Projector>((resolve, reject) => {
|
|
269
398
|
let projector: Projector;
|
|
270
399
|
const mode = this.#projectorMode ? this.#projectorMode : "append";
|
|
@@ -274,12 +403,10 @@ export abstract class CustomElement extends HTMLElement {
|
|
|
274
403
|
if (eventName === "renderStart" || eventName === "renderDone") {
|
|
275
404
|
this.#invokeRenderLifecycleFn(eventName);
|
|
276
405
|
}
|
|
277
|
-
}
|
|
406
|
+
},
|
|
278
407
|
});
|
|
279
408
|
projector[mode](element, render.bind(this));
|
|
280
409
|
this.#projector = projector;
|
|
281
|
-
this.#canReflect = true;
|
|
282
|
-
this.#reflectProperties();
|
|
283
410
|
projector.renderNow();
|
|
284
411
|
resolve(projector);
|
|
285
412
|
this.dispatchEvent(new CustomEvent("firstRender", {}));
|
|
@@ -287,74 +414,230 @@ export abstract class CustomElement extends HTMLElement {
|
|
|
287
414
|
});
|
|
288
415
|
}
|
|
289
416
|
|
|
290
|
-
|
|
291
|
-
this
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
417
|
+
#callInitFunction() {
|
|
418
|
+
if (typeof (this as any).init === "function") {
|
|
419
|
+
(this as any).init();
|
|
420
|
+
this.#controllers.forEach((controller) => {
|
|
421
|
+
if (controller?.init) {
|
|
422
|
+
controller.init();
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
296
426
|
}
|
|
297
427
|
|
|
298
|
-
|
|
299
|
-
this
|
|
428
|
+
#invokeRenderLifecycleFn(eventName: string) {
|
|
429
|
+
if (this[eventName]) {
|
|
430
|
+
this[eventName](
|
|
431
|
+
eventName === "renderStart"
|
|
432
|
+
? this.#isFirstRenderStart
|
|
433
|
+
: this.#isFirstRenderDone,
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
const controllerEventName = `host${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}`;
|
|
300
437
|
this.#controllers.forEach((controller) => {
|
|
301
|
-
controller
|
|
438
|
+
if (controller[controllerEventName]) {
|
|
439
|
+
controller[controllerEventName](
|
|
440
|
+
eventName === "renderStart"
|
|
441
|
+
? this.#isFirstRenderStart
|
|
442
|
+
: this.#isFirstRenderDone,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
302
445
|
});
|
|
446
|
+
if (eventName === "renderStart") {
|
|
447
|
+
this.#isFirstRenderStart = false;
|
|
448
|
+
} else {
|
|
449
|
+
this.#isFirstRenderDone = false;
|
|
450
|
+
}
|
|
303
451
|
}
|
|
304
452
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
453
|
+
/**
|
|
454
|
+
* Safe upgrade of properties that may have been set before the element was upgraded.
|
|
455
|
+
* This captures the value, deletes the own property, and resets it to trigger the setter.
|
|
456
|
+
* @private
|
|
457
|
+
*/
|
|
458
|
+
#upgradeProperties() {
|
|
459
|
+
const ctor = this.constructor as ComponentConstructor;
|
|
460
|
+
if (ctor._propertyInfo) {
|
|
461
|
+
const properties = Array.from(ctor._propertyInfo.entries());
|
|
462
|
+
let i = 0;
|
|
463
|
+
const propertiesLength = properties.length;
|
|
464
|
+
while (i < propertiesLength) {
|
|
465
|
+
const [prop] = properties[i];
|
|
466
|
+
if (Object.hasOwn(this, prop)) {
|
|
467
|
+
const value = (this as any)[prop];
|
|
468
|
+
delete (this as any)[prop];
|
|
469
|
+
(this as any)[prop] = value;
|
|
470
|
+
}
|
|
471
|
+
i = i + 1;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
310
474
|
}
|
|
311
475
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if (
|
|
319
|
-
|
|
476
|
+
#init() {
|
|
477
|
+
const staticProjectorMode = (this.constructor as any).projectorMode;
|
|
478
|
+
this.#projectorMode = staticProjectorMode ? staticProjectorMode : "append";
|
|
479
|
+
const isFormAssociated = (this.constructor as any).formAssociated;
|
|
480
|
+
const delegatesFocus = (this.constructor as any).delegatesFocus;
|
|
481
|
+
this.#delegatesFocus = delegatesFocus;
|
|
482
|
+
if (isFormAssociated) {
|
|
483
|
+
this.#internals = this.attachInternals();
|
|
320
484
|
}
|
|
485
|
+
const css = (this.constructor as any).style;
|
|
321
486
|
|
|
322
|
-
if(
|
|
323
|
-
this
|
|
324
|
-
|
|
487
|
+
if (css) {
|
|
488
|
+
this.#useShadowRoot = true;
|
|
489
|
+
this.#projectorMode = "replace";
|
|
490
|
+
if (!this.shadowRoot) {
|
|
491
|
+
this.attachShadow({
|
|
492
|
+
mode: "open",
|
|
493
|
+
delegatesFocus: this.#delegatesFocus,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
this.#initStylesheet(css);
|
|
497
|
+
const div = document.createElement("div");
|
|
498
|
+
this.shadowRoot.appendChild(div);
|
|
499
|
+
requestAnimationFrame(() => {
|
|
500
|
+
this.#createProjector(div, (this as any).render).then(() => {
|
|
501
|
+
this.#upgradeProperties();
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
window.addEventListener("updatecssapply", () => {
|
|
505
|
+
this.#polyfillCssApply();
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
#polyfillCssApply(): string {
|
|
511
|
+
let style = replaceApplyToCssVars(this.#cssText);
|
|
512
|
+
if (this.#cssText !== style) {
|
|
513
|
+
this.#cssText = style;
|
|
514
|
+
if (this.#hasAdoptedStyleSheetsSupport && this.#cssSheet) {
|
|
515
|
+
this.#cssSheet.replaceSync(style);
|
|
516
|
+
} else if (!this.#hasAdoptedStyleSheetsSupport) {
|
|
517
|
+
if (this.#linkElement) {
|
|
518
|
+
URL.revokeObjectURL(this.#linkElement.href);
|
|
519
|
+
}
|
|
520
|
+
this.#linkElement.href = URL.createObjectURL(
|
|
521
|
+
new Blob([style], { type: "text/css" }),
|
|
522
|
+
);
|
|
523
|
+
}
|
|
325
524
|
}
|
|
525
|
+
return style;
|
|
526
|
+
}
|
|
326
527
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
this[prop.name] = newValue !== null;
|
|
334
|
-
} else if (type === "object") {
|
|
335
|
-
this[prop.name] = JSON.parse(newValue);
|
|
528
|
+
#initStylesheet(style: string) {
|
|
529
|
+
this.#cssText = style;
|
|
530
|
+
if (this.#useShadowRoot && this.shadowRoot) {
|
|
531
|
+
this.addStylesheetToRootNode(style, this.shadowRoot);
|
|
532
|
+
} else if (!this.#useShadowRoot) {
|
|
533
|
+
this.addStylesheetToRootNode(style, document);
|
|
336
534
|
}
|
|
337
535
|
}
|
|
338
536
|
|
|
339
|
-
#
|
|
537
|
+
#getHashCode(s: string) {
|
|
538
|
+
for (var i = 0, h = 0; i < s.length; i++)
|
|
539
|
+
h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
|
|
540
|
+
return h;
|
|
541
|
+
}
|
|
340
542
|
|
|
341
|
-
#
|
|
543
|
+
get #hasAdoptedStyleSheetsSupport(): boolean {
|
|
544
|
+
return (
|
|
545
|
+
Array.isArray(document.adoptedStyleSheets) &&
|
|
546
|
+
"replace" in CSSStyleSheet.prototype
|
|
547
|
+
);
|
|
548
|
+
}
|
|
342
549
|
|
|
550
|
+
/**
|
|
551
|
+
* Lifecycle callback invoked when the element is added to the DOM.
|
|
552
|
+
* Initializes controllers, processes pending property updates, and renders.
|
|
553
|
+
*/
|
|
554
|
+
connectedCallback(): void {
|
|
555
|
+
if (this.#connected) {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
this.#connected = true;
|
|
559
|
+
let i = 0;
|
|
560
|
+
const controllersLength = this.#controllers.length;
|
|
561
|
+
while (i < controllersLength) {
|
|
562
|
+
const controller = this.#controllers[i];
|
|
563
|
+
if (controller?.connected) {
|
|
564
|
+
controller.connected();
|
|
565
|
+
}
|
|
566
|
+
i = i + 1;
|
|
567
|
+
}
|
|
568
|
+
// Process any pending updated callbacks that were queued during construction
|
|
569
|
+
processPendingUpdates(this);
|
|
570
|
+
this.renderNow();
|
|
571
|
+
}
|
|
343
572
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
573
|
+
/**
|
|
574
|
+
* Lifecycle callback invoked when the element is removed from the DOM.
|
|
575
|
+
* Marks the component as disconnected.
|
|
576
|
+
*/
|
|
577
|
+
disconnectedCallback(): void {
|
|
578
|
+
if (this.#connected === false) {
|
|
579
|
+
return;
|
|
347
580
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
581
|
+
this.#connected = false;
|
|
582
|
+
let i = 0;
|
|
583
|
+
const controllersLength = this.#controllers.length;
|
|
584
|
+
while (i < controllersLength) {
|
|
585
|
+
const controller = this.#controllers[i];
|
|
586
|
+
if (controller?.disconnected) {
|
|
587
|
+
controller.disconnected();
|
|
352
588
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
589
|
+
i = i + 1;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Lifecycle hook called when observed attributes change
|
|
595
|
+
* Synchronizes HTML attribute changes to reactive properties
|
|
596
|
+
* Prevents infinite loops by skipping updates initiated by property setters
|
|
597
|
+
*
|
|
598
|
+
* @param {string} name - The name of the changed attribute
|
|
599
|
+
* @param {string | null} oldValue - The previous attribute value (unused)
|
|
600
|
+
* @param {string | null} newValue - The new attribute value
|
|
601
|
+
*
|
|
602
|
+
* @private
|
|
603
|
+
*/
|
|
604
|
+
attributeChangedCallback(
|
|
605
|
+
name: string,
|
|
606
|
+
oldValue: string | null,
|
|
607
|
+
newValue: string | null,
|
|
608
|
+
): void {
|
|
609
|
+
// Skip if this attribute change came from a property setter (prevent infinite loop)
|
|
610
|
+
// Also skip if the value didn't actually change (some browsers may call this callback even if the value is the same)
|
|
611
|
+
if (isSettingAttribute(this) || newValue === oldValue) {
|
|
612
|
+
return;
|
|
358
613
|
}
|
|
614
|
+
const ctor = this.constructor as ComponentConstructor;
|
|
615
|
+
if (!ctor?._propertyInfo) {
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const propertyFound: PropertyOptionsWithName | undefined = Array.from(
|
|
620
|
+
ctor._propertyInfo.values(),
|
|
621
|
+
).find(
|
|
622
|
+
(options) =>
|
|
623
|
+
options.attribute === name && typeof options.attribute === "string",
|
|
624
|
+
);
|
|
625
|
+
if (!propertyFound) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const converter = propertyFound.converter;
|
|
629
|
+
let value: unknown = newValue;
|
|
630
|
+
|
|
631
|
+
if (converter?.fromAttribute) {
|
|
632
|
+
value = converter.fromAttribute(newValue);
|
|
633
|
+
} else if (propertyFound.type === Boolean) {
|
|
634
|
+
value = newValue !== null;
|
|
635
|
+
} else if (propertyFound.type === Number) {
|
|
636
|
+
value = newValue === null ? null : Number(newValue);
|
|
637
|
+
}
|
|
638
|
+
(this as Record<string, unknown>)[propertyFound.name] = value;
|
|
639
|
+
this.scheduleRender();
|
|
359
640
|
}
|
|
641
|
+
|
|
642
|
+
abstract render(): VNode;
|
|
360
643
|
}
|