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,150 +1,822 @@
1
- type PropertyType = "string" | "number" | "boolean" | "object";
2
-
3
- export interface AttributeConverter<T> {
4
- fromAttribute?(value: string | null): T;
5
- toAttribute?(value: T): string;
6
- }
7
-
8
- export interface PropertyOptions {
9
- type?: PropertyType;
10
- reflect?: boolean;
11
- attribute?: string;
12
- readonly?: boolean;
13
- converter?: AttributeConverter<any>;
14
- }
15
-
16
- export interface PropertyInfo extends PropertyOptions {
17
- name: string;
18
- }
19
-
20
- const typeToStringValue = <T>(type: PropertyType, value: T): string => {
21
- if (value === undefined) {
22
- return undefined;
23
- }
24
- else if (value === null) {
25
- return null;
26
- }
27
- switch (type) {
28
- case "string":
29
- return value.toString();
30
- case "number":
31
- return value.toString();
32
- case "object":
33
- return JSON.stringify(value);
34
- }
35
- };
36
-
37
- export const property = (propertyOptions: PropertyOptions) => {
38
- return (target: any, name: string) => {
39
- const ctor = target.constructor as any;
40
- const sym = Symbol(name + "_PropertyDecorator");
41
- if (ctor && !ctor.observedAttributes) {
42
- ctor.observedAttributes = [];
43
- }
44
- if (ctor && !ctor.__properties__) {
45
- ctor.__properties__ = [];
46
- }
47
- ctor.__properties__.push(name);
48
- if (
49
- propertyOptions.attribute &&
50
- !propertyOptions.readonly &&
51
- ctor.observedAttributes.indexOf(propertyOptions.attribute) === -1
52
- ) {
53
- ctor.observedAttributes.push(propertyOptions.attribute);
54
- }
55
- Object.defineProperty(target, `__property_${name}__`, {
56
- configurable: true,
57
- enumerable: true,
58
- get: function () {
59
- return propertyOptions;
60
- },
61
- });
62
-
63
- if (propertyOptions.readonly === true) {
64
- Object.defineProperty(target, name, {
65
- get: function () {
66
- return this[sym];
67
- },
68
- set: function (value) {
69
- this[sym] = value;
70
- this?.scheduleRender();
71
- delete this[name];
72
- Object.defineProperty(this, name, {
73
- configurable: true,
74
- enumerable: true,
75
- get: function () {
76
- return this[sym];
77
- },
78
- set: function () {
79
- throw new Error("Cannot set read-only property");
80
- },
81
- });
82
- Object.seal(this[sym]);
83
- },
84
- });
85
- return;
86
- }
87
-
88
- Object.defineProperty(target, name, {
89
- configurable: true,
90
- enumerable: true,
91
- get: function () {
92
- return this[sym];
93
- },
94
- set: function (value) {
95
- const oldValue = this[sym];
96
- const skipSetValue = this?.shouldUpdate && !this.shouldUpdate(name, oldValue, value);
97
- const elementToSetAttribute = this.isCustomElementController ? this.hostElement : this;
98
- const attributeValue = propertyOptions.converter
99
- ? propertyOptions.converter.toAttribute(value)
100
- : typeToStringValue(propertyOptions.type, value);
101
- if (skipSetValue) {
102
- this[sym] = oldValue;
103
- requestAnimationFrame(() => {
104
- elementToSetAttribute.setAttribute(propertyOptions.attribute, oldValue)
105
- });
106
- } else {
107
- this[sym] = value;
108
- }
109
- const handleChange = () => {
110
- if (this[sym] !== oldValue) {
111
- this?.scheduleRender && this.scheduleRender();
112
- this?.updated && this.updated(name, oldValue, this[sym]);
113
- }
114
- };
115
- if (propertyOptions.reflect && propertyOptions.attribute && elementToSetAttribute._canReflect) {
116
- if (propertyOptions.type !== "boolean") {
117
- if (value === undefined || value === null) {
118
- elementToSetAttribute.removeAttribute(propertyOptions.attribute);
119
- handleChange();
120
- return;
121
- }
122
- if (
123
- elementToSetAttribute.getAttribute(propertyOptions.attribute) === attributeValue + ""
124
- ) {
125
- handleChange();
126
- return;
127
- }
128
- if (attributeValue !== null) {
129
- elementToSetAttribute.setAttribute(propertyOptions.attribute, attributeValue + "");
130
- } else {
131
- elementToSetAttribute.removeAttribute(propertyOptions.attribute);
132
- }
133
- } else {
134
-
135
- if (value && !elementToSetAttribute.hasAttribute(propertyOptions.attribute)) {
136
- elementToSetAttribute.setAttribute(propertyOptions.attribute, "");
137
- } else if (!value && elementToSetAttribute.hasAttribute(propertyOptions.attribute)) {
138
- elementToSetAttribute.removeAttribute(propertyOptions.attribute);
139
- }
140
- handleChange();
141
- return;
142
- }
143
- }
144
- handleChange();
145
- this?.scheduleRender && this.scheduleRender();
146
- },
147
- });
148
- };
149
- };
150
-
1
+ /**
2
+ * Supported property type constructors for reactive properties.
3
+ * These constructors are used to convert attribute values to the correct JavaScript type.
4
+ *
5
+ * @typedef {StringConstructor | NumberConstructor | BooleanConstructor | ObjectConstructor | ArrayConstructor} PropertyType
6
+ */
7
+ type PropertyType =
8
+ | StringConstructor
9
+ | NumberConstructor
10
+ | BooleanConstructor
11
+ | ObjectConstructor
12
+ | ArrayConstructor;
13
+
14
+ /**
15
+ * Legacy string-based property types (deprecated).
16
+ * Maintained for backward compatibility. Use PropertyType constructors instead.
17
+ *
18
+ * @typedef {('string' | 'number' | 'boolean' | 'object' | 'array')} oldPropertyType
19
+ * @deprecated Use PropertyType constructors (String, Number, Boolean, Object, Array) instead
20
+ */
21
+ type oldPropertyType = "string" | "number" | "boolean" | "object" | "array";
22
+
23
+ /**
24
+ * Configuration options for the @property decorator.
25
+ * Defines how a property should sync with HTML attributes and trigger updates.
26
+ *
27
+ * @interface PropertyOptions
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * @property({ type: String, reflect: true })
32
+ * name = 'default';
33
+ *
34
+ * @property({ type: Number, attribute: 'data-count' })
35
+ * count = 0;
36
+ *
37
+ * @property({ type: Boolean })
38
+ * active = false;
39
+ * ```
40
+ */
41
+ export interface PropertyOptions {
42
+ /**
43
+ * The type constructor for converting attribute values to property values.
44
+ * Use String, Number, Boolean, Object, or Array.
45
+ * @type {PropertyType | oldPropertyType}
46
+ */
47
+ type?: PropertyType | oldPropertyType;
48
+
49
+ /**
50
+ * The HTML attribute name to sync with, or false to disable attribute syncing.
51
+ * - `true` (default): Use kebab-case version of property name
52
+ * - `string`: Use specific attribute name
53
+ * - `false`: No attribute syncing
54
+ * @type {string | boolean}
55
+ */
56
+ attribute?: string | boolean;
57
+
58
+ /**
59
+ * Whether to reflect property changes back to the HTML attribute.
60
+ * When true, setting the property will update the attribute.
61
+ * @type {boolean}
62
+ * @default false
63
+ */
64
+ reflect?: boolean;
65
+
66
+ readonly?: boolean;
67
+
68
+ /**
69
+ * Custom converter for complex attribute/property transformations.
70
+ * @type {AttributeConverter<any>}
71
+ */
72
+ converter?: AttributeConverter<any>;
73
+ }
74
+
75
+ /**
76
+ * PropertyOptions extended with the property name.
77
+ * Used internally to track registered properties.
78
+ *
79
+ * @typedef {PropertyOptions & { name: string }} PropertyOptionsWithName
80
+ */
81
+ export type PropertyOptionsWithName = PropertyOptions & { name: string };
82
+
83
+ /**
84
+ * Track which elements are currently updating attributes from property setters.
85
+ * Prevents infinite loops when reflection is enabled (property → attribute → property).
86
+ *
87
+ * @type {WeakSet<HTMLElement>}
88
+ * @private
89
+ */
90
+ const settingAttributes = new WeakSet<HTMLElement>();
91
+
92
+ /**
93
+ * Queue of pending attribute reflections for elements not yet connected to the DOM.
94
+ * Attribute reflections are deferred until the element is connected.
95
+ *
96
+ * @type {WeakMap<HTMLElement, Map<string, {value: any, type: any, converter?: AttributeConverter<any>}>>}
97
+ * @private
98
+ */
99
+ const pendingAttributeReflections = new WeakMap<
100
+ HTMLElement,
101
+ Map<string, { value: any; type: any; converter?: AttributeConverter<any> }>
102
+ >();
103
+
104
+ /**
105
+ * Queue of pending update callbacks for elements not yet connected to the DOM.
106
+ * Updates are deferred until the element is connected to avoid calling lifecycle
107
+ * methods on unattached elements.
108
+ *
109
+ * @type {WeakMap<HTMLElement, Array<{propertyKey: string, oldValue: any, newValue: any}>>}
110
+ * @private
111
+ */
112
+ const pendingUpdates = new WeakMap<
113
+ HTMLElement,
114
+ Array<{ propertyKey: string; oldValue: any; newValue: any }>
115
+ >();
116
+
117
+ /**
118
+ * Checks if an element is currently updating its attributes from property setters.
119
+ * Used to prevent circular updates during property reflection.
120
+ *
121
+ * @param {HTMLElement} element - The element to check
122
+ * @returns {boolean} True if the element is currently setting attributes
123
+ *
124
+ * @example
125
+ * ```typescript
126
+ * if (!isSettingAttribute(this)) {
127
+ * // Safe to update property from attribute
128
+ * }
129
+ * ```
130
+ */
131
+ export function isSettingAttribute(element: HTMLElement): boolean {
132
+ return settingAttributes.has(element);
133
+ }
134
+
135
+ /**
136
+ * Process any pending update callbacks for an element.
137
+ * Should be called when the element connects to the DOM to trigger
138
+ * deferred lifecycle callbacks.
139
+ *
140
+ * @param {any} element - The element with pending updates
141
+ * @returns {void}
142
+ *
143
+ * @example
144
+ * ```typescript
145
+ * connectedCallback() {
146
+ * processPendingUpdates(this);
147
+ * }
148
+ * ```
149
+ */
150
+ export function processPendingUpdates(element: any): void {
151
+ // Process pending attribute reflections first
152
+ const pendingReflections = pendingAttributeReflections.get(element);
153
+ if (pendingReflections) {
154
+ settingAttributes.add(element);
155
+ try {
156
+ pendingReflections.forEach((reflection, attributeName) => {
157
+ updateAttribute(
158
+ element,
159
+ attributeName,
160
+ reflection.value,
161
+ reflection.type,
162
+ reflection.converter,
163
+ );
164
+ });
165
+ } finally {
166
+ settingAttributes.delete(element);
167
+ }
168
+ pendingAttributeReflections.delete(element);
169
+ }
170
+
171
+ // Process pending updated callbacks
172
+ const pending = pendingUpdates.get(element);
173
+ if (pending && element.updated) {
174
+ const pendingLength = pending.length;
175
+ let i = 0;
176
+ while (i < pendingLength) {
177
+ const update = pending[i];
178
+ element.updated(update.propertyKey, update.oldValue, update.newValue);
179
+ i = i + 1;
180
+ }
181
+ pendingUpdates.delete(element);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Interface for custom attribute conversion functions.
187
+ *
188
+ * @interface AttributeConverter
189
+ * @template T The type of the property value
190
+ * @description Provides custom conversion logic between HTML attribute strings
191
+ * and JavaScript property values for complex data types or custom formatting.
192
+ */
193
+ export interface AttributeConverter<T> {
194
+ /**
195
+ * Converts an HTML attribute string value to a JavaScript property value.
196
+ *
197
+ * @param {string | null} value - The attribute value to convert
198
+ * @returns {T} The converted JavaScript value
199
+ * @optional
200
+ *
201
+ * @example
202
+ * ```typescript
203
+ * fromAttribute: (value: string | null) => {
204
+ * return value ? JSON.parse(value) : null;
205
+ * }
206
+ * ```
207
+ */
208
+ fromAttribute?(value: string | null): T;
209
+
210
+ /**
211
+ * Converts a JavaScript property value to an HTML attribute string.
212
+ *
213
+ * @param {T} value - The JavaScript value to convert
214
+ * @returns {string} The converted attribute string
215
+ * @optional
216
+ *
217
+ * @example
218
+ * ```typescript
219
+ * toAttribute: (value: object) => {
220
+ * return JSON.stringify(value);
221
+ * }
222
+ * ```
223
+ */
224
+ toAttribute?(value: T): string;
225
+ }
226
+
227
+ /**
228
+ * Converts legacy string-based type to PropertyType constructor.
229
+ * Provides backward compatibility for old string type declarations.
230
+ *
231
+ * @param {PropertyType | oldPropertyType | undefined} type - The type to normalize
232
+ * @returns {PropertyType} The normalized PropertyType constructor (defaults to String)
233
+ * @private
234
+ *
235
+ * @example
236
+ * ```typescript
237
+ * normalizeType('string') // Returns String constructor
238
+ * normalizeType(Number) // Returns Number constructor
239
+ * ```
240
+ */
241
+ function normalizeType(
242
+ type: PropertyType | oldPropertyType | undefined,
243
+ ): PropertyType {
244
+ if (typeof type === "string") {
245
+ switch (type) {
246
+ case "string":
247
+ return String;
248
+ case "number":
249
+ return Number;
250
+ case "boolean":
251
+ return Boolean;
252
+ case "object":
253
+ return Object;
254
+ case "array":
255
+ return Array;
256
+ default:
257
+ return null;
258
+ }
259
+ }
260
+ return type;
261
+ }
262
+
263
+ /**
264
+ * Converts a property value to the specified type.
265
+ * Uses custom converter if provided, otherwise performs standard type conversion.
266
+ *
267
+ * @param {any} value - The value to convert
268
+ * @param {PropertyType} [type] - The target type constructor
269
+ * @param {AttributeConverter<any>} [converter] - Optional custom converter
270
+ * @returns {any} The converted value
271
+ * @private
272
+ *
273
+ * @example
274
+ * ```typescript
275
+ * convertPropertyValue('42', Number) // Returns 42
276
+ * convertPropertyValue('true', Boolean) // Returns true
277
+ * convertPropertyValue('{"a":1}', Object) // Returns {a: 1}
278
+ * ```
279
+ */
280
+ function convertPropertyValue(
281
+ value: any,
282
+ type?: PropertyType,
283
+ converter?: AttributeConverter<any>,
284
+ ): any {
285
+ // Use custom converter if provided
286
+ if (converter && converter.fromAttribute && typeof value === "string") {
287
+ return converter.fromAttribute(value);
288
+ }
289
+
290
+ // Preserve null/undefined
291
+ if (value === null || value === undefined) {
292
+ return value;
293
+ }
294
+
295
+ // Standard type conversion
296
+ switch (type) {
297
+ case String:
298
+ return String(value);
299
+ case Number:
300
+ return Number(value);
301
+ case Boolean:
302
+ return Boolean(value);
303
+ case Object:
304
+ return typeof value === "object" ? value : JSON.parse(String(value));
305
+ case Array:
306
+ return Array.isArray(value) ? value : JSON.parse(String(value));
307
+ default:
308
+ return value;
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Converts a camelCase string to kebab-case.
314
+ * Used to derive HTML attribute names from JavaScript property names.
315
+ *
316
+ * @param {string} str - The camelCase string to convert
317
+ * @returns {string} The kebab-case string
318
+ * @private
319
+ *
320
+ * @example
321
+ * ```typescript
322
+ * camelToKebabCase('myProperty') // Returns 'my-property'
323
+ * camelToKebabCase('isActive') // Returns 'is-active'
324
+ * ```
325
+ */
326
+ function camelToKebabCase(str: string): string {
327
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
328
+ }
329
+
330
+ /**
331
+ * Calculates the HTML attribute name based on property options.
332
+ *
333
+ * @param {string} propertyKey - The JavaScript property name
334
+ * @param {string | boolean | undefined} attribute - The attribute option from PropertyOptions
335
+ * @returns {string | undefined} The attribute name, or undefined if attribute syncing is disabled
336
+ * @private
337
+ *
338
+ * @example
339
+ * ```typescript
340
+ * getAttributeName('myProp', true) // Returns 'my-prop'
341
+ * getAttributeName('myProp', 'data-value') // Returns 'data-value'
342
+ * getAttributeName('myProp', false) // Returns undefined
343
+ * ```
344
+ */
345
+ function getAttributeName(
346
+ propertyKey: string,
347
+ attribute: string | boolean | undefined,
348
+ ): string | undefined {
349
+ if (attribute === false) return undefined;
350
+ if (attribute === true) return camelToKebabCase(propertyKey);
351
+ if (typeof attribute === "string") return attribute;
352
+ return undefined;
353
+ }
354
+
355
+ /**
356
+ * Updates the element's HTML attribute based on the property value.
357
+ * Handles special cases for Boolean, Object, and Array types.
358
+ *
359
+ * @param {HTMLElement} element - The element to update
360
+ * @param {string} attributeName - The attribute name to update
361
+ * @param {any} value - The property value to reflect
362
+ * @param {PropertyType} [type] - The property type constructor
363
+ * @param {AttributeConverter<any>} [converter] - Optional custom converter
364
+ * @returns {void}
365
+ * @private
366
+ *
367
+ * @example
368
+ * ```typescript
369
+ * updateAttribute(element, 'active', true, Boolean) // Sets empty attribute
370
+ * updateAttribute(element, 'count', 5, Number) // Sets attribute='5'
371
+ * ```
372
+ */
373
+ function updateAttribute(
374
+ element: HTMLElement,
375
+ attributeName: string,
376
+ value: any,
377
+ type?: PropertyType,
378
+ converter?: AttributeConverter<any>,
379
+ ): void {
380
+ if (type === Boolean) {
381
+ if (value) {
382
+ element.setAttribute(attributeName, "");
383
+ } else {
384
+ element.removeAttribute(attributeName);
385
+ }
386
+ } else if (type === Object || type === Array) {
387
+ if (converter && converter.toAttribute) {
388
+ element.setAttribute(attributeName, converter.toAttribute(value));
389
+ } else {
390
+ element.setAttribute(attributeName, JSON.stringify(value));
391
+ }
392
+ } else {
393
+ // Treat null/undefined as absence of attribute instead of the string "null"
394
+ if (value === null || value === undefined) {
395
+ element.removeAttribute(attributeName);
396
+ } else {
397
+ element.setAttribute(attributeName, String(value));
398
+ }
399
+ }
400
+ }
401
+
402
+ const alreadLogedNodeNames = new Set<string>();
403
+
404
+ function logConnectedCallback(nodeName: string) {
405
+ if (!alreadLogedNodeNames.has(nodeName)) {
406
+ alreadLogedNodeNames.add(nodeName);
407
+ if (typeof console !== "undefined" && console.info) {
408
+ console.info(
409
+ `[CustomElement] connectedCallback called for <${nodeName.toLocaleLowerCase()}>`,
410
+ );
411
+ }
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Class field decorator that creates a reactive property with automatic attribute syncing.
417
+ *
418
+ * Features:
419
+ * - Automatic type conversion between attributes and properties
420
+ * - Optional property → attribute reflection
421
+ * - Custom converters for complex types
422
+ * - Automatic observedAttributes registration
423
+ * - Deferred updates for disconnected elements
424
+ * - Lifecycle integration (updated, shouldUpdate, renderNow)
425
+ *
426
+ * @decorator
427
+ * @param {PropertyOptions} [options={}] - Configuration options for the property
428
+ * @returns {(target: any, propertyKey: string) => void} The decorator function
429
+ *
430
+ * @example
431
+ * Basic usage with different types:
432
+ * ```typescript
433
+ * class MyElement extends BaseComponent {
434
+ * @property({ type: String })
435
+ * name = 'default';
436
+ *
437
+ * @property({ type: Number })
438
+ * count = 0;
439
+ *
440
+ * @property({ type: Boolean })
441
+ * active = false;
442
+ * }
443
+ * ```
444
+ *
445
+ * @example
446
+ * With reflection (property changes update attribute):
447
+ * ```typescript
448
+ * class MyElement extends BaseComponent {
449
+ * @property({ type: String, reflect: true })
450
+ * status = 'pending';
451
+ *
452
+ * updateStatus() {
453
+ * this.status = 'complete'; // Also updates status="complete" attribute
454
+ * }
455
+ * }
456
+ * ```
457
+ *
458
+ * @example
459
+ * Custom attribute name and converter:
460
+ * ```typescript
461
+ * class MyElement extends BaseComponent {
462
+ * @property({
463
+ * type: Object,
464
+ * attribute: 'data-config',
465
+ * converter: {
466
+ * fromAttribute: (value) => value ? JSON.parse(value) : {},
467
+ * toAttribute: (value) => JSON.stringify(value)
468
+ * }
469
+ * })
470
+ * config = {};
471
+ * }
472
+ * ```
473
+ *
474
+ * @example
475
+ * Property without attribute syncing:
476
+ * ```typescript
477
+ * class MyElement extends BaseComponent {
478
+ * @property({ attribute: false })
479
+ * internalState = null; // No attribute created or observed
480
+ * }
481
+ * ```
482
+ */
483
+ export function property(
484
+ options: PropertyOptions = {},
485
+ ): (target: any, propertyKey: string) => void {
486
+ const {
487
+ type: rawType,
488
+ attribute = true,
489
+ reflect = false,
490
+ converter,
491
+ readonly = false,
492
+ } = options;
493
+
494
+ const type = normalizeType(rawType);
495
+
496
+ return (target: any, propertyKey: string): void => {
497
+ // Use WeakMap to store instance-specific values
498
+ const instanceMap = new WeakMap<any, any>();
499
+ const attributeName = getAttributeName(propertyKey, attribute);
500
+
501
+ // --- Patch lifecycle methods ONLY if defined in derived class ---
502
+ // Only patch once per class
503
+ if (!target.__p_elements_core_lifecycle_patch_applied) {
504
+ // Patch connectedCallback
505
+ if (
506
+ Object.prototype.hasOwnProperty.call(target, "connectedCallback") &&
507
+ typeof target.connectedCallback === "function"
508
+ ) {
509
+ const originalConnected = target.connectedCallback;
510
+ target.connectedCallback = function patchedConnectedCallback(
511
+ ...args: any[]
512
+ ) {
513
+ const PATCHED_FLAG = "__p_elements_core_pending_updates_called";
514
+ if (!this[PATCHED_FLAG]) {
515
+ let superCalled = false;
516
+ const origProcess = processPendingUpdates;
517
+ const self = this;
518
+ function wrappedProcessPendingUpdates(element: any) {
519
+ if (element === self) {
520
+ superCalled = true;
521
+ }
522
+ return origProcess(element);
523
+ }
524
+ (globalThis as any).__origProcessPendingUpdates =
525
+ processPendingUpdates;
526
+ (globalThis as any).processPendingUpdates =
527
+ wrappedProcessPendingUpdates;
528
+ try {
529
+ if (originalConnected) {
530
+ originalConnected.apply(this, args);
531
+ }
532
+ } finally {
533
+ (globalThis as any).processPendingUpdates = (
534
+ globalThis as any
535
+ ).__origProcessPendingUpdates;
536
+ delete (globalThis as any).__origProcessPendingUpdates;
537
+ }
538
+ if (!superCalled) {
539
+ let baseProto = Object.getPrototypeOf(target);
540
+ let baseConnected = baseProto && baseProto.connectedCallback;
541
+ if (typeof baseConnected === "function") {
542
+ baseConnected.apply(this, args);
543
+ if (typeof console !== "undefined" && console.info) {
544
+ // log this once per nodeName
545
+ logConnectedCallback(this.nodeName);
546
+ }
547
+ } else {
548
+ processPendingUpdates(this);
549
+ if (typeof console !== "undefined" && console.info) {
550
+ console.info(
551
+ `[p-elements-core] called processPendingUpdates automatically for <${this.nodeName}>`,
552
+ );
553
+ }
554
+ }
555
+ }
556
+ this[PATCHED_FLAG] = true;
557
+ } else if (originalConnected) {
558
+ originalConnected.apply(this, args);
559
+ }
560
+ };
561
+ }
562
+
563
+ // Patch disconnectedCallback
564
+ if (
565
+ Object.prototype.hasOwnProperty.call(target, "disconnectedCallback") &&
566
+ typeof target.disconnectedCallback === "function"
567
+ ) {
568
+ const originalDisconnected = target.disconnectedCallback;
569
+ target.disconnectedCallback = function patchedDisconnectedCallback(
570
+ ...args: any[]
571
+ ) {
572
+ const PATCHED_FLAG = "__p_elements_core_disconnected_called";
573
+ if (!this[PATCHED_FLAG]) {
574
+ let superCalled = false;
575
+ const baseProto = Object.getPrototypeOf(target);
576
+ const baseDisconnected =
577
+ baseProto && baseProto.disconnectedCallback;
578
+ if (originalDisconnected) {
579
+ originalDisconnected.apply(this, args);
580
+ superCalled = true;
581
+ }
582
+ if (!superCalled && typeof baseDisconnected === "function") {
583
+ baseDisconnected.apply(this, args);
584
+ }
585
+ this[PATCHED_FLAG] = true;
586
+ } else if (originalDisconnected) {
587
+ originalDisconnected.apply(this, args);
588
+ }
589
+ };
590
+ }
591
+
592
+ // Patch attributeChangedCallback
593
+ if (
594
+ Object.prototype.hasOwnProperty.call(
595
+ target,
596
+ "attributeChangedCallback",
597
+ ) &&
598
+ typeof target.attributeChangedCallback === "function"
599
+ ) {
600
+ const originalAttributeChanged = target.attributeChangedCallback;
601
+ target.attributeChangedCallback =
602
+ function patchedAttributeChangedCallback(...args: any[]) {
603
+ const PATCHED_FLAG = "__p_elements_core_attribute_changed_called";
604
+
605
+ if (!this[PATCHED_FLAG]) {
606
+ let superCalled = false;
607
+ const baseProto = Object.getPrototypeOf(target);
608
+ const baseAttributeChanged =
609
+ baseProto && baseProto.attributeChangedCallback;
610
+ if (originalAttributeChanged) {
611
+ originalAttributeChanged.apply(this, args);
612
+ superCalled = true;
613
+ }
614
+ if (!superCalled && typeof baseAttributeChanged === "function") {
615
+ baseAttributeChanged.apply(this, args);
616
+ }
617
+ this[PATCHED_FLAG] = true;
618
+ } else if (originalAttributeChanged) {
619
+ originalAttributeChanged.apply(this, args);
620
+ }
621
+ };
622
+ }
623
+
624
+ target.__p_elements_core_lifecycle_patch_applied = true;
625
+ }
626
+
627
+ Object.defineProperty(target, propertyKey, {
628
+ get(this: any) {
629
+ // On first access, check if an HTML attribute exists and use that instead
630
+ if (
631
+ !instanceMap.has(this) &&
632
+ attributeName &&
633
+ this.hasAttribute &&
634
+ this.hasAttribute(attributeName)
635
+ ) {
636
+ const attrValue = this.getAttribute(attributeName);
637
+ if (attrValue !== null) {
638
+ let convertedValue: any;
639
+ if (converter && converter.fromAttribute) {
640
+ convertedValue = converter.fromAttribute(attrValue);
641
+ } else if (type === Boolean) {
642
+ convertedValue = true;
643
+ } else if (type === Number) {
644
+ convertedValue = Number(attrValue);
645
+ } else {
646
+ convertedValue = attrValue;
647
+ }
648
+ instanceMap.set(this, convertedValue);
649
+
650
+ // Trigger update for initial HTML attribute
651
+ if (typeof this.renderNow === "function") {
652
+ this.renderNow();
653
+ }
654
+
655
+ return convertedValue;
656
+ }
657
+ }
658
+
659
+ // Return the value if it exists, otherwise return default for type
660
+ if (instanceMap.has(this)) {
661
+ return instanceMap.get(this);
662
+ }
663
+ // Return type-appropriate default for undefined values
664
+ if (type === Boolean) {
665
+ return false;
666
+ }
667
+ return undefined;
668
+ },
669
+ set(this: any, value: any) {
670
+ // If readonly is true and value already exists, prevent setting
671
+ if (readonly && instanceMap.has(this)) {
672
+ return;
673
+ }
674
+
675
+ const oldValue = instanceMap.get(this);
676
+ let convertedValue: any;
677
+ let isRenderOnSet = false;
678
+
679
+ if ((type === null || type === undefined) && value !== oldValue) {
680
+ isRenderOnSet = true;
681
+ }
682
+
683
+ // On first set, check if an HTML attribute exists and use that instead of the default
684
+ if (
685
+ !instanceMap.has(this) &&
686
+ attributeName &&
687
+ this.hasAttribute &&
688
+ this.hasAttribute(attributeName)
689
+ ) {
690
+ const attrValue = this.getAttribute(attributeName);
691
+ if (attrValue !== null) {
692
+ if (converter && converter.fromAttribute) {
693
+ convertedValue = converter.fromAttribute(attrValue);
694
+ } else if (type === Boolean) {
695
+ convertedValue = true;
696
+ } else if (type === Number) {
697
+ convertedValue = Number(attrValue);
698
+ } else {
699
+ convertedValue = attrValue;
700
+ }
701
+ instanceMap.set(this, convertedValue);
702
+
703
+ // Trigger update for initial HTML attribute only if connected
704
+ if (this.isConnected) {
705
+ if (typeof this.renderNow === "function") {
706
+ this.renderNow();
707
+ }
708
+ // Call updated callback
709
+ if (this.updated) {
710
+ this.updated(propertyKey, oldValue, convertedValue);
711
+ }
712
+ } else {
713
+ // Queue for later when element connects
714
+ if (this.updated) {
715
+ if (!pendingUpdates.has(this)) {
716
+ pendingUpdates.set(this, []);
717
+ }
718
+ pendingUpdates
719
+ .get(this)!
720
+ .push({ propertyKey, oldValue, newValue: convertedValue });
721
+ }
722
+ }
723
+
724
+ return;
725
+ }
726
+ }
727
+ if (isRenderOnSet) {
728
+ convertedValue = value;
729
+ } else {
730
+ convertedValue = convertPropertyValue(value, type, converter);
731
+ }
732
+
733
+ if (oldValue === convertedValue) {
734
+ return;
735
+ }
736
+
737
+ const shouldUpdate = this.shouldUpdate
738
+ ? this.shouldUpdate(propertyKey, oldValue, convertedValue)
739
+ : true;
740
+
741
+ instanceMap.set(this, shouldUpdate ? convertedValue : oldValue);
742
+
743
+ // Update attribute if reflect is enabled
744
+ if (reflect && attributeName !== undefined) {
745
+ if (this.isConnected) {
746
+ // Element is connected, update attribute immediately
747
+ settingAttributes.add(this);
748
+ try {
749
+ updateAttribute(
750
+ this,
751
+ attributeName,
752
+ shouldUpdate ? convertedValue : oldValue,
753
+ type,
754
+ converter,
755
+ );
756
+ } finally {
757
+ settingAttributes.delete(this);
758
+ }
759
+ } else {
760
+ // Element not connected yet, queue for later
761
+ if (!pendingAttributeReflections.has(this)) {
762
+ pendingAttributeReflections.set(this, new Map());
763
+ }
764
+ pendingAttributeReflections.get(this)!.set(attributeName, {
765
+ value: shouldUpdate ? convertedValue : oldValue,
766
+ type,
767
+ converter,
768
+ });
769
+ }
770
+ }
771
+
772
+ // Trigger update only if connected
773
+ if (this.isConnected) {
774
+ if (typeof this.renderNow === "function") {
775
+ this.renderNow();
776
+ }
777
+ // Call updated callback
778
+ if (this.updated) {
779
+ this.updated(propertyKey, oldValue, convertedValue);
780
+ }
781
+ } else {
782
+ // Queue updated callback for later when element connects
783
+ if (this.updated) {
784
+ if (!pendingUpdates.has(this)) {
785
+ pendingUpdates.set(this, []);
786
+ }
787
+ pendingUpdates
788
+ .get(this)!
789
+ .push({ propertyKey, oldValue, newValue: convertedValue });
790
+ }
791
+ }
792
+
793
+ // Call renderNow if @RenderOnSet is used and value changed
794
+ if (isRenderOnSet && typeof this.renderNow === "function") {
795
+ this.renderNow();
796
+ }
797
+ },
798
+ configurable: true,
799
+ });
800
+
801
+ // Register property info for attribute change detection
802
+ if (!target.constructor._propertyInfo) {
803
+ target.constructor._propertyInfo = new Map<string, PropertyOptions>();
804
+ }
805
+ target.constructor._propertyInfo.set(propertyKey, {
806
+ ...options,
807
+ name: propertyKey,
808
+ attribute: attributeName,
809
+ type,
810
+ });
811
+
812
+ // Register observed attributes
813
+ if (attributeName) {
814
+ if (!target.constructor.observedAttributes) {
815
+ target.constructor.observedAttributes = [];
816
+ }
817
+ if (!target.constructor.observedAttributes.includes(attributeName)) {
818
+ target.constructor.observedAttributes.push(attributeName);
819
+ }
820
+ }
821
+ };
822
+ }