@oscarpalmer/oui 0.19.0 → 0.21.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.
Files changed (63) hide show
  1. package/dist/css/a11y.css +0 -1
  2. package/dist/css/form.css +0 -1
  3. package/dist/css/popover.css +22 -0
  4. package/dist/css/reset.css +8 -8
  5. package/dist/css/table.css +1 -1
  6. package/dist/css/tooltip.css +43 -0
  7. package/dist/js/focus-trap/embedded.d.mts +26 -0
  8. package/dist/js/focus-trap/{embedded.js → embedded.mjs} +11 -9
  9. package/dist/js/focus-trap/standalone.d.mts +1 -0
  10. package/dist/js/focus-trap/{standalone.js → standalone.mjs} +5 -2
  11. package/dist/js/index.d.mts +4 -0
  12. package/dist/js/index.mjs +7 -0
  13. package/dist/js/internal/attributable.d.mts +4 -0
  14. package/dist/js/{attributable.full.js → internal/attributable.mjs} +8 -1
  15. package/dist/js/internal/draggable.d.mts +2 -0
  16. package/dist/js/internal/draggable.mjs +212 -0
  17. package/dist/js/internal/floatable.d.mts +26 -0
  18. package/dist/js/{floatable.js → internal/floatable.mjs} +73 -50
  19. package/dist/js/movable-BEo2LbUI.d.mts +101 -0
  20. package/dist/js/movable.d.mts +2 -0
  21. package/dist/js/movable.mjs +152 -0
  22. package/dist/js/popover.d.mts +20 -0
  23. package/dist/js/{popover.js → popover.mjs} +10 -8
  24. package/dist/js/sortable.d.mts +2 -0
  25. package/dist/js/sortable.mjs +243 -0
  26. package/dist/js/splitter.d.mts +25 -0
  27. package/dist/js/{splitter.js → splitter.mjs} +62 -60
  28. package/dist/js/tooltip.d.mts +4 -0
  29. package/dist/js/{tooltip.js → tooltip.mjs} +22 -20
  30. package/package.json +66 -69
  31. package/src/css/mixins.scss +30 -1
  32. package/src/css/popover.scss +6 -1
  33. package/src/css/reset.scss +4 -4
  34. package/src/css/table.scss +1 -1
  35. package/src/css/tooltip.scss +55 -1
  36. package/src/js/focus-trap/embedded.ts +4 -10
  37. package/src/js/focus-trap/standalone.ts +1 -1
  38. package/src/js/index.ts +3 -1
  39. package/src/js/{attributable.ts → internal/attributable.ts} +11 -1
  40. package/src/js/internal/draggable.ts +515 -0
  41. package/src/js/{floatable.ts → internal/floatable.ts} +141 -63
  42. package/src/js/movable.ts +260 -0
  43. package/src/js/popover.ts +1 -1
  44. package/src/js/sortable.ts +467 -0
  45. package/src/js/splitter.ts +3 -9
  46. package/src/js/tooltip.ts +11 -9
  47. package/dist/js/attributable.js +0 -36
  48. package/dist/js/floatable.full.js +0 -398
  49. package/dist/js/focus-trap/embedded.full.js +0 -408
  50. package/dist/js/focus-trap/standalone.full.js +0 -447
  51. package/dist/js/index.js +0 -5
  52. package/dist/js/oui.full.js +0 -1573
  53. package/dist/js/popover.full.js +0 -760
  54. package/dist/js/splitter.full.js +0 -769
  55. package/dist/js/tooltip.full.js +0 -613
  56. package/types/attributable.d.ts +0 -1
  57. package/types/floatable.d.ts +0 -24
  58. package/types/focus-trap/embedded.d.ts +0 -24
  59. package/types/focus-trap/standalone.d.ts +0 -1
  60. package/types/index.d.ts +0 -4
  61. package/types/popover.d.ts +0 -17
  62. package/types/splitter.d.ts +0 -23
  63. package/types/tooltip.d.ts +0 -2
@@ -91,7 +91,6 @@ export class Floatable {
91
91
  }
92
92
 
