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 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
- iconId?: string; data?: any; cls?: string; checked?: boolean
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?: true; multisel?: true
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
- clear(): void
1122
- done(): void // hide after 5s if no error, else show OK button
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
- - `GlobalEvent` for application-wide or distributed messages
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
- ### GlobalEvent
87
+ ### Global messages
88
88
 
89
- Use `GlobalEvent` for application-wide messages.
89
+ Use a global message (`Application.fireGlobal`) for application-wide messages.
90
90
 
91
- A GlobalEvent may also be transported between processes, clients or a server.
91
+ A global message may also be transported between processes, clients or a server.
92
92
 
93
- Views should not care how a GlobalEvent is transported.
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. GlobalEvent
298
+ ## 9. Global messages
299
299
 
300
- Use `GlobalEvent` for messages whose scope is the application or distributed system rather than a local component relationship.
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 GlobalEvent into the same payload strategy.
336
+ Do not force every global message into the same payload strategy.
322
337
 
323
338
  Most importantly:
324
339
 
325
- > GlobalEvent represents the message, not its transport.
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
- GlobalEvent communicates application-wide occurrences
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
- To retheme an application, redefine the primary colour scale (`--color-primary-a5` … `--color-primary-a90`) once per theme.
504
-
505
- Do not recolour components one by one, and do not override the variables derived from the scale.
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
- A dark theme gets its own scale.
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 / GlobalEvent
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x4js",
3
- "version": "2.3.23",
3
+ "version": "2.3.25",
4
4
  "type": "module",
5
5
  "main": "src/x4.ts",
6
6
  "module": "src/x4.ts",
@@ -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( --color-primary-a80 );
21
- --color-dialog-caption-text: var( --color-primary-a0 );
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.
@@ -31,8 +31,9 @@
31
31
  flex-direction: row;
32
32
  }
33
33
 
34
+ // flex: false: the height of its content (flex-grow alone would leave it at 0, flex-basis 0)
34
35
  &.no-flex {
35
- flex-grow: 0;
36
+ flex: none;
36
37
  }
37
38
  }
38
39
 
@@ -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
 
@@ -30,8 +30,8 @@
30
30
  background-color: var( --label-background );
31
31
  gap: 0.2em;
32
32
 
33
- &.al-right
34
- &.right {
33
+ &.al-right,
34
+ &.right {
35
35
  text-align: right;
36
36
  justify-content: end;
37
37
  }
@@ -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
  }