@fuzdev/fuz_ui 0.169.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/LICENSE +21 -0
- package/README.md +93 -0
- package/dist/Alert.svelte +108 -0
- package/dist/Alert.svelte.d.ts +16 -0
- package/dist/Alert.svelte.d.ts.map +1 -0
- package/dist/ApiDeclarationList.svelte +35 -0
- package/dist/ApiDeclarationList.svelte.d.ts +9 -0
- package/dist/ApiDeclarationList.svelte.d.ts.map +1 -0
- package/dist/ApiIndex.svelte +65 -0
- package/dist/ApiIndex.svelte.d.ts +23 -0
- package/dist/ApiIndex.svelte.d.ts.map +1 -0
- package/dist/ApiModule.svelte +124 -0
- package/dist/ApiModule.svelte.d.ts +22 -0
- package/dist/ApiModule.svelte.d.ts.map +1 -0
- package/dist/Breadcrumb.svelte +83 -0
- package/dist/Breadcrumb.svelte.d.ts +23 -0
- package/dist/Breadcrumb.svelte.d.ts.map +1 -0
- package/dist/Card.svelte +157 -0
- package/dist/Card.svelte.d.ts +13 -0
- package/dist/Card.svelte.d.ts.map +1 -0
- package/dist/ColorSchemeInput.svelte +65 -0
- package/dist/ColorSchemeInput.svelte.d.ts +11 -0
- package/dist/ColorSchemeInput.svelte.d.ts.map +1 -0
- package/dist/Contextmenu.svelte +30 -0
- package/dist/Contextmenu.svelte.d.ts +32 -0
- package/dist/Contextmenu.svelte.d.ts.map +1 -0
- package/dist/ContextmenuEntry.svelte +74 -0
- package/dist/ContextmenuEntry.svelte.d.ts +12 -0
- package/dist/ContextmenuEntry.svelte.d.ts.map +1 -0
- package/dist/ContextmenuLinkEntry.svelte +112 -0
- package/dist/ContextmenuLinkEntry.svelte.d.ts +12 -0
- package/dist/ContextmenuLinkEntry.svelte.d.ts.map +1 -0
- package/dist/ContextmenuRoot.svelte +372 -0
- package/dist/ContextmenuRoot.svelte.d.ts +71 -0
- package/dist/ContextmenuRoot.svelte.d.ts.map +1 -0
- package/dist/ContextmenuRootForSafariCompatibility.svelte +541 -0
- package/dist/ContextmenuRootForSafariCompatibility.svelte.d.ts +79 -0
- package/dist/ContextmenuRootForSafariCompatibility.svelte.d.ts.map +1 -0
- package/dist/ContextmenuSeparator.svelte +16 -0
- package/dist/ContextmenuSeparator.svelte.d.ts +4 -0
- package/dist/ContextmenuSeparator.svelte.d.ts.map +1 -0
- package/dist/ContextmenuSubmenu.svelte +116 -0
- package/dist/ContextmenuSubmenu.svelte.d.ts +10 -0
- package/dist/ContextmenuSubmenu.svelte.d.ts.map +1 -0
- package/dist/ContextmenuTextEntry.svelte +21 -0
- package/dist/ContextmenuTextEntry.svelte.d.ts +10 -0
- package/dist/ContextmenuTextEntry.svelte.d.ts.map +1 -0
- package/dist/CopyToClipboard.svelte +81 -0
- package/dist/CopyToClipboard.svelte.d.ts +18 -0
- package/dist/CopyToClipboard.svelte.d.ts.map +1 -0
- package/dist/DeclarationDetail.svelte +340 -0
- package/dist/DeclarationDetail.svelte.d.ts +8 -0
- package/dist/DeclarationDetail.svelte.d.ts.map +1 -0
- package/dist/DeclarationLink.svelte +50 -0
- package/dist/DeclarationLink.svelte.d.ts +8 -0
- package/dist/DeclarationLink.svelte.d.ts.map +1 -0
- package/dist/Details.svelte +51 -0
- package/dist/Details.svelte.d.ts +20 -0
- package/dist/Details.svelte.d.ts.map +1 -0
- package/dist/Dialog.svelte +217 -0
- package/dist/Dialog.svelte.d.ts +30 -0
- package/dist/Dialog.svelte.d.ts.map +1 -0
- package/dist/Dialogs.svelte +28 -0
- package/dist/Dialogs.svelte.d.ts +11 -0
- package/dist/Dialogs.svelte.d.ts.map +1 -0
- package/dist/Docs.svelte +179 -0
- package/dist/Docs.svelte.d.ts +13 -0
- package/dist/Docs.svelte.d.ts.map +1 -0
- package/dist/DocsContent.svelte +40 -0
- package/dist/DocsContent.svelte.d.ts +14 -0
- package/dist/DocsContent.svelte.d.ts.map +1 -0
- package/dist/DocsFooter.svelte +64 -0
- package/dist/DocsFooter.svelte.d.ts +15 -0
- package/dist/DocsFooter.svelte.d.ts.map +1 -0
- package/dist/DocsLink.svelte +41 -0
- package/dist/DocsLink.svelte.d.ts +12 -0
- package/dist/DocsLink.svelte.d.ts.map +1 -0
- package/dist/DocsList.svelte +44 -0
- package/dist/DocsList.svelte.d.ts +11 -0
- package/dist/DocsList.svelte.d.ts.map +1 -0
- package/dist/DocsMenu.svelte +55 -0
- package/dist/DocsMenu.svelte.d.ts +11 -0
- package/dist/DocsMenu.svelte.d.ts.map +1 -0
- package/dist/DocsMenuHeader.svelte +15 -0
- package/dist/DocsMenuHeader.svelte.d.ts +9 -0
- package/dist/DocsMenuHeader.svelte.d.ts.map +1 -0
- package/dist/DocsModulesList.svelte +32 -0
- package/dist/DocsModulesList.svelte.d.ts +7 -0
- package/dist/DocsModulesList.svelte.d.ts.map +1 -0
- package/dist/DocsPageLinks.svelte +61 -0
- package/dist/DocsPageLinks.svelte.d.ts +8 -0
- package/dist/DocsPageLinks.svelte.d.ts.map +1 -0
- package/dist/DocsPrimaryNav.svelte +93 -0
- package/dist/DocsPrimaryNav.svelte.d.ts +11 -0
- package/dist/DocsPrimaryNav.svelte.d.ts.map +1 -0
- package/dist/DocsSearch.svelte +48 -0
- package/dist/DocsSearch.svelte.d.ts +11 -0
- package/dist/DocsSearch.svelte.d.ts.map +1 -0
- package/dist/DocsSecondaryNav.svelte +63 -0
- package/dist/DocsSecondaryNav.svelte.d.ts +9 -0
- package/dist/DocsSecondaryNav.svelte.d.ts.map +1 -0
- package/dist/DocsTertiaryNav.svelte +118 -0
- package/dist/DocsTertiaryNav.svelte.d.ts +10 -0
- package/dist/DocsTertiaryNav.svelte.d.ts.map +1 -0
- package/dist/EcosystemLinks.svelte +53 -0
- package/dist/EcosystemLinks.svelte.d.ts +7 -0
- package/dist/EcosystemLinks.svelte.d.ts.map +1 -0
- package/dist/EcosystemLinksPanel.svelte +22 -0
- package/dist/EcosystemLinksPanel.svelte.d.ts +8 -0
- package/dist/EcosystemLinksPanel.svelte.d.ts.map +1 -0
- package/dist/GithubLink.svelte +75 -0
- package/dist/GithubLink.svelte.d.ts +14 -0
- package/dist/GithubLink.svelte.d.ts.map +1 -0
- package/dist/Glyph.svelte +28 -0
- package/dist/Glyph.svelte.d.ts +9 -0
- package/dist/Glyph.svelte.d.ts.map +1 -0
- package/dist/Hashlink.svelte +41 -0
- package/dist/Hashlink.svelte.d.ts +8 -0
- package/dist/Hashlink.svelte.d.ts.map +1 -0
- package/dist/HiddenPersonalLinks.svelte +6 -0
- package/dist/HiddenPersonalLinks.svelte.d.ts +27 -0
- package/dist/HiddenPersonalLinks.svelte.d.ts.map +1 -0
- package/dist/HueInput.svelte +127 -0
- package/dist/HueInput.svelte.d.ts +11 -0
- package/dist/HueInput.svelte.d.ts.map +1 -0
- package/dist/ImgOrSvg.svelte +58 -0
- package/dist/ImgOrSvg.svelte.d.ts +25 -0
- package/dist/ImgOrSvg.svelte.d.ts.map +1 -0
- package/dist/LibraryDetail.svelte +297 -0
- package/dist/LibraryDetail.svelte.d.ts +15 -0
- package/dist/LibraryDetail.svelte.d.ts.map +1 -0
- package/dist/LibrarySummary.svelte +151 -0
- package/dist/LibrarySummary.svelte.d.ts +16 -0
- package/dist/LibrarySummary.svelte.d.ts.map +1 -0
- package/dist/MdnLink.svelte +40 -0
- package/dist/MdnLink.svelte.d.ts +8 -0
- package/dist/MdnLink.svelte.d.ts.map +1 -0
- package/dist/Mdz.svelte +30 -0
- package/dist/Mdz.svelte.d.ts +10 -0
- package/dist/Mdz.svelte.d.ts.map +1 -0
- package/dist/MdzNodeView.svelte +93 -0
- package/dist/MdzNodeView.svelte.d.ts +9 -0
- package/dist/MdzNodeView.svelte.d.ts.map +1 -0
- package/dist/ModuleLink.svelte +48 -0
- package/dist/ModuleLink.svelte.d.ts +8 -0
- package/dist/ModuleLink.svelte.d.ts.map +1 -0
- package/dist/PasteFromClipboard.svelte +35 -0
- package/dist/PasteFromClipboard.svelte.d.ts +9 -0
- package/dist/PasteFromClipboard.svelte.d.ts.map +1 -0
- package/dist/PendingAnimation.svelte +62 -0
- package/dist/PendingAnimation.svelte.d.ts +13 -0
- package/dist/PendingAnimation.svelte.d.ts.map +1 -0
- package/dist/PendingButton.svelte +75 -0
- package/dist/PendingButton.svelte.d.ts +17 -0
- package/dist/PendingButton.svelte.d.ts.map +1 -0
- package/dist/ProjectLinks.svelte +54 -0
- package/dist/ProjectLinks.svelte.d.ts +19 -0
- package/dist/ProjectLinks.svelte.d.ts.map +1 -0
- package/dist/Redirect.svelte +44 -0
- package/dist/Redirect.svelte.d.ts +23 -0
- package/dist/Redirect.svelte.d.ts.map +1 -0
- package/dist/Spiders.svelte +57 -0
- package/dist/Spiders.svelte.d.ts +9 -0
- package/dist/Spiders.svelte.d.ts.map +1 -0
- package/dist/Svg.svelte +99 -0
- package/dist/Svg.svelte.d.ts +54 -0
- package/dist/Svg.svelte.d.ts.map +1 -0
- package/dist/Teleport.svelte +48 -0
- package/dist/Teleport.svelte.d.ts +15 -0
- package/dist/Teleport.svelte.d.ts.map +1 -0
- package/dist/ThemeInput.svelte +75 -0
- package/dist/ThemeInput.svelte.d.ts +15 -0
- package/dist/ThemeInput.svelte.d.ts.map +1 -0
- package/dist/Themed.svelte +101 -0
- package/dist/Themed.svelte.d.ts +24 -0
- package/dist/Themed.svelte.d.ts.map +1 -0
- package/dist/TomeContent.svelte +67 -0
- package/dist/TomeContent.svelte.d.ts +12 -0
- package/dist/TomeContent.svelte.d.ts.map +1 -0
- package/dist/TomeHeader.svelte +56 -0
- package/dist/TomeHeader.svelte.d.ts +4 -0
- package/dist/TomeHeader.svelte.d.ts.map +1 -0
- package/dist/TomeLink.svelte +29 -0
- package/dist/TomeLink.svelte.d.ts +10 -0
- package/dist/TomeLink.svelte.d.ts.map +1 -0
- package/dist/TomeSection.svelte +65 -0
- package/dist/TomeSection.svelte.d.ts +24 -0
- package/dist/TomeSection.svelte.d.ts.map +1 -0
- package/dist/TomeSectionHeader.svelte +90 -0
- package/dist/TomeSectionHeader.svelte.d.ts +13 -0
- package/dist/TomeSectionHeader.svelte.d.ts.map +1 -0
- package/dist/TypeLink.svelte +19 -0
- package/dist/TypeLink.svelte.d.ts +7 -0
- package/dist/TypeLink.svelte.d.ts.map +1 -0
- package/dist/alert.d.ts +7 -0
- package/dist/alert.d.ts.map +1 -0
- package/dist/alert.js +6 -0
- package/dist/api_search.svelte.d.ts +16 -0
- package/dist/api_search.svelte.d.ts.map +1 -0
- package/dist/api_search.svelte.js +61 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +3 -0
- package/dist/context_helpers.d.ts +17 -0
- package/dist/context_helpers.d.ts.map +1 -0
- package/dist/context_helpers.js +19 -0
- package/dist/contextmenu_helpers.d.ts +16 -0
- package/dist/contextmenu_helpers.d.ts.map +1 -0
- package/dist/contextmenu_helpers.js +39 -0
- package/dist/contextmenu_state.svelte.d.ts +152 -0
- package/dist/contextmenu_state.svelte.d.ts.map +1 -0
- package/dist/contextmenu_state.svelte.js +424 -0
- package/dist/csp.d.ts +160 -0
- package/dist/csp.d.ts.map +1 -0
- package/dist/csp.js +354 -0
- package/dist/csp_of_ryanatkn.d.ts +6 -0
- package/dist/csp_of_ryanatkn.d.ts.map +1 -0
- package/dist/csp_of_ryanatkn.js +14 -0
- package/dist/declaration.svelte.d.ts +84 -0
- package/dist/declaration.svelte.d.ts.map +1 -0
- package/dist/declaration.svelte.js +66 -0
- package/dist/declaration_contextmenu.d.ts +4 -0
- package/dist/declaration_contextmenu.d.ts.map +1 -0
- package/dist/declaration_contextmenu.js +14 -0
- package/dist/dialog.d.ts +24 -0
- package/dist/dialog.d.ts.map +1 -0
- package/dist/dialog.js +12 -0
- package/dist/dimensions.svelte.d.ts +5 -0
- package/dist/dimensions.svelte.d.ts.map +1 -0
- package/dist/dimensions.svelte.js +4 -0
- package/dist/docs_helpers.svelte.d.ts +48 -0
- package/dist/docs_helpers.svelte.d.ts.map +1 -0
- package/dist/docs_helpers.svelte.js +99 -0
- package/dist/helpers.d.ts +2 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +16 -0
- package/dist/intersect.svelte.d.ts +47 -0
- package/dist/intersect.svelte.d.ts.map +1 -0
- package/dist/intersect.svelte.js +92 -0
- package/dist/library.svelte.d.ts +197 -0
- package/dist/library.svelte.d.ts.map +1 -0
- package/dist/library.svelte.js +130 -0
- package/dist/library_gen.d.ts +34 -0
- package/dist/library_gen.d.ts.map +1 -0
- package/dist/library_gen.js +123 -0
- package/dist/library_gen_helpers.d.ts +85 -0
- package/dist/library_gen_helpers.d.ts.map +1 -0
- package/dist/library_gen_helpers.js +188 -0
- package/dist/library_helpers.d.ts +54 -0
- package/dist/library_helpers.d.ts.map +1 -0
- package/dist/library_helpers.js +102 -0
- package/dist/logos.d.ts +134 -0
- package/dist/logos.d.ts.map +1 -0
- package/dist/logos.js +281 -0
- package/dist/mdz.d.ts +106 -0
- package/dist/mdz.d.ts.map +1 -0
- package/dist/mdz.js +1481 -0
- package/dist/mdz_components.d.ts +37 -0
- package/dist/mdz_components.d.ts.map +1 -0
- package/dist/mdz_components.js +12 -0
- package/dist/module.svelte.d.ts +47 -0
- package/dist/module.svelte.d.ts.map +1 -0
- package/dist/module.svelte.js +56 -0
- package/dist/module_contextmenu.d.ts +4 -0
- package/dist/module_contextmenu.d.ts.map +1 -0
- package/dist/module_contextmenu.js +14 -0
- package/dist/module_helpers.d.ts +69 -0
- package/dist/module_helpers.d.ts.map +1 -0
- package/dist/module_helpers.js +87 -0
- package/dist/rune_helpers.svelte.d.ts +6 -0
- package/dist/rune_helpers.svelte.d.ts.map +1 -0
- package/dist/rune_helpers.svelte.js +10 -0
- package/dist/storage.d.ts +13 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +43 -0
- package/dist/svelte_helpers.d.ts +37 -0
- package/dist/svelte_helpers.d.ts.map +1 -0
- package/dist/svelte_helpers.js +245 -0
- package/dist/themer.svelte.d.ts +24 -0
- package/dist/themer.svelte.d.ts.map +1 -0
- package/dist/themer.svelte.js +43 -0
- package/dist/tome.d.ts +80 -0
- package/dist/tome.d.ts.map +1 -0
- package/dist/tome.js +27 -0
- package/dist/ts_helpers.d.ts +110 -0
- package/dist/ts_helpers.d.ts.map +1 -0
- package/dist/ts_helpers.js +533 -0
- package/dist/tsdoc_helpers.d.ts +98 -0
- package/dist/tsdoc_helpers.d.ts.map +1 -0
- package/dist/tsdoc_helpers.js +221 -0
- package/package.json +128 -0
- package/src/lib/alert.ts +14 -0
- package/src/lib/api_search.svelte.ts +85 -0
- package/src/lib/constants.ts +3 -0
- package/src/lib/context_helpers.ts +47 -0
- package/src/lib/contextmenu_helpers.ts +63 -0
- package/src/lib/contextmenu_state.svelte.ts +515 -0
- package/src/lib/csp.ts +576 -0
- package/src/lib/csp_of_ryanatkn.ts +16 -0
- package/src/lib/declaration.svelte.ts +102 -0
- package/src/lib/declaration_contextmenu.ts +22 -0
- package/src/lib/dialog.ts +35 -0
- package/src/lib/dimensions.svelte.ts +4 -0
- package/src/lib/docs_helpers.svelte.ts +149 -0
- package/src/lib/helpers.ts +10 -0
- package/src/lib/intersect.svelte.ts +152 -0
- package/src/lib/library.svelte.ts +162 -0
- package/src/lib/library_gen.ts +160 -0
- package/src/lib/library_gen_helpers.ts +262 -0
- package/src/lib/library_helpers.ts +123 -0
- package/src/lib/logos.ts +302 -0
- package/src/lib/mdz.ts +1819 -0
- package/src/lib/mdz_components.ts +34 -0
- package/src/lib/module.svelte.ts +78 -0
- package/src/lib/module_contextmenu.ts +20 -0
- package/src/lib/module_helpers.ts +113 -0
- package/src/lib/rune_helpers.svelte.ts +10 -0
- package/src/lib/storage.ts +48 -0
- package/src/lib/svelte_helpers.ts +303 -0
- package/src/lib/themer.svelte.ts +68 -0
- package/src/lib/tome.ts +38 -0
- package/src/lib/ts_helpers.ts +662 -0
- package/src/lib/tsdoc_helpers.ts +259 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import {onDestroy, type Snippet} from 'svelte';
|
|
2
|
+
import type {Result} from '@fuzdev/fuz_util/result.js';
|
|
3
|
+
import {is_promise} from '@fuzdev/fuz_util/async.js';
|
|
4
|
+
import {BROWSER} from 'esm-env';
|
|
5
|
+
import type {SvelteHTMLElements} from 'svelte/elements';
|
|
6
|
+
import {EMPTY_OBJECT} from '@fuzdev/fuz_util/object.js';
|
|
7
|
+
import type {Attachment} from 'svelte/attachments';
|
|
8
|
+
|
|
9
|
+
import {Dimensions} from './dimensions.svelte.js';
|
|
10
|
+
import {create_context} from './context_helpers.js';
|
|
11
|
+
import {url_to_root_relative} from './library_helpers.js';
|
|
12
|
+
|
|
13
|
+
export type ContextmenuParams =
|
|
14
|
+
| Snippet
|
|
15
|
+
// TODO maybe this should be generic?
|
|
16
|
+
| {snippet: 'link'; props: {href: string; icon?: string}}
|
|
17
|
+
| {snippet: 'text'; props: {content: string; icon: string; run: ContextmenuRun}}
|
|
18
|
+
| {snippet: 'separator'; props: SvelteHTMLElements['li']};
|
|
19
|
+
|
|
20
|
+
export type ContextmenuActivateResult =
|
|
21
|
+
| void
|
|
22
|
+
| undefined // eslint-disable-line @typescript-eslint/no-redundant-type-constituents
|
|
23
|
+
| Result<{close?: boolean}, {message?: string}>;
|
|
24
|
+
|
|
25
|
+
export type ItemState = SubmenuState | EntryState;
|
|
26
|
+
|
|
27
|
+
export class EntryState {
|
|
28
|
+
readonly is_menu = false; // TODO rename to `type`?
|
|
29
|
+
readonly menu: SubmenuState | RootMenuState;
|
|
30
|
+
|
|
31
|
+
readonly run: () => ContextmenuRun;
|
|
32
|
+
readonly disabled: () => boolean;
|
|
33
|
+
|
|
34
|
+
selected: boolean = $state(false);
|
|
35
|
+
pending: boolean = $state(false);
|
|
36
|
+
error_message: string | null = $state(null);
|
|
37
|
+
promise: Promise<ContextmenuActivateResult> | null = $state.raw(null);
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
menu: SubmenuState | RootMenuState,
|
|
41
|
+
run: () => ContextmenuRun,
|
|
42
|
+
disabled: () => boolean = () => false,
|
|
43
|
+
) {
|
|
44
|
+
this.menu = menu;
|
|
45
|
+
this.run = run;
|
|
46
|
+
this.disabled = disabled;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class SubmenuState {
|
|
51
|
+
readonly is_menu = true;
|
|
52
|
+
readonly menu: SubmenuState | RootMenuState;
|
|
53
|
+
readonly depth: number;
|
|
54
|
+
|
|
55
|
+
selected: boolean = $state(false);
|
|
56
|
+
items: ReadonlyArray<ItemState> = $state.raw([]);
|
|
57
|
+
|
|
58
|
+
constructor(menu: SubmenuState | RootMenuState, depth: number) {
|
|
59
|
+
this.menu = menu;
|
|
60
|
+
this.depth = depth;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class RootMenuState {
|
|
65
|
+
readonly is_menu = true;
|
|
66
|
+
readonly menu = null;
|
|
67
|
+
readonly depth = 1;
|
|
68
|
+
|
|
69
|
+
items: ReadonlyArray<ItemState> = $state.raw([]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type ContextmenuRun = () => ContextmenuActivateResult | Promise<ContextmenuActivateResult>;
|
|
73
|
+
|
|
74
|
+
export interface ContextmenuStateOptions {
|
|
75
|
+
layout?: Dimensions; // TODO consider making this a prop on `ContextmenuRoot`, and being assigned here
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Creates a `contextmenu` store.
|
|
80
|
+
* See usage with `ContextmenuRoot.svelte` and `Contextmenu.svelte`.
|
|
81
|
+
*
|
|
82
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event
|
|
83
|
+
*/
|
|
84
|
+
export class ContextmenuState {
|
|
85
|
+
layout: Dimensions; // TODO $state?
|
|
86
|
+
/**
|
|
87
|
+
* If an initial layout is provided, control is deferred externally.
|
|
88
|
+
* Otherwise the layout syncs to the page dimensions.
|
|
89
|
+
*/
|
|
90
|
+
readonly has_custom_layout: boolean;
|
|
91
|
+
|
|
92
|
+
// State for external consumers.
|
|
93
|
+
opened: boolean = $state(false);
|
|
94
|
+
x: number = $state(0);
|
|
95
|
+
y: number = $state(0);
|
|
96
|
+
params: ReadonlyArray<ContextmenuParams> = $state.raw([]);
|
|
97
|
+
error: string | undefined = $state();
|
|
98
|
+
|
|
99
|
+
// These arrays use immutable updates (reassignment, not mutation).
|
|
100
|
+
// If you need reactivity, use `$contextmenu` in a reactive statement to react to all changes, and
|
|
101
|
+
// then access the immutable `contextmenu.root_menu` and `contextmenu.selections`.
|
|
102
|
+
// See `ContextmenuEntry.svelte` and `ContextmenuSubmenu.svelte` for reactive usage examples.
|
|
103
|
+
readonly root_menu: RootMenuState = new RootMenuState();
|
|
104
|
+
selections: ReadonlyArray<ItemState> = $state.raw([]);
|
|
105
|
+
|
|
106
|
+
can_collapse = $derived(this.selections.length > 1);
|
|
107
|
+
|
|
108
|
+
can_expand = $derived.by(() => {
|
|
109
|
+
const selected = this.selections.at(-1);
|
|
110
|
+
return !!selected?.is_menu && selected.items.length > 0;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
can_select_next = $derived.by(() => {
|
|
114
|
+
const menu = this.selections.at(-1)?.menu ?? this.root_menu;
|
|
115
|
+
return menu.items.length > 1;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
can_select_previous = $derived.by(() => {
|
|
119
|
+
const menu = this.selections.at(-1)?.menu ?? this.root_menu;
|
|
120
|
+
return menu.items.length > 1;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
can_activate = $derived.by(() => {
|
|
124
|
+
const selected = this.selections.at(-1);
|
|
125
|
+
if (!selected) return false;
|
|
126
|
+
if (selected.is_menu) return selected.items.length > 0;
|
|
127
|
+
return !selected.disabled();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
constructor(options: ContextmenuStateOptions = EMPTY_OBJECT) {
|
|
131
|
+
const {layout} = options;
|
|
132
|
+
|
|
133
|
+
this.has_custom_layout = !!layout;
|
|
134
|
+
this.layout = layout ?? new Dimensions();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
open(params: Array<ContextmenuParams>, x: number, y: number): void {
|
|
138
|
+
this.selections = [];
|
|
139
|
+
this.opened = true;
|
|
140
|
+
this.x = x;
|
|
141
|
+
this.y = y;
|
|
142
|
+
this.params = params;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
close(): void {
|
|
146
|
+
if (!this.opened) return;
|
|
147
|
+
this.reset_items(this.root_menu.items);
|
|
148
|
+
this.opened = false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
reset_items(items: ReadonlyArray<ItemState>): void {
|
|
152
|
+
for (const item of items) {
|
|
153
|
+
if (item.is_menu) {
|
|
154
|
+
this.reset_items(item.items);
|
|
155
|
+
} else {
|
|
156
|
+
item.promise = null;
|
|
157
|
+
item.pending = false;
|
|
158
|
+
item.error_message = null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
activate(item: ItemState): boolean | Promise<ContextmenuActivateResult> {
|
|
164
|
+
if (item.is_menu) {
|
|
165
|
+
this.expand_selected();
|
|
166
|
+
} else {
|
|
167
|
+
if (item.disabled()) return false;
|
|
168
|
+
let returned;
|
|
169
|
+
try {
|
|
170
|
+
returned = item.run()();
|
|
171
|
+
} catch (error) {
|
|
172
|
+
const message = typeof error?.message === 'string' ? error.message : undefined;
|
|
173
|
+
item.error_message = message ?? 'unknown error';
|
|
174
|
+
this.error = message;
|
|
175
|
+
}
|
|
176
|
+
if (is_promise(returned)) {
|
|
177
|
+
item.pending = true;
|
|
178
|
+
item.error_message = null;
|
|
179
|
+
const promise = (item.promise = returned
|
|
180
|
+
.then(
|
|
181
|
+
(result) => {
|
|
182
|
+
if (promise !== item.promise) return;
|
|
183
|
+
if (typeof result?.ok === 'boolean') {
|
|
184
|
+
if (result.ok) {
|
|
185
|
+
if (result.close !== false) {
|
|
186
|
+
this.close();
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
const message = typeof result.message === 'string' ? result.message : undefined;
|
|
190
|
+
item.error_message = message ?? 'unknown error';
|
|
191
|
+
this.error = message;
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
// void or undefined - default behavior is to close
|
|
195
|
+
this.close();
|
|
196
|
+
}
|
|
197
|
+
return result;
|
|
198
|
+
},
|
|
199
|
+
(err) => {
|
|
200
|
+
if (promise !== item.promise) return;
|
|
201
|
+
const message = typeof err?.message === 'string' ? err.message : undefined;
|
|
202
|
+
item.error_message = message ?? 'unknown error';
|
|
203
|
+
this.error = message;
|
|
204
|
+
},
|
|
205
|
+
)
|
|
206
|
+
.finally(() => {
|
|
207
|
+
if (promise !== item.promise) return;
|
|
208
|
+
item.pending = false;
|
|
209
|
+
item.promise = null;
|
|
210
|
+
}));
|
|
211
|
+
return item.promise; // async path
|
|
212
|
+
}
|
|
213
|
+
// synchronous path
|
|
214
|
+
if (typeof returned?.ok === 'boolean') {
|
|
215
|
+
if (returned.ok) {
|
|
216
|
+
if (returned.close !== false) {
|
|
217
|
+
this.close();
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
const message = typeof returned.message === 'string' ? returned.message : undefined;
|
|
221
|
+
item.error_message = message ?? 'unknown error';
|
|
222
|
+
this.error = message;
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
// void or undefined - default behavior is to close
|
|
226
|
+
this.close();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
activate_selected(): void | boolean | Promise<ContextmenuActivateResult> {
|
|
233
|
+
const selected = this.selections.at(-1);
|
|
234
|
+
if (selected) {
|
|
235
|
+
return this.activate(selected);
|
|
236
|
+
}
|
|
237
|
+
this.select_first();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Instead of diffing, this does the simple thing and
|
|
241
|
+
// deselects everything and then re-creates the list of selections.
|
|
242
|
+
// Could be improved but it's fine because we're using mutation and the N is very small,
|
|
243
|
+
// and it allows us to have a single code path for the various selection methods.
|
|
244
|
+
/**
|
|
245
|
+
* Activates the selected entry, or if none, selects the first.
|
|
246
|
+
*/
|
|
247
|
+
// TODO implement focus management per APG: call .focus() on the selected item's DOM element (requires storing element refs in EntryState/SubmenuState)
|
|
248
|
+
select(item: ItemState): void {
|
|
249
|
+
if (this.selections.at(-1) === item) return;
|
|
250
|
+
for (const s of this.selections) s.selected = false;
|
|
251
|
+
const new_selections: Array<ItemState> = [];
|
|
252
|
+
let i: ItemState | RootMenuState = item;
|
|
253
|
+
do {
|
|
254
|
+
i.selected = true;
|
|
255
|
+
new_selections.unshift(i);
|
|
256
|
+
} while ((i = i.menu) && i.menu); // eslint-disable-line @typescript-eslint/no-unnecessary-condition
|
|
257
|
+
this.selections = new_selections;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
collapse_selected(): void {
|
|
261
|
+
if (!this.can_collapse) return;
|
|
262
|
+
const deselected = this.selections.at(-1)!;
|
|
263
|
+
deselected.selected = false;
|
|
264
|
+
this.selections = this.selections.slice(0, -1);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
expand_selected(): void {
|
|
268
|
+
if (!this.can_expand) return;
|
|
269
|
+
const parent = this.selections.at(-1);
|
|
270
|
+
if (!parent?.is_menu || !parent.items.length) return;
|
|
271
|
+
const selected = parent.items[0]!;
|
|
272
|
+
selected.selected = true;
|
|
273
|
+
this.selections = [...this.selections, selected];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
select_next(): void {
|
|
277
|
+
if (!this.selections.length) {
|
|
278
|
+
this.select_first();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const item = this.selections.at(-1)!;
|
|
282
|
+
const index = item.menu.items.indexOf(item);
|
|
283
|
+
this.select(item.menu.items[index === item.menu.items.length - 1 ? 0 : index + 1]!);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
select_previous(): void {
|
|
287
|
+
if (!this.selections.length) {
|
|
288
|
+
this.select_last();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const item = this.selections.at(-1)!;
|
|
292
|
+
const index = item.menu.items.indexOf(item);
|
|
293
|
+
this.select(item.menu.items[index === 0 ? item.menu.items.length - 1 : index - 1]!);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
select_first(): void {
|
|
297
|
+
const menu = this.selections.at(-1)?.menu ?? this.root_menu;
|
|
298
|
+
if (!menu.items.length) return;
|
|
299
|
+
this.select(menu.items[0]!);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
select_last(): void {
|
|
303
|
+
const {items} = this.selections.at(-1)?.menu ?? this.root_menu;
|
|
304
|
+
this.select(items.at(-1)!);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Used by `ContextmenuEntry` and custom entry components
|
|
309
|
+
* @initializes
|
|
310
|
+
*/
|
|
311
|
+
add_entry(run: () => ContextmenuRun, disabled: () => boolean = () => false): EntryState {
|
|
312
|
+
const menu = contextmenu_submenu_context.get_maybe() ?? this.root_menu;
|
|
313
|
+
const entry = new EntryState(menu, run, disabled);
|
|
314
|
+
menu.items = [...menu.items, entry];
|
|
315
|
+
// TODO messy, runs more than needed
|
|
316
|
+
onDestroy(() => {
|
|
317
|
+
menu.items = [];
|
|
318
|
+
});
|
|
319
|
+
return entry;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* @initializes
|
|
324
|
+
*/
|
|
325
|
+
add_submenu(): SubmenuState {
|
|
326
|
+
const menu = contextmenu_submenu_context.get_maybe() ?? this.root_menu;
|
|
327
|
+
const submenu = new SubmenuState(menu, menu.depth + 1);
|
|
328
|
+
menu.items = [...menu.items, submenu];
|
|
329
|
+
contextmenu_submenu_context.set(submenu);
|
|
330
|
+
// TODO messy, runs more than needed
|
|
331
|
+
onDestroy(() => {
|
|
332
|
+
menu.items = [];
|
|
333
|
+
});
|
|
334
|
+
return submenu;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// The dataset key must not have capital letters or dashes or it'll differ between JS and DOM:
|
|
339
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset
|
|
340
|
+
const CONTEXTMENU_DATASET_KEY = 'contextmenu';
|
|
341
|
+
const CONTEXTMENU_DOM_QUERY = `a,[data-${CONTEXTMENU_DATASET_KEY}]`;
|
|
342
|
+
const contextmenu_cache: Map<string, ContextmenuParams | Array<ContextmenuParams>> = new Map();
|
|
343
|
+
let cache_key_counter = 0;
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Creates an attachment that sets up contextmenu behavior on an element.
|
|
347
|
+
* @param params Contextmenu parameters or nullish to disable
|
|
348
|
+
*/
|
|
349
|
+
export const contextmenu_attachment =
|
|
350
|
+
<T extends ContextmenuParams, U extends T | Array<T>>(
|
|
351
|
+
params: U | null | undefined,
|
|
352
|
+
): Attachment<HTMLElement | SVGElement> =>
|
|
353
|
+
(el): undefined | (() => void) => {
|
|
354
|
+
// TODO could clean up the dataset attr, maybe use a weakmap?
|
|
355
|
+
if (params == null) return;
|
|
356
|
+
|
|
357
|
+
// Only create key once per element, reuse on updates
|
|
358
|
+
let key = el.dataset[CONTEXTMENU_DATASET_KEY];
|
|
359
|
+
if (!key) {
|
|
360
|
+
key = cache_key_counter++ + '';
|
|
361
|
+
el.dataset[CONTEXTMENU_DATASET_KEY] = key;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
contextmenu_cache.set(key, params);
|
|
365
|
+
|
|
366
|
+
return () => {
|
|
367
|
+
contextmenu_cache.delete(key);
|
|
368
|
+
};
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const CONTEXTMENU_OPEN_VIBRATE_DURATION = 17;
|
|
372
|
+
|
|
373
|
+
export interface ContextmenuOpenOptions {
|
|
374
|
+
link_enabled?: boolean;
|
|
375
|
+
text_enabled?: boolean;
|
|
376
|
+
separator_enabled?: boolean;
|
|
377
|
+
vibrate?: boolean;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Opens the contextmenu, if appropriate,
|
|
382
|
+
* querying the menu items from the DOM starting at the event target.
|
|
383
|
+
* @param target the leaf element from which to open the contextmenu
|
|
384
|
+
* @param x the page X coordinate at which to open the contextmenu, typically the mouse `pageX`
|
|
385
|
+
* @param y the page Y coordinate at which to open the contextmenu, typically the mouse `pageY`
|
|
386
|
+
* @param contextmenu the contextmenu store
|
|
387
|
+
* @param options optional configuration for filtering entries and haptic feedback
|
|
388
|
+
* @returns a boolean indicating if the menu was opened or not
|
|
389
|
+
*/
|
|
390
|
+
export const contextmenu_open = (
|
|
391
|
+
target: HTMLElement | SVGElement,
|
|
392
|
+
x: number,
|
|
393
|
+
y: number,
|
|
394
|
+
contextmenu: ContextmenuState,
|
|
395
|
+
options?: ContextmenuOpenOptions,
|
|
396
|
+
): boolean => {
|
|
397
|
+
const {
|
|
398
|
+
link_enabled = true,
|
|
399
|
+
text_enabled = true,
|
|
400
|
+
separator_enabled = true,
|
|
401
|
+
vibrate = true,
|
|
402
|
+
} = options ?? EMPTY_OBJECT;
|
|
403
|
+
|
|
404
|
+
const params = contextmenu_query_params(target)?.filter(
|
|
405
|
+
(p) =>
|
|
406
|
+
typeof p === 'function' ||
|
|
407
|
+
((p.snippet !== 'link' || link_enabled) &&
|
|
408
|
+
(p.snippet !== 'text' || text_enabled) &&
|
|
409
|
+
(p.snippet !== 'separator' || separator_enabled)),
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
// No-op if empty
|
|
413
|
+
if (!params?.length) return false;
|
|
414
|
+
|
|
415
|
+
contextmenu.open(params, x, y);
|
|
416
|
+
|
|
417
|
+
// `navigator.vibrate()` works with `ContextmenuRoot` but gets blocked by some browsers
|
|
418
|
+
// when used with `ContextmenuRootForSafariCompatibility` because its longpress
|
|
419
|
+
// workaround triggers from a timeout rather than a direct user interaction.
|
|
420
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
421
|
+
if (BROWSER && vibrate && navigator.vibrate) {
|
|
422
|
+
navigator.vibrate(CONTEXTMENU_OPEN_VIBRATE_DURATION);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return true;
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const contextmenu_query_params = (
|
|
429
|
+
target: HTMLElement | SVGElement,
|
|
430
|
+
): null | Array<ContextmenuParams> => {
|
|
431
|
+
let params: null | Array<ContextmenuParams> = null;
|
|
432
|
+
// crawl DOM for contextmenu entries
|
|
433
|
+
let el: HTMLElement | SVGElement | null | undefined = target;
|
|
434
|
+
let cache_key: string, cached: ContextmenuParams | Array<ContextmenuParams> | undefined;
|
|
435
|
+
while ((el = el?.closest(CONTEXTMENU_DOM_QUERY))) {
|
|
436
|
+
if ((cache_key = el.dataset[CONTEXTMENU_DATASET_KEY]!)) {
|
|
437
|
+
params ??= [];
|
|
438
|
+
cached = contextmenu_cache.get(cache_key);
|
|
439
|
+
if (cached === undefined) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
// preserve bubbling order
|
|
443
|
+
if (Array.isArray(cached)) {
|
|
444
|
+
(params ??= []).push(...cached);
|
|
445
|
+
} else {
|
|
446
|
+
(params ??= []).push(cached);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (el.tagName === 'A') {
|
|
450
|
+
(params ??= []).push({
|
|
451
|
+
snippet: 'link',
|
|
452
|
+
// anchor elements have the full url, but we want the slash-prefixed/absolute/root-relative version
|
|
453
|
+
props: {href: url_to_root_relative((el as HTMLAnchorElement).href)},
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
el = el.parentElement;
|
|
457
|
+
}
|
|
458
|
+
// add "copy text" entry if anything is selected and we have any other entries
|
|
459
|
+
if (params) {
|
|
460
|
+
const text = window.getSelection()?.toString();
|
|
461
|
+
if (text) {
|
|
462
|
+
params.unshift({
|
|
463
|
+
snippet: 'text',
|
|
464
|
+
props: {
|
|
465
|
+
content: 'copy text',
|
|
466
|
+
icon: '📋',
|
|
467
|
+
run: async () => {
|
|
468
|
+
await navigator.clipboard.writeText(text);
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return params;
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
export const contextmenu_context = create_context<ContextmenuState>();
|
|
478
|
+
|
|
479
|
+
export const contextmenu_submenu_context = create_context<SubmenuState>();
|
|
480
|
+
|
|
481
|
+
export const contextmenu_dimensions_context = create_context(() => new Dimensions());
|
|
482
|
+
|
|
483
|
+
// Global registry of non-scoped contextmenu roots (only used in DEV)
|
|
484
|
+
const non_scoped_roots: Set<symbol> = new Set();
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Registers a contextmenu root and warns if multiple non-scoped roots are detected.
|
|
488
|
+
* Only active in development mode. Automatically handles cleanup on unmount.
|
|
489
|
+
*
|
|
490
|
+
* @param get_scoped Getter function that returns the current scoped value
|
|
491
|
+
*/
|
|
492
|
+
export const contextmenu_check_global_root = (get_scoped: () => boolean): void => {
|
|
493
|
+
$effect(() => {
|
|
494
|
+
const id = Symbol('contextmenu_root');
|
|
495
|
+
|
|
496
|
+
if (!get_scoped()) {
|
|
497
|
+
// Register as global (non-scoped)
|
|
498
|
+
non_scoped_roots.add(id);
|
|
499
|
+
|
|
500
|
+
if (non_scoped_roots.size > 1) {
|
|
501
|
+
// eslint-disable-next-line no-console
|
|
502
|
+
console.error(
|
|
503
|
+
`Detected multiple non-scoped contextmenu roots (${non_scoped_roots.size} mounted). ` +
|
|
504
|
+
'Only one global contextmenu root should be active at a time. ' +
|
|
505
|
+
'Are you missing a `scoped` attribute?',
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Cleanup: unregister when scoped changes or component unmounts
|
|
511
|
+
return () => {
|
|
512
|
+
non_scoped_roots.delete(id);
|
|
513
|
+
};
|
|
514
|
+
});
|
|
515
|
+
};
|