x4js 2.3.20 → 2.3.21
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 +46 -28
- 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/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 +7 -3
- 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/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 +34 -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
|
@@ -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
|
}
|
|
@@ -1162,8 +1169,9 @@ class VSizer extends CSizer // vertical splitter
|
|
|
1162
1169
|
class ScrollView extends Component { getViewport(): Component }
|
|
1163
1170
|
class Viewport extends Component
|
|
1164
1171
|
class Canvas extends Component { getContext(): CanvasRenderingContext2D; paint(): void }
|
|
1165
|
-
class Video extends Box {
|
|
1166
|
-
play()
|
|
1172
|
+
class Video extends Box { // class x4video
|
|
1173
|
+
play(): Promise<boolean> // false: could not start (a click of the user wanted, a source not playable); no source: waits for one
|
|
1174
|
+
pause(); mute(set: boolean)
|
|
1167
1175
|
get player(): HTMLVideoElement; get duration(): number; get paused(): boolean
|
|
1168
1176
|
get volume(): number; get videoWidth(): number; get videoHeight(): number
|
|
1169
1177
|
set stream(stream: MediaStream)
|
|
@@ -1171,10 +1179,10 @@ class Video extends Box {
|
|
|
1171
1179
|
class Calendar extends VBox { // props: date?, minDate?, maxDate? (days out of them cannot be chosen), change ("change": the day clicked)
|
|
1172
1180
|
getDate(): Date; setDate(date: Date): void // copies: the calendar keeps its own date
|
|
1173
1181
|
}
|
|
1174
|
-
class ColorPicker extends VBox { // props: color, swatches?, nullable
|
|
1182
|
+
class ColorPicker extends VBox { // props: color, swatches?, nullable?, alpha? (false: no alpha slider, opaque colors); "change": EvColorChange, also fired once when built
|
|
1175
1183
|
getColor(): Color; setColor(color: Color | string): void; setSwatches(swatches): void
|
|
1176
1184
|
}
|
|
1177
|
-
class Panel extends VBox { setTitle(title: string): void }
|
|
1185
|
+
class Panel extends VBox { setTitle(title: string): void } // the text of its title: its icon stays
|
|
1178
1186
|
class Breadcrumbs extends HBox { setItems(elements): void }
|
|
1179
1187
|
interface KeyboardProps extends BoxProps { locale?: string } // "fr-FR" (default), "de-CH", "pl"...
|
|
1180
1188
|
class Keyboard extends HBox { // virtual keyboard, shown while an <input> has the focus
|
|
@@ -1287,7 +1295,7 @@ parseRoute(str: string|RegExp, loose?: boolean): Segment
|
|
|
1287
1295
|
|
|
1288
1296
|
## Shortcuts (`core_shortcuts`)
|
|
1289
1297
|
|
|
1290
|
-
Keyboard shortcuts. A sequence is written as it is read: `"Mod+C"`, `"Shift+Mod+Z"`, `"Delete"`, `"Alt+ArrowLeft"`.
|
|
1298
|
+
Keyboard shortcuts. A sequence is written as it is read: `"Mod+C"`, `"Shift+Mod+Z"`, `"Delete"`, `"Alt+ArrowLeft"`, `"Ctrl+Space"`.
|
|
1291
1299
|
Modifiers: `Shift`, `Ctrl`, `Cmd`, `Alt`, and `Mod` = the key of the commands (Ctrl on Windows and Linux, Cmd on a Mac). Prefer `Mod`.
|
|
1292
1300
|
|
|
1293
1301
|
```ts
|
|
@@ -1393,7 +1401,7 @@ dragManager.registerDropTarget(column, (cmd, el, infos) => {
|
|
|
1393
1401
|
|
|
1394
1402
|
- `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
1403
|
- 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`
|
|
1404
|
+
- `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
1405
|
- `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
1406
|
- `Image` (`candrop`) and `FileDrop` already use it for files.
|
|
1399
1407
|
|
|
@@ -1406,8 +1414,10 @@ Two built-in languages (`fr` default, and `en`).
|
|
|
1406
1414
|
```ts
|
|
1407
1415
|
export let _tr // current translation object (proxy)
|
|
1408
1416
|
createLanguage(name: string, base: string): void
|
|
1409
|
-
addTranslation(name: string, ...parts: any[]): void
|
|
1417
|
+
addTranslation(name: string, ...parts: any[]): void // seen at once, the language being selected or not
|
|
1410
1418
|
selectLanguage(name: string): typeof _tr
|
|
1419
|
+
// a key missing in a language is taken from its base (createLanguage), at the same path;
|
|
1420
|
+
// a key no language has: console.error "I18N error: unable to find _tr.x.y", undefined
|
|
1411
1421
|
getCurrentLanguage(): string
|
|
1412
1422
|
getAvailableLanguages(): string[]
|
|
1413
1423
|
isLanguage(name: string): boolean
|
|
@@ -1426,7 +1436,7 @@ isLanguage(name: string): boolean
|
|
|
1426
1436
|
```ts
|
|
1427
1437
|
class UnsafeHtml extends String // raw, unescaped HTML marker
|
|
1428
1438
|
unsafeHtml(x: string): UnsafeHtml
|
|
1429
|
-
unsafe(strings, ...values): UnsafeHtml // template tag: unsafe`<b>${x}</b
|
|
1439
|
+
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
1440
|
sanitizeHtml(input: string): string // escape &<>"' for safe text insertion
|
|
1431
1441
|
safeText(v: string | UnsafeHtml): string // UnsafeHtml passes through; plain string is escaped
|
|
1432
1442
|
|
|
@@ -1443,10 +1453,12 @@ interface IRect { left; top; height; width: number }
|
|
|
1443
1453
|
class Timer { setTimeout; clearTimeout; setInterval; clearInterval; clearAllTimeouts }
|
|
1444
1454
|
|
|
1445
1455
|
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
|
-
|
|
1456
|
+
// new Color(value) | new Color(r,g,b,a?) a: 0..1, default 1
|
|
1457
|
+
setValue(value): this; setRgb(r,g,b,a?): this; setHsv(h,s,v,a?): this; setAlpha(a): this
|
|
1458
|
+
getAlpha(): number; lighten(percent): this
|
|
1459
|
+
isInvalid(): boolean // the last setValue could not be read (the color is red); setRgb/setHsv make it valid
|
|
1460
|
+
toHexString(): string; toNumber(): number
|
|
1461
|
+
toRgbString(withAlpha = false): string // "rgb(r,g,b)"; true: "rgba(r,g,b,0.500)" when not opaque, "rgb(...)" when opaque
|
|
1450
1462
|
toRgb(): Rgb; toHsv(): Hsv
|
|
1451
1463
|
}
|
|
1452
1464
|
|
|
@@ -1465,10 +1477,11 @@ pascalCase(s): string; camelCase(s): string; clamp<T>(v, min, max): T
|
|
|
1465
1477
|
measureText(str, size, bold?): number // width in Helvetica, from core_pdf (see PDF)
|
|
1466
1478
|
|
|
1467
1479
|
// Dates
|
|
1468
|
-
date_format(date, options?): string
|
|
1469
|
-
date_diff(date1, date2, options?): string
|
|
1480
|
+
date_format(date, options?): string // options not read yet: formatIntlDate(date)
|
|
1481
|
+
date_diff(date1, date2, options?): string // "30 secondes", "5 minutes", "2 heures 5 minutes" (either order); options not read
|
|
1470
1482
|
date_to_sql(date, withHours): string; date_sql_utc(date): Date
|
|
1471
|
-
date_hash(date): number; date_clone(date): Date
|
|
1483
|
+
date_hash(date): number; date_clone(date): Date
|
|
1484
|
+
date_calc_weeknum(date): number // ISO 8601: weeks from monday, week 1 holds the first thursday (29/12/2025 → 1)
|
|
1472
1485
|
parseIntlDate(value, fmts?): Date; formatIntlDate(date, fmt?, utc?): string
|
|
1473
1486
|
calcAge(birth, ref?): number
|
|
1474
1487
|
|
|
@@ -1504,7 +1517,7 @@ const makeUniqueComponentId // () => string, "x4-<n>" (exported as a value)
|
|
|
1504
1517
|
|
|
1505
1518
|
```
|
|
1506
1519
|
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)
|
|
1520
|
+
w week number (ISO 8601) W week (2 digits) m month (no zero) M month (2 digits)
|
|
1508
1521
|
o short month O long month y/Y year h 24h hour
|
|
1509
1522
|
H hour (2 digits) i minutes I minutes (2 dig) s seconds
|
|
1510
1523
|
S seconds (2 dig) a am/pm l ms L ms (3 digits)
|
|
@@ -1526,6 +1539,9 @@ class SvgBuilder extends SvgGroup {
|
|
|
1526
1539
|
}
|
|
1527
1540
|
// Items: SvgGroup, SvgPath (moveTo/lineTo/curveTo/arc/closePath), SvgText (font/fontSize/textAlign),
|
|
1528
1541
|
// SvgShape, SvgIcon, SvgGradient (addStop)
|
|
1542
|
+
// arc(x, y, r, start, end): angles in degrees, 0 is the top, 90 the right; it starts a new subpath
|
|
1543
|
+
// addStop(offset, color): a number is a percentage (20 → "20%"), "50%" as it is
|
|
1544
|
+
// getNumAttr(name): 0 when not set; SvgIcon(svg): its svg needs xmlns="http://www.w3.org/2000/svg"
|
|
1529
1545
|
```
|
|
1530
1546
|
|
|
1531
1547
|
---
|
|
@@ -1793,6 +1809,8 @@ Optional keys: `external`, `define`, `esbuild`, `dev` (`host`, `port`, `https`,
|
|
|
1793
1809
|
}
|
|
1794
1810
|
```
|
|
1795
1811
|
|
|
1812
|
+
`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.
|
|
1813
|
+
|
|
1796
1814
|
### `src/index.html`
|
|
1797
1815
|
|
|
1798
1816
|
```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
|
}
|
|
@@ -30,6 +30,9 @@ export interface ColorPickerProps extends ComponentProps {
|
|
|
30
30
|
|
|
31
31
|
/** Adds a "no color" swatch: picking it fires `change` with a null color. */
|
|
32
32
|
nullable?: boolean;
|
|
33
|
+
|
|
34
|
+
/** false: the colors are opaque, there is no alpha slider (default true). */
|
|
35
|
+
alpha?: boolean;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
interface HueChangeEvent extends ComponentEvent {
|
|
@@ -83,13 +86,17 @@ export class Saturation extends Box<BoxProps,CommonEvents> {
|
|
|
83
86
|
created: () => this.updateThumbMarker( ),
|
|
84
87
|
} );
|
|
85
88
|
|
|
86
|
-
|
|
89
|
+
// the color at start: its hue, and the thumb on its saturation and value
|
|
90
|
+
this.setColor( init );
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
mousedown( ev: PointerEvent ) {
|
|
90
94
|
this.mdown = true;
|
|
91
95
|
this.irect = this.getBoundingRect( );
|
|
92
96
|
this.setCapture( ev.pointerId );
|
|
97
|
+
|
|
98
|
+
// a click takes the point too, without a move
|
|
99
|
+
this.mousemove( ev );
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
mousemove( ev: PointerEvent ) {
|
|
@@ -213,6 +220,9 @@ class HueSlider extends Box<BoxProps,CommonEvents> {
|
|
|
213
220
|
this.mdown = true;
|
|
214
221
|
this.irect = this.getBoundingRect( );
|
|
215
222
|
this.setCapture( ev.pointerId );
|
|
223
|
+
|
|
224
|
+
// a click takes the point too, without a move
|
|
225
|
+
this.mousemove( ev );
|
|
216
226
|
}
|
|
217
227
|
|
|
218
228
|
mousemove( ev: PointerEvent ) {
|
|
@@ -286,14 +296,17 @@ class AlphaSlider extends Box<BoxProps,CommonEvents> {
|
|
|
286
296
|
pointerup: ( e ) => this._on_mouseup( e ),
|
|
287
297
|
} );
|
|
288
298
|
|
|
289
|
-
|
|
290
|
-
this.
|
|
299
|
+
// the color at start: its gradient, and the thumb on its alpha
|
|
300
|
+
this.setColor( init );
|
|
291
301
|
}
|
|
292
302
|
|
|
293
303
|
_on_mousedown( ev: PointerEvent ) {
|
|
294
304
|
this.mdown = true;
|
|
295
305
|
this.irect = this.getBoundingRect( );
|
|
296
306
|
this.setCapture( ev.pointerId );
|
|
307
|
+
|
|
308
|
+
// a click takes the point too, without a move
|
|
309
|
+
this._on_mousemove( ev );
|
|
297
310
|
}
|
|
298
311
|
|
|
299
312
|
_on_mousemove( ev: PointerEvent ) {
|
|
@@ -370,7 +383,10 @@ interface ColorPickerChangeEvents extends ComponentEvents {
|
|
|
370
383
|
}
|
|
371
384
|
|
|
372
385
|
/**
|
|
373
|
-
*
|
|
386
|
+
* @cssvar
|
|
387
|
+
* ```
|
|
388
|
+
* --colorpicker-none
|
|
389
|
+
* ```
|
|
374
390
|
*/
|
|
375
391
|
|
|
376
392
|
@class_ns( "x4" )
|
|
@@ -395,6 +411,11 @@ export class ColorPicker extends VBox<ColorPickerProps,ColorPickerChangeEvents>
|
|
|
395
411
|
this._base = new Color( props.color );
|
|
396
412
|
}
|
|
397
413
|
|
|
414
|
+
// no alpha: the color is opaque
|
|
415
|
+
if( props.alpha===false ) {
|
|
416
|
+
this._base.setAlpha( 1 );
|
|
417
|
+
}
|
|
418
|
+
|
|
398
419
|
const hsv = this._hsv = this._base.toHsv( );
|
|
399
420
|
|
|
400
421
|
this.setAttribute( "tabindex", 0 );
|
|
@@ -417,6 +438,8 @@ export class ColorPicker extends VBox<ColorPickerProps,ColorPickerChangeEvents>
|
|
|
417
438
|
this._swatches = new HBox( { cls: "swatches" } ),
|
|
418
439
|
]);
|
|
419
440
|
|
|
441
|
+
this._alpha.show( props.alpha!==false );
|
|
442
|
+
|
|
420
443
|
this._sat.on( "sat_change", ( ev ) => {
|
|
421
444
|
this._hsv.saturation = ev.saturation;
|
|
422
445
|
this._hsv.value = ev.value;
|
|
@@ -460,14 +483,21 @@ export class ColorPicker extends VBox<ColorPickerProps,ColorPickerChangeEvents>
|
|
|
460
483
|
private _changed( ) {
|
|
461
484
|
const hsv = this._hsv;
|
|
462
485
|
|
|
463
|
-
|
|
486
|
+
// no alpha: the hidden slider can still be moved by the keys, the color stays opaque
|
|
487
|
+
let alpha = hsv.alpha;
|
|
488
|
+
if( this.props.alpha===false ) {
|
|
489
|
+
alpha = 1;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
this._base.setHsv( hsv.hue, hsv.saturation, hsv.value, alpha );
|
|
464
493
|
this._refresh( );
|
|
465
494
|
this._notify( );
|
|
466
495
|
}
|
|
467
496
|
|
|
468
497
|
private _refresh( ) {
|
|
469
|
-
|
|
470
|
-
this._swatch.
|
|
498
|
+
// with its alpha: the swatch is drawn over checkers
|
|
499
|
+
this._swatch.setStyleValue( "backgroundColor", this._base.toRgbString( true ) );
|
|
500
|
+
this._swatch.setAttribute( "tooltip", this._base.toRgbString( true ) );
|
|
471
501
|
}
|
|
472
502
|
|
|
473
503
|
private _notify( ) {
|
|
@@ -499,8 +529,13 @@ export class ColorPicker extends VBox<ColorPickerProps,ColorPickerChangeEvents>
|
|
|
499
529
|
setColor( color: Color | string ) {
|
|
500
530
|
const rgb = ( color instanceof Color ? color : new Color( color ) ).toRgb( );
|
|
501
531
|
|
|
532
|
+
let alpha = rgb.alpha;
|
|
533
|
+
if( this.props.alpha===false ) {
|
|
534
|
+
alpha = 1;
|
|
535
|
+
}
|
|
536
|
+
|
|
502
537
|
// the color itself is kept as given: going through hsv could shift it
|
|
503
|
-
this._base.setRgb( rgb.red, rgb.green, rgb.blue,
|
|
538
|
+
this._base.setRgb( rgb.red, rgb.green, rgb.blue, alpha );
|
|
504
539
|
|
|
505
540
|
const hsv = this._base.toHsv( );
|
|
506
541
|
this._hsv.hue = hsv.hue;
|
|
@@ -538,14 +573,17 @@ export class ColorPicker extends VBox<ColorPickerProps,ColorPickerChangeEvents>
|
|
|
538
573
|
continue;
|
|
539
574
|
}
|
|
540
575
|
|
|
541
|
-
|
|
576
|
+
const item = new Component( {
|
|
542
577
|
cls: "item",
|
|
543
578
|
tooltip: color.toHexString( ),
|
|
544
|
-
style: { backgroundColor: color.toRgbString( ) },
|
|
545
579
|
dom_events: {
|
|
546
580
|
click: ( ) => this._pick( color ),
|
|
547
581
|
},
|
|
548
|
-
} )
|
|
582
|
+
} );
|
|
583
|
+
|
|
584
|
+
// as it is taken: opaque without alpha; the stylesheet draws it over checkers
|
|
585
|
+
item.setStyleVariable( "--swatch-color", color.toRgbString( this.props.alpha!==false ) );
|
|
586
|
+
items.push( item );
|
|
549
587
|
}
|
|
550
588
|
|
|
551
589
|
this._swatches.setContent( items );
|
|
@@ -593,6 +631,14 @@ export class ColorPicker extends VBox<ColorPickerProps,ColorPickerChangeEvents>
|
|
|
593
631
|
}
|
|
594
632
|
break;
|
|
595
633
|
}
|
|
634
|
+
|
|
635
|
+
default: {
|
|
636
|
+
// not for the picker: Tab, Escape... go on
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
596
639
|
}
|
|
640
|
+
|
|
641
|
+
// the arrows move the picker, not the page
|
|
642
|
+
ev.preventDefault( );
|
|
597
643
|
}
|
|
598
644
|
}
|