2025-09-05 14:59:21 +08:00

172 lines
5.4 KiB
JavaScript

import { Suspense, computed, defineComponent, h, inject, mergeProps, nextTick, onMounted, provide, shallowReactive, shallowRef, unref } from "vue";
import { useRoute, useRouter } from "../composables/router.js";
import { useNuxtApp } from "../nuxt.js";
import { _wrapInTransition } from "./utils.js";
import { LayoutMetaSymbol, PageRouteSymbol } from "./injections.js";
import { useRoute as useVueRouterRoute } from "#build/pages";
import layouts from "#build/layouts";
import { appLayoutTransition as defaultLayoutTransition } from "#build/nuxt.config.mjs";
const LayoutLoader = defineComponent({
name: "LayoutLoader",
inheritAttrs: false,
props: {
name: String,
layoutProps: Object
},
setup(props, context) {
return () => h(layouts[props.name], props.layoutProps, context.slots);
}
});
const nuxtLayoutProps = {
name: {
type: [String, Boolean, Object],
default: null
},
fallback: {
type: [String, Object],
default: null
}
};
export default defineComponent({
name: "NuxtLayout",
inheritAttrs: false,
props: nuxtLayoutProps,
setup(props, context) {
const nuxtApp = useNuxtApp();
const injectedRoute = inject(PageRouteSymbol);
const shouldUseEagerRoute = !injectedRoute || injectedRoute === useRoute();
const route = shouldUseEagerRoute ? useVueRouterRoute() : injectedRoute;
const layout = computed(() => {
let layout2 = unref(props.name) ?? route?.meta.layout ?? "default";
if (layout2 && !(layout2 in layouts)) {
if (import.meta.dev && layout2 !== "default") {
console.warn(`Invalid layout \`${layout2}\` selected.`);
}
if (props.fallback) {
layout2 = unref(props.fallback);
}
}
return layout2;
});
const layoutRef = shallowRef();
context.expose({ layoutRef });
const done = nuxtApp.deferHydration();
if (import.meta.client && nuxtApp.isHydrating) {
const removeErrorHook = nuxtApp.hooks.hookOnce("app:error", done);
useRouter().beforeEach(removeErrorHook);
}
if (import.meta.dev) {
nuxtApp._isNuxtLayoutUsed = true;
}
let lastLayout;
return () => {
const hasLayout = layout.value && layout.value in layouts;
const transitionProps = route?.meta.layoutTransition ?? defaultLayoutTransition;
const previouslyRenderedLayout = lastLayout;
lastLayout = layout.value;
return _wrapInTransition(hasLayout && transitionProps, {
default: () => h(Suspense, { suspensible: true, onResolve: () => {
nextTick(done);
} }, {
default: () => h(
LayoutProvider,
{
layoutProps: mergeProps(context.attrs, { ref: layoutRef }),
key: layout.value || void 0,
name: layout.value,
shouldProvide: !props.name,
isRenderingNewLayout: (name) => {
return name !== previouslyRenderedLayout && name === layout.value;
},
hasTransition: !!transitionProps
},
context.slots
)
})
}).default();
};
}
});
const LayoutProvider = defineComponent({
name: "NuxtLayoutProvider",
inheritAttrs: false,
props: {
name: {
type: [String, Boolean]
},
layoutProps: {
type: Object
},
hasTransition: {
type: Boolean
},
shouldProvide: {
type: Boolean
},
isRenderingNewLayout: {
type: Function,
required: true
}
},
setup(props, context) {
const name = props.name;
if (props.shouldProvide) {
provide(LayoutMetaSymbol, {
isCurrent: (route) => name === (route.meta.layout ?? "default")
});
}
const injectedRoute = inject(PageRouteSymbol);
const isNotWithinNuxtPage = injectedRoute && injectedRoute === useRoute();
if (isNotWithinNuxtPage) {
const vueRouterRoute = useVueRouterRoute();
const reactiveChildRoute = {};
for (const _key in vueRouterRoute) {
const key = _key;
Object.defineProperty(reactiveChildRoute, key, {
enumerable: true,
get: () => {
return props.isRenderingNewLayout(props.name) ? vueRouterRoute[key] : injectedRoute[key];
}
});
}
provide(PageRouteSymbol, shallowReactive(reactiveChildRoute));
}
let vnode;
if (import.meta.dev && import.meta.client) {
onMounted(() => {
nextTick(() => {
if (["#comment", "#text"].includes(vnode?.el?.nodeName)) {
if (name) {
console.warn(`[nuxt] \`${name}\` layout does not have a single root node and will cause errors when navigating between routes.`);
} else {
console.warn("[nuxt] `<NuxtLayout>` needs to be passed a single root node in its default slot.");
}
}
});
});
}
return () => {
if (!name || typeof name === "string" && !(name in layouts)) {
if (import.meta.dev && import.meta.client && props.hasTransition) {
vnode = context.slots.default?.();
return vnode;
}
return context.slots.default?.();
}
if (import.meta.dev && import.meta.client && props.hasTransition) {
vnode = h(
LayoutLoader,
{ key: name, layoutProps: props.layoutProps, name },
context.slots
);
return vnode;
}
return h(
LayoutLoader,
{ key: name, layoutProps: props.layoutProps, name },
context.slots
);
};
}
});