x4js 2.3.18 → 2.3.20

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 (38) hide show
  1. package/aicli.md +3 -3
  2. package/aicontext.md +51 -23
  3. package/cli/config.mjs +14 -7
  4. package/cli/copy-plugin.mjs +8 -0
  5. package/cli/create.mjs +8 -2
  6. package/cli/dev.mjs +4 -1
  7. package/cli/less-plugin.mjs +5 -1
  8. package/cli/main.mjs +15 -5
  9. package/cli/rawfile-plugin.mjs +23 -3
  10. package/cli/templates.mjs +5 -1
  11. package/package.json +1 -1
  12. package/src/components/calendar/calendar.module.scss +16 -1
  13. package/src/components/calendar/calendar.ts +52 -9
  14. package/src/components/checkbox/checkbox.ts +1 -0
  15. package/src/components/combobox/combobox.ts +51 -23
  16. package/src/components/dialog/dialog.ts +4 -1
  17. package/src/components/form/form.ts +9 -5
  18. package/src/components/gridview/gridview.ts +2 -1
  19. package/src/components/input/input.ts +63 -38
  20. package/src/components/keyboard/keyboard.module.scss +35 -4
  21. package/src/components/keyboard/keyboard.ts +120 -21
  22. package/src/components/listbox/listbox.ts +6 -1
  23. package/src/components/messages/messages.ts +12 -3
  24. package/src/components/popup/popup.ts +19 -20
  25. package/src/components/radio/radio.ts +8 -4
  26. package/src/components/rating/rating.ts +38 -48
  27. package/src/components/slider/slider.ts +38 -8
  28. package/src/components/spreadsheet/spreadsheet.ts +140 -131
  29. package/src/components/switch/switch.ts +2 -1
  30. package/src/components/textarea/textarea.module.scss +2 -4
  31. package/src/components/textarea/textarea.ts +2 -1
  32. package/src/components/textedit/textedit.ts +12 -2
  33. package/src/core/core_application.ts +8 -4
  34. package/src/core/core_data.ts +64 -26
  35. package/src/core/core_dom.ts +9 -1
  36. package/src/core/core_pdf.ts +1200 -46
  37. package/src/core/core_router.ts +42 -44
  38. package/src/core/core_state.ts +22 -5
@@ -144,30 +144,15 @@ export class Combobox extends Component<ComboboxProps,ComboboxEvents> {
144
144
  }
145
145
 
146
146
  this._popup = new DropdownList( { items: props.items, renderer: props.renderer } );
147
- const list = this._popup.getList( );
148
-
149
- const _select = ( sel: ListboxID ) => {
150
- const itm = list.getItem(sel);
151
- if( itm ) {
152
- list.select( sel, false );
153
- this._input.setValue( itm.text as string );
154
- }
155
-
156
- if( !this._prevent_close ) {
157
- this._popup.show( false );
158
- }
159
- }
160
-
161
147
  this._popup.on( "click", ( ev ) => {
162
148
  const sel = ev.context as ListboxID;
163
149
  if( sel!==undefined ) { // no empty sel
164
- _select( sel );
165
- this.fire( "selectionChange", { selection: [sel], empty: false } );
150
+ this._choose( sel, true );
166
151
  }
167
152
  });
168
153
 
169
154
  if( props.value!==undefined && props.value!==null ) {
170
- _select( props.value );
155
+ this._choose( props.value, false );
171
156
  }
172
157
 
173
158
  this._button.addDOMEvent( "click", ( ) => this._on_click( ) );
@@ -180,6 +165,27 @@ export class Combobox extends Component<ComboboxProps,ComboboxEvents> {
180
165
  })
181
166
  }
182
167
 
