x4js 2.0.34 → 2.1.0-manual

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 (134) hide show
  1. package/README.md +21 -21
  2. package/package.json +39 -26
  3. package/src/components/base.scss +25 -89
  4. package/src/components/boxes/boxes.module.scss +54 -54
  5. package/src/components/boxes/boxes.ts +513 -513
  6. package/src/components/breadcrumb/breadcrumb.scss +56 -56
  7. package/src/components/breadcrumb/breadcrumb.ts +93 -93
  8. package/src/components/btngroup/btngroup.module.scss +40 -40
  9. package/src/components/btngroup/btngroup.ts +152 -152
  10. package/src/components/button/button.module.scss +172 -172
  11. package/src/components/button/button.ts +232 -232
  12. package/src/components/calendar/calendar.module.scss +162 -162
  13. package/src/components/calendar/calendar.ts +326 -326
  14. package/src/components/canvas/canvas.module.scss +24 -24
  15. package/src/components/canvas/canvas.ts +195 -195
  16. package/src/components/canvas/canvas_ex.ts +275 -275
  17. package/src/components/checkbox/check.svg +3 -3
  18. package/src/components/checkbox/checkbox.module.scss +141 -141
  19. package/src/components/checkbox/checkbox.ts +139 -139
  20. package/src/components/colorinput/colorinput.module.scss +64 -64
  21. package/src/components/colorinput/colorinput.ts +90 -90
  22. package/src/components/colorpicker/colorpicker.module.scss +132 -132
  23. package/src/components/colorpicker/colorpicker.ts +481 -481
  24. package/src/components/combobox/combobox.module.scss +145 -145
  25. package/src/components/combobox/combobox.ts +282 -282
  26. package/src/components/combobox/updown.svg +3 -3
  27. package/src/components/components.ts +45 -44
  28. package/src/components/dialog/dialog.module.scss +103 -105
  29. package/src/components/dialog/dialog.ts +233 -233
  30. package/src/components/filedrop/filedrop.module.scss +69 -69
  31. package/src/components/filedrop/filedrop.ts +130 -130
  32. package/src/components/form/form.module.scss +38 -38
  33. package/src/components/form/form.ts +172 -172
  34. package/src/components/gridview/gridview.module.scss +323 -337
  35. package/src/components/gridview/gridview.ts +1276 -1315
  36. package/src/components/header/header.module.scss +40 -40
  37. package/src/components/header/header.ts +141 -141
  38. package/src/components/icon/icon.module.scss +32 -32
  39. package/src/components/icon/icon.ts +165 -165
  40. package/src/components/image/image.module.scss +27 -27
  41. package/src/components/image/image.ts +168 -168
  42. package/src/components/input/input.module.scss +74 -74
  43. package/src/components/input/input.ts +537 -537
  44. package/src/components/keyboard/keyboard.module.scss +136 -136
  45. package/src/components/keyboard/keyboard.ts +549 -549
  46. package/src/components/label/label.module.scss +90 -91
  47. package/src/components/label/label.ts +101 -101
  48. package/src/components/link/link.module.scss +44 -44
  49. package/src/components/link/link.ts +87 -87
  50. package/src/components/listbox/listbox.module.scss +179 -179
  51. package/src/components/listbox/listbox.ts +596 -596
  52. package/src/components/menu/menu.module.scss +128 -128
  53. package/src/components/menu/menu.ts +174 -174
  54. package/src/components/messages/messages.module.scss +92 -146
  55. package/src/components/messages/messages.ts +237 -303
  56. package/src/components/normalize.scss +391 -391
  57. package/src/components/notification/notification.module.scss +83 -83
  58. package/src/components/notification/notification.ts +107 -107
  59. package/src/components/panel/panel.module.scss +66 -71
  60. package/src/components/panel/panel.ts +57 -57
  61. package/src/components/popup/popup.module.scss +51 -51
  62. package/src/components/popup/popup.ts +457 -457
  63. package/src/components/progress/progress.module.scss +56 -56
  64. package/src/components/progress/progress.ts +43 -43
  65. package/src/components/propgrid/progrid.module.scss +111 -111
  66. package/src/components/propgrid/propgrid.ts +300 -300
  67. package/src/components/propgrid/updown.svg +3 -3
  68. package/src/components/radio/radio.module.scss +163 -163
  69. package/src/components/radio/radio.svg +3 -3
  70. package/src/components/radio/radio.ts +141 -141
  71. package/src/components/rating/rating.module.scss +22 -22
  72. package/src/components/rating/rating.ts +131 -131
  73. package/src/components/select/select.module.scss +8 -8
  74. package/src/components/select/select.ts +134 -134
  75. package/src/components/shared.scss +141 -71
  76. package/src/components/sizers/sizer.module.scss +90 -107
  77. package/src/components/sizers/sizer.ts +131 -134
  78. package/src/components/slider/slider.module.scss +117 -117
  79. package/src/components/slider/slider.ts +197 -197
  80. package/src/components/spreadsheet/spreadsheet.module.scss +307 -307
  81. package/src/components/spreadsheet/spreadsheet.ts +1223 -1223
  82. package/src/components/switch/switch.module.scss +126 -126
  83. package/src/components/switch/switch.ts +61 -61
  84. package/src/components/tabs/tabs.module.scss +46 -67
  85. package/src/components/tabs/tabs.ts +229 -234
  86. package/src/components/textarea/textarea.module.scss +63 -63
  87. package/src/components/textarea/textarea.ts +131 -131
  88. package/src/components/textedit/textedit.module.scss +115 -115
  89. package/src/components/textedit/textedit.ts +122 -122
  90. package/src/components/themes.scss +90 -90
  91. package/src/components/tickline/tickline.module.scss +25 -25
  92. package/src/components/tickline/tickline.ts +81 -81
  93. package/src/components/tooltips/tooltips.scss +71 -71
  94. package/src/components/tooltips/tooltips.ts +120 -120
  95. package/src/components/treeview/treeview.module.scss +192 -192
  96. package/src/components/treeview/treeview.ts +484 -484
  97. package/src/components/viewport/viewport.module.scss +31 -31
  98. package/src/components/viewport/viewport.ts +41 -41
  99. package/src/core/component.ts +1299 -1299
  100. package/src/core/core_application.ts +361 -361
  101. package/src/core/core_colors.ts +512 -512
  102. package/src/core/core_data.ts +1297 -1297
  103. package/src/core/core_dom.ts +481 -481
  104. package/src/core/core_dragdrop.ts +225 -225
  105. package/src/core/core_element.ts +221 -221
  106. package/src/core/core_events.ts +214 -214
  107. package/src/core/core_i18n.ts +395 -395
  108. package/src/core/core_pdf.ts +454 -454
  109. package/src/core/core_react.ts +78 -78
  110. package/src/core/core_router.ts +296 -296
  111. package/src/core/core_state.ts +62 -62
  112. package/src/core/core_styles.ts +213 -213
  113. package/src/core/core_svg.ts +1042 -1042
  114. package/src/core/core_tools.ts +996 -996
  115. package/src/types/scss.d.ts +4 -4
  116. package/src/types/x4react.d.ts +8 -8
  117. package/src/x4.scss +19 -19
  118. package/src/x4.ts +36 -36
  119. package/src/x4tsx.d.ts +26 -26
  120. package/.vscode/launch.json +0 -14
  121. package/.vscode/settings.json +0 -2
  122. package/demo/assets/house-light.svg +0 -1
  123. package/demo/assets/radio.svg +0 -4
  124. package/demo/index.html +0 -12
  125. package/demo/main.scss +0 -23
  126. package/demo/main.ts +0 -324
  127. package/demo/package.json +0 -26
  128. package/demo/scss.d.ts +0 -4
  129. package/demo/svg.d.ts +0 -1
  130. package/demo/tsconfig.json +0 -14
  131. package/src/components/gridview/folder-open.svg +0 -1
  132. package/src/components/messages/spinner.svg +0 -1
  133. package/src/x4.d.ts +0 -10
  134. package/tsconfig.json +0 -11
