x4js 2.0.28 → 2.0.30

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 (70) 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/prepack.mjs +3 -0
  16. package/scripts/prepack.mjs +342 -0
  17. package/src/colors.scss +246 -0
  18. package/src/components/boxes/boxes.module.scss +1 -1
  19. package/src/components/boxes/boxes.ts +139 -28
  20. package/src/components/button/button.ts +80 -33
  21. package/src/components/combobox/combobox.ts +1 -1
  22. package/src/components/dialog/dialog.ts +4 -0
  23. package/src/components/gridview/gridview.ts +104 -6
  24. package/src/components/icon/icon.ts +42 -14
  25. package/src/components/input/input.ts +146 -74
  26. package/src/components/keyboard/keyboard.module.scss +1 -1
  27. package/src/components/keyboard/keyboard.ts +31 -9
  28. package/src/components/label/label.module.scss +9 -0
  29. package/src/components/label/label.ts +10 -6
  30. package/src/components/link/link.module.scss +44 -0
  31. package/src/components/link/link.ts +7 -1
  32. package/src/components/listbox/listbox.module.scss +18 -4
  33. package/src/components/listbox/listbox.ts +32 -12
  34. package/src/components/menu/menu.module.scss +14 -2
  35. package/src/components/menu/menu.ts +1 -1
  36. package/src/components/messages/messages.ts +13 -5
  37. package/src/components/panel/panel.module.scss +7 -0
  38. package/src/components/popup/popup.ts +14 -10
  39. package/src/components/propgrid/propgrid.ts +1 -1
  40. package/src/components/shared.scss +4 -0
  41. package/src/components/spreadsheet/spreadsheet.ts +81 -34
  42. package/src/components/tabs/tabs.module.scss +1 -0
  43. package/src/components/textarea/textarea.ts +8 -2
  44. package/src/components/textedit/textedit.ts +7 -0
  45. package/src/components/themes.scss +2 -0
  46. package/src/components/tooltips/tooltips.ts +15 -3
  47. package/src/core/component.ts +358 -162
  48. package/src/core/core_application.ts +129 -32
  49. package/src/core/core_colors.ts +382 -119
  50. package/src/core/core_data.ts +73 -86
  51. package/src/core/core_dom.ts +10 -0
  52. package/src/core/core_dragdrop.ts +32 -7
  53. package/src/core/core_element.ts +111 -4
  54. package/src/core/core_events.ts +48 -11
  55. package/src/core/core_i18n.ts +2 -0
  56. package/src/core/core_pdf.ts +454 -0
  57. package/src/core/core_router.ts +64 -5
  58. package/src/core/core_state.ts +1 -0
  59. package/src/core/core_styles.ts +11 -12
  60. package/src/core/core_svg.ts +346 -51
  61. package/src/core/core_tools.ts +105 -17
  62. package/src/x4.ts +1 -0
  63. package/src/x4tsx.d.ts +2 -1
  64. package/tsconfig.json +11 -0
  65. package/lib/README.txt +0 -20
  66. package/lib/cjs/x4.css +0 -1
  67. package/lib/cjs/x4.js +0 -2
  68. package/lib/esm/x4.css +0 -1
  69. package/lib/esm/x4.mjs +0 -2
  70. package/lib/styles/x4.css +0 -1
@@ -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
  }
