x4js 2.0.28 → 2.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/.vscode/launch.json +14 -0
  2. package/.vscode/settings.json +2 -0
  3. package/ai-comments.txt +97 -0
  4. package/demo/assets/house-light.svg +1 -0
  5. package/demo/assets/radio.svg +4 -0
  6. package/demo/index.html +12 -0
  7. package/demo/main.scss +23 -0
  8. package/demo/main.ts +324 -0
  9. package/demo/package.json +26 -0
  10. package/demo/scss.d.ts +4 -0
  11. package/demo/svg.d.ts +1 -0
  12. package/demo/tsconfig.json +14 -0
  13. package/lib/types/x4js.d.ts +0 -2374
  14. package/package.json +23 -47
  15. package/src/colors.scss +246 -0
  16. package/src/components/boxes/boxes.module.scss +1 -1
  17. package/src/components/boxes/boxes.ts +139 -28
  18. package/src/components/button/button.ts +76 -29
  19. package/src/components/combobox/combobox.ts +1 -1
  20. package/src/components/dialog/dialog.ts +4 -0
  21. package/src/components/gridview/gridview.ts +104 -6
  22. package/src/components/icon/icon.ts +42 -14
  23. package/src/components/input/input.ts +146 -74
  24. package/src/components/keyboard/keyboard.module.scss +1 -1
  25. package/src/components/keyboard/keyboard.ts +31 -9
  26. package/src/components/label/label.module.scss +9 -0
  27. package/src/components/label/label.ts +10 -6
  28. package/src/components/link/link.module.scss +44 -0
  29. package/src/components/link/link.ts +7 -1
  30. package/src/components/listbox/listbox.module.scss +18 -4
  31. package/src/components/listbox/listbox.ts +32 -12
  32. package/src/components/menu/menu.module.scss +14 -2
  33. package/src/components/menu/menu.ts +1 -1
  34. package/src/components/messages/messages.ts +13 -5
  35. package/src/components/panel/panel.module.scss +7 -0
  36. package/src/components/popup/popup.ts +14 -10
  37. package/src/components/propgrid/propgrid.ts +1 -1
  38. package/src/components/shared.scss +4 -0
  39. package/src/components/spreadsheet/spreadsheet.ts +81 -34
  40. package/src/components/tabs/tabs.module.scss +1 -0
  41. package/src/components/textarea/textarea.ts +8 -2
  42. package/src/components/textedit/textedit.ts +7 -0
  43. package/src/components/themes.scss +2 -0
  44. package/src/components/tooltips/tooltips.ts +15 -3
  45. package/src/core/component.ts +358 -162
  46. package/src/core/core_application.ts +129 -32
  47. package/src/core/core_colors.ts +382 -119
  48. package/src/core/core_data.ts +73 -86
  49. package/src/core/core_dom.ts +10 -0
  50. package/src/core/core_dragdrop.ts +32 -7
  51. package/src/core/core_element.ts +111 -4
  52. package/src/core/core_events.ts +48 -11
  53. package/src/core/core_i18n.ts +2 -0
  54. package/src/core/core_pdf.ts +454 -0
  55. package/src/core/core_router.ts +64 -5
  56. package/src/core/core_state.ts +1 -0
  57. package/src/core/core_styles.ts +11 -12
  58. package/src/core/core_svg.ts +346 -51
  59. package/src/core/core_tools.ts +105 -17
  60. package/src/x4.d.ts +10 -0
  61. package/src/x4.ts +1 -0
  62. package/src/x4tsx.d.ts +2 -1
  63. package/tsconfig.json +11 -0
  64. package/lib/README.txt +0 -20
  65. package/lib/cjs/x4.css +0 -1
  66. package/lib/cjs/x4.js +0 -2
  67. package/lib/esm/x4.css +0 -1
  68. package/lib/esm/x4.mjs +0 -2
  69. package/lib/styles/x4.css +0 -1
@@ -20,75 +20,156 @@ import { class_ns, formatIntlDate, IComponentInterface, IFormElement, isString }
20
20
 
21
21
  import "./input.module.scss"
22
22
 