@@ -1,1299 +1,1299 @@
1
- /**
2
- * ___ ___ __
3
- * \ \/ / / _
4
- * \ / /_| |_
5
- * / \____ _|
6
- * /__/\__\ |_|
7
- *
8
- * @file component.ts
9
- * @author Etienne Cochard
10
- *
11
- * @copyright (c) 2024 R-libre ingenierie
12
- *
13
- * Use of this source code is governed by an MIT-style license
14
- * that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
15
- **/
16
-
17
- import { isArray, UnsafeHtml, isNumber, Rect, Constructor, class_ns, x4_class_ns_sym, IRect } from './core_tools.ts';
18
- import { CoreElement } from './core_element.ts';
19
- import { ariaValues, unitless } from './core_styles.ts';
20
- import { CoreEvent, EventMap } from './core_events.ts';
21
- import { addEvent, DOMEventHandler, GlobalDOMEvents } from './core_dom.ts';
22
- import { Application, EvMessage } from './core_application.ts';
23
-
24
- interface RefType<T extends Component> {
25
- dom: T;
26
- }
27
-
28
- type ComponentAttributes = Record<string,string|number|boolean>;
29
- type CreateComponentCallBack = ( attrs: Record<string,string> ) => ComponentContent;
30
-
31
- const FRAGMENT = Symbol( "fragment" );
32
- const COMPONENT = Symbol( "component" );
33
-
34
- const RE_NUMBER = /^-?\d+(\.\d*)?$/;
35
-
36
- /**
37
- * you can change css classname prefix by adding
38
- *
39
- * ```
40
- * static "$cls-ns" = "<your prefix>";
41
- * ```
42
- *
43
- * to your class to avoid autogenerated css class names conflicts
44
- */
45
-
46
- function genClassNames( x: any ): string[] {
47
-
48
- const classes = [];
49
- let self = Object.getPrototypeOf(x);
50
-
51
- if( self.constructor==Component ) {
52
- return ["x4-comp"];
53
- }
54
-
55
- while (self && self.constructor !== Component ) {
56
- const clsname:string = self.constructor.name;
57
- const clsns: string = Object.prototype.hasOwnProperty.call(self.constructor,x4_class_ns_sym) ? self.constructor[x4_class_ns_sym] : "";
58
- classes.push( clsns+clsname.toLowerCase() );
59
- self = Object.getPrototypeOf(self);
60
- }
61
-
62
- return classes;
63
- }
64
-
65
- /**
66
- *
67
- */
68
-
69
- export type ComponentContent = Component | Component[] | string | string[] | UnsafeHtml| UnsafeHtml[] | number | boolean;
70
-
71
- let gen_id = 1000;
72
-
73
- export const makeUniqueComponentId = ( ) => {
74
- return `x4-${gen_id++}`;
75
- }
76
-
77
- /**
78
- * Base properties for all components.
79
- */
80
-
81
- export interface ComponentProps {
82
- /** HTML tag name (default: `"div"`). */
83
- tag?: string;
84
- /** Namespace for SVG/MathML elements. */
85
- ns?: string;
86
- /** Inline CSS styles. */
87
- style?: Partial<CSSStyleDeclaration>;
88
- /** HTML attributes. */
89
- attrs?: Record<string,string|number|boolean>;
90
- /** Child content (components, strings, or HTML). */
91
- content?: ComponentContent;
92
- /** DOM event listeners. */
93
- dom_events?: GlobalDOMEvents;
94
- /** Additional CSS classes. */
95
- cls?: string;
96
- /** Element ID. */
97
- id?: string;
98
- /** Reference to the component instance. */
99
- ref?: RefType<any>;
100
-
101
- // shortcuts
102
- /** Width (px or string like `"50%"` or `"3em"`). */
103
- width?: string | number;
104
- /** Height (px or string like `"50%"`). */
105
- height?: string | number;
106
- /** Component is initialy disabled. */
107
- disabled?: boolean,
108
- /** Component is initialy hidden. */
109
- hidden?: boolean,
110
- /** Enables flex layout (boolean) or sets flex-grow (number). */
111
- flex?: boolean | number;
112
- /** Tooltip text. */
113
- tooltip?: string;
114
- /** Existing DOM element to wrap. */
115
- existingDOM?: HTMLElement;
116
-
117
- // index signature
118
- // to avoid errors: Type 'X' has no properties in common with type 'Y'
119
- // because all memebers here are optional.
120
- // this allow TS to recongnize derived props as ComponentProps
121
- //[key: string]: any;
122
- }
123
-
124
-
125
- /**
126
- *
127
- */
128
-
129
- export interface ComponentEvent extends CoreEvent {
130
- }
131
-
132
- /**
133
- *
134
- */
135
-
136
- export interface ComponentEvents extends EventMap {
137
- }
138
-
139
- /**
140
- * Base component class with DOM integration and event handling.
141
- * Auto-generates CSS class: `x4comp` + derived class names (e.g., `x4button`).
142
- *
143
- * @example
144
- * ```ts
145
- * // Basic div
146
- * const div = new Component({ tag: "div", content: "Hello" });
147
- *
148
- * // Custom element
149
- * class MyComponent extends Component {}
150
- * const inst = new MyComponent({ cls: "my-class" });
151
- * ```
152
- */
153
-
154
- @class_ns( "x4" )
155
- export class Component<P extends ComponentProps = ComponentProps, E extends ComponentEvents = ComponentEvents>
156
- extends CoreElement<E> {
157
-
158
- /** The underlying DOM element of the component. */
159
- readonly dom: Element;
160
-
161
- /** The properties passed to the component's constructor. */
162
- readonly props: P;
163
-
164
- protected readonly clsprefix: string; // internal class name prefix (x4 internal)
165
-
166
- #store: Map<string|symbol,any>;
167
-
168
-
169
- constructor( props: P ) {
170
- super( );
171
-
172
- this.props = props; // copy ?
173
-
174
- if( props.existingDOM ) {
175
- this.dom = props.existingDOM;
176
- }
177
- else {
178
- if( props.ns ) {
179
- this.dom = document.createElementNS( props.ns, props.tag ?? "div" );
180
- }
181
- else {
182
- this.dom = document.createElement( props.tag ?? "div" );
183
- }
184
-
185
- if (props.attrs) {
186
- this.setAttributes( props.attrs );
187
- }
188
-
189
- if( props.cls ) {
190
- this.addClass( props.cls );
191
- }
192
-
193
- if( props.hidden ) {
194
- this.show( false );
195
- }
196
-
197
- if( props.flex===true ) {
198
- this.addClass( "x4flex" );
199
- }
200
- else if( props.flex!==undefined ) {
201
- this.setStyle( {
202
- "flexGrow": props.flex+""
203
- });
204
- }
205
-
206
- if( props.id!==undefined ) {
207
- this.setAttribute( "id", props.id );
208
- }
209
-
210
- // small shortcut
211
- if( props.width!==undefined ) {
212
- this.setStyleValue( "width", props.width );
213
- }
214
-
215
- if( props.height!==undefined ) {
216
- this.setStyleValue( "height", props.height );
217
- }
218
-
219
- if( props.tooltip ) {
220
- this.setAttribute( "tooltip", props.tooltip );
221
- }
222
-
223
- if( props.style ) {
224
- this.setStyle( props.style );
225
- }
226
-
227
- if( props.content ) {
228
- this.setContent( props.content );
229
- }
230
-
231
- if( props.dom_events ) {
232
- this.setDOMEvents( props.dom_events );
233
- }
234
-
235
- const classes = genClassNames( this );
236
- this.dom.classList.add( ...classes );
237
-
238
- // need to have children for next statements
239
- // and children way be created in caller
240
- if( props.disabled ) {
241
- this.addDOMEvent( "created", ( ) => {
242
- this.enable( false );
243
- } );
244
- }
245
- }
246
-
247
- (this.dom as any)[COMPONENT] = this;
248
- }
249
-
250
- /**
251
- * Attaches a listener for global messages dispatched by the application.
252
- * The listener is automatically removed when the component's DOM element is removed.
253
- * @param cb - The callback function to execute when a global message is received.
254
- */
255
-
256
- onGlobalEvent( cb: ( ev: EvMessage ) => void ) {
257
-
258
- const off = Application.instance().on( "global", ev => {
259
- cb( ev );
260
- })
261
-
262
- this.addDOMEvent( "removed", ( ) => off.off() );
263
- }
264
-
265
-
266
- // :: CLASSES ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
267
-
268
- /**
269
- * Checks if the component's DOM element has a specific CSS class.
270
- * @param cls - The CSS class name to check.
271
- * @returns `true` if the class is present, `false` otherwise.
272
- */
273
-
274
- hasClass( cls: string ) {
275
- return this.dom.classList.contains( cls );
276
- }
277
-
278
- /**
279
- * Adds one or more CSS classes to the component's DOM element.
280
- * Multiple classes can be provided as a space-separated string.
281
- * @param cls - The CSS class(es) to add.
282
- */
283
-
284
- addClass( cls: string ) {
285
- if( !cls ) return;
286
-
287
- cls = cls.trim( );
288
-
289
- if( cls.includes(' ') ) {
290
- const ccs = cls.split( " " );
291
- this.dom.classList.add( ...ccs.filter(x=>x) );
292
- }
293
- else {
294
- this.dom.classList.add(cls);
295
- }
296
- }
297
-
298
- /**
299
- * Removes one or more CSS classes from the component's DOM element.
300
- * If `*` is passed as the class name, all classes will be removed.
301
- * Multiple classes can be provided as a space-separated string.
302
- * @param cls - The CSS class(es) to remove, or `*` to clear all.
303
- */
304
-
305
- removeClass( cls: string ) {
306
- if( !cls ) return;
307
-
308
- if( cls=='*' ) {
309
- this.dom.classList.value = "";
310
- return;
311
- }
312
-
313
- if( cls.indexOf(' ')>=0 ) {
314
- const ccs = cls.split( " " );
315
- this.dom.classList.remove(...ccs);
316
- }
317
- else {
318
- this.dom.classList.remove(cls);
319
- }
320
- }
321
-
322
- /**
323
- * Removes all CSS classes from the component's DOM element that match a given regular expression.
324
- * @param re - The regular expression to match against class names.
325
- */
326
-
327
- removeClassEx( re: RegExp ) {
328
- const all = Array.from( this.dom.classList );
329
- all.forEach( x => {
330
- if( x.match(re) ) {
331
- this.dom.classList.remove( x );
332
- }
333
- });
334
- }
335
-
336
- /**
337
- * Toggles the presence of one or more CSS classes on the component's DOM element.
338
- * If a class is present, it's removed; otherwise, it's added.
339
- * Multiple classes can be provided as a space-separated string.
340
- * @param cls - The CSS class(es) to toggle.
341
- */
342
-
343
- toggleClass( cls: string ) {
344
- if( !cls ) return;
345
-
346
- const toggle = ( x: string ) => {
347
- this.dom.classList.toggle(x);
348
- }
349
-
350
- if( cls.indexOf(' ')>=0 ) {
351
- const ccs = cls.split( " " );
352
- ccs.forEach( toggle );
353
- }
354
- else {
355
- toggle( cls );
356
- }
357
- }
358
-
359
- /**
360
- * Sets or removes a CSS class based on a boolean condition.
361
- * @param cls - The CSS class to manage.
362
- * @param set - If `true`, the class is added; if `false`, it's removed. Defaults to `true`.
363
- * @returns The component instance for chaining.
364
- */
365
-
366
- setClass( cls: string, set: boolean = true ) : this {
367
- if( set ) this.addClass(cls);
368
- else this.removeClass( cls );
369
- return this;
370
- }
371
-
372
- // :: ATTRIBUTES ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
373
-
374
- /**
375
- * Sets multiple HTML attributes on the component's DOM element.
376
- * @param attrs - An object where keys are attribute names and values are their corresponding values.
377
- * @returns The component instance for chaining.
378
- */
379
-
380
- setAttributes( attrs: ComponentAttributes ): this {
381
- for( const name in attrs ) {
382
- this.setAttribute( name, attrs[name] );
383
- }
384
- return this;
385
- }
386
-
387
- /**
388
- * Sets a single HTML attribute on the component's DOM element.
389
- * If `value` is `null`, `undefined`, or `false`, the attribute will be removed.
390
- * @param name - The name of the attribute.
391
- * @param value - The value of the attribute.
392
- */
393
-
394
- setAttribute( name: string, value: string | number | boolean ) {
395
- if( value===null || value===undefined || value===false ) {
396
- this.dom.removeAttribute( name );
397
- }
398
- else {
399
- this.dom.setAttribute( name, ""+value );
400
- }
401
- }
402
-
403
- /**
404
- * Retrieves the value of an HTML attribute from the component's DOM element.
405
- * @param name - The name of the attribute.
406
- * @returns The string value of the attribute, or `null` if not present.
407
- */
408
-
409
- getAttribute( name: string ): string {
410
- return this.dom.getAttribute( name );
411
- }
412
-
413
- /**
414
- * Retrieves the value of a `data-*` attribute from the component's DOM element.
415
- * @param name - The suffix of the `data-` attribute (e.g., for `data-foo`, use `"foo"`).
416
- * @returns The string value of the `data-*` attribute, or `null` if not present.
417
- *
418
- * @cf setIntData/getIntData (number)
419
- * @cf setInternalData/getInternalData (typed data)
420
- */
421
-
422
- getData( name: string ) : string {
423
- return this.getAttribute( "data-"+name );
424
- }
425
-
426
- /**
427
- * Retrieves the integer value of a `data-*` attribute from the component's DOM element.
428
- * Returns `undefined` if the attribute is not present or cannot be parsed as a number.
429
- * @param name - The suffix of the `data-` attribute.
430
- * @returns The integer value of the `data-*` attribute, or `undefined`.
431
- */
432
-
433
- getIntData( name: string ) : number {
434
- const v = parseInt( this.getAttribute( "data-"+name ) );
435
- if( Number.isFinite(v) ) {
436
- return v;
437
- }
438
-
439
- return undefined;
440
- }
441
-
442
- /**
443
- * Sets the value of a `data-*` attribute on the component's DOM element.
444
- * @param name - The suffix of the `data-` attribute.
445
- * @param value - The string value to set.
446
- */
447
-
448
- setData( name: string, value: string ) {
449
- return this.setAttribute( "data-"+name, value );
450
- }
451
-
452
- /**
453
- * idem as setData but onot on dom, you can store anything
454
- */
455
-
456
- setInternalData<T>( name: string|symbol, value: T ): this {
457
- if( !this.#store ) {
458
- this.#store = new Map( );
459
- }
460
-
461
- this.#store.set( name, value );
462
- return this;
463
- }
464
-
465
- getInternalData<T = any>( name: string|symbol ): T {
466
- return this.#store?.get(name) as T;
467
- }
468
-
469
-
470
- // :: DOM EVENTS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
471
-
472
- /**
473
- * Adds a DOM event listener to the component's DOM element.
474
- * @param name - The name of the DOM event (e.g., `'click'`, `'mouseover'`).
475
- * @param listener - The event handler function.
476
- * @param prepend - If `true`, the listener is added to the beginning of the event listener list. Defaults to `false`.
477
- */
478
-
479
- addDOMEvent<K extends keyof GlobalDOMEvents>( name: K, listener: GlobalDOMEvents[K], prepend = false ) {
480
- addEvent( this.dom, name, listener as DOMEventHandler, prepend );
481
- }
482
-
483
- /**
484
- * Sets multiple DOM event listeners on the component's DOM element.
485
- * @param events - An object where keys are event names and values are their corresponding handler functions.
486
- */
487
-
488
- setDOMEvents( events: GlobalDOMEvents ) {
489
- for( const name in events ) {
490
- this.addDOMEvent( name as any, (events as any)[name] );
491
- }
492
- }
493
-
494
- // :: HILEVEL EVENTS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
495
-
496
- /**
497
- * tool to move named events to internal event map
498
- * @internal
499
- */
500
-
501
- protected mapPropEvents<N extends keyof E>(props: P, ...elements: N[] ) {
502
- const p = props as any;
503
- elements.forEach( n => {
504
- if (Object.prototype.hasOwnProperty.call(p,n) && p[n]) {
505
- this.on( n, p[n] );
506
- }
507
- });
508
- }
509
-
510
- // :: CONTENT ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
511
-
512
- /**
513
- * Removes all child nodes from the component's DOM element.
514
- */
515
-
516
- clearContent( ) {
517
- const d = this.dom;
518
- while( d.firstChild ) {
519
- d.removeChild( d.firstChild );
520
- }
521
- }
522
-
523
- /**
524
- * Replaces the entire content of the component's DOM element with new content.
525
- * Any existing content will be cleared before the new content is added.
526
- * @param content - The new content to set. Can be a single item or an array of items.
527
- */
528
-
529
- setContent( content: ComponentContent ) {
530
- this.clearContent( );
531
- this.appendContent( content );
532
- }
533
-
534
- /**
535
- * Appends content to the end of the component's DOM element.
536
- * Content can be a single Component, an array of Components, a string, an array of strings,
537
- * raw HTML, an array of raw HTML, a number, or a boolean.
538
- * @param content - The content to append.
539
- */
540
-
541
- appendContent( content: ComponentContent ) {
542
- const set = ( d: any, c: Component | string | UnsafeHtml | number | boolean ) => {
543
-
544
- if (c instanceof Component ) {
545
- d.appendChild( c.dom );
546
- }
547
- else if( c instanceof UnsafeHtml) {
548
- d.insertAdjacentHTML( 'beforeend' , c.toString() );
549
- }
550
- else if (typeof c === "string" || typeof c === "number") {
551
- const tnode = document.createTextNode(c.toString());
552
- d.appendChild( tnode );
553
- }
554
- else if( c ) {
555
- console.warn("Unknown type to append: ", c);
556
- }
557
- }
558
-
559
- if( !isArray(content) ) {
560
- set( this.dom, content );
561
- }
562
- else if( content.length<=8 ) {
563
- for( const c of content ) {
564
- set( this.dom, c );
565
- }
566
- }
567
- else {
568
- const fragment = document.createDocumentFragment( ) as any;
569
-
570
- // polyfill
571
- fragment.insertAdjacentHTML = ( position: string, html: string ) => {
572
- const temp = document.createElement('div');
573
- temp.innerHTML = html;
574
- const nodes = Array.from(temp.childNodes);
575
- fragment.append( ...nodes );
576
- }
577
-
578
- for (const child of content ) {
579
- set( fragment, child );
580
- }
581
-
582
- this.dom.appendChild( fragment );
583
- }
584
- }
585
-
586
- /**
587
- * Prepends content to the beginning of the component's DOM element.
588
- * Content can be a single Component, an array of Components, a string, an array of strings,
589
- * raw HTML, an array of raw HTML, a number, or a boolean.
590
- * @param content - The content to prepend.
591
- */
592
-
593
- prependContent( content: ComponentContent ) {
594
- const d = this.dom;
595
- const set = ( c: Component | string | UnsafeHtml | number | boolean ) => {
596
- if (c instanceof Component ) {
597
- d.insertAdjacentElement( 'afterbegin', c.dom );
598
- }
599
- else if( c instanceof UnsafeHtml) {
600
- d.insertAdjacentHTML( 'afterbegin', c.toString() );
601
- }
602
- else if (typeof c === "string" || typeof c === "number") {
603
- d.insertAdjacentText( 'afterbegin', c.toString() );
604
- }
605
- else {
606
- console.warn("Unknown type to append: ", c);
607
- }
608
- }
609
-
610
- if( !isArray(content) ) {
611
- set( content );
612
- }
613
- else {
614
- const fragment = document.createDocumentFragment( );
615
- for (const child of content ) {
616
- set( child );
617
- }
618
-
619
- d.insertBefore( d.firstChild, fragment );
620
- }
621
- }
622
-
623
- /**
624
- * Removes a specific child component from this component's DOM element.
625
- * @param child - The child component instance to remove.
626
- * @cf clearContent
627
- */
628
-
629
- removeChild( child: Component ) {
630
- this.dom.removeChild( child.dom );
631
- }
632
-
633
-
634
- /**
635
- * Queries all descendant DOM elements matching a CSS selector and wraps them as Component instances.
636
- * @param selector - The CSS selector string.
637
- * @returns An array of Component instances.
638
- */
639
-
640
- queryAll( selector: string ): Component[] {
641
- const all = this.dom.querySelectorAll( selector );
642
- const rc = new Array( all.length );
643
- all.forEach( (x,i) => rc[i]=wrapDOM(x as HTMLElement) );
644
- return rc;
645
- }
646
-
647
- /**
648
- * Queries the first descendant DOM element matching a CSS selector and wraps it as a Component instance.
649
- * @param selector - The CSS selector string.
650
- * @returns The first matching Component instance, or `null` if no match is found.
651
- */
652
-
653
- query<T extends Component = Component>( selector: string ): T {
654
- const r = this.dom.querySelector( selector );
655
- return componentFromDOM<T>(r);
656
- }
657
-
658
-
659
- // :: STYLES ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
660
-
661
-
662
- /**
663
- * Sets an ARIA attribute on the component's DOM element.
664
- * @param name - The name of the ARIA attribute (e.g., `'aria-label'`).
665
- * @param value - The value of the ARIA attribute.
666
- * @returns The component instance for chaining.
667
- */
668
-
669
- setAria( name: keyof ariaValues, value: string | number | boolean ): this {
670
- this.setAttribute( name, value );
671
- return this;
672
- }
673
-
674
-
675
- /**
676
- * Sets multiple inline CSS styles on the component's DOM element.
677
- * Numeric values for properties like `width` or `height` will automatically append `"px"` unless they are unitless.
678
- * @param style - An object where keys are CSS property names and values are their corresponding styles.
679
- * @returns The component instance for chaining.
680
- */
681
-
682
- setStyle( style: Partial<CSSStyleDeclaration> ): this {
683
- const _style = (this.dom as HTMLElement).style;
684
-
685
- for( const name in style ) {
686
-
687
- let value = style[name];
688
- if( !unitless[name] && (isNumber(value) || RE_NUMBER.test(value)) ) {
689
- value += "px";
690
- }
691
-
692
- _style[name] = value;
693
- }
694
-
695
- return this;
696
- }
697
-
698
- /**
699
- * Sets a single inline CSS style property on the component's DOM element.
700
- * Numeric values for properties like `width` or `height` will automatically append `"px"` unless they are unitless.
701
- * @param name - The name of the CSS property.
702
- * @param value - The value of the CSS property.
703
- * @returns The component instance for chaining.
704
- */
705
-
706
- setStyleValue<K extends keyof CSSStyleDeclaration>( name: K, value: CSSStyleDeclaration[K] | number ): this {
707
-
708
- const _style = (this.dom as HTMLElement).style;
709
-
710
- if( isNumber(value) ) {
711
- let v = value+"";
712
- if( !unitless[name as string] ) {
713
- v += "px";
714
- }
715
-
716
- (_style as any)[name] = v;
717
- }
718
- else {
719
- _style[name] = value;
720
- }
721
-
722
- return this;
723
- }
724
-
725
- /**
726
- * Retrieves the computed inline CSS style value for a specific property.
727
- * This only returns styles explicitly set via `setStyle` or `setStyleValue`, not inherited or stylesheet-defined styles.
728
- * @param name - The name of the CSS property.
729
- * @returns The value of the inline CSS property.
730
- */
731
-
732
- getStyleValue<K extends keyof CSSStyleDeclaration>( name: K ) {
733
- const _style = (this.dom as HTMLElement).style;
734
- return _style[name];
735
- }
736
-
737
- /**
738
- * Sets the width of the component.
739
- * @param w - The width value. Can be a number (interpreted as pixels) or a string (e.g., `"100px"`, `"50%"`).
740
- */
741
-
742
- setWidth( w: number | string ) {
743
- this.setStyleValue( "width", isNumber(w) ? w+"px" : w );
744
- }
745
-
746
- /**
747
- * Sets the height of the component.
748
- * @param h - The height value. Can be a number (interpreted as pixels) or a string (e.g., `"100px"`, `"50%"`).
749
- */
750
-
751
- setHeight( h: number | string ) {
752
- this.setStyleValue( "height", isNumber(h) ? h+"px" : h );
753
- }
754
-
755
- /**
756
- * Sets a CSS custom property (CSS variable) on the component's DOM element.
757
- * @param name - The name of the CSS variable (e.g., `'--my-color'`).
758
- * @param value - The value to set for the CSS variable.
759
- */
760
-
761
- setStyleVariable( name: string, value: string ) {
762
- (this.dom as HTMLElement).style.setProperty( name, value );
763
- }
764
-
765
- /**
766
- * Retrieves the value of a CSS custom property (CSS variable) for the component.
767
- * The computed style of the element is used.
768
- * @param name - The name of the CSS variable.
769
- * @returns The string value of the CSS variable.
770
- */
771
-
772
- getStyleVariable( name: string ) {
773
- const style = this.getComputedStyle( );
774
- return style.getPropertyValue( name );
775
- }
776
-
777
- /**
778
- * Retrieves the computed style for the component's DOM element.
779
- * @returns A `CSSStyleDeclaration` object representing the computed styles.
780
- */
781
-
782
- getComputedStyle( ) {
783
- return getComputedStyle( this.dom );
784
- }
785
-
786
- /**
787
- * Sets pointer capture on the component's DOM element for a specific pointer.
788
- * @param pointerId - The unique ID of the pointer.
789
- *
790
- * @ex
791
- * control.on("pointerdown", (ev) => {
792
- * ev.preventDefault(); // Prevent default browser actions
793
- * control.setCapture(ev.pointerId);
794
- * }
795
- */
796
-
797
- setCapture( pointerId: number ) {
798
- this.dom.setPointerCapture( pointerId );
799
- }
800
-
801
- /**
802
- * Releases pointer capture on the component's DOM element for a specific pointer.
803
- * @param pointerId - The unique ID of the pointer.
804
- */
805
-
806
- releaseCapture( pointerId: number ) {
807
- this.dom.releasePointerCapture( pointerId );
808
- }
809
-
810
- /**
811
- * Returns the size and position of the component's DOM element relative to the viewport.
812
- * @returns A `Rect` object containing the bounding rectangle.
813
- */
814
-
815
- getBoundingRect( ): Rect {
816
- const rc = this.dom.getBoundingClientRect( );
817
- return new Rect( rc.x, rc.y, rc.width, rc.height );
818
- }
819
-
820
- // :: MISC ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
821
-
822
- /**
823
- * Gives focus to the component's DOM element.
824
- * @returns The component instance for chaining.
825
- */
826
-
827
- focus( ): this {
828
- (this.dom as HTMLElement).focus( );
829
- return this;
830
- }
831
-
832
- /**
833
- * Checks if the component's DOM element currently has focus.
834
- * @returns `true` if the component is focused, `false` otherwise.
835
- */
836
-
837
- hasFocus( ) {
838
- return document.activeElement==this.dom;
839
- }
840
-
841
- /**
842
- * Scrolls the component's DOM element into the visible area of the browser window.
843
- * @param arg - Optional. A boolean (`true` for smooth scroll) or an object specifying scroll options.
844
- */
845
-
846
- scrollIntoView(arg?: boolean | ScrollIntoViewOptions) {
847
- this.dom.scrollIntoView(arg);
848
- }
849
-
850
- /**
851
- * Checks if the component's DOM element is currently visible (i.e., not hidden by `display: none`).
852
- * @returns `true` if the component is visible, `false` otherwise.
853
- */
854
-
855
- isVisible( ) {
856
- return (this.dom as HTMLElement).offsetParent !== null;
857
- }
858
-
859
- /**
860
- * Shows or hides the component.
861
- * It toggles the `x4hidden` CSS class.
862
- * @param vis - If `true`, the component is shown; if `false`, it's hidden. Defaults to `true`.
863
- * @returns The component instance for chaining.
864
- */
865
-
866
- show( vis = true ): this {
867
- this.setClass( 'x4hidden', !vis );
868
- return this;
869
- }
870
-
871
- /**
872
- * Hides the component by applying the `x4hidden` CSS class.
873
- * @returns The component instance for chaining.
874
- */
875
-
876
- hide( ): this {
877
- this.show( false );
878
- return this;
879
- }
880
-
881
- /**
882
- * Enables or disables the component.
883
- * This sets the `disabled` attribute and also propagates the disabled state to child input elements.
884
- * @param ena - If `true`, the component is enabled; if `false`, it's disabled. Defaults to `true`.
885
- * @returns The component instance for chaining.
886
- */
887
-
888
- enable( ena = true ): this {
889
- this.setAttribute( "disabled", !ena ? 'true' : null );
890
-
891
- if( this.dom instanceof HTMLInputElement || this.dom instanceof HTMLButtonElement ) {
892
- this.dom.disabled = !ena;
893
- }
894
-
895
- // propagate diable state to all input children
896
- const nodes = this.enumChildNodes( true );
897
- nodes.forEach( x => {
898
- if( x instanceof HTMLInputElement || x instanceof HTMLButtonElement ) {
899
- x.disabled = !ena;
900
- }
901
- });
902
-
903
- return this;
904
- }
905
-
906
- /**
907
- * Disables the component.
908
- * @returns The component instance for chaining.
909
- */
910
-
911
- disable( ): this {
912
- this.enable( false );
913
- return this;
914
- }
915
-
916
- /**
917
- * Checks if the component is marked as disabled.
918
- * This checks for the presence of the `disabled` attribute.
919
- * @returns The string value of the `disabled` attribute, or `null` if not present.
920
- */
921
-
922
- isDisabled( ) {
923
- return this.getAttribute('disabled');
924
- }
925
-
926
- /**
927
- * Returns the next sibling element as a Component instance.
928
- * @returns The next sibling component, or `null` if none exists.
929
- */
930
-
931
- nextElement<T extends Component = Component>( ): T {
932
- const nxt = this.dom.nextElementSibling;
933
- return componentFromDOM<T>( nxt );
934
- }
935
-
936
- /**
937
- * Returns the previous sibling element as a Component instance.
938
- * @returns The previous sibling component, or `null` if none exists.
939
- */
940
-
941
- prevElement<T extends Component = Component>( ): T {
942
- const nxt = this.dom.previousElementSibling;
943
- return componentFromDOM<T>( nxt );
944
- }
945
-
946
- /**
947
- * Searches up the DOM tree for a parent element that is a Component and optionally matches a specific constructor.
948
- * @param cls - Optional. The constructor of the Component type to match.
949
- * @returns The matching parent Component instance, or `null` if not found.
950
- */
951
-
952
- parentElement<T extends Component>( cls?: Constructor<T> ): T {
953
- return Component.parentElement<T>( this.dom, cls );
954
- }
955
-
956
- /**
957
- *
958
- */
959
-
960
- childCount( ) {
961
- return this.dom.childElementCount;
962
- }
963
-
964
- /**
965
- * Static method to search up the DOM tree for a parent element that is a Component and optionally matches a specific constructor.
966
- * @param dom - The starting DOM node from which to search upwards.
967
- * @param cls - Optional. The constructor of the Component type to match.
968
- * @returns The matching parent Component instance, or `null` if not found.
969
- */
970
-
971
-
972
- static parentElement<T extends Component>( dom: Node, cls?: Constructor<T> ): T {
973
-
974
- while( dom.parentElement ) {
975
- const cp = componentFromDOM( dom.parentElement );
976
- if( !cls ) {
977
- return cp as T;
978
- }
979
-
980
- if( cp && cp instanceof cls ) {
981
- return cp;
982
- }
983
-
984
- dom = dom.parentElement;
985
- }
986
-
987
- return null;
988
- }
989
-
990
- /**
991
- * Returns the first child element as a Component instance.
992
- * @returns The first child component, or `null` if none exists.
993
- */
994
-
995
- firstChild<T extends Component = Component>( ) : T {
996
- const nxt = this.dom.firstElementChild;
997
- return componentFromDOM<T>( nxt );
998
- }
999
-
1000
- /**
1001
- * Returns the last child element as a Component instance.
1002
- * @returns The last child component, or `null` if none exists.
1003
- */
1004
-
1005
- lastChild<T extends Component = Component>( ) : T {
1006
- const nxt = this.dom.lastElementChild;
1007
- return componentFromDOM( nxt );
1008
- }
1009
-
1010
- /**
1011
- * Enumerates all child components of this component.
1012
- * @param recursive - If `true`, searches all descendants; otherwise, only direct children.
1013
- * @returns An array of child Component instances.
1014
- */
1015
-
1016
- enumChildComponents( recursive: boolean ) {
1017
-
1018
- const children: Component[] = [];
1019
-
1020
- const nodes = this.enumChildNodes( recursive );
1021
- nodes.forEach( ( c: Node ) => {
1022
- const cc = componentFromDOM( c as HTMLElement );
1023
- if( cc ) {
1024
- children.push(cc);
1025
- }
1026
- } );
1027
-
1028
- return children;
1029
- }
1030
-
1031
- /**
1032
- * Enumerates all child DOM nodes of this component.
1033
- * Not all nodes may be components.
1034
- * @param recursive - If `true`, searches all descendant nodes; otherwise, only direct children.
1035
- * @returns An array of child DOM nodes.
1036
- */
1037
-
1038
- enumChildNodes( recursive: boolean ) {
1039
- const children: Node[] = Array.from( recursive ? this.dom.querySelectorAll( '*' ) : this.dom.children );
1040
- return children;
1041
- }
1042
-
1043
- /**
1044
- * Visits all descendant components of this component, executing a callback function for each.
1045
- * The traversal stops if the callback returns `true`.
1046
- * @param cb - The callback function to execute for each component.
1047
- */
1048
-
1049
- visitChildren( cb: ( el: Component ) => boolean ) {
1050
-
1051
- const visit = ( p: Element ) => {
1052
- for( let d=p.firstElementChild; d; d=d.nextElementSibling ) {
1053
- const comp = componentFromDOM( d as Element );
1054
- if( comp ) {
1055
- if( cb( comp ) ) {
1056
- return true;
1057
- }
1058
- }
1059
-
1060
- // avoid visit of svg elements
1061
- if( d.firstElementChild && d.tagName!="svg" && d.tagName!="SVG" ) {
1062
- if( visit( d ) ) {
1063
- return true;
1064
- }
1065
- }
1066
- }
1067
- }
1068
-
1069
- visit( this.dom );
1070
- }
1071
-
1072
- /**
1073
- * Animates the component's DOM element using the Web Animations API.
1074
- * @param keyframes - An array of keyframe objects or a `Keyframe` object.
1075
- * @param duration - The duration of the animation in milliseconds, or a `KeyframeAnimationOptions` object.
1076
- */
1077
-
1078
- animate( keyframes: Keyframe[], duration: number ) {
1079
- this.dom.animate(keyframes,duration);
1080
- }
1081
-
1082
-
1083
- // :: TSX/REACT ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
1084
-
1085
- /**
1086
- * Creates a new Component or an array of Components from JSX elements.
1087
- * This method is typically called by the TypeScript/JavaScript compiler when JSX is transpiled.
1088
- * @param clsOrTag - The class constructor of the component, an HTML tag name string, a symbol for fragments, or a callback.
1089
- * @param attrs - An object containing attributes and properties for the component.
1090
- * @param children - Any child components or content passed within the JSX.
1091
- * @returns A Component instance or an array of Components.
1092
- */
1093
-
1094
- static createElement( clsOrTag: string | ComponentConstructor | symbol | CreateComponentCallBack, attrs: any, ...children: Component[] ): Component | Component[] {
1095
-
1096
- let comp: Component;
1097
-
1098
- // fragment
1099
- if( clsOrTag==this.createFragment || clsOrTag===FRAGMENT ) {
1100
- return children;
1101
- }
1102
-
1103
- // class constructor, yes : dirty
1104
- if( clsOrTag instanceof Function ) {
1105
- attrs = attrs ?? {};
1106
- if( !attrs.children && children && children.length ) {
1107
- attrs.content = children;
1108
- }
1109
-
1110
- comp = new (clsOrTag as any)( attrs ?? {} );
1111
- }
1112
- // basic tag
1113
- else {
1114
- comp = new Component( {
1115
- tag: clsOrTag,
1116
- content: children,
1117
- ...attrs,
1118
- });
1119
- }
1120
-
1121
- if( children && children.length ) {
1122
- //comp.setContent( children );
1123
- }
1124
-
1125
- return comp;
1126
- }
1127
-
1128
- /**
1129
- * Creates a fragment, which is an array of components without a parent DOM element.
1130
- * Used for grouping multiple children in JSX without introducing an extra DOM node.
1131
- * @returns An array of components.
1132
- */
1133
-
1134
- static createFragment( ): Component[] {
1135
- return this.createElement( FRAGMENT, null ) as Component[];
1136
- }
1137
-
1138
- // :: SPECIALS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
1139
-
1140
- /**
1141
- * Queries for a specific system or application-defined interface on the component.
1142
- * Common system interfaces include "form-element" and "tab-handler".
1143
- * @param name - The name of the interface to query.
1144
- * @returns An object conforming to the requested interface, or `null` if not supported.
1145
- *
1146
- * system interfaces:
1147
- * "form-element"
1148
- * "tab-handler"
1149
- */
1150
-
1151
- queryInterface<T>( name: string ): T {
1152
- return null;
1153
- }
1154
- }
1155
-
1156
-
1157
- /**
1158
- * Type definition for a constructor that creates a Component instance.
1159
- */
1160
-
1161
- type ComponentConstructor = {
1162
- new(...params: any[]): Component;
1163
- };
1164
-
1165
- /**
1166
- * Retrieves a Component instance associated with a given DOM element.
1167
- * Components created by this library internally store a reference to their instance on their `dom` property.
1168
- * @param node - The DOM element to check.
1169
- * @returns The Component instance if found, otherwise `null`.
1170
- */
1171
-
1172
- export function componentFromDOM<T extends Component = Component>( node: Element ) {
1173
- return node ? (node as any)[COMPONENT] as T : null;
1174
- }
1175
-
1176
- /**
1177
- * Wraps an existing HTMLElement with a new Component instance.
1178
- * If the HTMLElement already has an associated Component, that instance is returned.
1179
- * Otherwise, a new `Component` is created to manage the existing DOM element.
1180
- * @param el - The HTMLElement to wrap.
1181
- * @returns A Component instance managing the provided HTMLElement.
1182
- */
1183
-
1184
- export function wrapDOM( el: HTMLElement ): Component {
1185
- const com = componentFromDOM(el);
1186
- if( com ) {
1187
- return com;
1188
- }
1189
-
1190
- return new Component( { existingDOM: el } );
1191
- }
1192
-
1193
-
1194
- // :: Special components ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
1195
-
1196
- /**
1197
- * A basic component class that provides flexible sizing, typically used in flex layouts.
1198
- * Automatically generates CSS class: `x4flex`.
1199
- */
1200
-
1201
- export class Flex extends Component {
1202
- constructor( ) {
1203
- super({})
1204
- }
1205
- }
1206
-
1207
- /**
1208
- * A simple spacer component used for creating empty space, often in flex containers.
1209
- * @example
1210
- * ```ts
1211
- * new Space(); // default spacer
1212
- * new Space(10); // 10px wide spacer
1213
- * new Space("1em", "my-spacer-class");
1214
- * ```
1215
- */
1216
-
1217
- export class Space extends Component {
1218
- constructor( width?: number|string, cls?: string ) {
1219
- super( { width, cls } )
1220
- }
1221
- }
1222
-
1223
-
1224
- // :: HIGH LEVEL BASIC EVENTS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
1225
-
1226
-
1227
-
1228
- /**
1229
- * Click Event
1230
- * click event do not have any additional parameters
1231
- */
1232
-
1233
- export interface EvClick extends ComponentEvent {
1234
- repeat?: number;
1235
- }
1236
-
1237
- /**
1238
- * Change Event
1239
- * value is the the element value
1240
- */
1241
-
1242
- export interface EvChange extends ComponentEvent {
1243
- readonly value: any;
1244
- }
1245
-
1246
- /**
1247
- * Focus event
1248
- */
1249
-
1250
- export interface EvFocus extends ComponentEvent {
1251
- readonly focus_out: boolean;
1252
- }
1253
-
1254
- /**
1255
- * Selection Event
1256
- * value is the new selection or null
1257
- */
1258
-
1259
- type ISelection = number | string | any;
1260
-
1261
- export interface EvSelectionChange extends ComponentEvent {
1262
- readonly selection: ISelection[];
1263
- readonly empty: boolean;
1264
- }
1265
-
1266
-
1267
- /**
1268
- * ContextMenu Event
1269
- */
1270
-
1271
- export interface EvContextMenu extends ComponentEvent {
1272
- uievent: MouseEvent; // UI event that fire this event
1273
- }
1274
-
1275
- /**
1276
- * Drag/Drop event
1277
- */
1278
-
1279
- export interface EvDrag extends ComponentEvent {
1280
- element: unknown;
1281
- data: any;
1282
- }
1283
-
1284
- /**
1285
- * Errors
1286
- */
1287
-
1288
- export interface EvError extends ComponentEvent {
1289
- code: number;
1290
- message: string;
1291
- }
1292
-
1293
- /**
1294
- * DblClick Event
1295
- */
1296
-
1297
- export interface EvDblClick extends ComponentEvent {
1298
- }
1299
-
1
+ /**
2
+ * ___ ___ __
3
+ * \ \/ / / _
4
+ * \ / /_| |_
5
+ * / \____ _|
6
+ * /__/\__\ |_|
7
+ *
8
+ * @file component.ts
9
+ * @author Etienne Cochard
10
+ *
11
+ * @copyright (c) 2024 R-libre ingenierie
12
+ *
13
+ * Use of this source code is governed by an MIT-style license
14
+ * that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
15
+ **/
16
+
17
+ import { isArray, UnsafeHtml, isNumber, Rect, Constructor, class_ns, x4_class_ns_sym, IRect } from './core_tools.ts';
18
+ import { CoreElement } from './core_element.ts';
19
+ import { ariaValues, unitless } from './core_styles.ts';
20
+ import { CoreEvent, EventMap } from './core_events.ts';
21
+ import { addEvent, DOMEventHandler, GlobalDOMEvents } from './core_dom.ts';
22
+ import { Application, EvMessage } from './core_application.ts';
23
+
24
+ interface RefType<T extends Component> {
25
+ dom: T;
26
+ }
27
+
28
+ type ComponentAttributes = Record<string,string|number|boolean>;
29
+ type CreateComponentCallBack = ( attrs: Record<string,string> ) => ComponentContent;
30
+
31
+ const FRAGMENT = Symbol( "fragment" );
32
+ const COMPONENT = Symbol( "component" );
33
+
34
+ const RE_NUMBER = /^-?\d+(\.\d*)?$/;
35
+
36
+ /**
37
+ * you can change css classname prefix by adding
38
+ *
39
+ * ```
40
+ * static "$cls-ns" = "<your prefix>";
41
+ * ```
42
+ *
43
+ * to your class to avoid autogenerated css class names conflicts
44
+ */
45
+
46
+ function genClassNames( x: any ): string[] {
47
+
48
+ const classes = [];
49
+ let self = Object.getPrototypeOf(x);
50
+
51
+ if( self.constructor==Component ) {
52
+ return ["x4-comp"];
53
+ }
54
+
55
+ while (self && self.constructor !== Component ) {
56
+ const clsname:string = self.constructor.name;
57
+ const clsns: string = Object.prototype.hasOwnProperty.call(self.constructor,x4_class_ns_sym) ? self.constructor[x4_class_ns_sym] : "";
58
+ classes.push( clsns+clsname.toLowerCase() );
59
+ self = Object.getPrototypeOf(self);
60
+ }
61
+
62
+ return classes;
63
+ }
64
+
65
+ /**
66
+ *
67
+ */
68
+
69
+ export type ComponentContent = Component | Component[] | string | string[] | UnsafeHtml| UnsafeHtml[] | number | boolean;
70
+
71
+ let gen_id = 1000;
72
+
73
+ export const makeUniqueComponentId = ( ) => {
74
+ return `x4-${gen_id++}`;
75
+ }
76
+
77
+ /**
78
+ * Base properties for all components.
79
+ */
80
+
81
+ export interface ComponentProps {
82
+ /** HTML tag name (default: `"div"`). */
83
+ tag?: string;
84
+ /** Namespace for SVG/MathML elements. */
85
+ ns?: string;
86
+ /** Inline CSS styles. */
87
+ style?: Partial<CSSStyleDeclaration>;
88
+ /** HTML attributes. */
89
+ attrs?: Record<string,string|number|boolean>;
90
+ /** Child content (components, strings, or HTML). */
91
+ content?: ComponentContent;
92
+ /** DOM event listeners. */
93
+ dom_events?: GlobalDOMEvents;
94
+ /** Additional CSS classes. */
95
+ cls?: string;
96
+ /** Element ID. */
97
+ id?: string;
98
+ /** Reference to the component instance. */
99
+ ref?: RefType<any>;
100
+
101
+ // shortcuts
102
+ /** Width (px or string like `"50%"` or `"3em"`). */
103
+ width?: string | number;
104
+ /** Height (px or string like `"50%"`). */
105
+ height?: string | number;
106
+ /** Component is initialy disabled. */
107
+ disabled?: boolean,
108
+ /** Component is initialy hidden. */
109
+ hidden?: boolean,
110
+ /** Enables flex layout (boolean) or sets flex-grow (number). */
111
+ flex?: boolean | number;
112
+ /** Tooltip text. */
113
+ tooltip?: string;
114
+ /** Existing DOM element to wrap. */
115
+ existingDOM?: HTMLElement;
116
+
117
+ // index signature
118
+ // to avoid errors: Type 'X' has no properties in common with type 'Y'
119
+ // because all memebers here are optional.
120
+ // this allow TS to recongnize derived props as ComponentProps
121
+ //[key: string]: any;
122
+ }
123
+
124
+
125
+ /**
126
+ *
127
+ */
128
+
129
+ export interface ComponentEvent extends CoreEvent {
130
+ }
131
+
132
+ /**
133
+ *
134
+ */
135
+
136
+ export interface ComponentEvents extends EventMap {
137
+ }
138
+
139
+ /**
140
+ * Base component class with DOM integration and event handling.
141
+ * Auto-generates CSS class: `x4comp` + derived class names (e.g., `x4button`).
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * // Basic div
146
+ * const div = new Component({ tag: "div", content: "Hello" });
147
+ *
148
+ * // Custom element
149
+ * class MyComponent extends Component {}
150
+ * const inst = new MyComponent({ cls: "my-class" });
151
+ * ```
152
+ */
153
+
154
+ @class_ns( "x4" )
155
+ export class Component<P extends ComponentProps = ComponentProps, E extends ComponentEvents = ComponentEvents>
156
+ extends CoreElement<E> {
157
+
158
+ /** The underlying DOM element of the component. */
159
+ readonly dom: Element;
160
+
161
+ /** The properties passed to the component's constructor. */
162
+ readonly props: P;
163
+
164
+ protected readonly clsprefix: string; // internal class name prefix (x4 internal)
165
+
166
+ #store: Map<string|symbol,any>;
167
+
168
+
169
+ constructor( props: P ) {
170
+ super( );
171
+
172
+ this.props = props; // copy ?
173
+
174
+ if( props.existingDOM ) {
175
+ this.dom = props.existingDOM;
176
+ }
177
+ else {
178
+ if( props.ns ) {
179
+ this.dom = document.createElementNS( props.ns, props.tag ?? "div" );
180
+ }
181
+ else {
182
+ this.dom = document.createElement( props.tag ?? "div" );
183
+ }
184
+
185
+ if (props.attrs) {
186
+ this.setAttributes( props.attrs );
187
+ }
188
+
189
+ if( props.cls ) {
190
+ this.addClass( props.cls );
191
+ }
192
+
193
+ if( props.hidden ) {
194
+ this.show( false );
195
+ }
196
+
197
+ if( props.flex===true ) {
198
+ this.addClass( "x4flex" );
199
+ }
200
+ else if( props.flex!==undefined ) {
201
+ this.setStyle( {
202
+ "flexGrow": props.flex+""
203
+ });
204
+ }
205
+
206
+ if( props.id!==undefined ) {
207
+ this.setAttribute( "id", props.id );
208
+ }
209
+
210
+ // small shortcut
211
+ if( props.width!==undefined ) {
212
+ this.setStyleValue( "width", props.width );
213
+ }
214
+
215
+ if( props.height!==undefined ) {
216
+ this.setStyleValue( "height", props.height );
217
+ }
218
+
219
+ if( props.tooltip ) {
220
+ this.setAttribute( "tooltip", props.tooltip );
221
+ }
222
+
223
+ if( props.style ) {
224
+ this.setStyle( props.style );
225
+ }
226
+
227
+ if( props.content ) {
228
+ this.setContent( props.content );
229
+ }
230
+
231
+ if( props.dom_events ) {
232
+ this.setDOMEvents( props.dom_events );
233
+ }
234
+
235
+ const classes = genClassNames( this );
236
+ this.dom.classList.add( ...classes );
237
+
238
+ // need to have children for next statements
239
+ // and children way be created in caller
240
+ if( props.disabled ) {
241
+ this.addDOMEvent( "created", ( ) => {
242
+ this.enable( false );
243
+ } );
244
+ }
245
+ }
246
+
247
+ (this.dom as any)[COMPONENT] = this;
248
+ }
249
+
250
+ /**
251
+ * Attaches a listener for global messages dispatched by the application.
252
+ * The listener is automatically removed when the component's DOM element is removed.
253
+ * @param cb - The callback function to execute when a global message is received.
254
+ */
255
+
256
+ onGlobalEvent( cb: ( ev: EvMessage ) => void ) {
257
+
258
+ const off = Application.instance().on( "global", ev => {
259
+ cb( ev );
260
+ })
261
+
262
+ this.addDOMEvent( "removed", ( ) => off.off() );
263
+ }
264
+
265
+
266
+ // :: CLASSES ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
267
+
268
+ /**
269
+ * Checks if the component's DOM element has a specific CSS class.
270
+ * @param cls - The CSS class name to check.
271
+ * @returns `true` if the class is present, `false` otherwise.
272
+ */
273
+
274
+ hasClass( cls: string ) {
275
+ return this.dom.classList.contains( cls );
276
+ }
277
+
278
+ /**
279
+ * Adds one or more CSS classes to the component's DOM element.
280
+ * Multiple classes can be provided as a space-separated string.
281
+ * @param cls - The CSS class(es) to add.
282
+ */
283
+
284
+ addClass( cls: string ) {
285
+ if( !cls ) return;
286
+
287
+ cls = cls.trim( );
288
+
289
+ if( cls.includes(' ') ) {
290
+ const ccs = cls.split( " " );
291
+ this.dom.classList.add( ...ccs.filter(x=>x) );
292
+ }
293
+ else {
294
+ this.dom.classList.add(cls);
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Removes one or more CSS classes from the component's DOM element.
300
+ * If `*` is passed as the class name, all classes will be removed.
301
+ * Multiple classes can be provided as a space-separated string.
302
+ * @param cls - The CSS class(es) to remove, or `*` to clear all.
303
+ */
304
+
305
+ removeClass( cls: string ) {
306
+ if( !cls ) return;
307
+
308
+ if( cls=='*' ) {
309
+ this.dom.classList.value = "";
310
+ return;
311
+ }
312
+
313
+ if( cls.indexOf(' ')>=0 ) {
314
+ const ccs = cls.split( " " );
315
+ this.dom.classList.remove(...ccs);
316
+ }
317
+ else {
318
+ this.dom.classList.remove(cls);
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Removes all CSS classes from the component's DOM element that match a given regular expression.
324
+ * @param re - The regular expression to match against class names.
325
+ */
326
+
327
+ removeClassEx( re: RegExp ) {
328
+ const all = Array.from( this.dom.classList );
329
+ all.forEach( x => {
330
+ if( x.match(re) ) {
331
+ this.dom.classList.remove( x );
332
+ }
333
+ });
334
+ }
335
+
336
+ /**
337
+ * Toggles the presence of one or more CSS classes on the component's DOM element.
338
+ * If a class is present, it's removed; otherwise, it's added.
339
+ * Multiple classes can be provided as a space-separated string.
340
+ * @param cls - The CSS class(es) to toggle.
341
+ */
342
+
343
+ toggleClass( cls: string ) {
344
+ if( !cls ) return;
345
+
346
+ const toggle = ( x: string ) => {
347
+ this.dom.classList.toggle(x);
348
+ }
349
+
350
+ if( cls.indexOf(' ')>=0 ) {
351
+ const ccs = cls.split( " " );
352
+ ccs.forEach( toggle );
353
+ }
354
+ else {
355
+ toggle( cls );
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Sets or removes a CSS class based on a boolean condition.
361
+ * @param cls - The CSS class to manage.
362
+ * @param set - If `true`, the class is added; if `false`, it's removed. Defaults to `true`.
363
+ * @returns The component instance for chaining.
364
+ */
365
+
366
+ setClass( cls: string, set: boolean = true ) : this {
367
+ if( set ) this.addClass(cls);
368
+ else this.removeClass( cls );
369
+ return this;
370
+ }
371
+
372
+ // :: ATTRIBUTES ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
373
+
374
+ /**
375
+ * Sets multiple HTML attributes on the component's DOM element.
376
+ * @param attrs - An object where keys are attribute names and values are their corresponding values.
377
+ * @returns The component instance for chaining.
378
+ */
379
+
380
+ setAttributes( attrs: ComponentAttributes ): this {
381
+ for( const name in attrs ) {
382
+ this.setAttribute( name, attrs[name] );
383
+ }
384
+ return this;
385
+ }
386
+
387
+ /**
388
+ * Sets a single HTML attribute on the component's DOM element.
389
+ * If `value` is `null`, `undefined`, or `false`, the attribute will be removed.
390
+ * @param name - The name of the attribute.
391
+ * @param value - The value of the attribute.
392
+ */
393
+
394
+ setAttribute( name: string, value: string | number | boolean ) {
395
+ if( value===null || value===undefined || value===false ) {
396
+ this.dom.removeAttribute( name );
397
+ }
398
+ else {
399
+ this.dom.setAttribute( name, ""+value );
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Retrieves the value of an HTML attribute from the component's DOM element.
405
+ * @param name - The name of the attribute.
406
+ * @returns The string value of the attribute, or `null` if not present.
407
+ */
408
+
409
+ getAttribute( name: string ): string {
410
+ return this.dom.getAttribute( name );
411
+ }
412
+
413
+ /**
414
+ * Retrieves the value of a `data-*` attribute from the component's DOM element.
415
+ * @param name - The suffix of the `data-` attribute (e.g., for `data-foo`, use `"foo"`).
416
+ * @returns The string value of the `data-*` attribute, or `null` if not present.
417
+ *
418
+ * @cf setIntData/getIntData (number)
419
+ * @cf setInternalData/getInternalData (typed data)
420
+ */
421
+
422
+ getData( name: string ) : string {
423
+ return this.getAttribute( "data-"+name );
424
+ }
425
+
426
+ /**
427
+ * Retrieves the integer value of a `data-*` attribute from the component's DOM element.
428
+ * Returns `undefined` if the attribute is not present or cannot be parsed as a number.
429
+ * @param name - The suffix of the `data-` attribute.
430
+ * @returns The integer value of the `data-*` attribute, or `undefined`.
431
+ */
432
+
433
+ getIntData( name: string ) : number {
434
+ const v = parseInt( this.getAttribute( "data-"+name ) );
435
+ if( Number.isFinite(v) ) {
436
+ return v;
437
+ }
438
+
439
+ return undefined;
440
+ }
441
+
442
+ /**
443
+ * Sets the value of a `data-*` attribute on the component's DOM element.
444
+ * @param name - The suffix of the `data-` attribute.
445
+ * @param value - The string value to set.
446
+ */
447
+
448
+ setData( name: string, value: string ) {
449
+ return this.setAttribute( "data-"+name, value );
450
+ }
451
+
452
+ /**
453
+ * idem as setData but onot on dom, you can store anything
454
+ */
455
+
456
+ setInternalData<T>( name: string|symbol, value: T ): this {
457
+ if( !this.#store ) {
458
+ this.#store = new Map( );
459
+ }
460
+
461
+ this.#store.set( name, value );
462
+ return this;
463
+ }
464
+
465
+ getInternalData<T = any>( name: string|symbol ): T {
466
+ return this.#store?.get(name) as T;
467
+ }
468
+
469
+
470
+ // :: DOM EVENTS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
471
+
472
+ /**
473
+ * Adds a DOM event listener to the component's DOM element.
474
+ * @param name - The name of the DOM event (e.g., `'click'`, `'mouseover'`).
475
+ * @param listener - The event handler function.
476
+ * @param prepend - If `true`, the listener is added to the beginning of the event listener list. Defaults to `false`.
477
+ */
478
+
479
+ addDOMEvent<K extends keyof GlobalDOMEvents>( name: K, listener: GlobalDOMEvents[K], prepend = false ) {
480
+ addEvent( this.dom, name, listener as DOMEventHandler, prepend );
481
+ }
482
+
483
+ /**
484
+ * Sets multiple DOM event listeners on the component's DOM element.
485
+ * @param events - An object where keys are event names and values are their corresponding handler functions.
486
+ */
487
+
488
+ setDOMEvents( events: GlobalDOMEvents ) {
489
+ for( const name in events ) {
490
+ this.addDOMEvent( name as any, (events as any)[name] );
491
+ }
492
+ }
493
+
494
+ // :: HILEVEL EVENTS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
495
+
496
+ /**
497
+ * tool to move named events to internal event map
498
+ * @internal
499
+ */
500
+
501
+ protected mapPropEvents<N extends keyof E>(props: P, ...elements: N[] ) {
502
+ const p = props as any;
503
+ elements.forEach( n => {
504
+ if (Object.prototype.hasOwnProperty.call(p,n) && p[n]) {
505
+ this.on( n, p[n] );
506
+ }
507
+ });
508
+ }
509
+
510
+ // :: CONTENT ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
511
+
512
+ /**
513
+ * Removes all child nodes from the component's DOM element.
514
+ */
515
+
516
+ clearContent( ) {
517
+ const d = this.dom;
518
+ while( d.firstChild ) {
519
+ d.removeChild( d.firstChild );
520
+ }
521
+ }
522
+
523
+ /**
524
+ * Replaces the entire content of the component's DOM element with new content.
525
+ * Any existing content will be cleared before the new content is added.
526
+ * @param content - The new content to set. Can be a single item or an array of items.
527
+ */
528
+
529
+ setContent( content: ComponentContent ) {
530
+ this.clearContent( );
531
+ this.appendContent( content );
532
+ }
533
+
534
+ /**
535
+ * Appends content to the end of the component's DOM element.
536
+ * Content can be a single Component, an array of Components, a string, an array of strings,
537
+ * raw HTML, an array of raw HTML, a number, or a boolean.
538
+ * @param content - The content to append.
539
+ */
540
+
541
+ appendContent( content: ComponentContent ) {
542
+ const set = ( d: any, c: Component | string | UnsafeHtml | number | boolean ) => {
543
+
544
+ if (c instanceof Component ) {
545
+ d.appendChild( c.dom );
546
+ }
547
+ else if( c instanceof UnsafeHtml) {
548
+ d.insertAdjacentHTML( 'beforeend' , c.toString() );
549
+ }
550
+ else if (typeof c === "string" || typeof c === "number") {
551
+ const tnode = document.createTextNode(c.toString());
552
+ d.appendChild( tnode );
553
+ }
554
+ else if( c ) {
555
+ console.warn("Unknown type to append: ", c);
556
+ }
557
+ }
558
+
559
+ if( !isArray(content) ) {
560
+ set( this.dom, content );
561
+ }
562
+ else if( content.length<=8 ) {
563
+ for( const c of content ) {
564
+ set( this.dom, c );
565
+ }
566
+ }
567
+ else {
568
+ const fragment = document.createDocumentFragment( ) as any;
569
+
570
+ // polyfill
571
+ fragment.insertAdjacentHTML = ( position: string, html: string ) => {
572
+ const temp = document.createElement('div');
573
+ temp.innerHTML = html;
574
+ const nodes = Array.from(temp.childNodes);
575
+ fragment.append( ...nodes );
576
+ }
577
+
578
+ for (const child of content ) {
579
+ set( fragment, child );
580
+ }
581
+
582
+ this.dom.appendChild( fragment );
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Prepends content to the beginning of the component's DOM element.
588
+ * Content can be a single Component, an array of Components, a string, an array of strings,
589
+ * raw HTML, an array of raw HTML, a number, or a boolean.
590
+ * @param content - The content to prepend.
591
+ */
592
+
593
+ prependContent( content: ComponentContent ) {
594
+ const d = this.dom;
595
+ const set = ( c: Component | string | UnsafeHtml | number | boolean ) => {
596
+ if (c instanceof Component ) {
597
+ d.insertAdjacentElement( 'afterbegin', c.dom );
598
+ }
599
+ else if( c instanceof UnsafeHtml) {
600
+ d.insertAdjacentHTML( 'afterbegin', c.toString() );
601
+ }
602
+ else if (typeof c === "string" || typeof c === "number") {
603
+ d.insertAdjacentText( 'afterbegin', c.toString() );
604
+ }
605
+ else {
606
+ console.warn("Unknown type to append: ", c);
607
+ }
608
+ }
609
+
610
+ if( !isArray(content) ) {
611
+ set( content );
612
+ }
613
+ else {
614
+ const fragment = document.createDocumentFragment( );
615
+ for (const child of content ) {
616
+ set( child );
617
+ }
618
+
619
+ d.insertBefore( d.firstChild, fragment );
620
+ }
621
+ }
622
+
623
+ /**
624
+ * Removes a specific child component from this component's DOM element.
625
+ * @param child - The child component instance to remove.
626
+ * @cf clearContent
627
+ */
628
+
629
+ removeChild( child: Component ) {
630
+ this.dom.removeChild( child.dom );
631
+ }
632
+
633
+
634
+ /**
635
+ * Queries all descendant DOM elements matching a CSS selector and wraps them as Component instances.
636
+ * @param selector - The CSS selector string.
637
+ * @returns An array of Component instances.
638
+ */
639
+
640
+ queryAll( selector: string ): Component[] {
641
+ const all = this.dom.querySelectorAll( selector );
642
+ const rc = new Array( all.length );
643
+ all.forEach( (x,i) => rc[i]=wrapDOM(x as HTMLElement) );
644
+ return rc;
645
+ }
646
+
647
+ /**
648
+ * Queries the first descendant DOM element matching a CSS selector and wraps it as a Component instance.
649
+ * @param selector - The CSS selector string.
650
+ * @returns The first matching Component instance, or `null` if no match is found.
651
+ */
652
+
653
+ query<T extends Component = Component>( selector: string ): T {
654
+ const r = this.dom.querySelector( selector );
655
+ return componentFromDOM<T>(r);
656
+ }
657
+
658
+
659
+ // :: STYLES ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
660
+
661
+
662
+ /**
663
+ * Sets an ARIA attribute on the component's DOM element.
664
+ * @param name - The name of the ARIA attribute (e.g., `'aria-label'`).
665
+ * @param value - The value of the ARIA attribute.
666
+ * @returns The component instance for chaining.
667
+ */
668
+
669
+ setAria( name: keyof ariaValues, value: string | number | boolean ): this {
670
+ this.setAttribute( name, value );
671
+ return this;
672
+ }
673
+
674
+
675
+ /**
676
+ * Sets multiple inline CSS styles on the component's DOM element.
677
+ * Numeric values for properties like `width` or `height` will automatically append `"px"` unless they are unitless.
678
+ * @param style - An object where keys are CSS property names and values are their corresponding styles.
679
+ * @returns The component instance for chaining.
680
+ */
681
+
682
+ setStyle( style: Partial<CSSStyleDeclaration> ): this {
683
+ const _style = (this.dom as HTMLElement).style;
684
+
685
+ for( const name in style ) {
686
+
687
+ let value = style[name];
688
+ if( !unitless[name] && (isNumber(value) || RE_NUMBER.test(value)) ) {
689
+ value += "px";
690
+ }
691
+
692
+ _style[name] = value;
693
+ }
694
+
695
+ return this;
696
+ }
697
+
698
+ /**
699
+ * Sets a single inline CSS style property on the component's DOM element.
700
+ * Numeric values for properties like `width` or `height` will automatically append `"px"` unless they are unitless.
701
+ * @param name - The name of the CSS property.
702
+ * @param value - The value of the CSS property.
703
+ * @returns The component instance for chaining.
704
+ */
705
+
706
+ setStyleValue<K extends keyof CSSStyleDeclaration>( name: K, value: CSSStyleDeclaration[K] | number ): this {
707
+
708
+ const _style = (this.dom as HTMLElement).style;
709
+
710
+ if( isNumber(value) ) {
711
+ let v = value+"";
712
+ if( !unitless[name as string] ) {
713
+ v += "px";
714
+ }
715
+
716
+ (_style as any)[name] = v;
717
+ }
718
+ else {
719
+ _style[name] = value;
720
+ }
721
+
722
+ return this;
723
+ }
724
+
725
+ /**
726
+ * Retrieves the computed inline CSS style value for a specific property.
727
+ * This only returns styles explicitly set via `setStyle` or `setStyleValue`, not inherited or stylesheet-defined styles.
728
+ * @param name - The name of the CSS property.
729
+ * @returns The value of the inline CSS property.
730
+ */
731
+
732
+ getStyleValue<K extends keyof CSSStyleDeclaration>( name: K ) {
733
+ const _style = (this.dom as HTMLElement).style;
734
+ return _style[name];
735
+ }
736
+
737
+ /**
738
+ * Sets the width of the component.
739
+ * @param w - The width value. Can be a number (interpreted as pixels) or a string (e.g., `"100px"`, `"50%"`).
740
+ */
741
+
742
+ setWidth( w: number | string ) {
743
+ this.setStyleValue( "width", isNumber(w) ? w+"px" : w );
744
+ }
745
+
746
+ /**
747
+ * Sets the height of the component.
748
+ * @param h - The height value. Can be a number (interpreted as pixels) or a string (e.g., `"100px"`, `"50%"`).
749
+ */
750
+
751
+ setHeight( h: number | string ) {
752
+ this.setStyleValue( "height", isNumber(h) ? h+"px" : h );
753
+ }
754
+
755
+ /**
756
+ * Sets a CSS custom property (CSS variable) on the component's DOM element.
757
+ * @param name - The name of the CSS variable (e.g., `'--my-color'`).
758
+ * @param value - The value to set for the CSS variable.
759
+ */
760
+
761
+ setStyleVariable( name: string, value: string ) {
762
+ (this.dom as HTMLElement).style.setProperty( name, value );
763
+ }
764
+
765
+ /**
766
+ * Retrieves the value of a CSS custom property (CSS variable) for the component.
767
+ * The computed style of the element is used.
768
+ * @param name - The name of the CSS variable.
769
+ * @returns The string value of the CSS variable.
770
+ */
771
+
772
+ getStyleVariable( name: string ) {
773
+ const style = this.getComputedStyle( );
774
+ return style.getPropertyValue( name );
775
+ }
776
+
777
+ /**
778
+ * Retrieves the computed style for the component's DOM element.
779
+ * @returns A `CSSStyleDeclaration` object representing the computed styles.
780
+ */
781
+
782
+ getComputedStyle( ) {
783
+ return getComputedStyle( this.dom );
784
+ }
785
+
786
+ /**
787
+ * Sets pointer capture on the component's DOM element for a specific pointer.
788
+ * @param pointerId - The unique ID of the pointer.
789
+ *
790
+ * @ex
791
+ * control.on("pointerdown", (ev) => {
792
+ * ev.preventDefault(); // Prevent default browser actions
793
+ * control.setCapture(ev.pointerId);
794
+ * }
795
+ */
796
+
797
+ setCapture( pointerId: number ) {
798
+ this.dom.setPointerCapture( pointerId );
799
+ }
800
+
801
+ /**
802
+ * Releases pointer capture on the component's DOM element for a specific pointer.
803
+ * @param pointerId - The unique ID of the pointer.
804
+ */
805
+
806
+ releaseCapture( pointerId: number ) {
807
+ this.dom.releasePointerCapture( pointerId );
808
+ }
809
+
810
+ /**
811
+ * Returns the size and position of the component's DOM element relative to the viewport.
812
+ * @returns A `Rect` object containing the bounding rectangle.
813
+ */
814
+
815
+ getBoundingRect( ): Rect {
816
+ const rc = this.dom.getBoundingClientRect( );
817
+ return new Rect( rc.x, rc.y, rc.width, rc.height );
818
+ }
819
+
820
+ // :: MISC ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
821
+
822
+ /**
823
+ * Gives focus to the component's DOM element.
824
+ * @returns The component instance for chaining.
825
+ */
826
+
827
+ focus( ): this {
828
+ (this.dom as HTMLElement).focus( );
829
+ return this;
830
+ }
831
+
832
+ /**
833
+ * Checks if the component's DOM element currently has focus.
834
+ * @returns `true` if the component is focused, `false` otherwise.
835
+ */
836
+
837
+ hasFocus( ) {
838
+ return document.activeElement==this.dom;
839
+ }
840
+
841
+ /**
842
+ * Scrolls the component's DOM element into the visible area of the browser window.
843
+ * @param arg - Optional. A boolean (`true` for smooth scroll) or an object specifying scroll options.
844
+ */
845
+
846
+ scrollIntoView(arg?: boolean | ScrollIntoViewOptions) {
847
+ this.dom.scrollIntoView(arg);
848
+ }
849
+
850
+ /**
851
+ * Checks if the component's DOM element is currently visible (i.e., not hidden by `display: none`).
852
+ * @returns `true` if the component is visible, `false` otherwise.
853
+ */
854
+
855
+ isVisible( ) {
856
+ return (this.dom as HTMLElement).offsetParent !== null;
857
+ }
858
+
859
+ /**
860
+ * Shows or hides the component.
861
+ * It toggles the `x4hidden` CSS class.
862
+ * @param vis - If `true`, the component is shown; if `false`, it's hidden. Defaults to `true`.
863
+ * @returns The component instance for chaining.
864
+ */
865
+
866
+ show( vis = true ): this {
867
+ this.setClass( 'x4hidden', !vis );
868
+ return this;
869
+ }
870
+
871
+ /**
872
+ * Hides the component by applying the `x4hidden` CSS class.
873
+ * @returns The component instance for chaining.
874
+ */
875
+
876
+ hide( ): this {
877
+ this.show( false );
878
+ return this;
879
+ }
880
+
881
+ /**
882
+ * Enables or disables the component.
883
+ * This sets the `disabled` attribute and also propagates the disabled state to child input elements.
884
+ * @param ena - If `true`, the component is enabled; if `false`, it's disabled. Defaults to `true`.
885
+ * @returns The component instance for chaining.
886
+ */
887
+
888
+ enable( ena = true ): this {
889
+ this.setAttribute( "disabled", !ena ? 'true' : null );
890
+
891
+ if( this.dom instanceof HTMLInputElement || this.dom instanceof HTMLButtonElement ) {
892
+ this.dom.disabled = !ena;
893
+ }
894
+
895
+ // propagate diable state to all input children
896
+ const nodes = this.enumChildNodes( true );
897
+ nodes.forEach( x => {
898
+ if( x instanceof HTMLInputElement || x instanceof HTMLButtonElement ) {
899
+ x.disabled = !ena;
900
+ }
901
+ });
902
+
903
+ return this;
904
+ }
905
+
906
+ /**
907
+ * Disables the component.
908
+ * @returns The component instance for chaining.
909
+ */
910
+
911
+ disable( ): this {
912
+ this.enable( false );
913
+ return this;
914
+ }
915
+
916
+ /**
917
+ * Checks if the component is marked as disabled.
918
+ * This checks for the presence of the `disabled` attribute.
919
+ * @returns The string value of the `disabled` attribute, or `null` if not present.
920
+ */
921
+
922
+ isDisabled( ) {
923
+ return this.getAttribute('disabled');
924
+ }
925
+
926
+ /**
927
+ * Returns the next sibling element as a Component instance.
928
+ * @returns The next sibling component, or `null` if none exists.
929
+ */
930
+
931
+ nextElement<T extends Component = Component>( ): T {
932
+ const nxt = this.dom.nextElementSibling;
933
+ return componentFromDOM<T>( nxt );
934
+ }
935
+
936
+ /**
937
+ * Returns the previous sibling element as a Component instance.
938
+ * @returns The previous sibling component, or `null` if none exists.
939
+ */
940
+
941
+ prevElement<T extends Component = Component>( ): T {
942
+ const nxt = this.dom.previousElementSibling;
943
+ return componentFromDOM<T>( nxt );
944
+ }
945
+
946
+ /**
947
+ * Searches up the DOM tree for a parent element that is a Component and optionally matches a specific constructor.
948
+ * @param cls - Optional. The constructor of the Component type to match.
949
+ * @returns The matching parent Component instance, or `null` if not found.
950
+ */
951
+
952
+ parentElement<T extends Component>( cls?: Constructor<T> ): T {
953
+ return Component.parentElement<T>( this.dom, cls );
954
+ }
955
+
956
+ /**
957
+ *
958
+ */
959
+
960
+ childCount( ) {
961
+ return this.dom.childElementCount;
962
+ }
963
+
964
+ /**
965
+ * Static method to search up the DOM tree for a parent element that is a Component and optionally matches a specific constructor.
966
+ * @param dom - The starting DOM node from which to search upwards.
967
+ * @param cls - Optional. The constructor of the Component type to match.
968
+ * @returns The matching parent Component instance, or `null` if not found.
969
+ */
970
+
971
+
972
+ static parentElement<T extends Component>( dom: Node, cls?: Constructor<T> ): T {
973
+
974
+ while( dom.parentElement ) {
975
+ const cp = componentFromDOM( dom.parentElement );
976
+ if( !cls ) {
977
+ return cp as T;
978
+ }
979
+
980
+ if( cp && cp instanceof cls ) {
981
+ return cp;
982
+ }
983
+
984
+ dom = dom.parentElement;
985
+ }
986
+
987
+ return null;
988
+ }
989
+
990
+ /**
991
+ * Returns the first child element as a Component instance.
992
+ * @returns The first child component, or `null` if none exists.
993
+ */
994
+
995
+ firstChild<T extends Component = Component>( ) : T {
996
+ const nxt = this.dom.firstElementChild;
997
+ return componentFromDOM<T>( nxt );
998
+ }
999
+
1000
+ /**
1001
+ * Returns the last child element as a Component instance.
1002
+ * @returns The last child component, or `null` if none exists.
1003
+ */
1004
+
1005
+ lastChild<T extends Component = Component>( ) : T {
1006
+ const nxt = this.dom.lastElementChild;
1007
+ return componentFromDOM( nxt );
1008
+ }
1009
+
1010
+ /**
1011
+ * Enumerates all child components of this component.
1012
+ * @param recursive - If `true`, searches all descendants; otherwise, only direct children.
1013
+ * @returns An array of child Component instances.
1014
+ */
1015
+
1016
+ enumChildComponents( recursive: boolean ) {
1017
+
1018
+ const children: Component[] = [];
1019
+
1020
+ const nodes = this.enumChildNodes( recursive );
1021
+ nodes.forEach( ( c: Node ) => {
1022
+ const cc = componentFromDOM( c as HTMLElement );
1023
+ if( cc ) {
1024
+ children.push(cc);
1025
+ }
1026
+ } );
1027
+
1028
+ return children;
1029
+ }
1030
+
1031
+ /**
1032
+ * Enumerates all child DOM nodes of this component.
1033
+ * Not all nodes may be components.
1034
+ * @param recursive - If `true`, searches all descendant nodes; otherwise, only direct children.
1035
+ * @returns An array of child DOM nodes.
1036
+ */
1037
+
1038
+ enumChildNodes( recursive: boolean ) {
1039
+ const children: Node[] = Array.from( recursive ? this.dom.querySelectorAll( '*' ) : this.dom.children );
1040
+ return children;
1041
+ }
1042
+
1043
+ /**
1044
+ * Visits all descendant components of this component, executing a callback function for each.
1045
+ * The traversal stops if the callback returns `true`.
1046
+ * @param cb - The callback function to execute for each component.
1047
+ */
1048
+
1049
+ visitChildren( cb: ( el: Component ) => boolean ) {
1050
+
1051
+ const visit = ( p: Element ) => {
1052
+ for( let d=p.firstElementChild; d; d=d.nextElementSibling ) {
1053
+ const comp = componentFromDOM( d as Element );
1054
+ if( comp ) {
1055
+ if( cb( comp ) ) {
1056
+ return true;
1057
+ }
1058
+ }
1059
+
1060
+ // avoid visit of svg elements
1061
+ if( d.firstElementChild && d.tagName!="svg" && d.tagName!="SVG" ) {
1062
+ if( visit( d ) ) {
1063
+ return true;
1064
+ }
1065
+ }
1066
+ }
1067
+ }
1068
+
1069
+ visit( this.dom );
1070
+ }
1071
+
1072
+ /**
1073
+ * Animates the component's DOM element using the Web Animations API.
1074
+ * @param keyframes - An array of keyframe objects or a `Keyframe` object.
1075
+ * @param duration - The duration of the animation in milliseconds, or a `KeyframeAnimationOptions` object.
1076
+ */
1077
+
1078
+ animate( keyframes: Keyframe[], duration: number ) {
1079
+ this.dom.animate(keyframes,duration);
1080
+ }
1081
+
1082
+
1083
+ // :: TSX/REACT ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
1084
+
1085
+ /**
1086
+ * Creates a new Component or an array of Components from JSX elements.
1087
+ * This method is typically called by the TypeScript/JavaScript compiler when JSX is transpiled.
1088
+ * @param clsOrTag - The class constructor of the component, an HTML tag name string, a symbol for fragments, or a callback.
1089
+ * @param attrs - An object containing attributes and properties for the component.
1090
+ * @param children - Any child components or content passed within the JSX.
1091
+ * @returns A Component instance or an array of Components.
1092
+ */
1093
+
1094
+ static createElement( clsOrTag: string | ComponentConstructor | symbol | CreateComponentCallBack, attrs: any, ...children: Component[] ): Component | Component[] {
1095
+
1096
+ let comp: Component;
1097
+
1098
+ // fragment
1099
+ if( clsOrTag==this.createFragment || clsOrTag===FRAGMENT ) {
1100
+ return children;
1101
+ }
1102
+
1103
+ // class constructor, yes : dirty
1104
+ if( clsOrTag instanceof Function ) {
1105
+ attrs = attrs ?? {};
1106
+ if( !attrs.children && children && children.length ) {
1107
+ attrs.content = children;
1108
+ }
1109
+
1110
+ comp = new (clsOrTag as any)( attrs ?? {} );
1111
+ }
1112
+ // basic tag
1113
+ else {
1114
+ comp = new Component( {
1115
+ tag: clsOrTag,
1116
+ content: children,
1117
+ ...attrs,
1118
+ });
1119
+ }
1120
+
1121
+ if( children && children.length ) {
1122
+ //comp.setContent( children );
1123
+ }
1124
+
1125
+ return comp;
1126
+ }
1127
+
1128
+ /**
1129
+ * Creates a fragment, which is an array of components without a parent DOM element.
1130
+ * Used for grouping multiple children in JSX without introducing an extra DOM node.
1131
+ * @returns An array of components.
1132
+ */
1133
+
1134
+ static createFragment( ): Component[] {
1135
+ return this.createElement( FRAGMENT, null ) as Component[];
1136
+ }
1137
+
1138
+ // :: SPECIALS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
1139
+
1140
+ /**
1141
+ * Queries for a specific system or application-defined interface on the component.
1142
+ * Common system interfaces include "form-element" and "tab-handler".
1143
+ * @param name - The name of the interface to query.
1144
+ * @returns An object conforming to the requested interface, or `null` if not supported.
1145
+ *
1146
+ * system interfaces:
1147
+ * "form-element"
1148
+ * "tab-handler"
1149
+ */
1150
+
1151
+ queryInterface<T>( name: string ): T {
1152
+ return null;
1153
+ }
1154
+ }
1155
+
1156
+
1157
+ /**
1158
+ * Type definition for a constructor that creates a Component instance.
1159
+ */
1160
+
1161
+ type ComponentConstructor = {
1162
+ new(...params: any[]): Component;
1163
+ };
1164
+
1165
+ /**
1166
+ * Retrieves a Component instance associated with a given DOM element.
1167
+ * Components created by this library internally store a reference to their instance on their `dom` property.
1168
+ * @param node - The DOM element to check.
1169
+ * @returns The Component instance if found, otherwise `null`.
1170
+ */
1171
+
1172
+ export function componentFromDOM<T extends Component = Component>( node: Element ) {
1173
+ return node ? (node as any)[COMPONENT] as T : null;
1174
+ }
1175
+
1176
+ /**
1177
+ * Wraps an existing HTMLElement with a new Component instance.
1178
+ * If the HTMLElement already has an associated Component, that instance is returned.
1179
+ * Otherwise, a new `Component` is created to manage the existing DOM element.
1180
+ * @param el - The HTMLElement to wrap.
1181
+ * @returns A Component instance managing the provided HTMLElement.
1182
+ */
1183
+
1184
+ export function wrapDOM( el: HTMLElement ): Component {
1185
+ const com = componentFromDOM(el);
1186
+ if( com ) {
1187
+ return com;
1188
+ }
1189
+
1190
+ return new Component( { existingDOM: el } );
1191
+ }
1192
+
1193
+
1194
+ // :: Special components ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
1195
+
1196
+ /**
1197
+ * A basic component class that provides flexible sizing, typically used in flex layouts.
1198
+ * Automatically generates CSS class: `x4flex`.
1199
+ */
1200
+
1201
+ export class Flex extends Component {
1202
+ constructor( ) {
1203
+ super({})
1204
+ }
1205
+ }
1206
+
1207
+ /**
1208
+ * A simple spacer component used for creating empty space, often in flex containers.
1209
+ * @example
1210
+ * ```ts
1211
+ * new Space(); // default spacer
1212
+ * new Space(10); // 10px wide spacer
1213
+ * new Space("1em", "my-spacer-class");
1214
+ * ```
1215
+ */
1216
+
1217
+ export class Space extends Component {
1218
+ constructor( width?: number|string, cls?: string ) {
1219
+ super( { width, cls } )
1220
+ }
1221
+ }
1222
+
1223
+
1224
+ // :: HIGH LEVEL BASIC EVENTS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
1225
+
1226
+
1227
+
1228
+ /**
1229
+ * Click Event
1230
+ * click event do not have any additional parameters
1231
+ */
1232
+
1233
+ export interface EvClick extends ComponentEvent {
1234
+ repeat?: number;
1235
+ }
1236
+
1237
+ /**
1238
+ * Change Event
1239
+ * value is the the element value
1240
+ */
1241
+
1242
+ export interface EvChange extends ComponentEvent {
1243
+ readonly value: any;
1244
+ }
1245
+
1246
+ /**
1247
+ * Focus event
1248
+ */
1249
+
1250
+ export interface EvFocus extends ComponentEvent {
1251
+ readonly focus_out: boolean;
1252
+ }
1253
+
1254
+ /**
1255
+ * Selection Event
1256
+ * value is the new selection or null
1257
+ */
1258
+
1259
+ type ISelection = number | string | any;
1260
+
1261
+ export interface EvSelectionChange extends ComponentEvent {
1262
+ readonly selection: ISelection[];
1263
+ readonly empty: boolean;
1264
+ }
1265
+
1266
+
1267
+ /**
1268
+ * ContextMenu Event
1269
+ */
1270
+
1271
+ export interface EvContextMenu extends ComponentEvent {
1272
+ uievent: MouseEvent; // UI event that fire this event
1273
+ }
1274
+
1275
+ /**
1276
+ * Drag/Drop event
1277
+ */
1278
+
1279
+ export interface EvDrag extends ComponentEvent {
1280
+ element: unknown;
1281
+ data: any;
1282
+ }
1283
+
1284
+ /**
1285
+ * Errors
1286
+ */
1287
+
1288
+ export interface EvError extends ComponentEvent {
1289
+ code: number;
1290
+ message: string;
1291
+ }
1292
+
1293
+ /**
1294
+ * DblClick Event
1295
+ */
1296
+
1297
+ export interface EvDblClick extends ComponentEvent {
1298
+ }
1299
+