@jxsuite/ui 0.0.0 → 0.1.0
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/components/jx-accordion-item.json +281 -0
- package/components/jx-accordion.json +33 -0
- package/components/jx-action-button.json +261 -33
- package/components/jx-action-group.json +123 -0
- package/components/jx-button.json +133 -25
- package/components/jx-checkbox.json +215 -0
- package/components/jx-color-area.json +263 -0
- package/components/jx-color-field.json +565 -0
- package/components/jx-color-slider.json +216 -0
- package/components/jx-combobox.json +544 -0
- package/components/jx-dialog.json +416 -0
- package/components/jx-divider.json +52 -0
- package/components/jx-dot.json +38 -0
- package/components/jx-field.json +162 -0
- package/components/jx-kbd.json +24 -0
- package/components/jx-listbox.json +76 -0
- package/components/jx-menu-item.json +5 -12
- package/components/jx-menu.json +45 -6
- package/components/jx-number-field.json +441 -0
- package/components/jx-option.json +350 -0
- package/components/jx-popover.json +227 -0
- package/components/jx-select.json +637 -0
- package/components/jx-spinner.json +129 -0
- package/components/jx-split.json +221 -0
- package/components/jx-swatch-group.json +107 -0
- package/components/jx-swatch.json +262 -0
- package/components/jx-switch.json +253 -0
- package/components/jx-tab-panel.json +58 -0
- package/components/jx-tab.json +296 -0
- package/components/jx-tabs.json +124 -0
- package/components/jx-textfield.json +639 -0
- package/components/jx-toast-host.json +121 -0
- package/components/jx-toast.json +255 -0
- package/components/jx-toolbar.json +81 -0
- package/components/jx-tooltip.json +207 -0
- package/components/jx-tree-item.json +325 -0
- package/components/jx-tree.json +202 -0
- package/icons/list.json +4 -0
- package/icons/manifest.json +12 -0
- package/package.json +27 -5
- package/project.json +164 -153
- package/src/behaviors/action-group.ts +255 -0
- package/src/behaviors/color-area.ts +303 -0
- package/src/behaviors/color-field.ts +465 -0
- package/src/behaviors/color-slider.ts +154 -0
- package/src/behaviors/combobox.ts +474 -0
- package/src/behaviors/dialog.ts +137 -0
- package/src/behaviors/listbox.ts +118 -0
- package/src/behaviors/menu.ts +80 -48
- package/src/behaviors/number-field.ts +161 -0
- package/src/behaviors/popover.ts +399 -0
- package/src/behaviors/select.ts +154 -0
- package/src/behaviors/split.ts +478 -0
- package/src/behaviors/swatch-group.ts +297 -0
- package/src/behaviors/tabs.ts +319 -0
- package/src/behaviors/textfield.ts +164 -0
- package/src/behaviors/toast-host.ts +241 -0
- package/src/behaviors/toast.ts +261 -0
- package/src/behaviors/toolbar.ts +413 -0
- package/src/behaviors/tooltip.ts +715 -0
- package/src/behaviors/tree.ts +830 -0
- package/src/color.ts +433 -0
- package/src/documents.ts +91 -1
- package/src/index.ts +41 -1
- package/src/loaders.ts +47 -0
- package/src/testing/anchor-support.ts +28 -0
- package/src/testing/popover-shim.ts +39 -6
- package/src/theme.ts +30 -6
- package/stylebook/jx-accordion-item.json +195 -0
- package/stylebook/jx-accordion.json +220 -0
- package/stylebook/jx-action-button.json +191 -23
- package/stylebook/jx-action-group.json +335 -0
- package/stylebook/jx-button.json +129 -26
- package/stylebook/jx-checkbox.json +245 -0
- package/stylebook/jx-color-area.json +272 -0
- package/stylebook/jx-color-field.json +261 -0
- package/stylebook/jx-color-slider.json +220 -0
- package/stylebook/jx-combobox.json +332 -0
- package/stylebook/jx-dialog.json +186 -0
- package/stylebook/jx-divider.json +125 -0
- package/stylebook/jx-dot.json +92 -0
- package/stylebook/jx-field.json +313 -0
- package/stylebook/jx-icon.json +4 -0
- package/stylebook/jx-kbd.json +90 -0
- package/stylebook/jx-listbox.json +337 -0
- package/stylebook/jx-menu.json +0 -1
- package/stylebook/jx-number-field.json +203 -0
- package/stylebook/jx-option.json +285 -0
- package/stylebook/jx-popover.json +123 -0
- package/stylebook/jx-select.json +407 -0
- package/stylebook/jx-spinner.json +111 -0
- package/stylebook/jx-split.json +170 -0
- package/stylebook/jx-swatch-group.json +175 -0
- package/stylebook/jx-swatch.json +249 -0
- package/stylebook/jx-switch.json +240 -0
- package/stylebook/jx-tab-panel.json +147 -0
- package/stylebook/jx-tab.json +201 -0
- package/stylebook/jx-tabs.json +278 -0
- package/stylebook/jx-textfield.json +309 -0
- package/stylebook/jx-toast-host.json +157 -0
- package/stylebook/jx-toast.json +266 -0
- package/stylebook/jx-toolbar.json +435 -0
- package/stylebook/jx-tooltip.json +162 -0
- package/stylebook/jx-tree-item.json +258 -0
- package/stylebook/jx-tree.json +429 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `jx-split`'s behaviour sidecar: the pointer maths, the keyboard, and the collapse toggle.
|
|
3
|
+
*
|
|
4
|
+
* The element is one `role="separator"` box between two things, and the platform gives a
|
|
5
|
+
* role-carrying custom element none of what a window splitter owes a reader — so all of it is
|
|
6
|
+
* here.
|
|
7
|
+
*
|
|
8
|
+
* **The value is a FRACTION of the box the splitter divides, and that is the whole design.** A
|
|
9
|
+
* splitter has no size of its own to report; what it reports is where it sits, and the only honest
|
|
10
|
+
* unit for that is the leading side's share of the track. Everything else follows: `gap` is a pixel
|
|
11
|
+
* floor converted into that unit at gesture time, a drag is a pointer delta over the measured track
|
|
12
|
+
* length, and the same stored number survives a window resize because it never meant pixels. A host
|
|
13
|
+
* that wants pixels is resizing a PANEL rather than moving a split, which is a different element
|
|
14
|
+
* and, in Studio, still `ui/panel-resize.ts`.
|
|
15
|
+
*
|
|
16
|
+
* **The track is measured, not declared.** {@link trackOf} walks up to the first ancestor that
|
|
17
|
+
* generates a box with a length along the dragged axis, skipping `display: contents` wrappers —
|
|
18
|
+
* which is not a nicety: Studio's pane grid puts the splitter inside two of them precisely so it
|
|
19
|
+
* lands as a grid item between the two cells, and the box it actually divides is the grid two
|
|
20
|
+
* levels up. Measuring at `pointerdown` rather than taking a prop is what keeps the bounds honest
|
|
21
|
+
* across a window resize with no listener anywhere: a prop computed from a width is stale the
|
|
22
|
+
* moment the width changes, and a splitter whose floor is stale lets a pane go below its minimum.
|
|
23
|
+
*
|
|
24
|
+
* **An unlaid-out track answers nothing rather than answering at its origin**, the same rule
|
|
25
|
+
* `color-area.ts` states: with no geometry there is no fraction, so a drag is refused outright. The
|
|
26
|
+
* KEYBOARD is not refused — {@link boundsOf} falls back to the declared `min`/`max` — because a
|
|
27
|
+
* step is arithmetic on a number the element already has, and a splitter that could not be moved by
|
|
28
|
+
* a reader who cannot see it would fail SC 2.1.1 on a technicality of layout timing.
|
|
29
|
+
*
|
|
30
|
+
* **Enter and a double click are one gesture with two doors, and they are what clears SC 2.5.7.**
|
|
31
|
+
* The success criterion asks for a single-pointer alternative to dragging, which the arrow keys do
|
|
32
|
+
* not supply (those answer 2.1.1). A double click that collapses the split and restores it is that
|
|
33
|
+
* alternative; Enter is the same move from the keyboard. `collapse` says where "collapsed" is —
|
|
34
|
+
* `min` by default, because that is what collapsing means for most splitters, and any position a
|
|
35
|
+
* host names for one whose two sides are both real panes.
|
|
36
|
+
*
|
|
37
|
+
* **Every `input` and `change` carries the modifier keys the gesture step was made with, and the
|
|
38
|
+
* element gives none of them a meaning of its own beyond `Shift`.** A host that snaps — Studio's
|
|
39
|
+
* Edit column pulls its width onto the document's breakpoints, with Alt as the way through — needs
|
|
40
|
+
* to know what the hand was holding on THIS step, and the element is the only thing that saw the
|
|
41
|
+
* event. It does not learn what a breakpoint is: it reports `{ altKey, ctrlKey, metaKey, shiftKey
|
|
42
|
+
* }` as the event's `detail`, a fact about one step rather than about the element, which is why it
|
|
43
|
+
* is on the event and not in the state — state is what is true between events, and a modifier the
|
|
44
|
+
* hand released is not. `Shift` is reported even though the keyboard already spent it on the large
|
|
45
|
+
* step, because what a host makes of a modifier is the host's business. {@link splitModifiersOf}
|
|
46
|
+
* reads the detail back off any `Event`, so a plain `input` a host dispatches itself means "no
|
|
47
|
+
* modifiers" rather than a type error.
|
|
48
|
+
*
|
|
49
|
+
* @docs extending/ui-kit
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/** The tag this sidecar belongs to. */
|
|
53
|
+
const HOST = "jx-split";
|
|
54
|
+
|
|
55
|
+
/** Below this, two fractions are the same position. */
|
|
56
|
+
const EPSILON = 1e-6;
|
|
57
|
+
|
|
58
|
+
/** The reactive scope a `jx-split` document hands its handlers. */
|
|
59
|
+
export interface SplitState {
|
|
60
|
+
/** Where the splitter sits: the leading side's share of the track, 0-1. */
|
|
61
|
+
value: number;
|
|
62
|
+
/** The smallest share the leading side may have. */
|
|
63
|
+
min: number;
|
|
64
|
+
/** The largest. */
|
|
65
|
+
max: number;
|
|
66
|
+
/** The smallest EITHER side may be, in pixels of the measured track. */
|
|
67
|
+
gap: number;
|
|
68
|
+
/** What one arrow key moves the value by. */
|
|
69
|
+
step: number;
|
|
70
|
+
/** What `Shift`+Arrow moves it by. */
|
|
71
|
+
largeStep: number;
|
|
72
|
+
/** `horizontal` or `vertical` — the separator's own orientation. */
|
|
73
|
+
orientation: string;
|
|
74
|
+
/** Whether the element refuses to move. */
|
|
75
|
+
disabled: boolean;
|
|
76
|
+
/** The value Enter and a double click move to, and back from. */
|
|
77
|
+
collapse: number;
|
|
78
|
+
/** Whether a pointer gesture is live. Written here, styled by the element. */
|
|
79
|
+
dragging: boolean;
|
|
80
|
+
/** The position the toggle returns to; negative until a collapse has recorded one. */
|
|
81
|
+
restore: number;
|
|
82
|
+
[key: string]: unknown;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The modifier keys one gesture step carried, as the platform names them on a pointer event and a
|
|
87
|
+
* key event alike. The `detail` of every `input` and `change` this element dispatches.
|
|
88
|
+
*/
|
|
89
|
+
export interface SplitModifiers {
|
|
90
|
+
altKey: boolean;
|
|
91
|
+
ctrlKey: boolean;
|
|
92
|
+
metaKey: boolean;
|
|
93
|
+
shiftKey: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The modifiers of a step that had none — also what a plain `Event` reads as. */
|
|
97
|
+
const NO_MODIFIERS: Readonly<SplitModifiers> = Object.freeze({
|
|
98
|
+
altKey: false,
|
|
99
|
+
ctrlKey: false,
|
|
100
|
+
metaKey: false,
|
|
101
|
+
shiftKey: false,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/** The four modifier flags off an event, each `true` only when it is exactly `true`. */
|
|
105
|
+
function modifiersOf(event: object): SplitModifiers {
|
|
106
|
+
const flags = event as Partial<Record<keyof SplitModifiers, unknown>>;
|
|
107
|
+
return {
|
|
108
|
+
altKey: flags.altKey === true,
|
|
109
|
+
ctrlKey: flags.ctrlKey === true,
|
|
110
|
+
metaKey: flags.metaKey === true,
|
|
111
|
+
shiftKey: flags.shiftKey === true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The modifiers an `input` or `change` from a `jx-split` carried.
|
|
117
|
+
*
|
|
118
|
+
* The one reader a host needs: the detail is on a `CustomEvent`, and a listener typed against the
|
|
119
|
+
* platform's own `input` sees an `Event`. Anything that is not this element's detail — a plain
|
|
120
|
+
* `Event` a host dispatched itself, a `UIEvent` whose `detail` is a click count — reads as no
|
|
121
|
+
* modifiers, so a host's snap runs and its bypass does not.
|
|
122
|
+
*
|
|
123
|
+
* @param event The `input` or `change`.
|
|
124
|
+
*/
|
|
125
|
+
export function splitModifiersOf(event: Event): SplitModifiers {
|
|
126
|
+
const { detail } = event as { detail?: unknown };
|
|
127
|
+
return detail !== null && typeof detail === "object" ? modifiersOf(detail) : { ...NO_MODIFIERS };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** One live gesture: where it started, and the bounds it was measured against. */
|
|
131
|
+
interface Drag {
|
|
132
|
+
/** The pointer coordinate at `pointerdown`, along the dragged axis. */
|
|
133
|
+
origin: number;
|
|
134
|
+
/** The value at `pointerdown`. */
|
|
135
|
+
start: number;
|
|
136
|
+
/** The track's length along that axis, measured once. */
|
|
137
|
+
length: number;
|
|
138
|
+
lo: number;
|
|
139
|
+
hi: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The gestures in flight, by element.
|
|
144
|
+
*
|
|
145
|
+
* A `WeakMap` rather than a module `Set` of ids: the key IS the element, so a splitter whose row
|
|
146
|
+
* the runtime drops mid-gesture takes its record with it rather than leaving one behind for a
|
|
147
|
+
* future element at the same address.
|
|
148
|
+
*/
|
|
149
|
+
const drags = new WeakMap<HTMLElement, Drag>();
|
|
150
|
+
|
|
151
|
+
/** The `jx-split` an event is being handled on. */
|
|
152
|
+
function hostOf(event: Event): HTMLElement | null {
|
|
153
|
+
const target = event.currentTarget;
|
|
154
|
+
return target instanceof HTMLElement && target.localName === HOST ? target : null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Say the value moved, from the HOST, in the platform's own names, carrying the step's modifiers.
|
|
159
|
+
*
|
|
160
|
+
* A `CustomEvent` rather than an `Event`, and only for the `detail`: it is still an `Event` to
|
|
161
|
+
* every listener that only wanted `e.target.value`.
|
|
162
|
+
*/
|
|
163
|
+
function announce(host: HTMLElement, modifiers: SplitModifiers, ...names: string[]): void {
|
|
164
|
+
for (const name of names) {
|
|
165
|
+
host.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail: modifiers }));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** A number, or the fallback when the scope holds something that is not one. */
|
|
170
|
+
function finiteOr(value: unknown, fallback: number): number {
|
|
171
|
+
const n = Number(value);
|
|
172
|
+
return Number.isFinite(n) ? n : fallback;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** A POSITIVE number, or the fallback. A step of zero is a key that does nothing. */
|
|
176
|
+
function positiveOr(value: unknown, fallback: number): number {
|
|
177
|
+
const n = Number(value);
|
|
178
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Whether the separator is dragged along x.
|
|
183
|
+
*
|
|
184
|
+
* Only `"horizontal"` turns it, the same one-value vocabulary `jx-divider` states: an unknown value
|
|
185
|
+
* falls back to the base case rather than to nothing.
|
|
186
|
+
*/
|
|
187
|
+
function isVertical(state: SplitState): boolean {
|
|
188
|
+
return String(state.orientation) !== "horizontal";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The pointer coordinate along the dragged axis. */
|
|
192
|
+
function coordOf(vertical: boolean, event: { clientX: number; clientY: number }): number {
|
|
193
|
+
return vertical ? event.clientX : event.clientY;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The length of the box the splitter divides, along the dragged axis.
|
|
198
|
+
*
|
|
199
|
+
* Two ancestors are skipped, and each for its own reason. One that computes `display: contents`
|
|
200
|
+
* generates no box at all, so whatever a browser reports for it describes its children rather than
|
|
201
|
+
* the track. One that measures zero is either unlaid-out or a wrapper of the same shape that this
|
|
202
|
+
* realm cannot see through — walking past it costs a frame's work once per gesture and is the only
|
|
203
|
+
* thing that makes the pane grid's two nested `display: contents` rows measurable at all.
|
|
204
|
+
*
|
|
205
|
+
* @param host The element.
|
|
206
|
+
* @param vertical Whether the drag is along x.
|
|
207
|
+
* @returns The length in pixels, or 0 when nothing above the splitter has one.
|
|
208
|
+
*/
|
|
209
|
+
function trackOf(host: HTMLElement, vertical: boolean): number {
|
|
210
|
+
for (let node = host.parentElement; node; node = node.parentElement) {
|
|
211
|
+
if (globalThis.getComputedStyle(node).display === "contents") {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const box = node.getBoundingClientRect();
|
|
215
|
+
const length = vertical ? box.width : box.height;
|
|
216
|
+
if (length > 0) {
|
|
217
|
+
return length;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The range the value may take right now: the declared bounds, narrowed by the pixel gap.
|
|
225
|
+
*
|
|
226
|
+
* The two are an INTERSECTION rather than a choice, because they answer different questions —
|
|
227
|
+
* `min`/`max` is the host's policy about the split and `gap` is the smallest a side can usefully be
|
|
228
|
+
* — and a splitter has to respect both. The gap is converted here, against the track measured this
|
|
229
|
+
* instant, which is why neither prop has to be recomputed by anybody when the window resizes.
|
|
230
|
+
*
|
|
231
|
+
* **A track too narrow for two gaps has no legal position, and the answer is the middle of the
|
|
232
|
+
* crossed bounds** rather than one end of them. With a symmetric gap that is the even split, which
|
|
233
|
+
* is the only position that treats the two sides alike when neither can have its minimum.
|
|
234
|
+
*
|
|
235
|
+
* @param state The element's reactive scope.
|
|
236
|
+
* @param host The element.
|
|
237
|
+
*/
|
|
238
|
+
function boundsOf(
|
|
239
|
+
state: SplitState,
|
|
240
|
+
host: HTMLElement,
|
|
241
|
+
): { lo: number; hi: number; length: number } {
|
|
242
|
+
const length = trackOf(host, isVertical(state));
|
|
243
|
+
const min = finiteOr(state.min, 0);
|
|
244
|
+
const max = finiteOr(state.max, 1);
|
|
245
|
+
const gap = length > 0 ? Math.max(0, finiteOr(state.gap, 0)) / length : 0;
|
|
246
|
+
let lo = Math.max(min, gap);
|
|
247
|
+
let hi = Math.min(max, 1 - gap);
|
|
248
|
+
if (lo > hi) {
|
|
249
|
+
const mid = (lo + hi) / 2;
|
|
250
|
+
lo = mid;
|
|
251
|
+
hi = mid;
|
|
252
|
+
}
|
|
253
|
+
return { hi, length, lo };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Move the splitter, bounded. Answers whether anything actually moved.
|
|
258
|
+
*
|
|
259
|
+
* @param state The element's reactive scope.
|
|
260
|
+
* @param next Where it is being asked to go.
|
|
261
|
+
* @param lo The low bound.
|
|
262
|
+
* @param hi The high one.
|
|
263
|
+
*/
|
|
264
|
+
function writeValue(state: SplitState, next: number, lo: number, hi: number): boolean {
|
|
265
|
+
const bounded = Math.min(hi, Math.max(lo, next));
|
|
266
|
+
if (!Number.isFinite(bounded) || bounded === Number(state.value)) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
state.value = bounded;
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Where the collapse toggle goes next, recording where it came from.
|
|
275
|
+
*
|
|
276
|
+
* Sitting at the collapsed position means the next press restores; anywhere else means the next
|
|
277
|
+
* press collapses and remembers. **A splitter that was BORN collapsed still opens**: with nothing
|
|
278
|
+
* remembered — or with a memory that is the collapsed position itself — the toggle goes to the
|
|
279
|
+
* middle of the legal range, which is a defined, reachable, uncollapsed place. Without that clause
|
|
280
|
+
* the first press on a splitter whose stored position happens to equal `collapse` would be a
|
|
281
|
+
* control that visibly does nothing, and the reader has no way to tell that from a broken one.
|
|
282
|
+
*
|
|
283
|
+
* @param state The element's reactive scope.
|
|
284
|
+
* @param lo The low bound.
|
|
285
|
+
* @param hi The high one.
|
|
286
|
+
*/
|
|
287
|
+
function toggleTarget(state: SplitState, lo: number, hi: number): number {
|
|
288
|
+
const collapse = Math.min(hi, Math.max(lo, finiteOr(state.collapse, lo)));
|
|
289
|
+
const value = Number(state.value);
|
|
290
|
+
if (Math.abs(value - collapse) > EPSILON) {
|
|
291
|
+
state.restore = value;
|
|
292
|
+
return collapse;
|
|
293
|
+
}
|
|
294
|
+
const restore = finiteOr(state.restore, -1);
|
|
295
|
+
return restore >= 0 && Math.abs(restore - collapse) > EPSILON ? restore : (lo + hi) / 2;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Which way an arrow key moves the value, or 0 for a key on the axis this splitter does not have.
|
|
300
|
+
*
|
|
301
|
+
* The CROSS-axis arrows are left alone rather than cancelled: a splitter is one tab stop inside
|
|
302
|
+
* somebody's application, and swallowing ArrowUp on a column splitter would take a scroll away from
|
|
303
|
+
* the reader for nothing.
|
|
304
|
+
*
|
|
305
|
+
* @param vertical Whether the drag is along x.
|
|
306
|
+
* @param key The event's key.
|
|
307
|
+
*/
|
|
308
|
+
function signFor(vertical: boolean, key: string): number {
|
|
309
|
+
if (vertical) {
|
|
310
|
+
return key === "ArrowLeft" ? -1 : key === "ArrowRight" ? 1 : 0;
|
|
311
|
+
}
|
|
312
|
+
return key === "ArrowUp" ? -1 : key === "ArrowDown" ? 1 : 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* A pointer went down on the splitter: measure once, capture, and take the focus.
|
|
317
|
+
*
|
|
318
|
+
* The value does NOT jump to the pointer. A splitter is the thing under the cursor already, so
|
|
319
|
+
* there is nothing to jump to, and a relative drag is what lets a reader nudge a split by three
|
|
320
|
+
* pixels without the first frame throwing it somewhere else.
|
|
321
|
+
*
|
|
322
|
+
* `preventDefault` is what stops the drag selecting the text on either side of the splitter, and it
|
|
323
|
+
* is why the focus is taken explicitly on the line after: a cancelled `pointerdown` never focuses,
|
|
324
|
+
* and a splitter the reader has just used must be the thing the arrow keys move next.
|
|
325
|
+
*
|
|
326
|
+
* @param state The element's reactive scope.
|
|
327
|
+
* @param event The pointer event.
|
|
328
|
+
*/
|
|
329
|
+
export function onSplitPointerDown(state: SplitState, event: PointerEvent): void {
|
|
330
|
+
const host = hostOf(event);
|
|
331
|
+
/* `button` through {@link finiteOr}, so a secondary button is refused and a synthetic event that
|
|
332
|
+
carries no button at all is still a primary press — happy-dom builds one of each. */
|
|
333
|
+
if (!host || state.disabled === true || finiteOr(event.button, 0) !== 0) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const { hi, length, lo } = boundsOf(state, host);
|
|
337
|
+
if (length <= 0) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
event.preventDefault();
|
|
341
|
+
host.focus();
|
|
342
|
+
drags.set(host, {
|
|
343
|
+
hi,
|
|
344
|
+
length,
|
|
345
|
+
lo,
|
|
346
|
+
origin: coordOf(isVertical(state), event),
|
|
347
|
+
start: Number(state.value),
|
|
348
|
+
});
|
|
349
|
+
if (typeof host.setPointerCapture === "function") {
|
|
350
|
+
/* Capture on the HOST, which is also the element the listener is on: a drag that runs off the
|
|
351
|
+
end of the splitter keeps arriving here instead of stopping at whatever it crossed. */
|
|
352
|
+
try {
|
|
353
|
+
host.setPointerCapture(event.pointerId);
|
|
354
|
+
} catch {
|
|
355
|
+
/* A SYNTHETIC pointer — a test, a screenshot lane, a driver — has no active pointer for the
|
|
356
|
+
browser to capture, and Chrome throws NotFoundError rather than ignoring it. The gesture
|
|
357
|
+
still works without capture as long as the pointer stays over the element, which is what
|
|
358
|
+
a synthetic drag does; the guard keeps an automation tool's press from becoming an
|
|
359
|
+
uncaught error in the console it is reading. */
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
state.dragging = true;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* The drag itself. Nothing moves without a press first, so a hover across the splitter moves no
|
|
367
|
+
* split.
|
|
368
|
+
*
|
|
369
|
+
* The bounds and the track length are the ones measured at `pointerdown` rather than re-read here:
|
|
370
|
+
* a gesture is one continuous act, and a floor that moved under it — the window resized mid-drag —
|
|
371
|
+
* would make the same pointer distance mean two different things.
|
|
372
|
+
*
|
|
373
|
+
* @param state The element's reactive scope.
|
|
374
|
+
* @param event The pointer event.
|
|
375
|
+
*/
|
|
376
|
+
export function onSplitPointerMove(state: SplitState, event: PointerEvent): void {
|
|
377
|
+
const host = hostOf(event);
|
|
378
|
+
const drag = host ? drags.get(host) : undefined;
|
|
379
|
+
if (!host || !drag) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const moved = coordOf(isVertical(state), event) - drag.origin;
|
|
383
|
+
if (writeValue(state, drag.start + moved / drag.length, drag.lo, drag.hi)) {
|
|
384
|
+
announce(host, modifiersOf(event), "input");
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Every way a gesture can END, not just the happy one.
|
|
390
|
+
*
|
|
391
|
+
* Bound to `pointerup`, `pointercancel` and `lostpointercapture` alike. `pointerup` alone left a
|
|
392
|
+
* capture lost any other way — the system taking the pointer, a touch gesture being stolen, the
|
|
393
|
+
* element's subtree being replaced under it — with the record still in the map and the element
|
|
394
|
+
* still drawn as though a hand were on it, and no further event able to clear either.
|
|
395
|
+
*
|
|
396
|
+
* The record is deleted FIRST, which is what makes releasing the capture safe: the release itself
|
|
397
|
+
* fires `lostpointercapture`, and the second pass through here finds nothing to end.
|
|
398
|
+
*
|
|
399
|
+
* `change` is dispatched even when the last move changed nothing, because the commit is about the
|
|
400
|
+
* gesture ending rather than about the final pixel.
|
|
401
|
+
*
|
|
402
|
+
* @param state The element's reactive scope.
|
|
403
|
+
* @param event The pointer event.
|
|
404
|
+
*/
|
|
405
|
+
export function onSplitPointerUp(state: SplitState, event: PointerEvent): void {
|
|
406
|
+
const host = hostOf(event);
|
|
407
|
+
if (!host || !drags.delete(host)) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
state.dragging = false;
|
|
411
|
+
if (
|
|
412
|
+
typeof host.releasePointerCapture === "function" &&
|
|
413
|
+
host.hasPointerCapture?.(event.pointerId)
|
|
414
|
+
) {
|
|
415
|
+
host.releasePointerCapture(event.pointerId);
|
|
416
|
+
}
|
|
417
|
+
announce(host, modifiersOf(event), "change");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* The splitter keyboard: the arrows of its own axis, Home and End, and Enter for the toggle.
|
|
422
|
+
*
|
|
423
|
+
* A keystroke is a move AND a commit, which is what the platform's own arrow does on a range: a
|
|
424
|
+
* host that persists on `change` persists once per key rather than never.
|
|
425
|
+
*
|
|
426
|
+
* Every key this element does not own reaches the host untouched — the same obligation `jx-tree`
|
|
427
|
+
* carries — so Tab still leaves, and an application chord pressed with the caret on a splitter
|
|
428
|
+
* still runs.
|
|
429
|
+
*
|
|
430
|
+
* @param state The element's reactive scope.
|
|
431
|
+
* @param event The keydown.
|
|
432
|
+
*/
|
|
433
|
+
export function onSplitKeydown(state: SplitState, event: KeyboardEvent): void {
|
|
434
|
+
const host = hostOf(event);
|
|
435
|
+
if (!host || state.disabled === true) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
const { hi, lo } = boundsOf(state, host);
|
|
439
|
+
let next: number;
|
|
440
|
+
if (event.key === "Home") {
|
|
441
|
+
next = lo;
|
|
442
|
+
} else if (event.key === "End") {
|
|
443
|
+
next = hi;
|
|
444
|
+
} else if (event.key === "Enter") {
|
|
445
|
+
next = toggleTarget(state, lo, hi);
|
|
446
|
+
} else {
|
|
447
|
+
const sign = signFor(isVertical(state), event.key);
|
|
448
|
+
if (sign === 0) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const step = event.shiftKey ? positiveOr(state.largeStep, 0.1) : positiveOr(state.step, 0.02);
|
|
452
|
+
next = Number(state.value) + sign * step;
|
|
453
|
+
}
|
|
454
|
+
event.preventDefault();
|
|
455
|
+
if (writeValue(state, next, lo, hi)) {
|
|
456
|
+
announce(host, modifiersOf(event), "input", "change");
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* A double click collapses the split, and the next one restores it.
|
|
462
|
+
*
|
|
463
|
+
* This is the SC 2.5.7 door rather than a convenience. It is deliberately the same function Enter
|
|
464
|
+
* runs, so the pointer alternative cannot drift from the keyboard one.
|
|
465
|
+
*
|
|
466
|
+
* @param state The element's reactive scope.
|
|
467
|
+
* @param event The double click.
|
|
468
|
+
*/
|
|
469
|
+
export function onSplitDoubleClick(state: SplitState, event: MouseEvent): void {
|
|
470
|
+
const host = hostOf(event);
|
|
471
|
+
if (!host || state.disabled === true) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const { hi, lo } = boundsOf(state, host);
|
|
475
|
+
if (writeValue(state, toggleTarget(state, lo, hi), lo, hi)) {
|
|
476
|
+
announce(host, modifiersOf(event), "input", "change");
|
|
477
|
+
}
|
|
478
|
+
}
|