23
+
24
+ function getRadioOwner( el: Element ) {
25
+
26
+ while( el!=document.body ) {
27
+ const comp = componentFromDOM(el);
28
+ const ifx = comp.queryInterface( "tab-handler");
29
+ if( ifx ) {
30
+ return el;
31
+ }
32
+
33
+ el = el.parentElement;
34
+ }
35
+
36
+ return document;
37
+ }
38
+
39
+
40
+ /**
41
+ * Base properties for all input types.
42
+ */
43
+
23
44
  export interface BaseProps extends ComponentProps {
45
+ /**
46
+ * Input field name.
47
+ * required if you want to use form getValues/setValues
48
+ */
49
+
24
50
  name?: string;
51
+ /** Automatically focus the input on page load. */
25
52
  autofocus?: boolean;
53
+ /** Marks the input as required. */
26
54
  required?: boolean;
55
+ /** Makes the input read-only. */
27
56
  readonly?: boolean;
28
- placeholder?: string;
29
-
30
- focus?: EventCallback<EvFocus>;
31
- change?: EventCallback<EvChange>;
57
+ /** Placeholder text displayed when empty. */
58
+ placeholder?: string;
59
+ /** Fired when the input receives/loses focus. */
60
+ focus?: EventCallback<EvFocus>;
61
+ /** Fired when the input value changes. */
62
+ change?: EventCallback<EvChange>;
32
63
  }
33
64
 
65
+ /**
66
+ * Checkbox-specific input properties.
67
+ */
68
+
34
69
  interface CheckboxProps extends BaseProps {
35
70
  type: "checkbox";
71
+ /** Checkbox value (submitted when checked). */
36
72
  value?: boolean | number | string;
73
+ /** Initial checked state. */
37
74
  checked?: boolean;
38
75
  }
39
76
 
77
+ /**
78
+ * Radio button-specific input properties.
79
+ */
80
+
40
81
  interface RadioProps extends BaseProps {
41
82
  type: "radio";
83
+ /** Radio value (submitted when selected). */
42
84
  value?: boolean | number | string;
85
+ /** Initial checked state. */
43
86
  checked?: boolean;
44
87
  }
45
88
 
89
+ /**
90
+ * Range slider input properties.
91
+ */
92
+
46
93
  export interface RangeProps extends BaseProps {
47
94
  type: "range";
95
+ /** Current slider value. */
48
96
  value?: number;
97
+ /** Minimum allowed value. */
49
98
  min: number;
99
+ /** Maximum allowed value. */
50
100
  max: number;
101
+ /** Step increment. */
51
102
  step?: number;
52
103
  }
53
104
 
105
+ /**
106
+ * File upload input properties.
107
+ */
54
108
  export interface FileProps extends BaseProps {
55
109
  type: "file";
110
+ /** Allowed file types (e.g., `"image/*"` or `[".pdf", ".doc"]`). */
56
111
  accept: string | string[];
57
112
  value?: never;
58
113
  }
59
114
 
60
-
115
+ /**
116
+ * Date picker input properties.
117
+ */
61
118
  export interface DateProps extends BaseProps {
62
119
  type: "date";
63
- value?: Date | string;
120
+ /** Current date value (Date object or ISO string). */
121
+ value?: Date | string;
64
122
  }
65
123
 
124
+ /**
125
+ * Time picker input properties.
126
+ */
127
+
66
128
  export interface TimeProps extends BaseProps {
67
129
  type: "time";
68
130
  readonly?: boolean;
69
131
  required?: boolean;
70
- value?: string;
132
+ /** Current time value (e.g., `"12:30"`). */
133
+ value?: string;
71
134
  }
72
135
 
136
+ /**
137
+ * Numeric input properties.
138
+ */
139
+
73
140
  export interface NumberProps extends BaseProps {
74
141
  type: "number";
75
142
  readonly?: boolean;
76
143
  required?: boolean;
77
- value?: number | string;
78
- min?: number;
79
- max?: number;
80
- step?: number;
144
+ /** Current numeric value. */
145
+ value?: number | string;
146
+ /** Minimum allowed value. */
147
+ min?: number;
148
+ /** Maximum allowed value. */
149
+ max?: number;
150
+ /** Step increment. */
151
+ step?: number;
81
152
  }
82
153
 
