x4js 2.3.24 → 2.3.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/aicontext.md +60 -10
- package/aiguide.md +64 -15
- package/package.json +1 -1
- package/src/components/assets/icons.ts +2 -0
- package/src/components/button/button.ts +5 -0
- package/src/components/dialog/dialog.ts +6 -0
- package/src/components/form/form.module.scss +2 -1
- package/src/components/form/form.ts +3 -0
- package/src/components/listbox/listbox.module.scss +59 -2
- package/src/components/listbox/listbox.ts +131 -21
- package/src/components/messages/messages.module.scss +70 -2
- package/src/components/messages/messages.ts +117 -23
- package/src/components/notification/notification.ts +3 -4
- package/src/components/popup/popup.ts +15 -0
- package/src/components/propgrid/propgrid.ts +30 -36
- package/src/components/tooltips/tooltips.ts +9 -0
- package/src/core/core_application.ts +7 -2
- package/src/core/core_events.ts +14 -2
- package/src/core/core_i18n.ts +28 -0
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
|
|
@@ -312,6 +315,9 @@ interface BoxProps extends ComponentProps { tag?: string }
|
|
|
312
315
|
|
|
313
316
|
### `Box.refs` — typed directory of the children to reach again later
|
|
314
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
|
+
|
|
315
321
|
`Box` owns a `protected refs: Record<string, Component>`. Declare its shape in the subclass, then assign each child
|
|
316
322
|
to `refs` while building the content: the assignment evaluates to the component, so it is written inline in the
|
|
317
323
|
content array. Do not keep such children in separate private fields, and do not use `refs.x` for a child that was
|
|
@@ -826,29 +832,46 @@ class FileDialog extends Component {} // programmatic file picker
|
|
|
826
832
|
interface ListItem {
|
|
827
833
|
id: ListboxID // number | string
|
|
828
834
|
text: string | UnsafeHtml
|
|
829
|
-
|
|
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)
|
|
830
839
|
}
|
|
831
840
|
interface ListboxProps extends Omit<ComponentProps,"content"> {
|
|
832
841
|
items?: ListItem[]
|
|
833
842
|
renderer?: (item: ListItem) => Component
|
|
834
843
|
title?: string; icon?: string
|
|
835
844
|
header?: Header; footer?: Component
|
|
836
|
-
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)
|
|
837
849
|
emptyMsg?: string // shown in place of the items while there are none
|
|
838
850
|
dblClick?: EventCallback<EvDblClick>
|
|
839
851
|
selectionChange?: EventCallback<EvSelectionChange>
|
|
840
852
|
contextMenu?: EventCallback<EvContextMenu>
|
|
853
|
+
checkChange?: EventCallback<EvCheckChange> // EvCheckChange { id: ListboxID, checked: boolean }
|
|
841
854
|
}
|
|
842
855
|
// emptyMsg: without it the text is _tr.global.empty_list ("Empty list"). It is shown by setItems
|
|
843
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.
|
|
844
864
|
class Listbox extends Component {
|
|
845
|
-
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)
|
|
846
867
|
appendItem(item: ListItem, prepend?: boolean, select?: boolean): void
|
|
847
868
|
updateItem(id: any, item: ListItem): void
|
|
848
869
|
getItem(id: ListboxID): ListItem
|
|
849
870
|
select(ids: ListboxID | ListboxID[], notify?: boolean): void
|
|
850
871
|
clearSelection(fireEvent?: boolean): void
|
|
851
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
|
|
852
875
|
filter(filter: string | RegExp): void
|
|
853
876
|
navigate(sens: kbNav): boolean
|
|
854
877
|
ensureSelectionVisible(): void
|
|
@@ -1036,6 +1059,7 @@ interface ButtonProps extends ComponentProps {
|
|
|
1036
1059
|
menu?: MenuElement[] | (() => MenuElement[]) // a click opens this Menu under the button, in place of the click event;
|
|
1037
1060
|
// a function is called at each click; the button gets the class "with-menu"
|
|
1038
1061
|
}
|
|
1062
|
+
// its <button> is type="button": in a <form>, Enter in a field does not click it. attrs: { type: "submit" } to submit
|
|
1039
1063
|
class Button extends Component {
|
|
1040
1064
|
click(): void // simulate a click
|
|
1041
1065
|
setText(text: string|UnsafeHtml): void
|
|
@@ -1078,6 +1102,8 @@ class Popup extends Box {
|
|
|
1078
1102
|
isOpen(): boolean
|
|
1079
1103
|
// show()/hide() inherited from Component; show() routes to displayCenter()
|
|
1080
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.
|
|
1081
1107
|
```
|
|
1082
1108
|
|
|
1083
1109
|
### `Dialog`
|
|
@@ -1101,6 +1127,10 @@ class Dialog extends Popup {
|
|
|
1101
1127
|
show(): this // each time: the focus on its first field
|
|
1102
1128
|
showAsync(): Promise<string> // the name of the clicked button; null when closed without a button (Escape, close box)
|
|
1103
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.
|
|
1104
1134
|
```
|
|
1105
1135
|
|
|
1106
1136
|
### `MessageBox` / `InputBox` / `PromptBox` / `ProgressionBox`
|
|
@@ -1120,10 +1150,22 @@ class PromptBox extends Dialog {
|
|
|
1120
1150
|
class ProgressionBox extends Dialog {
|
|
1121
1151
|
constructor(title: string)
|
|
1122
1152
|
addText(text: string|UnsafeHtml, perc: number): void
|
|
1123
|
-
addError(text: string|UnsafeHtml, perc: number): void
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
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).
|
|
1127
1169
|
```
|
|
1128
1170
|
|
|
1129
1171
|
### `Notification`
|
|
@@ -1154,6 +1196,9 @@ class Form extends Box {
|
|
|
1154
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)
|
|
1155
1197
|
}
|
|
1156
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.
|
|
1157
1202
|
```
|
|
1158
1203
|
|
|
1159
1204
|
---
|
|
@@ -1405,7 +1450,7 @@ class MyView extends VBox {
|
|
|
1405
1450
|
}
|
|
1406
1451
|
```
|
|
1407
1452
|
|
|
1408
|
-
- 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).
|
|
1409
1454
|
- A key typed in an `input`, a `textarea` or a `select` is not a shortcut, unless `editable` is set.
|
|
1410
1455
|
- A key that what has the focus already used (it called `preventDefault` or `stopPropagation`) is not a shortcut.
|
|
1411
1456
|
- A shortcut always takes its key (`preventDefault`). For a key that must sometimes be left alone, use a `keydown` handler instead.
|
|
@@ -1720,6 +1765,11 @@ createPainter(c2d: CanvasRenderingContext2D, w: number, h: number): CanvasEx /
|
|
|
1720
1765
|
initTooltips(): void // initialize the global tooltip system
|
|
1721
1766
|
```
|
|
1722
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
|
+
|
|
1723
1773
|
---
|
|
1724
1774
|
|
|
1725
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 ) {
|
|
@@ -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();
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|