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
@@ -28,6 +28,11 @@ import "./button.module.scss";
28
28
  */
29
29
 
30
30
  interface ButtonEvents extends ComponentEvents {
31
+ /**
32
+ * Fired when the button is clicked
33
+ * ex: myButton.on( "click", ( e: EvClick ) => { console.log( "click") } );
34
+ */
35
+
31
36
  click: EvClick;
32
37
  }
33
38
 
@@ -36,29 +41,62 @@ interface ButtonEvents extends ComponentEvents {
36
41
  */
37
42
 
38
43
  export interface ButtonProps extends ComponentProps {
39
- label?: string;
44
+ /** Text or HTML content of the button */
45
+ label?: string | UnsafeHtml;
46
+
47
+ /** Icon identifier to display */
40
48
  icon?: string;
49
+
50
+ /**
51
+ * Tab index for keyboard navigation.
52
+ * - `false` to exclude from tab order
53
+ * - `number` to set specific tab index
54
+ */
41
55
  tabindex?: boolean | number;
56
+
57
+ /**
58
+ * Enable auto-repeat behavior when button is held down.
59
+ * - `true` uses default 200ms repeat interval
60
+ * - `number` specifies custom repeat interval in milliseconds
61
+ *
62
+ * First click triggers after 500ms, then repeats at specified interval.
63
+ */
42
64
  autorepeat?: number | boolean;
65
+
66
+ /**
67
+ * Callback function invoked when button is clicked
68
+ * cf. ButtonEvents
69
+ */
43
70
  click?: EventCallback<EvClick>;
44
71
  }
45
72
 
46
73
  /**
47
- * Button component.
74
+ * Represents a clickable button component.
75
+ *
76
+ * Generates the CSS class **x4button** based on the class name.
77
+ * The button can contain an optional icon and label, supports keyboard activation,
78
+ * and may trigger auto-repeated click events while pointer is held down.
79
+ *
48
80
  */
49
81
 
82
+
50
83
  @class_ns( "x4" )
51
84
  export class Button extends Component<ButtonProps,ButtonEvents> {
52
85
 
53
86
  #text: Component;
54
87
 
55
88
  /**
56
- * Creates an instance of Button.
57
- *
58
- * @param props - The properties for the button component, including label and icon.
59
- * @example
60
- * const button = new Button({ label: 'Submit', icon: 'check-icon' });
61
- */
89
+ * Create a new Button.
90
+ *
91
+ * @param {ButtonProps} props - Configuration options such as `label`, `icon`, `tabindex`, `autorepeat`, and `click`.
92
+ *
93
+ * @example
94
+ * const btn = new Button({
95
+ * label: "Save",
96
+ * icon: "check",
97
+ * click: () => console.log("clicked"),
98
+ * });
99
+ */
62
100
 
63
101
  constructor( props: ButtonProps ) {
64
102
  super( { ...props, tag: 'button', content: null } );
@@ -88,7 +126,7 @@ export class Button extends Component<ButtonProps,ButtonEvents> {
88
126
  }
89
127
 
90
128
  /**
91
- * called by the system on click event
129
+ * @internal
92
130
  */
93
131
 
94
132
  protected _on_click( ev: MouseEvent ) {
@@ -109,6 +147,10 @@ export class Button extends Component<ButtonProps,ButtonEvents> {
109
147
  ev.stopPropagation();
110
148
  }
111
149
 
150
+ /**
151
+ * @internal
152
+ */
153
+
112
154
  protected _on_mouse( e: PointerEvent ) {
113
155
 
114
156
  let count = 0;
@@ -119,11 +161,10 @@ export class Button extends Component<ButtonProps,ButtonEvents> {
119
161
  const rt = this.props.autorepeat===true ? 200 : this.props.autorepeat as number;
120
162
 
121
163
  this.setTimeout( 'repeat', 500, ( ) => {
122
- count++;
123
-
124
- this.fire('click', {} );
164
+ this.fire( "click", {} );
125
165
  this.setInterval( 'repeat', rt, ( ) => {
126
- this.fire('click', {} );
166
+ count++;
167
+ this.fire( "click", {repeat:count} );
127
168
  })
128
169
  } );
129
170
  }
@@ -131,13 +172,16 @@ export class Button extends Component<ButtonProps,ButtonEvents> {
131
172
  this.clearTimeout( 'repeat' );
132
173
 
133
174
  if( !count ) {
134
- this.fire('click', {} );
175
+ this.fire("click", {} );
135
176
  }
136
177
  }
137
178
  }
138
179
 
139
180
  /**
140
- * simulate a click
181
+ * Activate the button as if it was clicked by a user.
182
+ *
183
+ * @example
184
+ * button.click();
141
185
  */
142
186
 
143
187
  click( ) {
@@ -145,7 +189,7 @@ export class Button extends Component<ButtonProps,ButtonEvents> {
145
189
  }
146
190
 
147
191
  /**
148
- * called on key down
192
+ * @internal
149
193
  */
150
194
 
151
195
  protected _on_keydown( e: KeyboardEvent ) {
@@ -156,13 +200,15 @@ export class Button extends Component<ButtonProps,ButtonEvents> {
156
200
  }
157
201
 
158
202
  /**
159
- * Sets the text content of the button's label.
160
- *
161
- * @param text - The new text or HTML content for the label.
162
- * @example
163
- * button.setText('Click Me');
164
- * button.setText(new UnsafeHtml('<b>Bold Text</b>'));
165
- */
203
+ * Set or change the button label.
204
+ *
205
+ * @param {string | UnsafeHtml} text - Text content or unsafe HTML.
206
+ *
207
+ * @example
208
+ * button.setText("Confirm");
209
+ * button.setText(new UnsafeHtml("<strong>OK</strong>"));
210
+ * button.setText( unsafe`<strong>OK</strong>` );
211
+ */
166
212
 
167
213
  public setText( text: string | UnsafeHtml ) {
168
214
  this.#text.setContent( text );
@@ -170,12 +216,13 @@ export class Button extends Component<ButtonProps,ButtonEvents> {
170
216
  }
171
217
 
172
218
  /**
173
- * Sets the icon of the button.
174
- *
175
- * @param icon - The new icon ID to set on the button.
176
- * @example
177
- * button.setIcon('new-icon-id');
178
- */
219
+ * Set or change the icon displayed by the button.
220
+ *
221
+ * @param {string} icon - Icon identifier to associate with the button.
222
+ *
223
+ * @example
224
+ * button.setIcon("arrow-right");
225
+ */
179
226
 
180
227
  public setIcon( icon: string ) {
181
228
  this.query<Icon>( "#icon" ).setIcon( icon );
@@ -52,7 +52,6 @@ export class DropdownList extends Popup<DropdownProps,DropdownEvents> {
52
52
  this.setContent( this._list );
53
53
 
54
54
  this.addDOMEvent( "mousedown", ( ev: Event ) => {
55
- console.log( "trap" );
56
55
  ev.stopImmediatePropagation( );
57
56
  ev.stopPropagation( );
58
57
  ev.preventDefault( );
@@ -228,6 +227,7 @@ export class Combobox extends Component<ComboboxProps,ComboboxEvents> {
228
227
  const rc = this._edit.getBoundingRect( );
229
228
  this._popup.setStyleValue( "minWidth", rc.width+"px" );
230
229
  this._popup.displayNear( rc, "top left", "bottom left", {x:0,y:6} );
230
+ this._popup.getList().ensureSelectionVisible( );
231
231
  }
232
232
 
233
233
  setItems( items: ListItem[] ) {
@@ -183,6 +183,10 @@ export class Dialog<P extends DialogProps = DialogProps, E extends DialogEvents
183
183
  return this.form.getValues();
184
184
  }
185
185
 
186
+ validate( ) {
187
+ return this.form.validate( );
188
+ }
189
+
186
190
  /**
187
191
  *
188
192
  */
@@ -34,7 +34,7 @@ import "./gridview.module.scss"
34
34
  export type CellRenderer = (rec: DataRecord) => Component;
35
35
  export type CellClassifier = (data: any, rec: DataRecord, col: string ) => string; // return the cell computed class
36
36
 
37
- type ColType = "number" | "money" | "checkbox" | "date" | "string" | "image" | "percent" | "icon";
37
+ type ColType = "number" | "money" | "checkbox" | "date" | "string" | "image" | "percent" | "icon" | "date-time";
38
38
 
39
39
  const SCROLL_LIMIT = 200;
40
40
 
@@ -121,9 +121,12 @@ export class Gridview<P extends GridviewProps = GridviewProps, E extends Gridvie
121
121
  private _end: number;
122
122
 
123
123
  private _selection: Set<number>;
124
- private _num_fmt = new Intl.NumberFormat('fr-FR');
125
- private _mny_fmt = new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' });
126
- private _dte_fmt = new Intl.DateTimeFormat('fr-FR', {});
124
+
125
+ // TODO: that
126
+ private _num_fmt = new Intl.NumberFormat('fr-FR');
127
+ private _mny_fmt = new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' });
128
+ private _dte_fmt = new Intl.DateTimeFormat('fr-FR', {});
129
+ private _dtetme_fmt = new Intl.DateTimeFormat('fr-FR', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:"2-digit"});
127
130
 
128
131
  private _has_fixed: boolean;
129
132
  private _has_footer: boolean;
@@ -345,6 +348,68 @@ export class Gridview<P extends GridviewProps = GridviewProps, E extends Gridvie
345
348
  }
346
349
  }
347
350
 
351
+ setColumns( columns: GridColumn[] ) {
352
+ this._columns = columns.map(x => x);
353
+ if( this.dom ) {
354
+
355
+ this._updateFlexs( );
356
+
357
+ // Rebuild headers
358
+ if (this._fheader) {
359
+ /*
360
+ const newFixedHeader = this._buildColHeader(true);
361
+ this._fheader.setContent(newFixedHeader.getChildren());
362
+ // On doit remplacer _fheader dans le DOM ou mettre à jour son contenu
363
+ // La méthode _buildColHeader retourne une Box.
364
+ // Ici, je vais simplifier en vidant et remplissant si possible,
365
+ // mais Box n'a pas forcément de méthode simple pour remplacer tout le DOM interne sans casser les events.
366
+ // Le plus simple est de remplacer les composants header dans le DOM global du gridview.
367
+
368
+ // Approche : on recrée les headers et on remplace les anciens
369
+ this._fheader.destroy();
370
+ this._hheader.destroy();
371
+
372
+ this._fheader = this._buildColHeader(true);
373
+ this._hheader = this._buildColHeader(false);
374
+
375
+ // Il faut réinsérer ces headers au bon endroit dans le DOM du Gridview.
376
+ // _init fait:
377
+ // this.setContent([
378
+ // this._fheader,
379
+ // this._hheader,
380
+ // this._vheader,
381
+ // this._viewport,
382
+ // this._ffooter,
383
+ // this._footer,
384
+ // ]);
385
+
386
+ // Donc on peut reconstruire le content complet
387
+ const content = [
388
+ this._fheader,
389
+ this._hheader,
390
+ this._vheader,
391
+ this._viewport
392
+ ];
393
+
394
+ if (this._has_footer) {
395
+ this._ffooter.destroy();
396
+ this._footer.destroy();
397
+ this._ffooter = this._buildColFooter(true);
398
+ this._footer = this._buildColFooter(false);
399
+ content.push(this._ffooter);
400
+ content.push(this._footer);
401
+ }
402
+
403
+ this.setContent(content);
404
+ */
405
+ console.assert( false, "TODO" );
406
+ }
407
+
408
+ this._computeFullSize( );
409
+ this._update( true );
410
+ }
411
+ }
412
+
348
413
  getView( ): DataView {
349
414
  return this._dataview;
350
415
  }
@@ -417,7 +482,7 @@ export class Gridview<P extends GridviewProps = GridviewProps, E extends Gridvie
417
482
  attrs: { "data-col": col },
418
483
  style: { width: cdata.width ? cdata.width + "px" : undefined },
419
484
  content: [
420
- new SimpleText({ text: cdata.title, align: cdata.header_align ?? "left" }),
485
+ new SimpleText({ cls: 'title', text: cdata.title, align: cdata.header_align ?? "left" }),
421
486
  new Component({ cls: "sorter" }),
422
487
  sizer
423
488
  ]
@@ -642,10 +707,23 @@ export class Gridview<P extends GridviewProps = GridviewProps, E extends Gridvie
642
707
  }
643
708
 
644
709
  case "date": {
710
+ if( isString(data) ) {
711
+ data = new Date( data );
712
+ }
713
+
645
714
  data = this._dte_fmt.format(data as Date);
646
715
  break;
647
716
  }
648
717
 
718
+ case "date-time": {
719
+ if( isString(data) ) {
720
+ data = new Date( data );
721
+ }
722
+
723
+ data = this._dtetme_fmt.format(data as Date);
724
+ break;
725
+ }
726
+
649
727
  default: {
650
728
  data = data + "";
651
729
  break;
@@ -1111,6 +1189,10 @@ export class Gridview<P extends GridviewProps = GridviewProps, E extends Gridvie
1111
1189
  *
1112
1190
  */
1113
1191
 
1192
+ clearSelection( ) {
1193
+ this._clearSelection( );
1194
+ }
1195
+
1114
1196
  private _clearSelection() {
1115
1197
  for (const ref of this._selection.keys()) {
1116
1198
  const els = this.queryAll(`.row[data-row="${ref}"]`)
@@ -1168,10 +1250,26 @@ export class Gridview<P extends GridviewProps = GridviewProps, E extends Gridvie
1168
1250
  *
1169
1251
  */
1170
1252
 
1171
- selectItem( id: any ) {
1253
+ selectItem( id: any, ensureVisible = true ) {
1172
1254
  const index = this._dataview.indexOfId( id );
1173
1255
  if( index>=0 ) {
1174
1256
  this._addSelection( index );
1257
+ if( ensureVisible ) {
1258
+ this._scrollToIndex( index );
1259
+ }
1260
+ }
1261
+ }
1262
+
1263
+ /**
1264
+ *
1265
+ */
1266
+
1267
+ setColTitle( col_name: any, title: string ) {
1268
+ const col = this._columns.findIndex( x => x.id==col_name );
1269
+ if( col>=0 ) {
1270
+ this._columns[col].title = title;
1271
+ const el = this._hheader.query<SimpleText>(`[data-col="${col}"] .title` )
1272
+ el.setText( title );
1175
1273
  }
1176
1274
  }
1177
1275
  }
@@ -14,8 +14,8 @@
14
14
  * that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
15
15
  **/
16
16
 
17
- import { class_ns } from '../../core/core_tools';
18
- import { Component, ComponentProps } from '../../core/component';
17
+ import { class_ns } from '../../core/core_tools.ts';
18
+ import { Component, ComponentProps } from '../../core/component.ts';
19
19
 
20
20
  import "./icon.module.scss"
21
21
 
@@ -68,20 +68,44 @@ class SvgLoader {
68
68
  export const svgLoader = new SvgLoader( );
69
69
 
70
70
  /**
71
- *
71
+ * Icon component properties.
72
+ *
73
+ * @property {string} [iconId] - Identifier or path of the icon to display.
72
74
  */
73
75
 
74
76
  export interface IconProps extends ComponentProps {
77
+ /**
78
+ * Identifier for the icon.
79
+ * Can be a URL, a CSS variable (prefixed with `var:`), or inline SVG data.
80
+ * @example
81
+ * // Using a CSS variable
82
+ * { iconId: "var:home" }
83
+ * @example
84
+ * // Using an imported SVG
85
+ * import myicon from "./myicon.svg";
86
+ * { iconId: myicon }
87
+ */
75
88
  iconId?: string;
76
89
  }
77
90
 
78
91
  /**
79
- *
92
+ * A component for rendering icons.
93
+ * Supports inline SVG, external SVG files, and CSS variables for icon paths.
94
+ * The CSS class for this component is automatically generated as `x4icon`.
80
95
  */
81
96
 
82
97
  @class_ns( "x4" )
83
98
  export class Icon extends Component<IconProps> {
84
99
 
100
+ /**
101
+ * Create a new Icon.
102
+ *
103
+ * @param {IconProps} props - Optional initial icon identifier.
104
+ *
105
+ * @example
106
+ * const icon = new Icon({ iconId: "check" });
107
+ */
108
+
85
109
  constructor( props: IconProps ) {
86
110
  super( props );
87
111
 
@@ -89,16 +113,20 @@ export class Icon extends Component<IconProps> {
89
113
  }
90
114
 
91
115
  /**
92
- * change the icon content
93
- * @param iconId if name is starting with var: then we use css variable name a path
94
- * @example
95
- *
96
- * setIcon( "var:home" )
97
- *
98
- * import myicon from "./myicon.svg"
99
- * setIcon( myicon );
100
- *
101
- */
116
+ * Sets or updates the icon content.
117
+ * @param iconId - Identifier for the icon.
118
+ * If it starts with `var:`, the value is treated as a CSS variable name.
119
+ * If it is a data URL (e.g., `data:image/svg+xml,<svg...`), the SVG is rendered directly.
120
+ * If it ends with `.svg`, the file is loaded asynchronously.
121
+ * Otherwise, it is treated as an image URL.
122
+ * @example
123
+ * // Using a CSS variable
124
+ * setIcon("var:home");
125
+ * @example
126
+ * // Using an imported SVG
127
+ * import myicon from "./myicon.svg";
128
+ * setIcon(myicon);
129
+ */
102
130
 
103
131
  setIcon( iconId: string ) {
104
132
  this.clearContent( );