@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.
Files changed (37) hide show
  1. package/dist/css/popover.css +9 -9
  2. package/dist/css/tooltip.css +20 -20
  3. package/dist/js/focus-trap/embedded.mjs +1 -1
  4. package/dist/js/focus-trap/standalone.mjs +1 -1
  5. package/dist/js/index.d.mts +47 -4
  6. package/dist/js/index.mjs +2537 -5
  7. package/dist/js/{attributable.d.mts → internal/attributable.d.mts} +1 -1
  8. package/dist/js/{attributable.mjs → internal/attributable.mjs} +1 -1
  9. package/dist/js/internal/draggable.d.mts +2 -0
  10. package/dist/js/internal/draggable.mjs +215 -0
  11. package/dist/js/{floatable.d.mts → internal/floatable.d.mts} +3 -3
  12. package/dist/js/internal/floatable.mjs +187 -0
  13. package/dist/js/movable-BKFgoaOl.d.mts +101 -0
  14. package/dist/js/movable.d.mts +2 -0
  15. package/dist/js/movable.mjs +152 -0
  16. package/dist/js/popover.mjs +2 -2
  17. package/dist/js/sortable.d.mts +2 -0
  18. package/dist/js/sortable.mjs +243 -0
  19. package/dist/js/splitter.mjs +4 -4
  20. package/dist/js/tooltip.d.mts +2 -2
  21. package/dist/js/tooltip.mjs +98 -100
  22. package/package.json +28 -18
  23. package/src/css/mixins.scss +8 -12
  24. package/src/css/popover.scss +1 -1
  25. package/src/css/tooltip.scss +22 -22
  26. package/src/js/focus-trap/standalone.ts +1 -1
  27. package/src/js/index.ts +3 -1
  28. package/src/js/internal/draggable.ts +515 -0
  29. package/src/js/internal/floatable.ts +373 -0
  30. package/src/js/movable.ts +260 -0
  31. package/src/js/popover.ts +2 -2
  32. package/src/js/sortable.ts +467 -0
  33. package/src/js/splitter.ts +1 -4
  34. package/src/js/tooltip.ts +132 -132
  35. package/dist/js/floatable.mjs +0 -284
  36. package/src/js/floatable.ts +0 -590
  37. /package/src/js/{attributable.ts → internal/attributable.ts} +0 -0
package/src/js/tooltip.ts CHANGED
@@ -1,56 +1,60 @@
1
1
  import {isNullableOrWhitespace} from '@oscarpalmer/atoms/is';
2
2
  import {on} from '@oscarpalmer/toretto/event';
3
3
  import {findAncestor} from '@oscarpalmer/toretto/find';
4
- import {attributable} from './attributable';
5
- import {Floatable} from './floatable';
6
-
7
- type Attributes = {
8
- ariaDescribedby: string | null;
9
- ariaLabelledBy: string | null;
10
- };
11
-
12
- type Content = {
13
- attributes: Attributes;
14
- element: HTMLElement;
15
- };
4
+ import {attributable} from './internal/attributable';
5
+ import {Floatable} from './internal/floatable';
16
6
 
17
7
  class Tooltip {
18
- attributes: Attributes;
8
+ elements: HTMLElement[];
19
9
 
20
10
  floatable: Floatable;
21
11
 
22
- timer: number | undefined;
12
+ constructor(anchor: HTMLElement, elements: HTMLElement[]) {
13
+ const content = getContent();
23
14
 
24
- constructor(anchor: HTMLElement, content: Content) {
25
- this.attributes = content.attributes;
15
+ this.elements = elements;
26
16
 
27
17
  this.floatable = new Floatable({
28
18
  anchor,
29
- content: content.element,
30
- defaultPosition: 'vertical',
19
+ content,
20
+ defaultPosition: 'above',
31
21
  interactive: false,
32
- preferAbove: true,
33
22
  positionAttribute: ATTRIBUTE_POSITION,
34
- });
23
+ reusable: true,
24
+ onAfter: (active: boolean): void => {
25
+ if (active) {
26
+ content.innerHTML = '';
27
+
28
+ content.append(...this.elements);
29
+ }
35
30
 
36
- anchor.insertAdjacentElement('afterend', content.element);
31
+ if (active || TOOLTIPS_ACTIVE.size > 0) {
32
+ return;
33
+ }
37
34
 
38
- TOOLTIPS_ALL.set(content.element, this);
35
+ content.removeAttribute(ATTRIBUTE_CONTENT_ACTIVE);
36
+
37
+ contentTimer = setTimeout(() => {
38
+ content.innerHTML = '';
39
+
40
+ content.remove();
41
+ }, delayClose);
42
+ },
43
+ });
39
44
  }
