@basmilius/routing 0.0.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/README.md +21 -0
- package/dist/index.d.mts +98 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +62 -0
- package/src/augmentations.ts +22 -0
- package/src/component/ModalRouterView.ts +34 -0
- package/src/component/RouterLink.ts +129 -0
- package/src/component/RouterView.ts +207 -0
- package/src/composable/useIsView.ts +12 -0
- package/src/composable/useModalRoute.ts +28 -0
- package/src/composable/useNamedRoute.ts +23 -0
- package/src/composable/useNavigate.ts +35 -0
- package/src/composable/useRoute.ts +83 -0
- package/src/composable/useRouteMeta.ts +32 -0
- package/src/composable/useRouteNames.ts +14 -0
- package/src/composable/useRouteParam.ts +16 -0
- package/src/composable/useRouteView.ts +23 -0
- package/src/createRouter.ts +23 -0
- package/src/index.ts +26 -0
- package/src/internal/RoutedView.ts +66 -0
- package/src/internal/modalContext.ts +205 -0
- package/src/internal/modalState.ts +54 -0
- package/src/internal/patchRouter.ts +86 -0
- package/src/internal/resolveModal.ts +20 -0
- package/src/symbol.ts +12 -0
- package/src/types.ts +34 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { type Component, defineComponent, Fragment, h, inject, nextTick, onBeforeUnmount, provide, type Ref, shallowRef, unref, type VNodeChild, watch } from 'vue';
|
|
2
|
+
import { RouterView as VueRouterView, useRoute, useRouter, viewDepthKey } from 'vue-router';
|
|
3
|
+
import { BackgroundProvider, ModalProvider } from '../internal/RoutedView';
|
|
4
|
+
import resolveModal from '../internal/resolveModal';
|
|
5
|
+
import { innerReadyKey, modalContextKey, routeOverrideKey } from '../symbol';
|
|
6
|
+
import type { ModalConfig } from '../types';
|
|
7
|
+
|
|
8
|
+
const RouterView: Component = defineComponent({
|
|
9
|
+
name: 'RouterView',
|
|
10
|
+
inheritAttrs: false,
|
|
11
|
+
props: {
|
|
12
|
+
// note: Opts this instance in as the host that renders the modal
|
|
13
|
+
// layer on top of the background route. Exactly one `<RouterView>`
|
|
14
|
+
// in the tree should set this; if multiple do, the first to mount
|
|
15
|
+
// wins (others render vanilla and emit a console warning).
|
|
16
|
+
modals: {
|
|
17
|
+
type: Boolean,
|
|
18
|
+
default: false
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
setup(props, {attrs, slots}) {
|
|
22
|
+
const ctx = inject(modalContextKey, null);
|
|
23
|
+
const override = inject(routeOverrideKey, null);
|
|
24
|
+
const route = useRoute();
|
|
25
|
+
const router = useRouter();
|
|
26
|
+
|
|
27
|
+
// note: Vue-router injects `viewDepthKey` from the parent
|
|
28
|
+
// `VueRouterView` (or 0 at the top). It can be a number or a
|
|
29
|
+
// `Ref<number>`; `unref` collapses both. Passed to
|
|
30
|
+
// `BackgroundProvider` so the background tree resumes at the
|
|
31
|
+
// correct matched index when this RouterView is nested.
|
|
32
|
+
const injectedViewDepth = inject(viewDepthKey, 0);
|
|
33
|
+
|
|
34
|
+
// note: Identity used to claim/release the modal host role on the
|
|
35
|
+
// shared `modalContext`. The matching unmount release targets
|
|
36
|
+
// exactly our claim — never a successor's.
|
|
37
|
+
const hostId = Symbol('basmilius:routing:router-view-host');
|
|
38
|
+
|
|
39
|
+
let isModalHost = false;
|
|
40
|
+
|
|
41
|
+
if (props.modals && override === null && ctx) {
|
|
42
|
+
isModalHost = ctx.claimHost(hostId);
|
|
43
|
+
|
|
44
|
+
if (!isModalHost) {
|
|
45
|
+
// eslint-disable-next-line no-console
|
|
46
|
+
console.warn(
|
|
47
|
+
'[routing] Multiple <RouterView modals> instances detected. ' +
|
|
48
|
+
'The first mounted instance owns the modal host role; this ' +
|
|
49
|
+
'instance will render as a vanilla RouterView.'
|
|
50
|
+
);
|
|
51
|
+
} else {
|
|
52
|
+
onBeforeUnmount(() => ctx.releaseHost(hostId));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// note: Wrapper component (e.g. FluxOverlay) stays mounted across
|
|
57
|
+
// modal close for its leave animation, and is reused when
|
|
58
|
+
// consecutive modals share the same wrapper.
|
|
59
|
+
const lastModal = shallowRef<ModalConfig | null>(null);
|
|
60
|
+
|
|
61
|
+
// note: Held as a ref because Vue's runtime `h()` can miss
|
|
62
|
+
// primitive prop updates on `ModalProvider` when route changes
|
|
63
|
+
// patch in the same tick. Ref identity stays stable.
|
|
64
|
+
const modalDepthRef: Ref<number> = shallowRef(0);
|
|
65
|
+
|
|
66
|
+
// note: Gates whether the modal's inner `<RouterView>` is attached.
|
|
67
|
+
// On a user-triggered open we hold it back one tick so the
|
|
68
|
+
// wrapper's `<Transition>` observes "no child -> child" and plays
|
|
69
|
+
// its enter animation. On hard refresh of a modal URL we skip the
|
|
70
|
+
// gate so the page arrives already-open without animating.
|
|
71
|
+
const innerReady = shallowRef(false);
|
|
72
|
+
|
|
73
|
+
// note: Snapshot at setup — `modalContext` flips `initiallyOpen`
|
|
74
|
+
// back to `false` once the background route resolves, which is
|
|
75
|
+
// too early to read from the watcher below.
|
|
76
|
+
const wasInitiallyOpen = ctx?.initiallyOpen.value ?? false;
|
|
77
|
+
|
|
78
|
+
let hasSeenActiveModal = false;
|
|
79
|
+
|
|
80
|
+
// note: Exposed so `ModalRouterView` inside consumer wrapper
|
|
81
|
+
// templates honours the same one-tick delay as the fallback slot.
|
|
82
|
+
provide(innerReadyKey, innerReady);
|
|
83
|
+
|
|
84
|
+
if (ctx && isModalHost) {
|
|
85
|
+
watch(() => ctx.backgroundRoute.value !== null, async (isOpen) => {
|
|
86
|
+
if (!isOpen) {
|
|
87
|
+
innerReady.value = false;
|
|
88
|
+
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const isFirstActivation = !hasSeenActiveModal;
|
|
93
|
+
hasSeenActiveModal = true;
|
|
94
|
+
|
|
95
|
+
// note: Pageload of a modal URL — wrapper and inner mount
|
|
96
|
+
// together without a transition.
|
|
97
|
+
if (isFirstActivation && wasInitiallyOpen) {
|
|
98
|
+
innerReady.value = true;
|
|
99
|
+
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// note: User-triggered open — wrapper renders first with
|
|
104
|
+
// an empty slot, inner attaches next tick to animate in.
|
|
105
|
+
innerReady.value = false;
|
|
106
|
+
await nextTick();
|
|
107
|
+
innerReady.value = true;
|
|
108
|
+
}, {immediate: true, flush: 'pre'});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (): VNodeChild => {
|
|
112
|
+
// note: Already inside a BackgroundProvider / ModalProvider
|
|
113
|
+
// subtree — render vanilla so we don't recurse into another
|
|
114
|
+
// background/modal split.
|
|
115
|
+
if (override !== null) {
|
|
116
|
+
return h(VueRouterView, attrs, slots);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!ctx) {
|
|
120
|
+
return h(VueRouterView, attrs, slots);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// note: Only the instance that claimed the host role renders
|
|
124
|
+
// modals. All others fall through to vanilla.
|
|
125
|
+
if (!isModalHost) {
|
|
126
|
+
return h(VueRouterView, attrs, slots);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const backgroundRoute = ctx.backgroundRoute.value;
|
|
130
|
+
const modalActive = ctx.isModal.value && backgroundRoute !== null;
|
|
131
|
+
|
|
132
|
+
// note: Resolve inline (not via watcher) — at setup the route
|
|
133
|
+
// is `START_LOCATION` with empty `matched`, so a watcher with
|
|
134
|
+
// `immediate: true` would miss the per-route `meta.modal`.
|
|
135
|
+
if (modalActive) {
|
|
136
|
+
const resolved = resolveModal(route, ctx.defaultModal);
|
|
137
|
+
|
|
138
|
+
if (resolved !== null && resolved !== lastModal.value) {
|
|
139
|
+
lastModal.value = resolved;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const wrapperConfig = lastModal.value;
|
|
144
|
+
|
|
145
|
+
// note: Never been a modal here -> plain RouterView.
|
|
146
|
+
if (!modalActive && !wrapperConfig) {
|
|
147
|
+
return h(VueRouterView, attrs, slots);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// note: Background renders the stored route while open; the
|
|
151
|
+
// current route while closed (wrapper lingering for leave
|
|
152
|
+
// animation). Keeps layout-level `useRoute()` stable.
|
|
153
|
+
const bgRoute = modalActive ? backgroundRoute : route;
|
|
154
|
+
|
|
155
|
+
// note: `ctx.depth` is the user-supplied parent count. Translate
|
|
156
|
+
// to an absolute `matched[]` index:
|
|
157
|
+
// depth 0 -> matched[length - 1] (deepest only)
|
|
158
|
+
// depth N -> matched[length - 1 - N]
|
|
159
|
+
const parentCount = ctx.depth.value;
|
|
160
|
+
const viewDepth = Math.max(0, route.matched.length - 1 - parentCount);
|
|
161
|
+
|
|
162
|
+
modalDepthRef.value = viewDepth;
|
|
163
|
+
|
|
164
|
+
// note: ModalProvider wraps the wrapper component so the
|
|
165
|
+
// wrapper's internal Teleport still inherits the provider
|
|
166
|
+
// context. Inner is `undefined` while closed or gated, so
|
|
167
|
+
// the wrapper's `<Transition>` observes an empty slot.
|
|
168
|
+
const modalInner = modalActive && innerReady.value
|
|
169
|
+
? h(VueRouterView)
|
|
170
|
+
: undefined;
|
|
171
|
+
|
|
172
|
+
const wrappedModalInner = wrapperConfig
|
|
173
|
+
? h(wrapperConfig.component, {
|
|
174
|
+
...(wrapperConfig.props ?? {}),
|
|
175
|
+
// note: Runtime props after the spread so consumers
|
|
176
|
+
// can't shadow them via `meta.modal.props`. Use
|
|
177
|
+
// `modalReady` (not `modalActive`) to v-if the
|
|
178
|
+
// inner `<ModalRouterView>` — `modalActive` is true
|
|
179
|
+
// at mount and would skip the enter animation.
|
|
180
|
+
modalRoute: route,
|
|
181
|
+
modalActive,
|
|
182
|
+
modalReady: modalActive && innerReady.value,
|
|
183
|
+
onClose: (): void => {
|
|
184
|
+
router.back();
|
|
185
|
+
}
|
|
186
|
+
}, {
|
|
187
|
+
default: (): VNodeChild => modalInner
|
|
188
|
+
})
|
|
189
|
+
: modalInner;
|
|
190
|
+
|
|
191
|
+
const modalLayer = h(ModalProvider, {route, depthRef: modalDepthRef}, {
|
|
192
|
+
default: (): VNodeChild => wrappedModalInner
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const hostViewDepth = unref(injectedViewDepth);
|
|
196
|
+
|
|
197
|
+
return h(Fragment, [
|
|
198
|
+
h(BackgroundProvider, {route: bgRoute, viewDepth: hostViewDepth}, {
|
|
199
|
+
default: (): VNodeChild => h(VueRouterView, attrs, slots)
|
|
200
|
+
}),
|
|
201
|
+
modalLayer
|
|
202
|
+
]);
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
export default RouterView;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { computed, type ComputedRef, unref } from 'vue';
|
|
2
|
+
import useRouteNames from './useRouteNames';
|
|
3
|
+
|
|
4
|
+
export default function (name: string, loose: boolean = false): ComputedRef<boolean> {
|
|
5
|
+
const names = useRouteNames();
|
|
6
|
+
|
|
7
|
+
if (loose) {
|
|
8
|
+
return computed(() => unref(names).some(n => n.startsWith(name)));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return computed(() => unref(names).some(n => n === name));
|
|
12
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { computed, type ComputedRef, inject, unref } from 'vue';
|
|
2
|
+
import { type RouteLocationNormalized, useRoute as useVueRoute } from 'vue-router';
|
|
3
|
+
import { isInModalKey, modalContextKey, routeOverrideKey } from '../symbol';
|
|
4
|
+
|
|
5
|
+
// note: Surfaces the route driving the currently active modal, or `null`
|
|
6
|
+
// when no modal is active. Inside a ModalProvider subtree the override
|
|
7
|
+
// IS the modal route; outside, the router's live currentRoute is the
|
|
8
|
+
// modal route (background is stashed on the context).
|
|
9
|
+
// `BackgroundProvider` also sets `routeOverrideKey`, so `isInModalKey`
|
|
10
|
+
// is the discriminator between background and modal subtrees.
|
|
11
|
+
export default function (): ComputedRef<RouteLocationNormalized | null> {
|
|
12
|
+
const override = inject(routeOverrideKey, null);
|
|
13
|
+
const isInModal = inject(isInModalKey, null);
|
|
14
|
+
const ctx = inject(modalContextKey, null);
|
|
15
|
+
const rawRoute = useVueRoute();
|
|
16
|
+
|
|
17
|
+
return computed(() => {
|
|
18
|
+
if (isInModal !== null && unref(isInModal) && override !== null) {
|
|
19
|
+
return unref(override);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (ctx !== null && unref(ctx.isModal)) {
|
|
23
|
+
return rawRoute;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return null;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { computed, type ComputedRef, provide, type Ref, unref } from 'vue';
|
|
2
|
+
import { viewDepthKey } from 'vue-router';
|
|
3
|
+
import useRoute, { type UseRoute } from './useRoute';
|
|
4
|
+
|
|
5
|
+
export default function (nameRef: Ref<string> | string): UseNamedRoute {
|
|
6
|
+
const route = useRoute();
|
|
7
|
+
|
|
8
|
+
const depth = computed(() => unref(route).matched.findIndex(m => !!m.components && unref(nameRef) in m.components));
|
|
9
|
+
const matched = computed(() => unref(route).matched[unref(depth)]);
|
|
10
|
+
const viewKey = computed(() => unref(matched)?.path);
|
|
11
|
+
|
|
12
|
+
provide(viewDepthKey, depth);
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
route,
|
|
16
|
+
viewKey
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type UseNamedRoute = {
|
|
21
|
+
readonly route: UseRoute;
|
|
22
|
+
readonly viewKey: ComputedRef<string | undefined>;
|
|
23
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type NavigationFailure, type RouteLocationRaw, useRouter } from 'vue-router';
|
|
2
|
+
|
|
3
|
+
type Result = NavigationFailure | void | undefined;
|
|
4
|
+
type To = Omit<RouteLocationRaw, 'replace'>;
|
|
5
|
+
type Navigate = (to: To, replace?: boolean) => Promise<Result>;
|
|
6
|
+
type Wrap = (fn: Navigate) => Navigate;
|
|
7
|
+
|
|
8
|
+
export default function (...wrap: Wrap[]): UseNavigate {
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
|
|
11
|
+
let navigate: Navigate = async (to: To, replace: boolean = false) => {
|
|
12
|
+
if (replace) {
|
|
13
|
+
return await router.replace(to);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return await router.push(to);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
for (const wrapper of wrap) {
|
|
20
|
+
navigate = wrapper(navigate);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
navigate,
|
|
25
|
+
|
|
26
|
+
push: (to: To) => navigate(to),
|
|
27
|
+
replace: (to: To) => navigate(to, true)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type UseNavigate = {
|
|
32
|
+
navigate(to: To, replace?: boolean): Promise<Result>;
|
|
33
|
+
push(to: To): Promise<Result>;
|
|
34
|
+
replace(to: To): Promise<Result>;
|
|
35
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { computed, inject, shallowRef, unref } from 'vue';
|
|
2
|
+
import { type RouteLocationNormalizedLoaded, useRoute as useVueRoute } from 'vue-router';
|
|
3
|
+
import { isInModalKey, modalContextKey, routeOverrideKey } from '../symbol';
|
|
4
|
+
|
|
5
|
+
export type UseRoute = RouteLocationNormalizedLoaded & {
|
|
6
|
+
readonly isModal: boolean;
|
|
7
|
+
|
|
8
|
+
promote(): Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const FALSE_REF = shallowRef(false);
|
|
12
|
+
const NOOP = async (): Promise<void> => undefined;
|
|
13
|
+
|
|
14
|
+
export default function (): UseRoute {
|
|
15
|
+
const override = inject(routeOverrideKey, null);
|
|
16
|
+
const ctx = inject(modalContextKey, null);
|
|
17
|
+
const isModalRef = inject(isInModalKey, FALSE_REF);
|
|
18
|
+
const raw = useVueRoute();
|
|
19
|
+
|
|
20
|
+
const routeRef = computed<RouteLocationNormalizedLoaded>(() => {
|
|
21
|
+
// note: Inside a Background/ModalProvider subtree — use the
|
|
22
|
+
// explicit override so background-tree components see the
|
|
23
|
+
// background route and modal-wrapper components see the modal
|
|
24
|
+
// route, independent of the router's live currentRoute.
|
|
25
|
+
if (override !== null) {
|
|
26
|
+
return unref(override) as RouteLocationNormalizedLoaded;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// note: Outside any provider (top-level layouts) — surface the
|
|
30
|
+
// background route while a modal is open so layout-level
|
|
31
|
+
// `:key`s stay stable across opens/closes.
|
|
32
|
+
if (ctx !== null && unref(ctx.isModal)) {
|
|
33
|
+
const bg = unref(ctx.backgroundRoute);
|
|
34
|
+
|
|
35
|
+
if (bg !== null) {
|
|
36
|
+
return bg as RouteLocationNormalizedLoaded;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return raw;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const promote = ctx !== null ? ctx.promote : NOOP;
|
|
44
|
+
|
|
45
|
+
// note: Proxy (not a Ref) so the object can be used directly in
|
|
46
|
+
// templates without auto-unwrapping stripping our extras. All
|
|
47
|
+
// route properties flow through `routeRef.value` to keep reactivity
|
|
48
|
+
// tracking intact in templates, computeds, and watchers.
|
|
49
|
+
return new Proxy({} as UseRoute, {
|
|
50
|
+
get(_target, key) {
|
|
51
|
+
if (key === 'isModal') {
|
|
52
|
+
return isModalRef.value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (key === 'promote') {
|
|
56
|
+
return promote;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (routeRef.value as unknown as Record<PropertyKey, unknown>)[key];
|
|
60
|
+
},
|
|
61
|
+
has(_target, key) {
|
|
62
|
+
if (key === 'isModal' || key === 'promote') {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return key in routeRef.value;
|
|
67
|
+
},
|
|
68
|
+
ownKeys(_target) {
|
|
69
|
+
return [...Reflect.ownKeys(routeRef.value), 'isModal', 'promote'];
|
|
70
|
+
},
|
|
71
|
+
getOwnPropertyDescriptor(_target, key) {
|
|
72
|
+
if (key === 'isModal') {
|
|
73
|
+
return {configurable: true, enumerable: true, value: isModalRef.value};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (key === 'promote') {
|
|
77
|
+
return {configurable: true, enumerable: true, value: promote};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return Object.getOwnPropertyDescriptor(routeRef.value, key);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { merge } from 'lodash-es';
|
|
2
|
+
import { computed, type ComputedRef, unref } from 'vue';
|
|
3
|
+
import type { RouteMeta } from 'vue-router';
|
|
4
|
+
import useRoute from './useRoute';
|
|
5
|
+
|
|
6
|
+
export default function (): ComputedRef<RouteMeta> {
|
|
7
|
+
const route = useRoute();
|
|
8
|
+
|
|
9
|
+
return computed(() => {
|
|
10
|
+
const matched = unref(route).matched;
|
|
11
|
+
let meta: RouteMeta = {};
|
|
12
|
+
|
|
13
|
+
for (let i = matched.length - 1; i >= 0; --i) {
|
|
14
|
+
const record = matched[i];
|
|
15
|
+
|
|
16
|
+
if (!record || typeof record.meta !== 'object') {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let matchMeta = {...record.meta};
|
|
21
|
+
|
|
22
|
+
if ('navigation' in meta) {
|
|
23
|
+
const {navigation: _navigation, ...matchMetaWithoutNavigation} = matchMeta;
|
|
24
|
+
matchMeta = matchMetaWithoutNavigation;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
meta = merge(meta, matchMeta);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return meta;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { computed, type ComputedRef, unref } from 'vue';
|
|
2
|
+
import useRoute from './useRoute';
|
|
3
|
+
|
|
4
|
+
export default function (): ComputedRef<string[]> {
|
|
5
|
+
const route = useRoute();
|
|
6
|
+
|
|
7
|
+
return computed(() => {
|
|
8
|
+
const names: string[] = [];
|
|
9
|
+
|
|
10
|
+
unref(route).matched.forEach(m => m.name && names.push(m.name as string));
|
|
11
|
+
|
|
12
|
+
return names;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Ref, ref, unref, watch } from 'vue';
|
|
2
|
+
import useRoute from './useRoute';
|
|
3
|
+
|
|
4
|
+
export default function (name: string, defaultValue: string | null = null): Ref<string | null> {
|
|
5
|
+
const route = useRoute();
|
|
6
|
+
const param = ref<string | null>(null);
|
|
7
|
+
|
|
8
|
+
// note: `defaultValue` applies on every read of a missing/empty
|
|
9
|
+
// param, not just at mount. The `immediate: true` flush handles
|
|
10
|
+
// the initial value so we don't duplicate the fallback logic.
|
|
11
|
+
watch(() => unref(route).params[name] as string | undefined, (value) => {
|
|
12
|
+
param.value = value || defaultValue;
|
|
13
|
+
}, {immediate: true});
|
|
14
|
+
|
|
15
|
+
return param;
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { computed, type ComputedRef, type Ref, unref } from 'vue';
|
|
2
|
+
import type { RouteComponent } from 'vue-router';
|
|
3
|
+
import useRoute from './useRoute';
|
|
4
|
+
|
|
5
|
+
export default function (nameRef: Ref<string> | string): ComputedRef<RouteComponent | null> {
|
|
6
|
+
const route = useRoute();
|
|
7
|
+
|
|
8
|
+
return computed(() => {
|
|
9
|
+
const name = unref(nameRef);
|
|
10
|
+
|
|
11
|
+
for (const match of unref(route).matched) {
|
|
12
|
+
if (!match.components) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (name in match.components) {
|
|
17
|
+
return match.components[name];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return null;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { App } from 'vue';
|
|
2
|
+
import { createRouter as createVueRouter, type Router } from 'vue-router';
|
|
3
|
+
import createModalContext from './internal/modalContext';
|
|
4
|
+
import patchRouter from './internal/patchRouter';
|
|
5
|
+
import { modalContextKey } from './symbol';
|
|
6
|
+
import type { RouterOptions } from './types';
|
|
7
|
+
|
|
8
|
+
export default function createRouter(options: RouterOptions): Router {
|
|
9
|
+
const {defaultModal, ...routerOptions} = options;
|
|
10
|
+
|
|
11
|
+
const router = createVueRouter(routerOptions);
|
|
12
|
+
const original = patchRouter(router);
|
|
13
|
+
const ctx = createModalContext(router, original, defaultModal ?? null);
|
|
14
|
+
|
|
15
|
+
const originalInstall = router.install.bind(router);
|
|
16
|
+
|
|
17
|
+
router.install = (app: App): void => {
|
|
18
|
+
originalInstall(app);
|
|
19
|
+
app.provide(modalContextKey, ctx);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return router;
|
|
23
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// note: Side-effect import — registers the `declare module 'vue-router'`
|
|
2
|
+
// augmentations (`RouteLocationOptions.modal`, `RouteMeta.modal`).
|
|
3
|
+
import './augmentations';
|
|
4
|
+
|
|
5
|
+
export * from 'vue-router';
|
|
6
|
+
|
|
7
|
+
// note: Named exports below shadow the star re-export — ES modules
|
|
8
|
+
// resolve direct exports over star re-exports. Our enhanced versions
|
|
9
|
+
// win for `createRouter`, `RouterView`, etc.; every other vue-router
|
|
10
|
+
// symbol flows through unchanged.
|
|
11
|
+
export { default as createRouter } from './createRouter';
|
|
12
|
+
export type { ModalConfig, ModalWrapperProps, RouterOptions, RouterViewProps } from './types';
|
|
13
|
+
|
|
14
|
+
export { default as ModalRouterView } from './component/ModalRouterView';
|
|
15
|
+
export { default as RouterLink } from './component/RouterLink';
|
|
16
|
+
export { default as RouterView } from './component/RouterView';
|
|
17
|
+
|
|
18
|
+
export { default as useIsView } from './composable/useIsView';
|
|
19
|
+
export { default as useModalRoute } from './composable/useModalRoute';
|
|
20
|
+
export { default as useNamedRoute } from './composable/useNamedRoute';
|
|
21
|
+
export { default as useNavigate } from './composable/useNavigate';
|
|
22
|
+
export { default as useRoute, type UseRoute } from './composable/useRoute';
|
|
23
|
+
export { default as useRouteMeta } from './composable/useRouteMeta';
|
|
24
|
+
export { default as useRouteNames } from './composable/useRouteNames';
|
|
25
|
+
export { default as useRouteParam } from './composable/useRouteParam';
|
|
26
|
+
export { default as useRouteView } from './composable/useRouteView';
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type Component, computed, defineComponent, type PropType, provide, type Ref, shallowRef, type VNodeChild } from 'vue';
|
|
2
|
+
import { type RouteLocationNormalized, routerViewLocationKey, viewDepthKey } from 'vue-router';
|
|
3
|
+
import { isInModalKey, routeOverrideKey } from '../symbol';
|
|
4
|
+
|
|
5
|
+
const TRUE_REF = shallowRef(true);
|
|
6
|
+
const FALSE_REF = shallowRef(false);
|
|
7
|
+
|
|
8
|
+
// note: Both providers override `routerViewLocationKey` so descendant
|
|
9
|
+
// `<RouterView>`s render the supplied `route` instead of the router's
|
|
10
|
+
// live currentRoute. `viewDepthKey` is set to the index at which the
|
|
11
|
+
// inner `VueRouterView` should resume in `matched[]` — for
|
|
12
|
+
// `BackgroundProvider`, the host's own depth (so a nested host doesn't
|
|
13
|
+
// re-render its ancestors); for `ModalProvider`, the absolute matched
|
|
14
|
+
// index computed by RouterView from `ctx.depth`.
|
|
15
|
+
|
|
16
|
+
export const BackgroundProvider: Component = defineComponent({
|
|
17
|
+
name: 'BackgroundProvider',
|
|
18
|
+
props: {
|
|
19
|
+
route: {
|
|
20
|
+
type: Object as PropType<RouteLocationNormalized>,
|
|
21
|
+
required: true
|
|
22
|
+
},
|
|
23
|
+
viewDepth: {
|
|
24
|
+
type: Number,
|
|
25
|
+
default: 0
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
setup(props, {slots}) {
|
|
29
|
+
const routeRef = computed(() => props.route);
|
|
30
|
+
|
|
31
|
+
provide(routerViewLocationKey, routeRef);
|
|
32
|
+
provide(viewDepthKey, props.viewDepth);
|
|
33
|
+
provide(routeOverrideKey, routeRef);
|
|
34
|
+
provide(isInModalKey, FALSE_REF);
|
|
35
|
+
|
|
36
|
+
return (): VNodeChild => slots.default?.();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const ModalProvider: Component = defineComponent({
|
|
41
|
+
name: 'ModalProvider',
|
|
42
|
+
props: {
|
|
43
|
+
route: {
|
|
44
|
+
type: Object as PropType<RouteLocationNormalized>,
|
|
45
|
+
required: true
|
|
46
|
+
},
|
|
47
|
+
// note: Ref instead of a plain number — Vue's runtime `h()` prop
|
|
48
|
+
// diffing can miss primitive updates when patching in a tight
|
|
49
|
+
// cycle alongside reactive route changes. Ref identity stays
|
|
50
|
+
// stable; `.value` propagates through normal reactivity.
|
|
51
|
+
depthRef: {
|
|
52
|
+
type: Object as PropType<Ref<number>>,
|
|
53
|
+
required: true
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
setup(props, {slots}) {
|
|
57
|
+
const routeRef = computed(() => props.route);
|
|
58
|
+
|
|
59
|
+
provide(routerViewLocationKey, routeRef);
|
|
60
|
+
provide(viewDepthKey, props.depthRef);
|
|
61
|
+
provide(routeOverrideKey, routeRef);
|
|
62
|
+
provide(isInModalKey, TRUE_REF);
|
|
63
|
+
|
|
64
|
+
return (): VNodeChild => slots.default?.();
|
|
65
|
+
}
|
|
66
|
+
});
|