x4js 2.3.29 → 2.3.31
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/README.md +1 -1
- package/aicontext.md +45 -7
- package/package.json +2 -1
- package/src/components/assets/icons.ts +2 -0
- package/src/components/button/button.module.scss +1 -1
- package/src/components/checkbox/checkbox.module.scss +4 -3
- package/src/components/checkbox/checkbox.ts +1 -0
- package/src/components/dialog/dialog.module.scss +2 -0
- package/src/components/dialog/dialog.ts +1 -0
- package/src/components/menu/menu.module.scss +63 -6
- package/src/components/menu/menu.ts +146 -2
- package/src/components/messages/circle-exclamation.svg +1 -1
- package/src/components/messages/circle-question.svg +1 -1
- package/src/components/messages/messages.module.scss +28 -0
- package/src/components/messages/messages.ts +7 -1
- package/src/components/notification/notification.module.scss +4 -0
- package/src/components/popup/popup.ts +51 -6
- package/src/components/radio/radio.module.scss +4 -5
- package/src/components/radio/radio.ts +1 -0
- package/src/components/shared.scss +2 -1
- package/src/components/spreadsheet/spreadsheet.ts +28 -21
- package/src/components/textedit/textedit.module.scss +1 -0
- package/src/components/textedit/textedit.ts +4 -1
- package/src/components/themes.scss +32 -10
- package/src/components/tooltips/tooltips.scss +8 -0
- package/src/components/tooltips/tooltips.ts +31 -0
- package/src/components/treeview/treeview.ts +44 -6
- package/src/core/core_clipboard.ts +25 -6
- package/src/components/menu/caret-right-solid.svg +0 -1
|
@@ -49,17 +49,57 @@ function getRoot( ) {
|
|
|
49
49
|
/**
|
|
50
50
|
* on the document while the event goes down (capture): a press outside
|
|
51
51
|
* every popup closes the group opened last, the others at the next presses.
|
|
52
|
+
* A press in a popup closes the groups opened after it, over it (a context
|
|
53
|
+
* menu, the picker of a ColorInput), not its own.
|
|
52
54
|
* The event is left as it is, what is pressed handles it as usual.
|
|
53
55
|
* One function for all the popups: it is removed by the one that empties the list.
|
|
54
56
|
*/
|
|
55
57
|
|
|
56
58
|
function dismissLast( e: UIEvent ) {
|
|
57
|
-
|
|
59
|
+
const target = e.target as Node;
|
|
60
|
+
|
|
61
|
+
// the newest popup the press is in
|
|
62
|
+
let idx = autoclose_list.length-1;
|
|
63
|
+
while( idx>=0 && !autoclose_list[idx].contains( target ) ) {
|
|
64
|
+
idx--;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// outside every popup: the newest one; dismiss( ) closes its whole group (a menu and its sub-menus)
|
|
68
|
+
if( idx<0 ) {
|
|
69
|
+
autoclose_list[autoclose_list.length-1].dismiss( );
|
|
58
70
|
return;
|
|
59
71
|
}
|
|
60
72
|
|
|
61
|
-
//
|
|
62
|
-
autoclose_list[
|
|
73
|
+
// in a popup: the groups opened after it, not its own (a menu and its sub-menus)
|
|
74
|
+
const group = autoclose_list[idx].getData( "close" );
|
|
75
|
+
const over = autoclose_list.slice( idx+1 ).filter( x => x.getData( "close" )!==group );
|
|
76
|
+
over.reverse( ).forEach( x => x.dismiss( ) );
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* a wheel outside every popup is ignored: the page under them does not scroll, it would leave
|
|
81
|
+
* them where they were. In a popup (a long menu), it scrolls as usual
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
function ignoreWheel( e: WheelEvent ) {
|
|
85
|
+
if( !autoclose_list.some( x => x.contains( e.target as Node ) ) ) {
|
|
86
|
+
e.preventDefault( );
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* the listeners of the document while a popup that closes by itself is shown
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
function watchOutside( on: boolean ) {
|
|
95
|
+
if( on ) {
|
|
96
|
+
document.addEventListener( "pointerdown", dismissLast, true );
|
|
97
|
+
document.addEventListener( "wheel", ignoreWheel, { capture: true, passive: false } );
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
document.removeEventListener( "pointerdown", dismissLast, true );
|
|
101
|
+
document.removeEventListener( "wheel", ignoreWheel, true );
|
|
102
|
+
}
|
|
63
103
|
}
|
|
64
104
|
|
|
65
105
|
/**
|
|
@@ -256,7 +296,7 @@ export class Popup<P extends PopupProps = PopupProps, E extends PopupEvents = Po
|
|
|
256
296
|
if( idx>=0 ) {
|
|
257
297
|
autoclose_list.splice( idx, 1 );
|
|
258
298
|
if( autoclose_list.length==0 ) {
|
|
259
|
-
|
|
299
|
+
watchOutside( false );
|
|
260
300
|
}
|
|
261
301
|
}
|
|
262
302
|
}
|
|
@@ -295,7 +335,7 @@ export class Popup<P extends PopupProps = PopupProps, E extends PopupEvents = Po
|
|
|
295
335
|
|
|
296
336
|
if( this.props.autoClose ) {
|
|
297
337
|
if( autoclose_list.length==0 ) {
|
|
298
|
-
|
|
338
|
+
watchOutside( true );
|
|
299
339
|
}
|
|
300
340
|
|
|
301
341
|
autoclose_list.push( this );
|
|
@@ -369,6 +409,11 @@ export class Popup<P extends PopupProps = PopupProps, E extends PopupEvents = Po
|
|
|
369
409
|
let aidx = -1;
|
|
370
410
|
if( after ) {
|
|
371
411
|
aidx = autoclose_list.indexOf( this );
|
|
412
|
+
|
|
413
|
+
// no longer open: none was opened after it
|
|
414
|
+
if( aidx<0 ) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
372
417
|
}
|
|
373
418
|
|
|
374
419
|
autoclose_list.forEach( (x,idx) => {
|
|
@@ -384,7 +429,7 @@ export class Popup<P extends PopupProps = PopupProps, E extends PopupEvents = Po
|
|
|
384
429
|
const list = inc_group.reverse( );
|
|
385
430
|
autoclose_list = excl_group;
|
|
386
431
|
if( autoclose_list.length==0 ) {
|
|
387
|
-
|
|
432
|
+
watchOutside( false );
|
|
388
433
|
}
|
|
389
434
|
|
|
390
435
|
list.forEach( x => x.close() );
|
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
|
|
23
23
|
--radio-button-border: var( --border );
|
|
24
24
|
--radio-button-color: var( --text-ternary );
|
|
25
|
-
--radio-button-color-checked: var( --accent-
|
|
25
|
+
--radio-button-color-checked: var( --accent-text );
|
|
26
26
|
--radio-button-background-checked: var( --background-secondary );
|
|
27
27
|
--radio-button-border-checked: var( --accent-background );
|
|
28
|
+
--radio-button-icon-size: 24px; // set by a class of the application on a radio: it wins over :root
|
|
28
29
|
|
|
29
30
|
--radio-danger-color: var( --alert-background );
|
|
30
31
|
--radio-danger-background: var( --alert-background-light );
|
|
@@ -132,8 +133,6 @@
|
|
|
132
133
|
// square: its former name, kept for the code that has it (not documented)
|
|
133
134
|
.x4radio.button,
|
|
134
135
|
.x4radio.square {
|
|
135
|
-
--size: 24px;
|
|
136
|
-
|
|
137
136
|
&.outline {
|
|
138
137
|
border-radius: 22px;;
|
|
139
138
|
}
|
|
@@ -159,8 +158,8 @@
|
|
|
159
158
|
#icon {
|
|
160
159
|
fill: var( --radio-button-color );
|
|
161
160
|
color: var( --radio-button-color );
|
|
162
|
-
width: var( --size );
|
|
163
|
-
height: var( --size );
|
|
161
|
+
width: var( --radio-button-icon-size );
|
|
162
|
+
height: var( --radio-button-icon-size );
|
|
164
163
|
}
|
|
165
164
|
}
|
|
166
165
|
|
|
@@ -62,8 +62,9 @@
|
|
|
62
62
|
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// its colour from the theme (--shadow-color): darker on a dark background
|
|
65
66
|
%shadow-xl {
|
|
66
|
-
box-shadow: 0 20px 25px -5px
|
|
67
|
+
box-shadow: 0 20px 25px -5px var( --shadow-color ), 0 8px 10px -6px var( --shadow-color );
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
%shadow-xxl {
|
|
@@ -388,22 +388,29 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
388
388
|
}
|
|
389
389
|
})
|
|
390
390
|
|
|
391
|
+
// the cell of the data under the mouse; undefined out of them
|
|
391
392
|
const targetCell = (e: MouseEvent) => {
|
|
392
393
|
let el = e.target as Element;
|
|
393
394
|
while (el && !el.classList.contains("cell")) {
|
|
394
395
|
el = el.parentElement;
|
|
395
396
|
}
|
|
396
397
|
|
|
397
|
-
if (el) {
|
|
398
|
-
|
|
399
|
-
return {
|
|
400
|
-
ref: cel.getIntData("ref"),
|
|
401
|
-
row: cel.getInternalData("row"),
|
|
402
|
-
col: cel.getInternalData("col"),
|
|
403
|
-
}
|
|
398
|
+
if (!el) {
|
|
399
|
+
return undefined;
|
|
404
400
|
}
|
|
405
401
|
|
|
406
|
-
|
|
402
|
+
// a cell of the header or of the footer (a title, the sizer of a column): no row
|
|
403
|
+
const cel = componentFromDOM(el);
|
|
404
|
+
const row = cel.getInternalData("row");
|
|
405
|
+
if (row === undefined) {
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
ref: cel.getIntData("ref"),
|
|
411
|
+
row,
|
|
412
|
+
col: cel.getInternalData("col"),
|
|
413
|
+
}
|
|
407
414
|
}
|
|
408
415
|
|
|
409
416
|
|
|
@@ -416,11 +423,11 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
416
423
|
// click on another cell while editing:
|
|
417
424
|
// validate now, rows may be rebuilt before the click event (and the click lost)
|
|
418
425
|
const ref = targetCell(e);
|
|
419
|
-
if( ref
|
|
426
|
+
if( ref ) {
|
|
420
427
|
this._endEdit( true, true );
|
|
421
428
|
|
|
422
429
|
if (!this._selection.has(ref.ref)) {
|
|
423
|
-
this.
|
|
430
|
+
this.clearSelection( false );
|
|
424
431
|
this._addSelection(ref.ref,true);
|
|
425
432
|
}
|
|
426
433
|
|
|
@@ -438,7 +445,7 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
438
445
|
if (ref) {
|
|
439
446
|
//TODO: multiselection
|
|
440
447
|
if (!this._selection.has(ref.ref)) {
|
|
441
|
-
this.
|
|
448
|
+
this.clearSelection( false );
|
|
442
449
|
this._addSelection(ref.ref,true);
|
|
443
450
|
}
|
|
444
451
|
}
|
|
@@ -454,7 +461,7 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
454
461
|
if (ref) {
|
|
455
462
|
//TODO: multiselection
|
|
456
463
|
if (!this._selection.has(ref.ref)) {
|
|
457
|
-
this.
|
|
464
|
+
this.clearSelection(false);
|
|
458
465
|
this._addSelection(ref.ref,true);
|
|
459
466
|
}
|
|
460
467
|
|
|
@@ -479,7 +486,7 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
479
486
|
if (ref) {
|
|
480
487
|
//TODO: multiselection
|
|
481
488
|
if (!this._selection.has(ref.ref)) {
|
|
482
|
-
this.
|
|
489
|
+
this.clearSelection( false );
|
|
483
490
|
this._addSelection(ref.ref, true );
|
|
484
491
|
}
|
|
485
492
|
|
|
@@ -638,7 +645,7 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
638
645
|
|
|
639
646
|
if (sens == kbNav.first || sens == kbNav.last) {
|
|
640
647
|
let nline = sens == kbNav.first ? 0 : this._store.getRowCount() - 1;
|
|
641
|
-
this.
|
|
648
|
+
this.clearSelection(false);
|
|
642
649
|
this._addSelection(mkid(nline,0), true);
|
|
643
650
|
this._scrollToIndex(nline);
|
|
644
651
|
return true;
|
|
@@ -649,7 +656,7 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
649
656
|
|
|
650
657
|
let nline = sens == kbNav.next ? fline + 1 : fline - 1;
|
|
651
658
|
if (nline >= 0 && nline < this._store.getRowCount()) {
|
|
652
|
-
this.
|
|
659
|
+
this.clearSelection(false);
|
|
653
660
|
this._addSelection( mkid(nline,col), true);
|
|
654
661
|
this._scrollToIndex( nline );
|
|
655
662
|
return true;
|
|
@@ -670,7 +677,7 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
670
677
|
}
|
|
671
678
|
|
|
672
679
|
if (nline != fline) {
|
|
673
|
-
this.
|
|
680
|
+
this.clearSelection( false );
|
|
674
681
|
this._addSelection(mkid(nline,col), true);
|
|
675
682
|
|
|
676
683
|
if (this._store.getRowCount() < SCROLL_LIMIT) {
|
|
@@ -686,7 +693,7 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
686
693
|
|
|
687
694
|
let ncol = sens == kbNav.right ? col+1 : col-1;
|
|
688
695
|
if (ncol >= 0 && ncol < this._columns.length ) {
|
|
689
|
-
this.
|
|
696
|
+
this.clearSelection(false);
|
|
690
697
|
this._addSelection( mkid(fline,ncol), true);
|
|
691
698
|
|
|
692
699
|
// fixed columns are always visible
|
|
@@ -1374,10 +1381,10 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
1374
1381
|
}
|
|
1375
1382
|
}
|
|
1376
1383
|
/**
|
|
1377
|
-
*
|
|
1384
|
+
* no cell selected; notify: selectionChange is fired, its selection empty
|
|
1378
1385
|
*/
|
|
1379
1386
|
|
|
1380
|
-
|
|
1387
|
+
clearSelection(notify = true) {
|
|
1381
1388
|
for (const ref of this._selection.keys()) {
|
|
1382
1389
|
const els = this.queryAll(`.cell[data-ref="${ref}"]`)
|
|
1383
1390
|
els.forEach(el => {
|
|
@@ -1433,7 +1440,7 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
1433
1440
|
|
|
1434
1441
|
selectItem(row: number, col: number, append = false) {
|
|
1435
1442
|
if (!append) {
|
|
1436
|
-
this.
|
|
1443
|
+
this.clearSelection(false);
|
|
1437
1444
|
}
|
|
1438
1445
|
|
|
1439
1446
|
this._addSelection(mkid(row, col), true);
|
|
@@ -1745,7 +1752,7 @@ export class Spreadsheet<P extends SpreadsheetProps = SpreadsheetProps, E extend
|
|
|
1745
1752
|
}
|
|
1746
1753
|
|
|
1747
1754
|
if( this._selection.size!=1 || !this._selection.has(ref) ) {
|
|
1748
|
-
this.
|
|
1755
|
+
this.clearSelection( false );
|
|
1749
1756
|
this._addSelection( ref, true );
|
|
1750
1757
|
}
|
|
1751
1758
|
|
|
@@ -88,7 +88,10 @@ export class TextEdit<E extends ComponentEvents = ComponentEvents> extends HBox<
|
|
|
88
88
|
|
|
89
89
|
constructor( props: TextEditProps ) {
|
|
90
90
|
// dom_events are for the input (focus, blur, keydown...): not on the box too,
|
|
91
|
-
// where keydown would run a second time, and focus never (it does not bubble)
|
|
91
|
+
// where keydown would run a second time, and focus never (it does not bubble).
|
|
92
|
+
// They are set when the input is made, before the handlers of Input and of the class
|
|
93
|
+
// (the keys of an AutoComplete): they run first. To run after them, add a handler to
|
|
94
|
+
// getInput( ) once the component is made.
|
|
92
95
|
super( { ...props, dom_events: undefined } );
|
|
93
96
|
|
|
94
97
|
if( !props.inputId ) {
|
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
$color--base: #569cd6;
|
|
20
20
|
$color--interval: 8.4%;
|
|
21
21
|
|
|
22
|
-
// the blues of the dark theme, from their own base
|
|
23
|
-
|
|
22
|
+
// the blues of the dark theme, from their own base, with the intervals of the light one: its accent
|
|
23
|
+
// (a60, the base less one interval) #245883
|
|
24
|
+
$color--dark-base: #2d6fa5;
|
|
24
25
|
|
|
25
26
|
:root {
|
|
26
27
|
--color-primary-a0: #ffffff;
|
|
@@ -87,12 +88,18 @@ $color--dark-base: #5796ca;
|
|
|
87
88
|
--accent-color-focus: var(--color-primary-a0);
|
|
88
89
|
--accent-color-active: var(--color-primary-a0);
|
|
89
90
|
|
|
91
|
+
// the accent as the colour of a text, on the background: a button checked, focused
|
|
92
|
+
--accent-text: var(--accent-background-active);
|
|
93
|
+
|
|
90
94
|
// Borders
|
|
91
95
|
--border: color-mix(in srgb, var(--color-primary-a100) 11.8%, var(--color-primary-a0)); // #e1e1e1
|
|
92
96
|
--border-light: color-mix(in srgb, var(--color-primary-a100) 8.2%, var(--color-primary-a0)); // #eaeaea
|
|
93
97
|
--border-hover: color-mix(in srgb, var(--color-primary-a100) 23.9%, var(--color-primary-a0)); // #c2c2c2
|
|
94
98
|
--border-focus: var(--accent-background);
|
|
95
99
|
|
|
100
|
+
// the shadow of what floats over the page (%shadow-xl: a dialog, a list, a notification, a tooltip)
|
|
101
|
+
--shadow-color: rgb(0 0 0 / 0.1);
|
|
102
|
+
|
|
96
103
|
// the red, orange and green do not follow the scale: their text stays white
|
|
97
104
|
--alert-background: var(--color-danger-a50);
|
|
98
105
|
--alert-background-active: var(--color-danger-a60);
|
|
@@ -123,14 +130,14 @@ html.x4theme-dark {
|
|
|
123
130
|
--color-primary-a20: #37373d; // list.inactiveSelectionBackground
|
|
124
131
|
--color-primary-a30: #3c3c3c; // input.border
|
|
125
132
|
|
|
126
|
-
// the blues: from $color--dark-base (chosen)
|
|
127
|
-
//
|
|
128
|
-
--color-primary-a40: #{color.adjust($color--dark-base, $lightness: -
|
|
129
|
-
--color-primary-a50: #{$color--dark-base};
|
|
130
|
-
--color-primary-a55: #{color.adjust($color--dark-base, $lightness: -5%)};
|
|
131
|
-
--color-primary-a60: #{$color--dark-base};
|
|
132
|
-
--color-primary-a70: #{color.adjust($color--dark-base, $lightness:
|
|
133
|
-
--color-primary-a80: #{color.adjust($color--dark-base, $lightness:
|
|
133
|
+
// the blues: from $color--dark-base (chosen), as the light theme makes them; a40, a selection,
|
|
134
|
+
// where VS Code has list.activeSelectionBackground
|
|
135
|
+
--color-primary-a40: #{color.adjust($color--dark-base, $lightness: -19.4%)}; // a selection
|
|
136
|
+
--color-primary-a50: #{$color--dark-base};
|
|
137
|
+
--color-primary-a55: #{color.adjust($color--dark-base, $lightness: -5%)}; // the accent hovered
|
|
138
|
+
--color-primary-a60: #{color.adjust($color--dark-base, $lightness: -$color--interval)}; // the accent
|
|
139
|
+
--color-primary-a70: #{color.adjust($color--dark-base, $lightness: -$color--interval * 2)}; // focused
|
|
140
|
+
--color-primary-a80: #{color.adjust($color--dark-base, $lightness: -$color--interval * 3)}; // active
|
|
134
141
|
|
|
135
142
|
--color-primary-a90: #cccccc;
|
|
136
143
|
--color-primary-a100: #cccccc; // foreground
|
|
@@ -158,10 +165,22 @@ html.x4theme-dark {
|
|
|
158
165
|
--accent-color-focus: #ffffff;
|
|
159
166
|
--accent-color-active: #ffffff;
|
|
160
167
|
|
|
168
|
+
// the accent as a text: the blues of the scale are dark, lost on the background; a light one (chosen)
|
|
169
|
+
--accent-text: #{color.adjust($color--dark-base, $lightness: 25%)};
|
|
170
|
+
|
|
171
|
+
// the edge of a button checked or focused, in that blue: the dark ones are lost on the background
|
|
172
|
+
--checkbox-button-border-checked: var( --accent-text );
|
|
173
|
+
--radio-button-border-checked: var( --accent-text );
|
|
174
|
+
--ol-button-border-focus: var( --accent-text );
|
|
175
|
+
|
|
161
176
|
// the title bar of a Dialog: the one of the window of VS Code
|
|
162
177
|
--color-dialog-caption: #181818; // titleBar.activeBackground
|
|
163
178
|
--color-dialog-caption-text: #cccccc; // titleBar.activeForeground
|
|
164
179
|
--color-dialog-background: #202020; // editorWidget.background
|
|
180
|
+
--color-dialog-border: #313131; // widget.border: its edge on the dark background
|
|
181
|
+
|
|
182
|
+
// a shadow seen on a dark background: darker
|
|
183
|
+
--shadow-color: rgb(0 0 0 / 0.5);
|
|
165
184
|
|
|
166
185
|
// the tiles of a MapView are light: turned dark, their colours kept
|
|
167
186
|
--mapview-tile-filter: invert( 1 ) hue-rotate( 180deg ) brightness( 0.95 ) contrast( 0.9 );
|
|
@@ -169,5 +188,8 @@ html.x4theme-dark {
|
|
|
169
188
|
// the marks of the events out of the view of a Planner: the darkest blue (a90 is the text here)
|
|
170
189
|
--planner-edge-background: var( --accent-background-active );
|
|
171
190
|
|
|
191
|
+
// the icon of a tooltip: the dark blues are lost on its background; the colour of the descriptions
|
|
192
|
+
--tooltip-icon-color: var( --text-secondary );
|
|
193
|
+
|
|
172
194
|
color-scheme: dark; // the native parts: select, scroll bars, number spinners
|
|
173
195
|
}
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
--tooltip-color: var( --color-primary-a90 );
|
|
28
28
|
--tooltip-border: var( --color-primary-a20 );
|
|
29
29
|
--tooltip-image-background: var( --background-primary );
|
|
30
|
+
|
|
31
|
+
--tooltip-max-width: 30vw; // a long text goes on the next line; none: the width of the text
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
.x4tooltip {
|
|
@@ -41,6 +43,12 @@
|
|
|
41
43
|
border: 1px solid var( --tooltip-border );
|
|
42
44
|
background-color: var( --tooltip-background );
|
|
43
45
|
width: max-content;
|
|
46
|
+
max-width: var( --tooltip-max-width );
|
|
47
|
+
|
|
48
|
+
// its content (a Box: it does not shrink) as wide as the tooltip at most: the text goes on the next line
|
|
49
|
+
> .x4hbox {
|
|
50
|
+
flex-shrink: 1;
|
|
51
|
+
}
|
|
44
52
|
|
|
45
53
|
.x4icon {
|
|
46
54
|
padding: 2px 4px;
|
|
@@ -72,6 +72,14 @@ export function initTooltips( ) {
|
|
|
72
72
|
|
|
73
73
|
}, true );
|
|
74
74
|
|
|
75
|
+
// a click: no tooltip, it would stay over what the click opens
|
|
76
|
+
document.addEventListener( "mousedown", ( ) => {
|
|
77
|
+
if( last_hit ) {
|
|
78
|
+
last_hit = null;
|
|
79
|
+
closeTT( );
|
|
80
|
+
}
|
|
81
|
+
}, true );
|
|
82
|
+
|
|
75
83
|
document.addEventListener( "mousemove", ( ev: MouseEvent ) => {
|
|
76
84
|
if( last_hit ) {
|
|
77
85
|
mouse_pos = { x:ev.pageX, y:ev.pageY }
|
|
@@ -85,6 +93,11 @@ function showTT( text: string, rc: Rect, pt: Point ) {
|
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
timer.setTimeout( "update", 300, ( ) => {
|
|
96
|
+
// its element gone in the meantime
|
|
97
|
+
if( !last_hit || isGone( last_hit ) ) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
88
101
|
tooltip.setText( unsafeHtml(text) );
|
|
89
102
|
|
|
90
103
|
let y = mouse_pos.y;
|
|
@@ -94,12 +107,29 @@ function showTT( text: string, rc: Rect, pt: Point ) {
|
|
|
94
107
|
|
|
95
108
|
//tooltip.displayNear( rc, "top left", "bottom left", {x:0,y:4} );
|
|
96
109
|
tooltip.displayAt( mouse_pos.x+17, y+5 );
|
|
110
|
+
|
|
111
|
+
// an element removed or hidden under the mouse has no mouseleave: it is checked every second
|
|
112
|
+
timer.setInterval( "check", 1000, ( ) => {
|
|
113
|
+
if( !last_hit || isGone( last_hit ) ) {
|
|
114
|
+
last_hit = null;
|
|
115
|
+
closeTT( );
|
|
116
|
+
}
|
|
117
|
+
} );
|
|
97
118
|
} );
|
|
98
119
|
}
|
|
99
120
|
|
|
100
121
|
function closeTT( ) {
|
|
101
122
|
tooltip.show( false );
|
|
102
123
|
timer.clearTimeout( "update" );
|
|
124
|
+
timer.clearInterval( "check" );
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* the element of the tooltip, not shown any more: removed from the page, or hidden
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
function isGone( el: HTMLElement ) {
|
|
132
|
+
return !el.isConnected || el.getClientRects( ).length==0;
|
|
103
133
|
}
|
|
104
134
|
|
|
105
135
|
/**
|
|
@@ -114,6 +144,7 @@ function closeTT( ) {
|
|
|
114
144
|
* --tooltip-color
|
|
115
145
|
* --tooltip-border
|
|
116
146
|
* --tooltip-image-background
|
|
147
|
+
* --tooltip-max-width
|
|
117
148
|
* ```
|
|
118
149
|
*/
|
|
119
150
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { asap } from '../../core/core_tools';
|
|
18
18
|
import { EventCallback } from '../../core/core_events';
|
|
19
|
-
import { Component, ComponentEvent, ComponentEvents, ComponentProps, EvClick, EvDblClick, EvSelectionChange } from '../../core/component';
|
|
19
|
+
import { Component, ComponentEvent, ComponentEvents, ComponentProps, EvClick, EvContextMenu, EvDblClick, EvSelectionChange } from '../../core/component';
|
|
20
20
|
|
|
21
21
|
import { ScrollView, Viewport } from '../viewport/viewport';
|
|
22
22
|
import { Label, SimpleText } from '../label/label';
|
|
@@ -59,12 +59,14 @@ export interface TreeviewProps extends Omit<ComponentProps,"content"> {
|
|
|
59
59
|
selectionChange?: EventCallback<EvSelectionChange>;
|
|
60
60
|
dblClick?: EventCallback<EvDblClick>;
|
|
61
61
|
click?: EventCallback<EvClick>;
|
|
62
|
+
contextMenu?: EventCallback<EvContextMenu>;
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
export interface TreeviewEvents extends ComponentEvents {
|
|
65
66
|
selectionChange?: EvSelectionChange;
|
|
66
67
|
dblClick?: EvDblClick;
|
|
67
68
|
click?: EvClick;
|
|
69
|
+
contextMenu?: EvContextMenu;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
/**
|
|
@@ -185,7 +187,15 @@ class CTreeViewItem extends Box {
|
|
|
185
187
|
|
|
186
188
|
|
|
187
189
|
/**
|
|
188
|
-
*
|
|
190
|
+
* A tree of items, folders that open and close.
|
|
191
|
+
*
|
|
192
|
+
* A right click selects the item under the mouse, then fires contextMenu, its context the
|
|
193
|
+
* id of the item (null out of the items); the menu of the browser is not shown.
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* new Treeview( { items, contextMenu: ( ev ) => {
|
|
197
|
+
* new Menu( { items: [ { text: "Rename", click: ( ) => rename( ev.context ) } ] } ).displayAt( ev.uievent.clientX, ev.uievent.clientY );
|
|
198
|
+
* } } );
|
|
189
199
|
*/
|
|
190
200
|
|
|
191
201
|
export class Treeview extends Component<TreeviewProps,TreeviewEvents> {
|
|
@@ -199,7 +209,7 @@ export class Treeview extends Component<TreeviewProps,TreeviewEvents> {
|
|
|
199
209
|
constructor( props: TreeviewProps ) {
|
|
200
210
|
super( props );
|
|
201
211
|
|
|
202
|
-
this.mapPropEvents( props, "selectionChange", "dblClick", "click" );
|
|
212
|
+
this.mapPropEvents( props, "selectionChange", "dblClick", "click", "contextMenu" );
|
|
203
213
|
|
|
204
214
|
const scroller = new ScrollView( { cls: "body" } );
|
|
205
215
|
this._view = scroller.getViewport( );
|
|
@@ -218,6 +228,7 @@ export class Treeview extends Component<TreeviewProps,TreeviewEvents> {
|
|
|
218
228
|
this.setDOMEvents( {
|
|
219
229
|
click: ( ev ) => this._on_click( ev ),
|
|
220
230
|
dblclick: (e) => this._on_click(e),
|
|
231
|
+
contextmenu: ( ev ) => this._on_ctx_menu( ev ),
|
|
221
232
|
keydown: ( ev ) => this._onkey( ev ),
|
|
222
233
|
});
|
|
223
234
|
|
|
@@ -244,10 +255,18 @@ export class Treeview extends Component<TreeviewProps,TreeviewEvents> {
|
|
|
244
255
|
}
|
|
245
256
|
}
|
|
246
257
|
|
|
258
|
+
/**
|
|
259
|
+
* the item of this tree an element is in, not one of a component around it; null when none
|
|
260
|
+
*/
|
|
261
|
+
|
|
262
|
+
private _itemOf( target: EventTarget ): Component {
|
|
263
|
+
const c = Component.closest( target as Element, ".item" );
|
|
264
|
+
return c && this.contains( c ) ? c : null;
|
|
265
|
+
}
|
|
266
|
+
|
|
247
267
|
private _on_click( ev: UIEvent ) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if( c && this.contains( c ) ) {
|
|
268
|
+
const c = this._itemOf( ev.target );
|
|
269
|
+
if( c ) {
|
|
251
270
|
const id = c.getInternalData( "id" );
|
|
252
271
|
|
|
253
272
|
const fev: ComponentEvent = { context:id };
|
|
@@ -268,6 +287,25 @@ export class Treeview extends Component<TreeviewProps,TreeviewEvents> {
|
|
|
268
287
|
this.clearSelection( );
|
|
269
288
|
}
|
|
270
289
|
|
|
290
|
+
/**
|
|
291
|
+
* a right click: the item under the mouse selected, then contextMenu
|
|
292
|
+
*/
|
|
293
|
+
|
|
294
|
+
private _on_ctx_menu( ev: MouseEvent ) {
|
|
295
|
+
ev.preventDefault( );
|
|
296
|
+
|
|
297
|
+
const c = this._itemOf( ev.target );
|
|
298
|
+
if( c ) {
|
|
299
|
+
const id = c.getInternalData( "id" );
|
|
300
|
+
|
|
301
|
+
this._selectItem( id, c );
|
|
302
|
+
this.fire( "contextMenu", { uievent: ev, context: id } );
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
this.fire( "contextMenu", { uievent: ev, context: null } );
|
|
307
|
+
}
|
|
308
|
+
|
|
271
309
|
private _onkey( ev: KeyboardEvent ) {
|
|
272
310
|
switch( ev.key ) {
|
|
273
311
|
case "ArrowDown": {
|
|
@@ -47,6 +47,19 @@ export interface ClipboardRead extends ClipboardContent {
|
|
|
47
47
|
/** what the application set last */
|
|
48
48
|
let kept: ClipboardContent = null;
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* The image to read among the types of the clipboard: the PNG when there is one, every
|
|
52
|
+
* program reads it; else the first image (a program puts its own format first: a SVG).
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
function imageTypeOf( types: readonly string[] ): string {
|
|
56
|
+
if( types.includes( "image/png" ) ) {
|
|
57
|
+
return "image/png";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return types.find( type => type.startsWith( "image/" ) );
|
|
61
|
+
}
|
|
62
|
+
|
|
50
63
|
function toPng( image: Blob ): Promise<Blob> {
|
|
51
64
|
return new Promise( ( resolve, reject ) => {
|
|
52
65
|
const url = URL.createObjectURL( image );
|
|
@@ -119,8 +132,9 @@ export async function setClipboard( data: ClipboardContent ): Promise<boolean> {
|
|
|
119
132
|
/**
|
|
120
133
|
* Read the clipboard, from a menu or a button.
|
|
121
134
|
*
|
|
122
|
-
* @returns what the system holds
|
|
123
|
-
* the application set last. Never files: see
|
|
135
|
+
* @returns what the system holds, its image as a PNG when it has one. When it
|
|
136
|
+
* refuses or cannot be asked, what the application set last. Never files: see
|
|
137
|
+
* getEventClipboard.
|
|
124
138
|
*
|
|
125
139
|
* @example
|
|
126
140
|
* const data = await getClipboard( );
|
|
@@ -143,12 +157,14 @@ export async function getClipboard( ): Promise<ClipboardRead> {
|
|
|
143
157
|
const data: ClipboardRead = { files: [] };
|
|
144
158
|
|
|
145
159
|
for( const item of await system.read( ) ) {
|
|
160
|
+
const imageType = imageTypeOf( item.types );
|
|
161
|
+
|
|
146
162
|
for( const type of item.types ) {
|
|
147
163
|
if( type==="text/plain" && !data.text ) {
|
|
148
164
|
const blob = await item.getType( type );
|
|
149
165
|
data.text = ( await blob.text( ) ) || undefined;
|
|
150
166
|
}
|
|
151
|
-
else if( type
|
|
167
|
+
else if( type===imageType && !data.image ) {
|
|
152
168
|
data.image = await item.getType( type );
|
|
153
169
|
}
|
|
154
170
|
}
|
|
@@ -212,8 +228,9 @@ export function setEventClipboard( ev: ClipboardEvent, data: ClipboardContent ):
|
|
|
212
228
|
}
|
|
213
229
|
|
|
214
230
|
/**
|
|
215
|
-
* Read the clipboard in a "paste" event
|
|
216
|
-
* as it is: prevent it when what was
|
|
231
|
+
* Read the clipboard in a "paste" event, its image the PNG file when there is
|
|
232
|
+
* one. The default of the event is left as it is: prevent it when what was
|
|
233
|
+
* read is used.
|
|
217
234
|
*
|
|
218
235
|
* @example
|
|
219
236
|
* this.addDOMEvent( "paste", ev => {
|
|
@@ -232,7 +249,9 @@ export function getEventClipboard( ev: ClipboardEvent ): ClipboardRead {
|
|
|
232
249
|
if( transfer ) {
|
|
233
250
|
data.text = transfer.getData( "text/plain" ) || undefined;
|
|
234
251
|
data.files = Array.from( transfer.files );
|
|
235
|
-
|
|
252
|
+
|
|
253
|
+
const imageType = imageTypeOf( data.files.map( file => file.type ) );
|
|
254
|
+
data.image = data.files.find( file => file.type===imageType );
|
|
236
255
|
}
|
|
237
256
|
|
|
238
257
|
return data;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 512" fill="var(--color-gray-8)"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M246.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-128-128c-9.2-9.2-22.9-11.9-34.9-6.9s-19.8 16.6-19.8 29.6l0 256c0 12.9 7.8 24.6 19.8 29.6s25.7 2.2 34.9-6.9l128-128z"/></svg>
|