UNPKG

portal-vue

Version:

> A Portal Component for Vue 3, to render DOM outside of a component, anywhere in the document.

266 lines (265 loc) 6.89 kB
import { inject, provide, watch, defineComponent, onMounted, onUpdated, onBeforeUnmount, computed, h, reactive, readonly, createApp, getCurrentInstance } from "vue"; const wormholeSymbol = Symbol("wormhole"); function useWormhole() { const wh = inject(wormholeSymbol); if (!wh) { throw new Error(` [portal-vue]: Necessary Injection not found. Make sur you installed the plugin properly.`); } return wh; } function provideWormhole(wormhole2) { provide(wormholeSymbol, wormhole2); } const inBrowser = typeof window !== "undefined"; function warn(msg) { console.log("[portal-vue]: " + msg); } function assertStaticProps(component, props, propNames) { propNames.forEach( (name) => { watch( () => props[name], () => { warn( `Prop '${name}' of component ${component} is static, but was dynamically changed by the parent. This change will not have any effect.` ); } ); }, { flush: "post" } ); } function stableSort(array, compareFn) { return array.map((v, idx) => { return [idx, v]; }).sort(function(a, b) { return compareFn(a[1], b[1]) || a[0] - b[0]; }).map((c) => c[1]); } function usePortal(props, slots) { const wormhole2 = useWormhole(); function sendUpdate() { if (!inBrowser) return; const { to, name: from, order } = props; if (slots.default) { wormhole2.open({ to, from, order, content: slots.default }); } else { clear(); } } function clear(target) { wormhole2.close({ to: target ?? props.to, from: props.name }); } onMounted(() => { if (!props.disabled) { sendUpdate(); } }); onUpdated(() => { if (props.disabled) { clear(); } else { sendUpdate(); } }); onBeforeUnmount(() => { clear(); }); watch( () => props.to, (newTo, oldTo) => { if (props.disabled) return; if (oldTo && oldTo !== newTo) { clear(oldTo); } sendUpdate(); } ); } const Portal = defineComponent({ compatConfig: { MODE: 3 }, name: "portal", props: { disabled: { type: Boolean }, name: { type: [String, Symbol], default: () => Symbol() }, order: { type: Number }, slotProps: { type: Object, default: () => ({}) }, to: { type: String, default: () => String(Math.round(Math.random() * 1e7)) } }, setup(props, { slots }) { assertStaticProps("Portal", props, ["order", "name"]); usePortal(props, slots); return () => { if (props.disabled && slots.default) { return slots.default(props.slotProps); } else { return null; } }; } }); const PortalTargetContent = (_, { slots }) => { var _a; return (_a = slots.default) == null ? void 0 : _a.call(slots); }; const PortalTarget = defineComponent({ compatConfig: { MODE: 3 }, name: "portalTarget", props: { multiple: { type: Boolean, default: false }, name: { type: String, required: true }, slotProps: { type: Object, default: () => ({}) } }, emits: ["change"], setup(props, { emit, slots }) { const wormhole2 = useWormhole(); const slotVnodes = computed( () => { const transports = wormhole2.getContentForTarget( props.name, props.multiple ); const wrapperSlot = slots.wrapper; const rawNodes = transports.map((t) => t.content(props.slotProps)); const vnodes = wrapperSlot ? rawNodes.flatMap( (nodes) => nodes.length ? wrapperSlot(nodes) : [] ) : rawNodes.flat(1); return { vnodes, vnodesFn: () => vnodes }; } ); watch( slotVnodes, ({ vnodes }) => { const hasContent = vnodes.length > 0; const content = wormhole2.transports.get(props.name); const sources = content ? [...content.keys()] : []; emit("change", { hasContent, sources }); }, { flush: "post" } ); return () => { var _a; const hasContent = !!slotVnodes.value.vnodes.length; if (hasContent) { return [ h("div", { style: "display: none", key: "__portal-vue-hacky-scoped-slot-repair__" }), h(PortalTargetContent, slotVnodes.value.vnodesFn) ]; } else { return (_a = slots.default) == null ? void 0 : _a.call(slots); } }; } }); function createWormhole(asReadonly = true) { const transports = reactive(/* @__PURE__ */ new Map()); function open(transport) { if (!inBrowser) return; const { to, from, content, order = Infinity } = transport; if (!to || !from || !content) return; if (!transports.has(to)) { transports.set(to, /* @__PURE__ */ new Map()); } const transportsForTarget = transports.get(to); const newTransport = { to, from, content, order }; transportsForTarget.set(from, newTransport); } function close(transport) { const { to, from } = transport; if (!to || !from) return; const transportsForTarget = transports.get(to); if (!transportsForTarget) { return; } transportsForTarget.delete(from); if (!transportsForTarget.size) { transports.delete(to); } } function getContentForTarget(target, returnAll) { const transportsForTarget = transports.get(target); if (!transportsForTarget) return []; const content = Array.from((transportsForTarget == null ? void 0 : transportsForTarget.values()) || []); if (!returnAll) { return [content.pop()]; } return stableSort( content, (a, b) => a.order - b.order ); } const wh = { open, close, transports, getContentForTarget }; return asReadonly ? readonly(wh) : wh; } const wormhole = createWormhole(); function mountPortalTarget(targetProps, el) { const app = createApp({ render: () => h(PortalTarget, targetProps) }); if (!targetProps.multiple) { const provides = getCurrentInstance().provides ?? {}; app._context.provides = Object.create(provides); } onMounted(() => { app.mount(el); }); onBeforeUnmount(() => { app.unmount(); }); } function install(app, options = {}) { options.portalName !== false && app.component(options.portalName || "Portal", Portal); options.portalTargetName !== false && app.component(options.portalTargetName || "PortalTarget", PortalTarget); const wormhole$1 = options.wormhole ?? wormhole; app.provide(wormholeSymbol, wormhole$1); } const Wormhole = wormhole; const version = "3.0.0"; export { Portal, PortalTarget, Wormhole, createWormhole, install as default, install, mountPortalTarget, provideWormhole, useWormhole, version }; //# sourceMappingURL=portal-vue.es.dev.mjs.map