Portal.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import { ComponentInternalInstance } from '../component'
  2. import { SuspenseBoundary } from './Suspense'
  3. import {
  4. RendererInternals,
  5. MoveType,
  6. RendererElement,
  7. RendererNode
  8. } from '../renderer'
  9. import { VNode, VNodeArrayChildren, VNodeProps } from '../vnode'
  10. import { isString, ShapeFlags } from '@vue/shared'
  11. import { warn } from '../warning'
  12. export const isPortal = (type: any): boolean => type.__isPortal
  13. export interface PortalProps {
  14. target: string | object
  15. disabled?: boolean
  16. }
  17. export const enum PortalMoveTypes {
  18. TARGET_CHANGE,
  19. TOGGLE, // enable / disable
  20. REORDER // moved in the main view
  21. }
  22. const isDisabled = (props: VNode['props']): boolean =>
  23. props && (props.disabled || props.disabled === '')
  24. const movePortal = (
  25. vnode: VNode,
  26. container: RendererElement,
  27. parentAnchor: RendererNode | null,
  28. { o: { insert }, m: move }: RendererInternals,
  29. moveType: PortalMoveTypes = PortalMoveTypes.REORDER
  30. ) => {
  31. // move target anchor if this is a target change.
  32. if (moveType === PortalMoveTypes.TARGET_CHANGE) {
  33. insert(vnode.targetAnchor!, container, parentAnchor)
  34. }
  35. const { el, anchor, shapeFlag, children, props } = vnode
  36. const isReorder = moveType === PortalMoveTypes.REORDER
  37. // move main view anchor if this is a re-order.
  38. if (isReorder) {
  39. insert(el!, container, parentAnchor)
  40. }
  41. // if this is a re-order and portal is enabled (content is in target)
  42. // do not move children. So the opposite is: only move children if this
  43. // is not a reorder, or the portal is disabled
  44. if (!isReorder || isDisabled(props)) {
  45. // Portal has either Array children or no children.
  46. if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  47. for (let i = 0; i < (children as VNode[]).length; i++) {
  48. move(
  49. (children as VNode[])[i],
  50. container,
  51. parentAnchor,
  52. MoveType.REORDER
  53. )
  54. }
  55. }
  56. }
  57. // move main view anchor if this is a re-order.
  58. if (isReorder) {
  59. insert(anchor!, container, parentAnchor)
  60. }
  61. }
  62. export const PortalImpl = {
  63. __isPortal: true,
  64. process(
  65. n1: VNode | null,
  66. n2: VNode,
  67. container: RendererElement,
  68. anchor: RendererNode | null,
  69. parentComponent: ComponentInternalInstance | null,
  70. parentSuspense: SuspenseBoundary | null,
  71. isSVG: boolean,
  72. optimized: boolean,
  73. internals: RendererInternals
  74. ) {
  75. const {
  76. mc: mountChildren,
  77. pc: patchChildren,
  78. pbc: patchBlockChildren,
  79. o: { insert, querySelector, createText, createComment }
  80. } = internals
  81. const targetSelector = n2.props && n2.props.target
  82. const disabled = isDisabled(n2.props)
  83. const { shapeFlag, children } = n2
  84. if (n1 == null) {
  85. if (__DEV__ && isString(targetSelector) && !querySelector) {
  86. warn(
  87. `Current renderer does not support string target for Portals. ` +
  88. `(missing querySelector renderer option)`
  89. )
  90. }
  91. // insert anchors in the main view
  92. const placeholder = (n2.el = __DEV__
  93. ? createComment('portal start')
  94. : createText(''))
  95. const mainAnchor = (n2.anchor = __DEV__
  96. ? createComment('portal end')
  97. : createText(''))
  98. insert(placeholder, container, anchor)
  99. insert(mainAnchor, container, anchor)
  100. // portal content needs an anchor to support patching multiple portals
  101. // appending to the same target element.
  102. const target = (n2.target = isString(targetSelector)
  103. ? querySelector!(targetSelector)
  104. : targetSelector)
  105. const targetAnchor = (n2.targetAnchor = createText(''))
  106. if (target) {
  107. insert(targetAnchor, target)
  108. } else if (__DEV__) {
  109. warn('Invalid Portal target on mount:', target, `(${typeof target})`)
  110. }
  111. const mount = (container: RendererElement, anchor: RendererNode) => {
  112. // Portal *always* has Array children. This is enforced in both the
  113. // compiler and vnode children normalization.
  114. if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  115. mountChildren(
  116. children as VNodeArrayChildren,
  117. container,
  118. anchor,
  119. parentComponent,
  120. parentSuspense,
  121. isSVG,
  122. optimized
  123. )
  124. }
  125. }
  126. if (disabled) {
  127. mount(container, mainAnchor)
  128. } else if (target) {
  129. mount(target, targetAnchor)
  130. }
  131. } else {
  132. // update content
  133. n2.el = n1.el
  134. const mainAnchor = (n2.anchor = n1.anchor)!
  135. const target = (n2.target = n1.target)!
  136. const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
  137. const wasDisabled = isDisabled(n1.props)
  138. const currentContainer = wasDisabled ? container : target
  139. const currentAnchor = wasDisabled ? mainAnchor : targetAnchor
  140. if (n2.dynamicChildren) {
  141. // fast path when the portal happens to be a block root
  142. patchBlockChildren(
  143. n1.dynamicChildren!,
  144. n2.dynamicChildren,
  145. currentContainer,
  146. parentComponent,
  147. parentSuspense,
  148. isSVG
  149. )
  150. } else if (!optimized) {
  151. patchChildren(
  152. n1,
  153. n2,
  154. currentContainer,
  155. currentAnchor,
  156. parentComponent,
  157. parentSuspense,
  158. isSVG
  159. )
  160. }
  161. if (disabled) {
  162. if (!wasDisabled) {
  163. // enabled -> disabled
  164. // move into main container
  165. movePortal(
  166. n2,
  167. container,
  168. mainAnchor,
  169. internals,
  170. PortalMoveTypes.TOGGLE
  171. )
  172. }
  173. } else {
  174. // target changed
  175. if (targetSelector !== (n1.props && n1.props.target)) {
  176. const nextTarget = (n2.target = isString(targetSelector)
  177. ? querySelector!(targetSelector)
  178. : targetSelector)
  179. if (nextTarget) {
  180. movePortal(
  181. n2,
  182. nextTarget,
  183. null,
  184. internals,
  185. PortalMoveTypes.TARGET_CHANGE
  186. )
  187. } else if (__DEV__) {
  188. warn(
  189. 'Invalid Portal target on update:',
  190. target,
  191. `(${typeof target})`
  192. )
  193. }
  194. } else if (wasDisabled) {
  195. // disabled -> enabled
  196. // move into portal target
  197. movePortal(
  198. n2,
  199. target,
  200. targetAnchor,
  201. internals,
  202. PortalMoveTypes.TOGGLE
  203. )
  204. }
  205. }
  206. }
  207. },
  208. remove(
  209. vnode: VNode,
  210. { r: remove, o: { remove: hostRemove } }: RendererInternals
  211. ) {
  212. const { shapeFlag, children, anchor } = vnode
  213. hostRemove(anchor!)
  214. if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  215. for (let i = 0; i < (children as VNode[]).length; i++) {
  216. remove((children as VNode[])[i])
  217. }
  218. }
  219. },
  220. move: movePortal
  221. }
  222. // Force-casted public typing for h and TSX props inference
  223. export const Portal = (PortalImpl as any) as {
  224. __isPortal: true
  225. new (): { $props: VNodeProps & PortalProps }
  226. }