@oscarpalmer/oui 0.23.0 → 0.24.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.
@@ -0,0 +1,59 @@
1
+ //#region src/js/floatable.d.ts
2
+ declare class Floatable {
3
+ #private;
4
+ anchor: HTMLElement;
5
+ content: HTMLElement;
6
+ options: FloatableOptions;
7
+ /**
8
+ * Is the _Floatable_ open?
9
+ */
10
+ get open(): boolean;
11
+ constructor(anchor: HTMLElement, content: HTMLElement, options: FloatableOptions, standalone: boolean);
12
+ /**
13
+ * Destroy the floatable, removing it from the DOM and cleaning up references
14
+ */
15
+ destroy(): void;
16
+ /**
17
+ * Hide the floatable
18
+ */
19
+ hide(): void;
20
+ /**
21
+ * Show the floatable
22
+ */
23
+ show(): void;
24
+ /**
25
+ * Update the position of the floatable, with an optional new position
26
+ *
27
+ * @param position Optional new position
28
+ */
29
+ update(position?: FloatablePosition): void;
30
+ }
31
+ type FloatableOptions = {
32
+ attribute?: string;
33
+ position: FloatablePosition;
34
+ };
35
+ type FloatablePosition = 'above' | 'above-end' | 'above-start' | 'below' | 'below-end' | 'below-start' | 'end' | 'end-bottom' | 'end-top' | 'start' | 'start-bottom' | 'start-top';
36
+ type GetFloatableOptions = {
37
+ /**
38
+ * Open the _Floatable_ when created? _(defaults to `true`)_
39
+ */
40
+ open?: boolean;
41
+ /**
42
+ * Position of the _Floatable_ _(defaults to `below`)_
43
+ */
44
+ position?: FloatablePosition;
45
+ };
46
+ declare function createFloatable(anchor: HTMLElement, content: HTMLElement, options: FloatableOptions, standalone: boolean): Floatable;
47
+ /**
48
+ * Create a _Floatable_ for an anchor and content element
49
+ *
50
+ * @param anchor Anchor element
51
+ * @param content Content element
52
+ * @param options _Floatable_ options
53
+ * @returns _Floatable_ instance
54
+ */
55
+ declare function getFloatable(anchor: HTMLElement | SVGElement, content: HTMLElement | SVGElement, options?: GetFloatableOptions): Floatable;
56
+ declare function removeFloatable(content: HTMLElement): void;
57
+ declare const ATTRIBUTE_FLOATABLE = "oui-floatable";
58
+ //#endregion
59
+ export { ATTRIBUTE_FLOATABLE, Floatable, FloatableOptions, FloatablePosition, createFloatable, getFloatable, removeFloatable };
@@ -0,0 +1,130 @@
1
+ import { isPlainObject } from "@oscarpalmer/atoms/is";
2
+ import { isHTMLOrSVGElement } from "@oscarpalmer/toretto/is";
3
+ //#region src/js/floatable.ts
4
+ var Floatable = class {
5
+ anchor;
6
+ content;
7
+ options;
8
+ #destroyed = false;
9
+ /**
10
+ * Is the _Floatable_ open?
11
+ */
12
+ get open() {
13
+ return this.content.checkVisibility();
14
+ }
15
+ constructor(anchor, content, options, standalone) {
16
+ this.anchor = anchor;
17
+ this.content = content;
18
+ this.options = options;
19
+ if (standalone) {
20
+ content.setAttribute(ATTRIBUTE_FLOATABLE, "");
21
+ content.popover = "manual";
22
+ }
23
+ const anchorName = getAnchorName(anchor);
24
+ anchor.style.anchorName = anchorName;
25
+ content.style.positionAnchor = anchorName;
26
+ }
27
+ /**
28
+ * Destroy the floatable, removing it from the DOM and cleaning up references
29
+ */
30
+ destroy() {
31
+ if (this.#destroyed) return;
32
+ this.#destroyed = true;
33
+ removeFloatable(this.content);
34
+ this.anchor = null;
35
+ this.content = null;
36
+ }
37
+ /**
38
+ * Hide the floatable
39
+ */
40
+ hide() {
41
+ if (!this.#destroyed) this.content.hidePopover();
42
+ }
43
+ /**
44
+ * Show the floatable
45
+ */
46
+ show() {
47
+ if (!this.#destroyed) this.content.showPopover();
48
+ }
49
+ /**
50
+ * Update the position of the floatable, with an optional new position
51
+ *
52
+ * @param position Optional new position
53
+ */
54
+ update(position) {
55
+ if (!this.#destroyed) setPosition(this, position);
56
+ }
57
+ };
58
+ function createFloatable(anchor, content, options, standalone) {
59
+ if (!isHTMLOrSVGElement(anchor) || !isHTMLOrSVGElement(content)) throw new TypeError("Anchor and content must be an HTMLElement or SVGElement.");
60
+ let floatable = instances.get(content);
61
+ if (floatable == null) {
62
+ floatable = new Floatable(anchor, content, options, standalone);
63
+ instances.set(content, floatable);
64
+ }
65
+ return floatable;
66
+ }
67
+ function getAnchorName(anchor) {
68
+ let { anchorName } = anchor.style;
69
+ if (anchorName.length > 0) return anchorName;
70
+ index += 1;
71
+ return `--oui-floatable-anchor-${index}`;
72
+ }
73
+ /**
74
+ * Create a _Floatable_ for an anchor and content element
75
+ *
76
+ * @param anchor Anchor element
77
+ * @param content Content element
78
+ * @param options _Floatable_ options
79
+ * @returns _Floatable_ instance
80
+ */
81
+ function getFloatable(anchor, content, options) {
82
+ const { open, position } = getOptions(options);
83
+ const floatable = createFloatable(anchor, content, { position }, true);
84
+ floatable?.update(position);
85
+ if (open) floatable?.show();
86
+ return floatable;
87
+ }
88
+ function getOptions(input) {
89
+ const object = isPlainObject(input) ? input : {};
90
+ return {
91
+ open: typeof object.open === "boolean" ? object.open : true,
92
+ position: object.position != null && object.position in AREAS ? object.position : "below"
93
+ };
94
+ }
95
+ function getPosition(floatable, override) {
96
+ if (override != null && override in AREAS) return override;
97
+ if (floatable.options.attribute == null) return floatable.options.position;
98
+ const attribute = floatable.anchor.getAttribute(floatable.options.attribute) ?? "";
99
+ return attribute in AREAS ? attribute : floatable.options.position;
100
+ }
101
+ function removeFloatable(content) {
102
+ instances.get(content)?.destroy();
103
+ instances.delete(content);
104
+ }
105
+ function setPosition(floatable, override) {
106
+ const area = AREAS[getPosition(floatable, override)];
107
+ floatable.content.style.positionArea = area;
108
+ floatable.content.setAttribute(ATTRIBUTE_POSITION, POSITIONS[area]);
109
+ }
110
+ const AREAS = {
111
+ above: "block-start",
112
+ "above-end": "start span-start",
113
+ "above-start": "start span-end",
114
+ below: "block-end",
115
+ "below-end": "end span-start",
116
+ "below-start": "end span-end",
117
+ end: "center end",
118
+ "end-bottom": "span-start end",
119
+ "end-top": "span-end end",
120
+ start: "center start",
121
+ "start-bottom": "span-start start",
122
+ "start-top": "span-end start"
123
+ };
124
+ const ATTRIBUTE_FLOATABLE = "oui-floatable";
125
+ const ATTRIBUTE_POSITION = "oui-position";
126
+ const POSITIONS = Object.fromEntries(Object.entries(AREAS).map(([position, area]) => [area, position]));
127
+ const instances = /* @__PURE__ */ new WeakMap();
128
+ let index = 0;
129
+ //#endregion
130
+ export { ATTRIBUTE_FLOATABLE, Floatable, createFloatable, getFloatable, removeFloatable };
@@ -1,3 +1,4 @@
1
+ import { getFloatable } from "./floatable.mjs";
1
2
  import { OuiSplitterElement } from "./splitter.mjs";
2
3
  import { setTooltipDelay } from "./tooltip.mjs";
3
- export { OuiSplitterElement, setTooltipDelay };
4
+ export { OuiSplitterElement, getFloatable, setTooltipDelay };
package/dist/js/index.mjs CHANGED
@@ -1,7 +1,8 @@
1
+ import { getFloatable } from "./floatable.mjs";
1
2
  import "./focus-trap/standalone.mjs";
2
3
  import "./movable.mjs";
3
4
  import "./popover.mjs";
4
5
  import "./sortable.mjs";
5
6
  import { OuiSplitterElement } from "./splitter.mjs";
6
7
  import { setTooltipDelay } from "./tooltip.mjs";
7
- export { OuiSplitterElement, setTooltipDelay };
8
+ export { OuiSplitterElement, getFloatable, setTooltipDelay };
@@ -1,10 +1,11 @@
1
+ import { isHTMLOrSVGElement } from "@oscarpalmer/toretto/is";
1
2
  //#region src/js/internal/attributable.ts
2
3
  function attributable(attribute, add, remove) {
3
4
  new MutationObserver((entries) => {
4
5
  const { length } = entries;
5
6
  for (let index = 0; index < length; index += 1) {
6
7
  const entry = entries[index];
7
- if (entry.type === "attributes" && entry.target instanceof HTMLElement) if (entry.target.hasAttribute(attribute)) handleAdded(attribute, [entry.target], add);
8
+ if (entry.type === "attributes" && isHTMLOrSVGElement(entry.target)) if (entry.target.hasAttribute(attribute)) handleAdded(attribute, [entry.target], add);
8
9
  else handleRemoved([entry.target], remove);
9
10
  else if (entry.type === "childList") {
10
11
  handleAdded(attribute, entry.addedNodes, add);
@@ -22,7 +23,7 @@ function attributable(attribute, add, remove) {
22
23
  });
23
24
  }
24
25
  function handleAdded(attribute, nodes, add) {
25
- const elements = [...nodes].filter((node) => node instanceof HTMLElement);
26
+ const elements = [...nodes].filter(isHTMLOrSVGElement);
26
27
  const { length } = elements;
27
28
  for (let index = 0; index < length; index += 1) {
28
29
  const element = elements[index];
@@ -31,7 +32,7 @@ function handleAdded(attribute, nodes, add) {
31
32
  }
32
33
  }
33
34
  function handleRemoved(nodes, remove) {
34
- const elements = [...nodes].filter((node) => node instanceof HTMLElement);
35
+ const elements = [...nodes].filter(isHTMLOrSVGElement);
35
36
  const { length } = elements;
36
37
  for (let index = 0; index < length; index += 1) {
37
38
  const element = elements[index];
@@ -1,6 +1,7 @@
1
+ import { createFloatable, removeFloatable } from "./floatable.mjs";
1
2
  import { attributable } from "./internal/attributable.mjs";
2
3
  import { createFocusTrap, removeFocusTrap } from "./focus-trap/embedded.mjs";
3
- import { createFloatable, removeFloatable } from "./internal/floatable.mjs";
4
+ import { isHTMLOrSVGElement } from "@oscarpalmer/toretto/is";
4
5
  import { on } from "@oscarpalmer/toretto/event";
5
6
  //#region src/js/popover.ts
6
7
  var Popover = class {
@@ -11,8 +12,11 @@ var Popover = class {
11
12
  listener;
12
13
  constructor(element) {
13
14
  this.element = element;
14
- this.anchor = element.ownerDocument.querySelector(`[popovertarget="${element.id}"]`);
15
- this.floatable = createFloatable(this.anchor, element, options);
15
+ const anchor = element.ownerDocument.querySelector(`[popovertarget="${element.id}"]`);
16
+ if (!isHTMLOrSVGElement(anchor)) throw new Error(`No anchor found for popover with id '${element.id}'.`);
17
+ if (element.contains(anchor)) throw new Error("Popover content cannot contain its anchor.");
18
+ this.anchor = anchor;
19
+ this.floatable = createFloatable(this.anchor, element, options, false);
16
20
  this.focusTrap = createFocusTrap(element, { noescape: true });
17
21
  this.listener = on(element, EVENT_TOGGLE, (event) => {
18
22
  if (event.newState === STATE_OPEN) {
@@ -34,7 +38,7 @@ var Popover = class {
34
38
  }
35
39
  };
36
40
  function onAdd(element) {
37
- if (!instances.has(element) && element.getAttribute(ATTRIBUTE) !== POPOVER_HINT) instances.set(element, new Popover(element));
41
+ if (!instances.has(element) && element.getAttribute(ATTRIBUTE) !== POPOVER_HINT && !element.hasAttribute("oui-floatable")) instances.set(element, new Popover(element));
38
42
  }
39
43
  function onPointerdown() {
40
44
  ignoreFocus = true;
@@ -50,7 +54,7 @@ const POPOVER_HINT = "hint";
50
54
  const STATE_OPEN = "open";
51
55
  const instances = /* @__PURE__ */ new WeakMap();
52
56
  const options = {
53
- attribute: "position",
57
+ attribute: `${ATTRIBUTE}position`,
54
58
  position: "below-start"
55
59
  };
56
60
  let ignoreFocus = false;
@@ -1,5 +1,6 @@
1
1
  import { attributable } from "./internal/attributable.mjs";
2
2
  import { Draggable, addDraggable, removeDraggable } from "./internal/draggable.mjs";
3
+ import { isHTMLOrSVGElement } from "@oscarpalmer/toretto/is";
3
4
  import { getPosition } from "@oscarpalmer/toretto/event";
4
5
  import { findAncestor, getElementFromPosition } from "@oscarpalmer/toretto/find";
5
6
  import { createElement } from "@oscarpalmer/toretto/create";
@@ -177,12 +178,13 @@ function setPlaceholder(state, sortable, origin) {
177
178
  parent: sortable.element,
178
179
  placeholder: element,
179
180
  create: (value) => {
180
- if (!(value instanceof HTMLElement)) return;
181
- placeholder.drag.custom = value;
182
- updatePlaceholder(sortable, value);
181
+ if (!isHTMLOrSVGElement(value)) return;
182
+ const element = value;
183
+ placeholder.drag.custom = element;
184
+ updatePlaceholder(sortable, element);
183
185
  state.element = {
184
- rectangle: value.getBoundingClientRect(),
185
- node: value
186
+ rectangle: element.getBoundingClientRect(),
187
+ node: element
186
188
  };
187
189
  }
188
190
  }
@@ -1,24 +1,28 @@
1
+ import { isNullableOrWhitespace } from "@oscarpalmer/atoms/is";
1
2
  import { clamp } from "@oscarpalmer/atoms/number";
2
3
  import { getPosition, on } from "@oscarpalmer/toretto/event";
3
4
  import { findAncestor } from "@oscarpalmer/toretto/find";
4
5
  import { toggleStyles } from "@oscarpalmer/toretto/style";
5
6
  import supportsTouch from "@oscarpalmer/toretto/touch";
6
- import { isNullableOrWhitespace } from "@oscarpalmer/atoms/is";
7
7
  import { round } from "@oscarpalmer/atoms/math";
8
8
  //#region src/js/splitter.ts
9
+ const ATTRIBUTE_MAX = "max";
10
+ const ATTRIBUTE_MIN = "min";
11
+ const ATTRIBUTE_SIZE = "size";
12
+ const ATTRIBUTE_TYPE = "type";
9
13
  var OuiSplitterElement = class extends HTMLElement {
10
14
  static observedAttributes = [
11
- "max",
12
- "min",
13
- "size",
14
- "type"
15
+ ATTRIBUTE_MAX,
16
+ ATTRIBUTE_MIN,
17
+ ATTRIBUTE_SIZE,
18
+ ATTRIBUTE_TYPE
15
19
  ];
16
20
  #splitter;
17
21
  get max() {
18
22
  return this.#splitter.values.max;
19
23
  }
20
24
  set max(value) {
21
- this.setAttribute("max", String(value));
25
+ this.setAttribute(ATTRIBUTE_MAX, String(value));
22
26
  }
23
27
  get min() {
24
28
  return this.#splitter.values.min;
@@ -52,12 +56,12 @@ var OuiSplitterElement = class extends HTMLElement {
52
56
  }
53
57
  attributeChangedCallback(name) {
54
58
  switch (name) {
55
- case "max":
56
- case "min":
57
- case "size":
59
+ case ATTRIBUTE_MAX:
60
+ case ATTRIBUTE_MIN:
61
+ case ATTRIBUTE_SIZE:
58
62
  this.#splitter.updateValues();
59
63
  break;
60
- case "type":
64
+ case ATTRIBUTE_TYPE:
61
65
  this.#splitter.updateOrientation();
62
66
  break;
63
67
  default: break;
@@ -157,14 +161,14 @@ function getValue(element, name, defaultValue) {
157
161
  }
158
162
  function getValues(element) {
159
163
  const values = {
160
- max: getValue(element, "max", NAVIGATION_MAXIMUM_PERCENTAGE),
161
- min: getValue(element, "min", NAVIGATION_MINIMUM_PERCENTAGE),
164
+ max: getValue(element, ATTRIBUTE_MAX, NAVIGATION_MAXIMUM_PERCENTAGE),
165
+ min: getValue(element, ATTRIBUTE_MIN, NAVIGATION_MINIMUM_PERCENTAGE),
162
166
  now: {
163
167
  current: .5,
164
168
  previous: .5
165
169
  }
166
170
  };
167
- const value = getContained(values.min, getValue(element, "size", values.now.current), values.max);
171
+ const value = getContained(values.min, getValue(element, ATTRIBUTE_SIZE, values.now.current), values.max);
168
172
  values.now.current = value;
169
173
  values.now.previous = value;
170
174
  return values;
@@ -1,4 +1,10 @@
1
1
  //#region src/js/tooltip.d.ts
2
+ /**
3
+ * Set the delay before showing the tooltip, in milliseconds
4
+ *
5
+ * _(Default delay is half a second, or 500 milliseconds)_
6
+ * @param value Delay in milliseconds
7
+ */
2
8
  declare function setTooltipDelay(value: number): void;
3
9
  //#endregion
4
10
  export { setTooltipDelay };
@@ -1,9 +1,10 @@
1
+ import { Floatable } from "./floatable.mjs";
1
2
  import { attributable } from "./internal/attributable.mjs";
2
- import { Floatable } from "./internal/floatable.mjs";
3
+ import { isNullableOrWhitespace } from "@oscarpalmer/atoms/is";
4
+ import { isHTMLOrSVGElement } from "@oscarpalmer/toretto/is";
3
5
  import { on } from "@oscarpalmer/toretto/event";
4
6
  import { findAncestor } from "@oscarpalmer/toretto/find";
5
7
  import { createElement } from "@oscarpalmer/toretto/create";
6
- import { isNullableOrWhitespace } from "@oscarpalmer/atoms/is";
7
8
  //#region src/js/tooltip.ts
8
9
  var Tooltip = class {
9
10
  anchor;
@@ -15,7 +16,7 @@ var Tooltip = class {
15
16
  this.content = content;
16
17
  instances.set(content, this);
17
18
  anchor.insertAdjacentElement("afterend", content);
18
- this.floatable = new Floatable(anchor, content, options);
19
+ this.floatable = new Floatable(anchor, content, options, false);
19
20
  this.floatable.update();
20
21
  }
21
22
  destroy() {
@@ -51,8 +52,8 @@ function activate(event, focus) {
51
52
  function closeTooltip(instance) {
52
53
  if (instance == null) return;
53
54
  if (instance.timer != null) clearTimeout(instance.timer);
54
- instance.content.hidePopover();
55
- instance.content.removeAttribute(ATTRIBUTE_CONTENT_ACTIVE);
55
+ instance.content?.hidePopover();
56
+ instance.content?.removeAttribute(ATTRIBUTE_CONTENT_ACTIVE);
56
57
  }
57
58
  function createTooltip(anchor) {
58
59
  const content = getContent(anchor);
@@ -64,7 +65,7 @@ function findElements(attribute) {
64
65
  for (const id of ids) {
65
66
  if (isNullableOrWhitespace(id)) continue;
66
67
  const element = document.querySelector(`#${id}`);
67
- if (element instanceof HTMLElement) {
68
+ if (isHTMLOrSVGElement(element)) {
68
69
  const cloned = element.cloneNode(true);
69
70
  cloned.hidden = false;
70
71
  elements.push(cloned);
@@ -118,6 +119,12 @@ function onRemove(element) {
118
119
  instances.get(element)?.destroy();
119
120
  instances.delete(element);
120
121
  }
122
+ /**
123
+ * Set the delay before showing the tooltip, in milliseconds
124
+ *
125
+ * _(Default delay is half a second, or 500 milliseconds)_
126
+ * @param value Delay in milliseconds
127
+ */
121
128
  function setTooltipDelay(value) {
122
129
  if (typeof value === "number" && !Number.isNaN(value) && value >= 0) delay = value;
123
130
  }
@@ -134,7 +141,7 @@ const SELECTOR = `[${ATTRIBUTE}]`;
134
141
  const SELECTOR_CONTENT = `[${ATTRIBUTE_CONTENT}]`;
135
142
  const instances = /* @__PURE__ */ new WeakMap();
136
143
  const options = {
137
- attribute: "oui-tooltip-position",
144
+ attribute: `${ATTRIBUTE}-position`,
138
145
  position: "above"
139
146
  };
140
147
  let delay = 500;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oscarpalmer/oui",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Styles, components, and utilities for building websites and applications.",
5
5
  "keywords": [
6
6
  "css",
@@ -0,0 +1,237 @@
1
+ import {isPlainObject} from '@oscarpalmer/atoms/is';
2
+ import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
3
+
4
+ export class Floatable {
5
+ #destroyed = false;
6
+
7
+ /**
8
+ * Is the _Floatable_ open?
9
+ */
10
+ get open(): boolean {
11
+ return this.content.checkVisibility();
12
+ }
13
+
14
+ constructor(
15
+ public anchor: HTMLElement,
16
+ public content: HTMLElement,
17
+ public options: FloatableOptions,
18
+ standalone: boolean,
19
+ ) {
20
+ if (standalone) {
21
+ content.setAttribute(ATTRIBUTE_FLOATABLE, '');
22
+
23
+ content.popover = 'manual';
24
+ }
25
+
26
+ const anchorName = getAnchorName(anchor);
27
+
28
+ anchor.style.anchorName = anchorName;
29
+ content.style.positionAnchor = anchorName;
30
+ }
31
+
32
+ /**
33
+ * Destroy the floatable, removing it from the DOM and cleaning up references
34
+ */
35
+ destroy(): void {
36
+ if (this.#destroyed) {
37
+ return;
38
+ }
39
+
40
+ this.#destroyed = true;
41
+
42
+ removeFloatable(this.content);
43
+
44
+ this.anchor = null as never;
45
+ this.content = null as never;
46
+ }
47
+
48
+ /**
49
+ * Hide the floatable
50
+ */
51
+ hide(): void {
52
+ if (!this.#destroyed) {
53
+ this.content.hidePopover();
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Show the floatable
59
+ */
60
+ show(): void {
61
+ if (!this.#destroyed) {
62
+ this.content.showPopover();
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Update the position of the floatable, with an optional new position
68
+ *
69
+ * @param position Optional new position
70
+ */
71
+ update(position?: FloatablePosition): void {
72
+ if (!this.#destroyed) {
73
+ setPosition(this, position);
74
+ }
75
+ }
76
+ }
77
+
78
+ export type FloatableOptions = {
79
+ attribute?: string;
80
+ position: FloatablePosition;
81
+ };
82
+
83
+ export type FloatablePosition =
84
+ | 'above'
85
+ | 'above-end'
86
+ | 'above-start'
87
+ | 'below'
88
+ | 'below-end'
89
+ | 'below-start'
90
+ | 'end'
91
+ | 'end-bottom'
92
+ | 'end-top'
93
+ | 'start'
94
+ | 'start-bottom'
95
+ | 'start-top';
96
+
97
+ type GetFloatableOptions = {
98
+ /**
99
+ * Open the _Floatable_ when created? _(defaults to `true`)_
100
+ */
101
+ open?: boolean;
102
+ /**
103
+ * Position of the _Floatable_ _(defaults to `below`)_
104
+ */
105
+ position?: FloatablePosition;
106
+ };
107
+
108
+ export function createFloatable(
109
+ anchor: HTMLElement,
110
+ content: HTMLElement,
111
+ options: FloatableOptions,
112
+ standalone: boolean,
113
+ ): Floatable {
114
+ if (!isHTMLOrSVGElement(anchor) || !isHTMLOrSVGElement(content)) {
115
+ throw new TypeError('Anchor and content must be an HTMLElement or SVGElement.');
116
+ }
117
+
118
+ let floatable = instances.get(content);
119
+
120
+ if (floatable == null) {
121
+ floatable = new Floatable(anchor, content, options, standalone);
122
+
123
+ instances.set(content, floatable);
124
+ }
125
+
126
+ return floatable;
127
+ }
128
+
129
+ function getAnchorName(anchor: HTMLElement): string {
130
+ let {anchorName} = anchor.style;
131
+
132
+ if (anchorName.length > 0) {
133
+ return anchorName;
134
+ }
135
+
136
+ index += 1;
137
+
138
+ return `--oui-floatable-anchor-${index}`;
139
+ }
140
+
141
+ /**
142
+ * Create a _Floatable_ for an anchor and content element
143
+ *
144
+ * @param anchor Anchor element
145
+ * @param content Content element
146
+ * @param options _Floatable_ options
147
+ * @returns _Floatable_ instance
148
+ */
149
+ export function getFloatable(
150
+ anchor: HTMLElement | SVGElement,
151
+ content: HTMLElement | SVGElement,
152
+ options?: GetFloatableOptions,
153
+ ): Floatable {
154
+ const {open, position} = getOptions(options);
155
+
156
+ const floatable = createFloatable(
157
+ anchor as HTMLElement,
158
+ content as HTMLElement,
159
+ {
160
+ position,
161
+ },
162
+ true,
163
+ );
164
+
165
+ floatable?.update(position);
166
+
167
+ if (open) {
168
+ floatable?.show();
169
+ }
170
+
171
+ return floatable;
172
+ }
173
+
174
+ function getOptions(input?: GetFloatableOptions): Required<GetFloatableOptions> {
175
+ const object = isPlainObject(input) ? input : {};
176
+
177
+ return {
178
+ open: typeof object.open === 'boolean' ? object.open : true,
179
+ position: object.position != null && object.position in AREAS ? object.position : 'below',
180
+ };
181
+ }
182
+
183
+ function getPosition(floatable: Floatable, override?: FloatablePosition): FloatablePosition {
184
+ if (override != null && override in AREAS) {
185
+ return override;
186
+ }
187
+
188
+ if (floatable.options.attribute == null) {
189
+ return floatable.options.position;
190
+ }
191
+
192
+ const attribute = floatable.anchor.getAttribute(floatable.options.attribute) ?? '';
193
+
194
+ return attribute in AREAS ? (attribute as FloatablePosition) : floatable.options.position;
195
+ }
196
+
197
+ export function removeFloatable(content: HTMLElement): void {
198
+ instances.get(content)?.destroy();
199
+ instances.delete(content);
200
+ }
201
+
202
+ function setPosition(floatable: Floatable, override?: FloatablePosition): void {
203
+ const position = getPosition(floatable, override);
204
+
205
+ const area = AREAS[position];
206
+
207
+ floatable.content.style.positionArea = area;
208
+
209
+ floatable.content.setAttribute(ATTRIBUTE_POSITION, POSITIONS[area]);
210
+ }
211
+
212
+ const AREAS: Record<FloatablePosition, string> = {
213
+ above: 'block-start',
214
+ 'above-end': 'start span-start',
215
+ 'above-start': 'start span-end',
216
+ below: 'block-end',
217
+ 'below-end': 'end span-start',
218
+ 'below-start': 'end span-end',
219
+ end: 'center end',
220
+ 'end-bottom': 'span-start end',
221
+ 'end-top': 'span-end end',
222
+ start: 'center start',
223
+ 'start-bottom': 'span-start start',
224
+ 'start-top': 'span-end start',
225
+ };
226
+
227
+ export const ATTRIBUTE_FLOATABLE = 'oui-floatable';
228
+
229
+ const ATTRIBUTE_POSITION = 'oui-position';
230
+
231
+ const POSITIONS = Object.fromEntries(
232
+ Object.entries(AREAS).map(([position, area]) => [area, position]),
233
+ ) as Record<string, FloatablePosition>;
234
+
235
+ const instances = new WeakMap<HTMLElement, Floatable>();
236
+
237
+ let index = 0;
package/src/js/index.ts CHANGED
@@ -3,5 +3,6 @@ import './movable';
3
3
  import './popover';
4
4
  import './sortable';
5
5
 
6
+ export {getFloatable} from './floatable'
6
7
  export * from './splitter';
7
- export * from './tooltip';
8
+ export {setTooltipDelay} from './tooltip';
@@ -1,3 +1,5 @@
1
+ import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
2
+
1
3
  export function attributable(
2
4
  attribute: string,
3
5
  add: (element: HTMLElement) => void,
@@ -9,7 +11,7 @@ export function attributable(
9
11
  for (let index = 0; index < length; index += 1) {
10
12
  const entry = entries[index];
11
13
 
12
- if (entry.type === 'attributes' && entry.target instanceof HTMLElement) {
14
+ if (entry.type === 'attributes' && isHTMLOrSVGElement(entry.target)) {
13
15
  if (entry.target.hasAttribute(attribute)) {
14
16
  handleAdded(attribute, [entry.target], add);
15
17
  } else {
@@ -39,7 +41,7 @@ function handleAdded(
39
41
  nodes: Node[] | NodeList,
40
42
  add: (element: HTMLElement) => void,
41
43
  ): void {
42
- const elements = [...nodes].filter(node => node instanceof HTMLElement) as HTMLElement[];
44
+ const elements = [...nodes].filter(isHTMLOrSVGElement) as HTMLElement[];
43
45
 
44
46
  const {length} = elements;
45
47
 
@@ -57,7 +59,7 @@ function handleAdded(
57
59
  }
58
60
 
59
61
  function handleRemoved(nodes: Node[] | NodeList, remove: (element: HTMLElement) => void): void {
60
- const elements = [...nodes].filter(node => node instanceof HTMLElement) as HTMLElement[];
62
+ const elements = [...nodes].filter(isHTMLOrSVGElement) as HTMLElement[];
61
63
 
62
64
  const {length} = elements;
63
65
 
package/src/js/popover.ts CHANGED
@@ -1,13 +1,15 @@
1
1
  import {on} from '@oscarpalmer/toretto/event';
2
+ import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
2
3
  import type {RemovableEventListener} from '@oscarpalmer/toretto/models';
3
- import {createFocusTrap, removeFocusTrap, type FocusTrap} from './focus-trap/embedded';
4
- import {attributable} from './internal/attributable';
5
4
  import {
5
+ ATTRIBUTE_FLOATABLE,
6
6
  createFloatable,
7
7
  removeFloatable,
8
8
  type Floatable,
9
9
  type FloatableOptions,
10
- } from './internal/floatable';
10
+ } from './floatable';
11
+ import {createFocusTrap, removeFocusTrap, type FocusTrap} from './focus-trap/embedded';
12
+ import {attributable} from './internal/attributable';
11
13
 
12
14
  class Popover {
13
15
  anchor: HTMLElement | null;
@@ -16,9 +18,19 @@ class Popover {
16
18
  listener: RemovableEventListener;
17
19
 
18
20
  constructor(public element: HTMLElement) {
19
- this.anchor = element.ownerDocument.querySelector(`[popovertarget="${element.id}"]`);
21
+ const anchor = element.ownerDocument.querySelector(`[popovertarget="${element.id}"]`);
22
+
23
+ if (!isHTMLOrSVGElement(anchor)) {
24
+ throw new Error(`No anchor found for popover with id '${element.id}'.`);
25
+ }
26
+
27
+ if (element.contains(anchor)) {
28
+ throw new Error('Popover content cannot contain its anchor.');
29
+ }
30
+
31
+ this.anchor = anchor as HTMLElement;
20
32
 
21
- this.floatable = createFloatable(this.anchor as HTMLElement, element, options);
33
+ this.floatable = createFloatable(this.anchor, element, options, false);
22
34
 
23
35
  this.focusTrap = createFocusTrap(element, {
24
36
  noescape: true,
@@ -55,7 +67,11 @@ class Popover {
55
67
  }
56
68
 
57
69
  function onAdd(element: HTMLElement): void {
58
- if (!instances.has(element) && element.getAttribute(ATTRIBUTE) !== POPOVER_HINT) {
70
+ if (
71
+ !instances.has(element) &&
72
+ element.getAttribute(ATTRIBUTE) !== POPOVER_HINT &&
73
+ !element.hasAttribute(ATTRIBUTE_FLOATABLE)
74
+ ) {
59
75
  instances.set(element, new Popover(element));
60
76
  }
61
77
  }
@@ -83,7 +99,7 @@ const STATE_OPEN = 'open';
83
99
  const instances = new WeakMap<HTMLElement, Popover>();
84
100
 
85
101
  const options: FloatableOptions = {
86
- attribute: 'position',
102
+ attribute: `${ATTRIBUTE}position`,
87
103
  position: 'below-start',
88
104
  };
89
105
 
@@ -2,6 +2,7 @@ import type {EventPosition, PlainObject} from '@oscarpalmer/atoms/models';
2
2
  import {createElement} from '@oscarpalmer/toretto/create';
3
3
  import {getPosition} from '@oscarpalmer/toretto/event';
4
4
  import {findAncestor, getElementFromPosition} from '@oscarpalmer/toretto/find';
5
+ import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
5
6
  import {attributable} from './internal/attributable';
6
7
  import {
7
8
  addDraggable,
@@ -333,17 +334,19 @@ function setPlaceholder(state: DraggableState, sortable: Sortable, origin: HTMLE
333
334
  parent: sortable.element,
334
335
  placeholder: element,
335
336
  create: (value: unknown) => {
336
- if (!(value instanceof HTMLElement)) {
337
+ if (!isHTMLOrSVGElement(value)) {
337
338
  return;
338
339
  }
339
340
 
340
- placeholder.drag.custom = value;
341
+ const element = value as HTMLElement;
341
342
 
342
- updatePlaceholder(sortable, value);
343
+ placeholder.drag.custom = element;
344
+
345
+ updatePlaceholder(sortable, element);
343
346
 
344
347
  state.element = {
345
- rectangle: value.getBoundingClientRect(),
346
- node: value,
348
+ rectangle: element.getBoundingClientRect(),
349
+ node: element,
347
350
  };
348
351
  },
349
352
  },
@@ -12,8 +12,21 @@ declare global {
12
12
  }
13
13
  }
14
14
 
15
+ const ATTRIBUTE_MAX = 'max';
16
+
17
+ const ATTRIBUTE_MIN = 'min';
18
+
19
+ const ATTRIBUTE_SIZE = 'size';
20
+
21
+ const ATTRIBUTE_TYPE = 'type';
22
+
15
23
  export class OuiSplitterElement extends HTMLElement {
16
- static readonly observedAttributes = ['max', 'min', 'size', 'type'];
24
+ static readonly observedAttributes = [
25
+ ATTRIBUTE_MAX,
26
+ ATTRIBUTE_MIN,
27
+ ATTRIBUTE_SIZE,
28
+ ATTRIBUTE_TYPE,
29
+ ];
17
30
 
18
31
  readonly #splitter: Splitter;
19
32
 
@@ -24,7 +37,7 @@ export class OuiSplitterElement extends HTMLElement {
24
37
  }
25
38
 
26
39
  set max(value: number) {
27
- this.setAttribute('max', String(value));
40
+ this.setAttribute(ATTRIBUTE_MAX, String(value));
28
41
  }
29
42
 
30
43
  get min(): number {
@@ -77,13 +90,13 @@ export class OuiSplitterElement extends HTMLElement {
77
90
 
78
91
  attributeChangedCallback(name: string): void {
79
92
  switch (name) {
80
- case 'max':
81
- case 'min':
82
- case 'size':
93
+ case ATTRIBUTE_MAX:
94
+ case ATTRIBUTE_MIN:
95
+ case ATTRIBUTE_SIZE:
83
96
  this.#splitter.updateValues();
84
97
  break;
85
98
 
86
- case 'type':
99
+ case ATTRIBUTE_TYPE:
87
100
  this.#splitter.updateOrientation();
88
101
  break;
89
102
 
@@ -271,15 +284,19 @@ function getValue(element: HTMLElement, name: string, defaultValue: number): num
271
284
 
272
285
  function getValues(element: OuiSplitterElement): Values {
273
286
  const values: Values = {
274
- max: getValue(element, 'max', NAVIGATION_MAXIMUM_PERCENTAGE),
275
- min: getValue(element, 'min', NAVIGATION_MINIMUM_PERCENTAGE),
287
+ max: getValue(element, ATTRIBUTE_MAX, NAVIGATION_MAXIMUM_PERCENTAGE),
288
+ min: getValue(element, ATTRIBUTE_MIN, NAVIGATION_MINIMUM_PERCENTAGE),
276
289
  now: {
277
290
  current: 0.5,
278
291
  previous: 0.5,
279
292
  },
280
293
  };
281
294
 
282
- const value = getContained(values.min, getValue(element, 'size', values.now.current), values.max);
295
+ const value = getContained(
296
+ values.min,
297
+ getValue(element, ATTRIBUTE_SIZE, values.now.current),
298
+ values.max,
299
+ );
283
300
 
284
301
  values.now.current = value;
285
302
  values.now.previous = value;
package/src/js/tooltip.ts CHANGED
@@ -2,8 +2,9 @@ import {isNullableOrWhitespace} from '@oscarpalmer/atoms/is';
2
2
  import {createElement} from '@oscarpalmer/toretto/create';
3
3
  import {on} from '@oscarpalmer/toretto/event';
4
4
  import {findAncestor} from '@oscarpalmer/toretto/find';
5
+ import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
5
6
  import {attributable} from './internal/attributable';
6
- import {Floatable, type FloatableOptions} from './internal/floatable';
7
+ import {Floatable, type FloatableOptions} from './floatable';
7
8
 
8
9
  class Tooltip {
9
10
  floatable: Floatable;
@@ -18,7 +19,7 @@ class Tooltip {
18
19
 
19
20
  anchor.insertAdjacentElement('afterend', content);
20
21
 
21
- this.floatable = new Floatable(anchor, content, options);
22
+ this.floatable = new Floatable(anchor, content, options, false);
22
23
 
23
24
  this.floatable.update();
24
25
  }
@@ -61,7 +62,7 @@ function activate(event: Event, focus: boolean): void {
61
62
  instance.timer = setTimeout(
62
63
  () => {
63
64
  instance.content.showPopover({
64
- source: instance.anchor,
65
+ source: instance.anchor as HTMLElement,
65
66
  });
66
67
 
67
68
  setTimeout(() => {
@@ -83,8 +84,8 @@ function closeTooltip(instance?: Tooltip): void {
83
84
  clearTimeout(instance.timer);
84
85
  }
85
86
 
86
- instance.content.hidePopover();
87
- instance.content.removeAttribute(ATTRIBUTE_CONTENT_ACTIVE);
87
+ instance.content?.hidePopover();
88
+ instance.content?.removeAttribute(ATTRIBUTE_CONTENT_ACTIVE);
88
89
  }
89
90
 
90
91
  function createTooltip(anchor: HTMLElement): void {
@@ -106,7 +107,7 @@ function findElements(attribute: string | null): HTMLElement[] {
106
107
 
107
108
  const element = document.querySelector(`#${id}`);
108
109
 
109
- if (element instanceof HTMLElement) {
110
+ if (isHTMLOrSVGElement(element)) {
110
111
  const cloned = element.cloneNode(true) as HTMLElement;
111
112
 
112
113
  cloned.hidden = false;
@@ -212,6 +213,12 @@ function onRemove(element: HTMLElement): void {
212
213
  instances.delete(element);
213
214
  }
214
215
 
216
+ /**
217
+ * Set the delay before showing the tooltip, in milliseconds
218
+ *
219
+ * _(Default delay is half a second, or 500 milliseconds)_
220
+ * @param value Delay in milliseconds
221
+ */
215
222
  export function setTooltipDelay(value: number): void {
216
223
  if (typeof value === 'number' && !Number.isNaN(value) && value >= 0) {
217
224
  delay = value;
@@ -243,7 +250,7 @@ const SELECTOR_CONTENT = `[${ATTRIBUTE_CONTENT}]`;
243
250
  const instances = new WeakMap<HTMLElement, Tooltip>();
244
251
 
245
252
  const options: FloatableOptions = {
246
- attribute: 'oui-tooltip-position',
253
+ attribute: `${ATTRIBUTE}-position`,
247
254
  position: 'above',
248
255
  };
249
256
 
@@ -1,18 +0,0 @@
1
- //#region src/js/internal/floatable.d.ts
2
- declare class Floatable {
3
- anchor: HTMLElement;
4
- content: HTMLElement;
5
- options: FloatableOptions;
6
- constructor(anchor: HTMLElement, content: HTMLElement, options: FloatableOptions);
7
- destroy(): void;
8
- update(): void;
9
- }
10
- type FloatableOptions = {
11
- attribute: string;
12
- position: FloatablePosition;
13
- };
14
- type FloatablePosition = 'above' | 'above-end' | 'above-start' | 'below' | 'below-end' | 'below-start' | 'end' | 'end-bottom' | 'end-top' | 'start' | 'start-bottom' | 'start-top';
15
- declare function createFloatable(anchor: HTMLElement | null, content: HTMLElement, options: FloatableOptions): Floatable | undefined;
16
- declare function removeFloatable(content: HTMLElement): void;
17
- //#endregion
18
- export { Floatable, FloatableOptions, FloatablePosition, createFloatable, removeFloatable };
@@ -1,67 +0,0 @@
1
- //#region src/js/internal/floatable.ts
2
- var Floatable = class {
3
- anchor;
4
- content;
5
- options;
6
- constructor(anchor, content, options) {
7
- this.anchor = anchor;
8
- this.content = content;
9
- this.options = options;
10
- const anchorName = getAnchorName(anchor);
11
- anchor.style.anchorName = anchorName;
12
- content.style.positionAnchor = anchorName;
13
- }
14
- destroy() {
15
- this.anchor = null;
16
- this.content = null;
17
- }
18
- update() {
19
- setPosition(this);
20
- }
21
- };
22
- function createFloatable(anchor, content, options) {
23
- if (anchor == null) return;
24
- let floatable = instances.get(content);
25
- if (floatable == null) {
26
- floatable = new Floatable(anchor, content, options);
27
- instances.set(content, floatable);
28
- }
29
- return floatable;
30
- }
31
- function getAnchorName(anchor) {
32
- let { anchorName } = anchor.style;
33
- if (anchorName.length > 0) return anchorName;
34
- index += 1;
35
- return `--oui-floatable-anchor-${index}`;
36
- }
37
- function removeFloatable(content) {
38
- instances.get(content)?.destroy();
39
- instances.delete(content);
40
- }
41
- function setPosition(floatable) {
42
- let position = floatable.anchor.getAttribute(floatable.options.attribute) ?? floatable.options.position;
43
- if (AREAS[position] == null) position = floatable.options.position;
44
- const area = AREAS[position];
45
- floatable.content.style.positionArea = area;
46
- floatable.content.setAttribute(ATTRIBUTE, POSITIONS[area]);
47
- }
48
- const AREAS = {
49
- above: "block-start",
50
- "above-end": "start span-start",
51
- "above-start": "start span-end",
52
- below: "block-end",
53
- "below-end": "end span-start",
54
- "below-start": "end span-end",
55
- end: "center end",
56
- "end-bottom": "span-start end",
57
- "end-top": "span-end end",
58
- start: "center start",
59
- "start-bottom": "span-start start",
60
- "start-top": "span-end start"
61
- };
62
- const ATTRIBUTE = "oui-position";
63
- const POSITIONS = Object.fromEntries(Object.entries(AREAS).map(([position, area]) => [area, position]));
64
- const instances = /* @__PURE__ */ new WeakMap();
65
- let index = 0;
66
- //#endregion
67
- export { Floatable, createFloatable, removeFloatable };
@@ -1,118 +0,0 @@
1
- export class Floatable {
2
- constructor(
3
- public anchor: HTMLElement,
4
- public content: HTMLElement,
5
- public options: FloatableOptions,
6
- ) {
7
- const anchorName = getAnchorName(anchor);
8
-
9
- anchor.style.anchorName = anchorName;
10
- content.style.positionAnchor = anchorName;
11
- }
12
-
13
- destroy(): void {
14
- this.anchor = null as never;
15
- this.content = null as never;
16
- }
17
-
18
- update(): void {
19
- setPosition(this);
20
- }
21
- }
22
-
23
- export type FloatableOptions = {
24
- attribute: string;
25
- position: FloatablePosition;
26
- };
27
-
28
- export type FloatablePosition =
29
- | 'above'
30
- | 'above-end'
31
- | 'above-start'
32
- | 'below'
33
- | 'below-end'
34
- | 'below-start'
35
- | 'end'
36
- | 'end-bottom'
37
- | 'end-top'
38
- | 'start'
39
- | 'start-bottom'
40
- | 'start-top';
41
-
42
- export function createFloatable(
43
- anchor: HTMLElement | null,
44
- content: HTMLElement,
45
- options: FloatableOptions,
46
- ): Floatable | undefined {
47
- if (anchor == null) {
48
- return;
49
- }
50
-
51
- let floatable = instances.get(content);
52
-
53
- if (floatable == null) {
54
- floatable = new Floatable(anchor, content, options);
55
-
56
- instances.set(content, floatable);
57
- }
58
-
59
- return floatable;
60
- }
61
-
62
- function getAnchorName(anchor: HTMLElement): string {
63
- let {anchorName} = anchor.style;
64
-
65
- if (anchorName.length > 0) {
66
- return anchorName;
67
- }
68
-
69
- index += 1;
70
-
71
- return `--oui-floatable-anchor-${index}`;
72
- }
73
-
74
- export function removeFloatable(content: HTMLElement): void {
75
- instances.get(content)?.destroy();
76
- instances.delete(content);
77
- }
78
-
79
- function setPosition(floatable: Floatable): void {
80
- let position: FloatablePosition =
81
- (floatable.anchor.getAttribute(floatable.options.attribute) as FloatablePosition) ??
82
- floatable.options.position;
83
-
84
- if (AREAS[position] == null) {
85
- position = floatable.options.position;
86
- }
87
-
88
- const area = AREAS[position];
89
-
90
- floatable.content.style.positionArea = area;
91
-
92
- floatable.content.setAttribute(ATTRIBUTE, POSITIONS[area]);
93
- }
94
-
95
- const AREAS: Record<FloatablePosition, string> = {
96
- above: 'block-start',
97
- 'above-end': 'start span-start',
98
- 'above-start': 'start span-end',
99
- below: 'block-end',
100
- 'below-end': 'end span-start',
101
- 'below-start': 'end span-end',
102
- end: 'center end',
103
- 'end-bottom': 'span-start end',
104
- 'end-top': 'span-end end',
105
- start: 'center start',
106
- 'start-bottom': 'span-start start',
107
- 'start-top': 'span-end start',
108
- };
109
-
110
- const ATTRIBUTE = 'oui-position';
111
-
112
- const POSITIONS = Object.fromEntries(
113
- Object.entries(AREAS).map(([position, area]) => [area, position]),
114
- ) as Record<string, FloatablePosition>;
115
-
116
- const instances = new WeakMap<HTMLElement, Floatable>();
117
-
118
- let index = 0;