| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305 |
- 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<string, VaporSlot> & {
- $?: DynamicSlotSource[]
- }
- export type StaticSlots = Record<string, VaporSlot>
- 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<RawSlots> = {
- 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<DynamicFragment, VaporSlot> })
- | 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 <slot/> 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
- }
|