x4js 2.3.17 → 2.3.19

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.
Files changed (46) hide show
  1. package/aicli.md +3 -3
  2. package/aicontext.md +211 -47
  3. package/cli/config.mjs +14 -7
  4. package/cli/copy-plugin.mjs +8 -0
  5. package/cli/create.mjs +8 -2
  6. package/cli/dev.mjs +4 -1
  7. package/cli/less-plugin.mjs +5 -1
  8. package/cli/main.mjs +15 -5
  9. package/cli/rawfile-plugin.mjs +23 -3
  10. package/cli/templates.mjs +5 -1
  11. package/package.json +1 -1
  12. package/src/components/calendar/calendar.module.scss +16 -1
  13. package/src/components/calendar/calendar.ts +52 -9
  14. package/src/components/checkbox/checkbox.ts +1 -0
  15. package/src/components/combobox/combobox.ts +51 -23
  16. package/src/components/dialog/dialog.ts +4 -1
  17. package/src/components/form/form.ts +9 -5
  18. package/src/components/gridview/gridview.ts +352 -176
  19. package/src/components/input/input.ts +63 -38
  20. package/src/components/keyboard/keyboard.module.scss +35 -4
  21. package/src/components/keyboard/keyboard.ts +295 -80
  22. package/src/components/listbox/listbox.ts +6 -1
  23. package/src/components/messages/messages.ts +12 -3
  24. package/src/components/popup/popup.ts +19 -20
  25. package/src/components/radio/radio.ts +8 -4
  26. package/src/components/rating/rating.ts +38 -48
  27. package/src/components/slider/slider.ts +38 -8
  28. package/src/components/spreadsheet/spreadsheet.ts +167 -132
  29. package/src/components/switch/switch.ts +2 -1
  30. package/src/components/tag/tag.ts +1 -2
  31. package/src/components/textarea/textarea.module.scss +6 -2
  32. package/src/components/textarea/textarea.ts +50 -11
  33. package/src/components/textedit/textedit.ts +12 -2
  34. package/src/core/component.ts +16 -9
  35. package/src/core/core_application.ts +8 -4
  36. package/src/core/core_clipboard.ts +239 -0
  37. package/src/core/core_data.ts +118 -49
  38. package/src/core/core_dom.ts +36 -6
  39. package/src/core/core_dragdrop.ts +11 -5
  40. package/src/core/core_element.ts +1 -1
  41. package/src/core/core_pdf.ts +696 -46
  42. package/src/core/core_router.ts +42 -44
  43. package/src/core/core_shortcuts.ts +1 -1
  44. package/src/core/core_state.ts +22 -5
  45. package/src/x4.d.ts +1 -0
  46. package/src/x4.ts +1 -0
package/aicli.md CHANGED
@@ -7,7 +7,7 @@ x4js create <project> [--template <name>] [--no-install]
7
7
  x4js templates
8
8
  x4js dev [--config <file>] [--env <file>] [--host <host>] [--port <port>] [--http|--https] [--open]
9
9
  x4js build [--config <file>] [--env <file>] [--debug]
10
- x4js --help | --version
10
+ x4js [<command>] --help | --version
11
11
  ```
12
12
 
13
13
  ## Install
@@ -124,7 +124,7 @@ The file sits at the root of the project. Every key is optional; without the fil
124
124
  | Key | Default | Meaning |
125
125
  |---|---|---|
126
126
  | `entryPoints` | `["src/main.ts"]` | Files to bundle. `src/main.ts` produces `main.js` and `main.css`. |
127
- | `outdir` | `"./bin"` | Output folder. It cannot be the project root. |
127
+ | `outdir` | `"./bin"` | Output folder. It cannot be the project root, nor a folder that contains it. |
128
128
  | `copy` | `[]` | Files or folders copied to the output after each successful build. `to` is relative to `outdir`. |
129
129
  | `external` | `[]` | Module names left out of the bundle. |
130
130
  | `define` | `{}` | Constants replaced at build time. Values are JavaScript expressions written as strings, for example `"\"prod\""`. |
@@ -240,7 +240,7 @@ declare const VERSION_ID: number;
240
240
 
241
241
  | Symptom | Cause |
242
242
  |---|---|
243
- | `index.html` is missing from the output folder | It is not listed in `copy`, or the command that ran is an older global copy. Compare the version on the first line with the project's. |
243
+ | `index.html` is missing from the output folder | `copy: not found: …` in the output: its `from` does not exist. Or it is not listed in `copy`, or the command that ran is an older global copy. Compare the version on the first line with the project's. |
244
244
  | A file placed in the output folder disappeared | `x4js build` empties the folder. Move the file to the sources and add it to `copy`. |
245
245
  | `Cannot find package 'esbuild'` | The dependencies of the `x4js` copy being run are not installed. Run `npm install` in the project. |
246
246
  | `Config file not found` | The file given to `--config` does not exist. Without `--config`, a missing `x4.config.json` is not an error. |
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" + propagates to child <input>/<button>
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
@@ -279,7 +279,7 @@ Singleton. Extends `CoreElement` (no DOM of its own). (`src/core/core_applicatio
279
279
  interface AppProps { mountPoint?: string } // CSS selector, default "body"
