x4js 2.3.24 → 2.3.25

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.
@@ -40,11 +40,20 @@ export interface ListItem {
40
40
  iconId?: string;
41
41
  data?: any;
42
42
  cls?: string;
43
- checked?: boolean;
43
+ checked?: boolean; // checkable listbox: the starting state of the case, getChecked() gives the current one
44
44
  }
45
45
 
46
46
  /**
47
- *
47
+ * a case changed by the user (click, Space)
48
+ */
49
+
50
+ export interface EvCheckChange extends ComponentEvent {
51
+ readonly id: ListboxID;
52
+ readonly checked: boolean;
53
+ }
54
+
55
+ /**
56
+ *
48
57
  */
49
58
 
50
59
  export interface ListboxEvents extends ComponentEvents {
@@ -52,6 +61,7 @@ export interface ListboxEvents extends ComponentEvents {
52
61
  dblClick?: EvDblClick;
53
62
  contextMenu?: EvContextMenu;
54
63
  selectionChange?: EvSelectionChange;
64
+ checkChange?: EvCheckChange;
55
65
  }
56
66
 
57
67
  /**
@@ -65,16 +75,25 @@ export interface ListboxProps extends Omit<ComponentProps,'content'> {
65
75
  icon?: string;
66
76
  header?: Header;
67
77
  footer?: Component,
68
- checkable?: true,
78
+ checkable?: boolean, // a case at the start of each line
69
79
  multisel?: true,
80
+ reselect?: boolean, // a click on the item already selected fires selectionChange again
70
81
  emptyMsg?: string,
71
82
 
72
83
  dblClick?: EventCallback<EvDblClick>;
73
84
  selectionChange?: EventCallback<EvSelectionChange>;
74
85
  contextMenu?: EventCallback<EvContextMenu>;
86
+ checkChange?: EventCallback<EvCheckChange>;
75
87
  }
76
88
 
77
89
  /**
90
+ * checkable: a click on the case, or Space for the last item selected, changes it and fires
91
+ * checkChange; neither click nor the selection. A double click on the case is ignored.
92
+ *
93
+ * reselect: a click of the user on the item already selected fires selectionChange with the
94
+ * same selection: lists linked together whose view shows what was clicked last. select( ) by
95
+ * the code, the arrows and a right click do not.
96
+ *
78
97
  * @cssvar
79
98
  * ```
80
99
  * --listbox-item-color
@@ -90,6 +109,12 @@ export interface ListboxProps extends Omit<ComponentProps,'content'> {
90
109
  * --listbox-border-focus
91
110
  * --listbox-title-background
92
111
  * --listbox-title-color
112
+ * --listbox-check-border
113
+ * --listbox-check-border-hover
114
+ * --listbox-check-border-sel
115
+ * --listbox-check-background
116
+ * --listbox-check-background-disabled
117
+ * --listbox-check-color
93
118
  * ```
94
119
  */
95
120
 
@@ -106,7 +131,8 @@ private _view: Viewport;
106
131
  super( { ...props } );
107
132
 
108
133
  this.setAttribute( "tabindex", 0 );
109
- this.mapPropEvents( props, "dblClick", "selectionChange", "contextMenu" );
134
+ this.mapPropEvents( props, "dblClick", "selectionChange", "contextMenu", "checkChange" );
135
+ this.setClass( "checkable", !!props.checkable );
110
136
 
111
137
  const scroller = new ScrollView( { cls: "body" } );
112
138
  this._view = scroller.getViewport( );
@@ -169,6 +195,18 @@ private _view: Viewport;
169
195
  break;
170
196
  }
171
197
 
198
+ case " ": {
199
+ if( !this.props.checkable || this._lastsel===undefined ) {
200
+ return;
201
+ }
202
+
203
+ const line = this._itemWithID( this._lastsel );
204
+ if( line ) {
205
+ this._toggleCheck( line );
206
+ }
207
+ break;
208
+ }
209
+
172
210
  default:
173
211
  return;
174
212
  }
@@ -246,6 +284,7 @@ private _view: Viewport;
246
284
 