40
45
 
41
46
  destroy(): void {
42
- const {attributes, floatable} = this;
47
+ const {floatable} = this;
43
48
 
44
49
  TOOLTIPS_ACTIVE.delete(this);
50
+ TOOLTIPS_PENDING.delete(this);
51
+
45
52
  TOOLTIPS_ALL.delete(floatable.options.anchor);
46
53
  TOOLTIPS_ALL.delete(floatable.options.content);
47
54
 
48
- reset(floatable.options.anchor, attributes);
49
-
50
- floatable.options.content.remove();
51
55
  floatable.destroy();
52
56
 
53
- this.attributes = undefined as never;
57
+ this.elements = [];
54
58
  this.floatable = undefined as never;
55
59
  }
56
60
  }
@@ -60,78 +64,54 @@ function addTooltip(anchor: HTMLElement): void {
60
64
  return;
61
65
  }
62
66
 
63
- const content = getContent(anchor);
67
+ const elements = getElements(anchor);
64
68
 
65
- if (content != null) {
66
- TOOLTIPS_ALL.set(anchor, new Tooltip(anchor, content));
69
+ if (elements != null) {
70
+ TOOLTIPS_ALL.set(anchor, new Tooltip(anchor, elements));
67
71
  }
68
72
  }
69
73
 
