@fuzdev/fuz_ui 0.202.0 → 0.203.1
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/ApiIndex.svelte +6 -3
- package/dist/ApiIndex.svelte.d.ts +1 -1
- package/dist/ApiIndex.svelte.d.ts.map +1 -1
- package/dist/ApiModule.svelte +72 -21
- package/dist/ApiModule.svelte.d.ts +1 -1
- package/dist/ApiModule.svelte.d.ts.map +1 -1
- package/dist/ContextmenuEntry.svelte +15 -16
- package/dist/ContextmenuEntry.svelte.d.ts +3 -2
- package/dist/ContextmenuEntry.svelte.d.ts.map +1 -1
- package/dist/ContextmenuIcon.svelte +30 -0
- package/dist/ContextmenuIcon.svelte.d.ts +9 -0
- package/dist/ContextmenuIcon.svelte.d.ts.map +1 -0
- package/dist/ContextmenuLinkEntry.svelte +23 -12
- package/dist/ContextmenuLinkEntry.svelte.d.ts +2 -1
- package/dist/ContextmenuLinkEntry.svelte.d.ts.map +1 -1
- package/dist/ContextmenuMenu.svelte +207 -0
- package/dist/ContextmenuMenu.svelte.d.ts +41 -0
- package/dist/ContextmenuMenu.svelte.d.ts.map +1 -0
- package/dist/ContextmenuRoot.svelte +58 -260
- package/dist/ContextmenuRoot.svelte.d.ts +2 -60
- package/dist/ContextmenuRoot.svelte.d.ts.map +1 -1
- package/dist/ContextmenuRootForSafariCompatibility.svelte +72 -283
- package/dist/ContextmenuRootForSafariCompatibility.svelte.d.ts +2 -58
- package/dist/ContextmenuRootForSafariCompatibility.svelte.d.ts.map +1 -1
- package/dist/ContextmenuSeparator.svelte +2 -1
- package/dist/ContextmenuSeparator.svelte.d.ts.map +1 -1
- package/dist/ContextmenuSubmenu.svelte +31 -25
- package/dist/ContextmenuSubmenu.svelte.d.ts +3 -2
- package/dist/ContextmenuSubmenu.svelte.d.ts.map +1 -1
- package/dist/ContextmenuTextEntry.svelte +6 -4
- package/dist/ContextmenuTextEntry.svelte.d.ts +3 -1
- package/dist/ContextmenuTextEntry.svelte.d.ts.map +1 -1
- package/dist/DeclarationLink.svelte +15 -2
- package/dist/DeclarationLink.svelte.d.ts +7 -0
- package/dist/DeclarationLink.svelte.d.ts.map +1 -1
- package/dist/DocsLink.svelte +3 -1
- package/dist/DocsLink.svelte.d.ts.map +1 -1
- package/dist/DocsTertiaryNav.svelte +2 -1
- package/dist/DocsTertiaryNav.svelte.d.ts.map +1 -1
- package/dist/LibraryDetail.svelte +9 -3
- package/dist/LibraryDetail.svelte.d.ts +1 -1
- package/dist/LibraryDetail.svelte.d.ts.map +1 -1
- package/dist/ModuleLink.svelte +2 -1
- package/dist/ModuleLink.svelte.d.ts.map +1 -1
- package/dist/TypeLink.svelte +2 -1
- package/dist/TypeLink.svelte.d.ts.map +1 -1
- package/dist/contextmenu_helpers.d.ts +324 -1
- package/dist/contextmenu_helpers.d.ts.map +1 -1
- package/dist/contextmenu_helpers.js +403 -2
- package/dist/contextmenu_state.svelte.d.ts +61 -15
- package/dist/contextmenu_state.svelte.d.ts.map +1 -1
- package/dist/contextmenu_state.svelte.js +206 -119
- package/dist/declaration.svelte.d.ts +1 -0
- package/dist/declaration.svelte.d.ts.map +1 -1
- package/dist/declaration.svelte.js +2 -1
- package/dist/icons.d.ts +15 -3
- package/dist/icons.d.ts.map +1 -1
- package/dist/icons.js +17 -3
- package/dist/library.svelte.d.ts +30 -3
- package/dist/library.svelte.d.ts.map +1 -1
- package/dist/library.svelte.js +38 -0
- package/dist/module.svelte.d.ts +62 -1
- package/dist/module.svelte.d.ts.map +1 -1
- package/dist/module.svelte.js +71 -1
- package/package.json +2 -2
- package/src/lib/contextmenu_helpers.ts +546 -3
- package/src/lib/contextmenu_state.svelte.ts +219 -119
- package/src/lib/declaration.svelte.ts +2 -1
- package/src/lib/icons.ts +19 -3
- package/src/lib/library.svelte.ts +43 -1
- package/src/lib/module.svelte.ts +97 -2
|
@@ -18,20 +18,19 @@
|
|
|
18
18
|
* and relies on the standard `contextmenu` event.
|
|
19
19
|
*/
|
|
20
20
|
import {swallow} from '@fuzdev/fuz_util/dom.js';
|
|
21
|
-
import {DEV} from 'esm-env';
|
|
22
21
|
import {on} from 'svelte/events';
|
|
23
|
-
import type {ComponentProps, Snippet} from 'svelte';
|
|
24
22
|
|
|
25
23
|
import {
|
|
26
24
|
contextmenu_context,
|
|
27
|
-
contextmenu_dimensions_context,
|
|
28
25
|
ContextmenuState,
|
|
29
26
|
contextmenu_open,
|
|
30
27
|
contextmenu_check_global_root,
|
|
31
28
|
} from './contextmenu_state.svelte.js';
|
|
32
|
-
import
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
import ContextmenuMenu, {
|
|
30
|
+
link_entry_default,
|
|
31
|
+
text_entry_default,
|
|
32
|
+
separator_entry_default,
|
|
33
|
+
} from './ContextmenuMenu.svelte';
|
|
35
34
|
import {
|
|
36
35
|
CONTEXTMENU_DEFAULT_OPEN_OFFSET_X,
|
|
37
36
|
CONTEXTMENU_DEFAULT_OPEN_OFFSET_Y,
|
|
@@ -40,10 +39,10 @@
|
|
|
40
39
|
CONTEXTMENU_DEFAULT_LONGPRESS_DURATION,
|
|
41
40
|
CONTEXTMENU_DEFAULT_LONGPRESS_MOVE_TOLERANCE,
|
|
42
41
|
contextmenu_is_valid_target,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
contextmenu_resolve_contextmenu_event,
|
|
43
|
+
ContextmenuBypassTracker,
|
|
44
|
+
ContextmenuOpenGuard,
|
|
45
|
+
type ContextmenuRootBaseProps,
|
|
47
46
|
} from './contextmenu_helpers.js';
|
|
48
47
|
|
|
49
48
|
const {
|
|
@@ -60,8 +59,7 @@
|
|
|
60
59
|
text_entry = text_entry_default,
|
|
61
60
|
separator_entry = separator_entry_default,
|
|
62
61
|
children,
|
|
63
|
-
}: {
|
|
64
|
-
contextmenu?: ContextmenuState;
|
|
62
|
+
}: ContextmenuRootBaseProps & {
|
|
65
63
|
/**
|
|
66
64
|
* The number of pixels the pointer can be moved without canceling `longpress`.
|
|
67
65
|
*/
|
|
@@ -70,93 +68,25 @@
|
|
|
70
68
|
* The number of milliseconds after a touch starts before opening the Fuz contextmenu.
|
|
71
69
|
*/
|
|
72
70
|
longpress_duration?: number;
|
|
73
|
-
/**
|
|
74
|
-
* Whether to detect tap-then-longpress to bypass the Fuz contextmenu.
|
|
75
|
-
* This allows access to the system contextmenu by tapping once then long-pressing.
|
|
76
|
-
* Setting to `false` disables the gesture.
|
|
77
|
-
*/
|
|
78
|
-
bypass_with_tap_then_longpress?: boolean;
|
|
79
|
-
/**
|
|
80
|
-
* The number of milliseconds between taps to detect a gesture that bypasses the Fuz contextmenu.
|
|
81
|
-
* Used only when `bypass_with_tap_then_longpress` is true.
|
|
82
|
-
* If the duration is too long, it'll detect more false positives and interrupt normal usage,
|
|
83
|
-
* but too short and some people will have difficulty performing the gesture.
|
|
84
|
-
*/
|
|
85
|
-
bypass_window?: number;
|
|
86
|
-
/**
|
|
87
|
-
* The number of pixels the pointer can be moved between taps to detect a tap-then-longpress.
|
|
88
|
-
* Used only when `bypass_with_tap_then_longpress` is true.
|
|
89
|
-
*/
|
|
90
|
-
bypass_move_tolerance?: number;
|
|
91
|
-
/**
|
|
92
|
-
* The number of pixels to offset from the pointer X position when opened.
|
|
93
|
-
* Useful to ensure the first menu item is immediately under the pointer.
|
|
94
|
-
*/
|
|
95
|
-
open_offset_x?: number;
|
|
96
|
-
/**
|
|
97
|
-
* The number of pixels to offset from the pointer Y position when opened.
|
|
98
|
-
* Useful to ensure the first menu item is immediately under the pointer.
|
|
99
|
-
*/
|
|
100
|
-
open_offset_y?: number;
|
|
101
|
-
/**
|
|
102
|
-
* If `true`, wraps `children` with a div and listens to events on it instead of the window.
|
|
103
|
-
*/
|
|
104
|
-
scoped?: boolean;
|
|
105
|
-
/**
|
|
106
|
-
* Snippet for rendering link entries.
|
|
107
|
-
* Set to `null` to disable automatic link detection.
|
|
108
|
-
* Defaults to `link_entry_default` which renders `ContextmenuLinkEntry`.
|
|
109
|
-
*/
|
|
110
|
-
link_entry?: Snippet<[ComponentProps<typeof ContextmenuLinkEntry>]> | null;
|
|
111
|
-
/**
|
|
112
|
-
* Snippet for rendering copy text entries.
|
|
113
|
-
* Set to `null` to disable automatic copy text detection.
|
|
114
|
-
* Defaults to `text_entry_default` which renders `ContextmenuTextEntry`.
|
|
115
|
-
*/
|
|
116
|
-
text_entry?: Snippet<[ComponentProps<typeof ContextmenuTextEntry>]> | null;
|
|
117
|
-
/**
|
|
118
|
-
* Snippet for rendering separator entries.
|
|
119
|
-
* Set to `null` to disable automatic separator rendering.
|
|
120
|
-
* Defaults to `separator_entry_default` which renders `ContextmenuSeparator`.
|
|
121
|
-
*/
|
|
122
|
-
separator_entry?: Snippet<[ComponentProps<typeof ContextmenuSeparator>]> | null;
|
|
123
|
-
children: Snippet;
|
|
124
71
|
} = $props();
|
|
125
72
|
|
|
126
73
|
contextmenu_context.set(() => contextmenu);
|
|
127
74
|
|
|
128
|
-
|
|
75
|
+
contextmenu_check_global_root(() => scoped); // DEV-only internally, eliminated from production bundles
|
|
129
76
|
|
|
77
|
+
// The menu element while opened, bound from `ContextmenuMenu.svelte`.
|
|
130
78
|
let el: HTMLElement | undefined = $state.raw();
|
|
131
79
|
|
|
132
|
-
const {layout} = $derived(contextmenu);
|
|
133
|
-
|
|
134
|
-
const dimensions = contextmenu_dimensions_context.set();
|
|
135
|
-
|
|
136
|
-
const x = $derived(
|
|
137
|
-
contextmenu_calculate_constrained_x(contextmenu.x, dimensions.width, layout.width),
|
|
138
|
-
);
|
|
139
|
-
const y = $derived(
|
|
140
|
-
contextmenu_calculate_constrained_y(contextmenu.y, dimensions.height, layout.height),
|
|
141
|
-
);
|
|
142
|
-
|
|
143
80
|
// TODO maybe show an indicator fade in at these coordinates
|
|
144
81
|
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
let
|
|
148
|
-
let
|
|
149
|
-
let
|
|
150
|
-
let longpress_timeout: NodeJS.Timeout | undefined | null = $state.raw();
|
|
151
|
-
let longpress_opened: boolean | undefined = $state.raw();
|
|
152
|
-
let longpress_bypass: boolean | undefined = $state.raw();
|
|
153
|
-
let tap_tracking_timeout: NodeJS.Timeout | undefined | null = $state.raw();
|
|
82
|
+
// Longpress tracking state - plain non-reactive variables, only used inside event handlers.
|
|
83
|
+
let touch_x = 0;
|
|
84
|
+
let touch_y = 0;
|
|
85
|
+
let longpress_timeout: NodeJS.Timeout | null = null;
|
|
86
|
+
let longpress_opened = false;
|
|
154
87
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
* iOS's synthesized click from activating the first menu item.
|
|
158
|
-
*/
|
|
159
|
-
let block_next_click = $state.raw(false);
|
|
88
|
+
const bypass_tracker = new ContextmenuBypassTracker();
|
|
89
|
+
const open_guard = new ContextmenuOpenGuard();
|
|
160
90
|
|
|
161
91
|
/**
|
|
162
92
|
* Adds contextmenu-pending class to body during longpress tracking.
|
|
@@ -180,7 +110,7 @@
|
|
|
180
110
|
*/
|
|
181
111
|
const reset_longpress_timeout = (): void => {
|
|
182
112
|
longpress_opened = false;
|
|
183
|
-
if (longpress_timeout
|
|
113
|
+
if (longpress_timeout !== null) {
|
|
184
114
|
clearTimeout(longpress_timeout);
|
|
185
115
|
longpress_timeout = null;
|
|
186
116
|
}
|
|
@@ -188,69 +118,45 @@
|
|
|
188
118
|
// Don't clear tap tracking state here - we need it for tap-then-longpress detection
|
|
189
119
|
};
|
|
190
120
|
|
|
191
|
-
/**
|
|
192
|
-
* Clears tap tracking state and bypass flag used for bypass detection.
|
|
193
|
-
*/
|
|
194
|
-
const reset_tap_tracking = (): void => {
|
|
195
|
-
first_tap_time = null;
|
|
196
|
-
touch_x = null;
|
|
197
|
-
touch_y = null;
|
|
198
|
-
longpress_bypass = false;
|
|
199
|
-
if (tap_tracking_timeout != null) {
|
|
200
|
-
clearTimeout(tap_tracking_timeout);
|
|
201
|
-
tap_tracking_timeout = null;
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
|
|
205
121
|
/**
|
|
206
122
|
* Resets all state - both longpress and tap tracking.
|
|
207
123
|
*/
|
|
208
124
|
const reset_all = (): void => {
|
|
209
125
|
reset_longpress_timeout(); // Also removes contextmenu-pending class
|
|
210
|
-
|
|
126
|
+
bypass_tracker.reset();
|
|
211
127
|
};
|
|
212
128
|
|
|
129
|
+
const open_options = $derived({
|
|
130
|
+
open_offset_x,
|
|
131
|
+
open_offset_y,
|
|
132
|
+
link_enabled: link_entry !== null,
|
|
133
|
+
text_enabled: text_entry !== null,
|
|
134
|
+
separator_enabled: separator_entry !== null,
|
|
135
|
+
});
|
|
136
|
+
|
|
213
137
|
const on_window_contextmenu = (e: MouseEvent) => {
|
|
214
|
-
//
|
|
215
|
-
if (
|
|
216
|
-
reset_tap_tracking(); // Clear bypass state after using it
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
const {target} = e;
|
|
138
|
+
// let the system contextmenu through after a tap-then-longpress bypass gesture
|
|
139
|
+
if (bypass_tracker.consume()) return;
|
|
220
140
|
// handle touch devices that trigger `'contextmenu'` slower than our longpress
|
|
221
141
|
if (longpress_opened) {
|
|
222
142
|
// Don't prevent contextmenu events on elements inside our own contextmenu
|
|
223
143
|
// This allows the browser's native contextmenu (useful for dev tools, inspecting elements, etc.)
|
|
224
|
-
if (el?.contains(target as Node)) {
|
|
144
|
+
if (el?.contains(e.target as Node)) {
|
|
225
145
|
return; // Let the event pass through
|
|
226
146
|
}
|
|
227
147
|
reset_all();
|
|
228
148
|
swallow(e);
|
|
229
149
|
return;
|
|
230
150
|
}
|
|
231
|
-
if (
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
// Don't open contextmenu when clicking on the menu itself
|
|
235
|
-
if (el?.contains(target)) {
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
if (
|
|
239
|
-
contextmenu_open(target, e.clientX + open_offset_x, e.clientY + open_offset_y, contextmenu, {
|
|
240
|
-
link_enabled: link_entry !== null,
|
|
241
|
-
text_enabled: text_entry !== null,
|
|
242
|
-
separator_enabled: separator_entry !== null,
|
|
243
|
-
})
|
|
244
|
-
) {
|
|
245
|
-
swallow(e);
|
|
151
|
+
if (contextmenu_resolve_contextmenu_event(e, contextmenu, el, open_guard, open_options)) {
|
|
246
152
|
reset_all(); // handle touch devices that trigger `'contextmenu'` faster than our longpress
|
|
247
153
|
}
|
|
248
154
|
};
|
|
249
155
|
|
|
250
|
-
// Needed for the iOS workaround. Registered
|
|
156
|
+
// Needed for the iOS workaround. Registered non-passively via `touch_event_attachment` (window or scoped element).
|
|
251
157
|
const touchstart = (e: TouchEvent): void => {
|
|
252
158
|
longpress_opened = false;
|
|
253
|
-
|
|
159
|
+
open_guard.touchstart(); // begins a gesture, clearing stale flags
|
|
254
160
|
const {touches, target} = e;
|
|
255
161
|
if (
|
|
256
162
|
contextmenu.opened ||
|
|
@@ -264,39 +170,13 @@
|
|
|
264
170
|
|
|
265
171
|
const {clientX, clientY} = touches[0]!;
|
|
266
172
|
|
|
267
|
-
// Bypass the contextmenu behavior in certain conditions including a tap-and-longpress gesture.
|
|
268
|
-
// To handle tap-then-longpress we need to see if `first_tap_time`
|
|
269
|
-
// is less than `bypass_window`, and also allow a small amount
|
|
270
|
-
// of pointer movement, `bypass_move_tolerance`.
|
|
271
173
|
// The builtin `'contextmenu'` event will still fire for non-iOS browsers,
|
|
272
|
-
// so `
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
) {
|
|
279
|
-
// Tap-then-longpress detected! Set bypass and clear tap tracking state.
|
|
280
|
-
// Must manually clear state (not call reset_tap_tracking) to preserve bypass flag.
|
|
281
|
-
longpress_bypass = true;
|
|
282
|
-
first_tap_time = null;
|
|
283
|
-
touch_x = null;
|
|
284
|
-
touch_y = null;
|
|
285
|
-
if (tap_tracking_timeout != null) {
|
|
286
|
-
clearTimeout(tap_tracking_timeout); // Prevent timeout from clearing bypass
|
|
287
|
-
tap_tracking_timeout = null;
|
|
288
|
-
}
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
// Record this tap for potential future bypass detection
|
|
292
|
-
first_tap_time = performance.now();
|
|
293
|
-
// Set timeout to clear stale tap tracking after the detection window expires
|
|
294
|
-
if (tap_tracking_timeout != null) {
|
|
295
|
-
clearTimeout(tap_tracking_timeout);
|
|
296
|
-
}
|
|
297
|
-
tap_tracking_timeout = setTimeout(() => {
|
|
298
|
-
reset_tap_tracking();
|
|
299
|
-
}, bypass_window);
|
|
174
|
+
// so the tracker's `bypassed` flag tells `on_window_contextmenu` to exit early.
|
|
175
|
+
if (
|
|
176
|
+
bypass_with_tap_then_longpress &&
|
|
177
|
+
bypass_tracker.track(clientX, clientY, bypass_window, bypass_move_tolerance)
|
|
178
|
+
) {
|
|
179
|
+
return;
|
|
300
180
|
}
|
|
301
181
|
|
|
302
182
|
touch_x = clientX;
|
|
@@ -305,26 +185,33 @@
|
|
|
305
185
|
// Add pending class to enable aggressive iOS blocking via CSS during tracking
|
|
306
186
|
add_contextmenu_pending_class();
|
|
307
187
|
|
|
308
|
-
if (longpress_timeout
|
|
188
|
+
if (longpress_timeout !== null) reset_longpress_timeout();
|
|
309
189
|
longpress_timeout = setTimeout(() => {
|
|
310
190
|
longpress_opened = true;
|
|
311
191
|
remove_contextmenu_pending_class(); // Tracking complete, menu opening
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
192
|
+
if (
|
|
193
|
+
contextmenu_open(
|
|
194
|
+
target,
|
|
195
|
+
touch_x + open_offset_x,
|
|
196
|
+
touch_y + open_offset_y,
|
|
197
|
+
contextmenu,
|
|
198
|
+
open_options,
|
|
199
|
+
)
|
|
200
|
+
) {
|
|
201
|
+
// guard the release of this gesture from interacting with the menu
|
|
202
|
+
open_guard.opened();
|
|
203
|
+
}
|
|
317
204
|
}, longpress_duration);
|
|
318
205
|
};
|
|
319
206
|
|
|
320
|
-
// Needed for the iOS workaround. Registered
|
|
207
|
+
// Needed for the iOS workaround. Registered non-passively via `touch_event_attachment` (window or scoped element).
|
|
321
208
|
const touchmove = (e: TouchEvent): void => {
|
|
322
209
|
// Exit early if no pending longpress or menu is already open
|
|
323
|
-
if (longpress_timeout
|
|
210
|
+
if (longpress_timeout === null || contextmenu.opened) return;
|
|
324
211
|
const {touches} = e;
|
|
325
212
|
if (touches.length !== 1) return;
|
|
326
213
|
const {clientX, clientY} = touches[0]!;
|
|
327
|
-
const distance = Math.hypot(clientX - touch_x
|
|
214
|
+
const distance = Math.hypot(clientX - touch_x, clientY - touch_y);
|
|
328
215
|
if (distance > longpress_move_tolerance) {
|
|
329
216
|
// User is scrolling - cancel longpress but DON'T preventDefault
|
|
330
217
|
reset_longpress_timeout();
|
|
@@ -334,43 +221,30 @@
|
|
|
334
221
|
// CRITICAL: Prevent iOS from showing magnifier, text selection, and link callouts
|
|
335
222
|
e.preventDefault();
|
|
336
223
|
};
|
|
337
|
-
// Needed for the iOS workaround. Registered
|
|
224
|
+
// Needed for the iOS workaround. Registered non-passively via `touch_event_attachment` (window or scoped element).
|
|
338
225
|
const touchend = (e: TouchEvent): void => {
|
|
226
|
+
// Swallow the release of a gesture that opened the menu (custom longpress or
|
|
227
|
+
// native `contextmenu`), stopping the browser from synthesizing mouse events at
|
|
228
|
+
// the touch point that would activate the first item, and arming the click
|
|
229
|
+
// blocker for iOS, which can synthesize the click regardless.
|
|
230
|
+
open_guard.touchend(e);
|
|
339
231
|
// Clear longpress timeout if it exists
|
|
340
|
-
if (longpress_timeout
|
|
341
|
-
if (longpress_opened) {
|
|
342
|
-
swallow(e);
|
|
343
|
-
// Block the next click to prevent iOS's synthesized click from activating the first menu item
|
|
344
|
-
block_next_click = true;
|
|
345
|
-
}
|
|
232
|
+
if (longpress_timeout !== null) {
|
|
346
233
|
reset_longpress_timeout();
|
|
347
234
|
}
|
|
348
|
-
// Clear bypass
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
reset_tap_tracking();
|
|
352
|
-
}
|
|
353
|
-
// Note: We don't clear tap tracking state here - we preserve it for tap-then-longpress detection
|
|
235
|
+
// Clear a pending bypass when the user lifts before the native contextmenu opens.
|
|
236
|
+
// Tap tracking is otherwise preserved for tap-then-longpress detection.
|
|
237
|
+
bypass_tracker.consume();
|
|
354
238
|
};
|
|
355
239
|
|
|
356
240
|
/**
|
|
357
241
|
* Handle touchcancel - this should reset all state since the gesture was interrupted.
|
|
358
242
|
*/
|
|
359
243
|
const touchcancel = (): void => {
|
|
244
|
+
open_guard.reset();
|
|
360
245
|
reset_all();
|
|
361
246
|
};
|
|
362
247
|
|
|
363
|
-
// Passive listener that runs during the event's `capture` phase
|
|
364
|
-
// so that things like the Dialog don't eat the events and prevent the contextmenu from closing.
|
|
365
|
-
const mousedown = (e: MouseEvent) => {
|
|
366
|
-
if (el && !el.contains(e.target as any)) {
|
|
367
|
-
contextmenu.close();
|
|
368
|
-
}
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
const keyboard_handlers = $derived(contextmenu_create_keyboard_handlers(contextmenu));
|
|
372
|
-
const keydown = $derived(contextmenu_create_keydown_handler(keyboard_handlers));
|
|
373
|
-
|
|
374
248
|
/**
|
|
375
249
|
* Creates an attachment that registers touch event listeners with { passive: false }
|
|
376
250
|
* to enable preventDefault() on iOS Safari. Works for both window (non-scoped mode) and
|
|
@@ -412,15 +286,15 @@
|
|
|
412
286
|
|
|
413
287
|
<svelte:window
|
|
414
288
|
oncontextmenu={scoped ? undefined : on_window_contextmenu}
|
|
415
|
-
onmousedown={!contextmenu.opened ? undefined : mousedown}
|
|
416
|
-
onkeydown={!contextmenu.opened ? undefined : keydown}
|
|
417
289
|
{@attach scoped ? undefined : touch_event_attachment}
|
|
418
290
|
/>
|
|
419
291
|
|
|
420
292
|
{#if scoped}
|
|
293
|
+
<!-- A transparent (`display: contents`) event-delegation wrapper. `role="group"` rather
|
|
294
|
+
than a landmark role - it satisfies the a11y lint without requiring an accessible name. -->
|
|
421
295
|
<div
|
|
422
296
|
class="contextmenu-root"
|
|
423
|
-
role="
|
|
297
|
+
role="group"
|
|
424
298
|
oncontextmenu={on_window_contextmenu}
|
|
425
299
|
{@attach touch_event_attachment}
|
|
426
300
|
>
|
|
@@ -430,61 +304,7 @@
|
|
|
430
304
|
{@render children()}
|
|
431
305
|
{/if}
|
|
432
306
|
|
|
433
|
-
{
|
|
434
|
-
<div
|
|
435
|
-
class="contextmenu-layout"
|
|
436
|
-
bind:clientWidth={layout.width}
|
|
437
|
-
bind:clientHeight={layout.height}
|
|
438
|
-
aria-hidden="true"
|
|
439
|
-
></div>
|
|
440
|
-
{/if}
|
|
441
|
-
|
|
442
|
-
<!-- TODO Maybe animate a subtle highlight around the contextmenu as it appears? -->
|
|
443
|
-
{#if contextmenu.opened}
|
|
444
|
-
<ul
|
|
445
|
-
class="contextmenu unstyled pane"
|
|
446
|
-
role="menu"
|
|
447
|
-
aria-label="context menu"
|
|
448
|
-
tabindex="-1"
|
|
449
|
-
bind:this={el}
|
|
450
|
-
bind:offsetWidth={dimensions.width}
|
|
451
|
-
bind:offsetHeight={dimensions.height}
|
|
452
|
-
style:transform="translate3d({x}px, {y}px, 0)"
|
|
453
|
-
onclickcapture={block_next_click
|
|
454
|
-
? (e) => {
|
|
455
|
-
// iOS synthesizes a click after touchend which
|
|
456
|
-
// can unintentionally activate the first menu item. This blocks it.
|
|
457
|
-
block_next_click = false;
|
|
458
|
-
swallow(e);
|
|
459
|
-
}
|
|
460
|
-
: undefined}
|
|
461
|
-
>
|
|
462
|
-
<!-- TODO maybe this should be generic? -->
|
|
463
|
-
{#each contextmenu.params as p (p)}
|
|
464
|
-
{#if typeof p === 'function'}
|
|
465
|
-
{@render p()}
|
|
466
|
-
{:else if p.snippet === 'link'}
|
|
467
|
-
{@render link_entry?.(p.props)}
|
|
468
|
-
{:else if p.snippet === 'text'}
|
|
469
|
-
{@render text_entry?.(p.props)}
|
|
470
|
-
{:else if p.snippet === 'separator'}
|
|
471
|
-
{@render separator_entry?.(p.props)}
|
|
472
|
-
{/if}
|
|
473
|
-
{/each}
|
|
474
|
-
</ul>
|
|
475
|
-
{/if}
|
|
476
|
-
|
|
477
|
-
{#snippet link_entry_default(props: ComponentProps<typeof ContextmenuLinkEntry>)}
|
|
478
|
-
<ContextmenuLinkEntry {...props} />
|
|
479
|
-
{/snippet}
|
|
480
|
-
|
|
481
|
-
{#snippet text_entry_default(props: ComponentProps<typeof ContextmenuTextEntry>)}
|
|
482
|
-
<ContextmenuTextEntry {...props} />
|
|
483
|
-
{/snippet}
|
|
484
|
-
|
|
485
|
-
{#snippet separator_entry_default(props: ComponentProps<typeof ContextmenuSeparator>)}
|
|
486
|
-
<ContextmenuSeparator {...props} />
|
|
487
|
-
{/snippet}
|
|
307
|
+
<ContextmenuMenu bind:el {contextmenu} {open_guard} {link_entry} {text_entry} {separator_entry} />
|
|
488
308
|
|
|
489
309
|
<style>
|
|
490
310
|
:global(body.contextmenu-pending) {
|
|
@@ -499,35 +319,4 @@
|
|
|
499
319
|
.contextmenu-root {
|
|
500
320
|
display: contents;
|
|
501
321
|
}
|
|
502
|
-
.contextmenu {
|
|
503
|
-
--icon_size: var(--icon_size_xs);
|
|
504
|
-
/* TODO maybe make this responsive or a max of the page width
|
|
505
|
-
minus some space to tap items covered by the menu on the side,
|
|
506
|
-
or consider a totally different design for small screens (more dialog-like) */
|
|
507
|
-
--contextmenu_width: 320px;
|
|
508
|
-
position: fixed;
|
|
509
|
-
left: 0;
|
|
510
|
-
top: 0;
|
|
511
|
-
z-index: var(--contextmenu_z_index, 200);
|
|
512
|
-
max-width: var(--contextmenu_width);
|
|
513
|
-
width: 100%;
|
|
514
|
-
/* Re-enable callouts on the menu itself to allow native contextmenu (for dev tools).
|
|
515
|
-
Resets the global body blocking. Prevents the menu from being selected. */
|
|
516
|
-
-webkit-touch-callout: initial !important;
|
|
517
|
-
}
|
|
518
|
-
/* TODO hacky */
|
|
519
|
-
.contextmenu,
|
|
520
|
-
.contextmenu :global(menu.pane) {
|
|
521
|
-
border: var(--contextmenu_border_width, var(--border_width))
|
|
522
|
-
var(--contextmenu_border_style, var(--border_style))
|
|
523
|
-
var(--contextmenu_border_color, var(--border_color));
|
|
524
|
-
border-radius: var(--contextmenu_border_radius, var(--border_radius_xs));
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/* TODO better way to do this? */
|
|
528
|
-
.contextmenu-layout {
|
|
529
|
-
z-index: -200;
|
|
530
|
-
position: fixed;
|
|
531
|
-
inset: 0;
|
|
532
|
-
}
|
|
533
322
|
</style>
|
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import ContextmenuLinkEntry from './ContextmenuLinkEntry.svelte';
|
|
4
|
-
import ContextmenuTextEntry from './ContextmenuTextEntry.svelte';
|
|
5
|
-
import ContextmenuSeparator from './ContextmenuSeparator.svelte';
|
|
6
|
-
type $$ComponentProps = {
|
|
7
|
-
contextmenu?: ContextmenuState;
|
|
1
|
+
import { type ContextmenuRootBaseProps } from './contextmenu_helpers.js';
|
|
2
|
+
type $$ComponentProps = ContextmenuRootBaseProps & {
|
|
8
3
|
/**
|
|
9
4
|
* The number of pixels the pointer can be moved without canceling `longpress`.
|
|
10
5
|
*/
|
|
@@ -13,57 +8,6 @@ type $$ComponentProps = {
|
|
|
13
8
|
* The number of milliseconds after a touch starts before opening the Fuz contextmenu.
|
|
14
9
|
*/
|
|
15
10
|
longpress_duration?: number;
|
|
16
|
-
/**
|
|
17
|
-
* Whether to detect tap-then-longpress to bypass the Fuz contextmenu.
|
|
18
|
-
* This allows access to the system contextmenu by tapping once then long-pressing.
|
|
19
|
-
* Setting to `false` disables the gesture.
|
|
20
|
-
*/
|
|
21
|
-
bypass_with_tap_then_longpress?: boolean;
|
|
22
|
-
/**
|
|
23
|
-
* The number of milliseconds between taps to detect a gesture that bypasses the Fuz contextmenu.
|
|
24
|
-
* Used only when `bypass_with_tap_then_longpress` is true.
|
|
25
|
-
* If the duration is too long, it'll detect more false positives and interrupt normal usage,
|
|
26
|
-
* but too short and some people will have difficulty performing the gesture.
|
|
27
|
-
*/
|
|
28
|
-
bypass_window?: number;
|
|
29
|
-
/**
|
|
30
|
-
* The number of pixels the pointer can be moved between taps to detect a tap-then-longpress.
|
|
31
|
-
* Used only when `bypass_with_tap_then_longpress` is true.
|
|
32
|
-
*/
|
|
33
|
-
bypass_move_tolerance?: number;
|
|
34
|
-
/**
|
|
35
|
-
* The number of pixels to offset from the pointer X position when opened.
|
|
36
|
-
* Useful to ensure the first menu item is immediately under the pointer.
|
|
37
|
-
*/
|
|
38
|
-
open_offset_x?: number;
|
|
39
|
-
/**
|
|
40
|
-
* The number of pixels to offset from the pointer Y position when opened.
|
|
41
|
-
* Useful to ensure the first menu item is immediately under the pointer.
|
|
42
|
-
*/
|
|
43
|
-
open_offset_y?: number;
|
|
44
|
-
/**
|
|
45
|
-
* If `true`, wraps `children` with a div and listens to events on it instead of the window.
|
|
46
|
-
*/
|
|
47
|
-
scoped?: boolean;
|
|
48
|
-
/**
|
|
49
|
-
* Snippet for rendering link entries.
|
|
50
|
-
* Set to `null` to disable automatic link detection.
|
|
51
|
-
* Defaults to `link_entry_default` which renders `ContextmenuLinkEntry`.
|
|
52
|
-
*/
|
|
53
|
-
link_entry?: Snippet<[ComponentProps<typeof ContextmenuLinkEntry>]> | null;
|
|
54
|
-
/**
|
|
55
|
-
* Snippet for rendering copy text entries.
|
|
56
|
-
* Set to `null` to disable automatic copy text detection.
|
|
57
|
-
* Defaults to `text_entry_default` which renders `ContextmenuTextEntry`.
|
|
58
|
-
*/
|
|
59
|
-
text_entry?: Snippet<[ComponentProps<typeof ContextmenuTextEntry>]> | null;
|
|
60
|
-
/**
|
|
61
|
-
* Snippet for rendering separator entries.
|
|
62
|
-
* Set to `null` to disable automatic separator rendering.
|
|
63
|
-
* Defaults to `separator_entry_default` which renders `ContextmenuSeparator`.
|
|
64
|
-
*/
|
|
65
|
-
separator_entry?: Snippet<[ComponentProps<typeof ContextmenuSeparator>]> | null;
|
|
66
|
-
children: Snippet;
|
|
67
11
|
};
|
|
68
12
|
declare const ContextmenuRootForSafariCompatibility: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
69
13
|
type ContextmenuRootForSafariCompatibility = ReturnType<typeof ContextmenuRootForSafariCompatibility>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContextmenuRootForSafariCompatibility.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/ContextmenuRootForSafariCompatibility.svelte"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ContextmenuRootForSafariCompatibility.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/ContextmenuRootForSafariCompatibility.svelte"],"names":[],"mappings":"AAmCA,OAAO,EAWL,KAAK,wBAAwB,EAC7B,MAAM,0BAA0B,CAAC;AAElC,KAAK,gBAAgB,GAAI,wBAAwB,GAAG;IACnD;;OAEG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC;;OAEG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAiQH,QAAA,MAAM,qCAAqC,sDAAwC,CAAC;AACpF,KAAK,qCAAqC,GAAG,UAAU,CAAC,OAAO,qCAAqC,CAAC,CAAC;AACtG,eAAe,qCAAqC,CAAC"}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
const {...rest}: SvelteHTMLElements['li'] = $props();
|
|
5
5
|
</script>
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
<!-- `aria-orientation` is omitted - it defaults to horizontal, correct for a vertical menu's rule -->
|
|
8
|
+
<li role="separator" {...rest} class:contextmenu-separator={true}></li>
|
|
8
9
|
|
|
9
10
|
<style>
|
|
10
11
|
.contextmenu-separator {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContextmenuSeparator.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/ContextmenuSeparator.svelte"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ContextmenuSeparator.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/ContextmenuSeparator.svelte"],"names":[],"mappings":"AAmBA,QAAA,MAAM,oBAAoB,gFAAwC,CAAC;AACnE,KAAK,oBAAoB,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACpE,eAAe,oBAAoB,CAAC"}
|