280
280
 
281
281
  constructor(props?: AppProps) // auto-mounts the mainview on page load
282
- setMainView(view: Component): void
282
+ setMainView(view: Component): void // once, in the constructor of the app: each call sets up the keyboard (Tab, shortcuts) again
283
283
  getMainView(): Component
284
284
  static instance<P extends Application>(): P
285
285
  setEnv(name: string, value: any): void
@@ -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
@@ -560,7 +577,7 @@ class Gauge extends Component {
560
577
 
561
578
  ```ts
562
579
  interface RatingProps extends ComponentProps {
563
- value?: number; steps?: number; icon?: string; name?: string
580
+ value?: number; steps?: number; icon?: string; name?: string // value: the count of stars lit (0 to steps), the star of the value clicked again gives 0; steps: 5 by default; name: for a Form, which reads a number
564
581
  change?: EventCallback<EvChange>
565
582
  }
566
583
  class Rating extends HBox {
@@ -581,12 +598,12 @@ type InputProps = TextInputProps | CheckboxProps | RadioProps | RangeProps
581
598
 
582
599
  interface BaseProps extends ComponentProps {
583
600
  name?: string // required for Form.getValues/setValues
584
- autofocus?: boolean; required?: boolean; readonly?: boolean; placeholder?: string
601
+ autofocus?: boolean; required?: boolean; readonly?: boolean; placeholder?: string // required, readonly: not on checkbox, radio; placeholder: text, email, password, number
585
602
  focus?: EventCallback<EvFocus>; change?: EventCallback<EvChange>
586
603
  }
587
604
  // TextInputProps: type?: "text"|"email"|"password"; value?: string|number; maxlength?, minlength?,
588
605
  // pattern?, spellcheck?, trim?
589
- // NumberProps: type: "number"; value?: string|number; min?, max?, step?
606
+ // NumberProps: type: "number"; value?: string|number; min?, max?, step? // as many decimals shown as the step, 4 at most
590
607
  // RangeProps: type: "range"; min: number; max: number; step?; value?: number
591
608
  // DateProps: type: "date"; value?: string|Date
592
609
  // TimeProps: type: "time"; value?: string
@@ -614,22 +631,44 @@ type TextEditProps = (TextInputProps | NumberProps | DateProps | TimeProps) & {
614
631
  labelWidth?: number; inputWidth?: number; inputId?: string
615
632
  inputGadgets?: Component[]; inputAttrs?: any
616
633
  }
634
+ // style, tooltip, hidden, flex, stretch, height: on the box (label + field); dom_events: on the input
617
635
  class TextEdit extends HBox {
618
636
  getValue(): string; setValue(value: string): void; getInput(): Input
619
637
  }
620
638
  ```
621
639
 
622
- ### `TextArea`
640
+ ### `TextArea`, `SimpleTextArea` — native `<textarea>`, with or without a label
623
641
 
624
642
  ```ts
625
- interface TextAreaProps extends BaseProps { // BaseProps: see Input (name, readonly, placeholder...)
626
- label?: string; value?: string
627
- resize?: boolean | "none" | "vertical" | "horizontal" | "both"
643
+ interface SimpleTextAreaProps extends BaseProps { // BaseProps: see Input (name, readonly, required, placeholder, autofocus, focus, change)
644
+ value?: string
645
+ resize?: boolean | "none" | "vertical" | "horizontal" | "both" // no handle by default; true = "both", false = "none"
628
646
  trim?: boolean // getText() trims, unless false
629
647
  }
630
- class TextArea extends VBox { getText(): string; setText(text: string): void }
648
+ interface TextAreaProps extends SimpleTextAreaProps { label?: string }
649
+
650
+ class SimpleTextArea extends Component { // the <textarea> alone
651
+ getText(): string; setText(text: string): void
652
+ scrollToBottom(): void
653
+ isValid(): boolean // false when it is required and empty
654
+ }
655
+ class TextArea extends VBox { // a Label above a SimpleTextArea
656
+ getText(): string; setText(text: string): void
657
+ scrollToBottom(): void
658
+ isValid(): boolean
659
+ }
631
660
  ```
632
661
 
662
+ ```ts
663
+ new TextArea({ label: 'Comment', name: 'comment', placeholder: 'A few words...', resize: 'vertical' }) // in a form
664
+ new SimpleTextArea({ flex: 1, placeholder: 'Your message', resize: false }) // no label
665
+ ```
666
+
667
+ - `TextArea` in a form; `SimpleTextArea` where a label has no place (a message to type, a log).
668
+ - Both are form elements: with a `name`, `Form.getValues()` and `setValues()` read and write them.
669
+ - The value is read and written with `getText()` / `setText()`, not `getValue()` / `setValue()`.
670
+ - `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`.
671
+ - `required`: an empty text is not valid. `Form.validate()` then returns `null`, or tells its validator that the form is not valid.
633
672
  ### `Checkbox`
634
673
 
635
674
  ```ts
@@ -637,6 +676,7 @@ interface CheckboxProps extends ComponentProps {
637
676
  label: string; checked?: boolean; value?: boolean|number|string; name?: string
638
677
  change?: EventCallback<EvChange>
639
678
  }
679
+ // in a Form: without value (or a boolean one), a boolean; with one, the value when checked, null when not
640
680
  class Checkbox extends Component {
641
681
  getCheck(): boolean; setCheck(ck: boolean): void
642
682
  setLabel(text: string): void; toggle(): void
@@ -657,15 +697,15 @@ interface RadioProps extends ComponentProps {
657
697
  class Radio extends Component {
658
698
  getValue(): string; setCheck(ck: boolean): void
659
699
  setLabel(text: string): void
660
- checkValue(vname: string): void // check the radio whose value === vname (group)
700
+ checkValue(vname: string): void // check the radio whose value === vname, in the group of its form (else of the page)
661
701
  }
662
702
  ```
663
703
 
664
704
  ### `Switch`
665
705
 
666
706
  ```ts
667
- interface SwitchProps extends ComponentProps { label: string; checked?: boolean; name?: string }
668
- // with a name, a Form reads and writes it as a boolean
707
+ interface SwitchProps extends ComponentProps { label: string; checked?: boolean; value?: string; name?: string }
708
+ // with a name, a Form reads and writes it: a boolean, or with a value, the value when checked and null when not
669
709
  class Switch extends HBox {}
670
710
  ```
671
711
 
@@ -675,10 +715,12 @@ class Switch extends HBox {}
675
715
  interface SliderProps extends ComponentProps {
676
716
  value: number; min: number; max: number // required
677
717
  step?: number; vertical?: boolean
678
- change?: EventCallback<EvChange>
718
+ name?: string // for a Form, which reads a number
719
+ change?: EventCallback<EvChange> // when the user moves the thumb (drag, arrows), not by the code
679
720
  }
680
721
  class Slider extends Component {
681
- setMin(min: number): void; setMax(max: number): void; setValue(v: number): void
722
+ getValue(): number
723
+ setMin(min: number): void; setMax(max: number): void; setValue(v: number): void // the value is kept between min and max, on the step
682
724
  }
683
725
  ```
684
726
 
@@ -708,13 +750,13 @@ interface ComboboxProps extends ComponentProps {
708
750
  readonly?: boolean // true by default: the user picks, and cannot type. false to let him type
709
751
  renderer?: (item: ListItem) => Component // draws an item of the list (the field still shows item.text)
710
752
  required?: boolean; name?: string
711
- selectionChange?: EventCallback<EvSelectionChange>
753
+ selectionChange?: EventCallback<EvSelectionChange> // at each item reached with the arrows too
712
754
  }
713
755
  class Combobox extends Component {
714
756
  getValue(): string; setValue(value: string): void
715
757
  getInput(): Input
716
- getSelection(): ListboxID
717
- selectItem(index: number): void
758
+ getSelection(): ListboxID // the item chosen (click or arrows); undefined when the text typed is not the one of an item
759
+ selectItem(index: ListboxID): void // null: none, the field is emptied
718
760
  setItems(items): void
719
761
  showDropDown(): void
720
762
  }
@@ -784,10 +826,13 @@ interface ListboxProps extends Omit<ComponentProps,"content"> {
784
826
  title?: string; icon?: string
785
827
  header?: Header; footer?: Component
786
828
  checkable?: true; multisel?: true
829
+ emptyMsg?: string // shown in place of the items while there are none
787
830
  dblClick?: EventCallback<EvDblClick>
788
831
  selectionChange?: EventCallback<EvSelectionChange>
789
832
  contextMenu?: EventCallback<EvContextMenu>
790
833
  }
834
+ // emptyMsg: without it the text is _tr.global.empty_list ("Empty list"). It is shown by setItems
835
+ // (the constructor calls it) when there is no item. The listbox then has the class "empty".
791
836
  class Listbox extends Component {
792
837
  setItems(items: ListItem[], keepSel?: boolean): void
793
838
  appendItem(item: ListItem, prepend?: boolean, select?: boolean): void
@@ -801,7 +846,6 @@ class Listbox extends Component {
801
846
  ensureSelectionVisible(): void
802
847
  renderItem(item: ListItem): Component // override to customize
803
848
  defaultRenderer(item: ListItem): Component
804
- preventFocus: boolean
805
849
  }
806
850
  ```
807
851
 
@@ -812,38 +856,51 @@ interface GridviewProps extends ComponentProps {
812
856
  store: DataStore // required
813
857
  columns: GridColumn[] // required
814
858
  footer?: boolean; emptyMsg?: string
815
- sort?: object
816
- click?: EventCallback<EvClick>
817
- dblClick?: EventCallback<EvDblClick>
859
+ multisel?: boolean // Ctrl+click adds or removes a row, Shift+click a range, Ctrl+A all
860
+ sort?: { field_id: string; asc?: boolean } // the first sort; ascending only when asc is true
861
+ click?: EventCallback<EvClick> // a click on a row; ev.context is its record
862
+ dblClick?: EventCallback<EvDblClick> // ev.context is the record of the row
818
863
  contextMenu?: EventCallback<EvContextMenu>
819
864
  selectionChange?: EventCallback<EvSelectionChange>
820
865
  }
821
866
  class Gridview extends Component {
822
867
  setStore(store: DataStore): void
823
- setColumns(columns: GridColumn[]): void
824
868
  setColTitle(col_name: string, title: string): void
825
- sortCol(colIdx: number, ascending: boolean): void
869
+ sortCol(colId: any, ascending: boolean): void // colId: the id of the column, not its index
826
870
  getView(): DataView
827
- getSelection(): DataRecord[]
828
- getFirstSel(): DataRecord
871
+ getSelection(): DataRecord[] // null when nothing is selected
872
+ getFirstSel(): DataRecord // null when nothing is selected
829
873
  selectItem(id, ensureVisible?: boolean): void
830
874
  clearSelection(): void
875
+ selectAll(): void // multisel only; keeps the id of every record: costs memory on a very large view
831
876
  navigate(sens: kbNav): boolean
832
877
  lock(lock: boolean): void
833
878
  }
879
+ // Selection: the selected records, not their rows. It survives a sort; a record that leaves the view (filter, delete) leaves it, with a selectionChange.
880
+ // getSelection(): every selected record, or null. selectionChange: ev.selection has them all.
881
+ // 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
882
  ```
835
883
 
836
884
  ### `Treeview`
837
885
 
838
886
  ```ts
839
887
  interface TreeItem extends ListItem { children?: TreeItem[]; open?: boolean }
840
- interface TreeviewProps extends ComponentProps { /* items, renderer... */ }
888
+ interface TreeviewProps extends Omit<ComponentProps,"content"> {
889
+ items: TreeItem[] // required
890
+ footer?: Component
891
+ emptyMsg?: string // shown in place of the items while there are none
892
+ click?: EventCallback<EvClick>
893
+ dblClick?: EventCallback<EvDblClick>
894
+ selectionChange?: EventCallback<EvSelectionChange>
895
+ }
896
+ // emptyMsg: as for a Listbox. Without it the text is _tr.global.empty_list ("Empty list").
897
+ // There is no `renderer`: an item is drawn from its `text`, `iconId`, `cls` and `sub_cols`.
841
898
  class Treeview extends Component {
842
899
  setItems(items: TreeItem[]): void
843
- getSelection(): ListboxID
844
- selectItem(id): void
900
+ getSelection(): ListboxID // one item, not an array
901
+ selectItem(id: ListboxID): void
845
902
  clearSelection(): void
846
- navigate(sens: kbNav): boolean // see kbTreeNav enum
903
+ navigate(sens: kbTreeNav): boolean // first, prev, next, last, parent, child, expand, collapse, toggle
847
904
  }
848
905
  ```
849
906
 
@@ -871,6 +928,7 @@ class Spreadsheet extends Component {
871
928
  stopEdit(commit = true): void; isEditing(): boolean
872
929
  }
873
930
 
931
+ // 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
932
  // Inline edition: F2 / Enter / double-click / typing starts, Enter / Tab / click elsewhere validates, Escape cancels,
875
933
  // Delete clears, editable checkboxes toggle on double-click or Space (a single click only selects).
876
934
  // Default editor is an Input matching the column type.
@@ -1029,7 +1087,7 @@ class Dialog extends Popup {
1029
1087
  getButton(name: string): Button
1030
1088
  getBtnBar(): BtnGroup
1031
1089
  setTitle(title: string): void
1032
- showAsync(): Promise<string> // resolves with the clicked button name
1090
+ showAsync(): Promise<string> // the name of the clicked button; null when closed without a button (Escape, close box)
1033
1091
  }
1034
1092
  ```
1035
1093
 
@@ -1042,7 +1100,7 @@ class MessageBox extends Dialog {
1042
1100
  }
1043
1101
  class InputBox extends Dialog {
1044
1102
  getValue(): string
1045
- // static showAsync(msg, value, title?, options?: {password?, trim?}): Promise<string> (null if cancelled)
1103
+ // static showAsync(msg, value, title?, options?: {password?, trim?}): Promise<string> (null if cancelled or closed)
1046
1104
  }
1047
1105
  class PromptBox extends Dialog {
1048
1106
  static showAsync(msg: string|UnsafeHtml, editor: Component, title?: string): Promise<string>
@@ -1080,8 +1138,8 @@ class Form extends Box {
1080
1138
  setValues(values: FormValues): void
1081
1139
  getValues<T extends FormValues>(): T
1082
1140
  setAutoComplete(on?: boolean): void
1083
- setValidator(validator: (values: FormValues, is_valid: boolean) => boolean): void
1084
- validate(): FormValues // null if invalid
1141
+ setValidator(validator: (values: FormValues, is_valid: boolean) => boolean): void // a new call replaces the validator
1142
+ 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)
1085
1143
  }
1086
1144
  // Collects all children (recursive) that have a "name" attribute and implement queryInterface("form-element").
1087
1145
  ```
@@ -1110,13 +1168,22 @@ class Video extends Box {
1110
1168
  get volume(): number; get videoWidth(): number; get videoHeight(): number
1111
1169
  set stream(stream: MediaStream)
1112
1170
  }
1113
- class Calendar extends VBox { getDate(): Date; setDate(date: Date): void }
1171
+ class Calendar extends VBox { // props: date?, minDate?, maxDate? (days out of them cannot be chosen), change ("change": the day clicked)
1172
+ getDate(): Date; setDate(date: Date): void // copies: the calendar keeps its own date
1173
+ }
1114
1174
  class ColorPicker extends VBox { // props: color, swatches?, nullable?; "change": EvColorChange, also fired once when built
1115
1175
  getColor(): Color; setColor(color: Color | string): void; setSwatches(swatches): void
1116
1176
  }
1117
1177
  class Panel extends VBox { setTitle(title: string): void }
1118
1178
  class Breadcrumbs extends HBox { setItems(elements): void }
1119
- class Keyboard extends HBox // virtual keyboard
1179
+ interface KeyboardProps extends BoxProps { locale?: string } // "fr-FR" (default), "de-CH", "pl"...
1180
+ class Keyboard extends HBox { // virtual keyboard, shown while an <input> has the focus
1181
+ setLocale(locale: string): void
1182
+ setZoom(perc: number): void
1183
+ showOn(el: Component): void
1184
+ }
1185
+ // languages: cs da de en es et fi fr ga hr hu it lt lv mt nl pl pt ro sk sl sv
1186
+ // bottom row: 123 (numeric pad), the letters of the language (éè, none in en), the signs (#+=); Abc comes back
1120
1187
  class TickLine extends Component
1121
1188
  class Header extends HBox // resizable column header; updates target style "--{name}-width"
1122
1189
  ```
@@ -1177,7 +1244,7 @@ class DataStore<T> extends EventSource {
1177
1244
  ```ts
1178
1245
  interface FilterInfo {
1179
1246
  op: "<"|">"|"<="|"="|">="|"<>"|"empty-result"|FilterFunc
1180
- field?: string; value?: string|RegExp; caseSensitive?: boolean
1247
+ field?: string; value?: string|number|RegExp; caseSensitive?: boolean // a RegExp is tested on the field as it is: caseSensitive does not apply, the regexp handles the case (/^jo/i)
1181
1248
  }
1182
1249
  interface SortProp { field: string; ascending: boolean; numeric?: boolean; callback?: SortCallback }
1183
1250
  // Event "view_change" → { change_type: "change"|"filter"|"sort" }
@@ -1189,6 +1256,8 @@ class DataView extends CoreElement {
1189
1256
  getRecId(rec): any; indexOfId(id): number
1190
1257
  getStore(): DataStore; getModel(): DataModel
1191
1258
  forEach(cb): void; changed(): void
1259
+ close(): void // stops following the store; call it on a view you made (createView) and do not use anymore
1260
+ open(): void // follows the store again after a close(), filtered and sorted with its data of the moment
1192
1261
  }
1193
1262
  ```
1194
1263
 
@@ -1206,7 +1275,7 @@ class DataProxy extends CoreElement { load(url?): Promise<void> } // Event "ch
1206
1275
  ```ts
1207
1276
  class Router extends EventSource {
1208
1277
  constructor(useHash?: boolean) // default true (# URLs)
1209
- get(uri: string|RegExp, handler: (params: any, path: string) => void): void
1278
+ get(uri: string|RegExp, handler: (params: any, path: string) => void): void // the first route that matches is called, in the order of get: "/*" last is the route for the rest (params.wild)
1210
1279
  init(): void // trigger the current route
1211
1280
  navigate(uri: string, notify?: boolean, replace?: boolean): boolean
1212
1281
  // Events: "change" → { value: string } ; "error" → { code: 404, message: string }
@@ -1256,6 +1325,80 @@ class MyView extends VBox {
1256
1325
 
1257
1326
  ---
1258
1327
 
1328
+ ## Clipboard (`core_clipboard`)
1329
+
1330
+ The clipboard of the system: a text, an image, files. Never call `navigator.clipboard` or `ev.clipboardData`.
1331
+
1332
+ ```ts
1333
+ interface ClipboardContent { text?: string; image?: Blob }
1334
+ interface ClipboardRead extends ClipboardContent { files: File[] } // files: only in an event
1335
+
1336
+ // from a menu or a button: asynchronous, never fail
1337
+ setClipboard(data: ClipboardContent): Promise<boolean> // false: the system refused
1338
+ getClipboard(): Promise<ClipboardRead>
1339
+ canReadClipboard(): boolean // false: nothing to paste, disable the item
1340
+
1341
+ // in a "copy", "cut" or "paste" event
1342
+ setEventClipboard(ev: ClipboardEvent, data: ClipboardContent): void // prevents the default
1343
+ getEventClipboard(ev: ClipboardEvent): ClipboardRead // synchronous, default left as it is
1344
+ ```
1345
+
1346
+ ```ts
1347
+ this.addDOMEvent('copy', ev => setEventClipboard(ev, { text: this.getSelectedText() }))
1348
+
1349
+ this.addDOMEvent('paste', ev => {
1350
+ const data = getEventClipboard(ev)
1351
+ if (data.image) {
1352
+ ev.preventDefault()
1353
+ this.setPicture(data.image)
1354
+ }
1355
+ })
1356
+
1357
+ new Button({ label: 'Paste', click: async () => {
1358
+ const data = await getClipboard()
1359
+ if (data.text) this.insert(data.text)
1360
+ } })
1361
+ ```
1362
+
1363
+ - 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.
1364
+ - The events go to the component that has the focus: give it a `tabindex`.
1365
+ - 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.
1366
+ - An image is written as a PNG; another format is converted. Files can be read in a `paste` event only, and cannot be written.
1367
+
1368
+ ---
1369
+
1370
+ ## Drag and drop (`core_dragdrop`)
1371
+
1372
+ `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.
1373
+
1374
+ ```ts
1375
+ interface DropInfo { pt: Point; data: DataTransfer }
1376
+ type DropCallback = (command: 'enter' | 'leave' | 'drag' | 'drop', el: Component, infos: DropInfo) => void
1377
+ type DropFilterCallback = (el: Component, data: DataTransfer) => boolean // false: nothing can be dropped here
1378
+
1379
+ dragManager.registerDraggableElement(el: Component): void
1380
+ dragManager.registerDropTarget(el: Component, cb: DropCallback, filterCB?: DropFilterCallback): void
1381
+ ```
1382
+
1383
+ ```ts
1384
+ dragManager.registerDraggableElement(card)
1385
+
1386
+ dragManager.registerDropTarget(column, (cmd, el, infos) => {
1387
+ if (cmd == 'drop') {
1388
+ if (infos.data.files.length) this.upload(infos.data.files) // files from the desktop
1389
+ else this.moveCard(el, infos.pt) // el: the component that was dragged
1390
+ }
1391
+ })
1392
+ ```
1393
+
1394
+ - `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.
1395
+ - 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.
1396
+ - `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`.
1397
+ - `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`.
1398
+ - `Image` (`candrop`) and `FileDrop` already use it for files.
1399
+
1400
+ ---
1401
+
1259
1402
  ## i18n (`core_i18n`)
1260
1403
 
1261
1404
  Two built-in languages (`fr` default, and `en`).
@@ -1319,7 +1462,7 @@ centerRect(inner: IRect, outer: IRect, margin?): IRect
1319
1462
  sprintf(format, ...args): string // replaces {0} {1}...
1320
1463
  pad(what, size, ch?): string // >0 padEnd, <0 padStart
1321
1464
  pascalCase(s): string; camelCase(s): string; clamp<T>(v, min, max): T
1322
- measureText(str, size): number
1465
+ measureText(str, size, bold?): number // width in Helvetica, from core_pdf (see PDF)
1323
1466
 
1324
1467
  // Dates
1325
1468
  date_format(date, options?): string
@@ -1351,8 +1494,9 @@ interface ITabHandler { focusNext(next: boolean): boolean }
1351
1494
  interface ITipHandler { getTip(): string }
1352
1495
 
1353
1496
  // Other variables
1354
- const dragManager; const svgLoader; const linearColorStops; const simpleColorStop
1355
- const unbubbleEvents // set of DOM event names that do NOT bubble through the x4 tree
1497
+ const dragManager // see Drag and drop
1498
+ const svgLoader; const linearColorStops; const simpleColorStop
1499
+ const unbubbleEvents // the events that stay on their node: see "Important stupid notes"
1356
1500
  const makeUniqueComponentId // () => string, "x4-<n>" (exported as a value)
1357
1501
  ```
1358
1502
 
@@ -1389,10 +1533,29 @@ class SvgBuilder extends SvgGroup {
1389
1533
  ## PDF (`core_pdf`)
1390
1534
 
1391
1535
  ```ts
1536
+ interface PDFProps { title?: string; author?: string; subject?: string }
1392
1537
  class X4PDFBuilder {
1393
- page(width: number, height: number, callback): void
1538
+ constructor(props?: PDFProps)
1539
+ page(width: number, height: number, callback: (ctx: PageContext) => void): void // points; without size 612 x 792
1394
1540
  build(): Uint8Array
1395
- }
1541
+ buildAsync(): Promise<Uint8Array> // streams compressed (FlateDecode)
1542
+ }
1543
+ export interface PageContext { // origin bottom left, y goes up
1544
+ text(str, x, y, size, opts?: { align?: 'left'|'center'|'right'; width?; color?; bold? }): void
1545
+ rect(x, y, w, h, fill: string): void
1546
+ line(x1, y1, x2, y2, stroke: string, lineWidth?): void
1547
+ path(segments: PathSegment[], fill: string, stroke?: string, lineWidth?): void // null: not drawn
1548
+ image(jpegBytes: Uint8Array, x, y, w, h): void
1549
+ link(url, x, y, w, h, opts?: { underline?: string }): void
1550
+ barcode(type: BarcodeType, text, x, y, w, h, opts?: BarcodeOptions): void // datamatrix: square min(w,h), (x,y) bottom left; code128: bars on w x h; no quiet zone
1551
+ }
1552
+ type PathSegment = ['m', x, y] | ['l', x, y] | ['c', x1, y1, x2, y2, x3, y3] | ['h']
1553
+ type BarcodeType = 'datamatrix' | 'code128'
1554
+ interface BarcodeOptions { gs1?: boolean /* "(01)...(17)..." */; color?: string }
1555
+ renderBarcodeImage(type, text, opts?: BarcodeOptions & { module?: number }): string // "data:image/svg+xml,..." for Image / Icon; on white, quiet zone included
1556
+ // Data Matrix ECC200 10x10..26x26 (44 chars, 88 digits, Latin-1); Code 128 ASCII. Too long or not encodable: throws.
1557
+ measureText(str: string, size: number, bold?: boolean): number // Helvetica
1558
+ // Text: Helvetica, Windows-1252 (é € … ’); another character comes out as '?'. Printed at its size (PrintScaling /None).
1396
1559
  ```
1397
1560
 
1398
1561
  ---
@@ -1656,7 +1819,7 @@ Optional keys: `external`, `define`, `esbuild`, `dev` (`host`, `port`, `https`,
1656
1819
  - Numbers passed to `setStyle`/`setStyleValue` are auto-suffixed with `"px"` except for unitless properties (see the `unitless` set: `zIndex`, `opacity`, `flexGrow`...).
1657
1820
  - `show(false)` / `hide()` toggle the `"x4hidden"` CSS class, not `display:none`.
1658
1821
  - 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()` propagate the `disabled` state to child `<input>` and `<button>` elements.
1822
+ - `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
1823
  - `Application` is a strict singleton — one instance per page (an assertion fails otherwise).
1661
1824
  - `Popup.show()` routes to `displayCenter()`. To position, call `displayAt()` or `displayNear()` instead.
1662
1825
  - `BtnGroup` accepts predefined strings (`"ok"`, `"cancel"`...) with options after a dot: `"ok.outline.default.disabled"`.
@@ -1666,4 +1829,5 @@ Optional keys: `external`, `define`, `esbuild`, `dev` (`host`, `port`, `https`,
1666
1829
  - `DataRecordID` is currently typed `any`; `strictNullChecks` is intentionally off in the recommended `tsconfig.json`.
1667
1830
  - `MonacoEditor` requires `MonacoEditor.start()` (async) before use; register extra typings with `MonacoEditor.addTypelib(name, code)`.
1668
1831
  - `x4_react.create_element` is the JSX pragma;
1669
- - `unbubbleEvents` is the set of DOM events that do not bubble through the x4 component tree.
1832
+ - 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`.
1833
+ - 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.
package/cli/config.mjs CHANGED
@@ -92,16 +92,20 @@ function readJson(filename) {
92
92
  }
93
93
  }
94
94
 
95
- function validateCopy(copy) {
95
+ function validateCopy(copy, env) {
96
96
  if (!Array.isArray(copy))
97
97
  throw new Error("copy must be an array");
98
98
 
99
99
  for (const item of copy) {
100
100
  if (!item || typeof item !== "object" || typeof item.from !== "string" || typeof item.to !== "string")
101
101
  throw new Error("Each copy entry must contain string 'from' and 'to' fields");
102
- if (path.isAbsolute(item.to))
102
+
103
+ // checked once expanded: a $NAME could hold "..".
104
+ // A root: "D:assets" is not absolute, but goes to the folder of D:
105
+ const to = expandEnv(item.to, env);
106
+ if (path.isAbsolute(to) || path.parse(to).root)
103
107
  throw new Error(`copy destination must be relative to outdir: '${item.to}'`);
104
- const normalized = path.normalize(item.to);
108
+ const normalized = path.normalize(to);
105
109
  if (normalized === ".." || normalized.startsWith(`..${path.sep}`))
106
110
  throw new Error(`copy destination escapes outdir: '${item.to}'`);
107
111
  }
@@ -125,7 +129,7 @@ export function loadConfig(root = process.cwd(), configFile = DEFAULT_CONFIG_FIL
125
129
  throw new Error("entryPoints must be a non-empty array of strings");
126
130
 
127
131
  const copy = cfg.copy ?? DEFAULTS.copy;
128
- validateCopy(copy);
132
+ validateCopy(copy, env);
129
133
 
130
134
  const external = cfg.external ?? DEFAULTS.external;
131
135
  if (!Array.isArray(external) || external.some((entry) => typeof entry !== "string"))
@@ -165,11 +169,14 @@ export function loadConfig(root = process.cwd(), configFile = DEFAULT_CONFIG_FIL
165
169
  throw new Error("outdir must be a string");
166
170
 
167
171
  const outdir = resolvePath(root, outdirRaw, env);
168
- if (outdir === root)
169
- throw new Error("outdir cannot be the project root");
170
172
  if (outdir === path.parse(outdir).root)
171
173
  throw new Error("outdir cannot be a filesystem root");
172
174
 
175
+ // the output folder is emptied before each build: it must not be the project, nor a folder that holds it
176
+ const fromOutdir = path.relative(outdir, root);
177
+ if (fromOutdir !== ".." && !fromOutdir.startsWith(`..${path.sep}`) && !path.isAbsolute(fromOutdir))
178
+ throw new Error(`outdir cannot be the project root, nor a folder that contains it: '${outdir}'`);
179
+
173
180
  let esbuildOptions = {};
174
181
  if (cfg.esbuild !== undefined) {
175
182
  if (!cfg.esbuild || typeof cfg.esbuild !== "object" || Array.isArray(cfg.esbuild))
@@ -184,7 +191,7 @@ export function loadConfig(root = process.cwd(), configFile = DEFAULT_CONFIG_FIL
184
191
  outdir,
185
192
  copy: copy.map((item) => ({
186
193
  from: resolvePath(root, item.from, env),
187
- to: path.normalize(item.to),
194
+ to: path.normalize(expandEnv(item.to, env)),
188
195
  })),
189
196
  external: [...external],
190
197
  define: Object.fromEntries(
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { warning } from "./log.mjs";
3
4
 
4
5
  async function walk(source) {
5
6
  const files = [];
@@ -51,6 +52,13 @@ export function copyPlugin(config, { dev = false } = {}) {
51
52
  for (const entry of entries) {
52
53
  const result = await walk(entry.from);
53
54
  entry.sourceIsDirectory = result.isDirectory;
55
+
56
+ // its folder is watched: in dev, its creation starts a build
57
+ if (!result.files.length && !result.isDirectory) {
58
+ warning(`copy: not found: ${entry.from}`);
59
+ dirs.push(path.dirname(entry.from));
60
+ }
61
+
54
62
  dirs.push(...result.dirs);
55
63
  for (const item of result.files) {
56
64
  files.push({ entry, ...item });
package/cli/create.mjs CHANGED
@@ -127,8 +127,10 @@ export async function create(argv = [], { version = "unknown", cwd = process.cwd
127
127
  try {
128
128
  const files = await fetchTemplateFiles(values.template, version);
129
129
 
130
- await Promise.all(files.map(async (file) => {
131
- if (!file.path || file.path.split("/").includes(".."))
130
+ // a \ is a separator on Windows: "..\x" would go out of the target
131
+ // allSettled: every write is over before the cleanup of the catch
132
+ const results = await Promise.allSettled(files.map(async (file) => {
133
+ if (!file.path || file.path.includes("\\") || file.path.split("/").includes(".."))
132
134
  throw new Error(`Unsafe template path '${file.path}'`);
133
135
  const destination = path.join(target, ...file.path.split("/"));
134
136
  await fs.mkdir(path.dirname(destination), { recursive: true });
@@ -136,6 +138,10 @@ export async function create(argv = [], { version = "unknown", cwd = process.cwd
136
138
  await fs.writeFile(destination, data);
137
139
  }));
138
140
 
141
+ const failed = results.find((result) => result.status === "rejected");
142
+ if (failed)
143
+ throw failed.reason;
144
+
139
145
  await patchPackage(target, projectName, version);
140
146
  projectReady = true;
141
147