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.
@@ -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
- if( autoclose_list.some( x => x.contains( e.target as Node ) ) ) {
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
- // the newest popup: dismiss( ) closes its whole group (a menu and its sub-menus)
62
- autoclose_list[autoclose_list.length-1].dismiss( );
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
- document.removeEventListener( "pointerdown", dismissLast, true );
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
- document.addEventListener( "pointerdown", dismissLast, true );
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
- document.removeEventListener( "pointerdown", dismissLast, true );
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-background-active );
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,6 +62,7 @@ interface RadioProps extends ComponentProps {
62
62
  * --radio-button-color-checked
63
63
  * --radio-button-background-checked
64
64
  * --radio-button-border-checked
65
+ * --radio-button-icon-size
65
66
  * --radio-danger-color
66
67
  * --radio-danger-background
67
68
  * ```
@@ -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 rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
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
- const cel = componentFromDOM(el);
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
- return undefined;
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 && ref.row!==undefined ) {
426
+ if( ref ) {
420
427
  this._endEdit( true, true );
421
428
 
422
429
  if (!this._selection.has(ref.ref)) {
423
- this._clearSelection( false );
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._clearSelection( false );
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._clearSelection(false);
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._clearSelection( false );
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._clearSelection(false);
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._clearSelection(false);
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._clearSelection( false );
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._clearSelection(false);
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
- private _clearSelection(notify = true) {
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._clearSelection(false);
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._clearSelection( false );
1755
+ this.clearSelection( false );
1749
1756
  this._addSelection( ref, true );
1750
1757
  }
1751
1758
 
@@ -59,6 +59,7 @@
59
59
  @extend %flex;
60
60
  outline: none;
61
61
  margin: 0;
62
+ align-self: stretch; // the height of the zone: its ground down to its line
62
63
  }
63
64
 
64
65
  .x4button {
@@ -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
- $color--dark-base: #5796ca;
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); where VS Code has list.activeSelectionBackground,
127
- // button.background and focusBorder, button.hoverBackground
128
- --color-primary-a40: #{color.adjust($color--dark-base, $lightness: -35%)}; // a selection
129
- --color-primary-a50: #{$color--dark-base}; // a button, the focus
130
- --color-primary-a55: #{color.adjust($color--dark-base, $lightness: -5%)}; // a button hovered
131
- --color-primary-a60: #{$color--dark-base};
132
- --color-primary-a70: #{color.adjust($color--dark-base, $lightness: -5%)};
133
- --color-primary-a80: #{color.adjust($color--dark-base, $lightness: -35%)};
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
- // an item of this tree, not of a component around it
249
- const c = Component.closest( ev.target as Element, ".item" );
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. When it refuses or cannot be asked, what
123
- * the application set last. Never files: see getEventClipboard.
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.startsWith( "image/" ) && !data.image ) {
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. The default of the event is left
216
- * as it is: prevent it when what was read is used.
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
- data.image = data.files.find( file => file.type.startsWith( "image/" ) );
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>