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.
Files changed (82) hide show
  1. package/.editorconfig +17 -17
  2. package/.gitlab-ci.yml +18 -18
  3. package/CHANGELOG.md +201 -0
  4. package/demo/sample.js +1 -1
  5. package/demo/screen.css +16 -16
  6. package/demo/theme.css +1 -0
  7. package/dist/p-elements-core-modern.js +1 -1
  8. package/dist/p-elements-core.js +1 -1
  9. package/docs/package-lock.json +6897 -6897
  10. package/docs/package.json +27 -27
  11. package/docs/src/404.md +8 -8
  12. package/docs/src/_data/demos/hello-world/hello-world.tsx +35 -35
  13. package/docs/src/_data/demos/hello-world/index.html +10 -10
  14. package/docs/src/_data/demos/hello-world/project.json +7 -7
  15. package/docs/src/_data/demos/timer/demo-timer.tsx +120 -120
  16. package/docs/src/_data/demos/timer/icons.tsx +62 -62
  17. package/docs/src/_data/demos/timer/index.html +12 -12
  18. package/docs/src/_data/demos/timer/project.json +8 -8
  19. package/docs/src/_data/global.js +13 -13
  20. package/docs/src/_data/helpers.js +19 -19
  21. package/docs/src/_includes/layouts/base.njk +30 -30
  22. package/docs/src/_includes/layouts/playground.njk +40 -40
  23. package/docs/src/_includes/partials/app-header.njk +8 -8
  24. package/docs/src/_includes/partials/head.njk +14 -14
  25. package/docs/src/_includes/partials/nav.njk +19 -19
  26. package/docs/src/_includes/partials/top-nav.njk +51 -51
  27. package/docs/src/documentation/custom-element.md +221 -221
  28. package/docs/src/documentation/decorators/bind.md +71 -71
  29. package/docs/src/documentation/decorators/custom-element-config.md +63 -63
  30. package/docs/src/documentation/decorators/property.md +83 -83
  31. package/docs/src/documentation/decorators/query.md +66 -66
  32. package/docs/src/documentation/decorators/render-property-on-set.md +60 -60
  33. package/docs/src/documentation/decorators.md +9 -9
  34. package/docs/src/documentation/reactive-properties.md +53 -53
  35. package/docs/src/index.d.ts +25 -25
  36. package/docs/src/index.md +3 -3
  37. package/docs/src/scripts/components/app-mode-switch/app-mode-switch.css +78 -78
  38. package/docs/src/scripts/components/app-mode-switch/app-mode-switch.tsx +166 -166
  39. package/docs/src/scripts/components/app-playground/app-playground.tsx +189 -189
  40. package/docs/tsconfig.json +22 -22
  41. package/index.html +15 -2
  42. package/p-elements-core.d.ts +12 -3
  43. package/package.json +11 -4
  44. package/readme.md +206 -206
  45. package/src/custom-element-controller.test.ts +226 -0
  46. package/src/custom-element-controller.ts +31 -31
  47. package/src/custom-element.test.ts +906 -0
  48. package/src/custom-element.ts +471 -188
  49. package/src/custom-style-element.ts +4 -1
  50. package/src/decorators/bind.test.ts +163 -0
  51. package/src/decorators/bind.ts +46 -46
  52. package/src/decorators/custom-element-config.ts +17 -17
  53. package/src/decorators/property.test.ts +279 -0
  54. package/src/decorators/property.ts +822 -150
  55. package/src/decorators/query.test.ts +146 -0
  56. package/src/decorators/query.ts +12 -12
  57. package/src/decorators/render-property-on-set.ts +3 -3
  58. package/src/helpers/css.test.ts +150 -0
  59. package/src/helpers/css.ts +71 -71
  60. package/src/maquette/cache.test.ts +150 -0
  61. package/src/maquette/cache.ts +35 -35
  62. package/src/maquette/dom.test.ts +263 -0
  63. package/src/maquette/dom.ts +115 -115
  64. package/src/maquette/h.test.ts +165 -0
  65. package/src/maquette/h.ts +100 -100
  66. package/src/maquette/index.ts +12 -12
  67. package/src/maquette/interfaces.ts +536 -536
  68. package/src/maquette/jsx.ts +61 -61
  69. package/src/maquette/mapping.test.ts +294 -0
  70. package/src/maquette/mapping.ts +56 -56
  71. package/src/maquette/maquette.test.ts +493 -0
  72. package/src/maquette/projection.test.ts +366 -0
  73. package/src/maquette/projection.ts +666 -666
  74. package/src/maquette/projector.test.ts +351 -0
  75. package/src/maquette/projector.ts +200 -200
  76. package/src/sample/mixin/highlight.tsx +33 -32
  77. package/src/sample/sample.tsx +167 -7
  78. package/src/test-setup.ts +85 -0
  79. package/src/test-utils.ts +223 -0
  80. package/tsconfig.json +1 -0
  81. package/vitest.config.ts +41 -0
  82. package/webpack.config.js +1 -1
