import { BM, BackendMode, type backend } from './backend' import { Component, convertGenerics, resolvePlaceholder, type GeneralComponent, type GeneralComponentDefinition, } from './component' import { type ComponentSpaceHooks } from './component_space' import { DeepCopyStrategy, getDeepCopyStrategy } from './data_proxy' import { deepCopy, simpleDeepCopy } from './data_utils' import { Element, type DoubleLinkedList } from './element' import { MutationObserverTarget } from './mutation_observer' import { NativeNode } from './native_node' import { type Node } from './node' import { TextNode } from './text_node' import { SHADOW_ROOT_SYMBOL, isElement, isShadowRoot } from './type_symbol' import { VirtualNode } from './virtual_node' import { ThirdError, triggerWarning } from './warning' export const enum SlotMode { Single = 1, Multiple, Dynamic, } export class ShadowRoot extends VirtualNode { /* @internal */ [SHADOW_ROOT_SYMBOL]: true /* @internal */ private _$host: GeneralComponent /* @internal */ private _$hooks: ComponentSpaceHooks /** @internal */ _$backendShadowRoot: backend.ShadowRootContext | null /* @internal */ private _$slotMode: SlotMode /* @internal */ private _$idMap: { [id: string]: Element } | null /* @internal */ private _$singleSlot?: Element | null /* @internal */ private _$slots?: Record /* @internal */ private _$slotsList?: Record> /* @internal */ private _$dynamicSlotsInserted?: boolean /* @internal */ private _$propertyPassingDeepCopy?: DeepCopyStrategy /* @internal */ private _$insertDynamicSlotHandler?: ( slots: { slot: Element name: string slotValues: { [name: string]: unknown } }[], ) => void /* @internal */ private _$removeDynamicSlotHandler?: (slots: Element[]) => void /* @internal */ private _$updateDynamicSlotHandler?: ( slot: Element, slotValues: { [name: string]: unknown }, ) => void /* @internal */ private _$updateDynamicSlotValueHandler?: (slot: Element, name: string) => void /* istanbul ignore next */ constructor() { throw new Error('Element cannot be constructed directly') // eslint-disable-next-line no-unreachable super() } static isShadowRoot = isShadowRoot static createShadowRoot(host: GeneralComponent): ShadowRoot { const node = Object.create(ShadowRoot.prototype) as ShadowRoot let be: backend.ShadowRootContext | null = null if (BM.SHADOW || (BM.DYNAMIC && host.getBackendMode() === BackendMode.Shadow)) { be = (host._$backendElement as backend.Element).getShadowRoot()! } node._$idMap = null let slotMode = SlotMode.Single const hostComponentOptions = host.getComponentOptions() if (hostComponentOptions.multipleSlots) slotMode = SlotMode.Multiple else if (hostComponentOptions.dynamicSlots) slotMode = SlotMode.Dynamic node._$slotMode = slotMode if (slotMode === SlotMode.Single) { node._$singleSlot = null } else if (slotMode === SlotMode.Multiple) { node._$slots = Object.create(null) as Record node._$slotsList = Object.create(null) as Record> } else if (slotMode === SlotMode.Dynamic) { node._$dynamicSlotsInserted = false node._$propertyPassingDeepCopy = getDeepCopyStrategy( hostComponentOptions.propertyPassingDeepCopy, ) node._$insertDynamicSlotHandler = undefined node._$removeDynamicSlotHandler = undefined node._$updateDynamicSlotHandler = undefined node._$updateDynamicSlotValueHandler = undefined } node._$backendShadowRoot = be node._$initialize('shadow', true, be, node, host._$nodeTreeContext) node._$host = host node._$hooks = host.getOwnerSpace().hooks host.shadowRoot = node if (be) { be.__wxElement = node be.associateValue(node) } return node } getHostNode(): GeneralComponent { return this._$host } createTextNode(text = ''): TextNode { return this._$hooks.createTextNode.call(this, (text) => new TextNode(text, this), text) } createNativeNode(tagName: string): NativeNode { return this._$hooks.createNativeNode.call( this, (tagName, stylingName) => NativeNode.create(tagName, this, stylingName), tagName, tagName, ) } createVirtualNode(virtualName = 'virtual'): VirtualNode { return VirtualNode.create(virtualName, this) } createNativeNodeWithInit( tagName: string, stylingName: string, initPropValues?: (comp: NativeNode) => void, ): NativeNode { const ret = this._$hooks.createNativeNode.call( this, (tagName, stylingName) => NativeNode.create(tagName, this, stylingName), tagName, stylingName, ) initPropValues?.(ret) return ret } resolveComponent( tagName: string, usingKey?: string, ): { using: GeneralComponentDefinition | string placeholderHandler: { onReplace(callback: () => void): void; destroy: () => void } | undefined } { const host = this._$host const beh = host._$behavior const hostGenericImpls = host._$genericImpls const space = beh.ownerSpace const compName = usingKey === undefined ? tagName : usingKey const possibleComponentDefinitions = [ // if the target is in using list, then use the one in using list beh._$using[compName], // if the target is in generics list, then use the one hostGenericImpls && hostGenericImpls[compName], ] for (let i = 0; i < possibleComponentDefinitions.length; i += 1) { const cwp = possibleComponentDefinitions[i] if (cwp === null || cwp === undefined) continue if (typeof cwp === 'string') return { using: cwp, placeholderHandler: undefined } if (cwp.final) return { using: cwp.final, placeholderHandler: undefined } if (cwp.placeholder !== null) { const placeholder = resolvePlaceholder(cwp.placeholder, space, cwp.source, hostGenericImpls) const waiting = cwp.waiting || undefined if (waiting) { let placeholderCallback: (() => void) | undefined const actualPlaceholderCallback = () => { placeholderCallback?.() placeholderCallback = undefined } waiting.add(actualPlaceholderCallback) waiting.hintUsed(host) return { using: placeholder, placeholderHandler: { onReplace(callback: () => void) { placeholderCallback = callback }, destroy() { waiting.remove(actualPlaceholderCallback) }, }, } } return { using: placeholder, placeholderHandler: undefined } } } // find in the space otherwise let comp = space.getGlobalUsingComponent(compName) if (comp === null && space._$allowUnusedNativeNode && compName !== '') { comp = compName } if (!comp) { comp = space.getDefaultComponent() /* istanbul ignore if */ if (!comp) { throw new ThirdError(`Cannot find component "${compName}"`, undefined, this._$host) } triggerWarning(`Cannot find component "${compName}", using default component.`, this._$host) } return { using: comp, placeholderHandler: undefined } } /** * Create a component if possible * * Placeholding status should be checked with `checkComponentPlaceholder` . * This function may create a native node if the using target is native node. */ createComponent( tagName: string, usingKey?: string, genericTargets?: { [key: string]: string }, initPropValues?: (comp: GeneralComponent | NativeNode) => void, ): GeneralComponent | NativeNode { const host = this._$host const beh = host._$behavior const { using } = this.resolveComponent(tagName, usingKey) const node = typeof using === 'string' ? this.createNativeNodeWithInit(using, tagName, initPropValues) : this._$hooks.createComponent.call( this, (tagName, compDef) => Component._$advancedCreate( tagName, compDef, this, null, convertGenerics(compDef, beh, host, genericTargets), initPropValues, ), tagName, using, ) return node } createComponentByDef( tagName: string, componentDef: GeneralComponentDefinition, genericTargets?: { [key: string]: string }, initPropValues?: (comp: GeneralComponent | NativeNode) => void, ): GeneralComponent createComponentByDef( tagName: string, componentDef: string, genericTargets?: { [key: string]: string }, initPropValues?: (comp: GeneralComponent | NativeNode) => void, ): NativeNode createComponentByDef( tagName: string, componentDef: GeneralComponentDefinition | string, genericTargets?: { [key: string]: string }, initPropValues?: (comp: GeneralComponent | NativeNode) => void, ): GeneralComponent | NativeNode createComponentByDef( tagName: string, componentDef: GeneralComponentDefinition | string, genericTargets?: { [key: string]: string }, initPropValues?: (comp: GeneralComponent | NativeNode) => void, ): GeneralComponent | NativeNode { const host = this._$host const beh = host._$behavior return typeof componentDef === 'string' ? this.createNativeNodeWithInit(componentDef, tagName, initPropValues) : this._$hooks.createComponent.call( this, (tagName, compDef) => Component._$advancedCreate( tagName, compDef, this, null, convertGenerics(compDef, beh, host, genericTargets), initPropValues, ), tagName, componentDef, ) } getElementById(id: string): Element | undefined { return this._$getIdMap()[id] } /** @internal */ _$markIdCacheDirty() { this._$idMap = null } /** @internal */ _$getIdMap(): { [id: string]: Element } { if (this._$idMap) return this._$idMap const idMap = Element._$generateIdMap(this) this._$idMap = idMap return idMap } /** Get the slot element with the specified name */ getSlotElementFromName(name: string): Element | Element[] | null { const slotMode = this._$slotMode /* istanbul ignore if */ if (slotMode === SlotMode.Single) return this._$singleSlot! if (slotMode === SlotMode.Multiple) { return this._$slots![name] || null } if (slotMode === SlotMode.Dynamic) { const slots: Element[] = [] for (let it = this._$subtreeSlotStart; it; it = it.next) { const slot = it.value const slotName = slot._$slotName || '' if (slotName === name) slots.push(slot) } return slots } return null } /** * Get the slot element for the slot content * * The provided node must be a valid child node of the host of this shadow root. * Otherwise the behavior is undefined. */ getContainingSlot(elem: Node | null): Element | null { const slotMode = this._$slotMode if (slotMode === SlotMode.Single) return this._$singleSlot as Element | null if (slotMode === SlotMode.Dynamic) { if (!elem) return null let subTreeRoot = elem while (subTreeRoot.parentNode?._$inheritSlots) { subTreeRoot = subTreeRoot.parentNode } const slotElement = subTreeRoot._$slotElement // FIXME is ownerShadowRoot check necessary? if (slotElement?.ownerShadowRoot === this) { return slotElement } } if (slotMode === SlotMode.Multiple) { let name: string if (isElement(elem)) { name = elem._$nodeSlot } else { name = '' } return this._$slots![name] || null } return null } /** * Get the elements that should be composed in specified slot * * This method always returns a new array (or null if the specified slot is invalid). * It is convenient but less performant. * For better performance, consider using `forEachNodeInSpecifiedSlot` . */ getSlotContentArray(slot: Element): Node[] | null { if (slot._$slotName === null) return null const ret: Node[] = [] this.forEachNodeInSpecifiedSlot(slot, (node) => { ret.push(node) }) return ret } /** * Iterate slots */ forEachSlot(f: (slot: Element) => boolean | void) { const slotMode = this._$slotMode if (slotMode === SlotMode.Single) { if (this._$singleSlot) f(this._$singleSlot) } else if (slotMode === SlotMode.Multiple) { const slots = Object.values(this._$slots!) for (let i = 0; i < slots.length; i += 1) { if (f(slots[i]!) === false) break } } else if (slotMode === SlotMode.Dynamic) { for (let it = this._$subtreeSlotStart; it; it = it.next) { if (f(it.value) === false) break } } } /** * Iterate through elements ahd their corresponding slots (including slots-inherited nodes) * @param f A function to execute for each element. Return false to break the iteration. * @returns A boolean indicating whether the iteration is complete. */ forEachNodeInSlot(f: (node: Node, slot: Element | null | undefined) => boolean | void): boolean { const childNodes = this._$host.childNodes for (let i = 0; i < childNodes.length; i += 1) { if (!Element.forEachNodeInSlot(childNodes[i]!, f)) return false } return true } /** * Iterate through elements of a specified slot (including slots-inherited nodes) * @param f A function to execute for each element. Return false to break the iteration. * @returns A boolean indicating whether the iteration is complete. */ forEachNodeInSpecifiedSlot(slot: Element | null, f: (node: Node) => boolean | void): boolean { if (slot) { const childNodes = slot.slotNodes! for (let i = 0; i < childNodes.length; i += 1) { const node = childNodes[i]! if (f(node) === false) return false } return true } return this.forEachNodeInSlot((node, slot) => (slot === null ? f(node) : true)) } /** * Iterate through elements ahd their corresponding slots (NOT including slots-inherited nodes) * @param f A function to execute for each element. Return false to break the iteration. * @returns A boolean indicating whether the iteration is complete. */ forEachSlotContentInSlot( f: (node: Node, slot: Element | null | undefined) => boolean | void, ): boolean { const childNodes = this._$host.childNodes for (let i = 0; i < childNodes.length; i += 1) { if (!Element.forEachSlotContentInSlot(childNodes[i]!, f)) return false } return true } /** * Iterate through elements of a specified slot (NOT including slots-inherited nodes) * @param f A function to execute for each element. Return false to break the iteration. * @returns A boolean indicating whether the iteration is complete. */ forEachSlotContentInSpecifiedSlot( slot: Element | null, f: (node: Node) => boolean | void, ): boolean { if (slot) { const childNodes = slot.slotNodes! for (let i = 0; i < childNodes.length; i += 1) { const node = childNodes[i]! if (!node._$inheritSlots && f(node) === false) return false } return true } return this.forEachSlotContentInSlot((node, slot) => (slot === null ? f(node) : true)) } /** * Check whether a node is connected to this shadow root */ isConnected(node: Node): boolean { if (node.ownerShadowRoot !== this) return false for (let p: Node | null = node; p; p = p.parentNode) { if (p === this) return true } return false } /** @internal */ private _$applyMultipleSlotInsertion(slot: Element, slotName: string, _move: boolean): void { const slotsList = this._$slotsList! const slots = this._$slots! // assuming that slot name duplication is very rare in practice, // the complexity without name duplication is priority guaranteed. if (slotsList[slotName]) { // with name duplication const firstSlot = slotsList[slotName]! let insertPos = { next: firstSlot } as DoubleLinkedList // this is a pointer to pointer let insertBeforeFirst = true for (let jt = this._$subtreeSlotStart; jt && insertPos.next; jt = jt.next) { if (jt.value === slot) break if (jt.value === insertPos.next.value) { insertBeforeFirst = false insertPos = insertPos.next } } if (insertBeforeFirst) { slotsList[slotName] = firstSlot.prev = { value: slot, prev: null, next: firstSlot } } else { const next = insertPos.next insertPos.next = { value: slot, prev: insertPos, next } if (next) next.prev = insertPos.next } } else { // without name duplication slotsList[slotName] = { value: slot, prev: null, next: null } } const oldSlot = slots[slotName] const newSlot = slotsList[slotName]!.value if (oldSlot === newSlot) return slots[slotName] = newSlot Element._$insertChildReassignSlot(this, slotName, oldSlot || null, newSlot) } /** @internal */ private _$applyMultipleSlotsRemoval(slot: Element, slotName: string, move: boolean): void { const slotsList = this._$slotsList! const slots = this._$slots! let oldSlot = slotsList[slotName] || null for (; oldSlot; oldSlot = oldSlot.next) { if (oldSlot.value === slot) break } if (!oldSlot) return const prev = oldSlot.prev const next = oldSlot.next if (prev) prev.next = next if (next) next.prev = prev const firstSlotRemoved = !prev if (!firstSlotRemoved) return const newFirstSlot = next if (newFirstSlot) { const oldNameNewSlot = newFirstSlot.value slotsList[slotName] = newFirstSlot // only update slotsList if slots were moved if (!move) { slots[slotName] = oldNameNewSlot Element._$insertChildReassignSlot(this, slotName, slot, oldNameNewSlot) } } else { delete slotsList[slotName] // only update slotsList if slots were moved if (!move) { delete slots[slotName] Element._$insertChildReassignSlot(this, slotName, slot, null) } } } /** @internal */ _$applySlotRename(slot: Element, newName: string, oldName: string): void { const slotMode = this._$slotMode // single slot does not care slot name if (slotMode === SlotMode.Single) return // for multiple slots, reassign slot contents if (slotMode === SlotMode.Multiple) { this._$applyMultipleSlotsRemoval(slot, oldName, false) this._$applyMultipleSlotInsertion(slot, newName, false) return } // for dynamic slotting, destroy and re-insert slot if (slotMode === SlotMode.Dynamic) { // wait until applySlotUpdates if (!this._$dynamicSlotsInserted) return const insertDynamicSlot = this._$insertDynamicSlotHandler const removeDynamicSlot = this._$removeDynamicSlotHandler removeDynamicSlot?.([slot]) insertDynamicSlot?.([{ slot, name: newName, slotValues: slot._$slotValues! }]) } } /** @internal */ _$applySlotsInsertion( slotStart: DoubleLinkedList, slotEnd: DoubleLinkedList, move: boolean, ): void { const slotMode = this._$slotMode if (slotMode === SlotMode.Single) { const oldSlot = this._$singleSlot as Element | null const newSlot = this._$subtreeSlotStart!.value if (oldSlot === newSlot) return this._$singleSlot = newSlot Element._$insertChildReassignSlot(this, null, oldSlot, newSlot) return } if (slotMode === SlotMode.Multiple) { for ( let it: DoubleLinkedList | null = slotStart; it && it !== slotEnd.next; it = it.next ) { const slot = it.value const slotName = slot._$slotName! this._$applyMultipleSlotInsertion(slot, slotName, move) } return } if (slotMode === SlotMode.Dynamic) { // dynamic-slotting should do nothing about slot movement if (move) return // wait until applySlotUpdates if (!this._$dynamicSlotsInserted) return const insertDynamicSlot = this._$insertDynamicSlotHandler const slots: { slot: Element; name: string; slotValues: { [name: string]: unknown } }[] = [] for ( let it: DoubleLinkedList | null = slotStart; it && it !== slotEnd.next; it = it.next ) { const slot = it.value const slotName = slot._$slotName! slots.push({ slot, name: slotName, slotValues: slot._$slotValues! }) } insertDynamicSlot?.(slots) } } /** @internal */ _$applySlotsRemoval( slotStart: DoubleLinkedList, slotEnd: DoubleLinkedList, move: boolean, ): void { const slotMode = this._$slotMode if (slotMode === SlotMode.Single) { // will call applySLotsInsertion after if slots were moved // no need to do anything here if (move) return const oldSlot = this._$singleSlot as Element | null const newSlot = this._$subtreeSlotStart?.value || null if (oldSlot === newSlot) return this._$singleSlot = newSlot Element._$insertChildReassignSlot(this, null, oldSlot, newSlot) return } if (slotMode === SlotMode.Multiple) { for ( let it: DoubleLinkedList | null = slotStart; it && it !== slotEnd.next; it = it.next ) { const slot = it.value const slotName = slot._$slotName! this._$applyMultipleSlotsRemoval(slot, slotName, move) } return } if (slotMode === SlotMode.Dynamic) { // dynamic-slotting should do nothing about slot movement if (move) return // wait until applySlotUpdates if (!this._$dynamicSlotsInserted) return const removeDynamicSlot = this._$removeDynamicSlotHandler const slots: Element[] = [] for ( let it: DoubleLinkedList | null = slotStart; it && it !== slotEnd.next; it = it.next ) { const slot = it.value slots.push(slot) } removeDynamicSlot?.(slots) } } /** * Set the dynamic slot handlers * * If the handlers have not been set yet, * the `insertSlotHandler` will be called for each slot that has been added to the shadow tree, * otherwise call `updateSlotHandler` for each slots. */ setDynamicSlotHandler( insertSlotHandler: ( slots: { slot: Element name: string slotValues: { [name: string]: unknown } }[], ) => void, removeSlotHandler: (slots: Element[]) => void, updateSlotHandler: (slot: Element, slotValues: { [name: string]: unknown }) => void, updateSlotValueHandler: (slot: Element, name: string) => void, ) { if (this._$slotMode !== SlotMode.Dynamic) return this._$insertDynamicSlotHandler = insertSlotHandler this._$removeDynamicSlotHandler = removeSlotHandler this._$updateDynamicSlotHandler = updateSlotHandler this._$updateDynamicSlotValueHandler = updateSlotValueHandler } /** * Use the same dynamic slot handlers with the `source` */ useDynamicSlotHandlerFrom(source: ShadowRoot) { if (source._$insertDynamicSlotHandler) { this.setDynamicSlotHandler( source._$insertDynamicSlotHandler, source._$removeDynamicSlotHandler!, source._$updateDynamicSlotHandler!, source._$updateDynamicSlotValueHandler!, ) } } /** * Update a slot value * * The updated value should be applied with `applySlotValueUpdates` call. */ replaceSlotValue(slot: Element, name: string, value: unknown): void { const slotValues = slot._$slotValues if (!slotValues) return const oldValue = slotValues[name] let newValue = value if (this._$propertyPassingDeepCopy !== DeepCopyStrategy.None) { if (this._$propertyPassingDeepCopy === DeepCopyStrategy.SimpleWithRecursion) { newValue = deepCopy(value, true) } else { newValue = simpleDeepCopy(value) } } if (oldValue === newValue) return slotValues[name] = newValue if (slot._$mutationObserverTarget) { MutationObserverTarget.callAttrObservers(slot, { type: 'properties', target: slot, nameType: 'slot-value', propertyName: name, }) } this._$updateDynamicSlotValueHandler?.(slot, name) } /** * Apply slot value updates */ applySlotValueUpdates(slot: Element) { this._$updateDynamicSlotHandler?.(slot, slot._$slotValues!) } applySlotUpdates(): void { if (!this._$dynamicSlotsInserted) { this._$dynamicSlotsInserted = true const insertDynamicSlot = this._$insertDynamicSlotHandler const slots: { slot: Element; name: string; slotValues: { [name: string]: unknown } }[] = [] for (let it = this._$subtreeSlotStart; it; it = it.next) { const slot = it.value const slotName = slot._$slotName! slots.push({ slot, name: slotName, slotValues: slot._$slotValues! }) } if (slots.length) insertDynamicSlot?.(slots) } else { for (let it = this._$subtreeSlotStart; it; it = it.next) { const slot = it.value this._$updateDynamicSlotHandler?.(slot, slot._$slotValues!) } } } /** * Get slot mode */ getSlotMode(): SlotMode { return this._$slotMode } } ShadowRoot.prototype[SHADOW_ROOT_SYMBOL] = true