x4js 2.3.20 → 2.3.22

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 (32) hide show
  1. package/aicontext.md +127 -32
  2. package/package.json +1 -1
  3. package/src/components/autocomplete/autocomplete.ts +3 -0
  4. package/src/components/boxes/boxes.ts +1 -1
  5. package/src/components/chart/chart.ts +12 -3
  6. package/src/components/colorinput/colorinput.module.scss +11 -2
  7. package/src/components/colorinput/colorinput.ts +37 -4
  8. package/src/components/colorpicker/colorpicker.module.scss +14 -2
  9. package/src/components/colorpicker/colorpicker.ts +57 -11
  10. package/src/components/components.ts +1 -0
  11. package/src/components/dialog/dialog.ts +16 -5
  12. package/src/components/form/form.ts +3 -4
  13. package/src/components/gridview/gridview.module.scss +18 -16
  14. package/src/components/gridview/gridview.ts +5 -0
  15. package/src/components/icon/icon.ts +34 -18
  16. package/src/components/keyboard/keyboard.ts +31 -5
  17. package/src/components/link/link.ts +18 -7
  18. package/src/components/messages/messages.ts +1 -1
  19. package/src/components/panel/panel.ts +3 -2
  20. package/src/components/settingsview/settingsview.module.scss +178 -0
  21. package/src/components/settingsview/settingsview.ts +289 -0
  22. package/src/components/spreadsheet/spreadsheet.module.scss +18 -16
  23. package/src/components/textarea/textarea.module.scss +10 -3
  24. package/src/components/video/video.ts +17 -3
  25. package/src/core/core_colors.ts +21 -10
  26. package/src/core/core_dragdrop.ts +5 -4
  27. package/src/core/core_i18n.ts +191 -54
  28. package/src/core/core_pdf.ts +15 -3
  29. package/src/core/core_shortcuts.ts +5 -1
  30. package/src/core/core_styles.ts +46 -25
  31. package/src/core/core_svg.ts +24 -16
  32. package/src/core/core_tools.ts +18 -9
@@ -30,6 +30,9 @@ export interface ColorPickerProps extends ComponentProps {
30
30
 
31
31
  /** Adds a "no color" swatch: picking it fires `change` with a null color. */
32
32
  nullable?: boolean;
33
+
34
+ /** false: the colors are opaque, there is no alpha slider (default true). */
35
+ alpha?: boolean;
33
36
  }
34
37
 
35
38
  interface HueChangeEvent extends ComponentEvent {
@@ -83,13 +86,17 @@ export class Saturation extends Box<BoxProps,CommonEvents> {
83
86
  created: () => this.updateThumbMarker( ),
84
87
  } );
85
88
 
86
- this.updateBaseColor( init );
89
+ // the color at start: its hue, and the thumb on its saturation and value
90
+ this.setColor( init );
87
91
  }
88
92
 
89
93
  mousedown( ev: PointerEvent ) {
90
94
  this.mdown = true;
91
95
  this.irect = this.getBoundingRect( );
92
96
  this.setCapture( ev.pointerId );
97
+
98
+ // a click takes the point too, without a move
99
+ this.mousemove( ev );
93
100
  }
94
101
 
95
102
  mousemove( ev: PointerEvent ) {
@@ -213,6 +220,9 @@ class HueSlider extends Box<BoxProps,CommonEvents> {
213
220
  this.mdown = true;
214
221
  this.irect = this.getBoundingRect( );
215
222
  this.setCapture( ev.pointerId );
223
+
224
+ // a click takes the point too, without a move
225
+ this.mousemove( ev );
216
226
  }
217
227
 
218
228
  mousemove( ev: PointerEvent ) {
@@ -286,14 +296,17 @@ class AlphaSlider extends Box<BoxProps,CommonEvents> {
286
296
  pointerup: ( e ) => this._on_mouseup( e ),
287
297
  } );
288
298
 
289
- this.updateAlpha( );
290
- this.updateBaseColor( init );
299
+ // the color at start: its gradient, and the thumb on its alpha
300
+ this.setColor( init );
291
301
  }
292
302
 
