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.
- package/aicontext.md +60 -10
- package/aiguide.md +64 -15
- package/package.json +1 -1
- package/src/components/assets/icons.ts +2 -0
- package/src/components/button/button.ts +5 -0
- package/src/components/dialog/dialog.ts +6 -0
- package/src/components/form/form.module.scss +2 -1
- package/src/components/form/form.ts +3 -0
- package/src/components/listbox/listbox.module.scss +59 -2
- package/src/components/listbox/listbox.ts +131 -21
- package/src/components/messages/messages.module.scss +70 -2
- package/src/components/messages/messages.ts +117 -23
- package/src/components/notification/notification.ts +3 -4
- package/src/components/popup/popup.ts +15 -0
- package/src/components/propgrid/propgrid.ts +30 -36
- package/src/components/tooltips/tooltips.ts +9 -0
- package/src/core/core_application.ts +7 -2
- package/src/core/core_events.ts +14 -2
- package/src/core/core_i18n.ts +28 -0
|
@@ -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?:
|
|
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
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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:
|
|
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
|
|
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
|
|
283
|
-
*
|
|
284
|
-
*
|
|
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
|
-
|
|
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
|
|
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.#
|
|
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
|
|
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.#
|
|
391
|
+
this.#errors = 0;
|
|
392
|
+
this.#warnings = 0;
|
|
351
393
|
this.clearInterval( "close" );
|
|
352
394
|
this.query( "#btnbar" ).show( false );
|
|
353
|
-
this.
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
const
|
|
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
|
-
|
|
362
|
-
|
|
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
|
|
367
|
-
*
|
|
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.#
|
|
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( "
|
|
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
|
}
|