x4js 2.3.18 → 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 +49 -23
- 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 +2 -1
- package/src/components/input/input.ts +63 -38
- package/src/components/keyboard/keyboard.module.scss +35 -4
- package/src/components/keyboard/keyboard.ts +120 -21
- 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 +140 -131
- package/src/components/switch/switch.ts +2 -1
- package/src/components/textarea/textarea.module.scss +2 -4
- package/src/components/textarea/textarea.ts +2 -1
- package/src/components/textedit/textedit.ts +12 -2
- package/src/core/core_application.ts +8 -4
- package/src/core/core_data.ts +64 -26
- package/src/core/core_dom.ts +9 -1
- package/src/core/core_pdf.ts +696 -46
- package/src/core/core_router.ts +42 -44
- package/src/core/core_state.ts +22 -5
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
|
@@ -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
|
|
@@ -577,7 +577,7 @@ class Gauge extends Component {
|
|
|
577
577
|
|
|
578
578
|
```ts
|
|
579
579
|
interface RatingProps extends ComponentProps {
|
|
580
|
-
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
|
|
581
581
|
change?: EventCallback<EvChange>
|
|
582
582
|
}
|
|
583
583
|
class Rating extends HBox {
|
|
@@ -598,12 +598,12 @@ type InputProps = TextInputProps | CheckboxProps | RadioProps | RangeProps
|
|
|
598
598
|
|
|
599
599
|
interface BaseProps extends ComponentProps {
|
|
600
600
|
name?: string // required for Form.getValues/setValues
|
|
601
|
-
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
|
|
602
602
|
focus?: EventCallback<EvFocus>; change?: EventCallback<EvChange>
|
|
603
603
|
}
|
|
604
604
|
// TextInputProps: type?: "text"|"email"|"password"; value?: string|number; maxlength?, minlength?,
|
|
605
605
|
// pattern?, spellcheck?, trim?
|
|
606
|
-
// 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
|
|
607
607
|
// RangeProps: type: "range"; min: number; max: number; step?; value?: number
|
|
608
608
|
// DateProps: type: "date"; value?: string|Date
|
|
609
609
|
// TimeProps: type: "time"; value?: string
|
|
@@ -631,6 +631,7 @@ type TextEditProps = (TextInputProps | NumberProps | DateProps | TimeProps) & {
|
|
|
631
631
|
labelWidth?: number; inputWidth?: number; inputId?: string
|
|
632
632
|
inputGadgets?: Component[]; inputAttrs?: any
|
|
633
633
|
}
|
|
634
|
+
// style, tooltip, hidden, flex, stretch, height: on the box (label + field); dom_events: on the input
|
|
634
635
|
class TextEdit extends HBox {
|
|
635
636
|
getValue(): string; setValue(value: string): void; getInput(): Input
|
|
636
637
|
}
|
|
@@ -641,7 +642,7 @@ class TextEdit extends HBox {
|
|
|
641
642
|
```ts
|
|
642
643
|
interface SimpleTextAreaProps extends BaseProps { // BaseProps: see Input (name, readonly, required, placeholder, autofocus, focus, change)
|
|
643
644
|
value?: string
|
|
644
|
-
resize?: boolean | "none" | "vertical" | "horizontal" | "both" // false = "none"
|
|
645
|
+
resize?: boolean | "none" | "vertical" | "horizontal" | "both" // no handle by default; true = "both", false = "none"
|
|
645
646
|
trim?: boolean // getText() trims, unless false
|
|
646
647
|
}
|
|
647
648
|
interface TextAreaProps extends SimpleTextAreaProps { label?: string }
|
|
@@ -675,6 +676,7 @@ interface CheckboxProps extends ComponentProps {
|
|
|
675
676
|
label: string; checked?: boolean; value?: boolean|number|string; name?: string
|
|
676
677
|
change?: EventCallback<EvChange>
|
|
677
678
|
}
|
|
679
|
+
// in a Form: without value (or a boolean one), a boolean; with one, the value when checked, null when not
|
|
678
680
|
class Checkbox extends Component {
|
|
679
681
|
getCheck(): boolean; setCheck(ck: boolean): void
|
|
680
682
|
setLabel(text: string): void; toggle(): void
|
|
@@ -695,15 +697,15 @@ interface RadioProps extends ComponentProps {
|
|
|
695
697
|
class Radio extends Component {
|
|
696
698
|
getValue(): string; setCheck(ck: boolean): void
|
|
697
699
|
setLabel(text: string): void
|
|
698
|
-
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)
|
|
699
701
|
}
|
|
700
702
|
```
|
|
701
703
|
|
|
702
704
|
### `Switch`
|
|
703
705
|
|
|
704
706
|
```ts
|
|
705
|
-
interface SwitchProps extends ComponentProps { label: string; checked?: boolean; name?: string }
|
|
706
|
-
// 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
|
|
707
709
|
class Switch extends HBox {}
|
|
708
710
|
```
|
|
709
711
|
|
|
@@ -713,10 +715,12 @@ class Switch extends HBox {}
|
|
|
713
715
|
interface SliderProps extends ComponentProps {
|
|
714
716
|
value: number; min: number; max: number // required
|
|
715
717
|
step?: number; vertical?: boolean
|
|
716
|
-
|
|
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
|
|
717
720
|
}
|
|
718
721
|
class Slider extends Component {
|
|
719
|
-
|
|
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
|
|
720
724
|
}
|
|
721
725
|
```
|
|
722
726
|
|
|
@@ -746,13 +750,13 @@ interface ComboboxProps extends ComponentProps {
|
|
|
746
750
|
readonly?: boolean // true by default: the user picks, and cannot type. false to let him type
|
|
747
751
|
renderer?: (item: ListItem) => Component // draws an item of the list (the field still shows item.text)
|
|
748
752
|
required?: boolean; name?: string
|
|
749
|
-
selectionChange?: EventCallback<EvSelectionChange>
|
|
753
|
+
selectionChange?: EventCallback<EvSelectionChange> // at each item reached with the arrows too
|
|
750
754
|
}
|
|
751
755
|
class Combobox extends Component {
|
|
752
756
|
getValue(): string; setValue(value: string): void
|
|
753
757
|
getInput(): Input
|
|
754
|
-
getSelection(): ListboxID
|
|
755
|
-
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
|
|
756
760
|
setItems(items): void
|
|
757
761
|
showDropDown(): void
|
|
758
762
|
}
|
|
@@ -1083,7 +1087,7 @@ class Dialog extends Popup {
|
|
|
1083
1087
|
getButton(name: string): Button
|
|
1084
1088
|
getBtnBar(): BtnGroup
|
|
1085
1089
|
setTitle(title: string): void
|
|
1086
|
-
showAsync(): Promise<string> //
|
|
1090
|
+
showAsync(): Promise<string> // the name of the clicked button; null when closed without a button (Escape, close box)
|
|
1087
1091
|
}
|
|
1088
1092
|
```
|
|
1089
1093
|
|
|
@@ -1096,7 +1100,7 @@ class MessageBox extends Dialog {
|
|
|
1096
1100
|
}
|
|
1097
1101
|
class InputBox extends Dialog {
|
|
1098
1102
|
getValue(): string
|
|
1099
|
-
// 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)
|
|
1100
1104
|
}
|
|
1101
1105
|
class PromptBox extends Dialog {
|
|
1102
1106
|
static showAsync(msg: string|UnsafeHtml, editor: Component, title?: string): Promise<string>
|
|
@@ -1134,8 +1138,8 @@ class Form extends Box {
|
|
|
1134
1138
|
setValues(values: FormValues): void
|
|
1135
1139
|
getValues<T extends FormValues>(): T
|
|
1136
1140
|
setAutoComplete(on?: boolean): void
|
|
1137
|
-
setValidator(validator: (values: FormValues, is_valid: boolean) => boolean): void
|
|
1138
|
-
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)
|
|
1139
1143
|
}
|
|
1140
1144
|
// Collects all children (recursive) that have a "name" attribute and implement queryInterface("form-element").
|
|
1141
1145
|
```
|
|
@@ -1164,7 +1168,9 @@ class Video extends Box {
|
|
|
1164
1168
|
get volume(): number; get videoWidth(): number; get videoHeight(): number
|
|
1165
1169
|
set stream(stream: MediaStream)
|
|
1166
1170
|
}
|
|
1167
|
-
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
|
+
}
|
|
1168
1174
|
class ColorPicker extends VBox { // props: color, swatches?, nullable?; "change": EvColorChange, also fired once when built
|
|
1169
1175
|
getColor(): Color; setColor(color: Color | string): void; setSwatches(swatches): void
|
|
1170
1176
|
}
|
|
@@ -1177,6 +1183,7 @@ class Keyboard extends HBox { // virtual keyboard, shown while an
|
|
|
1177
1183
|
showOn(el: Component): void
|
|
1178
1184
|
}
|
|
1179
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
|
|
1180
1187
|
class TickLine extends Component
|
|
1181
1188
|
class Header extends HBox // resizable column header; updates target style "--{name}-width"
|
|
1182
1189
|
```
|
|
@@ -1237,7 +1244,7 @@ class DataStore<T> extends EventSource {
|
|
|
1237
1244
|
```ts
|
|
1238
1245
|
interface FilterInfo {
|
|
1239
1246
|
op: "<"|">"|"<="|"="|">="|"<>"|"empty-result"|FilterFunc
|
|
1240
|
-
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)
|
|
1241
1248
|
}
|
|
1242
1249
|
interface SortProp { field: string; ascending: boolean; numeric?: boolean; callback?: SortCallback }
|
|
1243
1250
|
// Event "view_change" → { change_type: "change"|"filter"|"sort" }
|
|
@@ -1268,7 +1275,7 @@ class DataProxy extends CoreElement { load(url?): Promise<void> } // Event "ch
|
|
|
1268
1275
|
```ts
|
|
1269
1276
|
class Router extends EventSource {
|
|
1270
1277
|
constructor(useHash?: boolean) // default true (# URLs)
|
|
1271
|
-
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)
|
|
1272
1279
|
init(): void // trigger the current route
|
|
1273
1280
|
navigate(uri: string, notify?: boolean, replace?: boolean): boolean
|
|
1274
1281
|
// Events: "change" → { value: string } ; "error" → { code: 404, message: string }
|
|
@@ -1455,7 +1462,7 @@ centerRect(inner: IRect, outer: IRect, margin?): IRect
|
|
|
1455
1462
|
sprintf(format, ...args): string // replaces {0} {1}...
|
|
1456
1463
|
pad(what, size, ch?): string // >0 padEnd, <0 padStart
|
|
1457
1464
|
pascalCase(s): string; camelCase(s): string; clamp<T>(v, min, max): T
|
|
1458
|
-
measureText(str, size): number
|
|
1465
|
+
measureText(str, size, bold?): number // width in Helvetica, from core_pdf (see PDF)
|
|
1459
1466
|
|
|
1460
1467
|
// Dates
|
|
1461
1468
|
date_format(date, options?): string
|
|
@@ -1526,10 +1533,29 @@ class SvgBuilder extends SvgGroup {
|
|
|
1526
1533
|
## PDF (`core_pdf`)
|
|
1527
1534
|
|
|
1528
1535
|
```ts
|
|
1536
|
+
interface PDFProps { title?: string; author?: string; subject?: string }
|
|
1529
1537
|
class X4PDFBuilder {
|
|
1530
|
-
|
|
1538
|
+
constructor(props?: PDFProps)
|
|
1539
|
+
page(width: number, height: number, callback: (ctx: PageContext) => void): void // points; without size 612 x 792
|
|
1531
1540
|
build(): Uint8Array
|
|
1532
|
-
|
|
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).
|
|
1533
1559
|
```
|
|
1534
1560
|
|
|
1535
1561
|
---
|
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
|
|
package/cli/dev.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { parseArgs } from "node:util";
|
|
|
5
5
|
import esbuild from "esbuild";
|
|
6
6
|
import { DEFAULT_CONFIG_FILE, expandEnv, loadConfig, loadEnvFile, resolveConfigFile, resolveTls } from "./config.mjs";
|
|
7
7
|
import { createBuildOptions } from "./build-options.mjs";
|
|
8
|
-
import { info, success, failure } from "./log.mjs";
|
|
8
|
+
import { info, success, failure, warning } from "./log.mjs";
|
|
9
9
|
|
|
10
10
|
function openBrowser(url) {
|
|
11
11
|
let command;
|
|
@@ -28,6 +28,9 @@ function openBrowser(url) {
|
|
|
28
28
|
detached: true,
|
|
29
29
|
stdio: "ignore",
|
|
30
30
|
});
|
|
31
|
+
|
|
32
|
+
// no browser could be started (xdg-open missing...): the server goes on
|
|
33
|
+
child.on("error", (error) => warning(`cannot open the browser: ${error.message}`));
|
|
31
34
|
child.unref();
|
|
32
35
|
}
|
|
33
36
|
|
package/cli/less-plugin.mjs
CHANGED
|
@@ -28,7 +28,11 @@ export function lessPlugin(projectRoot) {
|
|
|
28
28
|
setup(build) {
|
|
29
29
|
build.onLoad({ filter: /\.less$/ }, async (args) => {
|
|
30
30
|
const source = await fs.readFile(args.path, "utf8");
|
|
31
|
-
|
|
31
|
+
// not kept when it fails: less can be installed while dev runs
|
|
32
|
+
lessPromise ??= loadLess(projectRoot).catch((error) => {
|
|
33
|
+
lessPromise = undefined;
|
|
34
|
+
throw error;
|
|
35
|
+
});
|
|
32
36
|
const less = await lessPromise;
|
|
33
37
|
|
|
34
38
|
const result = await less.render(source, {
|
package/cli/main.mjs
CHANGED
|
@@ -25,17 +25,27 @@ Usage:
|
|
|
25
25
|
x4js build [--config <file>] [--env <file>] [--debug]
|
|
26
26
|
|
|
27
27
|
Options:
|
|
28
|
-
--config <file>
|
|
29
|
-
--env <file>
|
|
30
|
-
-
|
|
31
|
-
-
|
|
28
|
+
--config <file> Config file (default: x4.config.json)
|
|
29
|
+
--env <file> File of NAME=value lines, for $NAME in the config
|
|
30
|
+
-t, --template <name> create: template to use (default: app)
|
|
31
|
+
--no-install create: do not run npm install
|
|
32
|
+
--host <host> dev: address to listen on (default: 127.0.0.1)
|
|
33
|
+
--port <port> dev: port to listen on (default: the first free port from 8000)
|
|
34
|
+
--http, --https dev: force the protocol, whatever the config says
|
|
35
|
+
--open dev: open the browser once the server is ready
|
|
36
|
+
--debug build: not minified, with source maps
|
|
37
|
+
-h, --help Show help
|
|
38
|
+
-v, --version Show version`);
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
const version = getVersion();
|
|
35
42
|
const [command, ...argv] = process.argv.slice(2);
|
|
36
43
|
|
|
44
|
+
// x4js <command> --help: the help, that gives the options of every command
|
|
45
|
+
const help = argv.includes("--help") || argv.includes("-h");
|
|
46
|
+
|
|
37
47
|
try {
|
|
38
|
-
switch (command) {
|
|
48
|
+
switch (help ? "help" : command) {
|
|
39
49
|
case "create": {
|
|
40
50
|
const { create } = await import("./create.mjs");
|
|
41
51
|
await create(argv, { version });
|
package/cli/rawfile-plugin.mjs
CHANGED
|
@@ -8,15 +8,35 @@ export function rawFilePlugin({ filter, mime }) {
|
|
|
8
8
|
name: "x4-raw-file",
|
|
9
9
|
|
|
10
10
|
setup(build) {
|
|
11
|
-
build.onResolve({ filter }, (args) => {
|
|
11
|
+
build.onResolve({ filter }, async (args) => {
|
|
12
12
|
// Seulement les imports JS/TS.
|
|
13
|
+
// pluginData: the call of build.resolve below, which esbuild resolves itself
|
|
13
14
|
if (
|
|
14
|
-
args.kind !== "import-statement" &&
|
|
15
|
-
args.kind !== "dynamic-import"
|
|
15
|
+
(args.kind !== "import-statement" &&
|
|
16
|
+
args.kind !== "dynamic-import") ||
|
|
17
|
+
args.pluginData === namespace
|
|
16
18
|
) {
|
|
17
19
|
return;
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
// a package: esbuild finds it in node_modules. Its file is read here
|
|
23
|
+
// too, in base64: Icon inserts the svg of the url as it is, a "%23"
|
|
24
|
+
// of a url in text would stay in it
|
|
25
|
+
if (!args.path.startsWith(".") && !path.isAbsolute(args.path)) {
|
|
26
|
+
const found = await build.resolve(args.path, {
|
|
27
|
+
kind: args.kind,
|
|
28
|
+
resolveDir: args.resolveDir,
|
|
29
|
+
importer: args.importer,
|
|
30
|
+
pluginData: namespace,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (found.errors.length) {
|
|
34
|
+
return { errors: found.errors };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { path: found.path, namespace };
|
|
38
|
+
}
|
|
39
|
+
|
|
20
40
|
return {
|
|
21
41
|
path: path.resolve(args.resolveDir, args.path),
|
|
22
42
|
namespace,
|
package/cli/templates.mjs
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
1
2
|
import { fetchTemplates } from "./templates-source.mjs";
|
|
2
3
|
import { color } from "./log.mjs";
|
|
3
4
|
|
|
4
|
-
export async function templates(
|
|
5
|
+
export async function templates(argv = [], { version = "unknown" } = {}) {
|
|
6
|
+
// takes no argument: one given is an error
|
|
7
|
+
parseArgs({ args: argv, options: {}, strict: true });
|
|
8
|
+
|
|
5
9
|
const manifest = await fetchTemplates(version);
|
|
6
10
|
const names = Object.keys(manifest).sort();
|
|
7
11
|
const width = names.reduce((max, name) => Math.max(max, name.length), 0) + 3;
|
package/package.json
CHANGED
|
@@ -41,7 +41,10 @@
|
|
|
41
41
|
.x4calendar {
|
|
42
42
|
border: 1px solid var( --border );
|
|
43
43
|
padding: 4px;
|
|
44
|
-
|
|
44
|
+
|
|
45
|
+
// too short for its weeks (6 in some months): what does not fit is cut, not drawn over what follows
|
|
46
|
+
overflow: hidden;
|
|
47
|
+
|
|
45
48
|
.month-sel {
|
|
46
49
|
height: 2em;
|
|
47
50
|
align-items: center;
|
|
@@ -147,6 +150,18 @@
|
|
|
147
150
|
color: var( --calendard-day-color-hover );
|
|
148
151
|
}
|
|
149
152
|
}
|
|
153
|
+
|
|
154
|
+
// out of minDate..maxDate: cannot be chosen
|
|
155
|
+
.disabled {
|
|
156
|
+
color: var( --calendar-out-color );
|
|
157
|
+
text-decoration: line-through;
|
|
158
|
+
cursor: default;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.disabled:hover .text {
|
|
162
|
+
background-color: transparent;
|
|
163
|
+
color: var( --calendar-out-color );
|
|
164
|
+
}
|
|
150
165
|
}
|
|
151
166
|
|
|
152
167
|
.header {
|
|
@@ -78,7 +78,7 @@ export class Calendar extends VBox<CalendarProps, CalendarEventMap>
|
|
|
78
78
|
super(props);
|
|
79
79
|
|
|
80
80
|
this.mapPropEvents( props, 'change' );
|
|
81
|
-
this.m_date = props.date ? date_clone( props.date ) : new Date();
|
|
81
|
+
this.m_date = this._clamp( props.date ? date_clone( props.date ) : new Date() );
|
|
82
82
|
|
|
83
83
|
this._update( );
|
|
84
84
|
}
|
|
@@ -130,7 +130,7 @@ export class Calendar extends VBox<CalendarProps, CalendarEventMap>
|
|
|
130
130
|
}),
|
|
131
131
|
new Flex( ),
|
|
132
132
|
new Button({ icon: icon_prev, click: () => this._next(false) } ),
|
|
133
|
-
new Button({ icon: icon_today, click: () => this.setDate(new Date()), tooltip: _tr.global.today } ),
|
|
133
|
+
new Button({ icon: icon_today, click: () => this.setDate( this._clamp( new Date() ) ), tooltip: _tr.global.today } ),
|
|
134
134
|
new Button({ icon: icon_next, click: () => this._next(true) } )
|
|
135
135
|
]
|
|
136
136
|
});
|
|
@@ -159,6 +159,9 @@ export class Calendar extends VBox<CalendarProps, CalendarEventMap>
|
|
|
159
159
|
}));
|
|
160
160
|
|
|
161
161
|
let cmonth = this.m_date.getMonth();
|
|
162
|
+
const { minDate, maxDate } = this.props;
|
|
163
|
+
const min_hash = minDate ? date_hash( minDate ) : null;
|
|
164
|
+
const max_hash = maxDate ? date_hash( maxDate ) : null;
|
|
162
165
|
|
|
163
166
|
// weeks
|
|
164
167
|
let first = true;
|
|
@@ -184,6 +187,13 @@ export class Calendar extends VBox<CalendarProps, CalendarEventMap>
|
|
|
184
187
|
cls += ' out';
|
|
185
188
|
}
|
|
186
189
|
|
|
190
|
+
// out of minDate..maxDate: shown, not chosen
|
|
191
|
+
const hash = date_hash( dte );
|
|
192
|
+
const off = (min_hash!==null && hash < min_hash) || (max_hash!==null && hash > max_hash);
|
|
193
|
+
if( off ) {
|
|
194
|
+
cls += ' disabled';
|
|
195
|
+
}
|
|
196
|
+
|
|
187
197
|
const mkItem = ( dte: Date ) => {
|
|
188
198
|
return new HBox({
|
|
189
199
|
cls,
|
|
@@ -192,7 +202,7 @@ export class Calendar extends VBox<CalendarProps, CalendarEventMap>
|
|
|
192
202
|
cls: "text",
|
|
193
203
|
content: unsafeHtml( `<span>${formatIntlDate(dte, 'd')}</span>` ),
|
|
194
204
|
}),
|
|
195
|
-
dom_events: {
|
|
205
|
+
dom_events: off ? undefined : {
|
|
196
206
|
click: () => this.select(dte)
|
|
197
207
|
}
|
|
198
208
|
})
|
|
@@ -230,8 +240,40 @@ export class Calendar extends VBox<CalendarProps, CalendarEventMap>
|
|
|
230
240
|
*/
|
|
231
241
|
|
|
232
242
|
private _next(n: boolean) {
|
|
233
|
-
this.m_date.
|
|
234
|
-
|
|
243
|
+
this._goto( this.m_date.getFullYear( ), this.m_date.getMonth() + (n ? 1 : -1) );
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* shows another month: the day is kept, not past the end of the month
|
|
248
|
+
* (31 January → 28 February), and the date stays between minDate and maxDate
|
|
249
|
+
*/
|
|
250
|
+
|
|
251
|
+
private _goto( year: number, month: number ) {
|
|
252
|
+
const day = this.m_date.getDate( );
|
|
253
|
+
const d = date_clone( this.m_date );
|
|
254
|
+
d.setDate( 1 );
|
|
255
|
+
d.setFullYear( year, month ); // month 12: January of the next year
|
|
256
|
+
d.setDate( Math.min( day, new Date( d.getFullYear( ), d.getMonth( )+1, 0 ).getDate( ) ) );
|
|
257
|
+
|
|
258
|
+
this.m_date = this._clamp( d );
|
|
259
|
+
this._update( );
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* the date brought between minDate and maxDate
|
|
264
|
+
*/
|
|
265
|
+
|
|
266
|
+
private _clamp( d: Date ) {
|
|
267
|
+
const { minDate, maxDate } = this.props;
|
|
268
|
+
if( minDate && d < minDate ) {
|
|
269
|
+
return date_clone( minDate );
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if( maxDate && d > maxDate ) {
|
|
273
|
+
return date_clone( maxDate );
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return d;
|
|
235
277
|
}
|
|
236
278
|
|
|
237
279
|
/**
|
|
@@ -246,7 +288,7 @@ export class Calendar extends VBox<CalendarProps, CalendarEventMap>
|
|
|
246
288
|
for (let m = 0; m < 12; m++) {
|
|
247
289
|
items.push(({
|
|
248
290
|
text: _tr.global.month_long[m],
|
|
249
|
-
click: () => { this.
|
|
291
|
+
click: () => { this._goto( this.m_date.getFullYear( ), m ); }
|
|
250
292
|
}));
|
|
251
293
|
}
|
|
252
294
|
}
|
|
@@ -258,7 +300,7 @@ export class Calendar extends VBox<CalendarProps, CalendarEventMap>
|
|
|
258
300
|
for (let m = max; m >= min; m--) {
|
|
259
301
|
items.push({
|
|
260
302
|
text: '' + m,
|
|
261
|
-
click: () => { this.
|
|
303
|
+
click: () => { this._goto( m, this.m_date.getMonth( ) ); }
|
|
262
304
|
});
|
|
263
305
|
}
|
|
264
306
|
}
|
|
@@ -271,12 +313,13 @@ export class Calendar extends VBox<CalendarProps, CalendarEventMap>
|
|
|
271
313
|
menu.displayAt(rc.left, rc.top);
|
|
272
314
|
}
|
|
273
315
|
|
|
316
|
+
// a copy: the calendar and its caller do not share the object
|
|
274
317
|
getDate() {
|
|
275
|
-
return this.m_date;
|
|
318
|
+
return date_clone( this.m_date );
|
|
276
319
|
}
|
|
277
320
|
|
|
278
321
|
setDate(date: Date) {
|
|
279
|
-
this.m_date = date;
|
|
322
|
+
this.m_date = date_clone( date );
|
|
280
323
|
this._update();
|
|
281
324
|
}
|
|
282
325
|
}
|
|
@@ -71,6 +71,7 @@ export class Checkbox extends Component<CheckboxProps,CheckBoxEvents> {
|
|
|
71
71
|
type:"checkbox",
|
|
72
72
|
id: inputId,
|
|
73
73
|
name: props.name,
|
|
74
|
+
value: props.value, // sent by a <form> of the browser when checked; a Form of x4 reads a boolean
|
|
74
75
|
checked: props.checked,
|
|
75
76
|
dom_events: {
|
|
76
77
|
change: ( ) => this._on_change( ),
|