@oscarpalmer/oui 0.24.0 → 0.26.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/{tooltip.css → floatable.css} +59 -24
- package/dist/css/reset.css +17 -0
- package/dist/js/dialog.d.mts +38 -0
- package/dist/js/dialog.mjs +92 -0
- package/dist/js/embedded-CHK82Uh5.d.mts +76 -0
- package/dist/js/embedded-ai6cqURc.d.mts +68 -0
- package/dist/js/floatable/embedded.d.mts +2 -0
- package/dist/js/floatable/embedded.mjs +157 -0
- package/dist/js/floatable/standalone.d.mts +2 -0
- package/dist/js/floatable/standalone.mjs +2 -0
- package/dist/js/focus-trap/embedded.d.mts +2 -26
- package/dist/js/focus-trap/embedded.mjs +158 -80
- package/dist/js/focus-trap/standalone.d.mts +2 -1
- package/dist/js/focus-trap/standalone.mjs +3 -3
- package/dist/js/index.d.mts +6 -3
- package/dist/js/index.mjs +9 -5
- package/dist/js/internal/draggable.d.mts +2 -2
- package/dist/js/internal/draggable.mjs +3 -3
- package/dist/js/{movable-BKFgoaOl.d.mts → movable-dNqBctx3.d.mts} +16 -16
- package/dist/js/movable.d.mts +2 -2
- package/dist/js/movable.mjs +4 -4
- package/dist/js/popover.d.mts +38 -1
- package/dist/js/popover.mjs +83 -44
- package/dist/js/sortable.d.mts +2 -2
- package/dist/js/sortable.mjs +5 -5
- package/dist/js/splitter.mjs +49 -48
- package/dist/js/toolbar.d.mts +1 -0
- package/dist/js/toolbar.mjs +107 -0
- package/dist/js/tooltip.d.mts +27 -1
- package/dist/js/tooltip.mjs +85 -49
- package/package.json +14 -2
- package/src/css/{tooltip.scss → floatable.scss} +47 -5
- package/src/css/mixins.scss +0 -2
- package/src/css/reset.scss +17 -0
- package/src/js/dialog.ts +188 -0
- package/src/js/floatable/embedded.ts +323 -0
- package/src/js/floatable/standalone.ts +1 -0
- package/src/js/focus-trap/embedded.ts +219 -93
- package/src/js/focus-trap/standalone.ts +4 -2
- package/src/js/index.ts +6 -4
- package/src/js/internal/draggable.ts +20 -20
- package/src/js/movable.ts +17 -17
- package/src/js/popover.ts +143 -61
- package/src/js/sortable.ts +26 -26
- package/src/js/splitter.ts +98 -96
- package/src/js/toolbar.ts +214 -0
- package/src/js/tooltip.ts +138 -54
- package/dist/css/popover.css +0 -30
- package/dist/js/floatable.d.mts +0 -59
- package/dist/js/floatable.mjs +0 -130
- package/src/css/popover.scss +0 -10
- package/src/js/floatable.ts +0 -237
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oscarpalmer/oui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "Styles, components, and utilities for building websites and applications.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"css",
|
|
@@ -33,6 +33,14 @@
|
|
|
33
33
|
"types": "./dist/js/index.d.mts",
|
|
34
34
|
"default": "./dist/js/index.mjs"
|
|
35
35
|
},
|
|
36
|
+
"./dialog": {
|
|
37
|
+
"types": "./dist/js/dialog.d.mts",
|
|
38
|
+
"default": "./dist/js/dialog.mjs"
|
|
39
|
+
},
|
|
40
|
+
"./floatable": {
|
|
41
|
+
"types": "./dist/js/floatable/standalone.d.mts",
|
|
42
|
+
"default": "./dist/js/floatable/standalone.mjs"
|
|
43
|
+
},
|
|
36
44
|
"./focus-trap": {
|
|
37
45
|
"types": "./dist/js/focus-trap/standalone.d.mts",
|
|
38
46
|
"default": "./dist/js/focus-trap/standalone.mjs"
|
|
@@ -53,6 +61,10 @@
|
|
|
53
61
|
"types": "./dist/js/splitter.d.mts",
|
|
54
62
|
"default": "./dist/js/splitter.mjs"
|
|
55
63
|
},
|
|
64
|
+
"./toolbar": {
|
|
65
|
+
"types": "./dist/js/toolbar.d.mts",
|
|
66
|
+
"default": "./dist/js/toolbar.mjs"
|
|
67
|
+
},
|
|
56
68
|
"./tooltip": {
|
|
57
69
|
"types": "./dist/js/tooltip.d.mts",
|
|
58
70
|
"default": "./dist/js/tooltip.mjs"
|
|
@@ -74,7 +86,7 @@
|
|
|
74
86
|
"@oscarpalmer/toretto": "^0.46"
|
|
75
87
|
},
|
|
76
88
|
"devDependencies": {
|
|
77
|
-
"@oxlint/plugins": "^1.
|
|
89
|
+
"@oxlint/plugins": "^1.70",
|
|
78
90
|
"@vitest/coverage-istanbul": "^4.1",
|
|
79
91
|
"autoprefixer": "^10.5",
|
|
80
92
|
"concurrently": "^10",
|
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
@use 'mixins.scss' as mixins;
|
|
2
2
|
|
|
3
|
+
:where([oui-dialog], [oui-popover], [oui-tooltip-content]),
|
|
4
|
+
:where([oui-dialog])::backdrop {
|
|
5
|
+
opacity: 0;
|
|
6
|
+
transition: 0.125s ease-in-out;
|
|
7
|
+
transition-property: opacity, scale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
:where([oui-dialog], [oui-popover], [oui-tooltip-content]) {
|
|
11
|
+
@include mixins.shadow-overlay;
|
|
12
|
+
|
|
13
|
+
background-color: var(--oui-absolute);
|
|
14
|
+
scale: 0.9;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
:where([oui-dialog]:focus, [oui-popover]:focus, [oui-tooltip-content]:focus) {
|
|
18
|
+
outline: none;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
:where(
|
|
22
|
+
[oui-dialog][oui-dialog-open],
|
|
23
|
+
[oui-popover][oui-popover-open],
|
|
24
|
+
[oui-tooltip-content][oui-tooltip-content-open]
|
|
25
|
+
),
|
|
26
|
+
:where([oui-dialog][oui-dialog-open])::backdrop {
|
|
27
|
+
opacity: 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
:where(
|
|
31
|
+
[oui-dialog][oui-dialog-open],
|
|
32
|
+
[oui-popover][oui-popover-open],
|
|
33
|
+
[oui-tooltip-content][oui-tooltip-content-open]
|
|
34
|
+
) {
|
|
35
|
+
scale: 1;
|
|
36
|
+
transition-delay: 0.125s;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
:where([oui-popover], [oui-tooltip-content]) {
|
|
40
|
+
@include mixins.floatable;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
:where([oui-popover]) {
|
|
44
|
+
padding: 1rem;
|
|
45
|
+
background-color: var(--oui-absolute);
|
|
46
|
+
border-radius: 0.25rem;
|
|
47
|
+
}
|
|
48
|
+
|
|
3
49
|
:where(
|
|
4
50
|
[oui-tooltip]:is(
|
|
5
51
|
abbr,
|
|
@@ -29,8 +75,6 @@
|
|
|
29
75
|
}
|
|
30
76
|
|
|
31
77
|
:where([oui-tooltip-content]) {
|
|
32
|
-
@include mixins.floatable;
|
|
33
|
-
|
|
34
78
|
max-width: 48ch;
|
|
35
79
|
padding: 0.75em 1em;
|
|
36
80
|
overflow: visible;
|
|
@@ -39,8 +83,6 @@
|
|
|
39
83
|
border-radius: 0.25rem;
|
|
40
84
|
font-size: 0.875rem;
|
|
41
85
|
pointer-events: none;
|
|
42
|
-
transition: opacity 0.125s ease-in-out 0s;
|
|
43
|
-
transition-behavior: allow-discrete;
|
|
44
86
|
|
|
45
87
|
&::after {
|
|
46
88
|
content: '';
|
|
@@ -97,7 +139,7 @@
|
|
|
97
139
|
}
|
|
98
140
|
}
|
|
99
141
|
|
|
100
|
-
:where([oui-tooltip-content][oui-tooltip-content-
|
|
142
|
+
:where([oui-tooltip-content][oui-tooltip-content-open]) {
|
|
101
143
|
opacity: 1;
|
|
102
144
|
pointer-events: all;
|
|
103
145
|
}
|
package/src/css/mixins.scss
CHANGED
package/src/css/reset.scss
CHANGED
|
@@ -121,6 +121,23 @@
|
|
|
121
121
|
display: list-item;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
:where(dialog) {
|
|
125
|
+
display: none !important;
|
|
126
|
+
overscroll-behavior: contain;
|
|
127
|
+
border: none;
|
|
128
|
+
color: currentcolor;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
:where([oui-dialog][aria-modal='true' i]) {
|
|
132
|
+
position: fixed;
|
|
133
|
+
inset: 50% auto auto 50%;
|
|
134
|
+
transform: translate3d(-50%, -50%, 0);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
:where(dialog[open]) {
|
|
138
|
+
display: inherit !important;
|
|
139
|
+
}
|
|
140
|
+
|
|
124
141
|
:where(dl, ol, ul) :where(dl, ol, ul) {
|
|
125
142
|
/** Remove the margin on nested lists in Chrome, Edge, and Safari [SE] */
|
|
126
143
|
margin: 0;
|
package/src/js/dialog.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import {on} from '@oscarpalmer/toretto/event';
|
|
2
|
+
import type {RemovableEventListener} from '@oscarpalmer/toretto/models';
|
|
3
|
+
import {getOnBeforeToggleListener} from './floatable/embedded';
|
|
4
|
+
import {attributable} from './internal/attributable';
|
|
5
|
+
|
|
6
|
+
// #region Types
|
|
7
|
+
|
|
8
|
+
class OuiDialog {
|
|
9
|
+
readonly #state: OuiDialogState;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Is the dialog open?
|
|
13
|
+
*/
|
|
14
|
+
get open(): boolean {
|
|
15
|
+
return this.#state.element.open;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
constructor(state: OuiDialogState) {
|
|
19
|
+
this.#state = state;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Closes the dialog by request
|
|
24
|
+
*/
|
|
25
|
+
hide(request: true, value?: string): void;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Closes the dialog
|
|
29
|
+
*/
|
|
30
|
+
hide(value?: string): void;
|
|
31
|
+
|
|
32
|
+
hide(first?: string | boolean, second?: string): void {
|
|
33
|
+
if (!this.#state.element.open) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (first === true) {
|
|
38
|
+
this.#state.element.requestClose(typeof second === 'string' ? second : undefined);
|
|
39
|
+
} else {
|
|
40
|
+
this.#state.element.close(typeof first === 'string' ? first : undefined);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Opens the dialog
|
|
46
|
+
*/
|
|
47
|
+
show(modal?: boolean): void {
|
|
48
|
+
const {element} = this.#state;
|
|
49
|
+
|
|
50
|
+
if (element == null || element.open) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const isModal = modal !== false;
|
|
55
|
+
|
|
56
|
+
element.setAttribute(ARIA_MODAL, String(isModal));
|
|
57
|
+
|
|
58
|
+
if (isModal) {
|
|
59
|
+
element.showModal();
|
|
60
|
+
} else {
|
|
61
|
+
element.show();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type OuiDialogState = {
|
|
67
|
+
dialog: OuiDialog;
|
|
68
|
+
element: HTMLDialogElement;
|
|
69
|
+
listeners: RemovableEventListener[];
|
|
70
|
+
modal: boolean;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// #endregion
|
|
74
|
+
|
|
75
|
+
// #region Functions
|
|
76
|
+
|
|
77
|
+
function addDialog(element: HTMLElement): void {
|
|
78
|
+
if (!(element instanceof HTMLDialogElement)) {
|
|
79
|
+
throw new TypeError(MESSAGE);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!states.has(element)) {
|
|
83
|
+
states.set(element, getState(element));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function destroyDialog(state: OuiDialogState): void {
|
|
88
|
+
state.element.close();
|
|
89
|
+
|
|
90
|
+
state.element.setAttribute(ARIA_MODAL, String(state.modal));
|
|
91
|
+
state.element.removeAttribute(ATTRIBUTE_OPEN);
|
|
92
|
+
|
|
93
|
+
for (const listener of state.listeners) {
|
|
94
|
+
listener();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
state.dialog = undefined as never;
|
|
98
|
+
state.element = undefined as never;
|
|
99
|
+
state.listeners = undefined as never;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get the _OuiDialog_ for an element
|
|
104
|
+
*
|
|
105
|
+
* @param element Element to get _OuiDialog_ for
|
|
106
|
+
* @returns _OuiDialog_ instance
|
|
107
|
+
*/
|
|
108
|
+
function getDialog(element: HTMLDialogElement): OuiDialog | undefined {
|
|
109
|
+
return states.get(element)?.dialog;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getState(element: HTMLDialogElement): OuiDialogState {
|
|
113
|
+
const state: OuiDialogState = {
|
|
114
|
+
element,
|
|
115
|
+
dialog: undefined as never,
|
|
116
|
+
listeners: [
|
|
117
|
+
getOnBeforeToggleListener(element),
|
|
118
|
+
on(element, EVENT_TOGGLE, event => {
|
|
119
|
+
const {source} = event;
|
|
120
|
+
|
|
121
|
+
if (element.open) {
|
|
122
|
+
const modal =
|
|
123
|
+
(source instanceof HTMLButtonElement && source.command === COMMAND_SHOW_MODAL) ||
|
|
124
|
+
element.matches(SELECTOR_MODAL);
|
|
125
|
+
|
|
126
|
+
element.setAttribute(ARIA_MODAL, String(modal));
|
|
127
|
+
element.setAttribute(ATTRIBUTE_OPEN, '');
|
|
128
|
+
} else {
|
|
129
|
+
element.removeAttribute(ATTRIBUTE_OPEN);
|
|
130
|
+
element.setAttribute(ARIA_MODAL, String(state.modal));
|
|
131
|
+
}
|
|
132
|
+
}),
|
|
133
|
+
],
|
|
134
|
+
modal: element.getAttribute(ARIA_MODAL) === TRUE,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
state.dialog = new OuiDialog(state);
|
|
138
|
+
|
|
139
|
+
return state;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function removeDialog(element: HTMLElement): void {
|
|
143
|
+
const state = states.get(element);
|
|
144
|
+
|
|
145
|
+
if (state == null) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
destroyDialog(state);
|
|
150
|
+
|
|
151
|
+
states.delete(element);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// #endregion
|
|
155
|
+
|
|
156
|
+
// #region Variables
|
|
157
|
+
|
|
158
|
+
const ARIA_MODAL = 'aria-modal';
|
|
159
|
+
|
|
160
|
+
const ATTRIBUTE = 'oui-dialog';
|
|
161
|
+
|
|
162
|
+
const ATTRIBUTE_OPEN = `${ATTRIBUTE}-open`;
|
|
163
|
+
|
|
164
|
+
const COMMAND_SHOW_MODAL = 'show-modal';
|
|
165
|
+
|
|
166
|
+
const EVENT_TOGGLE = 'toggle';
|
|
167
|
+
|
|
168
|
+
const MESSAGE = 'The element must be an instance of HTMLDialogElement';
|
|
169
|
+
|
|
170
|
+
const SELECTOR_MODAL = ':modal';
|
|
171
|
+
|
|
172
|
+
const TRUE = 'true';
|
|
173
|
+
|
|
174
|
+
const states = new WeakMap<HTMLElement, OuiDialogState>();
|
|
175
|
+
|
|
176
|
+
// #endregion
|
|
177
|
+
|
|
178
|
+
// #region Initialization
|
|
179
|
+
|
|
180
|
+
attributable(ATTRIBUTE, addDialog, removeDialog);
|
|
181
|
+
|
|
182
|
+
// #endregion
|
|
183
|
+
|
|
184
|
+
// #region Exports
|
|
185
|
+
|
|
186
|
+
export {getDialog, type OuiDialog};
|
|
187
|
+
|
|
188
|
+
// #endregion
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import {isPlainObject} from '@oscarpalmer/atoms/is';
|
|
2
|
+
import {on} from '@oscarpalmer/toretto/event';
|
|
3
|
+
import {isHTMLOrSVGElement} from '@oscarpalmer/toretto/is';
|
|
4
|
+
import type {RemovableEventListener} from '@oscarpalmer/toretto/models';
|
|
5
|
+
|
|
6
|
+
// #region Types
|
|
7
|
+
|
|
8
|
+
type CreateOuiFloatableOptions = {
|
|
9
|
+
/**
|
|
10
|
+
* Open the _OuiFloatable_ when created? _(defaults to `true`)_
|
|
11
|
+
*/
|
|
12
|
+
open?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Position of the _OuiFloatable_ _(defaults to `below`)_
|
|
15
|
+
*/
|
|
16
|
+
position?: OuiFloatablePosition;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export class OuiFloatable {
|
|
20
|
+
#destroyed = false;
|
|
21
|
+
|
|
22
|
+
readonly #state: OuiFloatableState;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Is the _OuiFloatable_ open?
|
|
26
|
+
*/
|
|
27
|
+
get open(): boolean {
|
|
28
|
+
return this.#state.content.checkVisibility();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Open or close the _OuiFloatable_
|
|
33
|
+
*/
|
|
34
|
+
set open(value: boolean) {
|
|
35
|
+
if (value === true) {
|
|
36
|
+
this.show();
|
|
37
|
+
} else if (value === false) {
|
|
38
|
+
this.hide();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
constructor(state: OuiFloatableState, standalone: boolean) {
|
|
43
|
+
this.#state = state;
|
|
44
|
+
|
|
45
|
+
const {anchor, content} = state;
|
|
46
|
+
|
|
47
|
+
if (standalone) {
|
|
48
|
+
content.setAttribute(ATTRIBUTE_FLOATABLE, '');
|
|
49
|
+
|
|
50
|
+
content.popover = 'manual';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const anchorName = getAnchorName(anchor);
|
|
54
|
+
|
|
55
|
+
anchor.style.anchorName = anchorName;
|
|
56
|
+
content.style.positionAnchor = anchorName;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Destroy the _OuiFloatable_, removing it from the DOM and cleaning up references
|
|
61
|
+
*/
|
|
62
|
+
destroy(): void {
|
|
63
|
+
if (this.#destroyed) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.#destroyed = true;
|
|
68
|
+
|
|
69
|
+
removeFloatable(this.#state.content);
|
|
70
|
+
|
|
71
|
+
this.#state.anchor = null as never;
|
|
72
|
+
this.#state.content = null as never;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Closes the _OuiFloatable_
|
|
77
|
+
*/
|
|
78
|
+
hide(): void {
|
|
79
|
+
if (!this.#destroyed && this.open) {
|
|
80
|
+
this.#state.content.hidePopover();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Opens the _OuiFloatable_
|
|
86
|
+
*/
|
|
87
|
+
show(): void {
|
|
88
|
+
if (!this.#destroyed && !this.open) {
|
|
89
|
+
this.#state.content.showPopover();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Update the position of the _OuiFloatable_, with an optional new position
|
|
95
|
+
*
|
|
96
|
+
* @param position Optional new position
|
|
97
|
+
*/
|
|
98
|
+
update(position?: OuiFloatablePosition): void {
|
|
99
|
+
if (!this.#destroyed) {
|
|
100
|
+
setPosition(this.#state, position);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type OuiFloatableOptions = {
|
|
106
|
+
attribute?: string;
|
|
107
|
+
position: OuiFloatablePosition;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export type OuiFloatablePosition =
|
|
111
|
+
| 'above'
|
|
112
|
+
| 'above-end'
|
|
113
|
+
| 'above-start'
|
|
114
|
+
| 'below'
|
|
115
|
+
| 'below-end'
|
|
116
|
+
| 'below-start'
|
|
117
|
+
| 'end'
|
|
118
|
+
| 'end-bottom'
|
|
119
|
+
| 'end-top'
|
|
120
|
+
| 'start'
|
|
121
|
+
| 'start-bottom'
|
|
122
|
+
| 'start-top';
|
|
123
|
+
|
|
124
|
+
type OuiFloatableState = {
|
|
125
|
+
anchor: HTMLElement;
|
|
126
|
+
content: HTMLElement;
|
|
127
|
+
options: OuiFloatableOptions;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// #endregion
|
|
131
|
+
|
|
132
|
+
// #region Functions
|
|
133
|
+
|
|
134
|
+
function getAnchorName(anchor: HTMLElement): string {
|
|
135
|
+
let {anchorName} = anchor.style;
|
|
136
|
+
|
|
137
|
+
if (anchorName.length > 0) {
|
|
138
|
+
return anchorName;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
index += 1;
|
|
142
|
+
|
|
143
|
+
return `--oui-floatable-anchor-${index}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function createEmbeddedFloatable(
|
|
147
|
+
anchor: HTMLElement,
|
|
148
|
+
content: HTMLElement,
|
|
149
|
+
options: OuiFloatableOptions,
|
|
150
|
+
): OuiFloatable {
|
|
151
|
+
return createInstance(anchor, content, options, false);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Create a _OuiFloatable_ for an anchor and content element
|
|
156
|
+
*
|
|
157
|
+
* @param anchor Anchor element
|
|
158
|
+
* @param content Content element
|
|
159
|
+
* @param options _OuiFloatable_ options
|
|
160
|
+
* @returns _OuiFloatable_ instance
|
|
161
|
+
*/
|
|
162
|
+
export function createFloatable(
|
|
163
|
+
anchor: HTMLElement,
|
|
164
|
+
content: HTMLElement,
|
|
165
|
+
options?: CreateOuiFloatableOptions,
|
|
166
|
+
): OuiFloatable {
|
|
167
|
+
const {open, position} = getOptions(options);
|
|
168
|
+
|
|
169
|
+
const floatable = createInstance(
|
|
170
|
+
anchor,
|
|
171
|
+
content,
|
|
172
|
+
{
|
|
173
|
+
position,
|
|
174
|
+
},
|
|
175
|
+
true,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
floatable?.update(position);
|
|
179
|
+
|
|
180
|
+
if (open) {
|
|
181
|
+
floatable?.show();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return floatable;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function createInstance(
|
|
188
|
+
anchor: HTMLElement,
|
|
189
|
+
content: HTMLElement,
|
|
190
|
+
options: OuiFloatableOptions,
|
|
191
|
+
standalone: boolean,
|
|
192
|
+
): OuiFloatable {
|
|
193
|
+
if (!isHTMLOrSVGElement(anchor) || !isHTMLOrSVGElement(content)) {
|
|
194
|
+
throw new TypeError(MESSAGE);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let floatable = instances.get(content);
|
|
198
|
+
|
|
199
|
+
if (floatable == null) {
|
|
200
|
+
floatable = new OuiFloatable(getState(anchor, content, options), standalone);
|
|
201
|
+
|
|
202
|
+
instances.set(content, floatable);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return floatable;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function getOnBeforeToggleListener(element: HTMLElement): RemovableEventListener {
|
|
209
|
+
return on(
|
|
210
|
+
element,
|
|
211
|
+
EVENT_BEFORE,
|
|
212
|
+
event => {
|
|
213
|
+
const {newState, source} = event;
|
|
214
|
+
|
|
215
|
+
if (newState === STATE_OPEN && source?.getAttribute(ARIA_DISABLED) === TRUE) {
|
|
216
|
+
event.preventDefault();
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
passive: false,
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function getOptions(input?: CreateOuiFloatableOptions): Required<CreateOuiFloatableOptions> {
|
|
226
|
+
const object = isPlainObject(input) ? input : {};
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
open: typeof object.open === 'boolean' ? object.open : true,
|
|
230
|
+
position: object.position != null && object.position in AREAS ? object.position : 'below',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function getPosition(
|
|
235
|
+
state: OuiFloatableState,
|
|
236
|
+
override?: OuiFloatablePosition,
|
|
237
|
+
): OuiFloatablePosition {
|
|
238
|
+
if (override != null && override in AREAS) {
|
|
239
|
+
return override;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const {anchor, options} = state;
|
|
243
|
+
|
|
244
|
+
if (options.attribute == null) {
|
|
245
|
+
return options.position;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const attribute = anchor.getAttribute(options.attribute) ?? '';
|
|
249
|
+
|
|
250
|
+
return attribute in AREAS ? (attribute as OuiFloatablePosition) : options.position;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function getState(
|
|
254
|
+
anchor: HTMLElement,
|
|
255
|
+
content: HTMLElement,
|
|
256
|
+
options: OuiFloatableOptions,
|
|
257
|
+
): OuiFloatableState {
|
|
258
|
+
const state: OuiFloatableState = {
|
|
259
|
+
anchor,
|
|
260
|
+
content,
|
|
261
|
+
options,
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
return state;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function removeFloatable(content: HTMLElement): void {
|
|
268
|
+
instances.get(content)?.destroy();
|
|
269
|
+
instances.delete(content);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function setPosition(state: OuiFloatableState, override?: OuiFloatablePosition): void {
|
|
273
|
+
const position = getPosition(state, override);
|
|
274
|
+
|
|
275
|
+
const area = AREAS[position];
|
|
276
|
+
|
|
277
|
+
state.content.style.positionArea = area;
|
|
278
|
+
|
|
279
|
+
state.content.setAttribute(ATTRIBUTE_POSITION, POSITIONS[area]);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// #endregion
|
|
283
|
+
|
|
284
|
+
// #region Variables
|
|
285
|
+
|
|
286
|
+
const AREAS: Record<OuiFloatablePosition, string> = {
|
|
287
|
+
above: 'block-start',
|
|
288
|
+
'above-end': 'start span-start',
|
|
289
|
+
'above-start': 'start span-end',
|
|
290
|
+
below: 'block-end',
|
|
291
|
+
'below-end': 'end span-start',
|
|
292
|
+
'below-start': 'end span-end',
|
|
293
|
+
end: 'center end',
|
|
294
|
+
'end-bottom': 'span-start end',
|
|
295
|
+
'end-top': 'span-end end',
|
|
296
|
+
start: 'center start',
|
|
297
|
+
'start-bottom': 'span-start start',
|
|
298
|
+
'start-top': 'span-end start',
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const ARIA_DISABLED = 'aria-disabled';
|
|
302
|
+
|
|
303
|
+
export const ATTRIBUTE_FLOATABLE = 'oui-floatable';
|
|
304
|
+
|
|
305
|
+
const ATTRIBUTE_POSITION = 'oui-position';
|
|
306
|
+
|
|
307
|
+
const EVENT_BEFORE = 'beforetoggle';
|
|
308
|
+
|
|
309
|
+
const MESSAGE = 'Anchor and content must be an HTMLElement or SVGElement';
|
|
310
|
+
|
|
311
|
+
const POSITIONS = Object.fromEntries(
|
|
312
|
+
Object.entries(AREAS).map(([position, area]) => [area, position]),
|
|
313
|
+
) as Record<string, OuiFloatablePosition>;
|
|
314
|
+
|
|
315
|
+
const STATE_OPEN = 'open';
|
|
316
|
+
|
|
317
|
+
const TRUE = 'true';
|
|
318
|
+
|
|
319
|
+
const instances = new WeakMap<HTMLElement, OuiFloatable>();
|
|
320
|
+
|
|
321
|
+
let index = 0;
|
|
322
|
+
|
|
323
|
+
// #endregion
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {createFloatable, type OuiFloatable, type OuiFloatablePosition} from './embedded';
|