import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared' import { type Block, type BlockFn, insert, setScopeId } from './block' import { rawPropsProxyHandlers, resolveFunctionSource } from './componentProps' import { type GenericComponentInstance, currentInstance, isAsyncWrapper, isRef, } from '@vue/runtime-dom' import type { LooseRawProps, VaporComponentInstance } from './component' import { renderEffect } from './renderEffect' import { insertionAnchor, insertionParent, isLastInsertion, resetInsertionState, } from './insertionState' import { advanceHydrationNode, isHydrating, locateHydrationNode, } from './dom/hydration' import { type DynamicFragment, SlotFragment, type VaporFragment, } from './fragment' import { createElement } from './dom/node' import { setDynamicProps } from './dom/prop' import { isInteropEnabled } from './vdomInteropState' /** * Flag to indicate if we are executing a once slot. * When true, renderEffect should skip creating reactive effect. */ export let inOnceSlot = false /** * Current slot scopeIds for vdom interop */ export let currentSlotScopeIds: string[] | null = null function setCurrentSlotScopeIds(scopeIds: string[] | null): string[] | null { try { return currentSlotScopeIds } finally { currentSlotScopeIds = scopeIds } } export type RawSlots = Record & { $?: DynamicSlotSource[] } export type StaticSlots = Record export type VaporSlot = BlockFn export type DynamicSlot = { name: string; fn: VaporSlot } export type DynamicSlotFn = () => DynamicSlot | DynamicSlot[] export type DynamicSlotSource = StaticSlots | DynamicSlotFn export const dynamicSlotsProxyHandlers: ProxyHandler = { get: getSlot, has: (target, key: string) => !!getSlot(target, key), getOwnPropertyDescriptor(target, key: string) { const slot = getSlot(target, key) if (slot) { return { configurable: true, enumerable: true, value: slot, } } }, ownKeys(target) { let keys = Object.keys(target) const dynamicSources = target.$ if (dynamicSources) { keys = keys.filter(k => k !== '$') for (const source of dynamicSources) { if (isFunction(source)) { const slot = resolveFunctionSource(source) if (slot) { if (isArray(slot)) { for (const s of slot) keys.push(String(s.name)) } else { keys.push(String(slot.name)) } } } else { keys.push(...Object.keys(source)) } } } return keys }, set: NO, deleteProperty: NO, } export function getSlot( target: RawSlots, key: string, ): | (VaporSlot & { _boundMap?: WeakMap }) | undefined { if (key === '$') return const dynamicSources = target.$ if (dynamicSources) { let i = dynamicSources.length let source while (i--) { source = dynamicSources[i] if (isFunction(source)) { const slot = resolveFunctionSource(source) if (slot) { if (isArray(slot)) { for (const s of slot) { if (String(s.name) === key) return s.fn } } else if (String(slot.name) === key) { return slot.fn } } } else if (hasOwn(source, key)) { return source[key] } } } if (hasOwn(target, key)) { return target[key] } } /** * Tracks the slot owner (the component that defines the slot content). * This is used for: * 1. Getting the correct rawSlots in forwarded slots (via createSlot) * 2. Inheriting the slot owner's scopeId */ export let currentSlotOwner: VaporComponentInstance | null = null export function setCurrentSlotOwner( owner: VaporComponentInstance | null, ): VaporComponentInstance | null { try { return currentSlotOwner } finally { currentSlotOwner = owner } } /** * Get the effective slot instance for accessing rawSlots and scopeId. * Prefers currentSlotOwner (if inside a slot), falls back to currentInstance. */ export function getScopeOwner(): VaporComponentInstance | null { return (currentSlotOwner || currentInstance) as VaporComponentInstance | null } /** * Wrap a slot function to track the slot owner. * * This ensures: * 1. createSlot gets rawSlots from the correct instance (slot owner) * 2. elements inherit the slot owner's scopeId */ export function withVaporCtx(fn: Function): BlockFn { const owner = getScopeOwner() return (...args: any[]) => { const prevOwner = setCurrentSlotOwner(owner) try { return fn(...args) } finally { setCurrentSlotOwner(prevOwner) } } } export function createSlot( name: string | (() => string), rawProps?: LooseRawProps | null, fallback?: VaporSlot, noSlotted?: boolean, once?: boolean, ): Block { const _insertionParent = insertionParent const _insertionAnchor = insertionAnchor const _isLastInsertion = isLastInsertion if (!isHydrating) resetInsertionState() const instance = getScopeOwner()! const rawSlots = instance.rawSlots const slotProps = rawProps ? new Proxy(rawProps, rawPropsProxyHandlers) : EMPTY_OBJ let fragment: SlotFragment if (isRef(rawSlots._) && isInteropEnabled) { if (isHydrating) locateHydrationNode() fragment = instance.appContext.vapor!.vdomSlot( rawSlots._, name, slotProps, instance, fallback, ) } else { fragment = new SlotFragment() // mark the slot as forwarded fragment.forwarded = currentSlotOwner != null && currentSlotOwner !== currentInstance const isDynamicName = isFunction(name) // Calculate slotScopeIds once (for vdom interop) const slotScopeIds: string[] = [] if (!noSlotted) { const scopeId = instance.type.__scopeId if (scopeId) { slotScopeIds.push(`${scopeId}-s`) } } const renderSlot = () => { const slotName = isFunction(name) ? name() : name // in custom element mode, render as actual slot outlets // because in shadowRoot: false mode the slot element gets // replaced by injected content if ( (instance as GenericComponentInstance).ce || (instance.parent && isAsyncWrapper(instance.parent) && instance.parent.ce) ) { const el = createElement('slot') renderEffect(() => { setDynamicProps(el, [ slotProps, slotName !== 'default' ? { name: slotName } : {}, ]) }) if (fallback) insert(fallback(), el) fragment.nodes = el return } const slot = getSlot(rawSlots, slotName) if (slot) { // Create and cache bound slot to keep it stable and avoid unnecessary // updates when it resolves to the same slot. Cache per-fragment // (v-for creates multiple fragments) so each fragment keeps its own // slotProps without cross-talk. const boundMap = slot._boundMap || (slot._boundMap = new WeakMap()) let bound = boundMap.get(fragment) if (!bound) { bound = () => { const prevSlotScopeIds = setCurrentSlotScopeIds( slotScopeIds.length > 0 ? slotScopeIds : null, ) const prev = inOnceSlot try { if (once) inOnceSlot = true return slot(slotProps) } finally { inOnceSlot = prev setCurrentSlotScopeIds(prevSlotScopeIds) } } boundMap.set(fragment, bound) } fragment.updateSlot(bound, fallback) } else { fragment.updateSlot(undefined, fallback) } } // dynamic slot name or has dynamicSlots if (!once && (isDynamicName || rawSlots.$)) { renderEffect(renderSlot) } else { renderSlot() } } if (!isHydrating) { if (!noSlotted) { const scopeId = instance.type.__scopeId if (scopeId) { setScopeId(fragment, [`${scopeId}-s`]) } } if (_insertionParent) insert(fragment, _insertionParent, _insertionAnchor) } else { if (fragment.insert) { ;(fragment as VaporFragment).hydrate!() } if (_isLastInsertion) { advanceHydrationNode(_insertionParent!) } } return fragment }