247
285
  private _on_click( ev: MouseEvent ) {
248
286
  let target = ev.target as HTMLElement;
287
+ let on_check = false;
249
288
 
250
289
  while( target && target!=this.dom ) {
251
290
  const c = componentFromDOM( target );
@@ -255,7 +294,19 @@ private _view: Viewport;
255
294
  return;
256
295
  }
257
296
 
297
+ if( c && c.hasClass("x4check") ) {
298
+ on_check = true;
299
+ }
300
+
258
301
  if( c && c.hasClass("x4item") ) {
302
+ // the case changes alone; a double click on it is ignored
303
+ if( on_check ) {
304
+ if( ev.type == 'click' ) {
305
+ this._toggleCheck( c );
306
+ }
307
+ return;
308
+ }
309
+
259
310
  const id = c.getInternalData( "id" );
260
311
  const fev: EvClick = { context:id };
261
312
  if (ev.type == 'click') {
@@ -266,7 +317,7 @@ private _view: Viewport;
266
317
  }
267
318
 
268
319
  if (!fev.defaultPrevented) {
269
- this._selectItem( id, c, ev.ctrlKey ? 'toggle' : 'single' );
320
+ this._selectItem( id, c, ev.ctrlKey ? 'toggle' : 'single', ev.type == 'click' && this.props.reselect );
270
321
  }
271
322
 
272
323
  return;
@@ -315,8 +366,8 @@ private _view: Viewport;
315
366
  *
316
367
  */
317
368
 
318
- private _selectItem( id: ListboxID, item: Component, mode: "single" | "toggle" ) {
319
-
369
+ private _selectItem( id: ListboxID, item: Component, mode: "single" | "toggle", again = false ) {
370
+
320
371
  if( !this.props.multisel ) {
321
372
  mode = 'single';
322
373
  }
@@ -324,7 +375,11 @@ private _view: Viewport;
324
375
  this._lastsel = id;
325
376
 
326
377
  if( mode=='single' ) {
378
+ // already selected: nothing changes; again (reselect): its selectionChange all the same
327
379
  if( this._multisel.has(id) ) {
380
+ if( again ) {
381
+ this.fire( "selectionChange", { selection: this.getSelection(), empty: false } );
382
+ }
328
383
  return;
329
384
  }
330
385
 
@@ -450,38 +505,43 @@ private _view: Viewport;
450
505
  }
451
506
 
452
507
  /**
453
- *
508
+ * replaces the items of the list
509
+ * @param items - the new items; empty or null: the message of an empty list (emptyMsg)
510
+ * @param keepSel - true: the items selected before stay selected, when they are in the new items
511
+ *
512
+ * selectionChange: fired once, after the items are drawn, with the selection they end with,
513
+ * when there was a selection before or there is one after. With keepSel and the same ids, it
514
+ * is fired too: the items changed, a view that shows the selection draws it again.
515
+ * Nothing selected before nor after: no selectionChange.
454
516
  */
455
517
 
456
518
  setItems( items: ListItem[], keepSel = false ) {
457
519
 
458
520
  const oldSel = this.getSelection( );
459
521
 
460
- this.clearSelection( );
522
+ // the selection changes silently: one selectionChange at the end
523
+ this.clearSelection( false );
461
524
  this._view.clearContent( );
462
525
  this._items = items ?? [];
463
526
 
464
- let update_sel = false;
465
-
466
527
  if( this._items.length ) {
467
528
  this.removeClass( "empty" );
468
529
  const content = items.map( x => this.renderItem(x) );
469
530
  this._view.setContent( content );
470
531
 
471
532
  if( keepSel && oldSel.length>0 ) {
472
- this.select( oldSel );
533
+ this.select( oldSel, false );
473
534
  }
474
535
  }
475
536
  else {
476
537
  this.addClass( "empty" );
477
- update_sel = oldSel.length>0;
478
538
  this._view.setContent( new Label( { cls: "empty vertical", icon: icons.empty, text: this.props.emptyMsg ?? _tr.global.empty_list } ) );
479
539
  }
480
-
481
- if( update_sel ) {
482
- this.setTimeout( "sel", 100, ( ) => {
483
- this.fire( "selectionChange", { selection: [], empty: true } );
484
- } );
540
+
541
+ // a selection before or after: one selectionChange, the same ids too (the items changed)
542
+ const sel = this.getSelection( );
543
+ if( oldSel.length || sel.length ) {
544
+ this.fire( "selectionChange", { selection: sel, empty: sel.length==0 } );
485
545
  }
486
546
  }
487
547
 
@@ -492,13 +552,52 @@ private _view: Viewport;
492
552
  renderItem( item: ListItem ) {
493
553
  const renderer = this.props.renderer ?? this.defaultRenderer;
494
554
  const line = renderer( item );
495
-
555
+
496
556
  line.addClass( "x4item" );
497
557
  line.setInternalData( "id", item.id );
498
-
558
+
559
+ if( this.props.checkable ) {
560
+ const check = new Component( { cls: "x4check" } );
561
+ check.dom.insertAdjacentHTML( "beforeend", icons.check );
562
+ line.prependContent( check );
563
+ line.setClass( "checked", !!item.checked );
564
+ }
565
+
499
566
  return line;
500
567
  }
501
568
 
569
+ /**
570
+ * the user changed the case of a line
571
+ */
572
+
573
+ private _toggleCheck( line: Component ) {
574
+ line.toggleClass( "checked" );
575
+ this.fire( "checkChange", { id: line.getInternalData( "id" ), checked: line.hasClass( "checked" ) } );
576
+ }
577
+
578
+ /**
579
+ * ids of the items checked, in the order of the list
580
+ */
581
+
582
+ getChecked( ): ListboxID[] {
583
+ return this._view.enumChildComponents( false )
584
+ .filter( x => x.hasClass( "checked" ) )
585
+ .map( x => x.getInternalData( "id" ) );
586
+ }
587
+
588
+ /**
589
+ * check these items, uncheck the others; no event
590
+ */
591
+
592
+ setChecked( ids: ListboxID[] ) {
593
+ const checked = new Set( ids );
594
+ this._view.enumChildComponents( false ).forEach( x => {
595
+ if( x.hasClass( "x4item" ) ) {
596
+ x.setClass( "checked", checked.has( x.getInternalData( "id" ) ) );
597
+ }
598
+ } );
599
+ }
600
+
502
601
  /**
503
602
  *
504
603
  */
@@ -574,11 +673,17 @@ private _view: Viewport;
574
673
  */
575
674
 
576
675
  appendItem( item: ListItem, prepend = false, select = true ) {
577
-
676
+
578
677
  if( select ) {
579
678
  this._clearSelection( );
580
679
  }
581
680
 
681
+ // the first item: the message of an empty list goes away
682
+ if( !this._items.length ) {
683
+ this._view.clearContent( );
684
+ this.removeClass( "empty" );
685
+ }
686
+
582
687
  let el = this.renderItem( item );
583
688
 
584
689
  if( prepend ) {
@@ -624,6 +729,11 @@ private _view: Viewport;
624
729
  _new.addClass( "selected" );
625
730
  }
626
731
 
732
+ // the case keeps its state
733
+ if( this.props.checkable ) {
734
+ _new.setClass( "checked", old.hasClass( "checked" ) );
735
+ }
736
+
627
737
  this._view.dom.replaceChild( _new.dom, old.dom );
628
738
  }
629
739
  }
@@ -23,6 +23,11 @@
23
23
  --msgbox-question-icon-color: var( --accent-background ); // a question, an InputBox, a PromptBox
24
24
 
25
25
  --msgbox-log-background: var( --background-ternary ); // the lines of a ProgressionBox
26
+ --msgbox-log-border: var( --border );
27
+ --msgbox-log-border-focus: var( --border-focus );
28
+ --msgbox-log-error-color: var( --alert-background );
29
+ --msgbox-log-warning-color: var( --warning-background );
30
+ --msgbox-log-scrollbar: var( --border-hover ); // the thumb; the track is the background of the lines
26
31
  }
27
32
 
28
33
  .x4messagebox {
@@ -95,6 +100,26 @@
95
100
  min-height: 200px;
96
101
  min-width: 500px;
97
102
 
103
+ // after an error: the title bar of an error of MessageBox
104
+ &.error > .caption {
105
+ background-color: var( --msgbox-caption-background );
106
+
107
+ & > #title {
108
+ color: var( --msgbox-caption-color );
109
+ }
110
+ }
111
+
112
+ // on the left of OK: the counts of the errors and the warnings, the icon of the worst
113
+ #alerts {
114
+ &.error {
115
+ --label-icon-color: var( --msgbox-log-error-color );
116
+ }
117
+
118
+ &.warning {
119
+ --label-icon-color: var( --msgbox-log-warning-color );
120
+ }
121
+ }
122
+
98
123
  &> .x4form {
99
124
  min-height: unset;
100
125
  }
@@ -122,8 +147,9 @@
122
147
  }
123
148
 
124
149
  #title {
125
- padding: 4px 16px;
150
+ padding: 0 16px;
126
151
  line-height: 1.8em;
152
+ font-weight: bold;
127
153
  }
