@oscarpalmer/oui 0.15.0 → 0.17.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/dist/css/button.css +0 -3
- package/dist/css/popover.css +3 -0
- package/dist/css/splitter.css +78 -0
- package/dist/js/floatable.cjs +77 -35
- package/dist/js/floatable.js +77 -35
- package/dist/js/popover.cjs +8 -2
- package/dist/js/popover.js +8 -2
- package/dist/js/splitter.cjs +272 -0
- package/dist/js/splitter.js +272 -0
- package/dist/js/tooltip.cjs +7 -1
- package/dist/js/tooltip.js +7 -1
- package/package.json +22 -2
- package/src/css/button.scss +0 -3
- package/src/css/popover.scss +4 -0
- package/src/css/splitter.scss +87 -0
- package/src/js/floatable.ts +124 -37
- package/src/js/popover.ts +9 -2
- package/src/js/splitter.ts +407 -0
- package/src/js/tooltip.ts +8 -1
- package/types/floatable.d.cts +5 -1
- package/types/floatable.d.ts +6 -1
- package/types/splitter.d.cts +10 -0
- package/types/splitter.d.ts +11 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import {isNullableOrWhitespace} from '@oscarpalmer/atoms/is';
|
|
2
|
+
import {round} from '@oscarpalmer/atoms/math';
|
|
3
|
+
import {clamp} from '@oscarpalmer/atoms/number';
|
|
4
|
+
import {getPosition, on} from '@oscarpalmer/toretto/event';
|
|
5
|
+
import {findAncestor} from '@oscarpalmer/toretto/find';
|
|
6
|
+
|
|
7
|
+
declare global {
|
|
8
|
+
interface HTMLElementTagNameMap {
|
|
9
|
+
'oui-splitter': OuiSplitterElement;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class OuiSplitterElement extends HTMLElement {
|
|
14
|
+
#splitter: Splitter;
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
super();
|
|
18
|
+
|
|
19
|
+
const panels = [...this.children].filter(
|
|
20
|
+
child => child instanceof HTMLElement,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
if (panels.length !== 2) {
|
|
24
|
+
throw new Error('oui-splitter must have exactly two panels.');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.#splitter = new Splitter(this, panels);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
connectedCallback(): void {
|
|
31
|
+
if (this.#splitter.types.auto) {
|
|
32
|
+
observer.observe(this);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
disconnectedCallback(): void {
|
|
37
|
+
if (this.#splitter.types.auto) {
|
|
38
|
+
observer.unobserve(this);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let index = 0;
|
|
44
|
+
|
|
45
|
+
class Splitter {
|
|
46
|
+
handle: HTMLSpanElement;
|
|
47
|
+
|
|
48
|
+
rectangle: DOMRect;
|
|
49
|
+
|
|
50
|
+
separator: HTMLDivElement;
|
|
51
|
+
|
|
52
|
+
types: Types;
|
|
53
|
+
|
|
54
|
+
values: Values;
|
|
55
|
+
|
|
56
|
+
constructor(
|
|
57
|
+
public element: OuiSplitterElement,
|
|
58
|
+
public panels: HTMLElement[],
|
|
59
|
+
) {
|
|
60
|
+
if (isNullableOrWhitespace(element.id)) {
|
|
61
|
+
element.id = `oui_splitter_${++index}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.values = getValues(element);
|
|
65
|
+
|
|
66
|
+
this.handle = createHandle();
|
|
67
|
+
this.rectangle = element.getBoundingClientRect();
|
|
68
|
+
this.separator = createSeparator(this);
|
|
69
|
+
|
|
70
|
+
this.separator.append(this.handle);
|
|
71
|
+
|
|
72
|
+
panels[0].insertAdjacentElement('afterend', this.separator);
|
|
73
|
+
|
|
74
|
+
mapped.set(element, this);
|
|
75
|
+
mapped.set(this.handle, this);
|
|
76
|
+
|
|
77
|
+
setSize(this, this.values.now.current);
|
|
78
|
+
|
|
79
|
+
this.types = getTypes(this);
|
|
80
|
+
|
|
81
|
+
if (this.types.horizontal) {
|
|
82
|
+
this.element.setAttribute('oui-splitter-horizontal', '');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
setAriaOrientation(this);
|
|
86
|
+
setAriaValue(this);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
type Types = {
|
|
91
|
+
auto: boolean;
|
|
92
|
+
horizontal: boolean;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
type Values = {
|
|
96
|
+
max: number;
|
|
97
|
+
min: number;
|
|
98
|
+
now: ValuesNow;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
type ValuesNow = {
|
|
102
|
+
current: number;
|
|
103
|
+
previous: number;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
//
|
|
107
|
+
|
|
108
|
+
function createHandle(): HTMLSpanElement {
|
|
109
|
+
const handle = document.createElement('span');
|
|
110
|
+
|
|
111
|
+
handle.setAttribute('aria-hidden', 'true');
|
|
112
|
+
handle.setAttribute('oui-splitter-handle', '');
|
|
113
|
+
|
|
114
|
+
return handle;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function createSeparator(splitter: Splitter): HTMLDivElement {
|
|
118
|
+
const separator = document.createElement('div');
|
|
119
|
+
|
|
120
|
+
separator.role = 'separator';
|
|
121
|
+
separator.tabIndex = 0;
|
|
122
|
+
|
|
123
|
+
separator.setAttribute('aria-controls', splitter.element.id);
|
|
124
|
+
separator.setAttribute('aria-label', 'Resize panels');
|
|
125
|
+
separator.setAttribute('oui-splitter-separator', '');
|
|
126
|
+
|
|
127
|
+
return separator;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getContained(min: number, value: number, max: number): number {
|
|
131
|
+
return round(clamp(value, min, max), 6);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getSize(splitter: Splitter, value: number): number {
|
|
135
|
+
const {height, left, top, width} = splitter.rectangle;
|
|
136
|
+
|
|
137
|
+
const offset = value - (splitter.types.horizontal ? top : left);
|
|
138
|
+
|
|
139
|
+
const fraction = offset / (splitter.types.horizontal ? height : width);
|
|
140
|
+
|
|
141
|
+
return getContained(splitter.values.min, fraction, splitter.values.max);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getTypes(splitter: Splitter, width?: number, height?: number): Types {
|
|
145
|
+
const type = splitter.element.getAttribute('type')?.trim() ?? '';
|
|
146
|
+
|
|
147
|
+
if (type === 'horizontal') {
|
|
148
|
+
return {
|
|
149
|
+
auto: false,
|
|
150
|
+
horizontal: true,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (type === 'auto') {
|
|
155
|
+
return {
|
|
156
|
+
auto: true,
|
|
157
|
+
horizontal:
|
|
158
|
+
(height ?? splitter.rectangle.height) >
|
|
159
|
+
(width ?? splitter.rectangle.width),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
auto: false,
|
|
165
|
+
horizontal: false,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function getValue(
|
|
170
|
+
element: HTMLElement,
|
|
171
|
+
name: string,
|
|
172
|
+
defaultValue: number,
|
|
173
|
+
): number {
|
|
174
|
+
const value = element.getAttribute(name);
|
|
175
|
+
|
|
176
|
+
if (isNullableOrWhitespace(value)) {
|
|
177
|
+
return defaultValue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const parsed = Number.parseFloat(value);
|
|
181
|
+
|
|
182
|
+
if (Number.isNaN(parsed)) {
|
|
183
|
+
return defaultValue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return parsed < 0 ? defaultValue : parsed > 1 ? parsed / 100 : parsed;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function getValues(element: OuiSplitterElement): Values {
|
|
190
|
+
const values: Values = {
|
|
191
|
+
max: getValue(element, 'max', 0.9),
|
|
192
|
+
min: getValue(element, 'min', 0.1),
|
|
193
|
+
now: {
|
|
194
|
+
current: 0.5,
|
|
195
|
+
previous: 0.5,
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const value = getContained(
|
|
200
|
+
values.min,
|
|
201
|
+
getValue(element, 'size', values.now.current),
|
|
202
|
+
values.max,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
values.now.current = value;
|
|
206
|
+
values.now.previous = value;
|
|
207
|
+
|
|
208
|
+
return values;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function onEnd(reset: boolean): void {
|
|
212
|
+
if (splitter != null) {
|
|
213
|
+
splitter.handle.removeAttribute('oui-splitter-active');
|
|
214
|
+
splitter.separator.removeAttribute('oui-splitter-active');
|
|
215
|
+
|
|
216
|
+
if (reset) {
|
|
217
|
+
setSize(splitter, splitter.values.now.previous);
|
|
218
|
+
} else {
|
|
219
|
+
splitter.values.now.previous = splitter.values.now.current;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
splitter = undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function onKeydown(event: KeyboardEvent): void {
|
|
227
|
+
switch (true) {
|
|
228
|
+
case allKeys.has(event.key):
|
|
229
|
+
onNavigate(event);
|
|
230
|
+
break;
|
|
231
|
+
|
|
232
|
+
case event.key === 'Escape':
|
|
233
|
+
onEnd(true);
|
|
234
|
+
break;
|
|
235
|
+
|
|
236
|
+
default:
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function onMousedown(event: Event): void {
|
|
242
|
+
splitter = mapped.get(
|
|
243
|
+
findAncestor(
|
|
244
|
+
event.target as never,
|
|
245
|
+
'[oui-splitter-handle]',
|
|
246
|
+
) as HTMLSpanElement,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
if (splitter == null) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
splitter.handle.setAttribute('oui-splitter-active', '');
|
|
254
|
+
splitter.separator.setAttribute('oui-splitter-active', '');
|
|
255
|
+
|
|
256
|
+
splitter.rectangle = splitter.element.getBoundingClientRect();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function onMousemove(event: MouseEvent): void {
|
|
260
|
+
if (splitter == null) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const position = getPosition(event);
|
|
265
|
+
|
|
266
|
+
if (position != null) {
|
|
267
|
+
splitter.values.now.current = getSize(
|
|
268
|
+
splitter,
|
|
269
|
+
splitter.types.horizontal ? position.y : position.x,
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
setSize(splitter, splitter.values.now.current);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function onMouseup(): void {
|
|
277
|
+
if (splitter != null) {
|
|
278
|
+
onEnd(false);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function onNavigate(event: KeyboardEvent): void {
|
|
283
|
+
const splitter = mapped.get(
|
|
284
|
+
findAncestor(
|
|
285
|
+
event.target as never,
|
|
286
|
+
'[oui-splitter-handle]',
|
|
287
|
+
) as HTMLSpanElement,
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
if (splitter == null) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (absoluteKeys.has(event.key)) {
|
|
295
|
+
setSize(
|
|
296
|
+
splitter,
|
|
297
|
+
event.key === 'End' ? splitter.values.max : splitter.values.min,
|
|
298
|
+
true,
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (
|
|
305
|
+
(splitter.types.horizontal && !horizontalKeys.has(event.key)) ||
|
|
306
|
+
(!splitter.types.horizontal && !verticalKeys.has(event.key))
|
|
307
|
+
) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const modifier = negativeKeys.has(event.key) ? -1 : 1;
|
|
312
|
+
|
|
313
|
+
setSize(splitter, splitter.values.now.current + modifier * 0.05, true);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function onObservation(entries: ResizeObserverEntry[]): void {
|
|
317
|
+
if (frame != null) {
|
|
318
|
+
cancelAnimationFrame(frame);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
frame = requestAnimationFrame(() => {
|
|
322
|
+
for (const entry of entries) {
|
|
323
|
+
const splitter = mapped.get(entry.target as HTMLElement);
|
|
324
|
+
|
|
325
|
+
if (splitter == null) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
splitter.types = getTypes(
|
|
330
|
+
splitter,
|
|
331
|
+
entry.contentRect.width,
|
|
332
|
+
entry.contentRect.height,
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
if (splitter.types.horizontal) {
|
|
336
|
+
splitter.element.setAttribute('oui-splitter-horizontal', '');
|
|
337
|
+
} else {
|
|
338
|
+
splitter.element.removeAttribute('oui-splitter-horizontal');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
setAriaOrientation(splitter);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
frame = undefined;
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function setAriaOrientation(splitter: Splitter): void {
|
|
349
|
+
splitter.separator.setAttribute(
|
|
350
|
+
'aria-orientation',
|
|
351
|
+
splitter.types.horizontal ? 'horizontal' : 'vertical',
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function setAriaValue(splitter: Splitter): void {
|
|
356
|
+
splitter.separator.setAttribute('aria-valuemax', String(splitter.values.max));
|
|
357
|
+
splitter.separator.setAttribute('aria-valuemin', String(splitter.values.min));
|
|
358
|
+
|
|
359
|
+
splitter.separator.setAttribute(
|
|
360
|
+
'aria-valuenow',
|
|
361
|
+
String(splitter.values.now.current),
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function setSize(splitter: Splitter, size: number, previous?: boolean): void {
|
|
366
|
+
splitter.values.now.current = size;
|
|
367
|
+
|
|
368
|
+
if (previous) {
|
|
369
|
+
splitter.values.now.previous = size;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
splitter.element.style.setProperty('--oui-splitter-size', `${size}`);
|
|
373
|
+
|
|
374
|
+
setAriaValue(splitter);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
//
|
|
378
|
+
|
|
379
|
+
const absoluteKeys = new Set(['End', 'Home']);
|
|
380
|
+
|
|
381
|
+
const horizontalKeys = new Set(['ArrowDown', 'ArrowUp']);
|
|
382
|
+
|
|
383
|
+
const mapped = new WeakMap<HTMLElement, Splitter>();
|
|
384
|
+
|
|
385
|
+
const observer = new ResizeObserver(onObservation);
|
|
386
|
+
|
|
387
|
+
const negativeKeys = new Set(['ArrowLeft', 'ArrowUp']);
|
|
388
|
+
|
|
389
|
+
const verticalKeys = new Set(['ArrowLeft', 'ArrowRight']);
|
|
390
|
+
|
|
391
|
+
const allKeys = new Set([...absoluteKeys, ...horizontalKeys, ...verticalKeys]);
|
|
392
|
+
|
|
393
|
+
let frame: DOMHighResTimeStamp | undefined;
|
|
394
|
+
|
|
395
|
+
let splitter: Splitter | undefined;
|
|
396
|
+
|
|
397
|
+
//
|
|
398
|
+
|
|
399
|
+
customElements.define('oui-splitter', OuiSplitterElement);
|
|
400
|
+
|
|
401
|
+
on(document, 'keydown', onKeydown);
|
|
402
|
+
|
|
403
|
+
on(document, 'mousedown', onMousedown);
|
|
404
|
+
|
|
405
|
+
on(document, 'mousemove', onMousemove);
|
|
406
|
+
|
|
407
|
+
on(document, 'mouseup', onMouseup);
|
package/src/js/tooltip.ts
CHANGED
|
@@ -20,7 +20,14 @@ class Tooltip {
|
|
|
20
20
|
|
|
21
21
|
constructor(anchor: HTMLElement, content: Content) {
|
|
22
22
|
this.attributes = content.attributes;
|
|
23
|
-
|
|
23
|
+
|
|
24
|
+
this.floatable = new Floatable(
|
|
25
|
+
anchor,
|
|
26
|
+
content.element,
|
|
27
|
+
`${selector}-position`,
|
|
28
|
+
'vertical',
|
|
29
|
+
true,
|
|
30
|
+
);
|
|
24
31
|
|
|
25
32
|
initialize(this);
|
|
26
33
|
}
|
package/types/floatable.d.cts
CHANGED
|
@@ -3,13 +3,17 @@
|
|
|
3
3
|
export declare class Floatable {
|
|
4
4
|
anchor: HTMLElement;
|
|
5
5
|
content: HTMLElement;
|
|
6
|
+
readonly attribute: string;
|
|
7
|
+
readonly position: Position;
|
|
8
|
+
readonly preferAbove: boolean;
|
|
6
9
|
activate: number | undefined;
|
|
7
10
|
active: boolean;
|
|
8
11
|
deactivate: number | undefined;
|
|
9
12
|
frame: DOMHighResTimeStamp | undefined;
|
|
10
|
-
constructor(anchor: HTMLElement, content: HTMLElement);
|
|
13
|
+
constructor(anchor: HTMLElement, content: HTMLElement, attribute: string, position: Position, preferAbove: boolean);
|
|
11
14
|
destroy(): void;
|
|
12
15
|
}
|
|
16
|
+
export type Position = "above" | "above-end" | "above-start" | "below" | "below-end" | "below-start" | "end" | "end-bottom" | "end-top" | "horizontal" | "horizontal-bottom" | "horizontal-top" | "start" | "start-bottom" | "start-top" | "vertical" | "vertical-end" | "vertical-start";
|
|
13
17
|
export declare function activate(floatable: Floatable, active: Set<Floatable>, order?: Floatable[], time?: number): void;
|
|
14
18
|
export declare function deactivate(floatable: Floatable, active: Set<Floatable>, order?: Floatable[], time?: number): void;
|
|
15
19
|
|
package/types/floatable.d.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
export declare class Floatable {
|
|
2
2
|
anchor: HTMLElement;
|
|
3
3
|
content: HTMLElement;
|
|
4
|
+
readonly attribute: string;
|
|
5
|
+
readonly position: Position;
|
|
6
|
+
readonly preferAbove: boolean;
|
|
4
7
|
activate: number | undefined;
|
|
5
8
|
active: boolean;
|
|
6
9
|
deactivate: number | undefined;
|
|
7
10
|
frame: DOMHighResTimeStamp | undefined;
|
|
8
|
-
constructor(anchor: HTMLElement, content: HTMLElement);
|
|
11
|
+
constructor(anchor: HTMLElement, content: HTMLElement, attribute: string, position: Position, preferAbove: boolean);
|
|
9
12
|
destroy(): void;
|
|
10
13
|
}
|
|
14
|
+
type Position = 'above' | 'above-end' | 'above-start' | 'below' | 'below-end' | 'below-start' | 'end' | 'end-bottom' | 'end-top' | 'horizontal' | 'horizontal-bottom' | 'horizontal-top' | 'start' | 'start-bottom' | 'start-top' | 'vertical' | 'vertical-end' | 'vertical-start';
|
|
11
15
|
export declare function activate(floatable: Floatable, active: Set<Floatable>, order?: Floatable[], time?: number): void;
|
|
12
16
|
export declare function deactivate(floatable: Floatable, active: Set<Floatable>, order?: Floatable[], time?: number): void;
|
|
17
|
+
export {};
|