@oscarpalmer/oui 0.15.0 → 0.16.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/button.css +0 -3
- package/dist/css/splitter.css +78 -0
- package/dist/js/floatable.cjs +1 -1
- package/dist/js/floatable.js +1 -1
- package/dist/js/popover.cjs +1 -1
- package/dist/js/popover.js +1 -1
- package/dist/js/splitter.cjs +211 -0
- package/dist/js/splitter.js +211 -0
- package/package.json +21 -1
- package/src/css/button.scss +0 -3
- package/src/css/splitter.scss +87 -0
- package/src/js/floatable.ts +1 -1
- package/src/js/popover.ts +1 -1
- package/src/js/splitter.ts +317 -0
- package/types/splitter.d.cts +10 -0
- package/types/splitter.d.ts +11 -0
package/dist/css/button.css
CHANGED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
|
|
2
|
+
:where(oui-splitter) {
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-flow: row nowrap;
|
|
5
|
+
flex: 1;
|
|
6
|
+
}
|
|
7
|
+
:where(oui-splitter > :first-child) {
|
|
8
|
+
flex: var(--oui-splitter-size, 0.5);
|
|
9
|
+
}
|
|
10
|
+
:where(oui-splitter > :last-child) {
|
|
11
|
+
flex: calc(1 - var(--oui-splitter-size, 0.5));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
:where([oui-splitter-separator]) {
|
|
15
|
+
width: 0.25rem;
|
|
16
|
+
background-color: rgba(0, 0, 0, 0.1);
|
|
17
|
+
border-radius: 0.25rem;
|
|
18
|
+
position: relative;
|
|
19
|
+
transition: background 0.125s ease-in-out;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
:where([oui-splitter-separator]:has([oui-splitter-handle]:focus)),
|
|
23
|
+
:where([oui-splitter-separator]:has([oui-splitter-handle]:hover)),
|
|
24
|
+
:where([oui-splitter-separator][oui-splitter-active]) {
|
|
25
|
+
background-color: rgba(0, 0, 255, 0.1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
:where([oui-splitter-handle]) {
|
|
29
|
+
width: 2rem;
|
|
30
|
+
height: 4rem;
|
|
31
|
+
padding: 0.875rem;
|
|
32
|
+
position: absolute;
|
|
33
|
+
inset: 50% auto auto 50%;
|
|
34
|
+
transform: translate3d(-50%, -50%, 0);
|
|
35
|
+
cursor: ew-resize;
|
|
36
|
+
}
|
|
37
|
+
:where([oui-splitter-handle]):focus {
|
|
38
|
+
outline: none;
|
|
39
|
+
}
|
|
40
|
+
:where([oui-splitter-handle])::after {
|
|
41
|
+
content: "";
|
|
42
|
+
width: 100%;
|
|
43
|
+
height: 100%;
|
|
44
|
+
display: block;
|
|
45
|
+
background-color: rgba(0, 0, 0, 0.25);
|
|
46
|
+
outline: 0.25rem solid transparent;
|
|
47
|
+
outline-offset: 0.25rem;
|
|
48
|
+
border-radius: 0.25rem;
|
|
49
|
+
transition: background 0.125s ease-in-out, outline 0.125s ease-in-out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
:where([oui-splitter-handle]:focus)::after,
|
|
53
|
+
:where([oui-splitter-handle]:hover)::after,
|
|
54
|
+
:where([oui-splitter-handle][oui-splitter-active])::after {
|
|
55
|
+
background-color: rgba(0, 0, 255, 0.25);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
:where([oui-splitter-handle]:focus-visible)::after {
|
|
59
|
+
outline-color: rgba(0, 0, 255, 0.25);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
:where(oui-splitter[type=horizontal i],
|
|
63
|
+
oui-splitter[oui-splitter-horizontal]) {
|
|
64
|
+
flex-flow: column nowrap;
|
|
65
|
+
}
|
|
66
|
+
:where(oui-splitter[type=horizontal i] [oui-splitter-separator],
|
|
67
|
+
oui-splitter[oui-splitter-horizontal] [oui-splitter-separator]) {
|
|
68
|
+
width: 100%;
|
|
69
|
+
height: 0.25rem;
|
|
70
|
+
}
|
|
71
|
+
:where(oui-splitter[type=horizontal i] [oui-splitter-handle],
|
|
72
|
+
oui-splitter[oui-splitter-horizontal] [oui-splitter-handle]) {
|
|
73
|
+
width: 4rem;
|
|
74
|
+
height: 2rem;
|
|
75
|
+
cursor: ns-resize;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
package/dist/js/floatable.cjs
CHANGED
|
@@ -128,7 +128,7 @@ function stop(floatable) {
|
|
|
128
128
|
floatable.deactivate = void 0;
|
|
129
129
|
floatable.frame = void 0;
|
|
130
130
|
}
|
|
131
|
-
const margin = Number.parseInt(getComputedStyle(document.documentElement).fontSize, 10) /
|
|
131
|
+
const margin = Number.parseInt(getComputedStyle(document.documentElement).fontSize, 10) / 2;
|
|
132
132
|
exports.Floatable = Floatable;
|
|
133
133
|
exports.activate = activate;
|
|
134
134
|
exports.deactivate = deactivate;
|
package/dist/js/floatable.js
CHANGED
|
@@ -126,7 +126,7 @@ function stop(floatable) {
|
|
|
126
126
|
floatable.deactivate = void 0;
|
|
127
127
|
floatable.frame = void 0;
|
|
128
128
|
}
|
|
129
|
-
const margin = Number.parseInt(getComputedStyle(document.documentElement).fontSize, 10) /
|
|
129
|
+
const margin = Number.parseInt(getComputedStyle(document.documentElement).fontSize, 10) / 2;
|
|
130
130
|
export {
|
|
131
131
|
Floatable,
|
|
132
132
|
activate,
|
package/dist/js/popover.cjs
CHANGED
package/dist/js/popover.js
CHANGED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const number = require("@oscarpalmer/atoms/number");
|
|
4
|
+
const event = require("@oscarpalmer/toretto/event");
|
|
5
|
+
const find = require("@oscarpalmer/toretto/find");
|
|
6
|
+
class OuiSplitterElement extends HTMLElement {
|
|
7
|
+
#splitter;
|
|
8
|
+
constructor() {
|
|
9
|
+
super();
|
|
10
|
+
const panels = [...this.children].filter(
|
|
11
|
+
(child) => child instanceof HTMLElement
|
|
12
|
+
);
|
|
13
|
+
if (panels.length !== 2) {
|
|
14
|
+
throw new Error("oui-splitter must have exactly two panels.");
|
|
15
|
+
}
|
|
16
|
+
this.#splitter = new Splitter(this, panels);
|
|
17
|
+
}
|
|
18
|
+
connectedCallback() {
|
|
19
|
+
if (this.#splitter.types.auto) {
|
|
20
|
+
observer.observe(this);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
disconnectedCallback() {
|
|
24
|
+
if (this.#splitter.types.auto) {
|
|
25
|
+
observer.unobserve(this);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
class Splitter {
|
|
30
|
+
constructor(element, panels) {
|
|
31
|
+
this.element = element;
|
|
32
|
+
this.panels = panels;
|
|
33
|
+
this.handle = createHandle();
|
|
34
|
+
this.rectangle = element.getBoundingClientRect();
|
|
35
|
+
this.separator = createSeparator();
|
|
36
|
+
this.separator.append(this.handle);
|
|
37
|
+
panels[0].insertAdjacentElement("afterend", this.separator);
|
|
38
|
+
mapped.set(element, this);
|
|
39
|
+
mapped.set(this.handle, this);
|
|
40
|
+
setSize(this, this.flex.value);
|
|
41
|
+
this.types = getTypes(this);
|
|
42
|
+
if (this.types.horizontal) {
|
|
43
|
+
this.element.setAttribute("oui-splitter-horizontal", "");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
flex = {
|
|
47
|
+
previous: 0.5,
|
|
48
|
+
value: 0.5
|
|
49
|
+
};
|
|
50
|
+
handle;
|
|
51
|
+
rectangle;
|
|
52
|
+
separator;
|
|
53
|
+
types;
|
|
54
|
+
}
|
|
55
|
+
function createHandle() {
|
|
56
|
+
const handle = document.createElement("span");
|
|
57
|
+
handle.tabIndex = 0;
|
|
58
|
+
handle.setAttribute("aria-label", "Resize panels");
|
|
59
|
+
handle.setAttribute("oui-splitter-handle", "");
|
|
60
|
+
return handle;
|
|
61
|
+
}
|
|
62
|
+
function createSeparator() {
|
|
63
|
+
const separator = document.createElement("div");
|
|
64
|
+
separator.setAttribute("oui-splitter-separator", "");
|
|
65
|
+
return separator;
|
|
66
|
+
}
|
|
67
|
+
function getFlex(splitter2, value) {
|
|
68
|
+
const { height, left, top, width } = splitter2.rectangle;
|
|
69
|
+
const offset = value - (splitter2.types.horizontal ? top : left);
|
|
70
|
+
const fraction = offset / (splitter2.types.horizontal ? height : width);
|
|
71
|
+
return number.clamp(fraction, 0.1, 0.9);
|
|
72
|
+
}
|
|
73
|
+
function getTypes(splitter2, width, height) {
|
|
74
|
+
const type = splitter2.element.getAttribute("type")?.trim() ?? "";
|
|
75
|
+
if (type === "horizontal") {
|
|
76
|
+
return {
|
|
77
|
+
auto: false,
|
|
78
|
+
horizontal: true
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (type === "auto") {
|
|
82
|
+
return {
|
|
83
|
+
auto: true,
|
|
84
|
+
horizontal: (height ?? splitter2.rectangle.height) > (width ?? splitter2.rectangle.width)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
auto: false,
|
|
89
|
+
horizontal: false
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function onEnd(reset) {
|
|
93
|
+
if (splitter != null) {
|
|
94
|
+
splitter.handle.removeAttribute("oui-splitter-active");
|
|
95
|
+
splitter.separator.removeAttribute("oui-splitter-active");
|
|
96
|
+
if (reset) {
|
|
97
|
+
setSize(splitter, splitter.flex.previous);
|
|
98
|
+
} else {
|
|
99
|
+
splitter.flex.previous = splitter.flex.value;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
splitter = void 0;
|
|
103
|
+
}
|
|
104
|
+
function onKeydown(event2) {
|
|
105
|
+
switch (true) {
|
|
106
|
+
case allKeys.has(event2.key):
|
|
107
|
+
onNavigate(event2);
|
|
108
|
+
break;
|
|
109
|
+
case event2.key === "Escape":
|
|
110
|
+
onEnd(true);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function onMousedown(event2) {
|
|
115
|
+
splitter = mapped.get(
|
|
116
|
+
find.findAncestor(
|
|
117
|
+
event2.target,
|
|
118
|
+
"[oui-splitter-handle]"
|
|
119
|
+
)
|
|
120
|
+
);
|
|
121
|
+
if (splitter == null) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
splitter.handle.setAttribute("oui-splitter-active", "");
|
|
125
|
+
splitter.separator.setAttribute("oui-splitter-active", "");
|
|
126
|
+
splitter.rectangle = splitter.element.getBoundingClientRect();
|
|
127
|
+
}
|
|
128
|
+
function onMousemove(event$1) {
|
|
129
|
+
if (splitter == null) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const position = event.getPosition(event$1);
|
|
133
|
+
if (position != null) {
|
|
134
|
+
splitter.flex.value = getFlex(
|
|
135
|
+
splitter,
|
|
136
|
+
splitter.types.horizontal ? position.y : position.x
|
|
137
|
+
);
|
|
138
|
+
setSize(splitter, splitter.flex.value);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function onMouseup() {
|
|
142
|
+
if (splitter != null) {
|
|
143
|
+
onEnd(false);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function onNavigate(event2) {
|
|
147
|
+
const splitter2 = mapped.get(
|
|
148
|
+
find.findAncestor(
|
|
149
|
+
event2.target,
|
|
150
|
+
"[oui-splitter-handle]"
|
|
151
|
+
)
|
|
152
|
+
);
|
|
153
|
+
if (splitter2 == null) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (absoluteKeys.has(event2.key)) {
|
|
157
|
+
setSize(splitter2, event2.key === "End" ? 0.9 : 0.1, true);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (splitter2.types.horizontal && !horizontalKeys.has(event2.key) || !splitter2.types.horizontal && !verticalKeys.has(event2.key)) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const modifier = negativeKeys.has(event2.key) ? -1 : 1;
|
|
164
|
+
setSize(splitter2, splitter2.flex.value + modifier * 0.05, true);
|
|
165
|
+
}
|
|
166
|
+
function onObservation(entries) {
|
|
167
|
+
if (frame != null) {
|
|
168
|
+
cancelAnimationFrame(frame);
|
|
169
|
+
}
|
|
170
|
+
frame = requestAnimationFrame(() => {
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
const splitter2 = mapped.get(entry.target);
|
|
173
|
+
if (splitter2 == null) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
splitter2.types = getTypes(
|
|
177
|
+
splitter2,
|
|
178
|
+
entry.contentRect.width,
|
|
179
|
+
entry.contentRect.height
|
|
180
|
+
);
|
|
181
|
+
if (splitter2.types.horizontal) {
|
|
182
|
+
splitter2.element.setAttribute("oui-splitter-horizontal", "");
|
|
183
|
+
} else {
|
|
184
|
+
splitter2.element.removeAttribute("oui-splitter-horizontal");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
frame = void 0;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
function setSize(splitter2, size, previous) {
|
|
191
|
+
splitter2.flex.value = size;
|
|
192
|
+
if (previous) {
|
|
193
|
+
splitter2.flex.previous = size;
|
|
194
|
+
}
|
|
195
|
+
splitter2.element.style.setProperty("--oui-splitter-size", `${size}`);
|
|
196
|
+
}
|
|
197
|
+
const absoluteKeys = /* @__PURE__ */ new Set(["End", "Home"]);
|
|
198
|
+
const horizontalKeys = /* @__PURE__ */ new Set(["ArrowDown", "ArrowUp"]);
|
|
199
|
+
const mapped = /* @__PURE__ */ new WeakMap();
|
|
200
|
+
const observer = new ResizeObserver(onObservation);
|
|
201
|
+
const negativeKeys = /* @__PURE__ */ new Set(["ArrowLeft", "ArrowUp"]);
|
|
202
|
+
const verticalKeys = /* @__PURE__ */ new Set(["ArrowLeft", "ArrowRight"]);
|
|
203
|
+
const allKeys = /* @__PURE__ */ new Set([...absoluteKeys, ...horizontalKeys, ...verticalKeys]);
|
|
204
|
+
let frame;
|
|
205
|
+
let splitter;
|
|
206
|
+
customElements.define("oui-splitter", OuiSplitterElement);
|
|
207
|
+
event.on(document, "keydown", onKeydown);
|
|
208
|
+
event.on(document, "mousedown", onMousedown);
|
|
209
|
+
event.on(document, "mousemove", onMousemove);
|
|
210
|
+
event.on(document, "mouseup", onMouseup);
|
|
211
|
+
exports.OuiSplitterElement = OuiSplitterElement;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { clamp } from "@oscarpalmer/atoms/number";
|
|
2
|
+
import { on, getPosition } from "@oscarpalmer/toretto/event";
|
|
3
|
+
import { findAncestor } from "@oscarpalmer/toretto/find";
|
|
4
|
+
class OuiSplitterElement extends HTMLElement {
|
|
5
|
+
#splitter;
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
const panels = [...this.children].filter(
|
|
9
|
+
(child) => child instanceof HTMLElement
|
|
10
|
+
);
|
|
11
|
+
if (panels.length !== 2) {
|
|
12
|
+
throw new Error("oui-splitter must have exactly two panels.");
|
|
13
|
+
}
|
|
14
|
+
this.#splitter = new Splitter(this, panels);
|
|
15
|
+
}
|
|
16
|
+
connectedCallback() {
|
|
17
|
+
if (this.#splitter.types.auto) {
|
|
18
|
+
observer.observe(this);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
disconnectedCallback() {
|
|
22
|
+
if (this.#splitter.types.auto) {
|
|
23
|
+
observer.unobserve(this);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
class Splitter {
|
|
28
|
+
constructor(element, panels) {
|
|
29
|
+
this.element = element;
|
|
30
|
+
this.panels = panels;
|
|
31
|
+
this.handle = createHandle();
|
|
32
|
+
this.rectangle = element.getBoundingClientRect();
|
|
33
|
+
this.separator = createSeparator();
|
|
34
|
+
this.separator.append(this.handle);
|
|
35
|
+
panels[0].insertAdjacentElement("afterend", this.separator);
|
|
36
|
+
mapped.set(element, this);
|
|
37
|
+
mapped.set(this.handle, this);
|
|
38
|
+
setSize(this, this.flex.value);
|
|
39
|
+
this.types = getTypes(this);
|
|
40
|
+
if (this.types.horizontal) {
|
|
41
|
+
this.element.setAttribute("oui-splitter-horizontal", "");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
flex = {
|
|
45
|
+
previous: 0.5,
|
|
46
|
+
value: 0.5
|
|
47
|
+
};
|
|
48
|
+
handle;
|
|
49
|
+
rectangle;
|
|
50
|
+
separator;
|
|
51
|
+
types;
|
|
52
|
+
}
|
|
53
|
+
function createHandle() {
|
|
54
|
+
const handle = document.createElement("span");
|
|
55
|
+
handle.tabIndex = 0;
|
|
56
|
+
handle.setAttribute("aria-label", "Resize panels");
|
|
57
|
+
handle.setAttribute("oui-splitter-handle", "");
|
|
58
|
+
return handle;
|
|
59
|
+
}
|
|
60
|
+
function createSeparator() {
|
|
61
|
+
const separator = document.createElement("div");
|
|
62
|
+
separator.setAttribute("oui-splitter-separator", "");
|
|
63
|
+
return separator;
|
|
64
|
+
}
|
|
65
|
+
function getFlex(splitter2, value) {
|
|
66
|
+
const { height, left, top, width } = splitter2.rectangle;
|
|
67
|
+
const offset = value - (splitter2.types.horizontal ? top : left);
|
|
68
|
+
const fraction = offset / (splitter2.types.horizontal ? height : width);
|
|
69
|
+
return clamp(fraction, 0.1, 0.9);
|
|
70
|
+
}
|
|
71
|
+
function getTypes(splitter2, width, height) {
|
|
72
|
+
const type = splitter2.element.getAttribute("type")?.trim() ?? "";
|
|
73
|
+
if (type === "horizontal") {
|
|
74
|
+
return {
|
|
75
|
+
auto: false,
|
|
76
|
+
horizontal: true
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (type === "auto") {
|
|
80
|
+
return {
|
|
81
|
+
auto: true,
|
|
82
|
+
horizontal: (height ?? splitter2.rectangle.height) > (width ?? splitter2.rectangle.width)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
auto: false,
|
|
87
|
+
horizontal: false
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function onEnd(reset) {
|
|
91
|
+
if (splitter != null) {
|
|
92
|
+
splitter.handle.removeAttribute("oui-splitter-active");
|
|
93
|
+
splitter.separator.removeAttribute("oui-splitter-active");
|
|
94
|
+
if (reset) {
|
|
95
|
+
setSize(splitter, splitter.flex.previous);
|
|
96
|
+
} else {
|
|
97
|
+
splitter.flex.previous = splitter.flex.value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
splitter = void 0;
|
|
101
|
+
}
|
|
102
|
+
function onKeydown(event) {
|
|
103
|
+
switch (true) {
|
|
104
|
+
case allKeys.has(event.key):
|
|
105
|
+
onNavigate(event);
|
|
106
|
+
break;
|
|
107
|
+
case event.key === "Escape":
|
|
108
|
+
onEnd(true);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function onMousedown(event) {
|
|
113
|
+
splitter = mapped.get(
|
|
114
|
+
findAncestor(
|
|
115
|
+
event.target,
|
|
116
|
+
"[oui-splitter-handle]"
|
|
117
|
+
)
|
|
118
|
+
);
|
|
119
|
+
if (splitter == null) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
splitter.handle.setAttribute("oui-splitter-active", "");
|
|
123
|
+
splitter.separator.setAttribute("oui-splitter-active", "");
|
|
124
|
+
splitter.rectangle = splitter.element.getBoundingClientRect();
|
|
125
|
+
}
|
|
126
|
+
function onMousemove(event) {
|
|
127
|
+
if (splitter == null) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const position = getPosition(event);
|
|
131
|
+
if (position != null) {
|
|
132
|
+
splitter.flex.value = getFlex(
|
|
133
|
+
splitter,
|
|
134
|
+
splitter.types.horizontal ? position.y : position.x
|
|
135
|
+
);
|
|
136
|
+
setSize(splitter, splitter.flex.value);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function onMouseup() {
|
|
140
|
+
if (splitter != null) {
|
|
141
|
+
onEnd(false);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function onNavigate(event) {
|
|
145
|
+
const splitter2 = mapped.get(
|
|
146
|
+
findAncestor(
|
|
147
|
+
event.target,
|
|
148
|
+
"[oui-splitter-handle]"
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
if (splitter2 == null) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (absoluteKeys.has(event.key)) {
|
|
155
|
+
setSize(splitter2, event.key === "End" ? 0.9 : 0.1, true);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (splitter2.types.horizontal && !horizontalKeys.has(event.key) || !splitter2.types.horizontal && !verticalKeys.has(event.key)) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const modifier = negativeKeys.has(event.key) ? -1 : 1;
|
|
162
|
+
setSize(splitter2, splitter2.flex.value + modifier * 0.05, true);
|
|
163
|
+
}
|
|
164
|
+
function onObservation(entries) {
|
|
165
|
+
if (frame != null) {
|
|
166
|
+
cancelAnimationFrame(frame);
|
|
167
|
+
}
|
|
168
|
+
frame = requestAnimationFrame(() => {
|
|
169
|
+
for (const entry of entries) {
|
|
170
|
+
const splitter2 = mapped.get(entry.target);
|
|
171
|
+
if (splitter2 == null) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
splitter2.types = getTypes(
|
|
175
|
+
splitter2,
|
|
176
|
+
entry.contentRect.width,
|
|
177
|
+
entry.contentRect.height
|
|
178
|
+
);
|
|
179
|
+
if (splitter2.types.horizontal) {
|
|
180
|
+
splitter2.element.setAttribute("oui-splitter-horizontal", "");
|
|
181
|
+
} else {
|
|
182
|
+
splitter2.element.removeAttribute("oui-splitter-horizontal");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
frame = void 0;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function setSize(splitter2, size, previous) {
|
|
189
|
+
splitter2.flex.value = size;
|
|
190
|
+
if (previous) {
|
|
191
|
+
splitter2.flex.previous = size;
|
|
192
|
+
}
|
|
193
|
+
splitter2.element.style.setProperty("--oui-splitter-size", `${size}`);
|
|
194
|
+
}
|
|
195
|
+
const absoluteKeys = /* @__PURE__ */ new Set(["End", "Home"]);
|
|
196
|
+
const horizontalKeys = /* @__PURE__ */ new Set(["ArrowDown", "ArrowUp"]);
|
|
197
|
+
const mapped = /* @__PURE__ */ new WeakMap();
|
|
198
|
+
const observer = new ResizeObserver(onObservation);
|
|
199
|
+
const negativeKeys = /* @__PURE__ */ new Set(["ArrowLeft", "ArrowUp"]);
|
|
200
|
+
const verticalKeys = /* @__PURE__ */ new Set(["ArrowLeft", "ArrowRight"]);
|
|
201
|
+
const allKeys = /* @__PURE__ */ new Set([...absoluteKeys, ...horizontalKeys, ...verticalKeys]);
|
|
202
|
+
let frame;
|
|
203
|
+
let splitter;
|
|
204
|
+
customElements.define("oui-splitter", OuiSplitterElement);
|
|
205
|
+
on(document, "keydown", onKeydown);
|
|
206
|
+
on(document, "mousedown", onMousedown);
|
|
207
|
+
on(document, "mousemove", onMousemove);
|
|
208
|
+
on(document, "mouseup", onMouseup);
|
|
209
|
+
export {
|
|
210
|
+
OuiSplitterElement
|
|
211
|
+
};
|
package/package.json
CHANGED
|
@@ -49,6 +49,26 @@
|
|
|
49
49
|
"default": "./dist/js/focus-trap/standalone.cjs"
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
|
+
"./popover": {
|
|
53
|
+
"import": {
|
|
54
|
+
"types": "./types/popover.d.ts",
|
|
55
|
+
"default": "./dist/js/popover.js"
|
|
56
|
+
},
|
|
57
|
+
"require": {
|
|
58
|
+
"types": "./types/popover.d.cts",
|
|
59
|
+
"default": "./dist/js/popover.cjs"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"./splitter": {
|
|
63
|
+
"import": {
|
|
64
|
+
"types": "./types/splitter.d.ts",
|
|
65
|
+
"default": "./dist/js/splitter.js"
|
|
66
|
+
},
|
|
67
|
+
"require": {
|
|
68
|
+
"types": "./types/splitter.d.cts",
|
|
69
|
+
"default": "./dist/js/splitter.cjs"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
52
72
|
"./tooltip": {
|
|
53
73
|
"import": {
|
|
54
74
|
"types": "./types/tooltip.d.ts",
|
|
@@ -94,5 +114,5 @@
|
|
|
94
114
|
},
|
|
95
115
|
"type": "module",
|
|
96
116
|
"types": "types/index.d.cts",
|
|
97
|
-
"version": "0.
|
|
117
|
+
"version": "0.16.0"
|
|
98
118
|
}
|
package/src/css/button.scss
CHANGED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
@use 'mixins.scss' as mixins;
|
|
2
|
+
|
|
3
|
+
oui-splitter {
|
|
4
|
+
:where(&) {
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-flow: row nowrap;
|
|
7
|
+
flex: 1;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
:where(& > :first-child) {
|
|
11
|
+
flex: var(--oui-splitter-size, .5);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
:where(& > :last-child) {
|
|
15
|
+
flex: calc(1 - var(--oui-splitter-size, .5));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
:where([oui-splitter-separator]) {
|
|
20
|
+
width: .25rem;
|
|
21
|
+
background-color: rgba(0, 0, 0, 0.1);
|
|
22
|
+
border-radius: .25rem;
|
|
23
|
+
position: relative;
|
|
24
|
+
transition: background .125s ease-in-out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
:where([oui-splitter-separator]:has([oui-splitter-handle]:focus)),
|
|
28
|
+
:where([oui-splitter-separator]:has([oui-splitter-handle]:hover)),
|
|
29
|
+
:where([oui-splitter-separator][oui-splitter-active]) {
|
|
30
|
+
background-color: rgba(0, 0, 255, .1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
:where([oui-splitter-handle]) {
|
|
34
|
+
width: 2rem;
|
|
35
|
+
height: 4rem;
|
|
36
|
+
padding: .875rem;
|
|
37
|
+
position: absolute;
|
|
38
|
+
inset: 50% auto auto 50%;
|
|
39
|
+
transform: translate3d(-50%, -50%, 0);
|
|
40
|
+
cursor: ew-resize;
|
|
41
|
+
|
|
42
|
+
&:focus {
|
|
43
|
+
outline: none;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
&::after {
|
|
47
|
+
content: '';
|
|
48
|
+
width: 100%;
|
|
49
|
+
height: 100%;
|
|
50
|
+
display: block;
|
|
51
|
+
background-color: rgba(0, 0, 0, 0.25);
|
|
52
|
+
outline: .25rem solid transparent;
|
|
53
|
+
outline-offset: .25rem;
|
|
54
|
+
border-radius: .25rem;
|
|
55
|
+
transition: background .125s ease-in-out, outline .125s ease-in-out;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
:where([oui-splitter-handle]:focus),
|
|
60
|
+
:where([oui-splitter-handle]:hover),
|
|
61
|
+
:where([oui-splitter-handle][oui-splitter-active]) {
|
|
62
|
+
&::after {
|
|
63
|
+
background-color: rgba(0, 0, 255, .25);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
:where([oui-splitter-handle]:focus-visible)::after {
|
|
68
|
+
outline-color: rgba(0, 0, 255, .25);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
oui-splitter[type='horizontal' i],
|
|
72
|
+
oui-splitter[oui-splitter-horizontal] {
|
|
73
|
+
:where(&) {
|
|
74
|
+
flex-flow: column nowrap;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
:where(& [oui-splitter-separator]) {
|
|
78
|
+
width: 100%;
|
|
79
|
+
height: .25rem;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
:where(& [oui-splitter-handle]) {
|
|
83
|
+
width: 4rem;
|
|
84
|
+
height: 2rem;
|
|
85
|
+
cursor: ns-resize;
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/js/floatable.ts
CHANGED
package/src/js/popover.ts
CHANGED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import {clamp} from '@oscarpalmer/atoms/number';
|
|
2
|
+
import {getPosition, on} from '@oscarpalmer/toretto/event';
|
|
3
|
+
import {findAncestor} from '@oscarpalmer/toretto/find';
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
interface HTMLElementTagNameMap {
|
|
7
|
+
'oui-splitter': OuiSplitterElement;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type Flex = {
|
|
12
|
+
previous: number;
|
|
13
|
+
value: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export class OuiSplitterElement extends HTMLElement {
|
|
17
|
+
#splitter: Splitter;
|
|
18
|
+
|
|
19
|
+
constructor() {
|
|
20
|
+
super();
|
|
21
|
+
|
|
22
|
+
const panels = [...this.children].filter(
|
|
23
|
+
child => child instanceof HTMLElement,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
if (panels.length !== 2) {
|
|
27
|
+
throw new Error('oui-splitter must have exactly two panels.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.#splitter = new Splitter(this, panels);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
connectedCallback(): void {
|
|
34
|
+
if (this.#splitter.types.auto) {
|
|
35
|
+
observer.observe(this);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
disconnectedCallback(): void {
|
|
40
|
+
if (this.#splitter.types.auto) {
|
|
41
|
+
observer.unobserve(this);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class Splitter {
|
|
47
|
+
flex: Flex = {
|
|
48
|
+
previous: 0.5,
|
|
49
|
+
value: 0.5,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
handle: HTMLSpanElement;
|
|
53
|
+
|
|
54
|
+
rectangle: DOMRect;
|
|
55
|
+
|
|
56
|
+
separator: HTMLDivElement;
|
|
57
|
+
|
|
58
|
+
types: Types;
|
|
59
|
+
|
|
60
|
+
constructor(
|
|
61
|
+
public element: OuiSplitterElement,
|
|
62
|
+
public panels: HTMLElement[],
|
|
63
|
+
) {
|
|
64
|
+
this.handle = createHandle();
|
|
65
|
+
this.rectangle = element.getBoundingClientRect();
|
|
66
|
+
this.separator = createSeparator();
|
|
67
|
+
|
|
68
|
+
this.separator.append(this.handle);
|
|
69
|
+
|
|
70
|
+
panels[0].insertAdjacentElement('afterend', this.separator);
|
|
71
|
+
|
|
72
|
+
mapped.set(element, this);
|
|
73
|
+
mapped.set(this.handle, this);
|
|
74
|
+
|
|
75
|
+
setSize(this, this.flex.value);
|
|
76
|
+
|
|
77
|
+
this.types = getTypes(this);
|
|
78
|
+
|
|
79
|
+
if (this.types.horizontal) {
|
|
80
|
+
this.element.setAttribute('oui-splitter-horizontal', '');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
type Types = {
|
|
86
|
+
auto: boolean;
|
|
87
|
+
horizontal: boolean;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
//
|
|
91
|
+
|
|
92
|
+
function createHandle(): HTMLSpanElement {
|
|
93
|
+
const handle = document.createElement('span');
|
|
94
|
+
|
|
95
|
+
handle.tabIndex = 0;
|
|
96
|
+
|
|
97
|
+
handle.setAttribute('aria-label', 'Resize panels');
|
|
98
|
+
handle.setAttribute('oui-splitter-handle', '');
|
|
99
|
+
|
|
100
|
+
return handle;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function createSeparator(): HTMLDivElement {
|
|
104
|
+
const separator = document.createElement('div');
|
|
105
|
+
|
|
106
|
+
separator.setAttribute('oui-splitter-separator', '');
|
|
107
|
+
|
|
108
|
+
return separator;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getFlex(splitter: Splitter, value: number): number {
|
|
112
|
+
const {height, left, top, width} = splitter.rectangle;
|
|
113
|
+
|
|
114
|
+
const offset = value - (splitter.types.horizontal ? top : left);
|
|
115
|
+
|
|
116
|
+
const fraction = offset / (splitter.types.horizontal ? height : width);
|
|
117
|
+
|
|
118
|
+
return clamp(fraction, 0.1, 0.9);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getTypes(splitter: Splitter, width?: number, height?: number): Types {
|
|
122
|
+
const type = splitter.element.getAttribute('type')?.trim() ?? '';
|
|
123
|
+
|
|
124
|
+
if (type === 'horizontal') {
|
|
125
|
+
return {
|
|
126
|
+
auto: false,
|
|
127
|
+
horizontal: true,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (type === 'auto') {
|
|
132
|
+
return {
|
|
133
|
+
auto: true,
|
|
134
|
+
horizontal:
|
|
135
|
+
(height ?? splitter.rectangle.height) >
|
|
136
|
+
(width ?? splitter.rectangle.width),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
auto: false,
|
|
142
|
+
horizontal: false,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function onEnd(reset: boolean): void {
|
|
147
|
+
if (splitter != null) {
|
|
148
|
+
splitter.handle.removeAttribute('oui-splitter-active');
|
|
149
|
+
splitter.separator.removeAttribute('oui-splitter-active');
|
|
150
|
+
|
|
151
|
+
if (reset) {
|
|
152
|
+
setSize(splitter, splitter.flex.previous);
|
|
153
|
+
} else {
|
|
154
|
+
splitter.flex.previous = splitter.flex.value;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
splitter = undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function onKeydown(event: KeyboardEvent): void {
|
|
162
|
+
switch (true) {
|
|
163
|
+
case allKeys.has(event.key):
|
|
164
|
+
onNavigate(event);
|
|
165
|
+
break;
|
|
166
|
+
|
|
167
|
+
case event.key === 'Escape':
|
|
168
|
+
onEnd(true);
|
|
169
|
+
break;
|
|
170
|
+
|
|
171
|
+
default:
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function onMousedown(event: Event): void {
|
|
177
|
+
splitter = mapped.get(
|
|
178
|
+
findAncestor(
|
|
179
|
+
event.target as never,
|
|
180
|
+
'[oui-splitter-handle]',
|
|
181
|
+
) as HTMLSpanElement,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
if (splitter == null) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
splitter.handle.setAttribute('oui-splitter-active', '');
|
|
189
|
+
splitter.separator.setAttribute('oui-splitter-active', '');
|
|
190
|
+
|
|
191
|
+
splitter.rectangle = splitter.element.getBoundingClientRect();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function onMousemove(event: MouseEvent): void {
|
|
195
|
+
if (splitter == null) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const position = getPosition(event);
|
|
200
|
+
|
|
201
|
+
if (position != null) {
|
|
202
|
+
splitter.flex.value = getFlex(
|
|
203
|
+
splitter,
|
|
204
|
+
splitter.types.horizontal ? position.y : position.x,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
setSize(splitter, splitter.flex.value);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function onMouseup(): void {
|
|
212
|
+
if (splitter != null) {
|
|
213
|
+
onEnd(false);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function onNavigate(event: KeyboardEvent): void {
|
|
218
|
+
const splitter = mapped.get(
|
|
219
|
+
findAncestor(
|
|
220
|
+
event.target as never,
|
|
221
|
+
'[oui-splitter-handle]',
|
|
222
|
+
) as HTMLSpanElement,
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
if (splitter == null) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (absoluteKeys.has(event.key)) {
|
|
230
|
+
setSize(splitter, event.key === 'End' ? 0.9 : 0.1, true);
|
|
231
|
+
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (
|
|
236
|
+
(splitter.types.horizontal && !horizontalKeys.has(event.key)) ||
|
|
237
|
+
(!splitter.types.horizontal && !verticalKeys.has(event.key))
|
|
238
|
+
) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const modifier = negativeKeys.has(event.key) ? -1 : 1;
|
|
243
|
+
|
|
244
|
+
setSize(splitter, splitter.flex.value + modifier * 0.05, true);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function onObservation(entries: ResizeObserverEntry[]): void {
|
|
248
|
+
if (frame != null) {
|
|
249
|
+
cancelAnimationFrame(frame);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
frame = requestAnimationFrame(() => {
|
|
253
|
+
for (const entry of entries) {
|
|
254
|
+
const splitter = mapped.get(entry.target as HTMLElement);
|
|
255
|
+
|
|
256
|
+
if (splitter == null) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
splitter.types = getTypes(
|
|
261
|
+
splitter,
|
|
262
|
+
entry.contentRect.width,
|
|
263
|
+
entry.contentRect.height,
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
if (splitter.types.horizontal) {
|
|
267
|
+
splitter.element.setAttribute('oui-splitter-horizontal', '');
|
|
268
|
+
} else {
|
|
269
|
+
splitter.element.removeAttribute('oui-splitter-horizontal');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
frame = undefined;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function setSize(splitter: Splitter, size: number, previous?: boolean): void {
|
|
278
|
+
splitter.flex.value = size;
|
|
279
|
+
|
|
280
|
+
if (previous) {
|
|
281
|
+
splitter.flex.previous = size;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
splitter.element.style.setProperty('--oui-splitter-size', `${size}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
//
|
|
288
|
+
|
|
289
|
+
const absoluteKeys = new Set(['End', 'Home']);
|
|
290
|
+
|
|
291
|
+
const horizontalKeys = new Set(['ArrowDown', 'ArrowUp']);
|
|
292
|
+
|
|
293
|
+
const mapped = new WeakMap<HTMLElement, Splitter>();
|
|
294
|
+
|
|
295
|
+
const observer = new ResizeObserver(onObservation);
|
|
296
|
+
|
|
297
|
+
const negativeKeys = new Set(['ArrowLeft', 'ArrowUp']);
|
|
298
|
+
|
|
299
|
+
const verticalKeys = new Set(['ArrowLeft', 'ArrowRight']);
|
|
300
|
+
|
|
301
|
+
const allKeys = new Set([...absoluteKeys, ...horizontalKeys, ...verticalKeys]);
|
|
302
|
+
|
|
303
|
+
let frame: DOMHighResTimeStamp | undefined;
|
|
304
|
+
|
|
305
|
+
let splitter: Splitter | undefined;
|
|
306
|
+
|
|
307
|
+
//
|
|
308
|
+
|
|
309
|
+
customElements.define('oui-splitter', OuiSplitterElement);
|
|
310
|
+
|
|
311
|
+
on(document, 'keydown', onKeydown);
|
|
312
|
+
|
|
313
|
+
on(document, 'mousedown', onMousedown);
|
|
314
|
+
|
|
315
|
+
on(document, 'mousemove', onMousemove);
|
|
316
|
+
|
|
317
|
+
on(document, 'mouseup', onMouseup);
|