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 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" → CSS var | "data:image/svg+xml,<svg..." → inline
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(); pause(); mute(set: boolean)
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?; "change": EvColorChange, also fired once when built
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` and `leave` are found by a timer: they may come up to 300 ms late, and a `leave` also follows a `drop`. Between them the target has the class `drop-over`.
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; isInvalid(): boolean
1449
- toHexString(): string; toRgbString(withAlpha?): string; toNumber(): number
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; date_calc_weeknum(date): number
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x4js",
3
- "version": "2.3.20",
3
+ "version": "2.3.21",
4
4
  "type": "module",
5
5
  "main": "src/x4.ts",
6
6
  "module": "src/x4.ts",
@@ -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.setAttribute( "grid-template-areas", t.map( x => '"' + x + '"' ).join(" ") );
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( s.values[i] ) ? s.values[i] : 0;
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
- const every = xaxis.step>0 ? Math.round( xaxis.step ) : Math.max( 1, Math.ceil( ( maxw+8 ) / ( xstep || plot.width ) ) );
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
- // props.nullable, no color: the checkerboard that stands for what is transparent
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: repeating-conic-gradient( #c8c8c8 0% 25%, #ffffff 0% 50% ) 0 0 / 6px 6px;
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
- return this.props.format=="hex" ? color.toHexString( ) : color.toRgbString( false );
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
- this._swatch.setStyleValue( "backgroundColor", color ? color.toRgbString(false) : null );
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: the checkerboard that stands for what is transparent
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: repeating-conic-gradient( #c8c8c8 0% 25%, #ffffff 0% 50% ) 0 0 / 6px 6px;
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
- this.updateBaseColor( init );
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
- this.updateAlpha( );
290
- this.updateBaseColor( init );
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
- this._base.setHsv( hsv.hue, hsv.saturation, hsv.value, hsv.alpha );
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
- this._swatch.setStyleValue( "backgroundColor", this._base.toRgbString() );
470
- this._swatch.setAttribute( "tooltip", this._base.toRgbString() );
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, rgb.alpha );
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
- items.push( new Component( {
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
  }