93
93
  type FloatableWindow = Window & {
94
- // biome-ignore lint/style/useNamingConvention: Non-standard name to avoid conflicts
95
94
  _oscarpalmer_oui_floatable: State;
96
95
  };
97
96
 
@@ -159,7 +158,7 @@ function closeAbove(element: HTMLElement): void {
159
158
  }
160
159
 
161
160
  while (index < state.order.length - 1) {
162
- void state.order.pop()?.toggle(false, true);
161
+ state.order.pop()?.toggle(false, true);
163
162
  }
164
163
  }
165
164
 
@@ -171,14 +170,14 @@ function closeAll(): void {
171
170
  state?.active.clear();
172
171
 
173
172
  for (const floatable of floatables) {
174
- void floatable.toggle(false, true);
173
+ floatable.toggle(false, true);
175
174
  }
176
175
  }
177
176
 
178
177
  function closeNonInteractive(state: State): void {
179
178
  for (const floatable of state.active) {
180
179
  if (!floatable.options.interactive) {
181
- void floatable.toggle(false);
180
+ floatable.toggle(false);
182
181
  }
183
182
  }
184
183
  }
@@ -193,78 +192,157 @@ export function deactivate(floatable: Floatable): void {
193
192
  floatable.options.onAfter?.(false);
194
193
  }
195
194
 
195
+ function getAttribute(
196
+ position: Position,
197
+ anchor: DOMRect,
198
+ top: number,
199
+ left: number,
200
+ ): Position | undefined {
201
+ switch (position) {
202
+ case 'horizontal':
203
+ case 'horizontal-bottom':
204
+ case 'horizontal-top':
205
+ return position.replace('horizontal', left >= anchor.right ? 'right' : 'left') as Position;
206
+
207
+ case 'vertical':
208
+ case 'vertical-end':
209
+ case 'vertical-start':
210
+ return position.replace('vertical', top >= anchor.bottom ? 'bottom' : 'top') as Position;
211
+
212
+ default:
213
+ break;
214
+ }
215
+ }
216
+
217
+ function getMargin(): number {
218
+ try {
219
+ const fontsize = parseFloat(getComputedStyle(document.documentElement).fontSize);
220
+
221
+ if (Number.isNaN(fontsize)) {
222
+ return 5;
223
+ }
224
+
225
+ const margin = fontsize / 2;
226
+
227
+ return margin < 5 ? 5 : margin;
228
+ } catch {
229
+ return 5;
230
+ }
231
+ }
232
+
196
233
  function getState(): State {
197
234
  return (window as unknown as FloatableWindow)._oscarpalmer_oui_floatable;
198
235
  }
199
236
 
