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.
- package/README.md +21 -21
- package/package.json +39 -26
- package/src/components/base.scss +25 -89
- package/src/components/boxes/boxes.module.scss +54 -54
- package/src/components/boxes/boxes.ts +513 -513
- package/src/components/breadcrumb/breadcrumb.scss +56 -56
- package/src/components/breadcrumb/breadcrumb.ts +93 -93
- package/src/components/btngroup/btngroup.module.scss +40 -40
- package/src/components/btngroup/btngroup.ts +152 -152
- package/src/components/button/button.module.scss +172 -172
- package/src/components/button/button.ts +232 -232
- package/src/components/calendar/calendar.module.scss +162 -162
- package/src/components/calendar/calendar.ts +326 -326
- package/src/components/canvas/canvas.module.scss +24 -24
- package/src/components/canvas/canvas.ts +195 -195
- package/src/components/canvas/canvas_ex.ts +275 -275
- package/src/components/checkbox/check.svg +3 -3
- package/src/components/checkbox/checkbox.module.scss +141 -141
- package/src/components/checkbox/checkbox.ts +139 -139
- package/src/components/colorinput/colorinput.module.scss +64 -64
- package/src/components/colorinput/colorinput.ts +90 -90
- package/src/components/colorpicker/colorpicker.module.scss +132 -132
- package/src/components/colorpicker/colorpicker.ts +481 -481
- package/src/components/combobox/combobox.module.scss +145 -145
- package/src/components/combobox/combobox.ts +282 -282
- package/src/components/combobox/updown.svg +3 -3
- package/src/components/components.ts +45 -44
- package/src/components/dialog/dialog.module.scss +103 -105
- package/src/components/dialog/dialog.ts +233 -233
- package/src/components/filedrop/filedrop.module.scss +69 -69
- package/src/components/filedrop/filedrop.ts +130 -130
- package/src/components/form/form.module.scss +38 -38
- package/src/components/form/form.ts +172 -172
- package/src/components/gridview/gridview.module.scss +323 -337
- package/src/components/gridview/gridview.ts +1276 -1315
- package/src/components/header/header.module.scss +40 -40
- package/src/components/header/header.ts +141 -141
- package/src/components/icon/icon.module.scss +32 -32
- package/src/components/icon/icon.ts +165 -165
- package/src/components/image/image.module.scss +27 -27
- package/src/components/image/image.ts +168 -168
- package/src/components/input/input.module.scss +74 -74
- package/src/components/input/input.ts +537 -537
- package/src/components/keyboard/keyboard.module.scss +136 -136
- package/src/components/keyboard/keyboard.ts +549 -549
- package/src/components/label/label.module.scss +90 -91
- package/src/components/label/label.ts +101 -101
- package/src/components/link/link.module.scss +44 -44
- package/src/components/link/link.ts +87 -87
- package/src/components/listbox/listbox.module.scss +179 -179
- package/src/components/listbox/listbox.ts +596 -596
- package/src/components/menu/menu.module.scss +128 -128
- package/src/components/menu/menu.ts +174 -174
- package/src/components/messages/messages.module.scss +92 -146
- package/src/components/messages/messages.ts +237 -303
- package/src/components/normalize.scss +391 -391
- package/src/components/notification/notification.module.scss +83 -83
- package/src/components/notification/notification.ts +107 -107
- package/src/components/panel/panel.module.scss +66 -71
- package/src/components/panel/panel.ts +57 -57
- package/src/components/popup/popup.module.scss +51 -51
- package/src/components/popup/popup.ts +457 -457
- package/src/components/progress/progress.module.scss +56 -56
- package/src/components/progress/progress.ts +43 -43
- package/src/components/propgrid/progrid.module.scss +111 -111
- package/src/components/propgrid/propgrid.ts +300 -300
- package/src/components/propgrid/updown.svg +3 -3
- package/src/components/radio/radio.module.scss +163 -163
- package/src/components/radio/radio.svg +3 -3
- package/src/components/radio/radio.ts +141 -141
- package/src/components/rating/rating.module.scss +22 -22
- package/src/components/rating/rating.ts +131 -131
- package/src/components/select/select.module.scss +8 -8
- package/src/components/select/select.ts +134 -134
- package/src/components/shared.scss +141 -71
- package/src/components/sizers/sizer.module.scss +90 -107
- package/src/components/sizers/sizer.ts +131 -134
- package/src/components/slider/slider.module.scss +117 -117
- package/src/components/slider/slider.ts +197 -197
- package/src/components/spreadsheet/spreadsheet.module.scss +307 -307
- package/src/components/spreadsheet/spreadsheet.ts +1223 -1223
- package/src/components/switch/switch.module.scss +126 -126
- package/src/components/switch/switch.ts +61 -61
- package/src/components/tabs/tabs.module.scss +46 -67
- package/src/components/tabs/tabs.ts +229 -234
- package/src/components/textarea/textarea.module.scss +63 -63
- package/src/components/textarea/textarea.ts +131 -131
- package/src/components/textedit/textedit.module.scss +115 -115
- package/src/components/textedit/textedit.ts +122 -122
- package/src/components/themes.scss +90 -90
- package/src/components/tickline/tickline.module.scss +25 -25
- package/src/components/tickline/tickline.ts +81 -81
- package/src/components/tooltips/tooltips.scss +71 -71
- package/src/components/tooltips/tooltips.ts +120 -120
- package/src/components/treeview/treeview.module.scss +192 -192
- package/src/components/treeview/treeview.ts +484 -484
- package/src/components/viewport/viewport.module.scss +31 -31
- package/src/components/viewport/viewport.ts +41 -41
- package/src/core/component.ts +1299 -1299
- package/src/core/core_application.ts +361 -361
- package/src/core/core_colors.ts +512 -512
- package/src/core/core_data.ts +1297 -1297
- package/src/core/core_dom.ts +481 -481
- package/src/core/core_dragdrop.ts +225 -225
- package/src/core/core_element.ts +221 -221
- package/src/core/core_events.ts +214 -214
- package/src/core/core_i18n.ts +395 -395
- package/src/core/core_pdf.ts +454 -454
- package/src/core/core_react.ts +78 -78
- package/src/core/core_router.ts +296 -296
- package/src/core/core_state.ts +62 -62
- package/src/core/core_styles.ts +213 -213
- package/src/core/core_svg.ts +1042 -1042
- package/src/core/core_tools.ts +996 -996
- package/src/types/scss.d.ts +4 -4
- package/src/types/x4react.d.ts +8 -8
- package/src/x4.scss +19 -19
- package/src/x4.ts +36 -36
- package/src/x4tsx.d.ts +26 -26
- package/.vscode/launch.json +0 -14
- package/.vscode/settings.json +0 -2
- package/demo/assets/house-light.svg +0 -1
- package/demo/assets/radio.svg +0 -4
- package/demo/index.html +0 -12
- package/demo/main.scss +0 -23
- package/demo/main.ts +0 -324
- package/demo/package.json +0 -26
- package/demo/scss.d.ts +0 -4
- package/demo/svg.d.ts +0 -1
- package/demo/tsconfig.json +0 -14
- package/src/components/gridview/folder-open.svg +0 -1
- package/src/components/messages/spinner.svg +0 -1
- package/src/x4.d.ts +0 -10
- package/tsconfig.json +0 -11
package/src/core/component.ts
CHANGED
|
@@ -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
|
+
|