70
- function closeTooltips(next: Tooltip | undefined): void {
71
- const tooltips = [...TOOLTIPS_ACTIVE];
74
+ function closeTooltips(current?: Tooltip): void {
75
+ const pending = [...TOOLTIPS_PENDING];
72
76
 
73
- for (const active of tooltips) {
74
- if (active === next) {
77
+ for (const item of pending) {
78
+ if (item === current) {
75
79
  continue;
76
80
  }
77
81
 
78
- if (active.timer != null) {
79
- clearTimeout(active.timer);
80
- }
81
-
82
- active.floatable?.options.content.removeAttribute('oui-tooltip-content-active');
83
-
84
- requestAnimationFrame(() => {
85
- active.timer = setTimeout(() => {
86
- TOOLTIPS_ACTIVE.delete(active);
87
- active.floatable?.toggle(false);
88
- }, delay) as never;
89
- });
82
+ TOOLTIPS_PENDING.delete(item);
90
83
  }
91
- }
92
-
93
- function getContent(anchor: HTMLElement): Content | undefined {
94
- const attributes = {
95
- ariaDescribedby: anchor.getAttribute('aria-describedby'),
96
- ariaLabelledBy: anchor.getAttribute('aria-labelledby'),
97
- };
98
-
99
- const wrapper = getWrapper(anchor);
100
84
 
101
- const labelledElements = getElements(attributes.ariaLabelledBy);
85
+ const active = [...TOOLTIPS_ACTIVE];
102
86
 
103
- if (labelledElements.length > 0) {
104
- anchor.setAttribute('aria-labelledby', wrapper.id);
105
-
106
- for (const element of labelledElements) {
107
- wrapper.append(element);
87
+ for (const item of active) {
88
+ if (item === current) {
89
+ continue;
108
90
  }
109
91
 
110
- return {
111
- attributes,
112
- element: wrapper,
113
- };
92
+ TOOLTIPS_ACTIVE.delete(item);
93
+
94
+ item.floatable.toggle(false);
114
95
  }
96
+ }
115
97
 
116
- const describedElements = getElements(attributes.ariaDescribedby);
98
+ function getElements(anchor: HTMLElement): HTMLElement[] | undefined {
99
+ let elements = findElements(anchor.getAttribute(ARIA_LABELLEDBY));
117
100
 
118
- if (describedElements.length > 0) {
119
- anchor.setAttribute('aria-describedby', wrapper.id);
101
+ if (elements.length > 0) {
102
+ return elements.map(element => element.cloneNode(true)) as HTMLElement[];
103
+ }
120
104
 
121
- for (const element of describedElements) {
122
- wrapper.append(element);
123
- }
105
+ elements = findElements(anchor.getAttribute(ARIA_DESCRIBEDBY));
124
106
 
125
- return {
126
- attributes,
127
- element: wrapper,
128
- };
107
+ if (elements.length > 0) {
108
+ return elements.map(element => element.cloneNode(true)) as HTMLElement[];
129
109
  }
130
110
 
131
- let content = anchor.getAttribute('aria-label');
111
+ let content = anchor.getAttribute(ARIA_LABEL);
132
112
 
133
113
  if (isNullableOrWhitespace(content)) {
134
- content = anchor.getAttribute('aria-description');
114
+ content = anchor.getAttribute(ARIA_DESCRIPTION);
135
115
  }
136
116
 
137
117
  if (isNullableOrWhitespace(content)) {
@@ -142,15 +122,10 @@ function getContent(anchor: HTMLElement): Content | undefined {
142
122
 
143
123
  paragraph.textContent = content;
144
124
 
145
- wrapper.append(paragraph);
146
-
147
- return {
148
- attributes,
149
- element: wrapper,
150
- };
125
+ return [paragraph];
151
126
  }
152
127
 
153
- function getElements(attribute: string | null): HTMLElement[] {
128
+ function findElements(attribute: string | null): HTMLElement[] {
154
129
  const ids = attribute?.split(EXPRESSION_WHITESPACE) ?? [];
155
130
  const elements: HTMLElement[] = [];
156
131
 
@@ -173,24 +148,31 @@ function getElements(attribute: string | null): HTMLElement[] {
173
148
  return elements;
174
149
  }
175
150
 
176
- function getWrapper(anchor: HTMLElement): HTMLElement {
177
- const wrapper = document.createElement('div');
151
+ function getContent(): HTMLElement {
152
+ if (contentElement != null) {
153
+ return contentElement;
154
+ }
155
+
156
+ contentElement = document.createElement('div');
178
157
 
179
- wrapper.setAttribute(ATTRIBUTE_CONTENT, '');
158
+ contentElement.setAttribute(ATTRIBUTE_CONTENT, '');
180
159
 
181
- wrapper.className = anchor.getAttribute(ATTRIBUTE_CLASS) ?? '';
182
- wrapper.hidden = true;
183
- wrapper.id = `${SELECTOR}-${++index}`;
184
- wrapper.role = 'tooltip';
185
- wrapper.tabIndex = -1;
160
+ contentElement.role = 'tooltip';
161
+ contentElement.tabIndex = -1;
186
162
 
187
- return wrapper;
163
+ return contentElement;
188
164
  }
189
165
 
190
166
  function onActivate(event: Event): void {
191
167
  onToggle(event, true);
192
168
  }
193
169
 
170
+ function onChange(): void {
171
+ if (TOOLTIPS_ACTIVE.size === 0) {
172
+ return;
173
+ }
174
+ }
175
+
194
176
  function onClick(event: Event): void {
195
177
  if (findAncestor(event, SELECTOR_CONTENT) != null) {
196
178
  event.stopPropagation();
@@ -206,45 +188,54 @@ function onToggle(event: Event, activate: boolean): void {
206
188
  cancelAnimationFrame(toggle);
207
189
  }
208
190
 
191
+ if (findAncestor(event, SELECTOR_CONTENT) != null) {
192
+ if (contentTimer != null) {
193
+ clearTimeout(contentTimer);
194
+ }
195
+
196
+ return;
197
+ }
198
+
209
199
  toggle = requestAnimationFrame(() => {
210
200
  toggle = undefined;
211
201
 
212
- const element = findAncestor(event, SELECTOR_FULL) as HTMLElement;
202
+ const element = findAncestor(event, SELECTOR) as HTMLElement;
213
203
 
214
204
  const tooltip = TOOLTIPS_ALL.get(element);
215
205
 
216
- if (TOOLTIPS_ACTIVE.size > 0) {
206
+ if (TOOLTIPS_ACTIVE.size > 0 || TOOLTIPS_PENDING.size > 0) {
217
207
  closeTooltips(tooltip);
218
208
  }
219
209
 
220
- if (
221
- tooltip == null ||
222
- tooltip.floatable.options.content.hasAttribute('oui-tooltip-content-active') === activate
223
- ) {
210
+ if (tooltip == null || tooltip.floatable.active === activate) {
224
211
  return;
225
212
  }
226
213
 
227
214
  if (activate) {
228
- if (tooltip.timer != null) {
229
- clearTimeout(tooltip.timer);
230
- }
215
+ clearTimeout(contentTimer);
216
+
217
+ TOOLTIPS_PENDING.add(tooltip);
218
+
219
+ contentTimer = setTimeout(() => {
220
+ TOOLTIPS_ACTIVE.add(tooltip);
221
+ TOOLTIPS_PENDING.delete(tooltip);
222
+
223
+ tooltip.floatable.options.content.style.positionAnchor =
224
+ tooltip.floatable.options.anchor.style.anchorName;
231
225
 
232
- TOOLTIPS_ACTIVE.add(tooltip);
233
- tooltip.floatable.toggle(true);
226
+ tooltip.floatable.toggle(true);
234
227
 
235
- requestAnimationFrame(() => {
236
- tooltip.floatable.options.content.setAttribute('oui-tooltip-content-active', '');
237
- });
228
+ setTimeout(() => {
229
+ tooltip.floatable.options.content.setAttribute(ATTRIBUTE_CONTENT_ACTIVE, '');
230
+ });
231
+ }, delayOpen);
238
232
 
239
233
  return;
240
234
  }
241
235
 
242
- tooltip.floatable.options.content.removeAttribute('oui-tooltip-content-active');
236
+ TOOLTIPS_ACTIVE.delete(tooltip);
243
237
 
244
- tooltip.timer ??= setTimeout(() => {
245
- TOOLTIPS_ACTIVE.delete(tooltip);
246
- tooltip.floatable.toggle(false);
247
- }, delay) as never;
238
+ tooltip.floatable.toggle(false);
248
239
  });
249
240
  }
250
241
 
@@ -254,31 +245,32 @@ function removeTooltip(element: HTMLElement): void {
254
245
  }
255
246
  }
256
247
 
257
- function reset(anchor: HTMLElement, attributes: Attributes): void {
258
- if (attributes.ariaDescribedby == null) {
259
- anchor.removeAttribute('aria-describedby');
260
- } else {
261
- anchor.setAttribute('aria-describedby', attributes.ariaDescribedby);
262
- }
263
- }
264
-
265
- function setDelay(value: number): void {
266
- delay = typeof value === 'number' && value >= 0 ? value : delay;
248
+ function setTooltipDelay(open?: number, close?: number): void {
249
+ delayClose = typeof close === 'number' && close >= 0 ? close : delayClose;
250
+ delayOpen = typeof open === 'number' && open >= 0 ? open : delayOpen;
267
251
  }
268
252
 
269
253
  //
270
254
 
271
- const SELECTOR = 'oui-tooltip';
255
+ const ARIA_DESCRIBEDBY = 'aria-describedby';
272
256
 
273
- const ATTRIBUTE_CLASS = `${SELECTOR}-class`;
257
+ const ARIA_DESCRIPTION = 'aria-description';
274
258
 
275
- const ATTRIBUTE_CONTENT = `${SELECTOR}-content`;
259
+ const ARIA_LABEL = 'aria-label';
276
260
 
277
- const ATTRIBUTE_POSITION = `${SELECTOR}-position`;
261
+ const ARIA_LABELLEDBY = 'aria-labelledby';
278
262
 
279
- const SELECTOR_CONTENT = `[${ATTRIBUTE_CONTENT}]`;
263
+ const ATTRIBUTE = 'oui-tooltip';
264
+
265
+ const ATTRIBUTE_CONTENT = `${ATTRIBUTE}-content`;
266
+
267
+ const ATTRIBUTE_CONTENT_ACTIVE = `${ATTRIBUTE_CONTENT}-active`;
280
268
 
281
- const SELECTOR_FULL = `[${SELECTOR}], [${ATTRIBUTE_CONTENT}]`;
269
+ const ATTRIBUTE_POSITION = `${ATTRIBUTE}-position`;
270
+
271
+ const SELECTOR = `[${ATTRIBUTE}]`;
272
+
273
+ const SELECTOR_CONTENT = `[${ATTRIBUTE_CONTENT}]`;
282
274
 
283
275
  const EXPRESSION_WHITESPACE = /\s+/;
284
276
 
@@ -286,22 +278,30 @@ const TOOLTIPS_ACTIVE: Set<Tooltip> = new Set();
286
278
 
287
279
  const TOOLTIPS_ALL: WeakMap<HTMLElement, Tooltip> = new WeakMap();
288
280
 
289
- let delay = 500;
281
+ const TOOLTIPS_PENDING: Set<Tooltip> = new Set();
282
+
283
+ let contentElement: HTMLElement | undefined;
284
+
285
+ let contentTimer: number | undefined;
286
+
287
+ let delayClose = 250;
290
288
 
291
- let index = 0;
289
+ let delayOpen = 500;
292
290
 
293
291
  let toggle: DOMHighResTimeStamp | undefined;
294
292
 
295
293
  //
296
294
 
297
- attributable(SELECTOR, addTooltip, removeTooltip);
295
+ attributable(ATTRIBUTE, addTooltip, removeTooltip);
298
296
 
299
297
  on(document, 'click', onClick, {capture: true});
300
298
  on(document, 'focusin', onActivate);
301
299
  on(document, 'focusout', onDeactivate);
302
300
  on(document, 'mousemove', onActivate);
301
+ on(window, 'resize', onChange);
302
+ on(document, 'scroll', onChange);
303
303
  on(document, 'touchstart', onActivate);
304
304
 
305
305
  //
306
306
 
307
- export {setDelay};
307
+ export {setTooltipDelay};
@@ -1,284 +0,0 @@
1
- import { on } from "@oscarpalmer/toretto/event";
2
- import { findAncestor } from "@oscarpalmer/toretto/find";
3
- //#region src/js/floatable.ts
4
- var Floatable = class {
5
- active = false;
6
- disabled = false;
7
- frame;
8
- ignoreFocus = false;
9
- constructor(options) {
10
- this.options = options;
11
- initialize(this);
12
- }
13
- destroy() {
14
- stop(this);
15
- const state = getState();
16
- state?.active.delete(this);
17
- state?.mapped.delete(this.options.anchor);
18
- state?.mapped.delete(this.options.content);
19
- this.options.anchor = void 0;
20
- this.options.content = void 0;
21
- }
22
- disable() {
23
- if (this.disabled) return;
24
- this.disabled = true;
25
- stop(this);
26
- getState()?.active.delete(this);
27
- }
28
- enable() {
29
- if (!this.disabled) return;
30
- this.disabled = false;
31
- initialize(this);
32
- }
33
- toggle(active, ignoreFocus) {
34
- if (this.disabled || this.active === active) return;
35
- const state = getState();
36
- if (state == null) return;
37
- this.ignoreFocus = !active && (ignoreFocus ?? false);
38
- stop(this);
39
- if (active) state.active.add(this);
40
- else state.active.delete(this);
41
- if (this.options.interactive) if (active) state.order.push(this);
42
- else {
43
- const index = state.order.indexOf(this);
44
- if (index > -1) state.order.splice(index, 1);
45
- }
46
- if (active) activate(this);
47
- else deactivate(this);
48
- }
49
- };
50
- function activate(floatable) {
51
- floatable.active = true;
52
- document.body.append(floatable.options.content);
53
- floatable.options.content.hidden = false;
54
- updatePosition(floatable);
55
- }
56
- function closeAbove(element) {
57
- const state = getState();
58
- if (state == null) return;
59
- const index = state.order.findIndex((floatable) => floatable.options.anchor === element || floatable.options.content === element);
60
- if (index === -1 || index === state.order.length - 1) return;
61
- while (index < state.order.length - 1) state.order.pop()?.toggle(false, true);
62
- }
63
- function closeAll() {
64
- const state = getState();
65
- const floatables = [...state?.active ?? []];
66
- state?.active.clear();
67
- for (const floatable of floatables) floatable.toggle(false, true);
68
- }
69
- function closeNonInteractive(state) {
70
- for (const floatable of state.active) if (!floatable.options.interactive) floatable.toggle(false);
71
- }
72
- function deactivate(floatable) {
73
- floatable.active = false;
74
- floatable.options.content.hidden = true;
75
- floatable.options.anchor.insertAdjacentElement(INSERT_AFTEREND, floatable.options.content);
76
- floatable.options.onAfter?.(false);
77
- }
78
- function getAttribute(position, anchor, top, left) {
79
- switch (position) {
80
- case "horizontal":
81
- case "horizontal-bottom":
82
- case "horizontal-top": return position.replace("horizontal", left >= anchor.right ? "right" : "left");
83
- case "vertical":
84
- case "vertical-end":
85
- case "vertical-start": return position.replace("vertical", top >= anchor.bottom ? "bottom" : "top");
86
- default: break;
87
- }
88
- }
89
- function getMargin() {
90
- try {
91
- const fontsize = parseFloat(getComputedStyle(document.documentElement).fontSize);
92
- if (Number.isNaN(fontsize)) return 5;
93
- const margin = fontsize / 2;
94
- return margin < 5 ? 5 : margin;
95
- } catch {
96
- return 5;
97
- }
98
- }
99
- function getState() {
100
- return window._oscarpalmer_oui_floatable;
101
- }
102
- function getX(anchor, content, position, margin) {
103
- if (position === "above" || position === "below" || position === "vertical") {
104
- const center = anchor.left + anchor.width / 2;
105
- const width = content.width / 2;
106
- if (center + width + margin >= document.body.clientWidth) return document.body.clientWidth - content.width - margin / 2;
107
- if (center - width - margin <= 0) return margin / 2;
108
- return center - width;
109
- }
110
- if (position.startsWith("end")) return anchor.right;
111
- if (position.startsWith("start")) return anchor.left - content.width;
112
- if (position.endsWith("end")) return anchor.right - content.width - margin <= 0 ? margin / 2 : anchor.right - content.width;
113
- if (position.endsWith("start")) {
114
- if (anchor.left + content.width + margin >= document.body.clientWidth) return document.body.clientWidth - content.width - margin / 2;
115
- return anchor.left - margin <= 0 ? margin / 2 : anchor.left;
116
- }
117
- if (!position.startsWith("horizontal")) return 0;
118
- if (anchor.right + content.width + margin >= document.body.clientWidth) return anchor.left - content.width;
119
- return anchor.right;
120
- }
121
- function getY(anchor, content, position, margin, preferAbove) {
122
- if (position === "end" || position === "start" || position === "horizontal") {
123
- const center = anchor.top + anchor.height / 2;
124
- const height = content.height / 2;
125
- if (center + height + margin >= document.body.clientHeight) return document.body.clientHeight - content.height - margin / 2;
126
- if (center - height - margin <= 0) return margin / 2;
127
- return center - height;
128
- }
129
- if (position.startsWith("above")) return anchor.top - content.height;
130
- if (position.startsWith("below")) return anchor.bottom;
131
- if (position.endsWith("bottom")) return anchor.bottom - content.height - margin <= 0 ? margin / 2 : anchor.bottom - content.height;
132
- if (position.endsWith("top")) return anchor.top + content.height + margin >= document.body.clientHeight ? document.body.clientHeight - content.height - margin : anchor.top;
133
- if (!position.startsWith("vertical")) return 0;
134
- if (preferAbove) {
135
- if (anchor.top - content.height - margin <= 0 && anchor.bottom + content.height + margin <= document.body.clientHeight) return anchor.bottom;
136
- return anchor.top - content.height;
137
- }
138
- if (anchor.bottom + content.height + margin >= document.body.clientHeight) return anchor.top - content.height;
139
- return anchor.bottom;
140
- }
141
- function initialize(floatable) {
142
- const { anchor, content, interactive } = floatable.options;
143
- if (interactive) {
144
- anchor.setAttribute(ATTRIBUTE_ANCHOR, "");
145
- content.setAttribute(ATTRIBUTE_CONTENT, "");
146
- }
147
- content.style.position = "fixed";
148
- content.style.inset = "0 auto auto 0";
149
- content.style.zIndex = "10";
150
- const state = getState();
151
- state?.mapped.set(anchor, floatable);
152
- state?.mapped.set(content, floatable);
153
- }
154
- function isDisabled(element) {
155
- return element.disabled || element.readOnly || element.getAttribute("aria-disabled") === "true";
156
- }
157
- function onClick(event) {
158
- const related = findAncestor(event, SELECTOR_ALL);
159
- if (!(related instanceof HTMLElement)) closeAll();
160
- else if (related.hasAttribute(ATTRIBUTE_CONTENT)) closeAbove(related);
161
- else if (!isDisabled(related)) toggle(related);
162
- }
163
- function onKeyDown(event) {
164
- if (event.key !== "Escape") return;
165
- const state = getState();
166
- if (state == null || state.active.size === 0) return;
167
- closeNonInteractive(state);
168
- const related = findAncestor(event, SELECTOR_CONTENT);
169
- if (!(related instanceof HTMLElement)) return;
170
- closeAbove(related);
171
- const floatable = state.mapped.get(related);
172
- if (floatable != null) floatable.toggle(false);
173
- }
174
- function setAttribute(position, anchor, floater, top, left) {
175
- let actual;
176
- if (/^(horizontal|vertical)/.test(position)) actual = getAttribute(position, anchor, top, left);
177
- floater.setAttribute("position", actual ?? position);
178
- }
179
- function stop(floatable) {
180
- if (floatable.frame != null) cancelAnimationFrame(floatable.frame);
181
- floatable.frame = void 0;
182
- }
183
- function toggle(anchor) {
184
- const floatable = getState()?.mapped.get(anchor);
185
- if (floatable == null) {
186
- closeAll();
187
- return;
188
- }
189
- const active = Boolean(floatable.active);
190
- const content = findAncestor(floatable.options.anchor, SELECTOR_CONTENT);
191
- if (content == null) closeAll();
192
- else closeAbove(content);
193
- floatable.toggle(!active);
194
- }
195
- function updatePosition(floatable) {
196
- let position = floatable.options.anchor.getAttribute(floatable.options.positionAttribute)?.trim() ?? floatable.options.defaultPosition;
197
- if (!POSITIONS.has(position)) position = floatable.options.defaultPosition;
198
- const margin = getMargin();
199
- let previousAnchor;
200
- let previousContent;
201
- let calculated = false;
202
- let runOnAfter = false;
203
- function run() {
204
- const anchor = floatable.options.anchor.getBoundingClientRect();
205
- const content = floatable.options.content.getBoundingClientRect();
206
- if (calculated) {
207
- if (previousAnchor != null && previousContent != null) {
208
- if (PROPERTIES.every((property) => anchor[property] === previousAnchor[property]) && PROPERTIES.every((property) => content[property] === previousContent[property])) {
209
- floatable.frame = requestAnimationFrame(run);
210
- return;
211
- }
212
- }
213
- } else calculated = true;
214
- update(anchor, content);
215
- }
216
- function update(anchor, content) {
217
- previousAnchor = anchor;
218
- previousContent = content;
219
- const left = getX(anchor, content, position, margin);
220
- const top = getY(anchor, content, position, margin, floatable.options.preferAbove);
221
- setAttribute(position, anchor, floatable.options.content, top, left);
222
- if (top + content.height > window.innerHeight) floatable.options.content.style.height = `${window.innerHeight - top}px`;
223
- else floatable.options.content.style.height = "";
224
- if (left + content.width > window.innerWidth) floatable.options.content.style.width = `${window.innerWidth - left}px`;
225
- else floatable.options.content.style.width = "";
226
- floatable.options.content.style.transform = `translate3d(${left}px, ${top}px, 0)`;
227
- floatable.frame = requestAnimationFrame(run);
228
- if (!runOnAfter) {
229
- runOnAfter = true;
230
- floatable.options.onAfter?.(true);
231
- }
232
- }
233
- floatable.frame = requestAnimationFrame(run);
234
- }
235
- const ATTRIBUTE_ANCHOR = "oui-floatable-anchor";
236
- const ATTRIBUTE_CONTENT = "oui-floatable-content";
237
- const INSERT_AFTEREND = "afterend";
238
- const POSITIONS = new Set([
239
- "above",
240
- "above-end",
241
- "above-start",
242
- "below",
243
- "below-end",
244
- "below-start",
245
- "end",
246
- "end-bottom",
247
- "end-top",
248
- "horizontal",
249
- "horizontal-bottom",
250
- "horizontal-top",
251
- "start",
252
- "start-bottom",
253
- "start-top",
254
- "vertical",
255
- "vertical-end",
256
- "vertical-start"
257
- ]);
258
- const PROPERTIES = [
259
- "bottom",
260
- "left",
261
- "right",
262
- "top"
263
- ];
264
- const SELECTOR_ALL = `[${ATTRIBUTE_ANCHOR}], [${ATTRIBUTE_CONTENT}]`;
265
- const SELECTOR_CONTENT = `[${ATTRIBUTE_CONTENT}]`;
266
- if (window._oscarpalmer_oui_floatable == null) {
267
- Object.defineProperty(window, "_oscarpalmer_oui_floatable", {
268
- enumerable: false,
269
- value: {
270
- active: /* @__PURE__ */ new Set(),
271
- mapped: /* @__PURE__ */ new Map(),
272
- order: []
273
- },
274
- writable: false
275
- });
276
- on(document, "keydown", (event) => {
277
- onKeyDown(event);
278
- });
279
- on(document, "click", (event) => {
280
- onClick(event);
281
- });
282
- }
283
- //#endregion
284
- export { Floatable, deactivate };