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.
- package/aicli.md +3 -3
- package/aicontext.md +211 -47
- package/cli/config.mjs +14 -7
- package/cli/copy-plugin.mjs +8 -0
- package/cli/create.mjs +8 -2
- package/cli/dev.mjs +4 -1
- package/cli/less-plugin.mjs +5 -1
- package/cli/main.mjs +15 -5
- package/cli/rawfile-plugin.mjs +23 -3
- package/cli/templates.mjs +5 -1
- package/package.json +1 -1
- package/src/components/calendar/calendar.module.scss +16 -1
- package/src/components/calendar/calendar.ts +52 -9
- package/src/components/checkbox/checkbox.ts +1 -0
- package/src/components/combobox/combobox.ts +51 -23
- package/src/components/dialog/dialog.ts +4 -1
- package/src/components/form/form.ts +9 -5
- package/src/components/gridview/gridview.ts +352 -176
- package/src/components/input/input.ts +63 -38
- package/src/components/keyboard/keyboard.module.scss +35 -4
- package/src/components/keyboard/keyboard.ts +295 -80
- package/src/components/listbox/listbox.ts +6 -1
- package/src/components/messages/messages.ts +12 -3
- package/src/components/popup/popup.ts +19 -20
- package/src/components/radio/radio.ts +8 -4
- package/src/components/rating/rating.ts +38 -48
- package/src/components/slider/slider.ts +38 -8
- package/src/components/spreadsheet/spreadsheet.ts +167 -132
- package/src/components/switch/switch.ts +2 -1
- package/src/components/tag/tag.ts +1 -2
- package/src/components/textarea/textarea.module.scss +6 -2
- package/src/components/textarea/textarea.ts +50 -11
- package/src/components/textedit/textedit.ts +12 -2
- package/src/core/component.ts +16 -9
- package/src/core/core_application.ts +8 -4
- package/src/core/core_clipboard.ts +239 -0
- package/src/core/core_data.ts +118 -49
- package/src/core/core_dom.ts +36 -6
- package/src/core/core_dragdrop.ts +11 -5
- package/src/core/core_element.ts +1 -1
- package/src/core/core_pdf.ts +696 -46
- package/src/core/core_router.ts +42 -44
- package/src/core/core_shortcuts.ts +1 -1
- package/src/core/core_state.ts +22 -5
- package/src/x4.d.ts +1 -0
- 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 |
|
|
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"
|
|
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
|
|
626
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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(
|
|
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 {
|
|
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:
|
|
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> //
|
|
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 {
|
|
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
|
-
|
|
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
|
|
1355
|
-
const
|
|
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
|
-
|
|
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()`
|
|
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
|
-
-
|
|
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
|
-
|
|
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(
|
|
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(
|
package/cli/copy-plugin.mjs
CHANGED
|
@@ -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
|
-
|
|
131
|
-
|
|
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
|
|