x4js 2.3.17 → 2.3.18
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 +163 -25
- package/package.json +1 -1
- package/src/components/gridview/gridview.ts +350 -175
- package/src/components/keyboard/keyboard.ts +181 -65
- package/src/components/spreadsheet/spreadsheet.ts +27 -1
- package/src/components/tag/tag.ts +1 -2
- package/src/components/textarea/textarea.module.scss +6 -0
- package/src/components/textarea/textarea.ts +48 -10
- package/src/core/component.ts +16 -9
- package/src/core/core_clipboard.ts +239 -0
- package/src/core/core_data.ts +54 -23
- package/src/core/core_dom.ts +27 -5
- package/src/core/core_dragdrop.ts +11 -5
- package/src/core/core_element.ts +1 -1
- package/src/core/core_shortcuts.ts +1 -1
- package/src/x4.d.ts +1 -0
- package/src/x4.ts +1 -0
package/aicontext.md
CHANGED
|
@@ -34,13 +34,13 @@ CoreElement<E> named timers + event surface (no DOM)
|
|
|
34
34
|
│ │ ├── Popup → Dialog → MessageBox / InputBox / PromptBox / ProgressionBox
|
|
35
35
|
│ │ │ → Menu, DropdownList, Notification
|
|
36
36
|
│ │ ├── BtnGroup, Tabs, Video, Saturation
|
|
37
|
-
│ ├── Button, Input, Icon, Label, SimpleText, Checkbox, Radio, Link
|
|
37
|
+
│ ├── Button, Input, Icon, Label, SimpleText, Checkbox, Radio, Link, SimpleTextArea
|
|
38
38
|
│ ├── Listbox, Gridview, Treeview, Select, Combobox, Slider, Gauge
|
|
39
39
|
│ ├── Progress, TickLine, Canvas, Image, Spreadsheet, MonacoEditor
|
|
40
40
|
│ ├── SvgComponent → Chart
|
|
41
41
|
│ ├── CSizer → HSizer / VSizer
|
|
42
42
|
│ ├── FileDialog, ScrollView, Viewport, DockingView, Flex, Space
|
|
43
|
-
│ ├── HBox-based: ColorInput, Rating, Switch, Keyboard, TextEdit → AutoComplete, Breadcrumbs
|
|
43
|
+
│ ├── HBox-based: ColorInput, Rating, Switch, Tag, Keyboard, TextEdit → AutoComplete, Breadcrumbs
|
|
44
44
|
│ └── VBox-based: Calendar, ColorPicker, FileDrop, Panel, PropertyGrid, TextArea
|
|
45
45
|
└── Application<E> singleton, no DOM of its own
|
|
46
46
|
|
|
@@ -228,7 +228,7 @@ static parentElement<T>(dom: Node, cls?: Constructor<T>): T
|
|
|
228
228
|
```ts
|
|
229
229
|
show(vis?: boolean): this // toggles "x4hidden" class (not display:none)
|
|
230
230
|
hide(): this
|
|
231
|
-
enable(ena?: boolean): this // sets "disabled"
|
|
231
|
+
enable(ena?: boolean): this // sets or removes "disabled" and "inert" on the component
|
|
232
232
|
disable(): this
|
|
233
233
|
isDisabled(): string | null
|
|
234
234
|
isVisible(): boolean
|
|
@@ -537,6 +537,23 @@ class Image extends Component {
|
|
|
537
537
|
}
|
|
538
538
|
```
|
|
539
539
|
|
|
540
|
+
### `Tag` — a short text in a rounded frame, with an icon
|
|
541
|
+
|
|
542
|
+
```ts
|
|
543
|
+
interface TagProps extends ComponentProps {
|
|
544
|
+
label?: string | UnsafeHtml // the text: `label`, not `text` as in Label
|
|
545
|
+
icon?: string
|
|
546
|
+
}
|
|
547
|
+
class Tag extends HBox {} // no method: its text and its icon are those of its props
|
|
548
|
+
// cls "alert": the colors of an alert
|
|
549
|
+
// CSS vars: --tag-background --tag-color --tag-icon-color --tag-border
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
```ts
|
|
553
|
+
new Tag({ label: 'Draft' })
|
|
554
|
+
new Tag({ label: 'Late', icon: 'assets/clock.svg', cls: 'alert' })
|
|
555
|
+
```
|
|
556
|
+
|
|
540
557
|
### `Progress`
|
|
541
558
|
|
|
542
559
|
```ts
|
|
@@ -619,17 +636,38 @@ class TextEdit extends HBox {
|
|
|
619
636
|
}
|
|
620
637
|
```
|
|
621
638
|
|
|
622
|
-
### `TextArea`
|
|
639
|
+
### `TextArea`, `SimpleTextArea` — native `<textarea>`, with or without a label
|
|
623
640
|
|
|
624
641
|
```ts
|
|
625
|
-
interface
|
|
626
|
-
|
|
627
|
-
resize?: boolean | "none" | "vertical" | "horizontal" | "both"
|
|
642
|
+
interface SimpleTextAreaProps extends BaseProps { // BaseProps: see Input (name, readonly, required, placeholder, autofocus, focus, change)
|
|
643
|
+
value?: string
|
|
644
|
+
resize?: boolean | "none" | "vertical" | "horizontal" | "both" // false = "none"
|
|
628
645
|
trim?: boolean // getText() trims, unless false
|
|
629
646
|
}
|
|
630
|
-
|
|
647
|
+
interface TextAreaProps extends SimpleTextAreaProps { label?: string }
|
|
648
|
+
|
|
649
|
+
class SimpleTextArea extends Component { // the <textarea> alone
|
|
650
|
+
getText(): string; setText(text: string): void
|
|
651
|
+
scrollToBottom(): void
|
|
652
|
+
isValid(): boolean // false when it is required and empty
|
|
653
|
+
}
|
|
654
|
+
class TextArea extends VBox { // a Label above a SimpleTextArea
|
|
655
|
+
getText(): string; setText(text: string): void
|
|
656
|
+
scrollToBottom(): void
|
|
657
|
+
isValid(): boolean
|
|
658
|
+
}
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
```ts
|
|
662
|
+
new TextArea({ label: 'Comment', name: 'comment', placeholder: 'A few words...', resize: 'vertical' }) // in a form
|
|
663
|
+
new SimpleTextArea({ flex: 1, placeholder: 'Your message', resize: false }) // no label
|
|
631
664
|
```
|
|
632
665
|
|
|
666
|
+
- `TextArea` in a form; `SimpleTextArea` where a label has no place (a message to type, a log).
|
|
667
|
+
- Both are form elements: with a `name`, `Form.getValues()` and `setValues()` read and write them.
|
|
668
|
+
- The value is read and written with `getText()` / `setText()`, not `getValue()` / `setValue()`.
|
|
669
|
+
- `change` is fired at each key that is typed, with the text as `ev.value`; `focus` when the focus comes and goes (`ev.focus_out`). As for an `Input`.
|
|
670
|
+
- `required`: an empty text is not valid. `Form.validate()` then returns `null`, or tells its validator that the form is not valid.
|
|
633
671
|
### `Checkbox`
|
|
634
672
|
|
|
635
673
|
```ts
|
|
@@ -784,10 +822,13 @@ interface ListboxProps extends Omit<ComponentProps,"content"> {
|
|
|
784
822
|
title?: string; icon?: string
|
|
785
823
|
header?: Header; footer?: Component
|
|
786
824
|
checkable?: true; multisel?: true
|
|
825
|
+
emptyMsg?: string // shown in place of the items while there are none
|
|
787
826
|
dblClick?: EventCallback<EvDblClick>
|
|
788
827
|
selectionChange?: EventCallback<EvSelectionChange>
|
|
789
828
|
contextMenu?: EventCallback<EvContextMenu>
|
|
790
829
|
}
|
|
830
|
+
// emptyMsg: without it the text is _tr.global.empty_list ("Empty list"). It is shown by setItems
|
|
831
|
+
// (the constructor calls it) when there is no item. The listbox then has the class "empty".
|
|
791
832
|
class Listbox extends Component {
|
|
792
833
|
setItems(items: ListItem[], keepSel?: boolean): void
|
|
793
834
|
appendItem(item: ListItem, prepend?: boolean, select?: boolean): void
|
|
@@ -801,7 +842,6 @@ class Listbox extends Component {
|
|
|
801
842
|
ensureSelectionVisible(): void
|
|
802
843
|
renderItem(item: ListItem): Component // override to customize
|
|
803
844
|
defaultRenderer(item: ListItem): Component
|
|
804
|
-
preventFocus: boolean
|
|
805
845
|
}
|
|
806
846
|
```
|
|
807
847
|
|
|
@@ -812,38 +852,51 @@ interface GridviewProps extends ComponentProps {
|
|
|
812
852
|
store: DataStore // required
|
|
813
853
|
columns: GridColumn[] // required
|
|
814
854
|
footer?: boolean; emptyMsg?: string
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
855
|
+
multisel?: boolean // Ctrl+click adds or removes a row, Shift+click a range, Ctrl+A all
|
|
856
|
+
sort?: { field_id: string; asc?: boolean } // the first sort; ascending only when asc is true
|
|
857
|
+
click?: EventCallback<EvClick> // a click on a row; ev.context is its record
|
|
858
|
+
dblClick?: EventCallback<EvDblClick> // ev.context is the record of the row
|
|
818
859
|
contextMenu?: EventCallback<EvContextMenu>
|
|
819
860
|
selectionChange?: EventCallback<EvSelectionChange>
|
|
820
861
|
}
|
|
821
862
|
class Gridview extends Component {
|
|
822
863
|
setStore(store: DataStore): void
|
|
823
|
-
setColumns(columns: GridColumn[]): void
|
|
824
864
|
setColTitle(col_name: string, title: string): void
|
|
825
|
-
sortCol(
|
|
865
|
+
sortCol(colId: any, ascending: boolean): void // colId: the id of the column, not its index
|
|
826
866
|
getView(): DataView
|
|
827
|
-
getSelection(): DataRecord[]
|
|
828
|
-
getFirstSel(): DataRecord
|
|
867
|
+
getSelection(): DataRecord[] // null when nothing is selected
|
|
868
|
+
getFirstSel(): DataRecord // null when nothing is selected
|
|
829
869
|
selectItem(id, ensureVisible?: boolean): void
|
|
830
870
|
clearSelection(): void
|
|
871
|
+
selectAll(): void // multisel only; keeps the id of every record: costs memory on a very large view
|
|
831
872
|
navigate(sens: kbNav): boolean
|
|
832
873
|
lock(lock: boolean): void
|
|
833
874
|
}
|
|
875
|
+
// Selection: the selected records, not their rows. It survives a sort; a record that leaves the view (filter, delete) leaves it, with a selectionChange.
|
|
876
|
+
// getSelection(): every selected record, or null. selectionChange: ev.selection has them all.
|
|
877
|
+
// Column sizer: dragging it gives the column that width (it is not flex anymore); a double click on it makes the column flex again.
|
|
834
878
|
```
|
|
835
879
|
|
|
836
880
|
### `Treeview`
|
|
837
881
|
|
|
838
882
|
```ts
|
|
839
883
|
interface TreeItem extends ListItem { children?: TreeItem[]; open?: boolean }
|
|
840
|
-
interface TreeviewProps extends ComponentProps {
|
|
884
|
+
interface TreeviewProps extends Omit<ComponentProps,"content"> {
|
|
885
|
+
items: TreeItem[] // required
|
|
886
|
+
footer?: Component
|
|
887
|
+
emptyMsg?: string // shown in place of the items while there are none
|
|
888
|
+
click?: EventCallback<EvClick>
|
|
889
|
+
dblClick?: EventCallback<EvDblClick>
|
|
890
|
+
selectionChange?: EventCallback<EvSelectionChange>
|
|
891
|
+
}
|
|
892
|
+
// emptyMsg: as for a Listbox. Without it the text is _tr.global.empty_list ("Empty list").
|
|
893
|
+
// There is no `renderer`: an item is drawn from its `text`, `iconId`, `cls` and `sub_cols`.
|
|
841
894
|
class Treeview extends Component {
|
|
842
895
|
setItems(items: TreeItem[]): void
|
|
843
|
-
getSelection(): ListboxID
|
|
844
|
-
selectItem(id): void
|
|
896
|
+
getSelection(): ListboxID // one item, not an array
|
|
897
|
+
selectItem(id: ListboxID): void
|
|
845
898
|
clearSelection(): void
|
|
846
|
-
navigate(sens:
|
|
899
|
+
navigate(sens: kbTreeNav): boolean // first, prev, next, last, parent, child, expand, collapse, toggle
|
|
847
900
|
}
|
|
848
901
|
```
|
|
849
902
|
|
|
@@ -871,6 +924,7 @@ class Spreadsheet extends Component {
|
|
|
871
924
|
stopEdit(commit = true): void; isEditing(): boolean
|
|
872
925
|
}
|
|
873
926
|
|
|
927
|
+
// Column sizer: dragging it gives the column that width (it is not flex anymore); a double click on it makes the column flex again.
|
|
874
928
|
// Inline edition: F2 / Enter / double-click / typing starts, Enter / Tab / click elsewhere validates, Escape cancels,
|
|
875
929
|
// Delete clears, editable checkboxes toggle on double-click or Space (a single click only selects).
|
|
876
930
|
// Default editor is an Input matching the column type.
|
|
@@ -1116,7 +1170,13 @@ class ColorPicker extends VBox { // props: color, swatches?, nullable?
|
|
|
1116
1170
|
}
|
|
1117
1171
|
class Panel extends VBox { setTitle(title: string): void }
|
|
1118
1172
|
class Breadcrumbs extends HBox { setItems(elements): void }
|
|
1119
|
-
|
|
1173
|
+
interface KeyboardProps extends BoxProps { locale?: string } // "fr-FR" (default), "de-CH", "pl"...
|
|
1174
|
+
class Keyboard extends HBox { // virtual keyboard, shown while an <input> has the focus
|
|
1175
|
+
setLocale(locale: string): void
|
|
1176
|
+
setZoom(perc: number): void
|
|
1177
|
+
showOn(el: Component): void
|
|
1178
|
+
}
|
|
1179
|
+
// languages: cs da de en es et fi fr ga hr hu it lt lv mt nl pl pt ro sk sl sv
|
|
1120
1180
|
class TickLine extends Component
|
|
1121
1181
|
class Header extends HBox // resizable column header; updates target style "--{name}-width"
|
|
1122
1182
|
```
|
|
@@ -1189,6 +1249,8 @@ class DataView extends CoreElement {
|
|
|
1189
1249
|
getRecId(rec): any; indexOfId(id): number
|
|
1190
1250
|
getStore(): DataStore; getModel(): DataModel
|
|
1191
1251
|
forEach(cb): void; changed(): void
|
|
1252
|
+
close(): void // stops following the store; call it on a view you made (createView) and do not use anymore
|
|
1253
|
+
open(): void // follows the store again after a close(), filtered and sorted with its data of the moment
|
|
1192
1254
|
}
|
|
1193
1255
|
```
|
|
1194
1256
|
|
|
@@ -1256,6 +1318,80 @@ class MyView extends VBox {
|
|
|
1256
1318
|
|
|
1257
1319
|
---
|
|
1258
1320
|
|
|
1321
|
+
## Clipboard (`core_clipboard`)
|
|
1322
|
+
|
|
1323
|
+
The clipboard of the system: a text, an image, files. Never call `navigator.clipboard` or `ev.clipboardData`.
|
|
1324
|
+
|
|
1325
|
+
```ts
|
|
1326
|
+
interface ClipboardContent { text?: string; image?: Blob }
|
|
1327
|
+
interface ClipboardRead extends ClipboardContent { files: File[] } // files: only in an event
|
|
1328
|
+
|
|
1329
|
+
// from a menu or a button: asynchronous, never fail
|
|
1330
|
+
setClipboard(data: ClipboardContent): Promise<boolean> // false: the system refused
|
|
1331
|
+
getClipboard(): Promise<ClipboardRead>
|
|
1332
|
+
canReadClipboard(): boolean // false: nothing to paste, disable the item
|
|
1333
|
+
|
|
1334
|
+
// in a "copy", "cut" or "paste" event
|
|
1335
|
+
setEventClipboard(ev: ClipboardEvent, data: ClipboardContent): void // prevents the default
|
|
1336
|
+
getEventClipboard(ev: ClipboardEvent): ClipboardRead // synchronous, default left as it is
|
|
1337
|
+
```
|
|
1338
|
+
|
|
1339
|
+
```ts
|
|
1340
|
+
this.addDOMEvent('copy', ev => setEventClipboard(ev, { text: this.getSelectedText() }))
|
|
1341
|
+
|
|
1342
|
+
this.addDOMEvent('paste', ev => {
|
|
1343
|
+
const data = getEventClipboard(ev)
|
|
1344
|
+
if (data.image) {
|
|
1345
|
+
ev.preventDefault()
|
|
1346
|
+
this.setPicture(data.image)
|
|
1347
|
+
}
|
|
1348
|
+
})
|
|
1349
|
+
|
|
1350
|
+
new Button({ label: 'Paste', click: async () => {
|
|
1351
|
+
const data = await getClipboard()
|
|
1352
|
+
if (data.text) this.insert(data.text)
|
|
1353
|
+
} })
|
|
1354
|
+
```
|
|
1355
|
+
|
|
1356
|
+
- The keys of the user (Ctrl+C, Ctrl+V) are the `copy`, `cut` and `paste` events, not shortcuts: the browser then gives its clipboard without asking for a permission.
|
|
1357
|
+
- The events go to the component that has the focus: give it a `tabindex`.
|
|
1358
|
+
- 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.
|
|
1359
|
+
- An image is written as a PNG; another format is converted. Files can be read in a `paste` event only, and cannot be written.
|
|
1360
|
+
|
|
1361
|
+
---
|
|
1362
|
+
|
|
1363
|
+
## Drag and drop (`core_dragdrop`)
|
|
1364
|
+
|
|
1365
|
+
`dragManager` makes a component something the user drags, and another one a place where things are dropped: components of the application, or files that come from the desktop.
|
|
1366
|
+
|
|
1367
|
+
```ts
|
|
1368
|
+
interface DropInfo { pt: Point; data: DataTransfer }
|
|
1369
|
+
type DropCallback = (command: 'enter' | 'leave' | 'drag' | 'drop', el: Component, infos: DropInfo) => void
|
|
1370
|
+
type DropFilterCallback = (el: Component, data: DataTransfer) => boolean // false: nothing can be dropped here
|
|
1371
|
+
|
|
1372
|
+
dragManager.registerDraggableElement(el: Component): void
|
|
1373
|
+
dragManager.registerDropTarget(el: Component, cb: DropCallback, filterCB?: DropFilterCallback): void
|
|
1374
|
+
```
|
|
1375
|
+
|
|
1376
|
+
```ts
|
|
1377
|
+
dragManager.registerDraggableElement(card)
|
|
1378
|
+
|
|
1379
|
+
dragManager.registerDropTarget(column, (cmd, el, infos) => {
|
|
1380
|
+
if (cmd == 'drop') {
|
|
1381
|
+
if (infos.data.files.length) this.upload(infos.data.files) // files from the desktop
|
|
1382
|
+
else this.moveCard(el, infos.pt) // el: the component that was dragged
|
|
1383
|
+
}
|
|
1384
|
+
})
|
|
1385
|
+
```
|
|
1386
|
+
|
|
1387
|
+
- `registerDraggableElement`: while it is dragged, the component has the class `dragging`, and a copy of it with the class `dragged`, added to the body, follows the pointer. Nothing else is done: no data of the application goes with the drag, the target gets the component itself.
|
|
1388
|
+
- The callback of a target: `enter` and `leave` when the pointer comes over it and goes away, `drag` again and again while it is over it, `drop`. Only `drag` and `drop` have `infos`; `pt` is in the coordinates of the page.
|
|
1389
|
+
- `enter` and `leave` are found by a timer: they may come up to 300 ms late, and a `leave` also follows a `drop`. Between them the target has the class `drop-over`.
|
|
1390
|
+
- `el` in the callbacks is the dragged component, not the target. There is none for files that come from the desktop: read `infos.data.files`.
|
|
1391
|
+
- `Image` (`candrop`) and `FileDrop` already use it for files.
|
|
1392
|
+
|
|
1393
|
+
---
|
|
1394
|
+
|
|
1259
1395
|
## i18n (`core_i18n`)
|
|
1260
1396
|
|
|
1261
1397
|
Two built-in languages (`fr` default, and `en`).
|
|
@@ -1351,8 +1487,9 @@ interface ITabHandler { focusNext(next: boolean): boolean }
|
|
|
1351
1487
|
interface ITipHandler { getTip(): string }
|
|
1352
1488
|
|
|
1353
1489
|
// Other variables
|
|
1354
|
-
const dragManager
|
|
1355
|
-
const
|
|
1490
|
+
const dragManager // see Drag and drop
|
|
1491
|
+
const svgLoader; const linearColorStops; const simpleColorStop
|
|
1492
|
+
const unbubbleEvents // the events that stay on their node: see "Important stupid notes"
|
|
1356
1493
|
const makeUniqueComponentId // () => string, "x4-<n>" (exported as a value)
|
|
1357
1494
|
```
|
|
1358
1495
|
|
|
@@ -1656,7 +1793,7 @@ Optional keys: `external`, `define`, `esbuild`, `dev` (`host`, `port`, `https`,
|
|
|
1656
1793
|
- Numbers passed to `setStyle`/`setStyleValue` are auto-suffixed with `"px"` except for unitless properties (see the `unitless` set: `zIndex`, `opacity`, `flexGrow`...).
|
|
1657
1794
|
- `show(false)` / `hide()` toggle the `"x4hidden"` CSS class, not `display:none`.
|
|
1658
1795
|
- x4 sets `user-select: none` on every element (`*`): text that must be selectable needs `user-select: text` on it and on its descendants.
|
|
1659
|
-
- `enable(false)` / `disable()`
|
|
1796
|
+
- `enable(false)` / `disable()` put `disabled` and `inert` on the component itself. Its children are not changed: the browser makes all that is inside an `inert` element inactive (no click, no focus).
|
|
1660
1797
|
- `Application` is a strict singleton — one instance per page (an assertion fails otherwise).
|
|
1661
1798
|
- `Popup.show()` routes to `displayCenter()`. To position, call `displayAt()` or `displayNear()` instead.
|
|
1662
1799
|
- `BtnGroup` accepts predefined strings (`"ok"`, `"cancel"`...) with options after a dot: `"ok.outline.default.disabled"`.
|
|
@@ -1666,4 +1803,5 @@ Optional keys: `external`, `define`, `esbuild`, `dev` (`host`, `port`, `https`,
|
|
|
1666
1803
|
- `DataRecordID` is currently typed `any`; `strictNullChecks` is intentionally off in the recommended `tsconfig.json`.
|
|
1667
1804
|
- `MonacoEditor` requires `MonacoEditor.start()` (async) before use; register extra typings with `MonacoEditor.addTypelib(name, code)`.
|
|
1668
1805
|
- `x4_react.create_element` is the JSX pragma;
|
|
1669
|
-
-
|
|
1806
|
+
- A DOM handler of an ancestor is called for the events of its descendants, as in the browser. Not for the events that do not bubble: `focus`, `blur`, `mouseenter`, `mouseleave` (use `focusin` / `focusout`, `mouseover` / `mouseout`), nor for those of x4: `created`, `removed`, `resized`.
|
|
1807
|
+
- Several handlers of one event on one node are called in the order they were added. One that returns `true` stops those that follow it.
|