@oscarpalmer/oui 0.20.0 → 0.22.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/popover.css +9 -9
- package/dist/css/tooltip.css +20 -20
- package/dist/js/focus-trap/embedded.mjs +1 -1
- package/dist/js/focus-trap/standalone.mjs +1 -1
- package/dist/js/index.d.mts +47 -4
- package/dist/js/index.mjs +2537 -5
- package/dist/js/{attributable.d.mts → internal/attributable.d.mts} +1 -1
- package/dist/js/{attributable.mjs → internal/attributable.mjs} +1 -1
- package/dist/js/internal/draggable.d.mts +2 -0
- package/dist/js/internal/draggable.mjs +215 -0
- package/dist/js/{floatable.d.mts → internal/floatable.d.mts} +3 -3
- package/dist/js/internal/floatable.mjs +187 -0
- package/dist/js/movable-BKFgoaOl.d.mts +101 -0
- package/dist/js/movable.d.mts +2 -0
- package/dist/js/movable.mjs +152 -0
- package/dist/js/popover.mjs +2 -2
- package/dist/js/sortable.d.mts +2 -0
- package/dist/js/sortable.mjs +243 -0
- package/dist/js/splitter.mjs +4 -4
- package/dist/js/tooltip.d.mts +2 -2
- package/dist/js/tooltip.mjs +98 -100
- package/package.json +28 -18
- package/src/css/mixins.scss +8 -12
- package/src/css/popover.scss +1 -1
- package/src/css/tooltip.scss +22 -22
- package/src/js/focus-trap/standalone.ts +1 -1
- package/src/js/index.ts +3 -1
- package/src/js/internal/draggable.ts +515 -0
- package/src/js/internal/floatable.ts +373 -0
- package/src/js/movable.ts +260 -0
- package/src/js/popover.ts +2 -2
- package/src/js/sortable.ts +467 -0
- package/src/js/splitter.ts +1 -4
- package/src/js/tooltip.ts +132 -132
- package/dist/js/floatable.mjs +0 -284
- package/src/js/floatable.ts +0 -590
- /package/src/js/{attributable.ts → internal/attributable.ts} +0 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import {on} from '@oscarpalmer/toretto/event';
|
|
2
|
+
import {findAncestor} from '@oscarpalmer/toretto/find';
|
|
3
|
+
|
|
4
|
+
export class Floatable {
|
|
5
|
+
active = false;
|
|
6
|
+
|
|
7
|
+
disabled = false;
|
|
8
|
+
|
|
9
|
+
frame: DOMHighResTimeStamp | undefined;
|
|
10
|
+
|
|
11
|
+
ignoreFocus = false;
|
|
12
|
+
|
|
13
|
+
constructor(readonly options: Options) {
|
|
14
|
+
initialize(this);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
destroy(): void {
|
|
18
|
+
stop(this);
|
|
19
|
+
|
|
20
|
+
const state = getState();
|
|
21
|
+
|
|
22
|
+
state?.active.delete(this);
|
|
23
|
+
state?.mapped.delete(this.options.anchor);
|
|
24
|
+
state?.mapped.delete(this.options.content);
|
|
25
|
+
|
|
26
|
+
this.options.anchor = undefined as never;
|
|
27
|
+
this.options.content = undefined as never;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
disable(): void {
|
|
31
|
+
if (this.disabled) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.disabled = true;
|
|
36
|
+
|
|
37
|
+
stop(this);
|
|
38
|
+
|
|
39
|
+
getState()?.active.delete(this);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
enable(): void {
|
|
43
|
+
if (!this.disabled) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.disabled = false;
|
|
48
|
+
|
|
49
|
+
initialize(this);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
toggle(active: boolean, ignoreFocus?: boolean): void {
|
|
53
|
+
if (this.disabled || this.active === active) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const state = getState();
|
|
58
|
+
|
|
59
|
+
if (state == null) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.ignoreFocus = !active && (ignoreFocus ?? false);
|
|
64
|
+
|
|
65
|
+
stop(this);
|
|
66
|
+
|
|
67
|
+
if (active) {
|
|
68
|
+
state.active.add(this);
|
|
69
|
+
} else {
|
|
70
|
+
state.active.delete(this);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (this.options.interactive) {
|
|
74
|
+
if (active) {
|
|
75
|
+
state.order.push(this);
|
|
76
|
+
} else {
|
|
77
|
+
const index = state.order.indexOf(this);
|
|
78
|
+
|
|
79
|
+
if (index > -1) {
|
|
80
|
+
state.order.splice(index, 1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (active) {
|
|
86
|
+
activate(this);
|
|
87
|
+
} else {
|
|
88
|
+
deactivate(this);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type FloatableWindow = Window & {
|
|
94
|
+
_oscarpalmer_oui_floatable: State;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
type Options = {
|
|
98
|
+
anchor: HTMLElement;
|
|
99
|
+
content: HTMLElement;
|
|
100
|
+
defaultPosition: Position;
|
|
101
|
+
interactive: boolean;
|
|
102
|
+
positionAttribute: string;
|
|
103
|
+
reusable: boolean;
|
|
104
|
+
onAfter?: (active: boolean) => void;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
type Position =
|
|
108
|
+
| 'above'
|
|
109
|
+
| 'above-end'
|
|
110
|
+
| 'above-start'
|
|
111
|
+
| 'below'
|
|
112
|
+
| 'below-end'
|
|
113
|
+
| 'below-start'
|
|
114
|
+
| 'end'
|
|
115
|
+
| 'end-bottom'
|
|
116
|
+
| 'end-top'
|
|
117
|
+
| 'start'
|
|
118
|
+
| 'start-bottom'
|
|
119
|
+
| 'start-top';
|
|
120
|
+
|
|
121
|
+
type State = {
|
|
122
|
+
active: Set<Floatable>;
|
|
123
|
+
mapped: Map<HTMLElement, Floatable>;
|
|
124
|
+
order: Floatable[];
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
//
|
|
128
|
+
|
|
129
|
+
function activate(floatable: Floatable): void {
|
|
130
|
+
floatable.active = true;
|
|
131
|
+
|
|
132
|
+
document.body.append(floatable.options.content);
|
|
133
|
+
|
|
134
|
+
floatable.options.content.hidden = false;
|
|
135
|
+
|
|
136
|
+
setPosition(floatable);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function closeAbove(element: HTMLElement): void {
|
|
140
|
+
const state = getState();
|
|
141
|
+
|
|
142
|
+
if (state == null) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const index = state.order.findIndex(
|
|
147
|
+
floatable => floatable.options.anchor === element || floatable.options.content === element,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
if (index === -1 || index === state.order.length - 1) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
while (index < state.order.length - 1) {
|
|
155
|
+
state.order.pop()?.toggle(false, true);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function closeAll(): void {
|
|
160
|
+
const state = getState();
|
|
161
|
+
|
|
162
|
+
const floatables = [...(state?.active ?? [])];
|
|
163
|
+
|
|
164
|
+
state?.active.clear();
|
|
165
|
+
|
|
166
|
+
for (const floatable of floatables) {
|
|
167
|
+
floatable.toggle(false, true);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function closeNonInteractive(state: State): void {
|
|
172
|
+
for (const floatable of state.active) {
|
|
173
|
+
if (!floatable.options.interactive) {
|
|
174
|
+
floatable.toggle(false);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function deactivate(floatable: Floatable): void {
|
|
180
|
+
floatable.active = false;
|
|
181
|
+
|
|
182
|
+
if (!floatable.options.reusable) {
|
|
183
|
+
floatable.options.content.hidden = true;
|
|
184
|
+
|
|
185
|
+
floatable.options.anchor.insertAdjacentElement(INSERT_AFTEREND, floatable.options.content);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
floatable.options.onAfter?.(false);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function getAnchorName(): string {
|
|
192
|
+
index += 1;
|
|
193
|
+
|
|
194
|
+
return `--oui-floatable-anchor-${index}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function getState(): State {
|
|
198
|
+
return (window as unknown as FloatableWindow)._oscarpalmer_oui_floatable;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function initialize(floatable: Floatable): void {
|
|
202
|
+
const {anchor, content, interactive} = floatable.options;
|
|
203
|
+
|
|
204
|
+
if (interactive) {
|
|
205
|
+
anchor.setAttribute(ATTRIBUTE_ANCHOR, '');
|
|
206
|
+
content.setAttribute(ATTRIBUTE_CONTENT, '');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const anchorName = getAnchorName();
|
|
210
|
+
|
|
211
|
+
anchor.style.anchorName = anchorName;
|
|
212
|
+
|
|
213
|
+
content.style.position = 'absolute';
|
|
214
|
+
content.style.positionAnchor = anchorName;
|
|
215
|
+
content.style.positionTry = 'normal flip-start';
|
|
216
|
+
|
|
217
|
+
const state = getState();
|
|
218
|
+
|
|
219
|
+
state?.mapped.set(anchor, floatable);
|
|
220
|
+
state?.mapped.set(content, floatable);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isDisabled(element: HTMLElement): boolean {
|
|
224
|
+
return (
|
|
225
|
+
(element as HTMLInputElement).disabled ||
|
|
226
|
+
(element as HTMLInputElement).readOnly ||
|
|
227
|
+
element.getAttribute('aria-disabled') === 'true'
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function onClick(event: PointerEvent): void {
|
|
232
|
+
const related = findAncestor(event, SELECTOR_ALL);
|
|
233
|
+
|
|
234
|
+
if (!(related instanceof HTMLElement)) {
|
|
235
|
+
closeAll();
|
|
236
|
+
} else if (related.hasAttribute(ATTRIBUTE_CONTENT)) {
|
|
237
|
+
closeAbove(related);
|
|
238
|
+
} else if (!isDisabled(related)) {
|
|
239
|
+
toggle(related);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function onKeyDown(event: KeyboardEvent): void {
|
|
244
|
+
if (event.key !== 'Escape') {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const state = getState();
|
|
249
|
+
|
|
250
|
+
if (state == null || state.active.size === 0) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
closeNonInteractive(state);
|
|
255
|
+
|
|
256
|
+
const related = findAncestor(event, SELECTOR_CONTENT);
|
|
257
|
+
|
|
258
|
+
if (!(related instanceof HTMLElement)) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
closeAbove(related);
|
|
263
|
+
|
|
264
|
+
const floatable = state.mapped.get(related);
|
|
265
|
+
|
|
266
|
+
if (floatable != null) {
|
|
267
|
+
floatable.toggle(false);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function setPosition(floatable: Floatable): void {
|
|
272
|
+
let position: Position =
|
|
273
|
+
(floatable.options.anchor.getAttribute(floatable.options.positionAttribute) as Position) ??
|
|
274
|
+
floatable.options.defaultPosition;
|
|
275
|
+
|
|
276
|
+
if (AREAS[position] == null) {
|
|
277
|
+
position = floatable.options.defaultPosition;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const area = AREAS[position];
|
|
281
|
+
|
|
282
|
+
floatable.options.content.style.positionArea = area;
|
|
283
|
+
|
|
284
|
+
floatable.options.content.setAttribute(ATTRIBUTE_POSITION, POSITIONS[area]);
|
|
285
|
+
|
|
286
|
+
floatable.options.onAfter?.(true);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function stop(floatable: Floatable): void {
|
|
290
|
+
if (floatable.frame != null) {
|
|
291
|
+
cancelAnimationFrame(floatable.frame);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
floatable.frame = undefined;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function toggle(anchor: HTMLElement): void {
|
|
298
|
+
const floatable = getState()?.mapped.get(anchor);
|
|
299
|
+
|
|
300
|
+
if (floatable == null) {
|
|
301
|
+
closeAll();
|
|
302
|
+
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const active = Boolean(floatable.active);
|
|
307
|
+
const content = findAncestor(floatable.options.anchor, SELECTOR_CONTENT);
|
|
308
|
+
|
|
309
|
+
if (content == null) {
|
|
310
|
+
closeAll();
|
|
311
|
+
} else {
|
|
312
|
+
closeAbove(content as HTMLElement);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
floatable.toggle(!active);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
//
|
|
319
|
+
|
|
320
|
+
const AREAS: Record<Position, string> = {
|
|
321
|
+
above: 'block-start',
|
|
322
|
+
'above-end': 'start span-start',
|
|
323
|
+
'above-start': 'start span-end',
|
|
324
|
+
below: 'block-end',
|
|
325
|
+
'below-end': 'end span-start',
|
|
326
|
+
'below-start': 'end span-end',
|
|
327
|
+
end: 'center end',
|
|
328
|
+
'end-bottom': 'span-start end',
|
|
329
|
+
'end-top': 'span-end end',
|
|
330
|
+
start: 'center start',
|
|
331
|
+
'start-bottom': 'span-start start',
|
|
332
|
+
'start-top': 'span-end start',
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const ATTRIBUTE_ANCHOR = 'oui-floatable-anchor';
|
|
336
|
+
|
|
337
|
+
const ATTRIBUTE_CONTENT = 'oui-floatable-content';
|
|
338
|
+
|
|
339
|
+
const ATTRIBUTE_POSITION = 'oui-position';
|
|
340
|
+
|
|
341
|
+
const INSERT_AFTEREND = 'afterend';
|
|
342
|
+
|
|
343
|
+
const POSITIONS = Object.fromEntries(
|
|
344
|
+
Object.entries(AREAS).map(([position, area]) => [area, position]),
|
|
345
|
+
) as Record<string, Position>;
|
|
346
|
+
|
|
347
|
+
const SELECTOR_ALL = `[${ATTRIBUTE_ANCHOR}], [${ATTRIBUTE_CONTENT}]`;
|
|
348
|
+
|
|
349
|
+
const SELECTOR_CONTENT = `[${ATTRIBUTE_CONTENT}]`;
|
|
350
|
+
|
|
351
|
+
let index = 0;
|
|
352
|
+
|
|
353
|
+
//
|
|
354
|
+
|
|
355
|
+
if ((window as unknown as FloatableWindow)._oscarpalmer_oui_floatable == null) {
|
|
356
|
+
Object.defineProperty(window, '_oscarpalmer_oui_floatable', {
|
|
357
|
+
enumerable: false,
|
|
358
|
+
value: {
|
|
359
|
+
active: new Set(),
|
|
360
|
+
mapped: new Map(),
|
|
361
|
+
order: [],
|
|
362
|
+
},
|
|
363
|
+
writable: false,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
on(document, 'keydown', event => {
|
|
367
|
+
onKeyDown(event);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
on(document, 'click', event => {
|
|
371
|
+
onClick(event);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import type {EventPosition, PlainObject} from '@oscarpalmer/atoms/models';
|
|
2
|
+
import {getPosition, on} from '@oscarpalmer/toretto/event';
|
|
3
|
+
import {attributable} from './internal/attributable';
|
|
4
|
+
import {
|
|
5
|
+
addDraggable,
|
|
6
|
+
Draggable,
|
|
7
|
+
removeDraggable,
|
|
8
|
+
type DraggableBound,
|
|
9
|
+
type DraggableState,
|
|
10
|
+
type DragMovePosition,
|
|
11
|
+
} from './internal/draggable';
|
|
12
|
+
|
|
13
|
+
// #region Types
|
|
14
|
+
|
|
15
|
+
export class Movable extends Draggable {
|
|
16
|
+
inset: string;
|
|
17
|
+
|
|
18
|
+
moved = false;
|
|
19
|
+
|
|
20
|
+
offset: Partial<EventPosition> | undefined;
|
|
21
|
+
|
|
22
|
+
position: string;
|
|
23
|
+
|
|
24
|
+
get dragged(): HTMLElement {
|
|
25
|
+
return this.element;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get origin(): HTMLElement | undefined {
|
|
29
|
+
return this.element;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
constructor(element: HTMLElement) {
|
|
33
|
+
super('move', element, {
|
|
34
|
+
container: {
|
|
35
|
+
attribute: ATTRIBUTE_CONTAINER,
|
|
36
|
+
map: CONTAINERS,
|
|
37
|
+
onDestroy: destroyContainer,
|
|
38
|
+
onInitialize: initializeContainer,
|
|
39
|
+
},
|
|
40
|
+
direction: {
|
|
41
|
+
attribute: ATTRIBUTE_DIRECTION,
|
|
42
|
+
onAfter: afterDirection,
|
|
43
|
+
},
|
|
44
|
+
drag: {
|
|
45
|
+
onBegin,
|
|
46
|
+
onCancel,
|
|
47
|
+
onEnd,
|
|
48
|
+
onMove,
|
|
49
|
+
},
|
|
50
|
+
position: {
|
|
51
|
+
getOffset: getOffsetPosition,
|
|
52
|
+
getOriginal: getOriginalPosition,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
this.hasHandles = element.querySelector(SELECTOR_HANDLE) != null;
|
|
57
|
+
|
|
58
|
+
this.inset = element.style.inset;
|
|
59
|
+
this.position = element.style.position;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
override destroy(): void {
|
|
63
|
+
super.destroy();
|
|
64
|
+
|
|
65
|
+
MOVED.delete(this);
|
|
66
|
+
|
|
67
|
+
this.element.style.inset = this.inset;
|
|
68
|
+
this.element.style.position = this.position;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// #endregion
|
|
73
|
+
|
|
74
|
+
// #region Functions
|
|
75
|
+
|
|
76
|
+
function afterDirection(draggable: Draggable): void {
|
|
77
|
+
const container = draggable.container?.getBoundingClientRect();
|
|
78
|
+
|
|
79
|
+
if (container != null) {
|
|
80
|
+
setOffset(draggable as Movable, container);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function destroyContainer(_: Draggable, container: HTMLElement, empty: boolean): void {
|
|
85
|
+
if (empty) {
|
|
86
|
+
resizer.unobserve(container);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getOffsetPosition(
|
|
91
|
+
state: DraggableState,
|
|
92
|
+
_: Draggable,
|
|
93
|
+
__: HTMLElement,
|
|
94
|
+
position: EventPosition,
|
|
95
|
+
): EventPosition {
|
|
96
|
+
return {
|
|
97
|
+
x: position.x - (state.element?.rectangle.left ?? 0),
|
|
98
|
+
y: position.y - (state.element?.rectangle.top ?? 0),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getOriginalPosition(state: DraggableState): EventPosition {
|
|
103
|
+
return {
|
|
104
|
+
x: state.element?.rectangle.left ?? 0,
|
|
105
|
+
y: state.element?.rectangle.top ?? 0,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function initializeContainer(draggable: Draggable): void {
|
|
110
|
+
resizer.observe(draggable.container!);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function onBegin(): void {}
|
|
114
|
+
|
|
115
|
+
function onCancel(state: DraggableState, draggable: Draggable): void {
|
|
116
|
+
const movable = draggable as Movable;
|
|
117
|
+
|
|
118
|
+
const {element, inset, moved, position} = movable;
|
|
119
|
+
const {x, y} = state.original ?? {x: 0, y: 0};
|
|
120
|
+
|
|
121
|
+
if (moved) {
|
|
122
|
+
element.style.inset = `${y}px auto auto ${x}px`;
|
|
123
|
+
} else {
|
|
124
|
+
element.style.position = position;
|
|
125
|
+
element.style.inset = inset;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function onEnd(
|
|
130
|
+
event: MouseEvent | TouchEvent,
|
|
131
|
+
state: DraggableState,
|
|
132
|
+
draggable: Draggable,
|
|
133
|
+
reset: () => void,
|
|
134
|
+
): void {
|
|
135
|
+
const movable = draggable as Movable;
|
|
136
|
+
|
|
137
|
+
if (!movable.moved) {
|
|
138
|
+
movable.moved = true;
|
|
139
|
+
|
|
140
|
+
if (movable.container != null) {
|
|
141
|
+
MOVED.add(movable);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const container = movable.container?.getBoundingClientRect();
|
|
146
|
+
|
|
147
|
+
if (container != null) {
|
|
148
|
+
setOffset(movable, container);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const end = new CustomEvent(EVENT_END, {
|
|
152
|
+
detail: {
|
|
153
|
+
from: {...state.original},
|
|
154
|
+
to: getPosition(event),
|
|
155
|
+
},
|
|
156
|
+
cancelable: true,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
movable.element.dispatchEvent(end);
|
|
160
|
+
|
|
161
|
+
setTimeout(() => {
|
|
162
|
+
if (end.defaultPrevented) {
|
|
163
|
+
onCancel(state, draggable);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
reset();
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function onMove(
|
|
171
|
+
_: MouseEvent | TouchEvent,
|
|
172
|
+
state: DraggableState,
|
|
173
|
+
__: Draggable,
|
|
174
|
+
position: DragMovePosition,
|
|
175
|
+
): PlainObject | undefined {
|
|
176
|
+
return {
|
|
177
|
+
from: {...state.original},
|
|
178
|
+
to: {...position.calculated},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function onResize(entries: ResizeObserverEntry[]): void {
|
|
183
|
+
const {length} = entries;
|
|
184
|
+
|
|
185
|
+
for (let index = 0; index < length; index += 1) {
|
|
186
|
+
const entry = entries[index];
|
|
187
|
+
const set = CONTAINERS.get(entry.target as HTMLElement);
|
|
188
|
+
|
|
189
|
+
if (set == null) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for (const movable of set) {
|
|
194
|
+
reposition(movable);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function onScroll(): void {
|
|
200
|
+
for (const moved of MOVED) {
|
|
201
|
+
reposition(moved);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function reposition(movable: Movable): void {
|
|
206
|
+
if (!movable.moved || movable.container == null) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const container = movable.container.getBoundingClientRect();
|
|
211
|
+
|
|
212
|
+
const x = container.left + (movable.offset?.x ?? 0);
|
|
213
|
+
const y = container.top + (movable.offset?.y ?? 0);
|
|
214
|
+
|
|
215
|
+
movable.element.style.inset = `${y}px auto auto ${x}px`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function setOffset(movable: Movable, container: DOMRect): void {
|
|
219
|
+
movable.offset = {};
|
|
220
|
+
|
|
221
|
+
const element = movable.element.getBoundingClientRect();
|
|
222
|
+
|
|
223
|
+
movable.offset.y = element.top - container.top;
|
|
224
|
+
movable.offset.x = element.left - container.left;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// #endregion
|
|
228
|
+
|
|
229
|
+
// #region Variables
|
|
230
|
+
|
|
231
|
+
const ATTRIBUTE = 'oui-movable';
|
|
232
|
+
|
|
233
|
+
const ATTRIBUTE_CONTAINER = `${ATTRIBUTE}-container`;
|
|
234
|
+
|
|
235
|
+
const ATTRIBUTE_DIRECTION = `${ATTRIBUTE}-direction`;
|
|
236
|
+
|
|
237
|
+
const CONTAINERS = new Map<HTMLElement, Set<Movable>>();
|
|
238
|
+
|
|
239
|
+
const EVENT_END = 'move:end';
|
|
240
|
+
|
|
241
|
+
const MOVED = new Set<Movable>();
|
|
242
|
+
|
|
243
|
+
const SELECTOR_HANDLE = `[${ATTRIBUTE}-handle]`;
|
|
244
|
+
|
|
245
|
+
const bound: DraggableBound = {
|
|
246
|
+
constructor: Movable,
|
|
247
|
+
property: 'movable',
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const resizer = new ResizeObserver(onResize);
|
|
251
|
+
|
|
252
|
+
// #endregion
|
|
253
|
+
|
|
254
|
+
// #region Initialization
|
|
255
|
+
|
|
256
|
+
attributable(ATTRIBUTE, addDraggable.bind(bound), removeDraggable.bind(bound));
|
|
257
|
+
|
|
258
|
+
on(document, 'scroll', onScroll);
|
|
259
|
+
|
|
260
|
+
// #endregion
|
package/src/js/popover.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {isNullableOrWhitespace} from '@oscarpalmer/atoms/is';
|
|
2
2
|
import {getFocusable} from '@oscarpalmer/toretto/focusable';
|
|
3
|
-
import {Floatable} from './floatable';
|
|
3
|
+
import {Floatable} from './internal/floatable';
|
|
4
4
|
import {createFocusTrap, type FocusTrap} from './focus-trap/embedded';
|
|
5
5
|
|
|
6
6
|
declare global {
|
|
@@ -52,7 +52,7 @@ export class OuiPopoverElement extends HTMLElement {
|
|
|
52
52
|
defaultPosition: 'below-start',
|
|
53
53
|
interactive: true,
|
|
54
54
|
positionAttribute: 'position',
|
|
55
|
-
|
|
55
|
+
reusable: false,
|
|
56
56
|
onAfter: (active: boolean): void => {
|
|
57
57
|
if (active) {
|
|
58
58
|
focus(content);
|