154
+ /**
155
+ * Text/email/password input properties.
156
+ */
157
+
83
158
  export interface TextInputProps extends BaseProps {
84
159
  type?: "text" | "email" | "password";
85
160
  readonly?: boolean;
86
161
  required?: boolean;
162
+ /** Regex pattern for validation. */
87
163
  pattern?: string;
164
+ /** Input value. */
88
165
  value?: string | number;
166
+ /** Enables/disables spellcheck. */
89
167
  spellcheck?: boolean;
168
+ /** Minimum input length. */
90
169
  minlength?: number;
170
+ /** Maximum input length. */
91
171
  maxlength?: number;
172
+ trim?: boolean;
92
173
  }
93
174
 
94
175
 
@@ -102,7 +183,21 @@ interface InputEvents extends ComponentEvent {
102
183
 
103
184
 
104
185
  /**
105
- *
186
+ * Customizable input component supporting multiple types (text, number, date, etc.).
187
+ * Auto-generates CSS class: `x4input`.
188
+ *
189
+ * @example
190
+ * ```ts
191
+ * // Text input
192
+ * const nameInput = new Input({ type: "text", placeholder: "Enter name" });
193
+ *
194
+ * // Checkbox
195
+ * const agreeCheckbox = new Input({
196
+ * type: "checkbox",
197
+ * checked: true,
198
+ * change: (e) => console.log("Checked:", e.value)
199
+ * });
200
+ * ```
106
201
  */
107
202
 
108
203
  @class_ns( "x4" )
@@ -262,27 +357,31 @@ export class Input extends Component<InputProps,InputEvents> {
262
357
  }
263
358
  }
264
359
 
265
- /**
266
- * @returns
267
- */
268
-
360
+ /** Gets the current input value as a string. */
361
+
269
362
  public getValue( ) {
270
- return (this.dom as HTMLInputElement).value;
363
+ let v = (this.dom as HTMLInputElement).value;
364
+ if( (this.props as any).trim!==false ) {
365
+ v = v.trim( );
366
+ }
367
+
368
+ return v;
271
369
  }
272
370
 
273
371
  /**
274
- *
275
- * @param value
276
- */
372
+ * Sets the input value.
373
+ * @param value - New value (converted to string).
374
+ */
277
375
 
278
376
  public setValue( value: string ) {
279
377
  (this.dom as HTMLInputElement).value = value+"";
280
378
  }
281
379
 
282
380
  /**
283
- *
284
- * @returns
285
- */
381
+ * Gets the numeric value (for `type="number"` or `type="range"`).
382
+ * @param defNan - Default value if parsing fails (default: `NaN`).
383
+ * @returns Parsed number or `defNan`.
384
+ */
286
385
 
