x4js 2.3.16 → 2.3.17
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 +56 -14
- package/package.json +1 -1
- package/src/components/colorinput/colorinput.module.scss +2 -2
- package/src/components/colorpicker/colorpicker.module.scss +2 -2
- package/src/components/combobox/combobox.ts +5 -1
- package/src/components/dockingview/dockingview.module.scss +21 -3
- package/src/components/dockingview/dockingview.ts +192 -55
- package/src/components/dockingview/docklayout.ts +125 -36
- package/src/components/gauge/gauge.ts +4 -0
- package/src/components/input/input.ts +6 -1
- package/src/components/notification/notification.module.scss +8 -3
- package/src/components/notification/notification.ts +3 -0
- package/src/components/progress/progress.ts +7 -3
- package/src/components/switch/switch.ts +2 -1
package/aicontext.md
CHANGED
|
@@ -265,7 +265,7 @@ interface EvDblClick extends ComponentEvent {}
|
|
|
265
265
|
interface EvBtnClick extends CoreEvent { button: string }
|
|
266
266
|
|
|
267
267
|
// Spacers
|
|
268
|
-
class Flex extends Component // CSS "x4flex"
|
|
268
|
+
class Flex extends Component // new Flex(), no argument; CSS "x4flex"
|
|
269
269
|
class Space extends Component // new Space(width?: number|string, cls?: string)
|
|
270
270
|
```
|
|
271
271
|
|
|
@@ -403,12 +403,14 @@ interface DockPanel {
|
|
|
403
403
|
kind?: 'tool' | 'content' // default 'tool'
|
|
404
404
|
closable?: boolean // for this panel, instead of the view's
|
|
405
405
|
floating?: boolean // idem; a content never floats
|
|
406
|
+
sides?: DockSide[] // a tool: the bands it may be docked in, instead of the view's
|
|
406
407
|
}
|
|
407
408
|
interface DockingViewProps extends ComponentProps {
|
|
408
409
|
panels: DockPanel[]
|
|
409
410
|
layout: DockLayout // where the panels are at first
|
|
410
411
|
floating?: boolean // tools can float above the rest
|
|
411
412
|
closable?: boolean // tools have a button that hides them
|
|
413
|
+
sides?: DockSide[] // the bands tools may be docked in; left and right when missing
|
|
412
414
|
persist?: string // a name: the layout is remembered under it (application storage)
|
|
413
415
|
layoutChange?: EventCallback<ComponentEvent>
|
|
414
416
|
}
|
|
@@ -421,12 +423,14 @@ class DockingView extends Component {
|
|
|
421
423
|
}
|
|
422
424
|
|
|
423
425
|
interface DockLayout {
|
|
424
|
-
left: DockBand; right: DockBand // the tools
|
|
426
|
+
left: DockBand; right: DockBand // the tools, on the sides
|
|
427
|
+
top?: DockBand; bottom?: DockBand // the tools above and under the contents, between left and right
|
|
425
428
|
content: DockNode // the contents; null when there is none
|
|
426
429
|
floats: DockFloat[] // floating tools: { panel, x, y, w, h }
|
|
427
430
|
hidden: string[] // ids of the closed panels
|
|
428
431
|
}
|
|
429
|
-
interface DockBand { size: number; root: DockNode } // size: width in px; root: null when empty
|
|
432
|
+
interface DockBand { size: number; root: DockNode } // size: width in px (height for top/bottom); root: null when empty
|
|
433
|
+
type DockSide = 'left' | 'right' | 'top' | 'bottom'
|
|
430
434
|
type DockNode = DockStack | DockSplit
|
|
431
435
|
interface DockStack { type: 'stack'; size: number; panels: string[]; active: string } // one tab per panel
|
|
432
436
|
interface DockSplit { type: 'split'; dir: 'row' | 'column'; size: number; children: DockNode[] }
|
|
@@ -455,7 +459,8 @@ const view = new DockingView({
|
|
|
455
459
|
```
|
|
456
460
|
|
|
457
461
|
- A closed panel is hidden, not removed: `showPanel(id)` brings it back where it was. Offer it in a menu.
|
|
458
|
-
- Tools only go in
|
|
462
|
+
- Tools only go in the bands or in `floats`; contents only in `content`. A panel missing from the layout is added where its kind goes.
|
|
463
|
+
- A tool goes in `left` or `right` only, unless `sides` says otherwise (on the view, or on one panel): `sides: ['bottom']` for what is wide and low (a log, a list of results). It is never offered a place in a band it may not be in.
|
|
459
464
|
- A content alone has no title bar; with several contents, each stack has tabs.
|
|
460
465
|
- What a panel shows is built once (`content`) and moved as the layout changes: never rebuild it.
|
|
461
466
|
- CSS variables: `--dock-line`, `--dock-accent`, `--dock-title-background`, `--dock-title-background-hover`, `--dock-title-color`, `--dock-title-color-active`, `--dock-float-background`, `--dock-float-shadow`.
|
|
@@ -535,14 +540,15 @@ class Image extends Component {
|
|
|
535
540
|
### `Progress`
|
|
536
541
|
|
|
537
542
|
```ts
|
|
538
|
-
interface ProgressProps extends ComponentProps { /*
|
|
543
|
+
interface ProgressProps extends ComponentProps { value: number; min?: number /* 0 */; max?: number /* 100 */ }
|
|
544
|
+
// the bar is where value is between min (0%) and max (100%)
|
|
539
545
|
class Progress extends Component { setValue(value: number): void }
|
|
540
546
|
```
|
|
541
547
|
|
|
542
548
|
### `Gauge`
|
|
543
549
|
|
|
544
550
|
```ts
|
|
545
|
-
interface GaugeProps extends ComponentProps { min?: number
|
|
551
|
+
interface GaugeProps extends ComponentProps { min?: number /* 0 */; max?: number /* 100 */; colors?: string[] }
|
|
546
552
|
class Gauge extends Component {
|
|
547
553
|
setRange(min: number, max: number): void
|
|
548
554
|
setPos(pos: number): void
|
|
@@ -616,6 +622,11 @@ class TextEdit extends HBox {
|
|
|
616
622
|
### `TextArea`
|
|
617
623
|
|
|
618
624
|
```ts
|
|
625
|
+
interface TextAreaProps extends BaseProps { // BaseProps: see Input (name, readonly, placeholder...)
|
|
626
|
+
label?: string; value?: string
|
|
627
|
+
resize?: boolean | "none" | "vertical" | "horizontal" | "both"
|
|
628
|
+
trim?: boolean // getText() trims, unless false
|
|
629
|
+
}
|
|
619
630
|
class TextArea extends VBox { getText(): string; setText(text: string): void }
|
|
620
631
|
```
|
|
621
632
|
|
|
@@ -636,7 +647,13 @@ class Checkbox extends Component {
|
|
|
636
647
|
### `Radio`
|
|
637
648
|
|
|
638
649
|
```ts
|
|
639
|
-
interface RadioProps extends ComponentProps {
|
|
650
|
+
interface RadioProps extends ComponentProps {
|
|
651
|
+
label: string; icon?: string
|
|
652
|
+
name: string // required: the radios that have the same name are one group
|
|
653
|
+
value: number | string // required
|
|
654
|
+
checked?: boolean
|
|
655
|
+
change?: EventCallback<EvChange>
|
|
656
|
+
}
|
|
640
657
|
class Radio extends Component {
|
|
641
658
|
getValue(): string; setCheck(ck: boolean): void
|
|
642
659
|
setLabel(text: string): void
|
|
@@ -647,14 +664,19 @@ class Radio extends Component {
|
|
|
647
664
|
### `Switch`
|
|
648
665
|
|
|
649
666
|
```ts
|
|
650
|
-
interface SwitchProps extends ComponentProps {
|
|
667
|
+
interface SwitchProps extends ComponentProps { label: string; checked?: boolean; name?: string }
|
|
668
|
+
// with a name, a Form reads and writes it as a boolean
|
|
651
669
|
class Switch extends HBox {}
|
|
652
670
|
```
|
|
653
671
|
|
|
654
672
|
### `Slider`
|
|
655
673
|
|
|
656
674
|
```ts
|
|
657
|
-
interface SliderProps extends ComponentProps {
|
|
675
|
+
interface SliderProps extends ComponentProps {
|
|
676
|
+
value: number; min: number; max: number // required
|
|
677
|
+
step?: number; vertical?: boolean
|
|
678
|
+
change?: EventCallback<EvChange>
|
|
679
|
+
}
|
|
658
680
|
class Slider extends Component {
|
|
659
681
|
setMin(min: number): void; setMax(max: number): void; setValue(v: number): void
|
|
660
682
|
}
|
|
@@ -665,17 +687,29 @@ class Slider extends Component {
|
|
|
665
687
|
```ts
|
|
666
688
|
interface SelectProps extends ComponentProps {
|
|
667
689
|
items: ListItem[] // required
|
|
668
|
-
value
|
|
669
|
-
multiple?: boolean; name?: string
|
|
690
|
+
value?: ListboxID // the id of the item that is selected
|
|
691
|
+
multiple?: boolean; required?: boolean; name?: string
|
|
670
692
|
change?: EventCallback<EvChange>; focus?: EventCallback<EvFocus>
|
|
671
693
|
}
|
|
672
|
-
class Select extends Component {
|
|
694
|
+
class Select extends Component {
|
|
695
|
+
getValue(): ListboxID; setValue(value: ListboxID): void
|
|
696
|
+
getSelValues(): ListboxID[]; setSelValues(values: ListboxID[]): void // multiple
|
|
697
|
+
setItems(items: ListItem[]): void
|
|
698
|
+
}
|
|
673
699
|
```
|
|
674
700
|
|
|
675
701
|
### `Combobox` — input + dropdown list
|
|
676
702
|
|
|
677
703
|
```ts
|
|
678
|
-
interface ComboboxProps extends ComponentProps {
|
|
704
|
+
interface ComboboxProps extends ComponentProps {
|
|
705
|
+
items: ListItem[] // required
|
|
706
|
+
value?: ListboxID // the id of the item that is selected
|
|
707
|
+
label?: string; labelWidth?: number | string
|
|
708
|
+
readonly?: boolean // true by default: the user picks, and cannot type. false to let him type
|
|
709
|
+
renderer?: (item: ListItem) => Component // draws an item of the list (the field still shows item.text)
|
|
710
|
+
required?: boolean; name?: string
|
|
711
|
+
selectionChange?: EventCallback<EvSelectionChange>
|
|
712
|
+
}
|
|
679
713
|
class Combobox extends Component {
|
|
680
714
|
getValue(): string; setValue(value: string): void
|
|
681
715
|
getInput(): Input
|
|
@@ -1025,7 +1059,14 @@ class ProgressionBox extends Dialog {
|
|
|
1025
1059
|
### `Notification`
|
|
1026
1060
|
|
|
1027
1061
|
```ts
|
|
1028
|
-
|
|
1062
|
+
interface NotificationProps extends ComponentProps {
|
|
1063
|
+
title: string; text: string | UnsafeHtml
|
|
1064
|
+
mode?: "success" | "danger"
|
|
1065
|
+
loading?: boolean // a turning icon
|
|
1066
|
+
iconId?: string
|
|
1067
|
+
closable?: boolean // a button that closes it
|
|
1068
|
+
}
|
|
1069
|
+
class Notification extends Popup { display(time_in_s?: number): void } // 0: until it is closed
|
|
1029
1070
|
```
|
|
1030
1071
|
|
|
1031
1072
|
---
|
|
@@ -1614,6 +1655,7 @@ Optional keys: `external`, `define`, `esbuild`, `dev` (`host`, `port`, `https`,
|
|
|
1614
1655
|
- `Component` stores its instance on its DOM node. Use `componentFromDOM(el)` to retrieve a component from an `HTMLElement`.
|
|
1615
1656
|
- Numbers passed to `setStyle`/`setStyleValue` are auto-suffixed with `"px"` except for unitless properties (see the `unitless` set: `zIndex`, `opacity`, `flexGrow`...).
|
|
1616
1657
|
- `show(false)` / `hide()` toggle the `"x4hidden"` CSS class, not `display:none`.
|
|
1658
|
+
- x4 sets `user-select: none` on every element (`*`): text that must be selectable needs `user-select: text` on it and on its descendants.
|
|
1617
1659
|
- `enable(false)` / `disable()` propagate the `disabled` state to child `<input>` and `<button>` elements.
|
|
1618
1660
|
- `Application` is a strict singleton — one instance per page (an assertion fails otherwise).
|
|
1619
1661
|
- `Popup.show()` routes to `displayCenter()`. To position, call `displayAt()` or `displayNear()` instead.
|
package/package.json
CHANGED
|
@@ -50,10 +50,10 @@
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
// props.nullable, no color:
|
|
53
|
+
// props.nullable, no color: the checkerboard that stands for what is transparent
|
|
54
54
|
&.none {
|
|
55
55
|
border: 1px solid var( --border );
|
|
56
|
-
background:
|
|
56
|
+
background: repeating-conic-gradient( #c8c8c8 0% 25%, #ffffff 0% 50% ) 0 0 / 6px 6px;
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -147,9 +147,9 @@
|
|
|
147
147
|
border-color: var( --border-focus );
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
// no color:
|
|
150
|
+
// no color: the checkerboard that stands for what is transparent
|
|
151
151
|
&.none {
|
|
152
|
-
background:
|
|
152
|
+
background: repeating-conic-gradient( #c8c8c8 0% 25%, #ffffff 0% 50% ) 0 0 / 6px 6px;
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
}
|
|
@@ -87,6 +87,10 @@ interface ComboboxProps extends Omit<ComponentProps,"content"> {
|
|
|
87
87
|
readonly?: boolean;
|
|
88
88
|
required?: boolean;
|
|
89
89
|
items: ListItem[];
|
|
90
|
+
|
|
91
|
+
/** draws an item of the list, instead of its text. The field still shows the text of the item that is chosen. */
|
|
92
|
+
renderer?: ( item: ListItem ) => Component;
|
|
93
|
+
|
|
90
94
|
selectionChange?: EventCallback<EvSelectionChange>,
|
|
91
95
|
}
|
|
92
96
|
|
|
@@ -139,7 +143,7 @@ export class Combobox extends Component<ComboboxProps,ComboboxEvents> {
|
|
|
139
143
|
this.setAttribute( "required", true );
|
|
140
144
|
}
|
|
141
145
|
|
|
142
|
-
this._popup = new DropdownList( { items: props.items } );
|
|
146
|
+
this._popup = new DropdownList( { items: props.items, renderer: props.renderer } );
|
|
143
147
|
const list = this._popup.getList( );
|
|
144
148
|
|
|
145
149
|
const _select = ( sel: ListboxID ) => {
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
// even in an HBox, which centers its children
|
|
35
35
|
align-self: stretch;
|
|
36
36
|
|
|
37
|
-
// The boxes of the layout: the band of the left,
|
|
38
|
-
//
|
|
39
|
-
//
|
|
37
|
+
// The boxes of the layout: the band of the left, what is between the
|
|
38
|
+
// two, the band of the right. The floating tools are placed from its
|
|
39
|
+
// top-left corner, which is the one of the box.
|
|
40
40
|
> .dock-frame {
|
|
41
41
|
position: relative;
|
|
42
42
|
display: flex;
|
|
@@ -45,6 +45,17 @@
|
|
|
45
45
|
min-height: 0;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// Between the bands of the left and of the right, one above the other:
|
|
49
|
+
// the band of the top, the room of the contents, the band of the bottom.
|
|
50
|
+
.dock-middle {
|
|
51
|
+
display: flex;
|
|
52
|
+
flex-direction: column;
|
|
53
|
+
flex: 1 1 0;
|
|
54
|
+
min-width: 0;
|
|
55
|
+
min-height: 0;
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
}
|
|
58
|
+
|
|
48
59
|
// The tools of one side. Its width is given by the layout; it only
|
|
49
60
|
// gets narrower when the box has no room left.
|
|
50
61
|
.dock-band {
|
|
@@ -58,6 +69,13 @@
|
|
|
58
69
|
> .dock-stack {
|
|
59
70
|
flex: 1 1 0;
|
|
60
71
|
}
|
|
72
|
+
|
|
73
|
+
// the bands that lie flat: it is their height that the layout gives
|
|
74
|
+
&.top,
|
|
75
|
+
&.bottom {
|
|
76
|
+
min-width: 0;
|
|
77
|
+
min-height: 48px;
|
|
78
|
+
}
|
|
61
79
|
}
|
|
62
80
|
|
|
63
81
|
// the room of the contents: what the bands leave
|
|
@@ -19,13 +19,13 @@ import { Component, ComponentEvent, ComponentEvents, ComponentProps } from '../.
|
|
|
19
19
|
import { EventCallback } from '../../core/core_events';
|
|
20
20
|
import { CSizer } from '../sizers/sizer';
|
|
21
21
|
|
|
22
|
-
import { bandOf, dockActivate, dockBand, dockFloat, dockMove, dockRestore, dockShow, dropZone, floatOf, isShown, keepInside, sharesOf, shownPanel, sideRect, stackOf, stacksOf, visiblePanels } from './docklayout';
|
|
23
|
-
import { DockFloat, DockKinds, DockLayout, DockNode, DockRect, DockStack } from './docklayout';
|
|
22
|
+
import { bandOf, dockActivate, dockBand, dockFloat, dockMove, dockRestore, dockShow, dockSides, dropZone, floatOf, isFlat, isShown, keepInside, sharesOf, shownPanel, sideRect, stackOf, stacksOf, visiblePanels } from './docklayout';
|
|
23
|
+
import { DockFloat, DockKinds, DockLayout, DockNode, DockRect, DockSide, DockStack } from './docklayout';
|
|
24
24
|
|
|
25
25
|
import "./dockingview.module.scss";
|
|
26
26
|
|
|
27
27
|
// the layout is given and read by the host: its types go with the component
|
|
28
|
-
export type { DockLayout, DockBand, DockNode, DockSplit, DockStack, DockFloat } from './docklayout';
|
|
28
|
+
export type { DockLayout, DockBand, DockNode, DockSplit, DockStack, DockFloat, DockSide } from './docklayout';
|
|
29
29
|
|
|
30
30
|
/** under this distance, in pixels, a press on a title is a click, not a drag */
|
|
31
31
|
const DRAG_SLOP = 4;
|
|
@@ -39,8 +39,12 @@ const MIN_SIZE = 48;
|
|
|
39
39
|
/** what a sizer always leaves to the contents, between the two bands */
|
|
40
40
|
const MIN_AREA = 80;
|
|
41
41
|
|
|
42
|
-
/** width of a band whose layout gives none */
|
|
42
|
+
/** width of a band whose layout gives none; height, for the band of the top or of the bottom */
|
|
43
43
|
const BAND_SIZE = 200;
|
|
44
|
+
const BAND_HEIGHT = 160;
|
|
45
|
+
|
|
46
|
+
/** the bands, in the order they are looked at */
|
|
47
|
+
const SIDES: DockSide[] = ["left", "right", "top", "bottom"];
|
|
44
48
|
|
|
45
49
|
/** smallest size of a panel that starts to float */
|
|
46
50
|
const FLOAT_MIN_W = 160;
|
|
@@ -68,6 +72,9 @@ export interface DockPanel {
|
|
|
68
72
|
|
|
69
73
|
/** a tool: instead of what the box says. A content never floats. */
|
|
70
74
|
floating?: boolean;
|
|
75
|
+
|
|
76
|
+
/** a tool: the bands it may be docked in, instead of what the box says */
|
|
77
|
+
sides?: DockSide[];
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
export interface DockingViewProps extends ComponentProps {
|
|
@@ -82,6 +89,14 @@ export interface DockingViewProps extends ComponentProps {
|
|
|
82
89
|
/** tools have a button that hides them (see showPanel) */
|
|
83
90
|
closable?: boolean;
|
|
84
91
|
|
|
92
|
+
/**
|
|
93
|
+
* The bands tools may be docked in: "left", "right", "top", "bottom".
|
|
94
|
+
* The left and the right ones when missing. A tool is never offered a
|
|
95
|
+
* place in a band it may not be in, neither beside one of its tools nor
|
|
96
|
+
* as one of their tabs.
|
|
97
|
+
*/
|
|
98
|
+
sides?: DockSide[];
|
|
99
|
+
|
|
85
100
|
/**
|
|
86
101
|
* A name: the layout is then remembered under it, in the storage of
|
|
87
102
|
* the application, and found again the next time.
|
|
@@ -142,6 +157,11 @@ interface Drag {
|
|
|
142
157
|
* dropped anywhere else floats above the rest. With `closable`, its title
|
|
143
158
|
* has a button that hides it; showPanel() brings it back.
|
|
144
159
|
*
|
|
160
|
+
* With `sides`, a tool may also be docked above or under the contents, in
|
|
161
|
+
* a band that lies between those of the left and of the right: for what is
|
|
162
|
+
* wide and low (a log, a list of results, a timeline). `sides` says which
|
|
163
|
+
* bands a tool may be in, for all the tools or for one of them.
|
|
164
|
+
*
|
|
145
165
|
* A content is what is worked on (a document, as the editors of an IDE).
|
|
146
166
|
* Contents share the room between the bands: a title dragged to a side of
|
|
147
167
|
* another content is put beside it, to its middle it becomes one of its
|
|
@@ -175,7 +195,7 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
|
|
|
175
195
|
|
|
176
196
|
// the boxes of the layout as it is shown now
|
|
177
197
|
private nodes = new Map<DockNode, Component>( );
|
|
178
|
-
private bands = new Map<
|
|
198
|
+
private bands = new Map<DockSide, Component>( );
|
|
179
199
|
private floats = new Map<string, Component>( );
|
|
180
200
|
private area: Component; // the room of the contents, between the bands
|
|
181
201
|
|
|
@@ -277,7 +297,23 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
|
|
|
277
297
|
|
|
278
298
|
private kinds( ): DockKinds {
|
|
279
299
|
const ids = this.props.panels.map( panel => panel.id );
|
|
280
|
-
|
|
300
|
+
const tools = ids.filter( id => this.isTool( id ) );
|
|
301
|
+
|
|
302
|
+
// what a tool says for itself, otherwise what the box says for all
|
|
303
|
+
const sides: Record<string, DockSide[]> = {};
|
|
304
|
+
for( const id of tools ) {
|
|
305
|
+
sides[id] = this.panel( id ).sides ?? this.props.sides;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return { tools, contents: ids.filter( id => !this.isTool( id ) ), sides };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* true when a tool may be docked in a band
|
|
313
|
+
*/
|
|
314
|
+
|
|
315
|
+
private mayDock( id: string, side: DockSide ): boolean {
|
|
316
|
+
return dockSides( this.kinds( ), id ).includes( side );
|
|
281
317
|
}
|
|
282
318
|
|
|
283
319
|
private mayFloat( id: string ): boolean {
|
|
@@ -316,8 +352,10 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
|
|
|
316
352
|
// :: BUILDING ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
317
353
|
|
|
318
354
|
/**
|
|
319
|
-
* Make the boxes of the layout again: the band of the left,
|
|
320
|
-
*
|
|
355
|
+
* Make the boxes of the layout again: the band of the left, then,
|
|
356
|
+
* one above the other, the band of the top, the room of the contents
|
|
357
|
+
* and the band of the bottom, then the band of the right; and the
|
|
358
|
+
* floating tools.
|
|
321
359
|
*
|
|
322
360
|
* What the panels show is not built again: their pages are moved. They
|
|
323
361
|
* go straight from a place in the document to another one, the park
|
|
@@ -346,9 +384,19 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
|
|
|
346
384
|
content: content && isShown( content, hidden ) ? this.makeNode( content, titled ) : null,
|
|
347
385
|
} );
|
|
348
386
|
|
|
387
|
+
// between the bands of the sides, which take the whole height
|
|
388
|
+
const middle = new Component( {
|
|
389
|
+
cls: "dock-middle",
|
|
390
|
+
content: [
|
|
391
|
+
...this.makeBand( "top" ),
|
|
392
|
+
this.area,
|
|
393
|
+
...this.makeBand( "bottom" ),
|
|
394
|
+
],
|
|
395
|
+
} );
|
|
396
|
+
|
|
349
397
|
this.frame.setContent( [
|
|
350
398
|
...this.makeBand( "left" ),
|
|
351
|
-
|
|
399
|
+
middle,
|
|
352
400
|
...this.makeBand( "right" ),
|
|
353
401
|
...floats.filter( float => !hidden.includes( float.panel ) ).map( float => this.makeFloat( float ) ),
|
|
354
402
|
] );
|
|
@@ -362,39 +410,58 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
|
|
|
362
410
|
}
|
|
363
411
|
|
|
364
412
|
/**
|
|
365
|
-
* A band and the sizer that changes its width,
|
|
366
|
-
*
|
|
413
|
+
* A band and the sizer that changes its width (its height, for the
|
|
414
|
+
* band of the top or of the bottom), in the order they are shown;
|
|
415
|
+
* nothing when none of its tools is shown.
|
|
367
416
|
*/
|
|
368
417
|
|
|
369
|
-
private makeBand( side:
|
|
418
|
+
private makeBand( side: DockSide ): Component[] {
|
|
370
419
|
const band = this.layout[side];
|
|
371
420
|
|
|
372
|
-
if( !band
|
|
421
|
+
if( !band?.root || !isShown( band.root, this.layout.hidden ) ) {
|
|
373
422
|
return [];
|
|
374
423
|
}
|
|
375
424
|
|
|
425
|
+
const flat = isFlat( side );
|
|
426
|
+
|
|
376
427
|
const box = new Component( {
|
|
377
428
|
cls: `dock-band ${side}`,
|
|
378
|
-
width: band.size || BAND_SIZE,
|
|
379
429
|
content: this.makeNode( band.root, true ),
|
|
380
430
|
} );
|
|
381
431
|
|
|
432
|
+
if( flat ) {
|
|
433
|
+
box.setHeight( band.size || BAND_HEIGHT );
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
box.setWidth( band.size || BAND_SIZE );
|
|
437
|
+
}
|
|
438
|
+
|
|
382
439
|
this.bands.set( side, box );
|
|
383
440
|
|
|
384
|
-
// what the band gets is taken from the contents
|
|
385
|
-
|
|
441
|
+
// what the band gets is taken from the contents: the sizer is on
|
|
442
|
+
// the side of the band that faces them
|
|
443
|
+
const first = side === "left" || side === "top";
|
|
444
|
+
const sign = first ? 1 : -1;
|
|
386
445
|
let size = 0;
|
|
387
446
|
let max = 0;
|
|
388
447
|
|
|
389
|
-
const sizer = this.makeSizer(
|
|
448
|
+
const sizer = this.makeSizer( !flat, ( ) => {
|
|
449
|
+
const room = this.area.getBoundingRect( );
|
|
450
|
+
|
|
390
451
|
size = band.size;
|
|
391
|
-
max = size + Math.max( 0,
|
|
452
|
+
max = size + Math.max( 0, ( flat ? room.height : room.width ) - MIN_AREA );
|
|
392
453
|
}, delta => {
|
|
393
454
|
band.size = Math.round( Math.max( MIN_SIZE, Math.min( size + delta * sign, max ) ) );
|
|
394
|
-
|
|
455
|
+
|
|
456
|
+
if( flat ) {
|
|
457
|
+
box.setHeight( band.size );
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
box.setWidth( band.size );
|
|
461
|
+
}
|
|
395
462
|
} );
|
|
396
463
|
|
|
397
|
-
return
|
|
464
|
+
return first ? [box, sizer] : [sizer, box];
|
|
398
465
|
}
|
|
399
466
|
|
|
400
467
|
private makeNode( node: DockNode, titled: boolean ): Component {
|
|
@@ -588,12 +655,23 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
|
|
|
588
655
|
visit( this.layout.content, null );
|
|
589
656
|
}
|
|
590
657
|
|
|
591
|
-
for( const side of
|
|
658
|
+
for( const side of SIDES ) {
|
|
592
659
|
const band = this.layout[side];
|
|
593
|
-
|
|
660
|
+
if( !band ) {
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// A band that is not shown has no box: its size stays what it
|
|
665
|
+
// was. Otherwise its width, or its height when it lies flat.
|
|
666
|
+
const box = this.bands.get( side );
|
|
667
|
+
|
|
668
|
+
if( box ) {
|
|
669
|
+
const rc = box.getBoundingRect( );
|
|
670
|
+
const size = Math.round( isFlat( side ) ? rc.height : rc.width );
|
|
594
671
|
|
|
595
|
-
|
|
596
|
-
|
|
672
|
+
if( size > 0 ) {
|
|
673
|
+
band.size = size;
|
|
674
|
+
}
|
|
597
675
|
}
|
|
598
676
|
|
|
599
677
|
if( band.root ) {
|
|
@@ -761,8 +839,11 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
|
|
|
761
839
|
*
|
|
762
840
|
* a side of a tool beside it, above or under it
|
|
763
841
|
* the middle of another tool one of its tabs
|
|
764
|
-
*
|
|
842
|
+
* a side of the contents the band of that side, after its tools
|
|
765
843
|
* anywhere else it floats, if it may
|
|
844
|
+
*
|
|
845
|
+
* The first three only for a band the tool may be docked in (`sides`):
|
|
846
|
+
* the left and the right ones, unless the host says otherwise.
|
|
766
847
|
*/
|
|
767
848
|
|
|
768
849
|
private dropTool( x: number, y: number ): Drop {
|
|
@@ -777,8 +858,10 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
|
|
|
777
858
|
|
|
778
859
|
const inside = ( rect: DockRect ) => dropZone( rect, x, y, 1 ) !== null;
|
|
779
860
|
|
|
780
|
-
// the width the tool brings to a band: the one it has now, a third of the box at most
|
|
861
|
+
// the width the tool brings to a band: the one it has now, a third of the box at most;
|
|
862
|
+
// and its height, for a band that lies flat
|
|
781
863
|
const width = Math.round( Math.max( MIN_SIZE, Math.min( ( float?.w ?? page.w ) || BAND_SIZE, within.w / 3 ) ) );
|
|
864
|
+
const height = Math.round( Math.max( MIN_SIZE, Math.min( ( float?.h ?? page.h ) || BAND_HEIGHT, within.h / 3 ) ) );
|
|
782
865
|
|
|
783
866
|
const floating = ( ): Drop => {
|
|
784
867
|
if( !this.mayFloat( panel ) ) {
|
|
@@ -800,44 +883,52 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
|
|
|
800
883
|
return floating( );
|
|
801
884
|
}
|
|
802
885
|
|
|
803
|
-
// a tool of a band
|
|
804
|
-
for( const
|
|
805
|
-
|
|
806
|
-
const rect = box ? this.rectOf( box ) : null;
|
|
807
|
-
const zone = rect ? dropZone( rect, x, y, SIDE_BAND ) : null;
|
|
808
|
-
|
|
809
|
-
if( !zone ) {
|
|
886
|
+
// a tool of a band, when the one that is dragged may be in that band
|
|
887
|
+
for( const side of SIDES ) {
|
|
888
|
+
if( !this.mayDock( panel, side ) ) {
|
|
810
889
|
continue;
|
|
811
890
|
}
|
|
812
891
|
|
|
813
|
-
const
|
|
892
|
+
for( const stack of stacksOf( layout[side]?.root ) ) {
|
|
893
|
+
const box = this.nodes.get( stack );
|
|
894
|
+
const rect = box ? this.rectOf( box ) : null;
|
|
895
|
+
const zone = rect ? dropZone( rect, x, y, SIDE_BAND ) : null;
|
|
814
896
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
}
|
|
897
|
+
if( !zone ) {
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
819
900
|
|
|
820
|
-
|
|
821
|
-
if( stack === own && panels.length === 1 ) {
|
|
822
|
-
return null;
|
|
823
|
-
}
|
|
901
|
+
const panels = visiblePanels( stack, hidden );
|
|
824
902
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
903
|
+
if( zone === "center" ) {
|
|
904
|
+
// the middle of its own stack: it leaves it
|
|
905
|
+
return stack === own ? floating( ) : { rect, apply: ( ) => dockMove( layout, panel, panels[0], "tab" ) };
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// beside the stack it is alone in: it is already there
|
|
909
|
+
if( stack === own && panels.length === 1 ) {
|
|
910
|
+
return null;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Half of the stack is shown. On the left or on the right, the
|
|
914
|
+
// band gets wider instead: the two tools keep their width. In
|
|
915
|
+
// a band that lies flat, it is above and under, and its height.
|
|
916
|
+
const half = ( zone === "left" || zone === "right" ? rect.w : rect.h ) / 2;
|
|
917
|
+
return { rect: sideRect( rect, zone, half ), apply: ( ) => dockMove( layout, panel, panels[0], zone, isFlat( side ) ? height : width ) };
|
|
918
|
+
}
|
|
829
919
|
}
|
|
830
920
|
|
|
831
|
-
//
|
|
921
|
+
// a side of the contents: the band of that side
|
|
832
922
|
const area = this.rectOf( this.area );
|
|
833
|
-
const side =
|
|
923
|
+
const side = this.edgeOf( panel, area, x, y );
|
|
834
924
|
|
|
835
925
|
if( !side ) {
|
|
836
926
|
return floating( );
|
|
837
927
|
}
|
|
838
928
|
|
|
929
|
+
const flat = isFlat( side );
|
|
839
930
|
const band = this.bands.get( side );
|
|
840
|
-
const root = layout[side]
|
|
931
|
+
const root = layout[side]?.root;
|
|
841
932
|
const shown = stacksOf( root ).filter( stack => isShown( stack, hidden ) );
|
|
842
933
|
|
|
843
934
|
// the only tool of that band: it is already there
|
|
@@ -845,14 +936,60 @@ export class DockingView extends Component<DockingViewProps, DockingViewEvents>
|
|
|
845
936
|
return null;
|
|
846
937
|
}
|
|
847
938
|
|
|
848
|
-
// A band that shows nothing is made beside the contents, as
|
|
849
|
-
// the tool is now. Otherwise the tool goes
|
|
850
|
-
// holds: one more row of it, or its lower half
|
|
851
|
-
|
|
939
|
+
// A band that shows nothing is made beside the contents, as big as
|
|
940
|
+
// the tool is now. Otherwise the tool goes after what the band
|
|
941
|
+
// holds: one more row of it, or its lower half; one more column, or
|
|
942
|
+
// its right half, for a band that lies flat.
|
|
943
|
+
const size = flat ? height : width;
|
|
944
|
+
const count = root?.type === "split" && root.dir === ( flat ? "row" : "column" ) ? root.children.filter( child => isShown( child, hidden ) ).length : 1;
|
|
852
945
|
const bandRect = band ? this.rectOf( band ) : null;
|
|
853
|
-
const rect = bandRect ? sideRect( bandRect, "bottom", bandRect.h / ( rows + 1 ) ) : sideRect( area, side, width );
|
|
854
946
|
|
|
855
|
-
|
|
947
|
+
let rect: DockRect;
|
|
948
|
+
|
|
949
|
+
if( !bandRect ) {
|
|
950
|
+
rect = sideRect( area, side, size );
|
|
951
|
+
}
|
|
952
|
+
else if( flat ) {
|
|
953
|
+
rect = sideRect( bandRect, "right", bandRect.w / ( count + 1 ) );
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
rect = sideRect( bandRect, "bottom", bandRect.h / ( count + 1 ) );
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
return { rect, apply: ( ) => dockBand( layout, panel, side, band ? 0 : size ) };
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* The side of the contents a point is near, among those the band of
|
|
964
|
+
* which a tool may be docked in; the nearest one, null for none.
|
|
965
|
+
*
|
|
966
|
+
* A point beyond the side counts too: it is between the contents and
|
|
967
|
+
* the band. But it has to be along the side: over a band of another
|
|
968
|
+
* side, it is not near this one.
|
|
969
|
+
*/
|
|
970
|
+
|
|
971
|
+
private edgeOf( panel: string, area: DockRect, x: number, y: number ): DockSide {
|
|
972
|
+
const along = ( side: DockSide ) => isFlat( side ) ? x >= area.x && x < area.x + area.w : y >= area.y && y < area.y + area.h;
|
|
973
|
+
|
|
974
|
+
// how deep the point is from each side: 0 on it, 1 where its zone ends
|
|
975
|
+
const depth = ( side: DockSide ): number => {
|
|
976
|
+
switch( side ) {
|
|
977
|
+
case "left": return ( x - area.x ) / SIDE_BAND;
|
|
978
|
+
case "right": return ( area.x + area.w - x ) / SIDE_BAND;
|
|
979
|
+
case "top": return ( y - area.y ) / SIDE_BAND;
|
|
980
|
+
case "bottom": return ( area.y + area.h - y ) / SIDE_BAND;
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
let nearest: DockSide = null;
|
|
985
|
+
|
|
986
|
+
for( const side of SIDES ) {
|
|
987
|
+
if( this.mayDock( panel, side ) && along( side ) && depth( side ) < 1 && ( nearest === null || depth( side ) < depth( nearest ) ) ) {
|
|
988
|
+
nearest = side;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
return nearest;
|
|
856
993
|
}
|
|
857
994
|
|
|
858
995
|
/**
|
|
@@ -19,16 +19,21 @@
|
|
|
19
19
|
*
|
|
20
20
|
* There are two kinds of panels, which never mix:
|
|
21
21
|
*
|
|
22
|
-
* tools docked in a band
|
|
23
|
-
* other, side by side, or as tabs of the same stack;
|
|
24
|
-
* can also float.
|
|
22
|
+
* tools docked in a band along a side of the contents: one above
|
|
23
|
+
* the other, side by side, or as tabs of the same stack;
|
|
24
|
+
* they can also float. Which bands a tool may be in is said
|
|
25
|
+
* by the view (DockKinds.sides): the left and the right
|
|
26
|
+
* ones, unless it says otherwise.
|
|
25
27
|
* contents what is worked on (a document, as the editors of an IDE).
|
|
26
28
|
* They share the room between the bands: side by side, one
|
|
27
29
|
* above the other, or as tabs of the same stack.
|
|
28
30
|
*
|
|
29
31
|
* A layout is plain data, so that it can be saved as it is:
|
|
30
32
|
*
|
|
31
|
-
* left, right the
|
|
33
|
+
* left, right the bands of the sides: a width, and a tree of tools.
|
|
34
|
+
* They take the whole height.
|
|
35
|
+
* top, bottom the bands above and under the contents, between those of
|
|
36
|
+
* the sides: a height, and a tree of tools.
|
|
32
37
|
* content the tree of the contents.
|
|
33
38
|
*
|
|
34
39
|
* In a tree, a split puts its children side by side (row)
|
|
@@ -73,7 +78,10 @@ export interface DockStack {
|
|
|
73
78
|
|
|
74
79
|
export type DockNode = DockSplit | DockStack;
|
|
75
80
|
|
|
76
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* The tools docked on one side: `size` is its width in pixels (its height,
|
|
83
|
+
* for the band of the top or of the bottom), `root` is null when it has none.
|
|
84
|
+
*/
|
|
77
85
|
export interface DockBand {
|
|
78
86
|
size: number;
|
|
79
87
|
root: DockNode;
|
|
@@ -91,9 +99,16 @@ export interface DockFloat extends DockRect {
|
|
|
91
99
|
panel: string;
|
|
92
100
|
}
|
|
93
101
|
|
|
102
|
+
/**
|
|
103
|
+
* `top` and `bottom` may be missing: a band that is missing is an empty
|
|
104
|
+
* one. A layout that does not use them does not have them.
|
|
105
|
+
*/
|
|
106
|
+
|
|
94
107
|
export interface DockLayout {
|
|
95
108
|
left: DockBand;
|
|
96
109
|
right: DockBand;
|
|
110
|
+
top?: DockBand;
|
|
111
|
+
bottom?: DockBand;
|
|
97
112
|
content: DockNode;
|
|
98
113
|
floats: DockFloat[];
|
|
99
114
|
hidden: string[];
|
|
@@ -103,12 +118,47 @@ export interface DockLayout {
|
|
|
103
118
|
export interface DockKinds {
|
|
104
119
|
tools: string[];
|
|
105
120
|
contents: string[];
|
|
121
|
+
|
|
122
|
+
/** the bands a tool may be docked in, by its id; the left and the right ones for a tool that is not there */
|
|
123
|
+
sides?: Record<string, DockSide[]>;
|
|
106
124
|
}
|
|
107
125
|
|
|
126
|
+
/** the bands, in the order they are looked at */
|
|
127
|
+
const SIDES: DockSide[] = ["left", "right", "top", "bottom"];
|
|
128
|
+
|
|
129
|
+
/** where a tool may be docked when nothing says it */
|
|
130
|
+
const USUAL_SIDES: DockSide[] = ["left", "right"];
|
|
131
|
+
|
|
108
132
|
function copy( layout: DockLayout ): DockLayout {
|
|
109
133
|
return structuredClone( layout );
|
|
110
134
|
}
|
|
111
135
|
|
|
136
|
+
/**
|
|
137
|
+
* true for the bands of the top and of the bottom: they lie flat. Their
|
|
138
|
+
* size is a height, and their tools are side by side where those of the
|
|
139
|
+
* left and of the right are one above the other.
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
export function isFlat( side: DockSide ): boolean {
|
|
143
|
+
return side === "top" || side === "bottom";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The bands a tool may be docked in. A list that is empty counts as none
|
|
148
|
+
* given: a tool has to be somewhere when its layout is lost.
|
|
149
|
+
*/
|
|
150
|
+
|
|
151
|
+
export function dockSides( kinds: DockKinds, panel: string ): DockSide[] {
|
|
152
|
+
const given = kinds.sides ? kinds.sides[panel] : null;
|
|
153
|
+
|
|
154
|
+
if( !given ) {
|
|
155
|
+
return USUAL_SIDES;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const sides = given.filter( side => SIDES.includes( side ) );
|
|
159
|
+
return sides.length ? sides : USUAL_SIDES;
|
|
160
|
+
}
|
|
161
|
+
|
|
112
162
|
/**
|
|
113
163
|
* the stacks of a tree, from left to right and from top to bottom
|
|
114
164
|
*/
|
|
@@ -122,7 +172,7 @@ export function stacksOf( node: DockNode ): DockStack[] {
|
|
|
122
172
|
}
|
|
123
173
|
|
|
124
174
|
function allStacks( layout: DockLayout ): DockStack[] {
|
|
125
|
-
return [...
|
|
175
|
+
return [...SIDES.flatMap( side => stacksOf( layout[side]?.root ) ), ...stacksOf( layout.content )];
|
|
126
176
|
}
|
|
127
177
|
|
|
128
178
|
function parentOf( root: DockNode, node: DockNode ): DockSplit {
|
|
@@ -179,8 +229,13 @@ function normalize( node: DockNode ): DockNode {
|
|
|
179
229
|
|
|
180
230
|
function tidy( layout: DockLayout ): void {
|
|
181
231
|
layout.content = normalize( layout.content );
|
|
182
|
-
|
|
183
|
-
|
|
232
|
+
|
|
233
|
+
for( const side of SIDES ) {
|
|
234
|
+
const band = layout[side];
|
|
235
|
+
if( band ) {
|
|
236
|
+
band.root = normalize( band.root );
|
|
237
|
+
}
|
|
238
|
+
}
|
|
184
239
|
}
|
|
185
240
|
|
|
186
241
|
/**
|
|
@@ -189,10 +244,13 @@ function tidy( layout: DockLayout ): void {
|
|
|
189
244
|
|
|
190
245
|
function detach( layout: DockLayout, panel: string ): void {
|
|
191
246
|
// A tool that was beside others, alone in its stack, takes its width
|
|
192
|
-
// with it: its band gets narrower by its share of the row.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
247
|
+
// with it: its band gets narrower by its share of the row. In a band
|
|
248
|
+
// that lies flat it is its height, and its share of the column.
|
|
249
|
+
for( const side of SIDES ) {
|
|
250
|
+
const band = layout[side];
|
|
251
|
+
const root = band?.root;
|
|
252
|
+
const across = isFlat( side ) ? "column" : "row";
|
|
253
|
+
const at = root?.type === "split" && root.dir === across ? root.children.findIndex( child => child.type === "stack" && child.panels.length === 1 && child.panels[0] === panel ) : -1;
|
|
196
254
|
|
|
197
255
|
if( root?.type === "split" && at >= 0 ) {
|
|
198
256
|
const shares = sharesOf( root.children );
|
|
@@ -227,14 +285,8 @@ export function stackOf( layout: DockLayout, panel: string ): DockStack {
|
|
|
227
285
|
* the band a panel is docked in, null when it is a content, floats or is unknown
|
|
228
286
|
*/
|
|
229
287
|
|
|
230
|
-
export function bandOf( layout: DockLayout, panel: string ):
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if( holds( layout.left ) ) {
|
|
234
|
-
return "left";
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return holds( layout.right ) ? "right" : null;
|
|
288
|
+
export function bandOf( layout: DockLayout, panel: string ): DockSide {
|
|
289
|
+
return SIDES.find( side => stacksOf( layout[side]?.root ).some( stack => stack.panels.includes( panel ) ) ) ?? null;
|
|
238
290
|
}
|
|
239
291
|
|
|
240
292
|
export function floatOf( layout: DockLayout, panel: string ): DockFloat {
|
|
@@ -286,6 +338,10 @@ export function sharesOf( nodes: DockNode[] ): number[] {
|
|
|
286
338
|
* takes half of its room. But a tool put on the left or on the right of
|
|
287
339
|
* another one does not make it narrower: with `width`, the band gets
|
|
288
340
|
* wider by that much, which is what the tool gets.
|
|
341
|
+
*
|
|
342
|
+
* In a band that lies flat (top, bottom) it is the other way round: a
|
|
343
|
+
* tool put above or under another one makes the band higher, by `width`,
|
|
344
|
+
* which is then a height.
|
|
289
345
|
*/
|
|
290
346
|
|
|
291
347
|
export function dockMove( layout: DockLayout, panel: string, target: string, where: DockWhere, width = 0 ): DockLayout {
|
|
@@ -319,7 +375,9 @@ export function dockMove( layout: DockLayout, panel: string, target: string, whe
|
|
|
319
375
|
|
|
320
376
|
const dir = where === "left" || where === "right" ? "row" : "column";
|
|
321
377
|
const before = where === "left" || where === "top";
|
|
322
|
-
|
|
378
|
+
|
|
379
|
+
// across the band: the direction in which it grows
|
|
380
|
+
const wider = !!band && dir === ( isFlat( side ) ? "column" : "row" ) && width > 0;
|
|
323
381
|
|
|
324
382
|
const half = dest.size / 2;
|
|
325
383
|
const fresh: DockStack = { type: "stack", size: wider ? width : half, panels: [panel], active: panel };
|
|
@@ -357,17 +415,22 @@ export function dockMove( layout: DockLayout, panel: string, target: string, whe
|
|
|
357
415
|
}
|
|
358
416
|
|
|
359
417
|
/**
|
|
360
|
-
* Dock a tool in a band,
|
|
361
|
-
*
|
|
418
|
+
* Dock a tool in a band, after the tools that are already there: under
|
|
419
|
+
* them on the left and on the right, on their right in a band that lies
|
|
420
|
+
* flat (top, bottom). `size` is the width the band gets when it was empty;
|
|
421
|
+
* its height, when it lies flat.
|
|
362
422
|
*/
|
|
363
423
|
|
|
364
|
-
export function dockBand( layout: DockLayout, panel: string, side:
|
|
424
|
+
export function dockBand( layout: DockLayout, panel: string, side: DockSide, size = 0 ): DockLayout {
|
|
365
425
|
const result = copy( layout );
|
|
366
|
-
const band = result[side];
|
|
426
|
+
const band = result[side] ??= { size: 0, root: null };
|
|
367
427
|
|
|
368
428
|
detach( result, panel );
|
|
369
429
|
tidy( result );
|
|
370
430
|
|
|
431
|
+
// along the band: the direction its tools follow each other in
|
|
432
|
+
const along = isFlat( side ) ? "row" : "column";
|
|
433
|
+
|
|
371
434
|
const fresh: DockStack = { type: "stack", size: 0, panels: [panel], active: panel };
|
|
372
435
|
const root = band.root;
|
|
373
436
|
|
|
@@ -378,15 +441,15 @@ export function dockBand( layout: DockLayout, panel: string, side: "left" | "rig
|
|
|
378
441
|
band.size = size;
|
|
379
442
|
}
|
|
380
443
|
}
|
|
381
|
-
else if( root.type === "split" && root.dir ===
|
|
382
|
-
// as
|
|
444
|
+
else if( root.type === "split" && root.dir === along ) {
|
|
445
|
+
// as big as the others, on average
|
|
383
446
|
fresh.size = sharesOf( root.children ).reduce( ( a, b ) => a + b ) / root.children.length;
|
|
384
447
|
root.children.push( fresh );
|
|
385
448
|
}
|
|
386
449
|
else {
|
|
387
|
-
// what was there and the tool share the
|
|
450
|
+
// what was there and the tool share the band
|
|
388
451
|
root.size = 0;
|
|
389
|
-
band.root = { type: "split", dir:
|
|
452
|
+
band.root = { type: "split", dir: along, size: 0, children: [root, fresh] };
|
|
390
453
|
}
|
|
391
454
|
|
|
392
455
|
return result;
|
|
@@ -475,7 +538,7 @@ function sizeOf( from: Record<string, unknown>, name: string ): number {
|
|
|
475
538
|
* A layout rebuilt from something that was saved, or null when it is not a
|
|
476
539
|
* layout or holds no panel. Only what is understood is kept, and only the
|
|
477
540
|
* panels that are known, each once and where its kind may be: a tool in a
|
|
478
|
-
* band or floating
|
|
541
|
+
* band it may be docked in, or floating; a content among the contents.
|
|
479
542
|
*
|
|
480
543
|
* (a field is read only once it is known to be there: what was saved may be
|
|
481
544
|
* a state of x4, which reports the reading of a missing property)
|
|
@@ -527,17 +590,21 @@ function sanitize( saved: unknown, kinds: DockKinds ): DockLayout {
|
|
|
527
590
|
return null;
|
|
528
591
|
};
|
|
529
592
|
|
|
530
|
-
|
|
593
|
+
// a band only keeps the tools that may be docked in it
|
|
594
|
+
const band = ( name: DockSide ): DockBand => {
|
|
531
595
|
const from = name in saved ? saved[name] : null;
|
|
532
596
|
if( !isObject( from ) ) {
|
|
533
597
|
return { size: 0, root: null };
|
|
534
598
|
}
|
|
535
599
|
|
|
536
|
-
|
|
600
|
+
const allowed = kinds.tools.filter( id => dockSides( kinds, id ).includes( name ) );
|
|
601
|
+
return { size: sizeOf( from, "size" ), root: normalize( "root" in from ? node( from.root, allowed ) : null ) };
|
|
537
602
|
};
|
|
538
603
|
|
|
539
604
|
const left = band( "left" );
|
|
540
605
|
const right = band( "right" );
|
|
606
|
+
const top = band( "top" );
|
|
607
|
+
const bottom = band( "bottom" );
|
|
541
608
|
const content = normalize( "content" in saved ? node( saved.content, kinds.contents ) : null );
|
|
542
609
|
|
|
543
610
|
const floats: DockFloat[] = [];
|
|
@@ -561,7 +628,23 @@ function sanitize( saved: unknown, kinds: DockKinds ): DockLayout {
|
|
|
561
628
|
}
|
|
562
629
|
|
|
563
630
|
// nothing in it that is known: not worth more than no layout at all
|
|
564
|
-
|
|
631
|
+
if( seen.size === 0 ) {
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// the bands of the top and of the bottom are only there when they hold
|
|
636
|
+
// something: a layout that does not use them is as it always was
|
|
637
|
+
const layout: DockLayout = { left, right, content, floats, hidden };
|
|
638
|
+
|
|
639
|
+
if( top.root ) {
|
|
640
|
+
layout.top = top;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if( bottom.root ) {
|
|
644
|
+
layout.bottom = bottom;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return layout;
|
|
565
648
|
}
|
|
566
649
|
|
|
567
650
|
/**
|
|
@@ -570,7 +653,9 @@ function sanitize( saved: unknown, kinds: DockKinds ): DockLayout {
|
|
|
570
653
|
*
|
|
571
654
|
* `kinds` are the panels that exist today. A saved layout may be older
|
|
572
655
|
* than the application: the panels it does not know are added where the
|
|
573
|
-
* default layout has them, those that are gone are dropped.
|
|
656
|
+
* default layout has them, those that are gone are dropped. So is a tool
|
|
657
|
+
* that is in a band it may not be docked in anymore: it goes where the
|
|
658
|
+
* default layout has it.
|
|
574
659
|
*/
|
|
575
660
|
|
|
576
661
|
export function dockRestore( saved: unknown, kinds: DockKinds, defaults: DockLayout ): DockLayout {
|
|
@@ -582,8 +667,10 @@ export function dockRestore( saved: unknown, kinds: DockKinds, defaults: DockLay
|
|
|
582
667
|
continue;
|
|
583
668
|
}
|
|
584
669
|
|
|
670
|
+
// where the default layout has it; otherwise on the right if it may, or in the first band it may be in
|
|
671
|
+
const allowed = dockSides( kinds, panel );
|
|
585
672
|
const float = floatOf( usual, panel );
|
|
586
|
-
const side = bandOf( usual, panel ) ?? "right";
|
|
673
|
+
const side = bandOf( usual, panel ) ?? ( allowed.includes( "right" ) ? "right" : allowed[0] );
|
|
587
674
|
|
|
588
675
|
if( kinds.contents.includes( panel ) ) {
|
|
589
676
|
layout = addContent( layout, panel );
|
|
@@ -592,7 +679,9 @@ export function dockRestore( saved: unknown, kinds: DockKinds, defaults: DockLay
|
|
|
592
679
|
layout = dockFloat( layout, panel, float );
|
|
593
680
|
}
|
|
594
681
|
else {
|
|
595
|
-
|
|
682
|
+
// as wide as the default layout has that band, when it has it
|
|
683
|
+
const band = usual[side];
|
|
684
|
+
layout = dockBand( layout, panel, side, band ? band.size : 0 );
|
|
596
685
|
}
|
|
597
686
|
|
|
598
687
|
if( usual.hidden.includes( panel ) ) {
|
|
@@ -59,6 +59,10 @@ export class Gauge extends Component<GaugeProps> {
|
|
|
59
59
|
constructor( props: GaugeProps ) {
|
|
60
60
|
super( props );
|
|
61
61
|
|
|
62
|
+
// no range given: 0 to 100
|
|
63
|
+
this.props.min = props.min ?? 0;
|
|
64
|
+
this.props.max = props.max ?? 100;
|
|
65
|
+
|
|
62
66
|
this.pos = 70;
|
|
63
67
|
this.stops = [];
|
|
64
68
|
|
|
@@ -410,7 +410,12 @@ export class Input extends Component<InputProps,InputEvents> {
|
|
|
410
410
|
*/
|
|
411
411
|
|
|
412
412
|
public setNumValue( value: number, ndec = -1 ) {
|
|
413
|
-
|
|
413
|
+
|
|
414
|
+
// no number (a field that is not filled): the field is emptied
|
|
415
|
+
if( value===undefined || value===null || isNaN( value ) ) {
|
|
416
|
+
return this.setValue( "" );
|
|
417
|
+
}
|
|
418
|
+
|
|
414
419
|
if( ndec==-2 && this.props.type=='number' ) {
|
|
415
420
|
const p = this.props as NumberProps;
|
|
416
421
|
|
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
|
|
19
19
|
:root {
|
|
20
20
|
--notification-border: var( --border );
|
|
21
|
-
--notification-background:
|
|
21
|
+
--notification-background: var( --background-primary );
|
|
22
|
+
|
|
23
|
+
--notification-title-color: var( --text-primary );
|
|
24
|
+
--notification-text-color: var( --text-ternary );
|
|
25
|
+
--notification-button-color: var( --text-ternary );
|
|
22
26
|
|
|
23
27
|
--notification-icon-default: var( --accent-background );
|
|
24
28
|
--notification-icon-success: var( --success-background );
|
|
@@ -54,11 +58,12 @@
|
|
|
54
58
|
.title {
|
|
55
59
|
font-weight: bold;
|
|
56
60
|
padding: 6px 6px 3px 6px;
|
|
61
|
+
color: var( --notification-title-color );
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
.text {
|
|
60
65
|
padding: 3px 6px 6px 6px;
|
|
61
|
-
color: var( --
|
|
66
|
+
color: var( --notification-text-color );
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
69
|
|
|
@@ -81,7 +86,7 @@
|
|
|
81
86
|
|
|
82
87
|
.x4button {
|
|
83
88
|
height: 24px;
|
|
84
|
-
color: var( --
|
|
89
|
+
color: var( --notification-button-color );
|
|
85
90
|
border: none;
|
|
86
91
|
|
|
87
92
|
//&:hover {
|
|
@@ -52,6 +52,9 @@ interface NotificationProps extends ComponentProps {
|
|
|
52
52
|
* ```
|
|
53
53
|
* --notification-border
|
|
54
54
|
* --notification-background
|
|
55
|
+
* --notification-title-color
|
|
56
|
+
* --notification-text-color
|
|
57
|
+
* --notification-button-color
|
|
55
58
|
* --notification-icon-default
|
|
56
59
|
* --notification-icon-success
|
|
57
60
|
* --notification-icon-danger
|
|
@@ -21,8 +21,8 @@ import "./progress.module.scss";
|
|
|
21
21
|
|
|
22
22
|
interface ProgressProps extends ComponentProps {
|
|
23
23
|
value: number;
|
|
24
|
-
min
|
|
25
|
-
max
|
|
24
|
+
min?: number; // 0 when missing
|
|
25
|
+
max?: number; // 100 when missing
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
@@ -46,7 +46,11 @@ export class Progress extends Component<ProgressProps> {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
setValue( value: number ) {
|
|
49
|
-
const
|
|
49
|
+
const min = this.props.min ?? 0;
|
|
50
|
+
const max = this.props.max ?? 100;
|
|
51
|
+
|
|
52
|
+
// where the value is between the two: min is 0%, max is 100%
|
|
53
|
+
const perc = (value-min) / (max-min) * 100;
|
|
50
54
|
this._bar.setStyleValue( "width", perc+"%" );
|
|
51
55
|
}
|
|
52
56
|
}
|
|
@@ -28,6 +28,7 @@ interface SwitchProps extends ComponentProps {
|
|
|
28
28
|
label: string;
|
|
29
29
|
checked?: boolean;
|
|
30
30
|
value?: string;
|
|
31
|
+
name?: string; // with it, a Form reads and writes the switch (a boolean)
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/**
|
|
@@ -52,7 +53,7 @@ export class Switch extends HBox<SwitchProps> {
|
|
|
52
53
|
new Component( {
|
|
53
54
|
cls: "switch",
|
|
54
55
|
content: [
|
|
55
|
-
new Input( { type: "checkbox", id: inputId, checked: props.checked } ),
|
|
56
|
+
new Input( { type: "checkbox", id: inputId, name: props.name, checked: props.checked } ),
|
|
56
57
|
new Component( { cls: "track" } ),
|
|
57
58
|
new Component( { cls: "thumb" } ),
|
|
58
59
|
]
|