168
+ /**
169
+ * the item chosen: by a click, by the arrows (the popup stays open), or by the props
170
+ */
171
+
172
+ private _choose( sel: ListboxID, notify: boolean ) {
173
+ const list = this._getList( );
174
+ const itm = list.getItem( sel );
175
+ if( itm ) {
176
+ list.select( sel, false );
177
+ this._input.setValue( itm.text as string );
178
+ }
179
+
180
+ if( !this._prevent_close ) {
181
+ this._popup.show( false );
182
+ }
183
+
184
+ if( notify ) {
185
+ this.fire( "selectionChange", { selection: [sel], empty: false } );
186
+ }
187
+ }
188
+
183
189
  private _on_key( ev: KeyboardEvent ) {
184
190
  switch( ev.key ) {
185
191
  case "Enter":
@@ -197,7 +203,13 @@ export class Combobox extends Component<ComboboxProps,ComboboxEvents> {
197
203
  this.showDropDown( );
198
204
  }
199
205
  else {
200
- this._popup.getList().navigate( kbNav.prev );
206
+ // the item reached is chosen at once
207
+ const list = this._getList( );
208
+ list.navigate( kbNav.prev );
209
+ const [sel] = list.getSelection( );
210
+ if( sel!==undefined ) {
211
+ this._choose( sel, true );
212
+ }
201
213
  }
202
214
 
203
215
  this._prevent_close = false;
@@ -209,7 +221,13 @@ export class Combobox extends Component<ComboboxProps,ComboboxEvents> {
209
221
  this.showDropDown( );
210
222
  }
211
223
  else {
212
- this._popup.getList().navigate( kbNav.next );
224
+ // the item reached is chosen at once
225
+ const list = this._getList( );
226
+ list.navigate( kbNav.next );
227
+ const [sel] = list.getSelection( );
228
+ if( sel!==undefined ) {
229
+ this._choose( sel, true );
230
+ }
213
231
  }
214
232
 
215
233
  this._prevent_close = false;
@@ -229,7 +247,18 @@ export class Combobox extends Component<ComboboxProps,ComboboxEvents> {
229
247
  this.showDropDown( );
230
248
  }
231
249
 
232
- this._popup.getList().filter( this._input.getValue( ) );
250
+ // the text typed is no longer the one of the item chosen: none is chosen
251
+ const list = this._getList( );
252
+ const sel = this.getSelection( );
253
+ if( sel!==undefined ) {
254
+ const itm = list.getItem( sel );
255
+ if( !itm || itm.text!==this._input.getValue( ) ) {
256
+ list.clearSelection( false );
257
+ this.fire( "selectionChange", { selection: [], empty: true } );
258
+ }
259
+ }
260
+
261
+ list.filter( this._input.getValue( ) );
233
262
  }
234
263
 
235
264
  private _on_focusout( ) {
@@ -268,10 +297,9 @@ export class Combobox extends Component<ComboboxProps,ComboboxEvents> {
268
297
  selectItem( index: ListboxID ) {
269
298
  const list = this._getList( );
270
299
  list.select( index );
300
+ // no item (null...): the text is emptied too
271
301
  const el = list.getItem( index );
272
- if( el ) {
273
- this.setValue( el.text as string );
274
- }
302
+ this.setValue( el ? el.text as string : "" );
275
303
  }
276
304
 
277
305
  getSelection( ) {
@@ -257,11 +257,14 @@ export class Dialog<P extends DialogProps = DialogProps, E extends DialogEvents
257
257
 
258
258
  this.on( "btnclick", ( ev ) => {
259
259
  asap( ( ) => {
260
- this.close( );
261
260
  resolve( ev.button );
261
+ this.close( );
262
262
  });
263
263
  });
264
264
 
265
+ // closed without a button (Escape, the close box): nothing was chosen
266
+ this.on( "closed", ( ) => resolve( null ) );
267
+
265
268
  this.show();
266
269
  } );
267
270
  }
@@ -117,13 +117,14 @@ export class Form<P extends FormProps = FormProps> extends Box<P> {
117
117
 
118
118
  setValidator( validator: ValidationFn ) {
119
119
 
120
+ // focusout: once, the first time
120
121
  if( !this.validator ) {
121
- this.validator = validator;
122
-
123
122
  // validation en live des valeurs
124
123
  // disable du bouton ok si pas bon
125
124
  this.addDOMEvent( "focusout", ( ) => this.validate() );
126
125
  }
126
+
127
+ this.validator = validator;
127
128
  }
128
129
 
129
130
  /**
@@ -155,11 +156,14 @@ export class Form<P extends FormProps = FormProps> extends Box<P> {
155
156
  return null;
156
157
  }
157
158
 
158
- for( let name in result ) {
159
+ // the elements already found: a select, a textarea, a rating... every radio of a group
160
+ for( const x of items ) {
161
+ const name = x.getAttribute( "name" );
159
162
  if( new_values[name] != result[name] ) {
160
- const x = this.query( `input[name="${name}"]` );
161
163
  const ifx = x.queryInterface( "form-element" ) as IFormElement;
162
- ifx.setRawValue( new_values[name] );
164
+ if( ifx ) {
165
+ ifx.setRawValue( new_values[name] );
166
+ }
163
167
  }
164
168
  }
165
169
 
@@ -699,7 +699,8 @@ export class Gridview<P extends GridviewProps = GridviewProps, E extends Gridvie
699
699
  if (lock) {
700
700
  this._lock++;
701
701
  }
702
- else {
702
+ // an unlock too many leaves it at 0: the grid goes on drawing
703
+ else if (this._lock > 0) {
703
704
  if (--this._lock == 0 && this._dirty) {
704
705
  this._update( true );
705
706
  }
@@ -15,22 +15,24 @@
15
15
  **/
16
16
 
17
17
  import { EventCallback } from '../../core/core_events';
18
- import { Component, ComponentEvent, componentFromDOM, ComponentProps, EvChange, EvFocus } from '../../core/component';
18
+ import { Component, ComponentEvent, ComponentProps, EvChange, EvFocus } from '../../core/component';
19
19
  import { class_ns, formatIntlDate, IComponentInterface, IFormElement, isString } from '../../core/core_tools';
20
20
 
21
21
  import "./input.module.scss"
22
22
 
23
23
 
24
- function getRadioOwner( el: Element ) {
24
+ /**
25
+ * @internal where to look for the radios of a group: the form that holds them,
26
+ * else the document
27
+ */
25
28
 
26
- while( el!=document.body ) {
27
- const comp = componentFromDOM(el);
28
- const ifx = comp.queryInterface( "tab-handler");
29
- if( ifx ) {
30
- return el;
31
- }
29
+ export function getRadioOwner( el: Element ): ParentNode {
32
30
 
33
- el = el.parentElement;
31
+ // two forms of a page can each have a group of the same name;
32
+ // a dialog has its form, its radios are in it
33
+ const form = el.closest( "form" );
34
+ if( form ) {
35
+ return form;
34
36
  }
35
37
 
36
38
  return document;
@@ -222,7 +224,19 @@ export class Input extends Component<InputProps,InputEvents> {
222
224
  if( props.autofocus===true ) {
223
225
  this.setAttribute( "autofocus", true );
224
226
  }
225
-
227
+
228
+ // every type but checkbox and radio (range and file ignore readonly)
229
+ if( props.type!="checkbox" && props.type!="radio" ) {
230
+ this.setAttribute( "required", props.required );
231
+ this.setAttribute( "readonly", props.readonly );
232
+ }
233
+
234
+ // placeholder: shown by the browser on text, email, password and number only,
235
+ // not on date nor time (they show their own pattern)
236
+ if( props.type===undefined || props.type=="text" || props.type=="email" || props.type=="password" || props.type=="number" ) {
237
+ this.setAttribute( "placeholder", props.placeholder );
238
+ }
239
+
226
240
  switch( props.type ) {
227
241
  case "checkbox":
228
242
  case "radio": {
@@ -244,9 +258,7 @@ export class Input extends Component<InputProps,InputEvents> {
244
258
 
245
259
  case "number": {
246
260
  const p = this.props as NumberProps;
247
-
248
- this.setAttribute( "required", p.required );
249
- this.setAttribute( "readonly", p.readonly );
261
+
250
262
  this.setAttribute( "min", p.min );
251
263
  this.setAttribute( "max", p.max );
252
264
  this.setAttribute( "step", p.step );
@@ -256,20 +268,26 @@ export class Input extends Component<InputProps,InputEvents> {
256
268
  this.addDOMEvent( "wheel", ( e: WheelEvent ) => {
257
269
  if( this.hasFocus() ) { // only if has focus
258
270
  e.preventDefault( );
259
- let v = this.getNumValue();
260
- const delta = e.deltaY < 0 ? 1 : -1;
261
- v += (p.step ? p.step : 1) * delta;
262
- this.setNumValue( v, -2 );
263
271
 
264
- this.dom.dispatchEvent(new Event('input'));
272
+ // the browser keeps the value between min and max, on the step,
273
+ // and starts from a valid one when the field is empty
274
+ const input = this.dom as HTMLInputElement;
275
+ if( e.deltaY < 0 ) {
276
+ input.stepUp( );
277
+ }
278
+ else {
279
+ input.stepDown( );
280
+ }
281
+
282
+ // the decimals of the step: 10.1 shown 10.10 for a step of 0.01
283
+ this.setNumValue( this.getNumValue( ), -2 );
284
+ this.dom.dispatchEvent(new Event('input'));
265
285
  }
266
286
  }, );
267
287
  break;
268
288
  }
269
289
 
270
290
  case "date": {
271
- this.setAttribute( "required", props.required );
272
-
273
291
  let v = props.value;
274
292
  if( v instanceof Date ) {
275
293
  this.setAttribute( "value", formatIntlDate( v, "Y-M-D" ) );
@@ -295,7 +313,6 @@ export class Input extends Component<InputProps,InputEvents> {
295
313
  }
296
314
 
297
315
  case "time": {
298
- this.setAttribute( "required", props.required );
299
316
  if( props.value!==null && props.value!==undefined ) {
300
317
  this.setAttribute( "value", props.value );
301
318
  }
@@ -303,9 +320,6 @@ export class Input extends Component<InputProps,InputEvents> {
303
320
  }
304
321
 
305
322
  default: {
306
- this.setAttribute( "required", props.required );
307
- this.setAttribute( "readonly", props.readonly );
308
-
309
323
  if( props.value!==null && props.value!==undefined ) {
310
324
  this.setAttribute( "value", props.value );
311
325
  }
@@ -314,10 +328,6 @@ export class Input extends Component<InputProps,InputEvents> {
314
328
  this.setAttribute( "pattern", props.pattern );
315
329
  }
316
330
 
317
- if( props.placeholder!==null && props.placeholder!==undefined ) {
318
- this.setAttribute( "placeholder", props.placeholder );
319
- }
320
-
321
331
  if( props.spellcheck===false ) {
322
332
  this.setAttribute( "spellcheck", false );
323
333
  }
@@ -417,14 +427,19 @@ export class Input extends Component<InputProps,InputEvents> {
417
427
  }
418
428
 
419
429
  if( ndec==-2 && this.props.type=='number' ) {
420
- const p = this.props as NumberProps;
430
+ const step = (this.props as NumberProps).step;
431
+
432
+ // as many decimals as the step, 4 at most: the step in 1/10000 is an integer,
433
+ // each 0 on its right is a decimal less (0.25 → 2500 → 2, 2.5 → 25000 → 1, 2 → 0)
434
+ if( step!==undefined && step!=1 ) {
435
+ let n = Math.round( step * 10000 );
436
+ let decimals = 4;
437
+ while( n && n%10==0 && decimals ) {
438
+ n /= 10;
439
+ decimals--;
440
+ }
421
441
 
422
- if( p.step<1 ) {
423
- let ndec = -Math.floor(Math.log10(p.step ?? 1) );
424
- return this.setValue( value.toFixed(ndec) );
425
- }
426
- else if( p.step>1 ) {
427
- return this.setValue( value.toFixed() );
442
+ return this.setValue( value.toFixed( decimals ) );
428
443
  }
429
444
  }
430
445
  else if( ndec>=0 ) {
@@ -506,9 +521,16 @@ export class Input extends Component<InputProps,InputEvents> {
506
521
  override queryInterface<T extends IComponentInterface>( name: string ): T {
507
522
  if( name=="form-element" ) {
508
523
  const i: IFormElement = {
509
- getRawValue: ( ): any => {
524
+ getRawValue: ( ): any => {
525
+ // checkbox without value, or a boolean one: a boolean; with one: the value when checked,
526
+ // null when not (as a <form>)
510
527
  if( this.props.type=='checkbox' ) {
511
- return this.getCheck( );
528
+ const value = this.props.value;
529
+ if( value===undefined || typeof value==="boolean" ) {
530
+ return this.getCheck( );
531
+ }
532
+
533
+ return this.getCheck( ) ? value : null;
512
534
  }
513
535
  else if( this.props.type=='radio' ) {
514
536
  const owner = getRadioOwner( this.dom );
@@ -523,8 +545,11 @@ export class Input extends Component<InputProps,InputEvents> {
523
545
  }
524
546
  },
525
547
  setRawValue: ( v: any ) => {
548
+ // checkbox with a value (not a boolean): checked by that value (or true)
526
549
  if( this.props.type=='checkbox' ) {
527
- this.setCheck( !!v );
550
+ const value = this.props.value;
551
+ const plain = value===undefined || typeof value==="boolean";
552
+ this.setCheck( plain ? !!v : ( v===value || v===true ) );
528
553
  }
529
554
  else if( this.props.type=='radio' ) {
530
555
  if( this.props.value==v ) {
@@ -17,6 +17,9 @@
17
17
  :root {
18
18
  --keyboard-key-background: #33302d;
19
19
  --keyboard-hilite-color: #5ba921;
20
+ --keyboard-key-color: #ffffff;
21
+ --keyboard-accents-color: #7cc7ff;
22
+ --keyboard-symbols-color: #f2c94c;
20
23
  --keyboard-zoom: 100%;
21
24
  }
22
25
 
@@ -62,7 +65,8 @@
62
65
  width: 100%;
63
66
  }
64
67
 
65
- color: white;
68
+ // the color of the page, for the keys of characters
69
+ color: var( --kb-page-color, var( --keyboard-key-color ) );
66
70
  text-align: center;
67
71
 
68
72
  background-color: var( --keyboard-key-background);
@@ -83,6 +87,29 @@
83
87
  }
84
88
 
85
89
 
90
+ .accents {
91
+ --kb-page-color: var( --keyboard-accents-color );
92
+ }
93
+
94
+ .symbols {
95
+ --kb-page-color: var( --keyboard-symbols-color );
96
+ }
97
+
98
+ // the keys of commands keep their color, the keys that call a page have its one
99
+ .cnum,
100
+ .calpha,
101
+ .cspace {
102
+ color: var( --keyboard-key-color );
103
+ }
104
+
105
+ .caccents {
106
+ color: var( --keyboard-accents-color );
107
+ }
108
+
109
+ .csymbols {
110
+ color: var( --keyboard-symbols-color );
111
+ }
112
+
86
113
  .cplace {
87
114
  visibility: hidden;
88
115
  }
@@ -109,8 +136,9 @@
109
136
 
110
137
  // color: rgba(255,255,255,0.3);
111
138
 
112
- .lower,
113
- .upper {
139
+ .alpha,
140
+ .accents,
141
+ .symbols {
114
142
 
115
143
  .cspace {
116
144
  flex-basis: 689px;
@@ -128,7 +156,10 @@
128
156
  flex-basis: 88px;
129
157
  }
130
158
 
131
- .cnum {
159
+ .cnum,
160
+ .caccents,
161
+ .csymbols,
162
+ .calpha {
132
163
  flex-basis: 88px;
133
164
  }
134
165
  }
@@ -30,8 +30,9 @@ import icon_hide from "./eye-slash.svg";
30
30
 
31
31
  /**
32
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.
33
+ * a language adds to them (its extra), shown on a page of their own,
34
+ * called by a key. The upper case is computed, the digits and the signs
35
+ * are the same for all.
35
36
  *
36
37
  * base: the countries where it is not QWERTY are those that
37
38
  * https://en.wikipedia.org/wiki/QWERTZ and /AZERTY list.
@@ -40,6 +41,7 @@ import icon_hide from "./eye-slash.svg";
40
41
  */
41
42
 
42
43
  type KbBase = "azerty" | "qwerty" | "qwertz";
44
+ type KbMode = "alpha" | "accents" | "symbols" | "number" | "date";
43
45
 
44
46
  interface KbLayout {
45
47
  base: KbBase;
@@ -90,6 +92,13 @@ const kb_countries: Record<string,Partial<KbLayout>> = {
90
92
  const kb_digits = "1 2 3 4 5 6 7 8 9 0";
91
93
  const kb_signs = "! @ # $ % ^ & * ( ) _ +";
92
94
 
95
+ // the page of the signs, that the letters do not have
96
+ const kb_symbols = [
97
+ "- / : ; ( ) € & @ \"",
98
+ "\\ | ~ _ ` § £ $ ^ %",
99
+ "[ ] { } < > = + * #",
100
+ ];
101
+
93
102
  const kb_pads = {
94
103
  number: [
95
104
  "1 2 3 {2}",
@@ -160,13 +169,11 @@ function extraLines( extra: string ): string[] {
160
169
 
161
170
  function letterLines( layout: KbLayout, upper: boolean ): string[] {
162
171
  let [l1, l2, l3] = kb_bases[layout.base];
163
- let extra = extraLines( layout.extra );
164
172
 
165
173
  if( upper ) {
166
174
  l1 = upperKeys( l1 );
167
175
  l2 = upperKeys( l2 );
168
176
  l3 = upperKeys( l3 );
169
- extra = extra.map( upperKeys );
170
177
  }
171
178
 
172
179
  return [
@@ -174,11 +181,64 @@ function letterLines( layout: KbLayout, upper: boolean ): string[] {
174
181
  l1 + " {2}",
175
182
  l2 + " {3}",
176
183
  "{4} " + l3 + ( upper ? " ? : {4}" : " , . {4}" ),
184
+ bottomLine( layout, "alpha" ),
185
+ ];
186
+ }
187
+
188
+ /**
189
+ * the page called by {10}: the extra letters
190
+ */
191
+
192
+ function accentLines( layout: KbLayout, upper: boolean ): string[] {
193
+ let extra = extraLines( layout.extra );
194
+ if( upper ) {
195
+ extra = extra.map( upperKeys );
196
+ }
197
+
198
+ return [
177
199
  ...extra,
178
- "{5} {6} ' {7}",
200
+ "{4} {2} {3}",
201
+ bottomLine( layout, "accents" ),
179
202
  ];
180
203
  }
181
204
 
205
+ /**
206
+ * the page called by {11}: the signs
207
+ */
208
+
209
+ function symbolLines( layout: KbLayout ): string[] {
210
+ const [s1, s2, s3] = kb_symbols;
211
+
212
+ return [
213
+ s1 + " {2}",
214
+ s2 + " {3}",
215
+ s3,
216
+ bottomLine( layout, "symbols" ),
217
+ ];
218
+ }
219
+
220
+ /**
221
+ * the last row of the pages of letters: the key of the page that is shown
222
+ * is replaced by {8} at its place, to come back to the letters.
223
+ * no key of the extra letters when the language has none
224
+ */
225
+
226
+ function bottomLine( layout: KbLayout, page: "alpha" | "accents" | "symbols" ): string {
227
+ const accents = !layout.extra ? "" : page=="accents" ? "{8} " : "{10} ";
228
+ const symbols = page=="symbols" ? "{8} " : "{11} ";
229
+
230
+ return "{5} " + accents + symbols + "{6} ' {7}";
231
+ }
232
+
233
+ /**
234
+ * the text of the key that calls the extra letters: the first two of them
235
+ */
236
+
237
+ function accentLabel( layout: KbLayout, upper: boolean ): string {
238
+ const label = layout.extra.split( " " ).slice( 0, 2 ).join( " " );
239
+ return ( upper ? upperKeys( label ) : label ).replace( " ", "" );
240
+ }
241
+
182
242
 
183
243
  const RE_sel = /text|password|search|tel|url/i;
184
244
 
@@ -195,13 +255,17 @@ interface KeyboardProps extends BoxProps {
195
255
  * ```
196
256
  * --keyboard-key-background
197
257
  * --keyboard-hilite-color
258
+ * --keyboard-key-color
259
+ * --keyboard-accents-color
260
+ * --keyboard-symbols-color
198
261
  * --keyboard-zoom
199
262
  * ```
200
263
  */
201
264
  @class_ns( "x4" )
202
265
  export class Keyboard extends HBox<KeyboardProps>
203
266
  {
204
- mode: "lower" | "upper" | "number" | "date";
267
+ mode: KbMode;
268
+ upper: boolean;
205
269
  locale: string;
206
270
  keyboard: Box;
207
271
  visible: boolean;
@@ -210,7 +274,8 @@ export class Keyboard extends HBox<KeyboardProps>
210
274
  constructor( props: KeyboardProps ) {
211
275
  super( { ...props, id: 'v-keyboard' } );
212
276
 
213
- this.mode = 'lower';
277
+ this.mode = 'alpha';
278
+ this.upper = false;
214
279
  this.locale = props.locale ?? 'fr-FR';
215
280
  this.visible = false;
216
281
 
@@ -255,6 +320,11 @@ export class Keyboard extends HBox<KeyboardProps>
255
320
  setLocale( locale: string ) {
256
321
  this.locale = locale;
257
322
 
323
+ // a language without extra letters has no page for them
324
+ if( this.mode=="accents" && !findLayout( locale ).extra ) {
325
+ this.mode = "alpha";
326
+ }
327
+
258
328
  // drawn at least once
259
329
  if( this.keyboard ) {
260
330
  this._redraw( );
@@ -301,15 +371,9 @@ export class Keyboard extends HBox<KeyboardProps>
301
371
 
302
372
  // shift
303
373
  case 4: {
304
- if( this.mode=='lower' ) {
305
- this.mode = 'upper';
306
- }
307
- else {
308
- this.mode = 'lower';
309
- }
310
-
374
+ this.upper = !this.upper;
311
375
  this._redraw( );
312
- break;
376
+ break;
313
377
  }
314
378
 
315
379
  // num + sym
@@ -331,7 +395,19 @@ export class Keyboard extends HBox<KeyboardProps>
331
395
  }
332
396
 
333
397
  case 8: {
334
- this._switchMode( "lower" );
398
+ this._switchMode( "alpha" );
399
+ break;
400
+ }
401
+
402
+ // extra letters
403
+ case 10: {
404
+ this._switchMode( "accents" );
405
+ break;
406
+ }
407
+
408
+ // signs
409
+ case 11: {
410
+ this._switchMode( "symbols" );
335
411
  break;
336
412
  }
337
413
 
@@ -354,7 +430,7 @@ export class Keyboard extends HBox<KeyboardProps>
354
430
  *
355
431
  */
356
432
 
357
- private _switchMode( m: "lower" | "upper" | "number" | "date" ) {
433
+ private _switchMode( m: KbMode ) {
358
434
  this.mode = m;
359
435
  this._redraw( );
360
436
  }
@@ -421,7 +497,8 @@ export class Keyboard extends HBox<KeyboardProps>
421
497
  this._switchMode( "date");
422
498
  }
423
499
  else {
424
- this._switchMode( "lower" );
500
+ this.upper = false;
501
+ this._switchMode( "alpha" );
425
502
  }
426
503
  }
427
504
  }
@@ -546,11 +623,19 @@ export class Keyboard extends HBox<KeyboardProps>
546
623
 
547
624
  private _createContent( ) {
548
625
  let lines: string[];
626
+ const layout = findLayout( this.locale );
627
+
549
628
  if( this.mode=="number" || this.mode=="date" ) {
550
629
  lines = kb_pads[this.mode];
551
630
  }
631
+ else if( this.mode=="accents" ) {
632
+ lines = accentLines( layout, this.upper );
633
+ }
634
+ else if( this.mode=="symbols" ) {
635
+ lines = symbolLines( layout );
636
+ }
552
637
  else {
553
- lines = letterLines( findLayout( this.locale ), this.mode=="upper" );
638
+ lines = letterLines( layout, this.upper );
554
639
  }
555
640
 
556
641
  let result: Box[] = [];
@@ -643,8 +728,22 @@ export class Keyboard extends HBox<KeyboardProps>
643
728
 
644
729
  case 9:
645
730
  {
646
- content = '';
647
- cls += ' cplace';
731
+ content = '';
732
+ cls += ' cplace';
733
+ break;
734
+ }
735
+
736
+ case 10:
737
+ {
738
+ content = accentLabel( layout, this.upper );
739
+ cls += ' caccents';
740
+ break;
741
+ }
742
+
743
+ case 11:
744
+ {
745
+ content = '#+=';
746
+ cls += ' csymbols';
648
747
  break;
649
748
  }
650
749
  }