287
386
  public getNumValue( defNan?: number ) {
288
387
  const v = parseFloat( this.getValue() );
@@ -293,11 +392,13 @@ export class Input extends Component<InputProps,InputEvents> {
293
392
  }
294
393
 
295
394
  /**
296
- *
297
- * @param value
298
- * @param ndec number of decimals or -1 for auto, -2 as prop.step
299
- *
300
- */
395
+ * Sets a numeric value with optional decimal precision.
396
+ * @param value - Numeric value to set.
397
+ * @param ndec - Decimal places:
398
+ * `-1` = auto,
399
+ * `-2` = use `step` prop,
400
+ * `≥0` = fixed decimals.
401
+ */
301
402
 
302
403
  public setNumValue( value: number, ndec = -1 ) {
303
404
 
@@ -319,48 +420,38 @@ export class Input extends Component<InputProps,InputEvents> {
319
420
  this.setValue( value+"" );
320
421
  }
321
422
 
322
- /**
323
- * @return the checked value
324
- */
325
-
326
- public getCheck() {
423
+ /** Gets the checked state (for checkboxes/radio buttons). */
424
+ public getCheck() {
327
425
  const d = this.dom as HTMLInputElement;
328
426
  return d.checked;
329
427
  }
330
428
 
331
- /**
332
- * change the checked value
333
- * @param {boolean} ck new checked value
334
- */
335
-
429
+ /** Sets the checked state (for checkboxes/radio buttons). */
430
+
336
431
  public setCheck(ck: boolean) {
337
432
  const d = this.dom as HTMLInputElement;
338
433
  d.checked = ck;
339
434
  }
340
435
 
341
- /**
342
- *
343
- */
344
-
436
+ /** Toggles read-only mode. */
437
+
345
438
  public setReadOnly( ro: boolean ) {
346
439
  const d = this.dom as HTMLInputElement;
347
440
  d.readOnly = ro;
348
441
  }
349
442
 
350
- /**
351
- * select all the text
352
- */
353
-
443
+ /** Selects all text in the input. */
444
+
354
445
  public selectAll( ) {
355
446
  const d = this.dom as HTMLInputElement;
356
447
  d.select();
357
448
  }
358
449
 
359
450
  /**
360
- * select a part of the text
361
- * @param start
362
- * @param length
363
- */
451
+ * Selects a text range.
452
+ * @param start - Start position
453
+ * @param length - Length of selection
454
+ */
364
455
 
365
456
  public select( start: number, length: number = 9999 ) : void {
366
457
  const d = this.dom as HTMLInputElement;
@@ -368,9 +459,9 @@ export class Input extends Component<InputProps,InputEvents> {
368
459
  }
369
460
 
370
461
  /**
371
- * get the selection as { start, length }
372
- */
373
-
462
+ * Gets the current text selection.
463
+ * @returns Object with `start` and `length` properties.
464
+ */
374
465
  public getSelection( ) {
375
466
  const d = this.dom as HTMLInputElement;
376
467
 
@@ -380,10 +471,8 @@ export class Input extends Component<InputProps,InputEvents> {
380
471
  };
381
472
  }
382
473
 
383
- /**
384
- *
385
- */
386
-
474
+ /** Validates the input (checks `required` constraint). */
475
+
387
476
  public isValid( ) {
388
477
 
389
478
  if( (this.props as any).required ) {
@@ -446,20 +535,3 @@ export class Input extends Component<InputProps,InputEvents> {
446
535
  }
447
536
  }
448
537
 
449
-
450
- function getRadioOwner( el: Element ) {
451
-
452
- while( el!=document.body ) {
453
- const comp = componentFromDOM(el);
454
- const ifx = comp.queryInterface( "tab-handler");
455
- if( ifx ) {
456
- return el;
457
- }
458
-
459
- el = el.parentElement;
460
- }
461
-
462
- return document;
463
- }
464
-
465
-
@@ -23,7 +23,7 @@
23
23
  .x4keyboard {
24
24
  z-index: 2000;
25
25
  position: absolute !important;
26
- width: 100vw;
26
+ width: 100%;
27
27
  bottom: 0;
28
28
  justify-content: center;
29
29
 
@@ -121,14 +121,14 @@ export class Keyboard extends HBox<KeyboardProps>
121
121
  this.hide( );
122
122
 
123
123
  this.addDOMEvent( "mousedown", (e) => {
124
- this.handleKey( e );
124
+ this.handleKeyEvent( e );
125
125
  e.preventDefault( );
126
126
  e.stopPropagation( );
127
127
  });
128
128
 
129
129
  // for rapid people
130
130
  this.addDOMEvent( "dblclick", (e) => {
131
- this.handleKey( e );
131
+ this.handleKeyEvent( e );
132
132
  e.preventDefault( );
133
133
  e.stopPropagation( );
134
134
  });
@@ -148,7 +148,7 @@ export class Keyboard extends HBox<KeyboardProps>
148
148
  *
149
149
  */
150
150
 
151
- private handleKey( e: UIEvent ) {
151
+ private handleKeyEvent( e: UIEvent ) {
152
152
  let target = e.target as HTMLElement;
153
153
  let key;
154
154
 
@@ -165,6 +165,10 @@ export class Keyboard extends HBox<KeyboardProps>
165
165
  return;
166
166
  }
167
167
 
168
+ this._handleKey( key );
169
+ }
170
+
171
+ private _handleKey( key: number ) {
168
172
  switch( key ) {
169
173
  // bk space
170
174
  case 2: {
@@ -326,11 +330,16 @@ export class Keyboard extends HBox<KeyboardProps>
326
330
  private handleFocus( target: Element, enter: boolean ) {
327
331
 
328
332
  if( enter ) {
329
- if( target.tagName=='INPUT' && !(target as HTMLInputElement).readOnly ) {
330
- this.input = target as HTMLInputElement;
331
- this.visible = true;
332
- this.setTimeout( "vis", 200, this._updateVis );
333
- return;
333
+ if( target.tagName=='INPUT' ) {
334
+ const input = target as HTMLInputElement;
335
+ if( !input.readOnly &&
336
+ input.type!='checkbox' && input.type!='radio' &&
337
+ input.type!='range' && input.type!='file' ) {
338
+ this.input = input;
339
+ this.visible = true;
340
+ this.setTimeout( "vis", 200, this._updateVis );
341
+ return;
342
+ }
334
343
  }
335
344
  }
336
345
 
@@ -430,6 +439,7 @@ export class Keyboard extends HBox<KeyboardProps>
430
439
  let content = line[i];
431
440
  let key;
432
441
  let icon = null;
442
+ let repeat = false;
433
443
 
434
444
  if( content.length>2 && content[0]=='{' && content[content.length-1]=='}') {
435
445
 
@@ -452,6 +462,7 @@ export class Keyboard extends HBox<KeyboardProps>
452
462
 
453
463
  case 2:
454
464
  {
465
+ repeat = true;
455
466
  content = undefined;
456
467
  icon = icon_bksp;
457
468
  cls += ' cdel';
@@ -516,7 +527,18 @@ export class Keyboard extends HBox<KeyboardProps>
516
527
  key = line[i].charCodeAt(0);
517
528
  }
518
529
 
519
- let el = new Button( { cls, label: content, attrs: {'data-key': key}, icon } );
530
+ let el = new Button( {
531
+ cls,
532
+ label: content,
533
+ attrs: {'data-key': key},
534
+ icon,
535
+ autorepeat: repeat,
536
+ click: ( e ) => {
537
+ if( e.repeat ) {
538
+ this._handleKey( key )
539
+ }
540
+ }
541
+ } );
520
542
  tl.push( el );
521
543
  }
522
544
 
@@ -30,16 +30,25 @@
30
30
  background-color: var( --label-background );
31
31
  gap: 0.2em;
32
32
 
33
+ &.al-right
33
34
  &.right {
34
35
  text-align: right;
35
36
  justify-content: end;
36
37
  }
37
38
 
39
+ &.al-center,
38
40
  &.center {
39
41
  text-align: center;
40
42
  justify-content: center;
41
43
  }
42
44
 
45
+ &.al-right {
46
+ padding-left: 8px;
47
+ #icon {
48
+ order: 2;
49
+ }
50
+ }
51
+
43
52
  #text {
44
53
  &:empty {
45
54
  display: none;
@@ -24,6 +24,7 @@ export interface LabelProps extends ComponentProps {
24
24
  text?: string | UnsafeHtml;
25
25
  icon?: string;
26
26
  labelFor?: string;
27
+ align?: "left" | "center" | "right";
27
28
  }
28
29
 
29
30
  /**
@@ -32,29 +33,32 @@ export interface LabelProps extends ComponentProps {
32
33
 
33
34
  @class_ns( "x4" )
34
35
  export class Label extends Component<LabelProps> {
35
- #text: Component;
36
36
 
37
37
  constructor( p: LabelProps ) {
38
38
  super( { ...p, content: null } );
39
39
 
40
40
  this.setContent( [
41
41
  new Icon( { id:"icon", iconId: this.props.icon } ),
42
- this.#text = new Component( { tag: 'span', id: 'text' } )
42
+ new Component( { tag: 'span', id: 'text' } )
43
43
  ] );
44
44
 
45
45
  // small hack for react:
46
46
  // p.content may be the text
47
- const text = this.props.text;
48
- this.setText( text );
47
+ this.setText( this.props.text );
49
48
 
50
49
  if( p.labelFor ) {
51
50
  this.setAttribute( "for", p.labelFor );
52
51
  }
52
+
53
+ if( p.align ) {
54
+ this.addClass( "al-"+p.align );
55
+ }
53
56
  }
54
57
 
55
58
  setText( text: string | UnsafeHtml ) {
56
- this.#text.setContent( text );
57
- this.#text.setClass( "empty", !text );
59
+ const lab = this.query<Icon>( "#text" );
60
+ lab.setContent( text );
61
+ lab.setClass( "empty", !text );
58
62
  }
59
63
 
60
64
  setIcon( icon: string ) {
@@ -0,0 +1,44 @@
1
+ /**
2
+ * ___ ___ __
3
+ * \ \/ / / _
4
+ * \ / /_| |_
5
+ * / \____ _|
6
+ * /__/\__\ |_|
7
+ *
8
+ * @file link.module.scss
9
+ * @author Etienne Cochard
10
+ *
11
+ * @copyright (c) 2026 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
+ .x4link {
18
+ cursor: pointer;
19
+
20
+ #text {
21
+ text-decoration: underline;
22
+ }
23
+
24
+ &[disabled] {
25
+ cursor: not-allowed;
26
+ border-color: var( --border );
27
+ color: var( --listbox-item-color-disabled );
28
+
29
+ &>.body {
30
+ .x4viewport {
31
+ pointer-events: none;
32
+ .x4item {
33
+ &.selected {
34
+ background-color: var( --listbox-item-color-sel-disabled );
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ &:focus-within {
42
+ border-color: var( --listbox-border-focus );
43
+ }
44
+ }
@@ -19,6 +19,7 @@ import { EventCallback } from '../../core/core_events';
19
19
  import { class_ns, UnsafeHtml } from '../../core/core_tools';
20
20
  import { Label } from '../label/label';
21
21
 
22
+ import "./link.module.scss"
22
23
 
23
24
  /**
24
25
  * Link events
@@ -36,6 +37,7 @@ interface LinkProps extends ComponentProps {
36
37
  href: string;
37
38
  text?: string | UnsafeHtml; // you can also use content for complexe content
38
39
  icon?: string;
40
+ align?: "left" | "center" | "right";
39
41
  click?: EventCallback<EvClick>;
40
42
  }
41
43
 
@@ -44,12 +46,16 @@ export class Link extends Component<LinkProps,LinkEvents> {
44
46
  constructor( props: LinkProps ) {
45
47
  super( { tag: "a", ...props} );
46
48
 
47
- this.setAttribute( "href", props.href );
49
+ if( props.href ) {
50
+ this.setAttribute( "href", props.href );
51
+ }
52
+
48
53
  this.mapPropEvents( props, "click" );
49
54
 
50
55
  this.setContent( new Label( {
51
56
  text: props.text,
52
57
  icon: props.icon,
58
+ align: props.align,
53
59
  } ) );
54
60
 
55
61
  this.addDOMEvent('click', (e) => this._on_click(e));
@@ -39,16 +39,20 @@
39
39
  border: 1px solid var( --listbox-border );
40
40
  background-color: var( --listbox-background );
41
41
 
42
- &:focus-within {
43
- border-color: var( --listbox-border-focus );
44
- }
45
-
46
42
  outline: none;
47
43
 
48
44
  &>.x4header {
49
45
  border-bottom: 1px solid var( --border );
50
46
  }
51
47
 
48
+ &>.title {
49
+ background: white;
50
+ padding: 4px;
51
+ font-weight: bold;
52
+ color: black;
53
+ border-bottom: 1px solid var(--border);
54
+ }
55
+
52
56
  &>.body {
53
57
  width: 100%;
54
58
  flex-basis: 0;
@@ -94,6 +98,12 @@
94
98
  }
95
99
  }
96
100
 
101
+ .ref-c1 { width: var( --ref-c1-width); }
102
+ .ref-c2 { width: var( --ref-c2-width); }
103
+ .ref-c3 { width: var( --ref-c3-width); }
104
+ .ref-c4 { width: var( --ref-c4-width); }
105
+ .ref-c5 { width: var( --ref-c5-width); }
106
+
97
107
  //&:active{
98
108
  //background-color: var( --color-80 );
99
109
  //color: var(--color-0);
@@ -162,4 +172,8 @@
162
172
  }
163
173
  }
164
174
  }
175
+
176
+ &:focus-within {
177
+ border-color: var( --listbox-border-focus );
178
+ }
165
179
  }