128
154
 
129
155
  // on the right, the bar and the lines end where the button OK does: the bar of
@@ -132,16 +158,58 @@
132
158
  margin: 0 6px 0 16px;
133
159
  }
134
160
 
135
- // the lines: a textarea read only, 10 lines high at least, a line is not wrapped
161
+ // the lines: a <pre>, 10 lines high at least; its text gives it no size (contain): it
162
+ // scrolls, the box does not grow; a line is not wrapped. An error, a warning: their
163
+ // icon, their colour
136
164
  #sub-text {
137
165
  flex-grow: 1;
138
166
  min-height: calc( 10lh + 10px ); // its padding and its border too
139
167
  max-height: 400px;
140
168
  overflow: auto;
141
169
  white-space: pre;
170
+ contain: size;
171
+ font: inherit;
172
+ line-height: 1.3em;
173
+ scrollbar-color: var( --msgbox-log-scrollbar ) transparent;
174
+ outline: none;
175
+ border: 1px solid var( --msgbox-log-border );
142
176
  background-color: var( --msgbox-log-background );
143
177
  padding: 4px 8px;
144
178
  margin: 0 6px 0 16px;
179
+
180
+ &, * {
181
+ user-select: text;
182
+ }
183
+
184
+ &:focus {
185
+ border-color: var( --msgbox-log-border-focus );
186
+ }
187
+
188
+ svg {
189
+ width: 1em;
190
+ height: 1em;
191
+ vertical-align: -0.125em;
192
+ margin-right: 4px;
193
+ }
194
+
195
+ .error {
196
+ color: var( --msgbox-log-error-color );
197
+ }
198
+
199
+ .warning {
200
+ color: var( --msgbox-log-warning-color );
201
+ }
202
+
203
+ // a section: its title, a click folds it; its lines below, under the title
204
+ details {
205
+ padding-left: 1em;
206
+ }
207
+
208
+ summary {
209
+ margin-left: -1em;
210
+ cursor: pointer;
211
+ font-weight: bold;
212
+ }
145
213
  }