@@ -56,6 +56,8 @@ export interface ListboxEvents extends ComponentEvents {
56
56
  export interface ListboxProps extends Omit<ComponentProps,'content'> {
57
57
  items?: ListItem[];
58
58
  renderer?: ( item: ListItem ) => Component;
59
+ title?: string;
60
+ icon?: string;
59
61
  header?: Header;
60
62
  footer?: Component,
61
63
  checkable?: true,
@@ -104,6 +106,7 @@ export class Listbox extends Component<ListboxProps,ListboxEvents> {
104
106
  }
105
107
 
106
108
  this.setContent( [
109
+ (props.title || props.icon) ? new Label( { cls: 'title', text: props.title, icon: props.icon }) : null,
107
110
  props.header ? props.header : null,
108
111
  scroller,
109
112
  props.footer,
@@ -117,7 +120,7 @@ export class Listbox extends Component<ListboxProps,ListboxEvents> {
117
120
  } );
118
121
 
119
122
  if( props.items ) {
120
- this.setItems( props.items );
123
+ this.setItems( props.items, false );
121
124
  }
122
125
  }
123
126
 
@@ -191,8 +194,15 @@ export class Listbox extends Component<ListboxProps,ListboxEvents> {
191
194
  }
192
195
  else {
193
196
  const selitem = this._itemWithID( this._lastsel );
194
- let nel = sens==kbNav.next ? selitem.nextElement() : selitem.prevElement();
195
- nel = next_visible( nel, sens==kbNav.next );
197
+
198
+ let nel;
199
+ if( selitem ) {
200
+ nel = sens==kbNav.next ? selitem.nextElement() : selitem.prevElement();
201
+ nel = next_visible( nel, sens==kbNav.next );
202
+ }
203
+ else {
204
+ nel = sens==kbNav.next ? this._view.firstChild() : this._view.lastChild( );
205
+ }
196
206
 
197
207
  if( nel ) {
198
208
  const id = nel.getInternalData( "id" );
@@ -406,10 +416,12 @@ export class Listbox extends Component<ListboxProps,ListboxEvents> {
406
416
  this._multisel.clear( );
407
417
  }
408
418
 
409
- clearSelection( ) {
419
+ clearSelection( fireEvent = true ) {
410
420
  if( this._multisel.size ) {
411
421
  this._clearSelection( );
412
- this.fire( "selectionChange", { selection: [], empty: true } );
422
+ if( fireEvent ) {
423
+ this.fire( "selectionChange", { selection: [], empty: true } );
424
+ }
413
425
  }
414
426
  }
415
427
 
@@ -425,24 +437,21 @@ export class Listbox extends Component<ListboxProps,ListboxEvents> {
425
437
  this._view.clearContent( );
426
438
  this._items = items ?? [];
427
439
 
428
- let upsel = false;
440
+ let update_sel = false;
429
441
 
430
442
  if( this._items.length ) {
431
443
  const content = items.map( x => this.renderItem(x) );
432
444
  this._view.setContent( content );
433
445
 
434
- if( keepSel ) {
446
+ if( keepSel && oldSel.length>0 ) {
435
447
  this.select( oldSel );
436
448
  }
437
- else {
438
- upsel = true;
439
- }
440
449
  }
441
450
  else {
442
- upsel = true;
451
+ update_sel = oldSel.length>0;
443
452
  }
444
453
 
445
- if( upsel ) {
454
+ if( update_sel ) {
446
455
  this.setTimeout( "sel", 100, ( ) => {
447
456
  this.fire( "selectionChange", { selection: [], empty: true } );
448
457
  } );
@@ -573,4 +582,15 @@ export class Listbox extends Component<ListboxProps,ListboxEvents> {
573
582
  getSelection( ) {
574
583
  return Array.from( this._multisel );
575
584
  }
585
+
586
+ ensureSelectionVisible( ) {
587
+ const sels = Array.from( this._multisel.values() );
588
+ if( sels.length) {
589
+ const item = this._itemWithID( sels[0] );
590
+ item?.scrollIntoView( {
591
+ behavior: "instant",
592
+ block: "nearest"
593
+ } )
594
+ }
595
+ }
576
596
  }
@@ -17,7 +17,7 @@
17
17
  @use "../shared.scss";
18
18
 
19
19
  :root {
20
- --menu-background: var( --background-primary );
20
+ --menu-background: var( --background-ternary );
21
21
  --menu-border: var( --border-hover );
22
22
 
23
23
  --menuitem-color: var( --text-ternary );;
@@ -28,7 +28,7 @@
28
28
  }
29
29
 
30
30
  .x4menu {
31
- @extend .shadow-lg;
31
+ @extend .shadow-xxl;
32
32
 
33
33
  position: absolute;
34
34
  overflow-y: auto;
@@ -39,6 +39,7 @@
39
39
 
40
40
  background-color: var(--menu-background);
41
41
  border: 1px solid var(--menu-border);
42
+ border-left: 4px solid var( --accent-background );
42
43
 
43
44
  max-height: calc( 100vh - 32px );
44
45
 
@@ -69,6 +70,17 @@
69
70
  }
70
71
  }
71
72
 
73
+ &.danger #icon {
74
+ .fa-primary {
75
+ fill: var( --color-danger-a50 );
76
+ }
77
+
78
+ .fa-secondary {
79
+ fill: var( --color-danger-a30 );
80
+ opacity: 1;
81
+ }
82
+ }
83
+
72
84
  #text {
73
85
  @extend .flex;
74
86
  }
@@ -34,7 +34,7 @@ export interface MenuItem {
34
34
  icon?: string;
35
35
  text: string | UnsafeHtml;
36
36
  menu?: Menu;
37
- disabled?: true;
37
+ disabled?: boolean;
38
38
  click?: DOMEventHandler;
39
39
  }
40
40
 
@@ -98,6 +98,11 @@ export class MessageBox extends Dialog<DialogProps>
98
98
  }
99
99
 
100
100
 
101
+ interface InputOptions {
102
+ password?: boolean;
103
+ trim?: boolean;
104
+ }
105
+
101
106
  @class_ns( "x4" )
102
107
  export class InputBox extends Dialog<DialogProps>
103
108
  {
@@ -110,7 +115,10 @@ export class InputBox extends Dialog<DialogProps>
110
115
  return input.getValue( );
111
116
  }
112
117
 
113
- private static _create( msg: string | UnsafeHtml, value: string, title: string ) {
118
+ private static _create( msg: string | UnsafeHtml, value: string, title: string, options?: InputOptions ) {
119
+
120
+ options = {trim: true, password: false, ...options };
121
+
114
122
  const box = new InputBox({
115
123
  modal: true,
116
124
  title,
@@ -122,7 +130,7 @@ export class InputBox extends Dialog<DialogProps>
122
130
  new Icon( { iconId: pen_icon }),
123
131
  new VBox( { flex: 1, content: [
124
132
  new Label( { text: msg } ),
125
- new Input( { value, type: "text" } )
133
+ new Input( { value, type: options.password ? "password" : "text", trim: options.trim } )
126
134
  ]})
127
135
  ]
128
136
  }),
@@ -138,11 +146,11 @@ export class InputBox extends Dialog<DialogProps>
138
146
  * idem with promise
139
147
  */
140
148
 
141
- static async showAsync( msg: string | UnsafeHtml, value: string, title?: string ) : Promise<string> {
149
+ static async showAsync( msg: string | UnsafeHtml, value: string, title?: string, options?: InputOptions ) : Promise<string> {
142
150
 
143
- return new Promise( (resolve, reject ) => {
151
+ return new Promise( (resolve, _reject ) => {
144
152
 
145
- const box = this._create( msg, value, title );
153
+ const box = this._create( msg, value, title, options );
146
154
 
147
155
  box.on( "btnclick", ( ev ) => {
148
156
  asap( ( ) => {
@@ -51,10 +51,17 @@
51
51
  border: none;
52
52
  border-top: 1px solid var( --border );
53
53
  margin-top: 1.5em;
54
+ border-radius: 0;
55
+
56
+ padding: 0;
54
57
 
55
58
  legend {
56
59
  background: none;
57
60
  top: -1.4em;
58
61
  left: 0;
59
62
  }
63
+
64
+ & >.body {
65
+ padding: 0;
66
+ }
60
67
  }
@@ -21,6 +21,7 @@ import { Rect, Point, class_ns, asap } from '../../core/core_tools';
21
21
  import { Box } from '../boxes/boxes'
22
22
 
23
23
  import "./popup.module.scss"
24
+ import { getGlobalZoom, getScrollbarSize } from '../../core/core_tools.js';
24
25
 
25
26
  export interface PopupEvents extends ComponentEvents {
26
27
  closed: ComponentEvent;
@@ -143,24 +144,27 @@ export class Popup<P extends PopupProps = PopupProps, E extends PopupEvents = Po
143
144
  */
144
145
 
145
146
  displayAt( x: number, y: number ) {
147
+ const zm = getGlobalZoom( );
148
+
146
149
  //TODO: check is already visible
147
150
  this.setStyle( {
148
- left: x+"px",
149
- top: y+"px",
151
+ left: (x/zm)+"px",
152
+ top: (y/zm)+"px",
150
153
  })
151
154
 
152
155
  this._do_show( ); // to compute size
153
156
 
154
- const rc = this.getBoundingRect( );
155
- const width = window.innerWidth - 16;
156
- const height = window.innerHeight - 16;
157
-
158
- if( rc.right>width ) {
159
- this.setStyleValue( "left", width-rc.width );
157
+ const rc = this.getBoundingRect( ).scale( 1/zm );
158
+ const sbw = getScrollbarSize( );
159
+
160
+ const screen_width = window.innerWidth - sbw;
161
+ if( rc.right>screen_width ) {
162
+ this.setStyleValue( "left", screen_width-rc.width );
160
163
  }
161
164
 
162
- if( rc.bottom>height ) {
163
- this.setStyleValue( "top", height-rc.height );
165
+ const screen_height = window.innerHeight - sbw;
166
+ if( rc.bottom>screen_height ) {
167
+ this.setStyleValue( "top", screen_height-rc.height );
164
168
  }
165
169
  }
166
170
 
@@ -73,7 +73,7 @@ export class PropertyGrid extends VBox {
73
73
  if( props.groups ) {
74
74
  this.setItems( props.groups );
75
75
  }
76
- };
76
+ }
77
77
 
78
78
  /**
79
79
  *
@@ -75,6 +75,10 @@
75
75
  box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
76
76
  }
77
77
 
78
+ .shadow-xxl {
79
+ box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.3), 0 8px 10px -6px rgb(0 0 0 / 0.3);
80
+ }
81
+
78
82
 
79
83
  @keyframes rotating {
80
84
  from {