200
- function getX(anchor: DOMRect, content: DOMRect, position: Position): number {
201
- if (position.endsWith(POSITION_SUFFIX_END)) {
202
- const end = anchor.right - content.width;
237
+ function getX(anchor: DOMRect, content: DOMRect, position: Position, margin: number): number {
238
+ if (position === 'above' || position === 'below' || position === 'vertical') {
239
+ const center = anchor.left + anchor.width / 2;
240
+ const width = content.width / 2;
203
241
 
204
- return end < 0 ? 0 : end;
205
- }
242
+ if (center + width + margin >= document.body.clientWidth) {
243
+ return document.body.clientWidth - content.width - margin / 2;
244
+ }
206
245
 
207
- if (position.endsWith(POSITION_SUFFIX_START)) {
208
- const start = anchor.left;
246
+ if (center - width - margin <= 0) {
247
+ return margin / 2;
248
+ }
209
249
 
210
- return start + content.width > window.innerWidth ? window.innerWidth - content.width : start;
250
+ return center - width;
211
251
  }
212
252
 
213
- if (position.startsWith(POSITION_END)) {
253
+ if (position.startsWith('end')) {
214
254
  return anchor.right;
215
255
  }
216
256
 
217
- if (position.startsWith(POSITION_START)) {
257
+ if (position.startsWith('start')) {
218
258
  return anchor.left - content.width;
219
259
  }
220
260
 
221
- if (position.startsWith(POSITION_HORIZONTAL)) {
222
- if (anchor.right + content.width <= window.innerWidth) {
223
- return anchor.right;
261
+ if (position.endsWith('end')) {
262
+ return anchor.right - content.width - margin <= 0 ? margin / 2 : anchor.right - content.width;
263
+ }
264
+
265
+ if (position.endsWith('start')) {
266
+ if (anchor.left + content.width + margin >= document.body.clientWidth) {
267
+ return document.body.clientWidth - content.width - margin / 2;
224
268
  }
225
269
 
226
- return anchor.left - content.width;
270
+ return anchor.left - margin <= 0 ? margin / 2 : anchor.left;
227
271
  }
228
272
 
229
- const left = anchor.left + (anchor.width - content.width) / 2;
273
+ if (!position.startsWith('horizontal')) {
274
+ return 0;
275
+ }
276
+
277
+ if (anchor.right + content.width + margin >= document.body.clientWidth) {
278
+ return anchor.left - content.width;
279
+ }
230
280
 
231
- return left < 0 ? 0 : left;
281
+ return anchor.right;
232
282
  }
233
283
 
234
- function getY(anchor: DOMRect, content: DOMRect, position: Position, preferAbove: boolean): number {
235
- if (position.startsWith(POSITION_ABOVE)) {
284
+ function getY(
285
+ anchor: DOMRect,
286
+ content: DOMRect,
287
+ position: Position,
288
+ margin: number,
289
+ preferAbove: boolean,
290
+ ): number {
291
+ if (position === 'end' || position === 'start' || position === 'horizontal') {
292
+ const center = anchor.top + anchor.height / 2;
293
+ const height = content.height / 2;
294
+
295
+ if (center + height + margin >= document.body.clientHeight) {
296
+ return document.body.clientHeight - content.height - margin / 2;
297
+ }
298
+
299
+ if (center - height - margin <= 0) {
300
+ return margin / 2;
301
+ }
302
+
303
+ return center - height;
304
+ }
305
+
306
+ if (position.startsWith('above')) {
236
307
  return anchor.top - content.height;
237
308
  }
238
309
 
239
- if (position.startsWith(POSITION_BELOW)) {
310
+ if (position.startsWith('below')) {
240
311
  return anchor.bottom;
241
312
  }
242
313
 
243
- if (position.endsWith(POSITION_BOTTOM)) {
244
- const bottom = anchor.bottom - content.height;
245
-
246
- return bottom < 0 ? 0 : bottom;
314
+ if (position.endsWith('bottom')) {
315
+ return anchor.bottom - content.height - margin <= 0
316
+ ? margin / 2
317
+ : anchor.bottom - content.height;
247
318
  }
248
319
 
249
- if (position.endsWith(POSITION_TOP)) {
250
- const top = anchor.top;
251
-
252
- return top + content.height > window.innerHeight ? window.innerHeight - content.height : top;
320
+ if (position.endsWith('top')) {
321
+ return anchor.top + content.height + margin >= document.body.clientHeight
322
+ ? document.body.clientHeight - content.height - margin
323
+ : anchor.top;
253
324
  }
254
325
 
255
- if (position.startsWith(POSITION_VERTICAL)) {
256
- if (preferAbove && anchor.top - content.height >= 0) {
257
- return anchor.top - content.height;
258
- }
326
+ if (!position.startsWith('vertical')) {
327
+ return 0;
328
+ }
259
329
 
260
- if (anchor.bottom + content.height <= document.body.clientHeight) {
330
+ if (preferAbove) {
331
+ if (
332
+ anchor.top - content.height - margin <= 0 &&
333
+ anchor.bottom + content.height + margin <= document.body.clientHeight
334
+ ) {
261
335
  return anchor.bottom;
262
336
  }
263
337
 
264
338
  return anchor.top - content.height;
265
339
  }
266
340
 
267
- return anchor.top + (anchor.height - content.height) / 2;
341
+ if (anchor.bottom + content.height + margin >= document.body.clientHeight) {
342
+ return anchor.top - content.height;
343
+ }
344
+
345
+ return anchor.bottom;
268
346
  }
269
347
 
270
348
  function initialize(floatable: Floatable): void {
@@ -294,7 +372,7 @@ function isDisabled(element: HTMLElement): boolean {
294
372
  }
295
373
 
296
374
  function onClick(event: PointerEvent): void {
297
- const related = findAncestor(event.target as never, SELECTOR_ALL);
375
+ const related = findAncestor(event, SELECTOR_ALL);
298
376
 
299
377
  if (!(related instanceof HTMLElement)) {
300
378
  closeAll();
@@ -318,7 +396,7 @@ function onKeyDown(event: KeyboardEvent): void {
318
396
 
319
397
  closeNonInteractive(state);
320
398
 
321
- const related = findAncestor(event.target as never, SELECTOR_CONTENT);
399
+ const related = findAncestor(event, SELECTOR_CONTENT);
322
400
 
323
401
  if (!(related instanceof HTMLElement)) {
324
402
  return;
@@ -329,8 +407,24 @@ function onKeyDown(event: KeyboardEvent): void {
329
407
  const floatable = state.mapped.get(related);
330
408
 
331
409
  if (floatable != null) {
332
- void floatable.toggle(false);
410
+ floatable.toggle(false);
411
+ }
412
+ }
413
+
414
+ function setAttribute(
415
+ position: Position,
416
+ anchor: DOMRect,
417
+ floater: HTMLElement,
418
+ top: number,
419
+ left: number,
420
+ ): void {
421
+ let actual: Position | undefined;
422
+
423
+ if (/^(horizontal|vertical)/.test(position)) {
424
+ actual = getAttribute(position, anchor, top, left);
333
425
  }
426
+
427
+ floater.setAttribute('position', actual ?? position);
334
428
  }
335
429
 
336
430
  function stop(floatable: Floatable): void {
@@ -359,7 +453,7 @@ function toggle(anchor: HTMLElement): void {
359
453
  closeAbove(content as HTMLElement);
360
454
  }
361
455
 
362
- void floatable.toggle(!active);
456
+ floatable.toggle(!active);
363
457
  }
364
458
 
365
459
  function updatePosition(floatable: Floatable): void {
@@ -372,6 +466,8 @@ function updatePosition(floatable: Floatable): void {
372
466
  position = floatable.options.defaultPosition;
373
467
  }
374
468
 
469
+ const margin = getMargin();
470
+
375
471
  let previousAnchor: DOMRect;
376
472
  let previousContent: DOMRect;
377
473
 
@@ -404,8 +500,10 @@ function updatePosition(floatable: Floatable): void {
404
500
  previousAnchor = anchor;
405
501
  previousContent = content;
406
502
 
407
- const left = getX(anchor, content, position);
408
- const top = getY(anchor, content, position, floatable.options.preferAbove);
503
+ const left = getX(anchor, content, position, margin);
504
+ const top = getY(anchor, content, position, margin, floatable.options.preferAbove);
505
+
506
+ setAttribute(position, anchor, floatable.options.content, top, left);
409
507
 
410
508
  if (top + content.height > window.innerHeight) {
411
509
  floatable.options.content.style.height = `${window.innerHeight - top}px`;
@@ -442,26 +540,6 @@ const ATTRIBUTE_CONTENT = 'oui-floatable-content';
442
540
 
443
541
  const INSERT_AFTEREND = 'afterend';
444
542
 
445
- const POSITION_ABOVE = 'above';
446
-
447
- const POSITION_BELOW = 'below';
448
-
449
- const POSITION_BOTTOM = 'bottom';
450
-
451
- const POSITION_END = 'end';
452
-
453
- const POSITION_HORIZONTAL = 'horizontal';
454
-
455
- const POSITION_START = 'start';
456
-
457
- const POSITION_SUFFIX_END = '-end';
458
-
459
- const POSITION_SUFFIX_START = '-start';
460
-
461
- const POSITION_TOP = 'top';
462
-
463
- const POSITION_VERTICAL = 'vertical';
464
-
465
543
  const POSITIONS: Set<Position> = new Set([
466
544
  'above',
467
545
  'above-end',
@@ -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 {