Teleport.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. import { ComponentInternalInstance } from '../component'
  2. import { SuspenseBoundary } from './Suspense'
  3. import {
  4. RendererInternals,
  5. MoveType,
  6. RendererElement,
  7. RendererNode,
  8. RendererOptions,
  9. traverseStaticChildren
  10. } from '../renderer'
  11. import { VNode, VNodeArrayChildren, VNodeProps } from '../vnode'
  12. import { isString, ShapeFlags } from '@vue/shared'
  13. import { warn } from '../warning'
  14. import { isHmrUpdating } from '../hmr'
  15. export type TeleportVNode = VNode<RendererNode, RendererElement, TeleportProps>
  16. export interface TeleportProps {
  17. to: string | RendererElement | null | undefined
  18. disabled?: boolean
  19. }
  20. export const isTeleport = (type: any): boolean => type.__isTeleport
  21. const isTeleportDisabled = (props: VNode['props']): boolean =>
  22. props && (props.disabled || props.disabled === '')
  23. const isTargetSVG = (target: RendererElement): boolean =>
  24. typeof SVGElement !== 'undefined' && target instanceof SVGElement
  25. const resolveTarget = <T = RendererElement>(
  26. props: TeleportProps | null,
  27. select: RendererOptions['querySelector']
  28. ): T | null => {
  29. const targetSelector = props && props.to
  30. if (isString(targetSelector)) {
  31. if (!select) {
  32. __DEV__ &&
  33. warn(
  34. `Current renderer does not support string target for Teleports. ` +
  35. `(missing querySelector renderer option)`
  36. )
  37. return null
  38. } else {
  39. const target = select(targetSelector)
  40. if (!target) {
  41. __DEV__ &&
  42. warn(
  43. `Failed to locate Teleport target with selector "${targetSelector}". ` +
  44. `Note the target element must exist before the component is mounted - ` +
  45. `i.e. the target cannot be rendered by the component itself, and ` +
  46. `ideally should be outside of the entire Vue component tree.`
  47. )
  48. }
  49. return target as T
  50. }
  51. } else {
  52. if (__DEV__ && !targetSelector && !isTeleportDisabled(props)) {
  53. warn(`Invalid Teleport target: ${targetSelector}`)
  54. }
  55. return targetSelector as T
  56. }
  57. }
  58. export const TeleportImpl = {
  59. __isTeleport: true,
  60. process(
  61. n1: TeleportVNode | null,
  62. n2: TeleportVNode,
  63. container: RendererElement,
  64. anchor: RendererNode | null,
  65. parentComponent: ComponentInternalInstance | null,
  66. parentSuspense: SuspenseBoundary | null,
  67. isSVG: boolean,
  68. slotScopeIds: string[] | null,
  69. optimized: boolean,
  70. internals: RendererInternals
  71. ) {
  72. const {
  73. mc: mountChildren,
  74. pc: patchChildren,
  75. pbc: patchBlockChildren,
  76. o: { insert, querySelector, createText, createComment }
  77. } = internals
  78. const disabled = isTeleportDisabled(n2.props)
  79. let { shapeFlag, children, dynamicChildren } = n2
  80. // #3302
  81. // HMR updated, force full diff
  82. if (__DEV__ && isHmrUpdating) {
  83. optimized = false
  84. dynamicChildren = null
  85. }
  86. if (n1 == null) {
  87. // insert anchors in the main view
  88. const placeholder = (n2.el = __DEV__
  89. ? createComment('teleport start')
  90. : createText(''))
  91. const mainAnchor = (n2.anchor = __DEV__
  92. ? createComment('teleport end')
  93. : createText(''))
  94. insert(placeholder, container, anchor)
  95. insert(mainAnchor, container, anchor)
  96. const target = (n2.target = resolveTarget(n2.props, querySelector))
  97. const targetAnchor = (n2.targetAnchor = createText(''))
  98. if (target) {
  99. insert(targetAnchor, target)
  100. // #2652 we could be teleporting from a non-SVG tree into an SVG tree
  101. isSVG = isSVG || isTargetSVG(target)
  102. } else if (__DEV__ && !disabled) {
  103. warn('Invalid Teleport target on mount:', target, `(${typeof target})`)
  104. }
  105. const mount = (container: RendererElement, anchor: RendererNode) => {
  106. // Teleport *always* has Array children. This is enforced in both the
  107. // compiler and vnode children normalization.
  108. if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  109. mountChildren(
  110. children as VNodeArrayChildren,
  111. container,
  112. anchor,
  113. parentComponent,
  114. parentSuspense,
  115. isSVG,
  116. slotScopeIds,
  117. optimized
  118. )
  119. }
  120. }
  121. if (disabled) {
  122. mount(container, mainAnchor)
  123. } else if (target) {
  124. mount(target, targetAnchor)
  125. }
  126. } else {
  127. // update content
  128. n2.el = n1.el
  129. const mainAnchor = (n2.anchor = n1.anchor)!
  130. const target = (n2.target = n1.target)!
  131. const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
  132. const wasDisabled = isTeleportDisabled(n1.props)
  133. const currentContainer = wasDisabled ? container : target
  134. const currentAnchor = wasDisabled ? mainAnchor : targetAnchor
  135. isSVG = isSVG || isTargetSVG(target)
  136. if (dynamicChildren) {
  137. // fast path when the teleport happens to be a block root
  138. patchBlockChildren(
  139. n1.dynamicChildren!,
  140. dynamicChildren,
  141. currentContainer,
  142. parentComponent,
  143. parentSuspense,
  144. isSVG,
  145. slotScopeIds
  146. )
  147. // even in block tree mode we need to make sure all root-level nodes
  148. // in the teleport inherit previous DOM references so that they can
  149. // be moved in future patches.
  150. traverseStaticChildren(n1, n2, true)
  151. } else if (!optimized) {
  152. patchChildren(
  153. n1,
  154. n2,
  155. currentContainer,
  156. currentAnchor,
  157. parentComponent,
  158. parentSuspense,
  159. isSVG,
  160. slotScopeIds,
  161. false
  162. )
  163. }
  164. if (disabled) {
  165. if (!wasDisabled) {
  166. // enabled -> disabled
  167. // move into main container
  168. moveTeleport(
  169. n2,
  170. container,
  171. mainAnchor,
  172. internals,
  173. TeleportMoveTypes.TOGGLE
  174. )
  175. }
  176. } else {
  177. // target changed
  178. if ((n2.props && n2.props.to) !== (n1.props && n1.props.to)) {
  179. const nextTarget = (n2.target = resolveTarget(
  180. n2.props,
  181. querySelector
  182. ))
  183. if (nextTarget) {
  184. moveTeleport(
  185. n2,
  186. nextTarget,
  187. null,
  188. internals,
  189. TeleportMoveTypes.TARGET_CHANGE
  190. )
  191. } else if (__DEV__) {
  192. warn(
  193. 'Invalid Teleport target on update:',
  194. target,
  195. `(${typeof target})`
  196. )
  197. }
  198. } else if (wasDisabled) {
  199. // disabled -> enabled
  200. // move into teleport target
  201. moveTeleport(
  202. n2,
  203. target,
  204. targetAnchor,
  205. internals,
  206. TeleportMoveTypes.TOGGLE
  207. )
  208. }
  209. }
  210. }
  211. updateCssVars(n2)
  212. },
  213. remove(
  214. vnode: VNode,
  215. parentComponent: ComponentInternalInstance | null,
  216. parentSuspense: SuspenseBoundary | null,
  217. optimized: boolean,
  218. { um: unmount, o: { remove: hostRemove } }: RendererInternals,
  219. doRemove: Boolean
  220. ) {
  221. const { shapeFlag, children, anchor, targetAnchor, target, props } = vnode
  222. if (target) {
  223. hostRemove(targetAnchor!)
  224. }
  225. // an unmounted teleport should always remove its children if not disabled
  226. if (doRemove || !isTeleportDisabled(props)) {
  227. hostRemove(anchor!)
  228. if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  229. for (let i = 0; i < (children as VNode[]).length; i++) {
  230. const child = (children as VNode[])[i]
  231. unmount(
  232. child,
  233. parentComponent,
  234. parentSuspense,
  235. true,
  236. !!child.dynamicChildren
  237. )
  238. }
  239. }
  240. }
  241. },
  242. move: moveTeleport,
  243. hydrate: hydrateTeleport
  244. }
  245. export const enum TeleportMoveTypes {
  246. TARGET_CHANGE,
  247. TOGGLE, // enable / disable
  248. REORDER // moved in the main view
  249. }
  250. function moveTeleport(
  251. vnode: VNode,
  252. container: RendererElement,
  253. parentAnchor: RendererNode | null,
  254. { o: { insert }, m: move }: RendererInternals,
  255. moveType: TeleportMoveTypes = TeleportMoveTypes.REORDER
  256. ) {
  257. // move target anchor if this is a target change.
  258. if (moveType === TeleportMoveTypes.TARGET_CHANGE) {
  259. insert(vnode.targetAnchor!, container, parentAnchor)
  260. }
  261. const { el, anchor, shapeFlag, children, props } = vnode
  262. const isReorder = moveType === TeleportMoveTypes.REORDER
  263. // move main view anchor if this is a re-order.
  264. if (isReorder) {
  265. insert(el!, container, parentAnchor)
  266. }
  267. // if this is a re-order and teleport is enabled (content is in target)
  268. // do not move children. So the opposite is: only move children if this
  269. // is not a reorder, or the teleport is disabled
  270. if (!isReorder || isTeleportDisabled(props)) {
  271. // Teleport has either Array children or no children.
  272. if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  273. for (let i = 0; i < (children as VNode[]).length; i++) {
  274. move(
  275. (children as VNode[])[i],
  276. container,
  277. parentAnchor,
  278. MoveType.REORDER
  279. )
  280. }
  281. }
  282. }
  283. // move main view anchor if this is a re-order.
  284. if (isReorder) {
  285. insert(anchor!, container, parentAnchor)
  286. }
  287. }
  288. interface TeleportTargetElement extends Element {
  289. // last teleport target
  290. _lpa?: Node | null
  291. }
  292. function hydrateTeleport(
  293. node: Node,
  294. vnode: TeleportVNode,
  295. parentComponent: ComponentInternalInstance | null,
  296. parentSuspense: SuspenseBoundary | null,
  297. slotScopeIds: string[] | null,
  298. optimized: boolean,
  299. {
  300. o: { nextSibling, parentNode, querySelector }
  301. }: RendererInternals<Node, Element>,
  302. hydrateChildren: (
  303. node: Node | null,
  304. vnode: VNode,
  305. container: Element,
  306. parentComponent: ComponentInternalInstance | null,
  307. parentSuspense: SuspenseBoundary | null,
  308. slotScopeIds: string[] | null,
  309. optimized: boolean
  310. ) => Node | null
  311. ): Node | null {
  312. const target = (vnode.target = resolveTarget<Element>(
  313. vnode.props,
  314. querySelector
  315. ))
  316. if (target) {
  317. // if multiple teleports rendered to the same target element, we need to
  318. // pick up from where the last teleport finished instead of the first node
  319. const targetNode =
  320. (target as TeleportTargetElement)._lpa || target.firstChild
  321. if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  322. if (isTeleportDisabled(vnode.props)) {
  323. vnode.anchor = hydrateChildren(
  324. nextSibling(node),
  325. vnode,
  326. parentNode(node)!,
  327. parentComponent,
  328. parentSuspense,
  329. slotScopeIds,
  330. optimized
  331. )
  332. vnode.targetAnchor = targetNode
  333. } else {
  334. vnode.anchor = nextSibling(node)
  335. // lookahead until we find the target anchor
  336. // we cannot rely on return value of hydrateChildren() because there
  337. // could be nested teleports
  338. let targetAnchor = targetNode
  339. while (targetAnchor) {
  340. targetAnchor = nextSibling(targetAnchor)
  341. if (
  342. targetAnchor &&
  343. targetAnchor.nodeType === 8 &&
  344. (targetAnchor as Comment).data === 'teleport anchor'
  345. ) {
  346. vnode.targetAnchor = targetAnchor
  347. ;(target as TeleportTargetElement)._lpa =
  348. vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
  349. break
  350. }
  351. }
  352. hydrateChildren(
  353. targetNode,
  354. vnode,
  355. target,
  356. parentComponent,
  357. parentSuspense,
  358. slotScopeIds,
  359. optimized
  360. )
  361. }
  362. }
  363. updateCssVars(vnode)
  364. }
  365. return vnode.anchor && nextSibling(vnode.anchor as Node)
  366. }
  367. // Force-casted public typing for h and TSX props inference
  368. export const Teleport = TeleportImpl as unknown as {
  369. __isTeleport: true
  370. new (): { $props: VNodeProps & TeleportProps }
  371. }
  372. function updateCssVars(vnode: VNode) {
  373. // presence of .ut method indicates owner component uses css vars.
  374. // code path here can assume browser environment.
  375. const ctx = vnode.ctx
  376. if (ctx && ctx.ut) {
  377. let node = (vnode.children as VNode[])[0].el!
  378. while (node !== vnode.targetAnchor) {
  379. if (node.nodeType === 1) node.setAttribute('data-v-owner', ctx.uid)
  380. node = node.nextSibling
  381. }
  382. ctx.ut()
  383. }
  384. }