293
303
  _on_mousedown( ev: PointerEvent ) {
294
304
  this.mdown = true;
295
305
  this.irect = this.getBoundingRect( );
296
306
  this.setCapture( ev.pointerId );
307
+
308
+ // a click takes the point too, without a move
309
+ this._on_mousemove( ev );
297
310
  }
298
311
 
299
312
  _on_mousemove( ev: PointerEvent ) {
@@ -370,7 +383,10 @@ interface ColorPickerChangeEvents extends ComponentEvents {
370
383
  }
371
384
 
372
385
  /**
373
- *
386
+ * @cssvar
387
+ * ```
388
+ * --colorpicker-none
389
+ * ```
374
390
  */
375
391
 
376
392
  @class_ns( "x4" )
@@ -395,6 +411,11 @@ export class ColorPicker extends VBox<ColorPickerProps,ColorPickerChangeEvents>
395
411
  this._base = new Color( props.color );
396
412
  }
397
413
 
414
+ // no alpha: the color is opaque
415
+ if( props.alpha===false ) {
416
+ this._base.setAlpha( 1 );
417
+ }
418
+
398
419
  const hsv = this._hsv = this._base.toHsv( );
399
420
 
400
421
  this.setAttribute( "tabindex", 0 );
@@ -417,6 +438,8 @@ export class ColorPicker extends VBox<ColorPickerProps,ColorPickerChangeEvents>
417
438
  this._swatches = new HBox( { cls: "swatches" } ),
418
439
  ]);
419
440
 
441
+ this._alpha.show( props.alpha!==false );
442
+
420
443
  this._sat.on( "sat_change", ( ev ) => {
421
444
  this._hsv.saturation = ev.saturation;
422
445
  this._hsv.value = ev.value;
@@ -460,14 +483,21 @@ export class ColorPicker extends VBox<ColorPickerProps,ColorPickerChangeEvents>
460
483
  private _changed( ) {
461
484
  const hsv = this._hsv;
462
485
 
463
- this._base.setHsv( hsv.hue, hsv.saturation, hsv.value, hsv.alpha );
486
+ // no alpha: the hidden slider can still be moved by the keys, the color stays opaque
487
+ let alpha = hsv.alpha;
488
+ if( this.props.alpha===false ) {
489
+ alpha = 1;
490
+ }
491
+
492
+ this._base.setHsv( hsv.hue, hsv.saturation, hsv.value, alpha );
464
493
  this._refresh( );
465
494
  this._notify( );
466
495
  }
467
496
 
468
497
  private _refresh( ) {
469
- this._swatch.setStyleValue( "backgroundColor", this._base.toRgbString() );
470
- this._swatch.setAttribute( "tooltip", this._base.toRgbString() );
498
+ // with its alpha: the swatch is drawn over checkers
499
+ this._swatch.setStyleValue( "backgroundColor", this._base.toRgbString( true ) );
500
+ this._swatch.setAttribute( "tooltip", this._base.toRgbString( true ) );
471
501
  }
472
502
 
473
503
  private _notify( ) {
@@ -499,8 +529,13 @@ export class ColorPicker extends VBox<ColorPickerProps,ColorPickerChangeEvents>
499
529
  setColor( color: Color | string ) {
500
530
  const rgb = ( color instanceof Color ? color : new Color( color ) ).toRgb( );
501
531
 
532
+ let alpha = rgb.alpha;
533
+ if( this.props.alpha===false ) {
534
+ alpha = 1;
535
+ }
536
+
502
537
  // the color itself is kept as given: going through hsv could shift it
503
- this._base.setRgb( rgb.red, rgb.green, rgb.blue, rgb.alpha );
538
+ this._base.setRgb( rgb.red, rgb.green, rgb.blue, alpha );
504
539
 
505
540
  const hsv = this._base.toHsv( );
506
541
  this._hsv.hue = hsv.hue;
@@ -538,14 +573,17 @@ export class ColorPicker extends VBox<ColorPickerProps,ColorPickerChangeEvents>
538
573
  continue;
539
574
  }
540
575
 
541
- items.push( new Component( {
576
+ const item = new Component( {
542
577
  cls: "item",
543
578
  tooltip: color.toHexString( ),
544
- style: { backgroundColor: color.toRgbString( ) },
545
579
  dom_events: {
546
580
  click: ( ) => this._pick( color ),
547
581
  },
548
- } ) );
582
+ } );
583
+
584
+ // as it is taken: opaque without alpha; the stylesheet draws it over checkers
585
+ item.setStyleVariable( "--swatch-color", color.toRgbString( this.props.alpha!==false ) );
586
+ items.push( item );
549
587
  }
550
588
 
551
589
  this._swatches.setContent( items );
@@ -593,6 +631,14 @@ export class ColorPicker extends VBox<ColorPickerProps,ColorPickerChangeEvents>
593
631
  }
594
632
  break;
595
633
  }
