x4js 2.3.20 → 2.3.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/aicontext.md +127 -32
- package/package.json +1 -1
- package/src/components/autocomplete/autocomplete.ts +3 -0
- package/src/components/boxes/boxes.ts +1 -1
- package/src/components/chart/chart.ts +12 -3
- package/src/components/colorinput/colorinput.module.scss +11 -2
- package/src/components/colorinput/colorinput.ts +37 -4
- package/src/components/colorpicker/colorpicker.module.scss +14 -2
- package/src/components/colorpicker/colorpicker.ts +57 -11
- package/src/components/components.ts +1 -0
- package/src/components/dialog/dialog.ts +16 -5
- package/src/components/form/form.ts +3 -4
- package/src/components/gridview/gridview.module.scss +18 -16
- package/src/components/gridview/gridview.ts +5 -0
- package/src/components/icon/icon.ts +34 -18
- package/src/components/keyboard/keyboard.ts +31 -5
- package/src/components/link/link.ts +18 -7
- package/src/components/messages/messages.ts +1 -1
- package/src/components/panel/panel.ts +3 -2
- package/src/components/settingsview/settingsview.module.scss +178 -0
- package/src/components/settingsview/settingsview.ts +289 -0
- package/src/components/spreadsheet/spreadsheet.module.scss +18 -16
- package/src/components/textarea/textarea.module.scss +10 -3
- package/src/components/video/video.ts +17 -3
- package/src/core/core_colors.ts +21 -10
- package/src/core/core_dragdrop.ts +5 -4
- package/src/core/core_i18n.ts +191 -54
- package/src/core/core_pdf.ts +15 -3
- package/src/core/core_shortcuts.ts +5 -1
- package/src/core/core_styles.ts +46 -25
- package/src/core/core_svg.ts +24 -16
- package/src/core/core_tools.ts +18 -9
package/aicontext.md
CHANGED
|
@@ -41,7 +41,7 @@ CoreElement<E> named timers + event surface (no DOM)
|
|
|
41
41
|
│ ├── CSizer → HSizer / VSizer
|
|
42
42
|
│ ├── FileDialog, ScrollView, Viewport, DockingView, Flex, Space
|
|
43
43
|
│ ├── HBox-based: ColorInput, Rating, Switch, Tag, Keyboard, TextEdit → AutoComplete, Breadcrumbs
|
|
44
|
-
│ └── VBox-based: Calendar, ColorPicker, FileDrop, Panel, PropertyGrid, TextArea
|
|
44
|
+
│ └── VBox-based: Calendar, ColorPicker, FileDrop, Panel, PropertyGrid, TextArea, SettingsView, SettingItem
|
|
45
45
|
└── Application<E> singleton, no DOM of its own
|
|
46
46
|
|
|
47
47
|
EventSource<E> standalone event registry
|
|
@@ -50,7 +50,7 @@ EventSource<E> standalone event registry
|
|
|
50
50
|
└── StateManager<T>
|
|
51
51
|
|
|
52
52
|
CoreElement-based (non-DOM): DataProxy, DataView, Store
|
|
53
|
-
Standalone: Rect, Timer, Color, DataModel, Shortcuts, Stylesheet, ComputedStyle,
|
|
53
|
+
Standalone: Rect, Timer, Color, DataModel, Shortcuts, Stylesheet (deprecated: CSS variables), ComputedStyle,
|
|
54
54
|
SvgBuilder, X4PDFBuilder, x4_react, CMover, UnsafeHtml (extends String)
|
|
55
55
|
```
|
|
56
56
|
|
|
@@ -499,10 +499,11 @@ interface LinkProps extends ComponentProps {
|
|
|
499
499
|
text?: string | UnsafeHtml
|
|
500
500
|
icon?: string
|
|
501
501
|
align?: "left" | "center" | "right"
|
|
502
|
-
click?: EventCallback<EvClick>
|
|
502
|
+
click?: EventCallback<EvClick> // ev.context = href; the link is followed unless ev.preventDefault()
|
|
503
|
+
// content: the link is its content (no label: text, icon, align are not used)
|
|
503
504
|
}
|
|
504
505
|
interface LinkEvents extends ComponentEvents { click: EvClick }
|
|
505
|
-
class Link extends Component { setText(text: string|UnsafeHtml): void }
|
|
506
|
+
class Link extends Component { setText(text: string|UnsafeHtml): void } // the text of its label: the icon stays
|
|
506
507
|
```
|
|
507
508
|
|
|
508
509
|
### `Icon`
|
|
@@ -510,9 +511,10 @@ class Link extends Component { setText(text: string|UnsafeHtml): void }
|
|
|
510
511
|
```ts
|
|
511
512
|
interface IconProps extends ComponentProps { iconId?: string }
|
|
512
513
|
class Icon extends Component { setIcon(iconId: string): void }
|
|
513
|
-
// iconId resolution: "var:name" →
|
|
514
|
+
// iconId resolution: "var:name" or "var:--name" → the variable --name set on <html> by the code
|
|
515
|
+
// (document.documentElement.style.setProperty("--name", svg); not one of a stylesheet) | "data:image/svg+xml,<svg..." → inline
|
|
514
516
|
// | "<svg..." → raw inline | "*.svg" → async load + cache | else → <img src>
|
|
515
|
-
export const svgLoader: SvgLoader // shared SVG cache
|
|
517
|
+
export const svgLoader: SvgLoader // shared SVG cache: load(url): Promise<string>; a file not found rejects, and is not asked again
|
|
516
518
|
```
|
|
517
519
|
|
|
518
520
|
### `Image`
|
|
@@ -647,12 +649,12 @@ interface SimpleTextAreaProps extends BaseProps { // BaseProps: see Input (nam
|
|
|
647
649
|
}
|
|
648
650
|
interface TextAreaProps extends SimpleTextAreaProps { label?: string }
|
|
649
651
|
|
|
650
|
-
class SimpleTextArea extends Component { // the <textarea> alone
|
|
652
|
+
class SimpleTextArea extends Component { // the <textarea> alone; never smaller than one character (min-height/min-width, padding included)
|
|
651
653
|
getText(): string; setText(text: string): void
|
|
652
654
|
scrollToBottom(): void
|
|
653
655
|
isValid(): boolean // false when it is required and empty
|
|
654
656
|
}
|
|
655
|
-
class TextArea extends VBox { // a Label above a SimpleTextArea
|
|
657
|
+
class TextArea extends VBox { // a Label above a SimpleTextArea; without a height, the textarea has its 2 rows (and the height the handle gives); with one, it fills it
|
|
656
658
|
getText(): string; setText(text: string): void
|
|
657
659
|
scrollToBottom(): void
|
|
658
660
|
isValid(): boolean
|
|
@@ -771,7 +773,7 @@ interface AutoCompleteProps extends TextEditProps { // label, labelWidth, requ
|
|
|
771
773
|
strict?: boolean // only a picked item (or empty) is accepted; default false
|
|
772
774
|
itemValue?: "text" | "id" // what goes into the field when an item is picked; default "text"
|
|
773
775
|
delay?: number // ms before search, default 250
|
|
774
|
-
minChars?: number // default 1
|
|
776
|
+
minChars?: number // default 1; ↓ searches whatever the length (ev.text may be "")
|
|
775
777
|
renderer?: (item: ListItem) => Component
|
|
776
778
|
}
|
|
777
779
|
class AutoComplete extends TextEdit {
|
|
@@ -788,12 +790,15 @@ class AutoComplete extends TextEdit {
|
|
|
788
790
|
interface ColorInputProps extends BoxProps {
|
|
789
791
|
color: Color | string // null: no color shown
|
|
790
792
|
name?: string
|
|
791
|
-
format?: "rgb" | "hex" // text of the field, "rgb" by default
|
|
793
|
+
format?: "rgb" | "hex" // text of the field, "rgb" by default; "rgba(...)" / "#rrggbbaa" when not opaque
|
|
792
794
|
picker?: boolean // a click on the swatch opens a ColorPicker
|
|
793
795
|
swatches?: (Color|string)[] | (() => (Color|string)[]) // a function is called each time the picker opens
|
|
794
796
|
nullable?: boolean // "no color" can be chosen
|
|
797
|
+
alpha?: boolean // false: colors kept opaque, no alpha slider in the picker (default true)
|
|
798
|
+
placeholder?: string // text of the empty field
|
|
795
799
|
change?: EventCallback<EvColorChange> // user changes only, not setColor
|
|
796
800
|
}
|
|
801
|
+
// typing: the text is not rewritten while typed; on blur it is written from the color in the typed form (#... or rgb...)
|
|
797
802
|
class ColorInput extends HBox { // queryInterface("form-element"): "#rrggbb" or null
|
|
798
803
|
getColor(): Color; getValue(): string
|
|
799
804
|
setColor(color: Color | string): void; setSwatches(swatches): void
|
|
@@ -870,7 +875,7 @@ class Gridview extends Component {
|
|
|
870
875
|
getView(): DataView
|
|
871
876
|
getSelection(): DataRecord[] // null when nothing is selected
|
|
872
877
|
getFirstSel(): DataRecord // null when nothing is selected
|
|
873
|
-
selectItem(id, ensureVisible?: boolean): void
|
|
878
|
+
selectItem(id, ensureVisible?: boolean): void // may be called before the grid is shown (then nothing is scrolled)
|
|
874
879
|
clearSelection(): void
|
|
875
880
|
selectAll(): void // multisel only; keeps the id of every record: costs memory on a very large view
|
|
876
881
|
navigate(sens: kbNav): boolean
|
|
@@ -954,7 +959,7 @@ interface ChartProps extends SvgProps {
|
|
|
954
959
|
type?: "line" | "area" | "bar" | "pie" | "donut" // default "line"
|
|
955
960
|
series: ChartSerie[] // required; pie & donut use the first serie
|
|
956
961
|
labels?: string[] // default: 1, 2, 3...
|
|
957
|
-
xaxis?: ChartAxis; yaxis?: ChartAxis // { name?, ticks?, min?, max?, step? }, all computed by default
|
|
962
|
+
xaxis?: ChartAxis; yaxis?: ChartAxis // { name?, ticks?, min?, max?, step? }, all computed by default; on x: max = number of points, step = points between labels (>= 1), min not used
|
|
958
963
|
legend?: boolean // default: shown when 2+ named series
|
|
959
964
|
stacked?: boolean
|
|
960
965
|
formatter?: (value: number) => string // default: chartFormatUnit (1.5k, 2M...)
|
|
@@ -1077,6 +1082,7 @@ interface DialogProps extends PopupProps {
|
|
|
1077
1082
|
title: string // required
|
|
1078
1083
|
buttons: BtnGroupItem[] // required
|
|
1079
1084
|
icon?: string; form?: Form
|
|
1085
|
+
content?: ComponentContent // without form: the content of its form
|
|
1080
1086
|
closable?: boolean | string; modal?: boolean
|
|
1081
1087
|
btnclick?: EventCallback<EvBtnClick>
|
|
1082
1088
|
}
|
|
@@ -1087,6 +1093,7 @@ class Dialog extends Popup {
|
|
|
1087
1093
|
getButton(name: string): Button
|
|
1088
1094
|
getBtnBar(): BtnGroup
|
|
1089
1095
|
setTitle(title: string): void
|
|
1096
|
+
show(): this // each time: the focus on its first field
|
|
1090
1097
|
showAsync(): Promise<string> // the name of the clicked button; null when closed without a button (Escape, close box)
|
|
1091
1098
|
}
|
|
1092
1099
|
```
|
|
@@ -1137,7 +1144,7 @@ interface FormProps extends BoxProps { autoComplete?: boolean }
|
|
|
1137
1144
|
class Form extends Box {
|
|
1138
1145
|
setValues(values: FormValues): void
|
|
1139
1146
|
getValues<T extends FormValues>(): T
|
|
1140
|
-
setAutoComplete(on?: boolean): void
|
|
1147
|
+
setAutoComplete(on?: boolean): void // attribute on the <form>: fields without their own follow it, added later too
|
|
1141
1148
|
setValidator(validator: (values: FormValues, is_valid: boolean) => boolean): void // a new call replaces the validator
|
|
1142
1149
|
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)
|
|
1143
1150
|
}
|
|
@@ -1146,6 +1153,78 @@ class Form extends Box {
|
|
|
1146
1153
|
|
|
1147
1154
|
---
|
|
1148
1155
|
|
|
1156
|
+
## `SettingsView` — a page of settings, as the settings of VS Code
|
|
1157
|
+
|
|
1158
|
+
The summary of the sections on the left, the sections on the right, one after the other, each a panel with its title on a band; in a section, each `SettingItem` with its name, what it does, then its controls. A click in the summary brings its section to the top; the summary follows the scroll and marks the section at the top of the view.
|
|
1159
|
+
|
|
1160
|
+
```ts
|
|
1161
|
+
interface SettingItemProps extends BoxProps {
|
|
1162
|
+
title: string
|
|
1163
|
+
description?: string // under the name
|
|
1164
|
+
content: Component | Component[] // its controls, on a line that wraps
|
|
1165
|
+
}
|
|
1166
|
+
class SettingItem extends VBox {
|
|
1167
|
+
getSearchText(): string // the title and the description: what a search looks in
|
|
1168
|
+
}
|
|
1169
|
+
// A SettingItem has no value of its own: its controls keep theirs. To read and write them together,
|
|
1170
|
+
// put the items in a Form (a section can hold a Form of SettingItem), give the controls a name,
|
|
1171
|
+
// and use getValues / setValues. The item is marked on its left while one of its controls has the focus.
|
|
1172
|
+
|
|
1173
|
+
interface SettingsSection {
|
|
1174
|
+
id: string
|
|
1175
|
+
title: string
|
|
1176
|
+
description?: string // under the title
|
|
1177
|
+
content: Component[] // SettingItem, Form of SettingItem, or any component
|
|
1178
|
+
}
|
|
1179
|
+
interface SettingsViewProps extends BoxProps {
|
|
1180
|
+
sections?: SettingsSection[]
|
|
1181
|
+
searchable?: boolean // a search field above the sections
|
|
1182
|
+
searchPlaceholder?: string // _tr.global.settings.search when missing
|
|
1183
|
+
noMatch?: string // shown when the search finds nothing; _tr.global.settings.nomatch when missing
|
|
1184
|
+
sectionChange?: EventCallback<EvSectionChange>
|
|
1185
|
+
}
|
|
1186
|
+
interface EvSectionChange extends ComponentEvent { readonly section: string } // the id of the section at the top
|
|
1187
|
+
class SettingsView<P extends SettingsViewProps = SettingsViewProps> extends VBox<P, SettingsViewEvents> {
|
|
1188
|
+
setSections(sections: SettingsSection[]): void // replaces the sections
|
|
1189
|
+
showSection(id: string, smooth = true): void // brings a section to the top
|
|
1190
|
+
getSection(): string // the id of the section at the top
|
|
1191
|
+
}
|
|
1192
|
+
// Search: the words typed are looked for without case nor accents; an item is shown when its title or
|
|
1193
|
+
// its description holds all of them. A section whose title or description holds them is shown whole;
|
|
1194
|
+
// otherwise only its items that hold them, and what holds those items (a Form, a box). What holds no
|
|
1195
|
+
// item (a table, a text) is shown with its whole section only. Escape empties the field.
|
|
1196
|
+
// The section marked in the summary is the last one whose top is above the top of the view (40px of
|
|
1197
|
+
// margin); the first one while the view is hidden (in a tab not shown). The last section shown is at
|
|
1198
|
+
// least as high as the view (class "last"): a click in the summary can bring any section to the top.
|
|
1199
|
+
// Classes: .x4settingsview > .search > .x4input, > .body > .toc (a Listbox), > .body > .sections;
|
|
1200
|
+
// .section(.last) > .title, .description; .sections > .nomatch; .x4settingitem > .name, .description, .controls
|
|
1201
|
+
// CSS variables: --settingsview-toc-width, --settingsview-section-background,
|
|
1202
|
+
// --settingsview-section-title-background, --settingsview-hover-background, --settingsview-marker
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
A page that builds its sections with references to its own components (`this.refs.x = ...`) cannot give them to `super()`: `this` is not there yet. It calls `setSections` after `super()`. `SettingsView` keeps its own parts in private fields, not in `refs`: `refs` is left to the classes that derive from it.
|
|
1206
|
+
|
|
1207
|
+
```ts
|
|
1208
|
+
class MySettings extends SettingsView {
|
|
1209
|
+
declare protected refs: { size: Input }
|
|
1210
|
+
|
|
1211
|
+
constructor(props: BoxProps) {
|
|
1212
|
+
super({ ...props, searchable: true })
|
|
1213
|
+
|
|
1214
|
+
this.setSections([
|
|
1215
|
+
{ id: "editor", title: "Editor", content: [
|
|
1216
|
+
new Form({ content: [
|
|
1217
|
+
new SettingItem({ title: "Font size", description: "The size of the text, in pixels.",
|
|
1218
|
+
content: this.refs.size = new Input({ type: "number", name: "fontSize", value: 14 }) }),
|
|
1219
|
+
] }),
|
|
1220
|
+
] },
|
|
1221
|
+
])
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
```
|
|
1225
|
+
|
|
1226
|
+
---
|
|
1227
|
+
|
|
1149
1228
|
## Sizers / viewport / misc
|
|
1150
1229
|
|
|
1151
1230
|
```ts
|
|
@@ -1162,8 +1241,9 @@ class VSizer extends CSizer // vertical splitter
|
|
|
1162
1241
|
class ScrollView extends Component { getViewport(): Component }
|
|
1163
1242
|
class Viewport extends Component
|
|
1164
1243
|
class Canvas extends Component { getContext(): CanvasRenderingContext2D; paint(): void }
|
|
1165
|
-
class Video extends Box {
|
|
1166
|
-
play()
|
|
1244
|
+
class Video extends Box { // class x4video
|
|
1245
|
+
play(): Promise<boolean> // false: could not start (a click of the user wanted, a source not playable); no source: waits for one
|
|
1246
|
+
pause(); mute(set: boolean)
|
|
1167
1247
|
get player(): HTMLVideoElement; get duration(): number; get paused(): boolean
|
|
1168
1248
|
get volume(): number; get videoWidth(): number; get videoHeight(): number
|
|
1169
1249
|
set stream(stream: MediaStream)
|
|
@@ -1171,12 +1251,15 @@ class Video extends Box {
|
|
|
1171
1251
|
class Calendar extends VBox { // props: date?, minDate?, maxDate? (days out of them cannot be chosen), change ("change": the day clicked)
|
|
1172
1252
|
getDate(): Date; setDate(date: Date): void // copies: the calendar keeps its own date
|
|
1173
1253
|
}
|
|
1174
|
-
class ColorPicker extends VBox { // props: color, swatches?, nullable
|
|
1254
|
+
class ColorPicker extends VBox { // props: color, swatches?, nullable?, alpha? (false: no alpha slider, opaque colors); "change": EvColorChange, also fired once when built
|
|
1175
1255
|
getColor(): Color; setColor(color: Color | string): void; setSwatches(swatches): void
|
|
1176
1256
|
}
|
|
1177
|
-
class Panel extends VBox { setTitle(title: string): void }
|
|
1257
|
+
class Panel extends VBox { setTitle(title: string): void } // the text of its title: its icon stays
|
|
1178
1258
|
class Breadcrumbs extends HBox { setItems(elements): void }
|
|
1179
|
-
interface KeyboardProps extends BoxProps {
|
|
1259
|
+
interface KeyboardProps extends BoxProps {
|
|
1260
|
+
locale?: string // "fr-FR" (default), "de-CH", "pl"...
|
|
1261
|
+
numpadOnly?: boolean // number or date field: the pad has no "abc" key back to the letters
|
|
1262
|
+
}
|
|
1180
1263
|
class Keyboard extends HBox { // virtual keyboard, shown while an <input> has the focus
|
|
1181
1264
|
setLocale(locale: string): void
|
|
1182
1265
|
setZoom(perc: number): void
|
|
@@ -1184,6 +1267,7 @@ class Keyboard extends HBox { // virtual keyboard, shown while an
|
|
|
1184
1267
|
}
|
|
1185
1268
|
// languages: cs da de en es et fi fr ga hr hu it lt lv mt nl pl pt ro sk sl sv
|
|
1186
1269
|
// bottom row: 123 (numeric pad), the letters of the language (éè, none in en), the signs (#+=); Abc comes back
|
|
1270
|
+
// a key acts on pointerdown; the keys that draw the keyboard again (shift, 123, abc, accents, signs) on the click
|
|
1187
1271
|
class TickLine extends Component
|
|
1188
1272
|
class Header extends HBox // resizable column header; updates target style "--{name}-width"
|
|
1189
1273
|
```
|
|
@@ -1287,7 +1371,7 @@ parseRoute(str: string|RegExp, loose?: boolean): Segment
|
|
|
1287
1371
|
|
|
1288
1372
|
## Shortcuts (`core_shortcuts`)
|
|
1289
1373
|
|
|
1290
|
-
Keyboard shortcuts. A sequence is written as it is read: `"Mod+C"`, `"Shift+Mod+Z"`, `"Delete"`, `"Alt+ArrowLeft"`.
|
|
1374
|
+
Keyboard shortcuts. A sequence is written as it is read: `"Mod+C"`, `"Shift+Mod+Z"`, `"Delete"`, `"Alt+ArrowLeft"`, `"Ctrl+Space"`.
|
|
1291
1375
|
Modifiers: `Shift`, `Ctrl`, `Cmd`, `Alt`, and `Mod` = the key of the commands (Ctrl on Windows and Linux, Cmd on a Mac). Prefer `Mod`.
|
|
1292
1376
|
|
|
1293
1377
|
```ts
|
|
@@ -1393,7 +1477,7 @@ dragManager.registerDropTarget(column, (cmd, el, infos) => {
|
|
|
1393
1477
|
|
|
1394
1478
|
- `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
1479
|
- 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`
|
|
1480
|
+
- `enter` comes at once, before the first `drag`. `leave` is found by a timer: it comes up to 300 ms after the pointer went away (over the children of a target the browser sends dragleave and dragover in turn), and a `leave` also follows a `drop`. Between them the target has the class `drop-over`.
|
|
1397
1481
|
- `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
1482
|
- `Image` (`candrop`) and `FileDrop` already use it for files.
|
|
1399
1483
|
|
|
@@ -1401,13 +1485,15 @@ dragManager.registerDropTarget(column, (cmd, el, infos) => {
|
|
|
1401
1485
|
|
|
1402
1486
|
## i18n (`core_i18n`)
|
|
1403
1487
|
|
|
1404
|
-
|
|
1488
|
+
Four built-in languages: `fr` (default), `en` (based on `fr`), `es` and `de` (based on `en`). Dates: `D/M/Y` in fr and es, `M/D/Y` in en, `D.M.Y` in de.
|
|
1405
1489
|
|
|
1406
1490
|
```ts
|
|
1407
1491
|
export let _tr // current translation object (proxy)
|
|
1408
1492
|
createLanguage(name: string, base: string): void
|
|
1409
|
-
addTranslation(name: string, ...parts: any[]): void
|
|
1493
|
+
addTranslation(name: string, ...parts: any[]): void // seen at once, the language being selected or not
|
|
1410
1494
|
selectLanguage(name: string): typeof _tr
|
|
1495
|
+
// a key missing in a language is taken from its base (createLanguage), at the same path;
|
|
1496
|
+
// a key no language has: console.error "I18N error: unable to find _tr.x.y", undefined
|
|
1411
1497
|
getCurrentLanguage(): string
|
|
1412
1498
|
getAvailableLanguages(): string[]
|
|
1413
1499
|
isLanguage(name: string): boolean
|
|
@@ -1416,7 +1502,8 @@ isLanguage(name: string): boolean
|
|
|
1416
1502
|
// search search_tip required_field invalid_format invalid_email invalid_number invalid_date
|
|
1417
1503
|
// empty_list err_403 property value copy cut paste filedrop
|
|
1418
1504
|
// diff_date_seconds/minutes/hours, date_input_formats, date_format, date_time_format,
|
|
1419
|
-
// day_short[] day_long[] month_short[] month_long[], keyboard.{next,numeric,alpha}
|
|
1505
|
+
// day_short[] day_long[] month_short[] month_long[], keyboard.{next,numeric,alpha},
|
|
1506
|
+
// settings.{search,nomatch}
|
|
1420
1507
|
```
|
|
1421
1508
|
|
|
1422
1509
|
---
|
|
@@ -1426,7 +1513,7 @@ isLanguage(name: string): boolean
|
|
|
1426
1513
|
```ts
|
|
1427
1514
|
class UnsafeHtml extends String // raw, unescaped HTML marker
|
|
1428
1515
|
unsafeHtml(x: string): UnsafeHtml
|
|
1429
|
-
unsafe(strings, ...values): UnsafeHtml // template tag: unsafe`<b>${x}</b
|
|
1516
|
+
unsafe(strings, ...values): UnsafeHtml // template tag: unsafe`<b>${x}</b>`; values escaped (an UnsafeHtml kept), null/undefined write nothing, 0 and false are written
|
|
1430
1517
|
sanitizeHtml(input: string): string // escape &<>"' for safe text insertion
|
|
1431
1518
|
safeText(v: string | UnsafeHtml): string // UnsafeHtml passes through; plain string is escaped
|
|
1432
1519
|
|
|
@@ -1443,10 +1530,12 @@ interface IRect { left; top; height; width: number }
|
|
|
1443
1530
|
class Timer { setTimeout; clearTimeout; setInterval; clearInterval; clearAllTimeouts }
|
|
1444
1531
|
|
|
1445
1532
|
class Color {
|
|
1446
|
-
// new Color(value) | new Color(r,g,b,a?)
|
|
1447
|
-
setValue(value): this; setRgb(r,g,b,a): this; setHsv(h,s,v,a?): this; setAlpha(a): this
|
|
1448
|
-
getAlpha(): number; lighten(percent): this
|
|
1449
|
-
|
|
1533
|
+
// new Color(value) | new Color(r,g,b,a?) a: 0..1, default 1
|
|
1534
|
+
setValue(value): this; setRgb(r,g,b,a?): this; setHsv(h,s,v,a?): this; setAlpha(a): this
|
|
1535
|
+
getAlpha(): number; lighten(percent): this
|
|
1536
|
+
isInvalid(): boolean // the last setValue could not be read (the color is red); setRgb/setHsv make it valid
|
|
1537
|
+
toHexString(): string; toNumber(): number
|
|
1538
|
+
toRgbString(withAlpha = false): string // "rgb(r,g,b)"; true: "rgba(r,g,b,0.500)" when not opaque, "rgb(...)" when opaque
|
|
1450
1539
|
toRgb(): Rgb; toHsv(): Hsv
|
|
1451
1540
|
}
|
|
1452
1541
|
|
|
@@ -1465,10 +1554,11 @@ pascalCase(s): string; camelCase(s): string; clamp<T>(v, min, max): T
|
|
|
1465
1554
|
measureText(str, size, bold?): number // width in Helvetica, from core_pdf (see PDF)
|
|
1466
1555
|
|
|
1467
1556
|
// Dates
|
|
1468
|
-
date_format(date, options?): string
|
|
1469
|
-
date_diff(date1, date2, options?): string
|
|
1557
|
+
date_format(date, options?): string // options not read yet: formatIntlDate(date)
|
|
1558
|
+
date_diff(date1, date2, options?): string // "30 secondes", "5 minutes", "2 heures 5 minutes" (either order); options not read
|
|
1470
1559
|
date_to_sql(date, withHours): string; date_sql_utc(date): Date
|
|
1471
|
-
date_hash(date): number; date_clone(date): Date
|
|
1560
|
+
date_hash(date): number; date_clone(date): Date
|
|
1561
|
+
date_calc_weeknum(date): number // ISO 8601: weeks from monday, week 1 holds the first thursday (29/12/2025 → 1)
|
|
1472
1562
|
parseIntlDate(value, fmts?): Date; formatIntlDate(date, fmt?, utc?): string
|
|
1473
1563
|
calcAge(birth, ref?): number
|
|
1474
1564
|
|
|
@@ -1504,7 +1594,7 @@ const makeUniqueComponentId // () => string, "x4-<n>" (exported as a value)
|
|
|
1504
1594
|
|
|
1505
1595
|
```
|
|
1506
1596
|
d day (no zero) D day (2 digits) j short day (mon) J long day (monday)
|
|
1507
|
-
w week number W week (2 digits) m month (no zero) M month (2 digits)
|
|
1597
|
+
w week number (ISO 8601) W week (2 digits) m month (no zero) M month (2 digits)
|
|
1508
1598
|
o short month O long month y/Y year h 24h hour
|
|
1509
1599
|
H hour (2 digits) i minutes I minutes (2 dig) s seconds
|
|
1510
1600
|
S seconds (2 dig) a am/pm l ms L ms (3 digits)
|
|
@@ -1526,6 +1616,9 @@ class SvgBuilder extends SvgGroup {
|
|
|
1526
1616
|
}
|
|
1527
1617
|
// Items: SvgGroup, SvgPath (moveTo/lineTo/curveTo/arc/closePath), SvgText (font/fontSize/textAlign),
|
|
1528
1618
|
// SvgShape, SvgIcon, SvgGradient (addStop)
|
|
1619
|
+
// arc(x, y, r, start, end): angles in degrees, 0 is the top, 90 the right; it starts a new subpath
|
|
1620
|
+
// addStop(offset, color): a number is a percentage (20 → "20%"), "50%" as it is
|
|
1621
|
+
// getNumAttr(name): 0 when not set; SvgIcon(svg): its svg needs xmlns="http://www.w3.org/2000/svg"
|
|
1529
1622
|
```
|
|
1530
1623
|
|
|
1531
1624
|
---
|
|
@@ -1793,6 +1886,8 @@ Optional keys: `external`, `define`, `esbuild`, `dev` (`host`, `port`, `https`,
|
|
|
1793
1886
|
}
|
|
1794
1887
|
```
|
|
1795
1888
|
|
|
1889
|
+
`experimentalDecorators` is required: the decorators `@data.*` of a `DataModel` are written for it. Without it esbuild applies the standard JavaScript decorators, and the fields of a model are not declared.
|
|
1890
|
+
|
|
1796
1891
|
### `src/index.html`
|
|
1797
1892
|
|
|
1798
1893
|
```html
|
package/package.json
CHANGED
|
@@ -158,6 +158,9 @@ export class AutoComplete extends TextEdit<AutoCompleteEvents> {
|
|
|
158
158
|
if( text.length<this._min_chars ) {
|
|
159
159
|
this.clearTimeout( "search" );
|
|
160
160
|
this._popup.show( false );
|
|
161
|
+
|
|
162
|
+
// the suggestions were for another text: ↓ asks for new ones
|
|
163
|
+
this._count = 0;
|
|
161
164
|
return;
|
|
162
165
|
}
|
|
163
166
|
|
|
@@ -508,7 +508,7 @@ export class GridBox<P extends GridBoxProps=GridBoxProps,E extends ComponentEven
|
|
|
508
508
|
*/
|
|
509
509
|
|
|
510
510
|
setTemplate( t: string[] ) {
|
|
511
|
-
this.
|
|
511
|
+
this.setStyleValue( "gridTemplateAreas", t.map( x => '"' + x + '"' ).join(" ") );
|
|
512
512
|
}
|
|
513
513
|
|
|
514
514
|
/**
|
|
@@ -31,7 +31,7 @@ export interface ChartTick {
|
|
|
31
31
|
export interface ChartAxis {
|
|
32
32
|
name?: string;
|
|
33
33
|
ticks?: ChartTick[];
|
|
34
|
-
min?: number;
|
|
34
|
+
min?: number; // not used on x axis
|
|
35
35
|
max?: number; // on x axis: number of points
|
|
36
36
|
step?: number; // on x axis: one label every 'step' points
|
|
37
37
|
}
|
|
@@ -102,6 +102,11 @@ export function chartFormatUnit( value: number, decimals = 2 ) {
|
|
|
102
102
|
|
|
103
103
|
function niceScale( min: number, max: number, maxTicks: number ) {
|
|
104
104
|
|
|
105
|
+
// a bound given beyond the values: an empty range at that bound
|
|
106
|
+
if( max<min ) {
|
|
107
|
+
max = min;
|
|
108
|
+
}
|
|
109
|
+
|
|
105
110
|
if( min==max ) {
|
|
106
111
|
if( min==0 ) { max = 1; }
|
|
107
112
|
else if( min>0 ) { min = 0; }
|
|
@@ -152,9 +157,12 @@ function computeStack( series: ChartSerie[], count: number ) {
|
|
|
152
157
|
const neg = new Array<number>( count ).fill( 0 );
|
|
153
158
|
|
|
154
159
|
return series.map( s => {
|
|
160
|
+
// a serie without values counts 0
|
|
161
|
+
const values = s.values || [];
|
|
162
|
+
|
|
155
163
|
const out: { lo: number, hi: number }[] = [];
|
|
156
164
|
for( let i=0; i<count; i++ ) {
|
|
157
|
-
const v = isNumber(
|
|
165
|
+
const v = isNumber( values[i] ) ? values[i] : 0;
|
|
158
166
|
const acc = v>=0 ? pos : neg;
|
|
159
167
|
|
|
160
168
|
out.push( { lo: acc[i], hi: acc[i]+v } );
|
|
@@ -591,7 +599,8 @@ export class Chart extends SvgComponent<ChartProps,ChartEvents> {
|
|
|
591
599
|
}
|
|
592
600
|
|
|
593
601
|
// too many labels: show one every n
|
|
594
|
-
|
|
602
|
+
// at least one point between two labels
|
|
603
|
+
const every = xaxis.step>0 ? Math.max( 1, Math.round( xaxis.step ) ) : Math.max( 1, Math.ceil( ( maxw+8 ) / ( xstep || plot.width ) ) );
|
|
595
604
|
for( let i=0; i<count; i+=every ) {
|
|
596
605
|
xticks.push( { value: i, text: this._label(i) } );
|
|
597
606
|
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
--colorinput-btn-color: var( --text-secondary );
|
|
20
20
|
--colorinput-btn-color-hover: var( --text-primary );
|
|
21
21
|
--colorinput-popup-background: var( --background-primary );
|
|
22
|
+
--colorinput-none: var( --alert-background );
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
.x4colorinput {
|
|
@@ -35,6 +36,10 @@
|
|
|
35
36
|
width: 1rem;
|
|
36
37
|
height: 1rem;
|
|
37
38
|
|
|
39
|
+
// its color over checkers: a color that is not opaque is seen as such
|
|
40
|
+
background: linear-gradient( var( --swatch-color ), var( --swatch-color ) ),
|
|
41
|
+
repeating-conic-gradient( #c8c8c8 0% 25%, #ffffff 0% 50% ) 0 0 / 6px 6px;
|
|
42
|
+
|
|
38
43
|
// props.picker: a click opens the color picker. It is a button:
|
|
39
44
|
// wide enough to be aimed at.
|
|
40
45
|
&.picker {
|
|
@@ -50,10 +55,14 @@
|
|
|
50
55
|
}
|
|
51
56
|
}
|
|
52
57
|
|
|
53
|
-
//
|
|
58
|
+
// no color: white, crossed out from bottom left to top right, the edges of the line
|
|
59
|
+
// blurred by 1px (smooth); checkers are for a transparent color
|
|
54
60
|
&.none {
|
|
55
61
|
border: 1px solid var( --border );
|
|
56
|
-
background:
|
|
62
|
+
background: linear-gradient( to bottom right,
|
|
63
|
+
transparent calc( 50% - 1.5px ), var( --colorinput-none ) calc( 50% - 0.5px ),
|
|
64
|
+
var( --colorinput-none ) calc( 50% + 0.5px ), transparent calc( 50% + 1.5px ) ),
|
|
65
|
+
white;
|
|
57
66
|
}
|
|
58
67
|
}
|
|
59
68
|
|
|
@@ -59,6 +59,12 @@ export interface ColorInputProps extends BoxProps {
|
|
|
59
59
|
/** The field may have no color: emptied, or "no color" picked. */
|
|
60
60
|
nullable?: boolean;
|
|
61
61
|
|
|
62
|
+
/** false: the colors are opaque, the picker has no alpha slider (default true). */
|
|
63
|
+
alpha?: boolean;
|
|
64
|
+
|
|
65
|
+
/** The text shown in the field while it is empty. */
|
|
66
|
+
placeholder?: string;
|
|
67
|
+
|
|
62
68
|
/** The user changed the color (not fired by setColor). */
|
|
63
69
|
change?: EventCallback<EvColorChange>;
|
|
64
70
|
}
|
|
@@ -74,6 +80,7 @@ interface ColorInputEvents extends ComponentEvents {
|
|
|
74
80
|
* --colorinput-btn-color
|
|
75
81
|
* --colorinput-btn-color-hover
|
|
76
82
|
* --colorinput-popup-background
|
|
83
|
+
* --colorinput-none
|
|
77
84
|
* ```
|
|
78
85
|
*/
|
|
79
86
|
|
|
@@ -100,7 +107,7 @@ export class ColorInput extends HBox<ColorInputProps,ColorInputEvents> {
|
|
|
100
107
|
|
|
101
108
|
this.setContent( [
|
|
102
109
|
this._swatch = new Component( { cls: "swatch" } ),
|
|
103
|
-
this._edit = new Input( { type: "text", value: "", spellcheck: false } ),
|
|
110
|
+
this._edit = new Input( { type: "text", value: "", spellcheck: false, placeholder: props.placeholder } ),
|
|
104
111
|
|
|
105
112
|
isFeatureAvailable("eyedropper") ? new Button( { icon: icon, click: ( ) => {
|
|
106
113
|
const eyeDropper = new (window as any).EyeDropper();
|
|
@@ -112,6 +119,7 @@ export class ColorInput extends HBox<ColorInputProps,ColorInputEvents> {
|
|
|
112
119
|
} } ) : null
|
|
113
120
|
])
|
|
114
121
|
|
|
122
|
+
// while it is typed, the text is left alone: the color follows it
|
|
115
123
|
this._edit.addDOMEvent( "input", ( ) => {
|
|
116
124
|
const txt = this._edit.getValue( );
|
|
117
125
|
|
|
@@ -122,10 +130,16 @@ export class ColorInput extends HBox<ColorInputProps,ColorInputEvents> {
|
|
|
122
130
|
|
|
123
131
|
const clr = new Color( txt );
|
|
124
132
|
if( !clr.isInvalid() ) {
|
|
125
|
-
this._changed( clr );
|
|
133
|
+
this._changed( clr, false );
|
|
126
134
|
}
|
|
127
135
|
});
|
|
128
136
|
|
|
137
|
+
// the field is left: the text is written from the color, in the form that was typed
|
|
138
|
+
// (a text that could not be read shows the color that was kept)
|
|
139
|
+
this._edit.addDOMEvent( "change", ( ) => {
|
|
140
|
+
this._show( this._color );
|
|
141
|
+
});
|
|
142
|
+
|
|
129
143
|
if( props.picker ) {
|
|
130
144
|
// A click on the swatch opens the picker, a second one closes it.
|
|
131
145
|
// The picker closes by itself as soon as the button goes down
|
|
@@ -160,7 +174,19 @@ export class ColorInput extends HBox<ColorInputProps,ColorInputEvents> {
|
|
|
160
174
|
}
|
|
161
175
|
|
|
162
176
|
private _format( color: Color ): string {
|
|
163
|
-
|
|
177
|
+
// the form of the text in the field: "#..." or "rgb...", else props.format
|
|
178
|
+
const text = this._edit.getValue( ).trim( ).toLowerCase( );
|
|
179
|
+
|
|
180
|
+
let hex = this.props.format=="hex";
|
|
181
|
+
if( text.startsWith( "#" ) ) {
|
|
182
|
+
hex = true;
|
|
183
|
+
}
|
|
184
|
+
else if( text.startsWith( "rgb" ) ) {
|
|
185
|
+
hex = false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// rgba / #rrggbbaa when the color is not opaque
|
|
189
|
+
return hex ? color.toHexString( ) : color.toRgbString( true );
|
|
164
190
|
}
|
|
165
191
|
|
|
166
192
|
/**
|
|
@@ -169,10 +195,16 @@ export class ColorInput extends HBox<ColorInputProps,ColorInputEvents> {
|
|
|
169
195
|
*/
|
|
170
196
|
|
|
171
197
|
private _show( color: Color, text = true ) {
|
|
198
|
+
// no alpha: every color shown is opaque, typed, picked or given
|
|
199
|
+
if( color && this.props.alpha===false ) {
|
|
200
|
+
color.setAlpha( 1 );
|
|
201
|
+
}
|
|
202
|
+
|
|
172
203
|
this._color = color;
|
|
173
204
|
|
|
174
205
|
this._swatch.setClass( "none", !color );
|
|
175
|
-
|
|
206
|
+
// the stylesheet draws it over checkers
|
|
207
|
+
this._swatch.setStyleVariable( "--swatch-color", color ? color.toRgbString( true ) : "" );
|
|
176
208
|
|
|
177
209
|
if( text ) {
|
|
178
210
|
this._edit.setValue( color ? this._format( color ) : "" );
|
|
@@ -201,6 +233,7 @@ export class ColorInput extends HBox<ColorInputProps,ColorInputEvents> {
|
|
|
201
233
|
color: this._color ? new Color( this._color.toHexString( ) ) : new Color( 255, 255, 255 ),
|
|
202
234
|
swatches,
|
|
203
235
|
nullable: this.props.nullable,
|
|
236
|
+
alpha: this.props.alpha,
|
|
204
237
|
} );
|
|
205
238
|
|
|
206
239
|
const popup = this._popup = new Popup( { cls: "x4colorinput-popup", autoClose: true, content: picker } );
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
|
|
17
17
|
@use "../shared.scss";
|
|
18
18
|
|
|
19
|
+
:root {
|
|
20
|
+
--colorpicker-none: var( --alert-background );
|
|
21
|
+
}
|
|
22
|
+
|
|
19
23
|
@mixin overlay-shadow {
|
|
20
24
|
width: 12px;
|
|
21
25
|
height: 12px;
|
|
@@ -143,13 +147,21 @@
|
|
|
143
147
|
border-radius: 3px;
|
|
144
148
|
cursor: pointer;
|
|
145
149
|
|
|
150
|
+
// its color over checkers: a color that is not opaque is seen as such
|
|
151
|
+
background: linear-gradient( var( --swatch-color ), var( --swatch-color ) ),
|
|
152
|
+
repeating-conic-gradient( #c8c8c8 0% 25%, #ffffff 0% 50% ) 0 0 / 6px 6px;
|
|
153
|
+
|
|
146
154
|
&:hover {
|
|
147
155
|
border-color: var( --border-focus );
|
|
148
156
|
}
|
|
149
157
|
|
|
150
|
-
// no color:
|
|
158
|
+
// no color: white, crossed out from bottom left to top right, the edges of the line
|
|
159
|
+
// blurred by 1px (smooth); checkers are for a transparent color
|
|
151
160
|
&.none {
|
|
152
|
-
background:
|
|
161
|
+
background: linear-gradient( to bottom right,
|
|
162
|
+
transparent calc( 50% - 1.5px ), var( --colorpicker-none ) calc( 50% - 0.5px ),
|
|
163
|
+
var( --colorpicker-none ) calc( 50% + 0.5px ), transparent calc( 50% + 1.5px ) ),
|
|
164
|
+
white;
|
|
153
165
|
}
|
|
154
166
|
}
|
|
155
167
|
}
|