146
214
  }
147
215
  }
@@ -2,7 +2,7 @@
2
2
  // :: MESSAGEBOX ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
3
3
 
4
4
  import { _tr } from '../../core/core_i18n';
5
- import { asap, class_ns, unsafeHtml, UnsafeHtml } from '../../core/core_tools';
5
+ import { asap, class_ns, sprintf, unsafeHtml, UnsafeHtml } from '../../core/core_tools';
6
6
 
7
7
  import { HBox, VBox } from '../boxes/boxes';
8
8
  import { Icon } from '../icon/icon';
@@ -11,10 +11,10 @@ import { Dialog, DialogProps } from "../dialog/dialog"
11
11
  import { Form } from '../form/form';
12
12
  import { BtnGroupItem } from '../btngroup/btngroup';
13
13
  import { Input } from '../input/input';
14
- import { SimpleTextArea } from '../textarea/textarea';
15
14
  import { Button } from '../button/button';
16
15
  import { Component } from '../../core/component';
17
16
  import { Progress } from '../components.js';
17
+ import icons from '../assets/icons';
18
18
 
19
19
  import "./messages.module.scss";
20
20
 
@@ -38,6 +38,11 @@ export interface MessageBoxProps extends DialogProps {
38
38
  * --msgbox-icon-color
39
39
  * --msgbox-question-icon-color
40
40
  * --msgbox-log-background
41
+ * --msgbox-log-border
42
+ * --msgbox-log-border-focus
43
+ * --msgbox-log-error-color
44
+ * --msgbox-log-warning-color
45
+ * --msgbox-log-scrollbar
41
46
  * ```
42
47
  * --msgbox-caption-* and --msgbox-icon-color are the ones of an error; a question, an InputBox
43
48
  * and a PromptBox have the title bar of a Dialog and their icon in --msgbox-question-icon-color
@@ -279,15 +284,21 @@ export class PromptBox extends Dialog<DialogProps>
279
284
 
280
285
  /**
281
286
  * a modal box that follows a long task: a title, a bar, and the lines of what is done
282
- * in a textarea read only (they are text: a markup is shown as it is written).
283
- * done( ) shows its button OK: after an error it waits for it, else it closes the box
284
- * after 5s, the seconds left in the button.
287
+ * in a <pre>: a string is text, an UnsafeHtml is html. An error, a warning have their
288
+ * icon and their colour. addSection puts the lines that follow in a section that folds.
289
+ * Ctrl+A selects the lines.
290
+ * done( ) shows its button OK: after an error or a warning it waits for it, their counts
291
+ * on the left of the bar; else it closes the box after 5s, the seconds left in the button.
285
292
  */
286
293
 
287
294
  @class_ns( "x4" )
288
295
  export class ProgressionBox extends Dialog {
289
296
 
290
- #has_errors = false;
297
+ // an error or a warning: done( ) waits for OK
298
+ #errors = 0;
299
+ #warnings = 0;
300
+
301
+ #section: Component; // the last section (a <details>): the lines go in it
291
302
 
292
303
  constructor( title: string ) {
293
304
  super( {
@@ -303,13 +314,15 @@ export class ProgressionBox extends Dialog {
303
314
  new VBox( { flex: 1, cls: "right", content: [
304
315
  new SimpleText( { id: "title", text: title } ),
305
316
  new Progress( { id:"prog", min: 0, max: 100, value: 0 } ),
306
- new SimpleTextArea( { id: "sub-text", readonly: true, trim: false } ),
317
+ new Component( { tag: "pre", id: "sub-text", attrs: { tabindex: 0 }, dom_events: {
318
+ keydown: ( ev ) => this._on_log_key( ev ),
319
+ } } ),
307
320
  ]})
308
321
  ]
309
322
  }),
310
323
  ]
311
324
  }),
312
- buttons: [ "ok.outline.default" ]
325
+ buttons: [ new Label( { id: "alerts", text: "" } ), "-", "ok.outline.default" ]
313
326
  });
314
327
 
315
328
  this.query("#btnbar").show( false );
@@ -328,50 +341,131 @@ export class ProgressionBox extends Dialog {
328
341
  this.query<Progress>( "#prog").setValue( perc );
329
342
  }
330
343
 
331
- /** a line at the end: done( ) then waits for the button OK */
344
+ /** a line at the end, with the icon of an error: done( ) then waits for the button OK */
332
345
 
333
346
  addError( text: string | UnsafeHtml, perc: number ) {
334
- this._addLine( text );
347
+ this._addLine( text, "error" );
348
+ this.query<Progress>( "#prog").setValue( perc );
349
+ this.addClass( "error" ); // the title bar in red, as the one of an error of MessageBox
350
+
351
+ this.#errors++;
352
+ }
353
+
354
+ /** a line at the end, with the icon of a warning: done( ) then waits for the button OK */
355
+
356
+ addWarning( text: string | UnsafeHtml, perc: number ) {
357
+ this._addLine( text, "warning" );
335
358
  this.query<Progress>( "#prog").setValue( perc );
336
359
 
337
- this.#has_errors = true;
360
+ this.#warnings++;
361
+ }
362
+
363
+ /**
364
+ * a section at the end, open: the lines that follow go in it. A click on its title folds
365
+ * it; its title has the icon of the worst of its lines (an error, a warning)
366
+ */
367
+
368
+ addSection( title: string | UnsafeHtml, perc: number ) {
369
+ const summary = new Component( { tag: "summary", content: new Component( { tag: "span", cls: "alert" } ) } );
370
+ summary.appendContent( title );
371
+
372
+ this.#section = new Component( { tag: "details", attrs: { open: true }, content: summary } );
373
+
374
+ const log = this.query( "#sub-text" );
375
+ log.appendContent( this.#section );
376
+ log.dom.scrollTop = log.dom.scrollHeight;
377
+ this.query<Progress>( "#prog").setValue( perc );
338
378
  }
339
379
 
340
380
  /** the text in place of all the lines */
341
381
 
342
382
  setText( text: string | UnsafeHtml, perc: number ) {
343
- this.query<SimpleTextArea>( "#sub-text" ).setText( text.toString( ) );
383
+ this.#section = null;
384
+ this.query( "#sub-text" ).setContent( text );
344
385
  this.query<Progress>( "#prog").setValue( perc );
345
386
  }
346
387
 
347
388
  /** as at the start: no line, no error, no button OK */
348
389
 
349
390
  clear( ) {
350
- this.#has_errors = false;
391
+ this.#errors = 0;
392
+ this.#warnings = 0;
351
393
  this.clearInterval( "close" );
352
394
  this.query( "#btnbar" ).show( false );
353
- this.query<SimpleTextArea>( "#sub-text" ).setText( "" );
395
+ this.removeClass( "error" );
396
+ this.query<Label>( "#alerts" ).setText( "" );
397
+ this.query<Label>( "#alerts" ).setIcon( null );
398
+ this.#section = null;
399
+ this.query( "#sub-text" ).clearContent( );
354
400
  }
355
401
 
356
- // the line after the others, the last one shown
357
- private _addLine( text: string | UnsafeHtml ) {
358
- const log = this.query<SimpleTextArea>( "#sub-text" );
359
- const lines = log.getText( );
402
+ // the line after the others (in the last section), the last one shown; an error, a
403
+ // warning in a span of their class, their icon first
404
+ private _addLine( text: string | UnsafeHtml, kind?: "error" | "warning" ) {
405
+ const log = this.query( "#sub-text" );
406
+ const target = this.#section ?? log;
407
+
408
+ // after the summary of a section, its first line starts below it
409
+ if( target.dom.lastChild && target.dom.lastChild.nodeName!="SUMMARY" ) {
410
+ target.appendContent( "\n" );
411
+ }
412
+
413
+ if( kind ) {
414
+ const icon = kind=="error" ? icons.danger : icons.warning;
415
+ const line = new Component( { tag: "span", cls: kind, content: unsafeHtml( icon ) } );
416
+ line.appendContent( text );
417
+ target.appendContent( line );
418
+
419
+ // the title of the section: the icon of the worst, in its colour
420
+ const alert = this.#section?.query( ".alert" );
421
+ if( alert && !alert.hasClass( "error" ) ) {
422
+ alert.setClass( "warning", kind=="warning" );
423
+ alert.setClass( "error", kind=="error" );
424
+ alert.setContent( unsafeHtml( icon ) );
425
+ }
426
+ }
427
+ else {
428
+ target.appendContent( text );
429
+ }
430
+
431
+ log.dom.scrollTop = log.dom.scrollHeight;
432
+ }
360
433
 
361
- log.setText( lines ? lines + "\n" + text : text.toString( ) );
362
- log.scrollToBottom( );
434
+ // Ctrl+A: the lines only, not the whole page
435
+ private _on_log_key( ev: KeyboardEvent ) {
436
+ if( ( ev.ctrlKey || ev.metaKey ) && ev.key.toLowerCase( )=="a" ) {
437
+ document.getSelection( ).selectAllChildren( ev.currentTarget as HTMLElement );
438
+ ev.preventDefault( );
439
+ }
363
440
  }
364
441
 
365
442
  /**
366
- * the task is over: the button OK is shown. After an error it waits for a click;
367
- * without one it counts the seconds left, "OK (5s)", and closes the box at 0
443
+ * the task is over: the button OK is shown. After an error or a warning it waits for
444
+ * a click, their counts on the left of the bar ("2 errors / 1 warning"); without one it counts the
445
+ * seconds left, "OK (5s)", and closes the box at 0
368
446
  */
369
447
 
370
448
  done( ) {
371
449
  const ok = this.query<Button>( "#ok" );
372
450
  this.query( "#btnbar" ).show( true );
373
451
 
374
- if( this.#has_errors ) {
452
+ if( this.#errors || this.#warnings ) {
453
+ const tr = _tr.global.progression;
454
+ const counts = [];
455
+ if( this.#errors ) {
456
+ counts.push( sprintf( this.#errors>1 ? tr.errors : tr.error, this.#errors ) );
457
+ }
458
+ if( this.#warnings ) {
459
+ counts.push( sprintf( this.#warnings>1 ? tr.warnings : tr.warning, this.#warnings ) );
460
+ }
461
+
462
+ // on the left, the icon of the worst
463
+ const alerts = this.query<Label>( "#alerts" );
464
+ alerts.setText( counts.join( " / " ) );
465
+ alerts.setIcon( this.#errors ? icons.danger : icons.warning );
466
+ alerts.setClass( "error", this.#errors>0 );
467
+ alerts.setClass( "warning", this.#errors==0 );
468
+
375
469
  ok.setText( _tr.global.ok );
376
470
  return;
377
471
  }
@@ -66,13 +66,12 @@ export class Notification extends Popup {
66
66
  private static list = new Set<Notification>( );
67
67
 
68
68
  constructor( props: NotificationProps ) {
69
- super( { } );
69
+ super( props );
70
70
 
71
71
  let icon = props.iconId;
72
72
  if( !icon ) {
73
73
  if( props.loading ) {
74
74
  icon = icons.loading;
75
- this.addClass( "")
76
75
  }
77
76
  else if( props.mode=="danger" ) {
78
77
  icon = icons.danger;
@@ -84,10 +83,10 @@ export class Notification extends Popup {
84
83
 
85
84
  this.addClass( props.mode );
86
85
 
86
+ // the icon of loading turns (x4rotate, base.scss)
87
87
  const _icon = new Icon( { iconId: icon } );
88
88
  if( props.loading ) {
89
- _icon.addClass( "rotate" );
90
- //this.props.modal = true;
89
+ _icon.addClass( "x4rotate" );
91
90
  }
92
91
 
93
92
  this.setContent( new HBox( {
@@ -75,6 +75,7 @@ export class Popup<P extends PopupProps = PopupProps, E extends PopupEvents = Po
75
75
 
76
76
  private _isshown = false;
77
77
  protected _ismodal = false;
78
+ private _prevFocus: HTMLElement; // modal: the element that had the focus when it opened
78
79
 
79
80
  constructor( props: P ) {
80
81
  super( props );
@@ -220,6 +221,11 @@ export class Popup<P extends PopupProps = PopupProps, E extends PopupEvents = Po
220
221
  return;
221
222
  }
222
223
 
224
+ // modal: the focus goes back to the element that had it when it opened, unless the
225
+ // user put it elsewhere (read before the popup leaves the page and its focus with it)
226
+ const active = document.activeElement;
227
+ const giveBack = this._ismodal && this._prevFocus && ( !active || active===document.body || this.dom.contains( active ) );
228
+
223
229
  this.__hide( );
224
230
  this.__remove( );
225
231
 
@@ -255,6 +261,11 @@ export class Popup<P extends PopupProps = PopupProps, E extends PopupEvents = Po
255
261
  }
256
262
  }
257
263
 
264
+ if( giveBack && this._prevFocus.isConnected ) {
265
+ this._prevFocus.focus( );
266
+ }
267
+ this._prevFocus = null;
268
+
258
269
  this._isshown = false;
259
270
  this.fire( "closed", {} );
260
271
  }
@@ -272,6 +283,10 @@ export class Popup<P extends PopupProps = PopupProps, E extends PopupEvents = Po
272
283
  this.__append( );
273
284
 
274
285
  if( this._ismodal ) {
286
+ // given back the focus when it closes; <body>: nothing had it
287
+ const active = document.activeElement as HTMLElement;
288
+ this._prevFocus = active && active!==document.body ? active : null;
289
+
275
290
  modal_stack.push( this );
276
291
  this._showModalMask( );
277
292
  }