@rvx/ui 0.1.6
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/LICENSE +21 -0
- package/dist/common/events.d.ts +72 -0
- package/dist/common/events.js +58 -0
- package/dist/common/events.js.map +1 -0
- package/dist/common/parsers.d.ts +88 -0
- package/dist/common/parsers.js +62 -0
- package/dist/common/parsers.js.map +1 -0
- package/dist/common/theme-test.d.ts +7 -0
- package/dist/common/theme-test.js +14 -0
- package/dist/common/theme-test.js.map +1 -0
- package/dist/common/theme.d.ts +144 -0
- package/dist/common/theme.js +2 -0
- package/dist/common/theme.js.map +1 -0
- package/dist/common/trim.d.ts +12 -0
- package/dist/common/trim.js +16 -0
- package/dist/common/trim.js.map +1 -0
- package/dist/common/types.d.ts +13 -0
- package/dist/common/types.js +10 -0
- package/dist/common/types.js.map +1 -0
- package/dist/common/writing-mode.d.ts +82 -0
- package/dist/common/writing-mode.js +61 -0
- package/dist/common/writing-mode.js.map +1 -0
- package/dist/components/button.d.ts +42 -0
- package/dist/components/button.js +26 -0
- package/dist/components/button.js.map +1 -0
- package/dist/components/checkbox.d.ts +9 -0
- package/dist/components/checkbox.js +32 -0
- package/dist/components/checkbox.js.map +1 -0
- package/dist/components/collapse-test.d.ts +8 -0
- package/dist/components/collapse-test.js +15 -0
- package/dist/components/collapse-test.js.map +1 -0
- package/dist/components/collapse.d.ts +13 -0
- package/dist/components/collapse.js +44 -0
- package/dist/components/collapse.js.map +1 -0
- package/dist/components/column.d.ts +12 -0
- package/dist/components/column.js +12 -0
- package/dist/components/column.js.map +1 -0
- package/dist/components/control-group.d.ts +7 -0
- package/dist/components/control-group.js +11 -0
- package/dist/components/control-group.js.map +1 -0
- package/dist/components/dialog.d.ts +33 -0
- package/dist/components/dialog.js +67 -0
- package/dist/components/dialog.js.map +1 -0
- package/dist/components/dropdown-input.d.ts +27 -0
- package/dist/components/dropdown-input.js +31 -0
- package/dist/components/dropdown-input.js.map +1 -0
- package/dist/components/dropdown.d.ts +123 -0
- package/dist/components/dropdown.js +176 -0
- package/dist/components/dropdown.js.map +1 -0
- package/dist/components/flex-space.d.ts +4 -0
- package/dist/components/flex-space.js +10 -0
- package/dist/components/flex-space.js.map +1 -0
- package/dist/components/heading.d.ts +9 -0
- package/dist/components/heading.js +14 -0
- package/dist/components/heading.js.map +1 -0
- package/dist/components/label.d.ts +14 -0
- package/dist/components/label.js +15 -0
- package/dist/components/label.js.map +1 -0
- package/dist/components/layer.d.ts +81 -0
- package/dist/components/layer.js +164 -0
- package/dist/components/layer.js.map +1 -0
- package/dist/components/link.d.ts +57 -0
- package/dist/components/link.js +26 -0
- package/dist/components/link.js.map +1 -0
- package/dist/components/page.d.ts +9 -0
- package/dist/components/page.js +17 -0
- package/dist/components/page.js.map +1 -0
- package/dist/components/popout.d.ts +134 -0
- package/dist/components/popout.js +259 -0
- package/dist/components/popout.js.map +1 -0
- package/dist/components/popover.d.ts +139 -0
- package/dist/components/popover.js +101 -0
- package/dist/components/popover.js.map +1 -0
- package/dist/components/radio-buttons.d.ts +17 -0
- package/dist/components/radio-buttons.js +26 -0
- package/dist/components/radio-buttons.js.map +1 -0
- package/dist/components/row.d.ts +10 -0
- package/dist/components/row.js +23 -0
- package/dist/components/row.js.map +1 -0
- package/dist/components/scroll-view.d.ts +6 -0
- package/dist/components/scroll-view.js +72 -0
- package/dist/components/scroll-view.js.map +1 -0
- package/dist/components/text-input.d.ts +53 -0
- package/dist/components/text-input.js +35 -0
- package/dist/components/text-input.js.map +1 -0
- package/dist/components/text.d.ts +7 -0
- package/dist/components/text.js +11 -0
- package/dist/components/text.js.map +1 -0
- package/dist/components/validation.d.ts +109 -0
- package/dist/components/validation.js +151 -0
- package/dist/components/validation.js.map +1 -0
- package/dist/components/value.d.ts +7 -0
- package/dist/components/value.js +11 -0
- package/dist/components/value.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/test.d.ts +2 -0
- package/dist/test.js +3 -0
- package/dist/test.js.map +1 -0
- package/dist/theme.module.css +679 -0
- package/dist/theme.module.css.map +1 -0
- package/package.json +29 -0
- package/src/common/events.tsx +130 -0
- package/src/common/parsers.tsx +167 -0
- package/src/common/theme-test.tsx +20 -0
- package/src/common/theme.tsx +165 -0
- package/src/common/trim.tsx +30 -0
- package/src/common/types.tsx +23 -0
- package/src/common/writing-mode.tsx +150 -0
- package/src/components/button.tsx +94 -0
- package/src/components/checkbox.tsx +64 -0
- package/src/components/collapse-test.tsx +23 -0
- package/src/components/collapse.tsx +75 -0
- package/src/components/column.tsx +28 -0
- package/src/components/control-group.tsx +22 -0
- package/src/components/dialog.tsx +137 -0
- package/src/components/dropdown-input.tsx +82 -0
- package/src/components/dropdown.tsx +352 -0
- package/src/components/flex-space.tsx +15 -0
- package/src/components/heading.tsx +23 -0
- package/src/components/label.tsx +37 -0
- package/src/components/layer.tsx +299 -0
- package/src/components/link.tsx +118 -0
- package/src/components/page.tsx +36 -0
- package/src/components/popout.tsx +461 -0
- package/src/components/popover.tsx +292 -0
- package/src/components/radio-buttons.tsx +81 -0
- package/src/components/row.tsx +37 -0
- package/src/components/scroll-view.tsx +97 -0
- package/src/components/text-input.tsx +117 -0
- package/src/components/text.tsx +22 -0
- package/src/components/validation.tsx +272 -0
- package/src/components/value.tsx +22 -0
- package/src/index.tsx +29 -0
- package/src/test.tsx +2 -0
- package/src/theme/base.scss +69 -0
- package/src/theme/common.scss +51 -0
- package/src/theme/components/button.scss +116 -0
- package/src/theme/components/checkbox.scss +25 -0
- package/src/theme/components/collapse.scss +64 -0
- package/src/theme/components/column.scss +28 -0
- package/src/theme/components/control-group.scss +14 -0
- package/src/theme/components/dialog.scss +44 -0
- package/src/theme/components/dropdown.scss +50 -0
- package/src/theme/components/flex-space.scss +6 -0
- package/src/theme/components/heading.scss +39 -0
- package/src/theme/components/label.scss +24 -0
- package/src/theme/components/link.scss +25 -0
- package/src/theme/components/page.scss +22 -0
- package/src/theme/components/popover.scss +58 -0
- package/src/theme/components/radio-buttons.scss +31 -0
- package/src/theme/components/row.scss +17 -0
- package/src/theme/components/scroll-view.scss +51 -0
- package/src/theme/components/text-input.scss +45 -0
- package/src/theme/components/text.scss +12 -0
- package/src/theme/components/validation.scss +15 -0
- package/src/theme/components/value.scss +4 -0
- package/src/theme/theme.scss +22 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import { captureSelf, Emitter, Event as RvxEvent, Expression, extract, get, getContext, ReadonlyContext, render, runInContext, sig, teardown, TeardownHook, untrack, View, viewNodes } from "rvx";
|
|
2
|
+
|
|
3
|
+
import { PASSIVE_ACTION_EVENT } from "../common/events.js";
|
|
4
|
+
import { axisEquals, Direction, DOWN, flip, getBlockStart, getInlineStart, getSize, getWindowSize, getWindowSpaceAround, INSET, LEFT, RIGHT, ScriptDirection, UP, WritingMode } from "../common/writing-mode.js";
|
|
5
|
+
import { LAYER, Layer } from "./layer.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Defines the direction in which the popout is placed in relation to it's anchor.
|
|
9
|
+
*
|
|
10
|
+
* + `"block"` coresponds to the block flow direction.
|
|
11
|
+
* + `"inline"` coresponds to the inline flow direction.
|
|
12
|
+
* + If `"-start"` or `"-end"` is present, that direction is used unless there isn't enough space for the current content and the opposite side has more available space.
|
|
13
|
+
*/
|
|
14
|
+
export type PopoutPlacement = "block" | "inline" | EffectivePopoutPlacement;
|
|
15
|
+
export type EffectivePopoutPlacement = "block-start" | "block-end" | "inline-start" | "inline-end";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Defines which side of the anchor and content are aligned orthogonally to the placement axis.
|
|
19
|
+
*
|
|
20
|
+
* + `"center"` aligns the popout center to the anchor center.
|
|
21
|
+
* + `"start"` aligns the popout start to the anchor start.
|
|
22
|
+
* + `"end"` aligns the popout end to the anchor end.
|
|
23
|
+
*/
|
|
24
|
+
export type PopoutAlignment = "center" | "start" | "end";
|
|
25
|
+
|
|
26
|
+
export interface PopoutPlacementArgs {
|
|
27
|
+
/**
|
|
28
|
+
* When set during the `onPlacement` event, this defines the gap between the anchor and the content in `px`.
|
|
29
|
+
*
|
|
30
|
+
* @default 0
|
|
31
|
+
*/
|
|
32
|
+
gap: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface PopoutPlacementInfo {
|
|
36
|
+
/**
|
|
37
|
+
* The rect of the anchor at the time of placement.
|
|
38
|
+
*/
|
|
39
|
+
anchorRect: DOMRect;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The content root element.
|
|
43
|
+
*/
|
|
44
|
+
content: HTMLElement;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The effective placement.
|
|
48
|
+
*/
|
|
49
|
+
placement: EffectivePopoutPlacement;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The effective placement direction.
|
|
53
|
+
*/
|
|
54
|
+
placementDir: Direction;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The block start or inline start direction orthogonal to the effective placement axis.
|
|
58
|
+
*/
|
|
59
|
+
alignStart: Direction;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface PopoutContent {
|
|
63
|
+
(props: {
|
|
64
|
+
/**
|
|
65
|
+
* The popout itself.
|
|
66
|
+
*/
|
|
67
|
+
popout: Popout;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Called after the popout content is attached to the document, but before calculating the placement.
|
|
71
|
+
*/
|
|
72
|
+
onPlacement: RvxEvent<[event: PopoutPlacementArgs]>;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Set the size reference element.
|
|
76
|
+
*
|
|
77
|
+
* If never called or if the last target was undefined, the popout root is used as reference.
|
|
78
|
+
*/
|
|
79
|
+
setSizeReference: (target: Element | undefined) => void;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reactively get information on the effective placement.
|
|
83
|
+
*/
|
|
84
|
+
placement: () => PopoutPlacementInfo | undefined;
|
|
85
|
+
}): unknown;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface PopoutOptions {
|
|
89
|
+
/**
|
|
90
|
+
* Defines the direction in which the popout is placed in relation to it's anchor.
|
|
91
|
+
*
|
|
92
|
+
* See {@link PopoutPlacement}
|
|
93
|
+
*/
|
|
94
|
+
placement: Expression<PopoutPlacement>;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Defines which side of the anchor and content are aligned orthogonally to the placement axis.
|
|
98
|
+
*
|
|
99
|
+
* See {@link PopoutAlignment}
|
|
100
|
+
*/
|
|
101
|
+
alignment: Expression<PopoutAlignment>;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The content component to render while the popout is visible.
|
|
105
|
+
*/
|
|
106
|
+
content: PopoutContent;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* An array of event names that cause the popout to hide automatically when dispatched outside of the current layer stack or the latest anchor.
|
|
110
|
+
*
|
|
111
|
+
* @default ["resize", "scroll", "mousedown", "touchstart", "focusin", "rvx-ui:passive-action"]
|
|
112
|
+
*/
|
|
113
|
+
foreignEvents?: string[];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface InstanceArgs {
|
|
117
|
+
anchor: View;
|
|
118
|
+
pointerEvent: Event | undefined;
|
|
119
|
+
contentBlockSize: number;
|
|
120
|
+
contentInlineSize: number;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface Instance {
|
|
124
|
+
dispose: TeardownHook;
|
|
125
|
+
view: View;
|
|
126
|
+
content: HTMLElement;
|
|
127
|
+
sizeReference: Element | undefined;
|
|
128
|
+
observer: ResizeObserver;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Utility to create automatically placed floating content like popovers or dropdowns.
|
|
133
|
+
*/
|
|
134
|
+
export class Popout {
|
|
135
|
+
#context: ReadonlyContext | undefined;
|
|
136
|
+
#placement: Expression<PopoutPlacement>;
|
|
137
|
+
#alignment: Expression<PopoutAlignment>;
|
|
138
|
+
#content: PopoutContent;
|
|
139
|
+
#foreignEvents: string[];
|
|
140
|
+
#instance?: Instance;
|
|
141
|
+
#instanceArgs?: InstanceArgs;
|
|
142
|
+
#visible = sig(false);
|
|
143
|
+
#onPlacement = new Emitter<[PopoutPlacementArgs]>();
|
|
144
|
+
#placementState = sig<PopoutPlacementInfo | undefined>(undefined);
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Create a new popout.
|
|
148
|
+
*
|
|
149
|
+
* The popout hides automatically when the current context is disposed.
|
|
150
|
+
*/
|
|
151
|
+
constructor(options: PopoutOptions) {
|
|
152
|
+
this.#context = getContext();
|
|
153
|
+
this.#placement = options.placement;
|
|
154
|
+
this.#alignment = options.alignment;
|
|
155
|
+
this.#content = options.content;
|
|
156
|
+
this.#foreignEvents = options.foreignEvents ?? ["resize", "scroll", "mousedown", "touchstart", "focusin", PASSIVE_ACTION_EVENT];
|
|
157
|
+
|
|
158
|
+
teardown(() => {
|
|
159
|
+
this.hide();
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Reactively check if the popout is currently visible.
|
|
165
|
+
*/
|
|
166
|
+
get visible(): boolean {
|
|
167
|
+
return this.#visible.value;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Show the popout or recalculate placement if already visible.
|
|
172
|
+
*
|
|
173
|
+
* @param anchor The anchor view to use. This doesn't affect the view in any way.
|
|
174
|
+
* @param pointerEvent An optional event to determine which anchor rect to use if the anchor has multiple client rects like wrapping texts or just multiple distinct root nodes.
|
|
175
|
+
*/
|
|
176
|
+
show(anchor: View, pointerEvent?: Event): void {
|
|
177
|
+
this.#instanceArgs = undefined;
|
|
178
|
+
this.#placementState.value = undefined;
|
|
179
|
+
|
|
180
|
+
// Get an object with the pointer posiiton:
|
|
181
|
+
let pointer: { clientX: number; clientY: number } | undefined;
|
|
182
|
+
if (globalThis.MouseEvent && pointerEvent instanceof MouseEvent) {
|
|
183
|
+
pointer = pointerEvent;
|
|
184
|
+
} else if (globalThis.TouchEvent && pointerEvent instanceof TouchEvent) {
|
|
185
|
+
pointer = pointerEvent.touches.item(0) ?? undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Find the preferred anchor rect & it's writing mode / script direction:
|
|
189
|
+
let anchorRect: DOMRect | undefined;
|
|
190
|
+
let writingMode: WritingMode | undefined;
|
|
191
|
+
let scriptDir: ScriptDirection | undefined;
|
|
192
|
+
if (pointer !== undefined) {
|
|
193
|
+
const { clientX, clientY } = pointer;
|
|
194
|
+
nodes: for (const node of viewNodes(anchor)) {
|
|
195
|
+
if (node instanceof Element) {
|
|
196
|
+
for (const rect of node.getClientRects()) {
|
|
197
|
+
if (clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom) {
|
|
198
|
+
const styles = getComputedStyle(node);
|
|
199
|
+
writingMode ??= styles.writingMode as WritingMode || "horizontal-tb";
|
|
200
|
+
scriptDir ??= styles.direction as ScriptDirection || "ltr";
|
|
201
|
+
anchorRect = rect;
|
|
202
|
+
break nodes;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (anchorRect === undefined) {
|
|
209
|
+
for (const node of viewNodes(anchor)) {
|
|
210
|
+
if (node instanceof Element) {
|
|
211
|
+
anchorRect = node.getBoundingClientRect();
|
|
212
|
+
const styles = getComputedStyle(node);
|
|
213
|
+
writingMode ??= styles.writingMode as WritingMode || "horizontal-tb";
|
|
214
|
+
scriptDir ??= styles.direction as ScriptDirection || "ltr";
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (anchorRect === undefined) {
|
|
219
|
+
throw new Error("anchor must contain at least one element.");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Ensure that there is a popout content instance:
|
|
224
|
+
let instance = this.#instance;
|
|
225
|
+
if (instance === undefined) {
|
|
226
|
+
runInContext(this.#context, () => {
|
|
227
|
+
captureSelf(dispose => {
|
|
228
|
+
instance = this.#instance = {
|
|
229
|
+
dispose,
|
|
230
|
+
content: undefined!,
|
|
231
|
+
view: undefined!,
|
|
232
|
+
observer: undefined!,
|
|
233
|
+
sizeReference: undefined,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
instance.view = render(<Layer>
|
|
237
|
+
{() => {
|
|
238
|
+
const layer = extract(LAYER)!;
|
|
239
|
+
const onForeignEvent = (event: Event): void => {
|
|
240
|
+
if (event.target instanceof Node) {
|
|
241
|
+
if (layer.stackContains(event.target)) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const args = this.#instanceArgs;
|
|
245
|
+
if (args !== undefined) {
|
|
246
|
+
for (const node of viewNodes(args.anchor)) {
|
|
247
|
+
if (node === event.target || node.contains(event.target)) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
this.hide();
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
this.#foreignEvents.forEach(t => window.addEventListener(t, onForeignEvent, { passive: true, capture: true }));
|
|
257
|
+
teardown(() => {
|
|
258
|
+
this.#foreignEvents.forEach(t => window.removeEventListener(t, onForeignEvent, { capture: true }));
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
instance!.content = <div style={{
|
|
262
|
+
"position": "fixed",
|
|
263
|
+
"writing-mode": writingMode,
|
|
264
|
+
}} dir={scriptDir}>
|
|
265
|
+
{this.#content({
|
|
266
|
+
popout: this,
|
|
267
|
+
onPlacement: this.#onPlacement.event,
|
|
268
|
+
placement: () => this.#placementState.value,
|
|
269
|
+
setSizeReference: target => {
|
|
270
|
+
instance!.sizeReference = target;
|
|
271
|
+
},
|
|
272
|
+
})}
|
|
273
|
+
</div> as HTMLElement;
|
|
274
|
+
return instance!.content;
|
|
275
|
+
}}
|
|
276
|
+
</Layer>);
|
|
277
|
+
document.body.appendChild(instance.view.take());
|
|
278
|
+
|
|
279
|
+
instance.observer = new ResizeObserver(entries => {
|
|
280
|
+
const args = this.#instanceArgs;
|
|
281
|
+
const size = entries[entries.length - 1]?.borderBoxSize[0];
|
|
282
|
+
if (args !== undefined && size !== undefined && (size.blockSize !== args.contentBlockSize || size.inlineSize !== args.contentInlineSize)) {
|
|
283
|
+
this.show(args.anchor, args.pointerEvent);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
instance.observer.observe(instance.content, { box: "border-box" });
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const placementArgs: PopoutPlacementArgs = { gap: 0 };
|
|
292
|
+
this.#onPlacement.emit(placementArgs);
|
|
293
|
+
const { gap } = placementArgs;
|
|
294
|
+
|
|
295
|
+
const { content, sizeReference } = instance!;
|
|
296
|
+
const inlineStart = getInlineStart(writingMode!, scriptDir!);
|
|
297
|
+
const blockStart = getBlockStart(writingMode!);
|
|
298
|
+
|
|
299
|
+
// Reset content styles for measuring it's intrinsic size:
|
|
300
|
+
content.style.setProperty("--popout-anchor-inline-size", `${anchorRect.width}px`);
|
|
301
|
+
content.style.left = "0px";
|
|
302
|
+
content.style.top = "0px";
|
|
303
|
+
content.style.right = "";
|
|
304
|
+
content.style.bottom = "";
|
|
305
|
+
const place = untrack(() => get(this.#placement));
|
|
306
|
+
switch (place) {
|
|
307
|
+
case "block":
|
|
308
|
+
case "block-start":
|
|
309
|
+
case "block-end":
|
|
310
|
+
content.style.maxInlineSize = `${getWindowSize(inlineStart)}px`;
|
|
311
|
+
content.style.maxBlockSize = `${Math.max(
|
|
312
|
+
getWindowSpaceAround(anchorRect, blockStart),
|
|
313
|
+
getWindowSpaceAround(anchorRect, flip(blockStart)),
|
|
314
|
+
) - gap}px`;
|
|
315
|
+
break;
|
|
316
|
+
|
|
317
|
+
case "inline":
|
|
318
|
+
case "inline-start":
|
|
319
|
+
case "inline-end":
|
|
320
|
+
content.style.maxBlockSize = `${getWindowSize(blockStart)}px`;
|
|
321
|
+
content.style.maxInlineSize = `${Math.max(
|
|
322
|
+
getWindowSpaceAround(anchorRect, inlineStart),
|
|
323
|
+
getWindowSpaceAround(anchorRect, flip(inlineStart)),
|
|
324
|
+
) - gap}px`;
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
content.style.setProperty("--popout-max-block-size", content.style.maxBlockSize);
|
|
328
|
+
content.style.setProperty("--popout-max-inline-size", content.style.maxInlineSize);
|
|
329
|
+
|
|
330
|
+
// Measure intrinsic content size & compute the final placement direction:
|
|
331
|
+
const contentRect = (sizeReference ?? content).getBoundingClientRect();
|
|
332
|
+
let placement: EffectivePopoutPlacement;
|
|
333
|
+
let placementDir: Direction;
|
|
334
|
+
let alignStart: Direction;
|
|
335
|
+
if (place === "inline" || place === "block") {
|
|
336
|
+
const start = place === "inline" ? inlineStart : blockStart;
|
|
337
|
+
const startSpace = getWindowSpaceAround(anchorRect, start);
|
|
338
|
+
const endSpace = getWindowSpaceAround(anchorRect, flip(start));
|
|
339
|
+
if (startSpace > endSpace) {
|
|
340
|
+
placementDir = start;
|
|
341
|
+
placement = place === "inline" ? "inline-start" : "block-start";
|
|
342
|
+
} else {
|
|
343
|
+
placementDir = flip(start);
|
|
344
|
+
placement = place === "inline" ? "inline-end" : "block-end";
|
|
345
|
+
}
|
|
346
|
+
alignStart = place === "inline" ? blockStart : inlineStart;
|
|
347
|
+
} else {
|
|
348
|
+
placement = place;
|
|
349
|
+
placementDir = place === "block-start" ? blockStart : (
|
|
350
|
+
place === "block-end" ? flip(blockStart) : (
|
|
351
|
+
place === "inline-start" ? inlineStart : flip(inlineStart)
|
|
352
|
+
)
|
|
353
|
+
);
|
|
354
|
+
alignStart = axisEquals(placementDir, blockStart) ? inlineStart : blockStart;
|
|
355
|
+
const space = getWindowSpaceAround(anchorRect, placementDir);
|
|
356
|
+
if (getSize(contentRect, placementDir) > space) {
|
|
357
|
+
if (getWindowSpaceAround(anchorRect, flip(placementDir)) > space) {
|
|
358
|
+
placementDir = flip(placementDir);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Apply inset along the placement direction:
|
|
364
|
+
content.style[INSET[flip(placementDir)]] = `${getAnchorRectInset(anchorRect, placementDir) + gap}px`;
|
|
365
|
+
content.style[INSET[placementDir]] = "";
|
|
366
|
+
|
|
367
|
+
// Compute the raw alignment:
|
|
368
|
+
const align = untrack(() => get(this.#alignment));
|
|
369
|
+
const alignContentSize = getSize(contentRect, alignStart);
|
|
370
|
+
let alignInset: number;
|
|
371
|
+
let alignEnd = false;
|
|
372
|
+
switch (align) {
|
|
373
|
+
case "center":
|
|
374
|
+
alignInset = getWindowSpaceAround(anchorRect, alignStart) + ((getSize(anchorRect, alignStart) - alignContentSize) / 2);
|
|
375
|
+
break;
|
|
376
|
+
|
|
377
|
+
case "start":
|
|
378
|
+
alignInset = getWindowSpaceAround(anchorRect, alignStart);
|
|
379
|
+
break;
|
|
380
|
+
|
|
381
|
+
case "end":
|
|
382
|
+
alignInset = getWindowSpaceAround(anchorRect, flip(alignStart));
|
|
383
|
+
alignEnd = true;
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Adjust alignment to fit the current window size:
|
|
388
|
+
const alignSpace = getWindowSize(alignStart);
|
|
389
|
+
alignInset = Math.max(0, Math.min(alignInset, alignSpace - alignContentSize));
|
|
390
|
+
|
|
391
|
+
// Apply the final alignment:
|
|
392
|
+
content.style[INSET[alignStart]] = alignEnd ? "" : `${alignInset}px`;
|
|
393
|
+
content.style[INSET[flip(alignStart)]] = alignEnd ? `${alignInset}px` : "";
|
|
394
|
+
|
|
395
|
+
this.#instanceArgs = {
|
|
396
|
+
anchor,
|
|
397
|
+
pointerEvent,
|
|
398
|
+
contentBlockSize: getSize(contentRect, blockStart),
|
|
399
|
+
contentInlineSize: getSize(contentRect, inlineStart),
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
this.#placementState.value = {
|
|
403
|
+
anchorRect,
|
|
404
|
+
content,
|
|
405
|
+
placement,
|
|
406
|
+
placementDir,
|
|
407
|
+
alignStart,
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
this.#visible.value = true;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Recalculate placement using the most recent anchor and pointer event if currently visible.
|
|
415
|
+
*
|
|
416
|
+
* Placement is recalculated automatically when the content is resized.
|
|
417
|
+
*
|
|
418
|
+
* Resizing or moving anchors don't trigger recalculation as it is expected, that content behind the popout doesn't change significantly while the popout is open.
|
|
419
|
+
*/
|
|
420
|
+
update(): void {
|
|
421
|
+
const args = this.#instanceArgs;
|
|
422
|
+
if (args !== undefined) {
|
|
423
|
+
this.show(args.anchor, args.pointerEvent);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* The same as {@link show}, but the popout is hidden instead if currently visible.
|
|
429
|
+
*/
|
|
430
|
+
toggle(anchor: View, pointerEvent?: Event): void {
|
|
431
|
+
if (this.visible) {
|
|
432
|
+
this.hide();
|
|
433
|
+
} else {
|
|
434
|
+
this.show(anchor, pointerEvent);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Hide the popout if currently visible.
|
|
440
|
+
*/
|
|
441
|
+
hide(): void {
|
|
442
|
+
const instance = this.#instance;
|
|
443
|
+
if (instance !== undefined) {
|
|
444
|
+
this.#instanceArgs = undefined;
|
|
445
|
+
this.#instance = undefined;
|
|
446
|
+
instance.observer.disconnect();
|
|
447
|
+
instance.dispose();
|
|
448
|
+
instance.view.detach();
|
|
449
|
+
this.#visible.value = false;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function getAnchorRectInset(rect: DOMRect, dir: Direction): number {
|
|
455
|
+
switch (dir) {
|
|
456
|
+
case UP: return window.innerHeight - rect.top;
|
|
457
|
+
case RIGHT: return rect.right;
|
|
458
|
+
case DOWN: return rect.bottom;
|
|
459
|
+
case LEFT: return window.innerWidth - rect.left;
|
|
460
|
+
}
|
|
461
|
+
}
|