x4js 2.3.30 → 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/aicontext.md +40 -5
- package/package.json +1 -1
- 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/menu/menu.module.scss +59 -0
- package/src/components/menu/menu.ts +142 -0
- 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 +22 -3
- package/src/components/radio/radio.module.scss +4 -5
- package/src/components/radio/radio.ts +1 -0
- 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 +14 -0
- package/src/components/treeview/treeview.ts +44 -6
- package/src/core/core_clipboard.ts +25 -6
package/aicontext.md
CHANGED
|
@@ -42,7 +42,7 @@ CoreElement<E> named timers + event surface (no DOM)
|
|
|
42
42
|
│ ├── SvgComponent → Chart
|
|
43
43
|
│ ├── CSizer → HSizer / VSizer
|
|
44
44
|
│ ├── FileDialog, ScrollView, Viewport, DockingView, Carousel, Flex, Space
|
|
45
|
-
│ ├── HBox-based: ColorInput, Rating, Switch, Tag, Keyboard, TextEdit → AutoComplete, Breadcrumbs
|
|
45
|
+
│ ├── HBox-based: ColorInput, Rating, Switch, Tag, Keyboard, MenuBar, TextEdit → AutoComplete, Breadcrumbs
|
|
46
46
|
│ └── VBox-based: Calendar, ColorPicker, FileDrop, Panel, PropertyGrid, TextArea, SettingsView, SettingItem
|
|
47
47
|
└── Application<E> singleton, no DOM of its own
|
|
48
48
|
|
|
@@ -832,7 +832,10 @@ type TextEditProps = (TextInputProps | NumberProps | DateProps | TimeProps) & {
|
|
|
832
832
|
labelWidth?: number; inputWidth?: number; inputId?: string
|
|
833
833
|
inputGadgets?: Component[]; inputAttrs?: any
|
|
834
834
|
}
|
|
835
|
-
// style, tooltip, hidden, flex, stretch, height: on the box (label + field); dom_events: on the input
|
|
835
|
+
// style, tooltip, hidden, flex, stretch, height: on the box (label + field); dom_events: on the input,
|
|
836
|
+
// set when it is made, before the handlers of Input and of the class (the keys of an AutoComplete): they run
|
|
837
|
+
// first, and see the state before the class reacts (Enter before the suggestion is chosen). To run after them:
|
|
838
|
+
// textEdit.getInput( ).addDOMEvent( "keydown", ... ) once the component is made
|
|
836
839
|
class TextEdit extends HBox {
|
|
837
840
|
getValue(): string; setValue(value: string): void; getInput(): Input
|
|
838
841
|
}
|
|
@@ -886,6 +889,7 @@ interface CheckboxProps extends ComponentProps {
|
|
|
886
889
|
// button: "button danger"); without the box, the input is still reached by Tab (--checkbox-button-*, -danger-*)
|
|
887
890
|
// A toggle button is a Checkbox with cls "button": getCheck() tells whether it is pressed, change when it changes
|
|
888
891
|
// new Checkbox( { cls: "button", icon: icon_bold, tooltip: "Bold", change: ( ev ) => this.setBold( ev.value ) } )
|
|
892
|
+
// its icon 24px: --checkbox-button-icon-size, set by a class of the application ( .small { --checkbox-button-icon-size: 16px } )
|
|
889
893
|
// in a Form: without value (or a boolean one), a boolean; with one, the value when checked, null when not
|
|
890
894
|
class Checkbox extends Component {
|
|
891
895
|
getCheck(): boolean; setCheck(ck: boolean): void
|
|
@@ -911,6 +915,7 @@ class Radio extends Component {
|
|
|
911
915
|
}
|
|
912
916
|
// cls: "button" (a button; "button outline" rounded), "danger" (checked in red, its round or its button: "button
|
|
913
917
|
// danger"). Without the round, the input is still reached by Tab.
|
|
918
|
+
// its icon 24px: --radio-button-icon-size, set by a class of the application ( .small { --radio-button-icon-size: 16px } )
|
|
914
919
|
```
|
|
915
920
|
|
|
916
921
|
### `Switch`
|
|
@@ -1162,6 +1167,7 @@ interface TreeviewProps extends Omit<ComponentProps,"content"> {
|
|
|
1162
1167
|
click?: EventCallback<EvClick>
|
|
1163
1168
|
dblClick?: EventCallback<EvDblClick>
|
|
1164
1169
|
selectionChange?: EventCallback<EvSelectionChange>
|
|
1170
|
+
contextMenu?: EventCallback<EvContextMenu> // as a Listbox: the item under the mouse selected, ev.context its id (null out of the items), no menu of the browser
|
|
1165
1171
|
}
|
|
1166
1172
|
// emptyMsg: as for a Listbox. Without it the text is _tr.global.empty_list ("Empty list").
|
|
1167
1173
|
// There is no `renderer`: an item is drawn from its `text`, `iconId`, `cls` and `sub_cols`.
|
|
@@ -1194,6 +1200,7 @@ interface SpreadsheetColumn { // GridColumn +
|
|
|
1194
1200
|
interface EvCellChange { row; col /* index */; colId /* store key */; value /* writable */; oldValue }
|
|
1195
1201
|
class Spreadsheet extends Component {
|
|
1196
1202
|
getSelection(): /* cell */; navigate(sens: kbNav): boolean; lock(lock): void
|
|
1203
|
+
clearSelection(notify = true): void // no cell selected; notify: selectionChange, its selection empty
|
|
1197
1204
|
editCell(row, col, clear = false): boolean // cell must be visible
|
|
1198
1205
|
stopEdit(commit = true): void; isEditing(): boolean
|
|
1199
1206
|
}
|
|
@@ -1266,13 +1273,33 @@ class PropertyGrid extends VBox {
|
|
|
1266
1273
|
interface MenuItem {
|
|
1267
1274
|
text: string | UnsafeHtml
|
|
1268
1275
|
cls?: string; icon?: string; menu?: Menu; disabled?: boolean
|
|
1276
|
+
checked?: boolean | (() => boolean) // a check as icon when there is no icon
|
|
1269
1277
|
click?: DOMEventHandler
|
|
1278
|
+
shortcut?: string // "Mod+S": shown at the right (shortcutText), not added: see addShortcut
|
|
1270
1279
|
}
|
|
1271
1280
|
type MenuElement = MenuItem | Component | string // string "-" = title/separator
|
|
1272
1281
|
interface MenuProps extends Omit<PopupProps,"content"> { items: MenuElement[] }
|
|
1273
1282
|
class Menu extends Popup {} // displayAt/displayNear/displayCenter/close
|
|
1274
1283
|
```
|
|
1275
1284
|
|
|
1285
|
+
### `MenuBar`
|
|
1286
|
+
|
|
1287
|
+
```ts
|
|
1288
|
+
interface MenuBarItem {
|
|
1289
|
+
text: string; icon?: string; disabled?: boolean
|
|
1290
|
+
items: MenuElement[] | (() => MenuElement[]) // its Menu, built at each opening; a function is called then
|
|
1291
|
+
}
|
|
1292
|
+
interface MenuBarProps extends Omit<BoxProps,"content"> { items: MenuBarItem[] }
|
|
1293
|
+
class MenuBar extends HBox {}
|
|
1294
|
+
// a click on a title opens its menu under it, a second one closes it; while a menu is open, the mouse
|
|
1295
|
+
// on another title opens that one. No keyboard. For a web application: a desktop application
|
|
1296
|
+
// (Electron...) should use the menu of the system, not a MenuBar
|
|
1297
|
+
new MenuBar({ items: [
|
|
1298
|
+
{ text: "File", items: [{ text: "Open…", shortcut: "Mod+O", click: open }, "-", { text: "Quit", click: quit }] },
|
|
1299
|
+
{ text: "Edit", items: () => [{ text: "Undo", disabled: !canUndo(), click: undo }] },
|
|
1300
|
+
] })
|
|
1301
|
+
```
|
|
1302
|
+
|
|
1276
1303
|
### `BtnGroup`
|
|
1277
1304
|
|
|
1278
1305
|
```ts
|
|
@@ -1336,7 +1363,8 @@ class Tabs extends Box {
|
|
|
1336
1363
|
```ts
|
|
1337
1364
|
interface PopupProps extends ComponentProps {
|
|
1338
1365
|
autoClose?: boolean | string // true, or a group name: closed by a press outside; while it is shown,
|
|
1339
|
-
// a wheel outside every popup is ignored (the page does not scroll)
|
|
1366
|
+
// a wheel outside every popup is ignored (the page does not scroll);
|
|
1367
|
+
// a press in a popup closes the other groups opened after it, not its own
|
|
1340
1368
|
sizable?: boolean; movable?: boolean
|
|
1341
1369
|
}
|
|
1342
1370
|
class Popup extends Box {
|
|
@@ -1344,7 +1372,8 @@ class Popup extends Box {
|
|
|
1344
1372
|
displayNear(rc: Rect, dst?: string, src?: string, offset?: {x,y}): void
|
|
1345
1373
|
displayCenter(center?: boolean): void
|
|
1346
1374
|
close(): void
|
|
1347
|
-
dismiss(after?: boolean): void
|
|
1375
|
+
dismiss(after?: boolean): void // closes the popups of its autoClose group; after: only those opened
|
|
1376
|
+
// after it (none when it is no longer open)
|
|
1348
1377
|
isOpen(): boolean
|
|
1349
1378
|
// show()/hide() inherited from Component; show() routes to displayCenter()
|
|
1350
1379
|
}
|
|
@@ -1386,6 +1415,10 @@ class MessageBox extends Dialog {
|
|
|
1386
1415
|
static showAsync(msg: string|UnsafeHtml, buttons?: BtnGroupItem[], title?: string,
|
|
1387
1416
|
icon_type?: "error"|"question"): Promise<string>
|
|
1388
1417
|
}
|
|
1418
|
+
// no icon_type: an error (its icon, the title _tr.global.error, the class "error"); its title bar and its buttons
|
|
1419
|
+
// in the colours of the alerts: a filled button in --alert-*, an outline one with a red border when focused.
|
|
1420
|
+
// The "!" of an error, the "?" of a question: white in the disc of the icon (--msgbox-icon-mark-color,
|
|
1421
|
+
// --msgbox-question-icon-mark-color)
|
|
1389
1422
|
class InputBox extends Dialog {
|
|
1390
1423
|
getValue(): string
|
|
1391
1424
|
// static showAsync(msg, value, title?, options?: {password?, trim?}): Promise<string> (null if cancelled or closed)
|
|
@@ -1760,6 +1793,7 @@ new Button({ label: 'Paste', click: async () => {
|
|
|
1760
1793
|
- The events go to the component that has the focus: give it a `tabindex`.
|
|
1761
1794
|
- Out of an event, the browser may refuse, and there is no system clipboard on a page that is not served in https. What the application set last is then what `getClipboard` gives: copy and paste still work inside the application.
|
|
1762
1795
|
- An image is written as a PNG; another format is converted. Files can be read in a `paste` event only, and cannot be written.
|
|
1796
|
+
- An image offered in several formats (Inkscape: a SVG, then a PNG) is read as the PNG, by `getClipboard` and `getEventClipboard`; else the first image.
|
|
1763
1797
|
|
|
1764
1798
|
---
|
|
1765
1799
|
|
|
@@ -1835,7 +1869,8 @@ by the browser, in its own locale, whatever this one.
|
|
|
1835
1869
|
--color-primary-a0 … --color-primary-a100 /* the scale: a0 the background, a100 the text, the accent between */
|
|
1836
1870
|
/* the globals the components use: --background-primary / -secondary / -ternary / -hover / -active / -input,
|
|
1837
1871
|
--text-primary / -secondary / -ternary, --border / -light / -hover / -focus, --accent-background / -hover /
|
|
1838
|
-
-focus / -active / -light, --accent-color (the text on the accent), --
|
|
1872
|
+
-focus / -active / -light, --accent-color (the text on the accent), --accent-text (the accent as a text on the
|
|
1873
|
+
background: a button checked or focused; light in the dark theme), --disabled-*, --icon-color / -active,
|
|
1839
1874
|
--alert-*, --warning-*, --success-*, --shadow-color (the shadow of what floats: dialog, list, notification, tooltip) */
|
|
1840
1875
|
```
|
|
1841
1876
|
// The greys are mixes of a0 and a100: a theme that reverses the scale gets them light on dark.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "x4js",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.31",
|
|
4
4
|
"description": "A full TypeScript framework for web and desktop applications: components written as classes (layouts, inputs, lists, grids, dialogs, forms, charts...) and a command line to create, serve and build them.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -29,9 +29,10 @@
|
|
|
29
29
|
|
|
30
30
|
--checkbox-button-border: var( --border );
|
|
31
31
|
--checkbox-button-color: var( --text-ternary );
|
|
32
|
-
--checkbox-button-color-checked: var( --accent-
|
|
32
|
+
--checkbox-button-color-checked: var( --accent-text );
|
|
33
33
|
--checkbox-button-background-checked: var( --background-secondary );
|
|
34
34
|
--checkbox-button-border-checked: var( --accent-background );
|
|
35
|
+
--checkbox-button-icon-size: 24px; // set by a class of the application on a checkbox: it wins over :root
|
|
35
36
|
|
|
36
37
|
--checkbox-danger-color: var( --alert-background );
|
|
37
38
|
--checkbox-danger-background: var( --alert-background-light );
|
|
@@ -187,8 +188,8 @@
|
|
|
187
188
|
#icon {
|
|
188
189
|
color: var( --checkbox-ink );
|
|
189
190
|
fill: var( --checkbox-ink );
|
|
190
|
-
width:
|
|
191
|
-
height:
|
|
191
|
+
width: var( --checkbox-button-icon-size );
|
|
192
|
+
height: var( --checkbox-button-icon-size );
|
|
192
193
|
}
|
|
193
194
|
}
|
|
194
195
|
|
|
@@ -54,6 +54,7 @@ interface CheckboxProps extends ComponentProps {
|
|
|
54
54
|
* --checkbox-button-color-checked
|
|
55
55
|
* --checkbox-button-background-checked
|
|
56
56
|
* --checkbox-button-border-checked
|
|
57
|
+
* --checkbox-button-icon-size
|
|
57
58
|
* --checkbox-danger-color
|
|
58
59
|
* --checkbox-danger-background
|
|
59
60
|
* ```
|
|
@@ -25,6 +25,15 @@
|
|
|
25
25
|
--menuitem-color-active: var( --accent-color-active );
|
|
26
26
|
--menuitem-background-hover: var( --accent-background-hover );
|
|
27
27
|
--menuitem-color-hover: var( --accent-color-hover );
|
|
28
|
+
--menuitem-shortcut-color: var( --text-secondary );
|
|
29
|
+
|
|
30
|
+
--menubar-background: var( --background-primary );
|
|
31
|
+
--menubar-border: var( --border );
|
|
32
|
+
|
|
33
|
+
--menubaritem-color: var( --text-primary );
|
|
34
|
+
--menubaritem-background-hover: var( --accent-background-hover );
|
|
35
|
+
--menubaritem-color-hover: var( --accent-color-hover );
|
|
36
|
+
--menubaritem-color-disabled: var( --disabled-color-dark );
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
.x4menu {
|
|
@@ -84,6 +93,20 @@
|
|
|
84
93
|
@extend %flex;
|
|
85
94
|
}
|
|
86
95
|
|
|
96
|
+
// the keys of the command: after the text, which takes the width, at the right
|
|
97
|
+
#shortcut {
|
|
98
|
+
color: var(--menuitem-shortcut-color);
|
|
99
|
+
padding-left: 16px;
|
|
100
|
+
white-space: nowrap;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// hovered, active, disabled: in the colour of the text
|
|
104
|
+
&:hover, &.opened, &:active, &[disabled] {
|
|
105
|
+
#shortcut {
|
|
106
|
+
color: inherit;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
87
110
|
&.title {
|
|
88
111
|
color: var( --text-primary );
|
|
89
112
|
cursor: auto;
|
|
@@ -124,4 +147,40 @@
|
|
|
124
147
|
border-bottom: 1px solid var(--border);
|
|
125
148
|
margin-bottom: 4px;
|
|
126
149
|
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.x4menubar {
|
|
153
|
+
padding: 0 4px;
|
|
154
|
+
|
|
155
|
+
background-color: var(--menubar-background);
|
|
156
|
+
border-bottom: 1px solid var(--menubar-border);
|
|
157
|
+
|
|
158
|
+
.x4cmenubaritem {
|
|
159
|
+
@extend %hbox;
|
|
160
|
+
|
|
161
|
+
gap: 6px;
|
|
162
|
+
padding: 4px 10px;
|
|
163
|
+
color: var(--menubaritem-color);
|
|
164
|
+
cursor: pointer;
|
|
165
|
+
user-select: none;
|
|
166
|
+
|
|
167
|
+
transition: background-color 0.1s;
|
|
168
|
+
|
|
169
|
+
#icon {
|
|
170
|
+
width: 16px;
|
|
171
|
+
height: 16px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// hovered, or its menu open: as an item of the menu
|
|
175
|
+
&:hover:not([disabled]),
|
|
176
|
+
&.opened {
|
|
177
|
+
background-color: var(--menubaritem-background-hover);
|
|
178
|
+
color: var(--menubaritem-color-hover);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
&[disabled] {
|
|
182
|
+
color: var(--menubaritem-color-disabled);
|
|
183
|
+
cursor: auto;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
127
186
|
}
|
|
@@ -17,9 +17,11 @@
|
|
|
17
17
|
import { Component } from "../../core/component"
|
|
18
18
|
import { DOMEventHandler } from '../../core/core_dom';
|
|
19
19
|
import { Timer, UnsafeHtml, isFunction, isString } from '../../core/core_tools';
|
|
20
|
+
import { shortcutText } from '../../core/core_shortcuts';
|
|
20
21
|
|
|
21
22
|
import { Popup, PopupProps } from '../popup/popup';
|
|
22
23
|
import { Icon } from '../icon/icon';
|
|
24
|
+
import { BoxProps, HBox } from '../boxes/boxes';
|
|
23
25
|
|
|
24
26
|
import icons from "../assets/icons";
|
|
25
27
|
import "./menu.module.scss"
|
|
@@ -38,6 +40,13 @@ export interface MenuItem {
|
|
|
38
40
|
disabled?: boolean;
|
|
39
41
|
checked?: boolean | ( () => boolean);
|
|
40
42
|
click?: DOMEventHandler;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* the keys of the command, shown at the right of the item: "Mod+S" is written
|
|
46
|
+
* "Ctrl+S", "⌘S" on a Mac (shortcutText). Shown only: the shortcut itself
|
|
47
|
+
* is added by Application.addShortcut or Component.addShortcut
|
|
48
|
+
*/
|
|
49
|
+
shortcut?: string;
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
export type MenuElement = MenuItem | Component | string;
|
|
@@ -56,6 +65,7 @@ export interface MenuProps extends Omit<PopupProps,"content"> {
|
|
|
56
65
|
* --menuitem-color-active
|
|
57
66
|
* --menuitem-background-hover
|
|
58
67
|
* --menuitem-color-hover
|
|
68
|
+
* --menuitem-shortcut-color
|
|
59
69
|
* ```
|
|
60
70
|
*/
|
|
61
71
|
|
|
@@ -97,6 +107,7 @@ class CMenuItem extends Component {
|
|
|
97
107
|
this.setContent( [
|
|
98
108
|
new Icon({ id:"icon", iconId }),
|
|
99
109
|
new Component( { id: "text", content: itm.text } ),
|
|
110
|
+
itm.shortcut ? new Component( { id: "shortcut", content: shortcutText( itm.shortcut ) } ) : null,
|
|
100
111
|
// a sub menu: a triangle at the right, in the colour of the text
|
|
101
112
|
itm.menu ? new Icon( { id: "sub", iconId: icons.caret } ) : null,
|
|
102
113
|
]);
|
|
@@ -207,3 +218,134 @@ export class Menu extends Popup {
|
|
|
207
218
|
}
|
|
208
219
|
}
|
|
209
220
|
|
|
221
|
+
/**
|
|
222
|
+
*
|
|
223
|
+
*/
|
|
224
|
+
|
|
225
|
+
export interface MenuBarItem {
|
|
226
|
+
text: string;
|
|
227
|
+
icon?: string;
|
|
228
|
+
disabled?: boolean;
|
|
229
|
+
|
|
230
|
+
/** its menu; a function is called at each opening: it gives the items of that time */
|
|
231
|
+
items: MenuElement[] | ( ( ) => MenuElement[] );
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface MenuBarProps extends Omit<BoxProps,"content"> {
|
|
235
|
+
items: MenuBarItem[];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* a title of the bar
|
|
240
|
+
*/
|
|
241
|
+
|
|
242
|
+
class CMenuBarItem extends Component {
|
|
243
|
+
static classPrefix = "x4";
|
|
244
|
+
|
|
245
|
+
constructor( itm: MenuBarItem ) {
|
|
246
|
+
super( { disabled: itm.disabled } );
|
|
247
|
+
|
|
248
|
+
this.setContent( [
|
|
249
|
+
itm.icon ? new Icon( { id: "icon", iconId: itm.icon } ) : null,
|
|
250
|
+
new Component( { id: "text", content: itm.text } ),
|
|
251
|
+
] );
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* A row of titles, each one opens its menu under it.
|
|
257
|
+
* A click on a title opens its menu, a second one closes it. While a menu is open, the
|
|
258
|
+
* mouse on another title opens the menu of that one.
|
|
259
|
+
*
|
|
260
|
+
* For a web application. A desktop application (Electron...) should not use it: the menu
|
|
261
|
+
* of the system is preferable, where the user expects it (the top of the screen on a Mac)
|
|
262
|
+
* and with the keyboard of the system.
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* new MenuBar( { items: [
|
|
266
|
+
* { text: "File", items: [ { text: "Open…", shortcut: "Mod+O", click: open }, "-", { text: "Quit", click: quit } ] },
|
|
267
|
+
* { text: "Edit", items: ( ) => [ { text: "Undo", disabled: !canUndo( ), click: undo } ] },
|
|
268
|
+
* ] } );
|
|
269
|
+
*
|
|
270
|
+
* @cssvar
|
|
271
|
+
* ```
|
|
272
|
+
* --menubar-background
|
|
273
|
+
* --menubar-border
|
|
274
|
+
* --menubaritem-color
|
|
275
|
+
* --menubaritem-background-hover
|
|
276
|
+
* --menubaritem-color-hover
|
|
277
|
+
* --menubaritem-color-disabled
|
|
278
|
+
* ```
|
|
279
|
+
*/
|
|
280
|
+
|
|
281
|
+
export class MenuBar extends HBox<MenuBarProps> {
|
|
282
|
+
static classPrefix = "x4";
|
|
283
|
+
|
|
284
|
+
private _menu: Menu; // the menu open, null when none
|
|
285
|
+
private _title: CMenuBarItem; // the title of that menu
|
|
286
|
+
private _closed: CMenuBarItem; // the title whose menu closed last
|
|
287
|
+
private _closedAt = -1; // when (performance.now)
|
|
288
|
+
|
|
289
|
+
constructor( props: MenuBarProps ) {
|
|
290
|
+
super( props );
|
|
291
|
+
|
|
292
|
+
this.setContent( props.items.map( itm => {
|
|
293
|
+
const title = new CMenuBarItem( itm );
|
|
294
|
+
|
|
295
|
+
// The menu closes by itself as soon as the button goes down outside of it,
|
|
296
|
+
// before the title hears of it: it was the menu of this title if it closed
|
|
297
|
+
// since this very press began.
|
|
298
|
+
let wasOpen = false;
|
|
299
|
+
|
|
300
|
+
title.addDOMEvent( "pointerdown", ev => { wasOpen = this._closed===title && this._closedAt >= ev.timeStamp; } );
|
|
301
|
+
title.addDOMEvent( "click", ( ) => {
|
|
302
|
+
if( !wasOpen ) {
|
|
303
|
+
this._open( title, itm );
|
|
304
|
+
}
|
|
305
|
+
} );
|
|
306
|
+
|
|
307
|
+
title.addDOMEvent( "mouseenter", ( ) => {
|
|
308
|
+
if( this._menu && this._title!==title ) {
|
|
309
|
+
this._open( title, itm );
|
|
310
|
+
}
|
|
311
|
+
} );
|
|
312
|
+
|
|
313
|
+
return title;
|
|
314
|
+
} ) );
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* closes the menu open, opens the one of the title under it
|
|
319
|
+
*/
|
|
320
|
+
|
|
321
|
+
private _open( title: CMenuBarItem, itm: MenuBarItem ) {
|
|
322
|
+
if( title.isDisabled( ) ) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if( this._menu ) {
|
|
327
|
+
this._menu.dismiss( );
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const menu = new Menu( { items: isFunction( itm.items ) ? itm.items( ) : itm.items } );
|
|
331
|
+
|
|
332
|
+
menu.on( "closed", ( ) => {
|
|
333
|
+
title.removeClass( "opened" );
|
|
334
|
+
|
|
335
|
+
this._closed = title;
|
|
336
|
+
this._closedAt = performance.now( );
|
|
337
|
+
|
|
338
|
+
if( this._menu===menu ) {
|
|
339
|
+
this._menu = null;
|
|
340
|
+
this._title = null;
|
|
341
|
+
}
|
|
342
|
+
} );
|
|
343
|
+
|
|
344
|
+
this._menu = menu;
|
|
345
|
+
this._title = title;
|
|
346
|
+
title.addClass( "opened" );
|
|
347
|
+
|
|
348
|
+
menu.displayNear( title.getBoundingRect( ), "top left", "bottom left" );
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512z"/><path class="mark" d="M256 128c13.3 0 24 10.7 24 24l0 112c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-112c0-13.3 10.7-24 24-24zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" fill="currentColor"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M320 576C461.4 576 576 461.4 576 320C576 178.6 461.4 64 320 64C178.6 64 64 178.6 64 320C64 461.4 178.6 576 320
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" fill="currentColor"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M320 576C461.4 576 576 461.4 576 320C576 178.6 461.4 64 320 64C178.6 64 64 178.6 64 320C64 461.4 178.6 576 320 576z"/><path class="mark" d="M320 240C302.3 240 288 254.3 288 272C288 285.3 277.3 296 264 296C250.7 296 240 285.3 240 272C240 227.8 275.8 192 320 192C364.2 192 400 227.8 400 272C400 319.2 364 339.2 344 346.5L344 350.3C344 363.6 333.3 374.3 320 374.3C306.7 374.3 296 363.6 296 350.3L296 342.2C296 321.7 310.8 307 326.1 302C332.5 299.9 339.3 296.5 344.3 291.7C348.6 287.5 352 281.7 352 272.1C352 254.4 337.7 240.1 320 240.1zM288 432C288 414.3 302.3 400 320 400C337.7 400 352 414.3 352 432C352 449.7 337.7 464 320 464C302.3 464 288 449.7 288 432z"/></svg>
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
--msgbox-icon-color: var( --alert-background );
|
|
23
23
|
--msgbox-question-icon-color: var( --accent-background ); // a question, an InputBox, a PromptBox
|
|
24
24
|
|
|
25
|
+
// the "!" and the "?" in the disc of the icon: as the text on the red, on the accent
|
|
26
|
+
--msgbox-icon-mark-color: var( --alert-color );
|
|
27
|
+
--msgbox-question-icon-mark-color: var( --accent-color );
|
|
28
|
+
|
|
25
29
|
--msgbox-log-background: var( --background-ternary ); // the lines of a ProgressionBox
|
|
26
30
|
--msgbox-log-border: var( --border );
|
|
27
31
|
--msgbox-log-border-focus: var( --border-focus );
|
|
@@ -44,6 +48,22 @@
|
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
// the buttons of an error in the red of its title bar: a filled one, the focus of an outline
|
|
52
|
+
// one (its text stays the one of the text: a red text is not read on a dark background)
|
|
53
|
+
&.error {
|
|
54
|
+
--button-background: var( --alert-background );
|
|
55
|
+
--button-background-hover: var( --alert-background-active );
|
|
56
|
+
--button-background-focus: var( --alert-background-active );
|
|
57
|
+
--button-background-active: var( --alert-background-active );
|
|
58
|
+
--button-color: var( --alert-color );
|
|
59
|
+
--button-color-hover: var( --alert-color );
|
|
60
|
+
--button-color-focus: var( --alert-color );
|
|
61
|
+
--button-color-active: var( --alert-color );
|
|
62
|
+
|
|
63
|
+
--ol-button-border-focus: var( --alert-background );
|
|
64
|
+
--ol-button-color-focus: var( --ol-button-color );
|
|
65
|
+
}
|
|
66
|
+
|
|
47
67
|
& .x4form {
|
|
48
68
|
min-width: 400px;
|
|
49
69
|
|
|
@@ -51,6 +71,10 @@
|
|
|
51
71
|
> .x4hbox > .x4icon {
|
|
52
72
|
color: var( --msgbox-icon-color );
|
|
53
73
|
height: 96px;
|
|
74
|
+
|
|
75
|
+
.mark {
|
|
76
|
+
fill: var( --msgbox-icon-mark-color );
|
|
77
|
+
}
|
|
54
78
|
}
|
|
55
79
|
|
|
56
80
|
.x4label {
|
|
@@ -61,6 +85,10 @@
|
|
|
61
85
|
|
|
62
86
|
&.question .x4form > .x4hbox > .x4icon {
|
|
63
87
|
color: var( --msgbox-question-icon-color );
|
|
88
|
+
|
|
89
|
+
.mark {
|
|
90
|
+
fill: var( --msgbox-question-icon-mark-color );
|
|
91
|
+
}
|
|
64
92
|
}
|
|
65
93
|
}
|
|
66
94
|
|
|
@@ -37,6 +37,8 @@ export interface MessageBoxProps extends DialogProps {
|
|
|
37
37
|
* --msgbox-caption-color
|
|
38
38
|
* --msgbox-icon-color
|
|
39
39
|
* --msgbox-question-icon-color
|
|
40
|
+
* --msgbox-icon-mark-color
|
|
41
|
+
* --msgbox-question-icon-mark-color
|
|
40
42
|
* --msgbox-log-background
|
|
41
43
|
* --msgbox-log-border
|
|
42
44
|
* --msgbox-log-border-focus
|
|
@@ -47,7 +49,11 @@ export interface MessageBoxProps extends DialogProps {
|
|
|
47
49
|
* --msgbox-log-scrollbar
|
|
48
50
|
* ```
|
|
49
51
|
* --msgbox-caption-* and --msgbox-icon-color are the ones of an error; a question, an InputBox
|
|
50
|
-
* and a PromptBox have the title bar of a Dialog and their icon in --msgbox-question-icon-color
|
|
52
|
+
* and a PromptBox have the title bar of a Dialog and their icon in --msgbox-question-icon-color.
|
|
53
|
+
* The "!" of an error and the "?" of a question are drawn in the disc of their icon, in
|
|
54
|
+
* --msgbox-icon-mark-color and --msgbox-question-icon-mark-color.
|
|
55
|
+
* The buttons of an error are in the colours of the alerts (--alert-*): a filled one, the border
|
|
56
|
+
* of an outline one that has the focus
|
|
51
57
|
*/
|
|
52
58
|
|
|
53
59
|
export class MessageBox extends Dialog<DialogProps>
|
|
@@ -54,7 +54,10 @@
|
|
|
54
54
|
color: var( --notification-icon-default );
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// a box does not shrink (.x4box): this one does, down to the frame, its close button kept in it
|
|
57
58
|
.body {
|
|
59
|
+
flex-shrink: 1;
|
|
60
|
+
|
|
58
61
|
.title {
|
|
59
62
|
font-weight: bold;
|
|
60
63
|
padding: 6px 6px 3px 6px;
|
|
@@ -64,6 +67,7 @@
|
|
|
64
67
|
.text {
|
|
65
68
|
padding: 3px 6px 6px 6px;
|
|
66
69
|
color: var( --notification-text-color );
|
|
70
|
+
overflow-wrap: anywhere; // a long word, an address: cut at the edge
|
|
67
71
|
}
|
|
68
72
|
}
|
|
69
73
|
|
|
@@ -49,17 +49,31 @@ 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( ) );
|
|
63
77
|
}
|
|
64
78
|
|
|
65
79
|
/**
|
|
@@ -395,6 +409,11 @@ export class Popup<P extends PopupProps = PopupProps, E extends PopupEvents = Po
|
|
|
395
409
|
let aidx = -1;
|
|
396
410
|
if( after ) {
|
|
397
411
|
aidx = autoclose_list.indexOf( this );
|
|
412
|
+
|
|
413
|
+
// no longer open: none was opened after it
|
|
414
|
+
if( aidx<0 ) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
398
417
|
}
|
|
399
418
|
|
|
400
419
|
autoclose_list.forEach( (x,idx) => {
|
|
@@ -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
|
|
|
@@ -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 ) {
|
|
@@ -88,6 +88,9 @@ $color--dark-base: #2d6fa5;
|
|
|
88
88
|
--accent-color-focus: var(--color-primary-a0);
|
|
89
89
|
--accent-color-active: var(--color-primary-a0);
|
|
90
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
|
+
|
|
91
94
|
// Borders
|
|
92
95
|
--border: color-mix(in srgb, var(--color-primary-a100) 11.8%, var(--color-primary-a0)); // #e1e1e1
|
|
93
96
|
--border-light: color-mix(in srgb, var(--color-primary-a100) 8.2%, var(--color-primary-a0)); // #eaeaea
|
|
@@ -162,6 +165,14 @@ html.x4theme-dark {
|
|
|
162
165
|
--accent-color-focus: #ffffff;
|
|
163
166
|
--accent-color-active: #ffffff;
|
|
164
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
|
+
|
|
165
176
|
// the title bar of a Dialog: the one of the window of VS Code
|
|
166
177
|
--color-dialog-caption: #181818; // titleBar.activeBackground
|
|
167
178
|
--color-dialog-caption-text: #cccccc; // titleBar.activeForeground
|
|
@@ -177,5 +188,8 @@ html.x4theme-dark {
|
|
|
177
188
|
// the marks of the events out of the view of a Planner: the darkest blue (a90 is the text here)
|
|
178
189
|
--planner-edge-background: var( --accent-background-active );
|
|
179
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
|
+
|
|
180
194
|
color-scheme: dark; // the native parts: select, scroll bars, number spinners
|
|
181
195
|
}
|
|
@@ -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;
|