@@ -1,7 +1,12 @@
1
- import { PropertyInfo } from "./decorators/property";
2
- import { replaceApplyToCssVars } from "./helpers/css";
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
- constructor(self?: any) {
17
- super();
18
- this.#init();
19
- this.#callInitFunction();
20
- return self;
21
- }
22
-
23
- #properties: PropertyInfo[];
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
- #delegatesFocus;
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
- #internals: ElementInternals;
72
+ #internalsObjectUntilAttached: object | null;
42
73
 
43
- #controllers :any[] = [];
74
+ readonly #controllers: ICustomElementController[] = [];
44
75
 
45
- #connected = false;
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
- #canReflect = false;
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
- get _canReflect(): boolean {
50
- return this.#canReflect;
114
+ this.#init();
115
+ this.#callInitFunction();
116
+ return self;
51
117
  }
52
118
 
53
- get properties() : readonly PropertyInfo[] {
54
- return this.#properties;
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
- addController(controller: ICustomElementController) {
58
- this.#controllers.push(controller);
59
- if (this.#connected && controller.connected) {
60
- controller.connected();
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
- #callInitFunction() {
65
- if (typeof (this as any).init === "function") {
66
- (this as any).init();
67
- this.#controllers.forEach((controller) => {
68
- controller?.init();
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
- #init() {
74
- this.#initProperties();
75
- const staticProjectorMode = (this.constructor as any).projectorMode;
76
- this.#projectorMode = staticProjectorMode ? staticProjectorMode : "append";
77
- const isFormAssociated = (this.constructor as any).formAssociated;
78
- const delegatesFocus = (this.constructor as any).delegatesFocus;
79
- this.#delegatesFocus = delegatesFocus;
80
- if (isFormAssociated) {
81
- this.#internals = this.attachInternals();
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 css = (this.constructor as any).style;
84
-
85
- if (css) {
86
- this.#useShadowRoot = true;
87
- this.#projectorMode = "replace";
88
- if (!this.shadowRoot) {
89
- this.attachShadow({ mode: "open", delegatesFocus: this.#delegatesFocus });
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
- #reflectProperties() {
105
- this.#properties.filter((p) => p.reflect).forEach((p) => {
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
- #initProperties = () => {
129
- const propertyNames = (this.constructor as any).__properties__;
130
- this.#properties = [];
131
- if (!propertyNames) {
132
- return;
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.#properties = propertyNames.map((propertyName) => {
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
- #polyfillCssApply(): string {
147
- let style = replaceApplyToCssVars(this.#cssText);
148
- if (this.#cssText !== style) {
149
- this.#cssText = style;
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
- #initStylesheet(style: string) {
165
- this.#cssText = style;
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
- #getHashCode(s: string) {
174
- for (var i = 0, h = 0; i < s.length; i++)
175
- h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
176
- return h;
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
- protected get internals() {
180
- return this.#internals;
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
- protected set internals(elementInternals: ElementInternals) {
184
- this.#internals = elementInternals;
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 fragmet = document.createDocumentFragment();
240
- fragmet.appendChild(templateElement.content);
241
- const styleElement = fragmet.querySelector("style");
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({ mode: "open", delegatesFocus: this.#delegatesFocus });
365
+ this.attachShadow({
366
+ mode: "open",
367
+ delegatesFocus: this.#delegatesFocus,
368
+ });
246
369
  }
247
370
  }
248
- this.#initStylesheet(styleElement.textContent);
249
- styleElement.remove();
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 fragmet;
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
- scheduleRender(): void {
291
- this.#projector?.scheduleRender();
292
- }
293
-
294
- renderNow(): void {
295
- this.#projector?.renderNow();
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
- connectedCallback() {
299
- this.#connected = true;
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?.connected();
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
- disconnectedCallback() {
306
- this.#connected = false;
307
- this.#controllers.forEach((controller) => {
308
- controller?.disconnected();
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
- attributeChangedCallback(
313
- name: string,
314
- oldValue: string | null,
315
- newValue: string | null
316
- ) {
317
- const prop = this.#properties.find((p) => p.attribute === name);
318
- if (!prop) {
319
- return;
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(prop.converter) {
323
- this[prop.name] = prop.converter.fromAttribute(newValue);
324
- return;
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
- const type = prop.type;
328
- if (type === "string") {
329
- this[prop.name] = newValue;
330
- } else if (type === "number") {
331
- this[prop.name] = parseFloat(newValue);
332
- } else if (type === "boolean") {
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
- #isFirstRenderStart = true;
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
- #isFirstRenderDone = true;
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
- #invokeRenderLifecycleFn(eventName: string) {
345
- if (this[eventName]){
346
- this[eventName](eventName === "renderStart" ? this.#isFirstRenderStart : this.#isFirstRenderDone);
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
- const controllerEventName = `host${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}`;
349
- this.#controllers.forEach((controller) => {
350
- if (controller[controllerEventName]) {
351
- controller[controllerEventName](eventName === "renderStart" ? this.#isFirstRenderStart : this.#isFirstRenderDone);
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
- if (eventName === "renderStart") {
355
- this.#isFirstRenderStart = false;
356
- } else {
357
- this.#isFirstRenderDone = false;
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
  }