x4js 2.3.16 → 2.3.18

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.
@@ -410,7 +410,12 @@ export class Input extends Component<InputProps,InputEvents> {
410
410
  */
411
411
 
412
412
  public setNumValue( value: number, ndec = -1 ) {
413
-
413
+
414
+ // no number (a field that is not filled): the field is emptied
415
+ if( value===undefined || value===null || isNaN( value ) ) {
416
+ return this.setValue( "" );
417
+ }
418
+
414
419
  if( ndec==-2 && this.props.type=='number' ) {
415
420
  const p = this.props as NumberProps;
416
421
 
@@ -28,75 +28,166 @@ import icon_hide from "./eye-slash.svg";
28
28
 
29
29
 
30
30
 
31
- const kb_def: any = {
32
- 'fr-FR': {
33
- lines: {
34
- lower: [
35
- "1 2 3 4 5 6 7 8 9 0",
36
- "a z e r t y u i o p {2}",
37
- "q s d f g h j k l m {3}",
38
- "{4} w x c v b n , . {4}",
39
- "{5} {6} ' {7}"
40
- ],
41
- upper: [
42
- "! @ # $ % ^ & * ( ) _ +",
43
- "A Z E R T Y U I O P {2}",
44
- "Q S D F G H J K L M {3}",
45
- "{4} W X C V B N ? : {4}",
46
- "{5} {6} ' {7}",
47
- ],
48
- number: [
49
- "1 2 3 {2}",
50
- "4 5 6 {8}",
51
- "7 8 9 {9}",
52
- "0 . {3} {9}"
53
- ],
54
- date: [
55
- "1 2 3 {2}",
56
- "4 5 6 {8}",
57
- "7 8 9 {9}",
58
- "0 / {3} {9}"
59
- ]
60
- }
61
- },
62
- 'en-GB': {
63
- lines: {
64
- lower: [
65
- "1 2 3 4 5 6 7 8 9 0",
66
- "a z e r t y u i o p {2}",
67
- "q s d f g h j k l m {3}",
68
- "{4} w x c v b n , . {4}",
69
- "{5} {6} ' {7}"
70
- ],
71
- upper: [
72
- "! @ # $ % ^ & * ( ) _ +",
73
- "A Z E R T Y U I O P {2}",
74
- "Q S D F G H J K L M {3}",
75
- "{4} W X C V B N ? : {4}",
76
- "{5} {6} ' {7}",
77
- ],
78
- number: [
79
- "1 2 3 {2}",
80
- "4 5 6 {8}",
81
- "7 8 9 {9}",
82
- "0 . {3} {9}"
83
- ],
84
- date: [
85
- "1 2 3 {2}",
86
- "4 5 6 {8}",
87
- "7 8 9 {9}",
88
- "0 / {3} {9}"
89
- ]
90
- }
91
- },
92
- 'en-US': 'en-GB'
31
+ /**
32
+ * A layout is the place of the 26 letters (its base) and the letters that
33
+ * a language adds to them (its extra), shown on rows of their own. The
34
+ * upper case is computed, the digits and the signs are the same for all.
35
+ *
36
+ * base: the countries where it is not QWERTY are those that
37
+ * https://en.wikipedia.org/wiki/QWERTZ and /AZERTY list.
38
+ * extra: the main letters of the language in the CLDR of Unicode
39
+ * (cldr-json, cldr-misc-full/main/<lang>/characters.json), without a..z.
40
+ */
41
+
42
+ type KbBase = "azerty" | "qwerty" | "qwertz";
43
+
44
+ interface KbLayout {
45
+ base: KbBase;
46
+ extra: string;
47
+ }
48
+
49
+ const kb_bases: Record<KbBase,string[]> = {
50
+ azerty: [ "a z e r t y u i o p", "q s d f g h j k l m", "w x c v b n" ],
51
+ qwerty: [ "q w e r t y u i o p", "a s d f g h j k l", "z x c v b n m" ],
52
+ qwertz: [ "q w e r t z u i o p", "a s d f g h j k l", "y x c v b n m" ],
53
+ };
54
+
55
+ // by language
56
+ const kb_langs: Record<string,KbLayout> = {
57
+ cs: { base: "qwertz", extra: "á č ď é ě í ň ó ř š ť ú ů ý ž" },
58
+ da: { base: "qwerty", extra: "æ ø å" },
59
+ de: { base: "qwertz", extra: "ä ö ü ß" },
60
+ en: { base: "qwerty", extra: "" },
61
+ es: { base: "qwerty", extra: "á é í ñ ó ú ü" },
62
+ et: { base: "qwerty", extra: "õ ä ö ü š ž" },
63
+ fi: { base: "qwerty", extra: "å ä ö š ž" },
64
+ fr: { base: "azerty", extra: "é è ê ë à â æ ç î ï ô œ ù û ü ÿ" },
65
+ ga: { base: "qwerty", extra: "á é í ó ú" },
66
+ hr: { base: "qwertz", extra: "č ć đ š ž" },
67
+ hu: { base: "qwertz", extra: "á é í ó ö ő ú ü ű" },
68
+ it: { base: "qwerty", extra: "à è é ì ò ù" },
69
+ lt: { base: "qwerty", extra: "ą č ę ė į š ų ū ž" },
70
+ lv: { base: "qwerty", extra: "ā č ē ģ ī ķ ļ ņ š ū ž" },
71
+ mt: { base: "qwerty", extra: "à ċ è ġ ħ ì ò ù ż" },
72
+ nl: { base: "qwerty", extra: "á ä é ë í ï ó ö ú ü" },
73
+ pl: { base: "qwerty", extra: "ą ć ę ł ń ó ś ź ż" },
74
+ pt: { base: "qwerty", extra: "á à â ã ç é ê í ó ò ô õ ú" },
75
+ ro: { base: "qwerty", extra: "ă â î ș ț" },
76
+ sk: { base: "qwertz", extra: "á ä č ď é í ĺ ľ ň ó ô ŕ š ť ú ý ž" },
77
+ sl: { base: "qwertz", extra: "č š ž" },
78
+ sv: { base: "qwerty", extra: "å ä ö é à" },
79
+ };
80
+
81
+ // the countries that do not have the keyboard of their language
82
+ const kb_countries: Record<string,Partial<KbLayout>> = {
83
+ "de-CH": { extra: "ä ö ü" }, // no ß in Switzerland
84
+ "fr-CH": { base: "qwertz" },
85
+ "it-CH": { base: "qwertz" },
86
+ "fr-LU": { base: "qwertz" }, // the Swiss French keyboard
87
+ "nl-BE": { base: "azerty" },
88
+ };
89
+
90
+ const kb_digits = "1 2 3 4 5 6 7 8 9 0";
91
+ const kb_signs = "! @ # $ % ^ & * ( ) _ +";
92
+
93
+ const kb_pads = {
94
+ number: [
95
+ "1 2 3 {2}",
96
+ "4 5 6 {8}",
97
+ "7 8 9 {9}",
98
+ "0 . {3} {9}"
99
+ ],
100
+ date: [
101
+ "1 2 3 {2}",
102
+ "4 5 6 {8}",
103
+ "7 8 9 {9}",
104
+ "0 / {3} {9}"
105
+ ]
106
+ };
107
+
108
+ const KB_MAX_EXTRA = 12; // keys on a row of extra letters, as many as there are signs
109
+
110
+ /**
111
+ * "de-CH", "de_ch", "de": the layout of the country, or else the one of
112
+ * its language. A language that is not known gets a QWERTY.
113
+ */
114
+
115
+ function findLayout( locale: string ): KbLayout {
116
+ const [lang, country] = locale.replace( "_", "-" ).split( "-" );
117
+
118
+ let layout = kb_langs[lang.toLowerCase( )];
119
+ if( !layout ) {
120
+ layout = kb_langs.en;
121
+ }
122
+
123
+ if( country ) {
124
+ const own = kb_countries[lang.toLowerCase( ) + "-" + country.toUpperCase( )];
125
+ if( own ) {
126
+ layout = { ...layout, ...own };
127
+ }
128
+ }
129
+
130
+ return layout;
131
+ }
132
+
133
+ function upperKeys( line: string ): string {
134
+ return line.split( " " ).map( key => {
135
+ const up = key.toUpperCase( );
136
+ return up.length===1 ? up : key; // ß has no upper case of one letter
137
+ } ).join( " " );
138
+ }
139
+
140
+ /**
141
+ * the extra letters, on rows of the same length
142
+ */
143
+
144
+ function extraLines( extra: string ): string[] {
145
+ if( !extra ) {
146
+ return [];
147
+ }
148
+
149
+ const keys = extra.split( " " );
150
+ const count = Math.ceil( keys.length / KB_MAX_EXTRA );
151
+ const size = Math.ceil( keys.length / count );
152
+
153
+ const lines: string[] = [];
154
+ for( let i=0; i<keys.length; i+=size ) {
155
+ lines.push( keys.slice( i, i+size ).join( " " ) );
156
+ }
157
+
158
+ return lines;
159
+ }
160
+
161
+ function letterLines( layout: KbLayout, upper: boolean ): string[] {
162
+ let [l1, l2, l3] = kb_bases[layout.base];
163
+ let extra = extraLines( layout.extra );
164
+
165
+ if( upper ) {
166
+ l1 = upperKeys( l1 );
167
+ l2 = upperKeys( l2 );
168
+ l3 = upperKeys( l3 );
169
+ extra = extra.map( upperKeys );
170
+ }
171
+
172
+ return [
173
+ upper ? kb_signs : kb_digits,
174
+ l1 + " {2}",
175
+ l2 + " {3}",
176
+ "{4} " + l3 + ( upper ? " ? : {4}" : " , . {4}" ),
177
+ ...extra,
178
+ "{5} {6} ' {7}",
179
+ ];
93
180
  }
94
181
 
95
182
 
96
183
  const RE_sel = /text|password|search|tel|url/i;
97
184
 
98
185
  interface KeyboardProps extends BoxProps {
99
-
186
+ /**
187
+ * the language of the keys, with or without its country: "fr-FR",
188
+ * "de-CH", "pl"... "fr-FR" when it is not given
189
+ */
190
+ locale?: string;
100
191
  }
101
192
 
102
193
  /**
@@ -120,7 +211,7 @@ export class Keyboard extends HBox<KeyboardProps>
120
211
  super( { ...props, id: 'v-keyboard' } );
121
212
 
122
213
  this.mode = 'lower';
123
- this.locale = 'fr-FR';
214
+ this.locale = props.locale ?? 'fr-FR';
124
215
  this.visible = false;
125
216
 
126
217
  document.addEventListener( 'focusin', (e) => this.handleFocus(e.target as Element,true), false );
@@ -152,6 +243,24 @@ export class Keyboard extends HBox<KeyboardProps>
152
243
  this.setStyleVariable( '--keyboard-zoom', perc+'%' );
153
244
  }
154
245
 
246
+ /**
247
+ * Change the language of the keys.
248
+ * @param locale - "fr-FR", "de-CH", "pl"... A country that has no keyboard
249
+ * of its own gets the one of its language, a language that is not known a QWERTY.
250
+ *
251
+ * @example
252
+ * keyboard.setLocale( "de-CH" );
253
+ */
254
+
255
+ setLocale( locale: string ) {
256
+ this.locale = locale;
257
+
258
+ // drawn at least once
259
+ if( this.keyboard ) {
260
+ this._redraw( );
261
+ }
262
+ }
263
+
155
264
  /**
156
265
  *
157
266
  */
@@ -436,7 +545,14 @@ export class Keyboard extends HBox<KeyboardProps>
436
545
  */
437
546
 
438
547
  private _createContent( ) {
439
- let lines = kb_def[this.locale].lines[this.mode];
548
+ let lines: string[];
549
+ if( this.mode=="number" || this.mode=="date" ) {
550
+ lines = kb_pads[this.mode];
551
+ }
552
+ else {
553
+ lines = letterLines( findLayout( this.locale ), this.mode=="upper" );
554
+ }
555
+
440
556
  let result: Box[] = [];
441
557
 
442
558
  for( let j=0; j<lines.length; j++ ) {
@@ -18,7 +18,11 @@
18
18
 
19
19
  :root {
20
20
  --notification-border: var( --border );
21
- --notification-background: white;
21
+ --notification-background: var( --background-primary );
22
+
23
+ --notification-title-color: var( --text-primary );
24
+ --notification-text-color: var( --text-ternary );
25
+ --notification-button-color: var( --text-ternary );
22
26
 
23
27
  --notification-icon-default: var( --accent-background );
24
28
  --notification-icon-success: var( --success-background );
@@ -54,11 +58,12 @@
54
58
  .title {
55
59
  font-weight: bold;
56
60
  padding: 6px 6px 3px 6px;
61
+ color: var( --notification-title-color );
57
62
  }
58
63
 
59
64
  .text {
60
65
  padding: 3px 6px 6px 6px;
61
- color: var( --color-gray-7 );
66
+ color: var( --notification-text-color );
62
67
  }
63
68
  }
64
69
 
@@ -81,7 +86,7 @@
81
86
 
82
87
  .x4button {
83
88
  height: 24px;
84
- color: var( --color-gray-7 );
89
+ color: var( --notification-button-color );
85
90
  border: none;
86
91
 
87
92
  //&:hover {
@@ -52,6 +52,9 @@ interface NotificationProps extends ComponentProps {
52
52
  * ```
53
53
  * --notification-border
54
54
  * --notification-background
55
+ * --notification-title-color
56
+ * --notification-text-color
57
+ * --notification-button-color
55
58
  * --notification-icon-default
56
59
  * --notification-icon-success
57
60
  * --notification-icon-danger
@@ -21,8 +21,8 @@ import "./progress.module.scss";
21
21
 
22
22
  interface ProgressProps extends ComponentProps {
23
23
  value: number;
24
- min: number;
25
- max: number;
24
+ min?: number; // 0 when missing
25
+ max?: number; // 100 when missing
26
26
  }
27
27
 
28
28
  /**
@@ -46,7 +46,11 @@ export class Progress extends Component<ProgressProps> {
46
46
  }
47
47
 
48
48
  setValue( value: number ) {
49
- const perc = value / (this.props.max-this.props.min) * 100;
49
+ const min = this.props.min ?? 0;
50
+ const max = this.props.max ?? 100;
51
+
52
+ // where the value is between the two: min is 0%, max is 100%
53
+ const perc = (value-min) / (max-min) * 100;
50
54
  this._bar.setStyleValue( "width", perc+"%" );
51
55
  }
52
56
  }
@@ -676,7 +676,9 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
676
676
  const sizer = new CSizer("right");
677
677
 
678
678
  sizer.on("stop", () => {
679
+ // the flex columns took what was left: the body gets the width of its columns
679
680
  this._updateFlexs();
681
+ this._computeFullSize();
680
682
  })
681
683
 
682
684
  sizer.on("resize", (ev) => {
@@ -698,6 +700,24 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
698
700
  }
699
701
  })
700
702
 
703
+ // a double click on the sizer: the column takes its share of the place
704
+ // that is left (flex 1). To stop taking it, the sizer is dragged
705
+ sizer.addDOMEvent("dblclick", (ev) => {
706
+ // both: the grid has a double click of its own
707
+ ev.stopPropagation();
708
+ ev.preventDefault();
709
+
710
+ // a fixed column is not in the place that is shared
711
+ if (fixed || cdata.flex) {
712
+ return;
713
+ }
714
+
715
+ cdata.flex = 1;
716
+
717
+ this._updateFlexs();
718
+ this._computeFullSize();
719
+ });
720
+
701
721
  const cell = new Component({
702
722
  cls: `cell`,
703
723
  attrs: { "data-col": col },
@@ -956,6 +976,7 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
956
976
 
957
977
  const el = new Component({
958
978
  cls: "cell",
979
+ attrs: { "data-col": col }, // the sizer of the column finds its cells by it
959
980
  style: { width: cdata?.width ? cdata.width + "px" : undefined, justifyContent: align },
960
981
  content
961
982
  });
@@ -993,7 +1014,12 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
993
1014
  for (let x = 0; x < ccount; x++) {
994
1015
  const cdata = this._getCol(x);
995
1016
 
996
- if (!cdata.fixed && cdata.flex) {
1017
+ // the fixed columns are not in the viewport: their width is not taken from its own
1018
+ if (cdata.fixed) {
1019
+ continue;
1020
+ }
1021
+
1022
+ if (cdata.flex) {
997
1023
  flexc += cdata.flex;
998
1024
  }
999
1025
  else {
@@ -28,6 +28,7 @@ interface SwitchProps extends ComponentProps {
28
28
  label: string;
29
29
  checked?: boolean;
30
30
  value?: string;
31
+ name?: string; // with it, a Form reads and writes the switch (a boolean)
31
32
  }
32
33
 
33
34
  /**
@@ -52,7 +53,7 @@ export class Switch extends HBox<SwitchProps> {
52
53
  new Component( {
53
54
  cls: "switch",
54
55
  content: [
55
- new Input( { type: "checkbox", id: inputId, checked: props.checked } ),
56
+ new Input( { type: "checkbox", id: inputId, name: props.name, checked: props.checked } ),
56
57
  new Component( { cls: "track" } ),
57
58
  new Component( { cls: "thumb" } ),
58
59
  ]
@@ -34,8 +34,7 @@ export interface TagProps extends ComponentProps {
34
34
  * @cssvar
35
35
  * ```
36
36
  * --tag-border
37
- * --tag-border-focus
38
- * --tag-selection
37
+ * --tag-background
39
38
  * --tag-color
40
39
  * --tag-icon-color
41
40
  * ```
@@ -39,6 +39,12 @@
39
39
  color: white;
40
40
  }
41
41
 
42
+ // as the one of an input
43
+ &::placeholder {
44
+ color: var( --input-placeholder );
45
+ font-style: italic;
46
+ }
47
+
42
48
  &[resize=false] {
43
49
  resize: none;
44
50
  }
@@ -17,7 +17,7 @@
17
17
  **/
18
18
 
19
19
  import { BaseProps } from '../input/input';
20
- import { Component, ComponentProps } from '../../core/component';
20
+ import { Component, ComponentEvents, ComponentProps, EvChange, EvFocus } from '../../core/component';
21
21
 
22
22
  import { Label } from '../label/label';
23
23
  import { VBox } from '../boxes/boxes';
@@ -29,25 +29,35 @@ import { class_ns, IFormElement } from '../../core/core_tools';
29
29
  *
30
30
  */
31
31
 
32
- interface TextAreaProps extends BaseProps {
33
- label?: string;
32
+ export interface SimpleTextAreaProps extends BaseProps {
34
33
  value?: string;
35
34
  resize?: boolean | "none" | "vertical" | "horizontal" | "both";
36
35
  readonly?: boolean;
37
36
  trim?: boolean;
38
37
  }
39
38
 
39
+ export interface TextAreaProps extends SimpleTextAreaProps {
40
+ label?: string;
41
+ }
42
+
43
+ export interface TextAreaEvents extends ComponentEvents {
44
+ focus: EvFocus;
45
+ change: EvChange;
46
+ }
47
+
40
48
 
41
49
  /**
42
- *
50
+ *
43
51
  */
44
52
 
45
53
  @class_ns( "x4" )
46
- export class SimpleTextArea extends Component<TextAreaProps> {
54
+ export class SimpleTextArea extends Component<SimpleTextAreaProps,TextAreaEvents> {
47
55
 
48
- constructor( props: TextAreaProps ) {
56
+ constructor( props: SimpleTextAreaProps ) {
49
57
  super( { ...props, tag: "textarea" } );
50
58
 
59
+ this.mapPropEvents( props, "focus", "change" );
60
+
51
61
  this.setAttribute( "name", props.name );
52
62
  if( props.value ) {
53
63
  this.setText( props.value+'' );
@@ -60,6 +70,22 @@ export class SimpleTextArea extends Component<TextAreaProps> {
60
70
  if( props.readonly ) {
61
71
  this.setAttribute( "readonly", true );
62
72
  }
73
+
74
+ if( props.placeholder ) {
75
+ this.setAttribute( "placeholder", props.placeholder );
76
+ }
77
+
78
+ if( props.required ) {
79
+ this.setAttribute( "required", true );
80
+ }
81
+
82
+ if( props.autofocus ) {
83
+ this.setAttribute( "autofocus", true );
84
+ }
85
+
86
+ this.addDOMEvent( "blur", ( ) => this.fire( "focus", { focus_out: true } ) );
87
+ this.addDOMEvent( "focus", ( ) => this.fire( "focus", { focus_out: false } ) );
88
+ this.addDOMEvent( "input", ( ) => this.fire( "change", { value: this.getText( ) } ) );
63
89
  }
64
90
 
65
91
  setText( text: string ) {
@@ -79,12 +105,18 @@ export class SimpleTextArea extends Component<TextAreaProps> {
79
105
  this.dom.scrollTop = this.dom.scrollHeight;
80
106
  }
81
107
 
108
+ /** false when it is required and empty */
109
+
110
+ isValid( ) {
111
+ return !this.props.required || this.getText( )!=="";
112
+ }
113
+
82
114
  queryInterface<T>(name: string): T {
83
115
  if( name=="form-element" ) {
84
116
  const i: IFormElement = {
85
117
  getRawValue: ( ): any => { return this.getText(); },
86
118
  setRawValue: ( v: any ) => { this.setText( v ); },
87
- isValid: ( ) => { return true; }
119
+ isValid: ( ) => { return this.isValid( ); }
88
120
  };
89
121
 
90
122
  return i as unknown as T;
@@ -115,11 +147,11 @@ export class TextArea extends VBox {
115
147
  super( props );
116
148
 
117
149
 
118
- const { label, value, resize, readonly, trim, name } = props;
150
+ const { value, resize, readonly, trim, name, placeholder, required, autofocus, focus, change } = props;
119
151
 
120
152
  this.setContent( [
121
153
  new Label( { text: props.label }),
122
- this._input = new SimpleTextArea( { label,value,resize,readonly,trim,name } )
154
+ this._input = new SimpleTextArea( { value,resize,readonly,trim,name,placeholder,required,autofocus,focus,change } )
123
155
  ])
124
156
  }
125
157
 
@@ -135,12 +167,18 @@ export class TextArea extends VBox {
135
167
  this._input.scrollToBottom( );
136
168
  }
137
169
 
170
+ /** false when it is required and empty */
171
+
172
+ isValid( ) {
173
+ return this._input.isValid( );
174
+ }
175
+
138
176
  queryInterface<T>(name: string): T {
139
177
  if( name=="form-element" ) {
140
178
  const i: IFormElement = {
141
179
  getRawValue: ( ): any => { return this.getText(); },
142
180
  setRawValue: ( v: any ) => { this.setText( v ); },
143
- isValid: ( ) => { return true; }
181
+ isValid: ( ) => { return this.isValid( ); }
144
182
  };
145
183
 
146
184
  return i as unknown as T;
@@ -234,7 +234,7 @@ export class Component<P extends ComponentProps = ComponentProps, E extends Comp
234
234
  this.setStyle( props.style );
235
235
  }
236
236
 
237
- if( props.content ) {
237
+ if( props.content || props.content===0 ) {
238
238
  this.setContent( props.content );
239
239
  }
240
240
 
@@ -321,7 +321,7 @@ export class Component<P extends ComponentProps = ComponentProps, E extends Comp
321
321
  }
322
322
 
323
323
  if( cls.indexOf(' ')>=0 ) {
324
- const ccs = cls.split( " " );
324
+ const ccs = cls.split( " " ).filter( x => x );
325
325
  this.dom.classList.remove(...ccs);
326
326
  }
327
327
  else {
@@ -358,7 +358,7 @@ export class Component<P extends ComponentProps = ComponentProps, E extends Comp
358
358
  }
359
359
 
360
360
  if( cls.indexOf(' ')>=0 ) {
361
- const ccs = cls.split( " " );
361
+ const ccs = cls.split( " " ).filter( x => x );
362
362
  ccs.forEach( toggle );
363
363
  }
364
364
  else {
@@ -897,7 +897,8 @@ export class Component<P extends ComponentProps = ComponentProps, E extends Comp
897
897
 
898
898
  /**
899
899
  * Enables or disables the component.
900
- * This sets the `disabled` attribute and also propagates the disabled state to child input elements.
900
+ * This sets or removes the `disabled` and `inert` attributes of the component itself. Its children
901
+ * are not changed: the browser makes all that is inside an `inert` element inactive.
901
902
  * @param ena - If `true`, the component is enabled; if `false`, it's disabled. Defaults to `true`.
902
903
  * @returns The component instance for chaining.
903
904
  */
@@ -1195,8 +1196,8 @@ export class Component<P extends ComponentProps = ComponentProps, E extends Comp
1195
1196
  if( !this.#shortcuts ) {
1196
1197
  const shortcuts = this.#shortcuts = new Shortcuts( );
1197
1198
 
1198
- // straight on the element, not with addDOMEvent: the handlers of the
1199
- // children are called exactly as before
1199
+ // straight on the element, not with addDOMEvent: it does not change
1200
+ // how the handlers of the children are called
1200
1201
  this.dom.addEventListener( "keydown", ( ev ) => shortcuts.handle( ev as KeyboardEvent ) );
1201
1202
  this.dom.addEventListener( "focusin", ( ) => Application.instance( ).setActiveShortcuts( this, shortcuts ) );
1202
1203
  }
@@ -1230,11 +1231,17 @@ export class Component<P extends ComponentProps = ComponentProps, E extends Comp
1230
1231
 
1231
1232
  const state = makeState( raw );
1232
1233
 
1233
- // the state is what is saved, not raw: makeState works on a copy of it
1234
+ // the state is what is saved, not raw: makeState works on a copy of it.
1235
+ // The write waits for the changes to stop. Its timer is the one of the
1236
+ // browser, not this.setTimeout: the timers of a component are cleared
1237
+ // when it leaves the DOM, and what changed just before must still be written
1238
+ let timer: ReturnType<typeof setTimeout>;
1239
+
1234
1240
  state.on( "change", ( ) => {
1235
- this.setTimeout( key, 500, ( ) => {
1241
+ clearTimeout( timer );
1242
+ timer = setTimeout( ( ) => {
1236
1243
  Application.instance().setStorage( key, JSON.stringify( state ) );
1237
- } );
1244
+ }, 500 );
1238
1245
  });
1239
1246
 
1240
1247
  this.#pstate[name] = state;