634
+
635
+ default: {
636
+ // not for the picker: Tab, Escape... go on
637
+ return;
638
+ }
596
639
  }
640
+
641
+ // the arrows move the picker, not the page
642
+ ev.preventDefault( );
597
643
  }
598
644
  }
@@ -35,6 +35,7 @@ export * from "./propgrid/propgrid"
35
35
  export * from "./radio/radio"
36
36
  export * from "./rating/rating"
37
37
  export * from "./select/select"
38
+ export * from "./settingsview/settingsview"
38
39
  export * from "./sizers/sizer"
39
40
  export * from "./slider/slider"
40
41
  export * from "./spreadsheet/spreadsheet"
@@ -64,7 +64,8 @@ export class Dialog<P extends DialogProps = DialogProps, E extends DialogEvents
64
64
  private _title: Label;
65
65
 
66
66
  constructor(props: P) {
67
- super({ tag: "dialog", modal: true, ...props });
67
+ // content is the content of its form
68
+ super({ tag: "dialog", modal: true, ...props, content: undefined });
68
69
 
69
70
  this._ismodal = this.props.modal;
70
71
 
@@ -95,7 +96,7 @@ export class Dialog<P extends DialogProps = DialogProps, E extends DialogEvents
95
96
  }) : null,
96
97
  ]
97
98
  }),
98
- this.form = props.form ? props.form : new Form({}),
99
+ this.form = props.form ? props.form : new Form({ content: props.content }),
99
100
  new BtnGroup({
100
101
  id: "btnbar",
101
102
  reverse: true,
@@ -133,9 +134,19 @@ export class Dialog<P extends DialogProps = DialogProps, E extends DialogEvents
133
134
  }
134
135
  })
135
136
 
136
- asap( ( ) => {
137
- this.focusNext( true );
138
- } );
137
+ }
138
+
139
+ /**
140
+ * each time it opens: the focus on its first field
141
+ */
142
+
143
+ override show( show = true ): this {
144
+ super.show( show );
145
+ if( show ) {
146
+ asap( ( ) => this.focusNext( true ) );
147
+ }
148
+
149
+ return this;
139
150
  }
140
151
 
