x4js 2.3.23 → 2.3.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/aicontext.md +81 -10
- package/aiguide.md +64 -15
- package/package.json +1 -1
- package/src/components/assets/icons.ts +2 -0
- package/src/components/button/button.ts +5 -0
- package/src/components/dialog/dialog.module.scss +2 -2
- package/src/components/dialog/dialog.ts +6 -0
- package/src/components/dockingview/dockingview.module.scss +0 -3
- package/src/components/form/form.module.scss +2 -1
- package/src/components/form/form.ts +3 -0
- package/src/components/keyboard/keyboard.ts +11 -1
- package/src/components/label/label.module.scss +2 -2
- package/src/components/listbox/listbox.module.scss +59 -2
- package/src/components/listbox/listbox.ts +131 -21
- package/src/components/messages/messages.module.scss +74 -5
- package/src/components/messages/messages.ts +161 -15
- package/src/components/notification/notification.ts +3 -4
- package/src/components/popup/popup.ts +15 -0
- package/src/components/propgrid/propgrid.ts +30 -36
- package/src/components/settingsview/settingsview.module.scss +0 -3
- package/src/components/themes.scss +54 -0
- package/src/components/tooltips/tooltips.scss +2 -2
- package/src/components/tooltips/tooltips.ts +9 -0
- package/src/core/core_application.ts +54 -2
- package/src/core/core_events.ts +14 -2
- package/src/core/core_i18n.ts +28 -0
package/aicontext.md
CHANGED
|
@@ -91,6 +91,8 @@ fire<K>(name: K, ev: E[K]): void
|
|
|
91
91
|
|
|
92
92
|
- `capturing = true` inserts at the head of the listener list.
|
|
93
93
|
- `fire()` automatically injects `source`, `type`, `stopPropagation()`, `preventDefault()` when absent.
|
|
94
|
+
- A listener that throws: `fire()` sends its error to `console.error`, calls the next listeners, and returns
|
|
95
|
+
normally; the one that fires never gets the error of a listener. (An `async` listener rejects its own promise.)
|
|
94
96
|
|
|
95
97
|
### `CoreEvent`
|
|
96
98
|
|
|
@@ -140,7 +142,7 @@ interface ComponentProps {
|
|
|
140
142
|
flex?: number | boolean // true → "x4flex"; number → flex-grow
|
|
141
143
|
disabled?: boolean
|
|
142
144
|
hidden?: boolean
|
|
143
|
-
tooltip?: string
|
|
145
|
+
tooltip?: string // shown as html: escape a text from a user (see initTooltips)
|
|
144
146
|
existingDOM?: HTMLElement // wrap an existing DOM node
|
|
145
147
|
}
|
|
146
148
|
|
|
@@ -220,7 +222,8 @@ childCount(): number
|
|
|
220
222
|
enumChildComponents(recursive: boolean): Component[]
|
|
221
223
|
enumChildNodes(recursive: boolean): Node[]
|
|
222
224
|
visitChildren(cb: (el: Component) => boolean): void
|
|
223
|
-
static parentElement<T>(dom: Node, cls?: Constructor<T>): T
|
|
225
|
+
static parentElement<T>(dom: Node, cls?: Constructor<T>): T // starts at the parent of dom, not at dom:
|
|
226
|
+
// test componentFromDOM(dom) first for the component of dom itself
|
|
224
227
|
```
|
|
225
228
|
|
|
226
229
|
### Visibility / state / focus / geometry
|
|
@@ -285,6 +288,9 @@ static instance<P extends Application>(): P
|
|
|
285
288
|
setEnv(name: string, value: any): void
|
|
286
289
|
getEnv(name: string, def_value?: any): any
|
|
287
290
|
static fireGlobal(msg: string, params?: any): void
|
|
291
|
+
static setTheme(name: string): void // the class x4theme-<name> on <html>: "" the light theme (default, no class),
|
|
292
|
+
// "dark" the dark theme of x4, another name a theme of the application
|
|
293
|
+
static getTheme(): string // "" for the light theme, "dark", or the name given to setTheme
|
|
288
294
|
focusNext(next: boolean): boolean
|
|
289
295
|
addShortcut(keys: string | string[], callback: (ev: KeyboardEvent) => void, options?: ShortcutOptions): void // see Shortcuts
|
|
290
296
|
setupSocketMessaging(path?: string, looseCallback?: () => void): void
|
|
@@ -309,6 +315,9 @@ interface BoxProps extends ComponentProps { tag?: string }
|
|
|
309
315
|
|
|
310
316
|
### `Box.refs` — typed directory of the children to reach again later
|
|
311
317
|
|
|
318
|
+
Only a `Box` has `refs`: a class derived from `Component` (not from `Box`) has none, and `this.refs.x = ...` there
|
|
319
|
+
compiles with a `declare refs` but throws a `TypeError` at run time.
|
|
320
|
+
|
|
312
321
|
`Box` owns a `protected refs: Record<string, Component>`. Declare its shape in the subclass, then assign each child
|
|
313
322
|
to `refs` while building the content: the assignment evaluates to the component, so it is written inline in the
|
|
314
323
|
content array. Do not keep such children in separate private fields, and do not use `refs.x` for a child that was
|
|
@@ -823,29 +832,46 @@ class FileDialog extends Component {} // programmatic file picker
|
|
|
823
832
|
interface ListItem {
|
|
824
833
|
id: ListboxID // number | string
|
|
825
834
|
text: string | UnsafeHtml
|
|
826
|
-
|
|
835
|
+
sub_cols?: (string | UnsafeHtml | Component)[] // extra columns after text (9 at most, ref-c2...ref-c10, see Header);
|
|
836
|
+
// a null value makes no cell. A Treeview draws them too
|
|
837
|
+
iconId?: string; data?: any; cls?: string
|
|
838
|
+
checked?: boolean // checkable: the starting state of the case (getChecked() gives the current one)
|
|
827
839
|
}
|
|
828
840
|
interface ListboxProps extends Omit<ComponentProps,"content"> {
|
|
829
841
|
items?: ListItem[]
|
|
830
842
|
renderer?: (item: ListItem) => Component
|
|
831
843
|
title?: string; icon?: string
|
|
832
844
|
header?: Header; footer?: Component
|
|
833
|
-
checkable?:
|
|
845
|
+
checkable?: boolean // a case at the start of each line
|
|
846
|
+
multisel?: true
|
|
847
|
+
reselect?: boolean // a click on the item already selected fires selectionChange again
|
|
848
|
+
// (lists linked together: the view shows what was clicked last)
|
|
834
849
|
emptyMsg?: string // shown in place of the items while there are none
|
|
835
850
|
dblClick?: EventCallback<EvDblClick>
|
|
836
851
|
selectionChange?: EventCallback<EvSelectionChange>
|
|
837
852
|
contextMenu?: EventCallback<EvContextMenu>
|
|
853
|
+
checkChange?: EventCallback<EvCheckChange> // EvCheckChange { id: ListboxID, checked: boolean }
|
|
838
854
|
}
|
|
839
855
|
// emptyMsg: without it the text is _tr.global.empty_list ("Empty list"). It is shown by setItems
|
|
840
856
|
// (the constructor calls it) when there is no item. The listbox then has the class "empty".
|
|
857
|
+
// click: no prop; listbox.on( "click", ev => ... ), ev.context the id of the item. It is fired on the item
|
|
858
|
+
// already selected too, which fires no selectionChange.
|
|
859
|
+
// checkable: renderItem puts the case (.x4check) first in the line, with a renderer of yours too;
|
|
860
|
+
// a checked line has the class "checked". A click on the case, or Space for the last item
|
|
861
|
+
// selected, changes it and fires checkChange: no click, no selection. A double click on the
|
|
862
|
+
// case is ignored (its two clicks change it twice). setChecked fires nothing; updateItem keeps
|
|
863
|
+
// the state of the case. With a header, the columns start after the case.
|
|
841
864
|
class Listbox extends Component {
|
|
842
|
-
setItems(items: ListItem[], keepSel?: boolean): void
|
|
865
|
+
setItems(items: ListItem[], keepSel?: boolean): void // one selectionChange at the end, with the selection it ends
|
|
866
|
+
// with, when there was one before or there is one after (keepSel: the same ids too)
|
|
843
867
|
appendItem(item: ListItem, prepend?: boolean, select?: boolean): void
|
|
844
868
|
updateItem(id: any, item: ListItem): void
|
|
845
869
|
getItem(id: ListboxID): ListItem
|
|
846
870
|
select(ids: ListboxID | ListboxID[], notify?: boolean): void
|
|
847
871
|
clearSelection(fireEvent?: boolean): void
|
|
848
872
|
getSelection(): ListboxID[]
|
|
873
|
+
getChecked(): ListboxID[] // checkable: in the order of the list
|
|
874
|
+
setChecked(ids: ListboxID[]): void // these checked, the others not; no event
|
|
849
875
|
filter(filter: string | RegExp): void
|
|
850
876
|
navigate(sens: kbNav): boolean
|
|
851
877
|
ensureSelectionVisible(): void
|
|
@@ -1033,6 +1059,7 @@ interface ButtonProps extends ComponentProps {
|
|
|
1033
1059
|
menu?: MenuElement[] | (() => MenuElement[]) // a click opens this Menu under the button, in place of the click event;
|
|
1034
1060
|
// a function is called at each click; the button gets the class "with-menu"
|
|
1035
1061
|
}
|
|
1062
|
+
// its <button> is type="button": in a <form>, Enter in a field does not click it. attrs: { type: "submit" } to submit
|
|
1036
1063
|
class Button extends Component {
|
|
1037
1064
|
click(): void // simulate a click
|
|
1038
1065
|
setText(text: string|UnsafeHtml): void
|
|
@@ -1075,6 +1102,8 @@ class Popup extends Box {
|
|
|
1075
1102
|
isOpen(): boolean
|
|
1076
1103
|
// show()/hide() inherited from Component; show() routes to displayCenter()
|
|
1077
1104
|
}
|
|
1105
|
+
// A modal popup (a Dialog, a MessageBox...) keeps the element that had the focus when it opens and gives
|
|
1106
|
+
// it back the focus when it closes, unless the focus was put elsewhere meanwhile.
|
|
1078
1107
|
```
|
|
1079
1108
|
|
|
1080
1109
|
### `Dialog`
|
|
@@ -1098,6 +1127,10 @@ class Dialog extends Popup {
|
|
|
1098
1127
|
show(): this // each time: the focus on its first field
|
|
1099
1128
|
showAsync(): Promise<string> // the name of the clicked button; null when closed without a button (Escape, close box)
|
|
1100
1129
|
}
|
|
1130
|
+
// Keys: Escape closes it (or clicks `closable` when it is a button name). Enter clicks its default
|
|
1131
|
+
// button ("ok.default" in buttons), except in a textarea or an element contenteditable, where Enter is
|
|
1132
|
+
// a new line and Ctrl+Enter (Cmd+Enter) clicks it. Out of a dialog, Enter goes to the next field,
|
|
1133
|
+
// except in a textarea or a contenteditable.
|
|
1101
1134
|
```
|
|
1102
1135
|
|
|
1103
1136
|
### `MessageBox` / `InputBox` / `PromptBox` / `ProgressionBox`
|
|
@@ -1117,10 +1150,22 @@ class PromptBox extends Dialog {
|
|
|
1117
1150
|
class ProgressionBox extends Dialog {
|
|
1118
1151
|
constructor(title: string)
|
|
1119
1152
|
addText(text: string|UnsafeHtml, perc: number): void
|
|
1120
|
-
addError(text: string|UnsafeHtml, perc: number): void
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1153
|
+
addError(text: string|UnsafeHtml, perc: number): void // its icon, in red; the title bar in red until clear()
|
|
1154
|
+
addWarning(text: string|UnsafeHtml, perc: number): void // its icon, in orange
|
|
1155
|
+
addSection(title: string|UnsafeHtml, perc: number): void // a section (<details>, open): the lines that follow go
|
|
1156
|
+
// in it; a click on its title folds it; its title has the icon
|
|
1157
|
+
// of the worst of its lines. Without any, the lines go in the log
|
|
1158
|
+
setText(text: string|UnsafeHtml, perc: number): void // in place of all the lines
|
|
1159
|
+
clear(): void // no line, no error, no button OK
|
|
1160
|
+
done(): void // shows OK: after an error or a warning it waits for a click,
|
|
1161
|
+
// their counts on the left of the bar ("2 errors / 1 warning",
|
|
1162
|
+
// from _tr.global.progression), the icon of the worst;
|
|
1163
|
+
// else it counts "OK (5s)" and closes the box at 0
|
|
1164
|
+
}
|
|
1165
|
+
// ProgressionBox: its lines in a <pre>, 10 lines high, scrolled both ways (the box keeps its size);
|
|
1166
|
+
// a string is text, an UnsafeHtml html. Ctrl+A in the lines selects them only (a folded section too).
|
|
1167
|
+
// Colours: --msgbox-log-background, -border, -border-focus, -error-color, -warning-color,
|
|
1168
|
+
// -scrollbar (the thumb of its scroll bars; their track is its background).
|
|
1124
1169
|
```
|
|
1125
1170
|
|
|
1126
1171
|
### `Notification`
|
|
@@ -1151,6 +1196,9 @@ class Form extends Box {
|
|
|
1151
1196
|
validate(): FormValues // null if invalid; the values the validator changes are written back in their fields (any element with the name, every radio of a group)
|
|
1152
1197
|
}
|
|
1153
1198
|
// Collects all children (recursive) that have a "name" attribute and implement queryInterface("form-element").
|
|
1199
|
+
// Never sent by the browser: its "submit" is prevented (Enter in its only field would send it).
|
|
1200
|
+
// A Form is flex by default (it takes the free height of its box) and has a padding of 16px.
|
|
1201
|
+
// flex: false: the height of its content (a toolbar, a line); its padding set back by a css rule.
|
|
1154
1202
|
```
|
|
1155
1203
|
|
|
1156
1204
|
---
|
|
@@ -1402,7 +1450,7 @@ class MyView extends VBox {
|
|
|
1402
1450
|
}
|
|
1403
1451
|
```
|
|
1404
1452
|
|
|
1405
|
-
- A component shortcut also works while the focus is nowhere, if the component is the last one with shortcuts that had the focus: of two views shown together, the one the user works in answers.
|
|
1453
|
+
- A component shortcut also works while the focus is nowhere, if the component is the last one with shortcuts that had the focus: of two views shown together, the one the user works in answers. Not while that component is hidden (in a tab not shown, for instance).
|
|
1406
1454
|
- A key typed in an `input`, a `textarea` or a `select` is not a shortcut, unless `editable` is set.
|
|
1407
1455
|
- A key that what has the focus already used (it called `preventDefault` or `stopPropagation`) is not a shortcut.
|
|
1408
1456
|
- A shortcut always takes its key (`preventDefault`). For a key that must sometimes be left alone, use a `keydown` handler instead.
|
|
@@ -1510,6 +1558,24 @@ isLanguage(name: string): boolean
|
|
|
1510
1558
|
|
|
1511
1559
|
---
|
|
1512
1560
|
|
|
1561
|
+
## Theme (`themes.scss`)
|
|
1562
|
+
|
|
1563
|
+
```css
|
|
1564
|
+
--color-primary-a0 … --color-primary-a100 /* the scale: a0 the background, a100 the text, the accent between */
|
|
1565
|
+
/* the globals the components use: --background-primary / -secondary / -ternary / -hover / -active / -input,
|
|
1566
|
+
--text-primary / -secondary / -ternary, --border / -light / -hover / -focus, --accent-background / -hover /
|
|
1567
|
+
-focus / -active / -light, --accent-color (the text on the accent), --disabled-*, --icon-color / -active,
|
|
1568
|
+
--alert-*, --warning-*, --success-* */
|
|
1569
|
+
```
|
|
1570
|
+
// The greys are mixes of a0 and a100: a theme that reverses the scale gets them light on dark.
|
|
1571
|
+
// Each component reads its own variables (the @cssvar of its class), which point to these globals:
|
|
1572
|
+
// a theme redefines the globals, or one variable of a component.
|
|
1573
|
+
// Themes: Application.setTheme(name) puts the class x4theme-<name> on <html>. "" is the light theme (by default),
|
|
1574
|
+
// "dark" the dark theme of x4 (the colours of VS Code Dark Modern). An application adds its own theme in its css,
|
|
1575
|
+
// html.x4theme-pink { --color-primary-a0: ...; ... }, then Application.setTheme("pink").
|
|
1576
|
+
|
|
1577
|
+
---
|
|
1578
|
+
|
|
1513
1579
|
## Utilities (`core_tools`)
|
|
1514
1580
|
|
|
1515
1581
|
```ts
|
|
@@ -1699,6 +1765,11 @@ createPainter(c2d: CanvasRenderingContext2D, w: number, h: number): CanvasEx /
|
|
|
1699
1765
|
initTooltips(): void // initialize the global tooltip system
|
|
1700
1766
|
```
|
|
1701
1767
|
|
|
1768
|
+
No tooltip is shown until the application calls `initTooltips()` once (the prop `tooltip` only sets an attribute).
|
|
1769
|
+
`initTooltips()` shows the text of the attribute `tooltip` (the prop `tooltip`), or the one `getTip()`
|
|
1770
|
+
of a `"tip-handler"` gives, **as html**. A text that comes from a user is escaped by the application
|
|
1771
|
+
with `sanitizeHtml( text )`: `tooltip: \`Open <b>${sanitizeHtml( file.name )}</b>\``.
|
|
1772
|
+
|
|
1702
1773
|
---
|
|
1703
1774
|
|
|
1704
1775
|
## State (`core_state`)
|
package/aiguide.md
CHANGED
|
@@ -20,7 +20,7 @@ Prefer:
|
|
|
20
20
|
- plain objects for application data
|
|
21
21
|
- `State` for observable application state
|
|
22
22
|
- local events for things that happened locally
|
|
23
|
-
- `
|
|
23
|
+
- a global message (`Application.fireGlobal`) for application-wide or distributed messages
|
|
24
24
|
- `DataStore` for specialized tabular data
|
|
25
25
|
- persistent `Component` objects backed by the real DOM
|
|
26
26
|
- CSS and CSS variables for presentation and theming
|
|
@@ -84,13 +84,13 @@ For a simple event payload, `context` can carry the relevant identity or value w
|
|
|
84
84
|
|
|
85
85
|
Do not put transient actions into State merely to trigger behavior.
|
|
86
86
|
|
|
87
|
-
###
|
|
87
|
+
### Global messages
|
|
88
88
|
|
|
89
|
-
Use `
|
|
89
|
+
Use a global message (`Application.fireGlobal`) for application-wide messages.
|
|
90
90
|
|
|
91
|
-
A
|
|
91
|
+
A global message may also be transported between processes, clients or a server.
|
|
92
92
|
|
|
93
|
-
Views should not care how a
|
|
93
|
+
Views should not care how a global message is transported.
|
|
94
94
|
|
|
95
95
|
In particular, do not expose WebSocket details to UI components merely because WebSocket is used as the transport.
|
|
96
96
|
|
|
@@ -295,9 +295,24 @@ Do not use events as a replacement for persistent application state.
|
|
|
295
295
|
|
|
296
296
|
---
|
|
297
297
|
|
|
298
|
-
## 9.
|
|
298
|
+
## 9. Global messages
|
|
299
299
|
|
|
300
|
-
|
|
300
|
+
A global message is sent by `Application.fireGlobal( msg, params )` and received by
|
|
301
|
+
`component.onGlobalEvent( ev => ... )`, `ev` an `EvMessage { msg, params }`. The listener of
|
|
302
|
+
`onGlobalEvent` is taken off when the component leaves the page.
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
Application.fireGlobal( "device.updated", { id: deviceId } );
|
|
306
|
+
|
|
307
|
+
this.onGlobalEvent( ( ev ) => {
|
|
308
|
+
if( ev.msg == "device.updated" ) {
|
|
309
|
+
this.refresh( ev.params.id );
|
|
310
|
+
}
|
|
311
|
+
} );
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Use a global message for what concerns the application or a distributed system, rather than a
|
|
315
|
+
local component relationship.
|
|
301
316
|
|
|
302
317
|
Global messages should normally use stable application-level names, for example:
|
|
303
318
|
|
|
@@ -318,11 +333,11 @@ The receiver can obtain the authoritative object through the application's model
|
|
|
318
333
|
|
|
319
334
|
For high-frequency realtime information such as telemetry, carrying the actual data can be appropriate.
|
|
320
335
|
|
|
321
|
-
Do not force every
|
|
336
|
+
Do not force every global message into the same payload strategy.
|
|
322
337
|
|
|
323
338
|
Most importantly:
|
|
324
339
|
|
|
325
|
-
>
|
|
340
|
+
> A global message represents the message, not its transport.
|
|
326
341
|
|
|
327
342
|
A view should normally not contain WebSocket-specific logic.
|
|
328
343
|
|
|
@@ -361,7 +376,7 @@ Model obtains or modifies domain data
|
|
|
361
376
|
State represents current application state
|
|
362
377
|
Component presents that state
|
|
363
378
|
Events communicate local occurrences
|
|
364
|
-
|
|
379
|
+
Global msg communicates application-wide occurrences (Application.fireGlobal)
|
|
365
380
|
```
|
|
366
381
|
|
|
367
382
|
Do not add additional layers unless they solve an actual problem.
|
|
@@ -500,11 +515,36 @@ Do not hide a control a component creates: configure the component through its p
|
|
|
500
515
|
|
|
501
516
|
If the props cannot express it, say so instead of working around it.
|
|
502
517
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
518
|
+
A theme is a class on `<html>`. `Application.setTheme( name )` puts `x4theme-<name>` there and
|
|
519
|
+
takes off the one before; `Application.setTheme( "" )` goes back to the light theme, the default;
|
|
520
|
+
`Application.getTheme( )` gives the name. x4 has one theme: `"dark"`, like Dark Modern of VS Code.
|
|
521
|
+
|
|
522
|
+
An application writes its own theme in its CSS, under `html.x4theme-<name>`, the way
|
|
523
|
+
`themes.scss` writes the dark one:
|
|
524
|
+
|
|
525
|
+
- the colour scale, `--color-primary-a0` (the background) to `--color-primary-a100` (the text).
|
|
526
|
+
The greys of x4 (backgrounds, texts, borders) are mixes of `a0` and `a100`: a scale whose
|
|
527
|
+
`a0` is dark and `a100` light gives them dark too;
|
|
528
|
+
- the global variables the scale does not give, when the theme wants other values:
|
|
529
|
+
`--background-*`, `--text-*`, `--border*`, `--disabled-*`, `--accent-*`, `--icon-color*`,
|
|
530
|
+
`--alert-*`, `--warning-*`, `--success-*`;
|
|
531
|
+
- `color-scheme: dark` for a dark theme: the native parts (select, scroll bars, spinners).
|
|
532
|
+
|
|
533
|
+
The variables of a component (`--listbox-*`, `--dialog-*`…, listed in the `@cssvar` of its
|
|
534
|
+
class) point to the global ones: the theme changes them all at once. Set one only when that
|
|
535
|
+
component must differ from the rest; do not recolour components one by one.
|
|
536
|
+
|
|
537
|
+
```css
|
|
538
|
+
html.x4theme-pink {
|
|
539
|
+
--color-primary-a0: #fff5f8;
|
|
540
|
+
/* … a5 to a90 … */
|
|
541
|
+
--color-primary-a100: #3a1020;
|
|
542
|
+
}
|
|
543
|
+
```
|
|
506
544
|
|
|
507
|
-
|
|
545
|
+
```ts
|
|
546
|
+
Application.setTheme( "pink" );
|
|
547
|
+
```
|
|
508
548
|
|
|
509
549
|
---
|
|
510
550
|
|
|
@@ -602,7 +642,7 @@ Component event
|
|
|
602
642
|
↓
|
|
603
643
|
Application operation
|
|
604
644
|
↓
|
|
605
|
-
State change /
|
|
645
|
+
State change / global message
|
|
606
646
|
↓
|
|
607
647
|
Existing Components update
|
|
608
648
|
```
|
|
@@ -643,6 +683,15 @@ const app_fr = { app: { open: "Ouvrir", close: "Fermer" } };
|
|
|
643
683
|
const app_en: typeof app_fr = { app: { open: "Open", close: "Close" } };
|
|
644
684
|
```
|
|
645
685
|
|
|
686
|
+
`_tr` is typed with the texts of x4 only: `_tr.app.open` does not compile. Give the
|
|
687
|
+
application its own typed view of `_tr`, once, from its first language:
|
|
688
|
+
|
|
689
|
+
```ts
|
|
690
|
+
export const tx = _tr as typeof _tr & typeof app_fr;
|
|
691
|
+
|
|
692
|
+
new Button( { label: tx.app.open } );
|
|
693
|
+
```
|
|
694
|
+
|
|
646
695
|
---
|
|
647
696
|
|
|
648
697
|
## Final rule
|
package/package.json
CHANGED
|
@@ -19,6 +19,8 @@ const x4icons = {
|
|
|
19
19
|
danger: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor"><!--!Font Awesome Free 6.5.1 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 512zm0-384c13.3 0 24 10.7 24 24V264c0 13.3-10.7 24-24 24s-24-10.7-24-24V152c0-13.3 10.7-24 24-24zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg>`,
|
|
20
20
|
loading: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" fill="currentColor"><!--!Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc.--><path d="M207.4 20.4c2.4 8.5-2.6 17.3-11.2 19.7C101.5 66.2 32 153 32 256c0 123.7 100.3 224 224 224s224-100.3 224-224c0-103-69.5-189.8-164.3-215.9c-8.5-2.4-13.5-11.2-11.2-19.7s11.2-13.5 19.7-11.2C432.5 39.1 512 138.2 512 256c0 141.4-114.6 256-256 256S0 397.4 0 256C0 138.2 79.5 39.1 187.7 9.2c8.5-2.4 17.3 2.6 19.7 11.2z"/></svg>`,
|
|
21
21
|
close_box: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" fill="currentColor"><!--!Font Awesome Pro 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc.--><path d="M192 233.4L59.5 100.9 36.9 123.5 169.4 256 36.9 388.5l22.6 22.6L192 278.6 324.5 411.1l22.6-22.6L214.6 256 347.1 123.5l-22.6-22.6L192 233.4z"/></svg>`,
|
|
22
|
+
// a triangle, a "!" cut in it
|
|
23
|
+
warning: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor"><path fill-rule="evenodd" d="M8 1L15.5 14.5H.5zM7.25 5.5h1.5V10h-1.5zM7 12a1 1 0 1 0 2 0a1 1 0 1 0-2 0z"/></svg>`,
|
|
22
24
|
};
|
|
23
25
|
|
|
24
26
|
export default x4icons;
|
|
@@ -134,6 +134,11 @@ export class Button extends Component<ButtonProps,ButtonEvents> {
|
|
|
134
134
|
constructor( props: ButtonProps ) {
|
|
135
135
|
super( { ...props, tag: 'button', content: null } );
|
|
136
136
|
|
|
137
|
+
// a button, not a submit: in a <form>, Enter in a field would click the first submit
|
|
138
|
+
if( !props.attrs?.type ) {
|
|
139
|
+
this.setAttribute( "type", "button" );
|
|
140
|
+
}
|
|
141
|
+
|
|
137
142
|
this.mapPropEvents( props, 'click' );
|
|
138
143
|
|
|
139
144
|
if( props.autorepeat ) {
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
@use "../shared";
|
|
18
18
|
|
|
19
19
|
:root {
|
|
20
|
-
--color-dialog-caption: var( --
|
|
21
|
-
--color-dialog-caption-text: var( --color
|
|
20
|
+
--color-dialog-caption: var( --accent-background-active );
|
|
21
|
+
--color-dialog-caption-text: var( --accent-color );
|
|
22
22
|
--color-dialog-background: var( --background-primary );
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -125,6 +125,12 @@ export class Dialog<P extends DialogProps = DialogProps, E extends DialogEvents
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
else if (ev.key == 'Enter') {
|
|
128
|
+
// in a text of several lines Enter is a new line: Ctrl+Enter (Cmd+Enter) validates
|
|
129
|
+
const target = ev.target as HTMLElement;
|
|
130
|
+
if( ( target.tagName==="TEXTAREA" || target.isContentEditable ) && !ev.ctrlKey && !ev.metaKey ) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
128
134
|
const def = this.query<Button>('button.default');
|
|
129
135
|
if (def) {
|
|
130
136
|
ev.preventDefault();
|
|
@@ -221,7 +221,6 @@
|
|
|
221
221
|
width: 16px;
|
|
222
222
|
height: 16px;
|
|
223
223
|
line-height: 16px;
|
|
224
|
-
border-radius: 3px;
|
|
225
224
|
text-align: center;
|
|
226
225
|
font-weight: 400;
|
|
227
226
|
cursor: pointer;
|
|
@@ -269,7 +268,6 @@
|
|
|
269
268
|
|
|
270
269
|
background: var( --dock-float-background );
|
|
271
270
|
border: 1px solid var( --dock-line );
|
|
272
|
-
border-radius: 4px;
|
|
273
271
|
box-shadow: var( --dock-float-shadow );
|
|
274
272
|
}
|
|
275
273
|
|
|
@@ -282,7 +280,6 @@
|
|
|
282
280
|
|
|
283
281
|
background: var( --dock-preview-background );
|
|
284
282
|
border: 1px solid var( --dock-accent );
|
|
285
|
-
border-radius: 3px;
|
|
286
283
|
}
|
|
287
284
|
|
|
288
285
|
// It holds the pointer during a drag: its cursor is the one that shows.
|
|
@@ -43,6 +43,9 @@ export class Form<P extends FormProps = FormProps> extends Box<P> {
|
|
|
43
43
|
constructor( props: P ) {
|
|
44
44
|
super( { tag: "form", ...props } );
|
|
45
45
|
|
|
46
|
+
// read by getValues, never sent by the browser (Enter in its only field would send it)
|
|
47
|
+
this.addDOMEvent( "submit", ( ev ) => ev.preventDefault( ) );
|
|
48
|
+
|
|
46
49
|
if( props.flex===false ) {
|
|
47
50
|
this.addClass( "no-flex" );
|
|
48
51
|
}
|
|
@@ -284,7 +284,10 @@ export class Keyboard extends HBox<KeyboardProps>
|
|
|
284
284
|
keyboard: Box;
|
|
285
285
|
visible: boolean;
|
|
286
286
|
input: HTMLInputElement;
|
|
287
|
-
|
|
287
|
+
|
|
288
|
+
// the pointerdown handled last (see handleKeyEvent)
|
|
289
|
+
private handled: UIEvent = null;
|
|
290
|
+
|
|
288
291
|
constructor( props: KeyboardProps ) {
|
|
289
292
|
super( { ...props, id: 'v-keyboard' } );
|
|
290
293
|
|
|
@@ -355,6 +358,13 @@ export class Keyboard extends HBox<KeyboardProps>
|
|
|
355
358
|
*/
|
|
356
359
|
|
|
357
360
|
private handleKeyEvent( e: UIEvent ) {
|
|
361
|
+
// the key backspace listens to pointerdown too (its autorepeat): this handler is
|
|
362
|
+
// called for its node and for the keyboard, the same event twice; one key only
|
|
363
|
+
if( e===this.handled ) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
this.handled = e;
|
|
367
|
+
|
|
358
368
|
let target = e.target as HTMLElement;
|
|
359
369
|
let key;
|
|
360
370
|
|
|
@@ -34,6 +34,13 @@
|
|
|
34
34
|
|
|
35
35
|
--listbox-title-background: var( --background-primary );
|
|
36
36
|
--listbox-title-color: var( --text-primary );
|
|
37
|
+
|
|
38
|
+
--listbox-check-border: var( --border );
|
|
39
|
+
--listbox-check-border-hover: var( --border-hover );
|
|
40
|
+
--listbox-check-border-sel: var( --accent-color );
|
|
41
|
+
--listbox-check-background: var( --accent-background );
|
|
42
|
+
--listbox-check-background-disabled: var( --disabled-background );
|
|
43
|
+
--listbox-check-color: var( --accent-color );
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
.x4listbox {
|
|
@@ -50,6 +57,11 @@
|
|
|
50
57
|
padding: 0 4px;
|
|
51
58
|
}
|
|
52
59
|
|
|
60
|
+
// the columns start after the case
|
|
61
|
+
&.checkable>.x4header {
|
|
62
|
+
padding-left: calc( 4px + 6px + 1rem );
|
|
63
|
+
}
|
|
64
|
+
|
|
53
65
|
&>.title {
|
|
54
66
|
background: var( --listbox-title-background );
|
|
55
67
|
padding: 4px;
|
|
@@ -129,9 +141,49 @@
|
|
|
129
141
|
}
|
|
130
142
|
}
|
|
131
143
|
|
|
144
|
+
// checkable: the case at the start of the line
|
|
145
|
+
&>.x4check {
|
|
146
|
+
flex: none;
|
|
147
|
+
display: inline-flex;
|
|
148
|
+
align-items: center;
|
|
149
|
+
justify-content: center;
|
|
150
|
+
align-self: center;
|
|
151
|
+
vertical-align: middle;
|
|
152
|
+
box-sizing: border-box;
|
|
153
|
+
width: 1rem;
|
|
154
|
+
height: 1rem;
|
|
155
|
+
margin-left: 6px;
|
|
156
|
+
border: 1px solid var( --listbox-check-border );
|
|
157
|
+
color: var( --listbox-check-color );
|
|
158
|
+
cursor: pointer;
|
|
159
|
+
|
|
160
|
+
svg {
|
|
161
|
+
width: 70%;
|
|
162
|
+
height: 70%;
|
|
163
|
+
visibility: hidden;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
&:hover {
|
|
167
|
+
border-color: var( --listbox-check-border-hover );
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
&.checked>.x4check {
|
|
172
|
+
background-color: var( --listbox-check-background );
|
|
173
|
+
border-color: var( --listbox-check-background );
|
|
174
|
+
|
|
175
|
+
svg {
|
|
176
|
+
visibility: visible;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
&.selected>.x4check {
|
|
181
|
+
border-color: var( --listbox-check-border-sel );
|
|
182
|
+
}
|
|
183
|
+
|
|
132
184
|
.column {
|
|
133
|
-
background-color: transparent;
|
|
134
|
-
overflow: hidden;
|
|
185
|
+
background-color: transparent;
|
|
186
|
+
overflow: hidden;
|
|
135
187
|
}
|
|
136
188
|
|
|
137
189
|
// 10 columns max (see Header)
|
|
@@ -232,6 +284,11 @@
|
|
|
232
284
|
&.selected {
|
|
233
285
|
background-color: var( --listbox-item-color-sel-disabled );
|
|
234
286
|
}
|
|
287
|
+
|
|
288
|
+
&.checked>.x4check {
|
|
289
|
+
background-color: var( --listbox-check-background-disabled );
|
|
290
|
+
border-color: var( --listbox-check-background-disabled );
|
|
291
|
+
}
|
|
235
292
|
}
|
|
236
293
|
}
|
|
237
294
|
}
|