141
152
  private focusNext( next: boolean) : boolean {
@@ -105,10 +105,9 @@ export class Form<P extends FormProps = FormProps> extends Box<P> {
105
105
  */
106
106
 
107
107
  setAutoComplete( on = true ) {
108
- const items = this._get_inputs( );
109
- items.forEach( x => {
110
- x.setAttribute( "autocomplete", on ? "on" : "off" );
111
- } );
108
+ // on the <form>: its fields without an autocomplete of their own follow it,
109
+ // those added later too
110
+ this.setAttribute( "autocomplete", on ? "on" : "off" );
112
111
  }
113
112
 
114
113
  /**
@@ -16,31 +16,32 @@
16
16
 
17
17
 
18
18
  :root {
19
- --gridview-background: white;
19
+ // from the theme: a dark theme (its scale --color-primary-* reversed) gives a dark grid
20
+ --gridview-background: var( --background-primary );
20
21
  --gridview-border: var( --border );
21
22
 
22
- --gridview-header-cell-background: white;
23
- --gridview-header-cell-color: black;
24
- --gridview-header-cell-vline: #f0f0f0;
25
- --gridview-header-cell-border: #cccccc;
23
+ --gridview-header-cell-background: var( --background-primary );
24
+ --gridview-header-cell-color: var( --text-primary );
25
+ --gridview-header-cell-vline: var( --border );
26
+ --gridview-header-cell-border: var( --border );
26
27
 
27
28
  --grid-check-background: var( --accent-background );
28
- --grid-check-color: white;
29
- --grid-check-background-hover: white;
29
+ --grid-check-color: var( --accent-color );
30
+ --grid-check-background-hover: var( --background-primary );
30
31
  --grid-check-color-hover: var( --accent-background );
31
32
 
32
33
  --grid-perc-background: var( --accent-background );
33
- --grid-perc-color: white;
34
- --grid-perc-background-hover: white;
34
+ --grid-perc-color: var( --accent-color );
35
+ --grid-perc-background-hover: var( --background-primary );
35
36
  --grid-perc-color-hover: var( --accent-background );
36
37
 
37
38
 
38
- --gridview-cell-color: black;
39
- --gridview-cell-color-sel: white;
39
+ --gridview-cell-color: var( --text-primary );
40
+ --gridview-cell-color-sel: var( --accent-color );
40
41
  --gridview-cell-vline: var( --border );
41
42
 
42
- --gridview-row-background: white;
43
- --gridview-row-odd-background: white;
43
+ --gridview-row-background: var( --background-primary );
44
+ --gridview-row-odd-background: var( --background-primary );
44
45
  --gridview-row-border: var( --border );
45
46
 
46
47
  --gridview-row-background-hover: var( --background-secondary );
@@ -71,6 +72,7 @@
71
72
 
72
73
  background-color: var( --gridview-background );
73
74
  border: 1px solid var( --gridview-border );
75
+ color: var( --gridview-cell-color );
74
76
 
75
77
  .cell {
76
78
  padding: 4px;
@@ -159,7 +161,7 @@
159
161
  width: 100%;
160
162
  border: 1px solid var( --border );
161
163
  height: 8px;
162
- background-color: white;
164
+ background-color: var( --gridview-background );
163
165
  overflow: hidden;
164
166
 
165
167
  div {
@@ -219,7 +221,7 @@
219
221
  display: inline-flex;
220
222
  align-items: center;
221
223
 
222
- color: black;
224
+ color: var( --gridview-header-cell-color );
223
225
  font-weight: 500;
224
226
  justify-content: center;
225
227
 
@@ -320,7 +322,7 @@
320
322
  width: var( --fixed-width );
321
323
  bottom: 0;
322
324
 
323
- background-color: white;
325
+ background-color: var( --gridview-background );
324
326
  z-index: 1;
325
327
 
326
328
  .row {
@@ -559,6 +559,11 @@ export class Gridview<P extends GridviewProps = GridviewProps, E extends Gridvie
559
559
 
560
560
  private _scrollToIndex( index: number, block = 'nearest' ) {
561
561
 
562
+ // not built yet: nothing to scroll
563
+ if( !this._viewport ) {
564
+ return;
565
+ }
566
+
562
567
  // is it already visible ?
563
568
  let rows = this.queryAll(`.row[data-row="${index}"]`);
564
569
  if (rows.length) {
@@ -27,7 +27,7 @@ type solveCallback = (data:string)=>void;
27
27
 
28
28
  class SvgLoader {
29
29
  private cache: Map<string,string>;
30
- private waiters: Map<string,solveCallback[]>;
30
+ private waiters: Map<string,{ resolve: solveCallback, reject: ( e: any ) => void }[]>;
31
31
 
32
32
  constructor( ) {
33
33
  this.cache = new Map( );
@@ -37,26 +37,35 @@ class SvgLoader {
37
37
  async load( file: string ): Promise<string> {
38
38
 
39
39
  if( this.cache.has(file) ) {
40
- return Promise.resolve( this.cache.get(file) );
40
+ // null: the file was not found, it is not asked again
41
+ const svg = this.cache.get( file );
42
+ if( svg===null ) {
43
+ return Promise.reject( new Error( `file not found: ${file}` ) );
44
+ }
45
+
46
+ return Promise.resolve( svg );
41
47
  }
42
48
 
43
49
  return new Promise( (resolve,reject) => {
50
+ // already asked: the same answer
44
51
  if( this.waiters.has(file) ) {
45
- this.waiters.get(file).push( resolve );
46
- }
47
- else {
48
- this.waiters.set( file, [resolve] );
49
- this._load( file )
50
- .then( ( data: string ) => {
51
- this.cache.set( file, data );
52
- const ww = this.waiters.get( file );
53
- ww.forEach( cb => cb(data ) );
54
- })
55
- .catch( e => {
56
- this.cache.set( file, null );
57
- reject( e );
58
- } )
52
+ this.waiters.get(file).push( { resolve, reject } );
53
+ return;
59
54
  }
55
+
56
+ this.waiters.set( file, [{ resolve, reject }] );
57
+ this._load( file )
58
+ .then( ( data: string ) => {
59
+ this.cache.set( file, data );
60
+ this.waiters.get( file ).forEach( w => w.resolve( data ) );
61
+ this.waiters.delete( file );
62
+ })
63
+ .catch( e => {
64
+ // kept as not found: it is not asked again
65
+ this.cache.set( file, null );
66
+ this.waiters.get( file ).forEach( w => w.reject( e ) );
67
+ this.waiters.delete( file );
68
+ } );
60
69
  });
61
70
  }
62
71
 
@@ -83,6 +92,8 @@ export interface IconProps extends ComponentProps {
83
92
  /**
84
93
  * Identifier for the icon.
85
94
  * Can be a URL, a CSS variable (prefixed with `var:`), or inline SVG data.
95
+ * `var:home` (or `var:--home`) is the variable --home set on <html> by the code:
96
+ * document.documentElement.style.setProperty( "--home", svg ).
86
97
  * @example
87
98
  * // Using a CSS variable
88
99
  * { iconId: "var:home" }
@@ -139,8 +150,13 @@ export class Icon extends Component<IconProps> {
139
150
  if( iconId ) {
140
151
  if( iconId.startsWith('var:') ) {
141
152
  do {
142
- const path = iconId.substring( 4 );
143
- iconId = document.documentElement.style.getPropertyValue( path );
153
+ // "var:home" or "var:--home": the variable --home, set on <html>
154
+ let name = iconId.substring( 4 );
155
+ if( !name.startsWith( "--" ) ) {
156
+ name = "--" + name;
157
+ }
158
+
159
+ iconId = document.documentElement.style.getPropertyValue( name );
144
160
  } while( iconId.startsWith('var:') );
145
161
  }
146
162
 
@@ -239,6 +239,14 @@ function accentLabel( layout: KbLayout, upper: boolean ): string {
239
239
  return ( upper ? upperKeys( label ) : label ).replace( " ", "" );
240
240
  }
241
241
 
242
+ /**
243
+ * the keys that draw the keyboard again: shift, 123, abc, accents, signs
244
+ */
245
+
246
+ function redrawKey( key: number ) {
247
+ return key==4 || key==5 || key==8 || key==10 || key==11;
248
+ }
249
+
242
250
 
243
251
  const RE_sel = /text|password|search|tel|url/i;
244
252
 
@@ -248,6 +256,12 @@ interface KeyboardProps extends BoxProps {
248
256
  * "de-CH", "pl"... "fr-FR" when it is not given
249
257
  */
250
258
  locale?: string;
259
+
260
+ /**
261
+ * on a number or date field, the keyboard stays on the pad: no "abc" key
262
+ * back to the letters. The other fields keep all the keys.
263
+ */
264
+ numpadOnly?: boolean;
251
265
  }
252
266
 
253
267
  /**
@@ -284,15 +298,20 @@ export class Keyboard extends HBox<KeyboardProps>
284
298
 
285
299
  this.hide( );
286
300
 
301
+ // a key: each contact, a finger or the mouse, the second tap of a quick double tap too.
302
+ // not the keys that draw the keyboard again, they act on the click
303
+ this.addDOMEvent( "pointerdown", (e) => {
304
+ this.handleKeyEvent( e );
305
+ });
306
+
307
+ // the focus stays in the field
287
308
  this.addDOMEvent( "mousedown", (e) => {
288
- this.handleKeyEvent( e );
289
309
  e.preventDefault( );
290
310
  e.stopPropagation( );
291
311
  });
292
312
 
293
- // for rapid people
313
+ // a double click or a double tap: its two pointerdown gave the two keys
294
314
  this.addDOMEvent( "dblclick", (e) => {
295
- this.handleKeyEvent( e );
296
315
  e.preventDefault( );
297
316
  e.stopPropagation( );
298
317
  });
@@ -348,7 +367,7 @@ export class Keyboard extends HBox<KeyboardProps>
348
367
  target = target.parentNode as HTMLElement;
349
368
  }
350
369
 
351
- if( !key ) {
370
+ if( !key || redrawKey( key ) ) {
352
371
  return;
353
372
  }
354
373
 
@@ -627,6 +646,11 @@ export class Keyboard extends HBox<KeyboardProps>
627
646
 
628
647
  if( this.mode=="number" || this.mode=="date" ) {
629
648
  lines = kb_pads[this.mode];
649
+
650
+ // no key back to the letters: its place stays empty
651
+ if( this.props.numpadOnly ) {
652
+ lines = lines.map( line => line.replace( "{8}", "{9}" ) );
653
+ }
630
654
  }
631
655
  else if( this.mode=="accents" ) {
632
656
  lines = accentLines( layout, this.upper );
@@ -761,7 +785,9 @@ export class Keyboard extends HBox<KeyboardProps>
761
785
  icon,
762
786
  autorepeat: repeat,
763
787
  click: ( e ) => {
764
- if( e.repeat ) {
788
+ // the keys that draw the keyboard again act on the click: the mousedown
789
+ // that follows a pointerdown would go to a key no longer in the page
790
+ if( e.repeat || redrawKey( key ) ) {
765
791
  this._handleKey( key )
766
792
  }
767
793
  }
@@ -56,11 +56,14 @@ export class Link extends Component<LinkProps,LinkEvents> {
56
56
 
57
57
  this.mapPropEvents( props, "click" );
58
58
 
59
- this.setContent( new Label( {
60
- text: props.text,
61
- icon: props.icon,
62
- align: props.align,
63
- } ) );
59
+ // content given: it is the link; else a label (icon and text)
60
+ if( props.content===undefined ) {
61
+ this.setContent( new Label( {
62
+ text: props.text,
63
+ icon: props.icon,
64
+ align: props.align,
65
+ } ) );
66
+ }
64
67
 
65
68
  this.addDOMEvent('click', (e) => this._on_click(e));
66
69
  }
@@ -71,7 +74,14 @@ export class Link extends Component<LinkProps,LinkEvents> {
71
74
  */
72
75
 
73
76
  setText( text: string | UnsafeHtml ) {
74
- this.setContent( text );
77
+ // the text of the label: its icon and alignment stay
78
+ const label = this.query<Label>( ":scope > .x4label" );
79
+ if( label ) {
80
+ label.setText( text );
81
+ }
82
+ else {
83
+ this.setContent( text );
84
+ }
75
85
  }
76
86
 
77
87
  /**
@@ -83,7 +93,8 @@ export class Link extends Component<LinkProps,LinkEvents> {
83
93
  const xev: EvClick = {context: this.props.href };
84
94
  this.fire('click', xev );
85
95
 
86
- if( xev.preventDefault ) {
96
+ // the handler called ev.preventDefault( ): the link is not followed
97
+ if( xev.defaultPrevented ) {
87
98
  ev.preventDefault();
88
99
  ev.stopPropagation();
89
100
  }
@@ -66,7 +66,7 @@ export class MessageBox extends Dialog<DialogProps>
66
66
  content: [
67
67
  new HBox( {
68
68
  content: [
69
- new Icon( { iconId: error_icon }),
69
+ new Icon( { iconId: icon }),
70
70
  new Label( { text: msg } ),
71
71
  ]
72
72
  }),
@@ -39,7 +39,7 @@ interface PanelProps extends ComponentProps {
39
39
  @class_ns( "x4" )
40
40
  export class Panel extends VBox<PanelProps> {
41
41
 
42
- private _title: Component;
42
+ private _title: Label;
43
43
  private _body: Component;
44
44
 
45
45
  constructor( props: PanelProps ) {
@@ -57,6 +57,7 @@ export class Panel extends VBox<PanelProps> {
57
57
  }
58
58
 
59
59
  setTitle( title: string | UnsafeHtml ) {
60
- this._title.setContent( title )
60
+ // the text of the label: its icon stays
61
+ this._title.setText( title );
61
62
  }
62
63
  }