11<template >
2- <div
3- v-auto-overflow-scroll =" open && overflowScroll"
4- class =" context"
5- :class =" {
6- 'visibility-hidden': !open || !updatedOnce,
7- 'context--overflow-scroll': overflowScroll,
8- }"
9- >
10- <slot v-if =" openedOnce" ></slot >
11- </div >
2+ <Teleport to =" body" >
3+ <div
4+ ref =" contextEl"
5+ v-auto-overflow-scroll =" open && overflowScroll"
6+ v-bind =" $attrs"
7+ class =" context"
8+ :class =" {
9+ 'visibility-hidden': !open || !updatedOnce,
10+ 'context--overflow-scroll': overflowScroll,
11+ }"
12+ >
13+ <slot v-if =" openedOnce" ></slot >
14+ </div >
15+ </Teleport >
1216</template >
1317
1418<script >
1519import {
16- isElement ,
20+ collectTeleportRootsForOutsideClick ,
21+ getTeleportedElementFromRef ,
1722 isDomElement ,
23+ isElement ,
1824 onClickOutside ,
1925} from ' @baserow/modules/core/utils/dom'
2026
21- import MoveToBody from ' @baserow/modules/core/mixins/moveToBody'
22-
2327export default {
2428 name: ' Context' ,
25- mixins: [MoveToBody],
29+ provide () {
30+ return {
31+ registerChild: this .registerChild ,
32+ unregisterChild: this .unregisterChild ,
33+ }
34+ },
35+ inject: {
36+ parentRegisterChild: {
37+ from: ' registerChild' ,
38+ default: null ,
39+ },
40+ parentUnregisterChild: {
41+ from: ' unregisterChild' ,
42+ default: null ,
43+ },
44+ },
45+ inheritAttrs: false ,
2646 props: {
2747 hideOnClickOutside: {
2848 type: Boolean ,
@@ -69,7 +89,19 @@ export default {
6989 // If opened once, should stay in DOM to keep nested content
7090 openedOnce: false ,
7191 maxHeightOffset: 10 ,
92+ children: [],
93+ }
94+ },
95+ mounted () {
96+ if (this .parentRegisterChild ) {
97+ this .parentRegisterChild (this )
98+ }
99+ },
100+ beforeUnmount () {
101+ if (this .parentUnregisterChild ) {
102+ this .parentUnregisterChild (this )
72103 }
104+ this .hide (false )
73105 },
74106 methods: {
75107 /**
@@ -131,6 +163,7 @@ export default {
131163 ) {
132164 const isElementOrigin = isDomElement (target)
133165 const updatePosition = () => {
166+ const el = this .$refs .contextEl
134167 const css = isElementOrigin
135168 ? this .calculatePositionElement (
136169 target,
@@ -160,11 +193,10 @@ export default {
160193 return
161194 }
162195
163- // Set the calculated positions of the context.
164196 for (const key in css) {
165197 const cssValue =
166198 css[key] !== null ? Math .ceil (css[key]) + ' px' : ' auto'
167- this . $ el .style [key] = cssValue
199+ el .style [key] = cssValue
168200 }
169201
170202 // The max height can optionally be automatically to prevent the context from
@@ -178,7 +210,7 @@ export default {
178210 this .getWindowScrollHeight ()
179211 } px)`
180212 : ' none'
181- this . $ el .style [' max-height' ] = maxHeight
213+ el .style [' max-height' ] = maxHeight
182214 }
183215
184216 this .updatedOnce = true
@@ -195,54 +227,62 @@ export default {
195227 await this .$nextTick ()
196228 updatePosition ()
197229
198- this .$el .cancelOnClickOutside = onClickOutside (this .$el , (target ) => {
199- if (
200- this .open &&
201- // If the prop allows it to be closed by clicking outside.
202- this .hideOnClickOutside &&
203- // If the click was not on the opener because they can trigger the toggle
204- // method.
205- ! isElement (this .opener , target) &&
206- // If the click was not inside one of the context children of this context
207- // menu.
208- ! this .moveToBody .children .some ((child ) => {
209- return isElement (child .$el , target)
210- })
211- ) {
212- this .hide ()
230+ // Cancel any previous handlers before setting up new ones
231+ this ._cleanupEventHandlers ()
232+
233+ const el = this .getTeleportedElement ()
234+
235+ const ignoreElements = () => {
236+ const childRoots = []
237+ for (const child of this .children ) {
238+ childRoots .push (... collectTeleportRootsForOutsideClick (child))
239+ }
240+ return [
241+ this .opener ,
242+ ... new Set (childRoots .filter ((el ) => el instanceof HTMLElement )),
243+ ].filter (Boolean )
244+ }
245+
246+ this ._cancelOnClickOutside = onClickOutside (
247+ el,
248+ () => {
249+ if (this .open && this .hideOnClickOutside ) {
250+ this .hide ()
251+ }
252+ },
253+ {
254+ ignoreElements,
213255 }
214- } )
256+ )
215257
216- this .$el . updatePositionViaScrollEvent = (event ) => {
258+ this ._updatePositionViaScrollEvent = (event ) => {
217259 if (this .hideOnScroll ) {
218260 this .hide ()
219261 } else if (
220- // The context menu itself can have a scrollbar, and resizing everytime you
221- // scroll internally doesn't make sense because it can't influence the position.
222- ! isElement (this .$el , event .target ) &&
223- // If the scroll was not inside one of the context children of this context
224- // menu.
225- ! this .moveToBody .children .some ((child ) => {
226- return isElement (child .$el , target)
227- })
262+ ! isElement (this .getTeleportedElement (), event .target ) &&
263+ ! this .children .some ((child ) =>
264+ collectTeleportRootsForOutsideClick (child).some ((root ) =>
265+ isElement (root, event .target )
266+ )
267+ )
228268 ) {
229269 updatePosition ()
230270 }
231271 }
232272 window .addEventListener (
233273 ' scroll' ,
234- this .$el . updatePositionViaScrollEvent ,
274+ this ._updatePositionViaScrollEvent ,
235275 true
236276 )
237277
238- this .$el . updatePositionViaResizeEvent = () => {
278+ this ._updatePositionViaResizeEvent = () => {
239279 if (this .hideOnResize ) {
240280 this .hide ()
241281 } else {
242282 updatePosition ()
243283 }
244284 }
245- window .addEventListener (' resize' , this .$el . updatePositionViaResizeEvent )
285+ window .addEventListener (' resize' , this ._updatePositionViaResizeEvent )
246286
247287 this .$emit (' shown' )
248288 },
@@ -309,22 +349,25 @@ export default {
309349 this .$emit (' hidden' )
310350 }
311351
312- // If the context menu was never opened, it doesn't have the
313- // `cancelOnClickOutside`, so we can't call it.
314- if (
315- Object .prototype .hasOwnProperty .call (this .$el , ' cancelOnClickOutside' )
316- ) {
317- this .$el .cancelOnClickOutside ()
352+ this ._cleanupEventHandlers ()
353+ },
354+ _cleanupEventHandlers () {
355+ if (this ._cancelOnClickOutside ) {
356+ this ._cancelOnClickOutside ()
357+ this ._cancelOnClickOutside = null
358+ }
359+ if (this ._updatePositionViaScrollEvent ) {
360+ window .removeEventListener (
361+ ' scroll' ,
362+ this ._updatePositionViaScrollEvent ,
363+ true
364+ )
365+ this ._updatePositionViaScrollEvent = null
366+ }
367+ if (this ._updatePositionViaResizeEvent ) {
368+ window .removeEventListener (' resize' , this ._updatePositionViaResizeEvent )
369+ this ._updatePositionViaResizeEvent = null
318370 }
319- window .removeEventListener (
320- ' scroll' ,
321- this .$el .updatePositionViaScrollEvent ,
322- true
323- )
324- window .removeEventListener (
325- ' resize' ,
326- this .$el .updatePositionViaResizeEvent
327- )
328371 },
329372 /**
330373 * Calculates the absolute position of the context based on the original clicked
@@ -483,11 +526,9 @@ export default {
483526 verticalOffset,
484527 horizontalOffset
485528 ) {
486- const contextRect = this .$el .getBoundingClientRect ()
487- // We need to use the scrollHeight in the calculations because we need to work
488- // with the full height of the element without scrollbar to calculate the optimal
489- // position.
490- const scrollHeight = this .$el .scrollHeight
529+ const el = this .$refs .contextEl
530+ const contextRect = el .getBoundingClientRect ()
531+ const scrollHeight = el .scrollHeight
491532 const canTop =
492533 targetRect .top -
493534 scrollHeight -
@@ -543,6 +584,23 @@ export default {
543584 isOpen () {
544585 return this .open
545586 },
587+ /**
588+ * Root element rendered inside `<Teleport>` (not the anchor `this.$el`).
589+ *
590+ * @returns {HTMLElement|null}
591+ */
592+ getTeleportedElement () {
593+ return getTeleportedElementFromRef (this .$refs , ' contextEl' )
594+ },
595+ registerChild (child ) {
596+ this .children .push (child)
597+ },
598+ unregisterChild (child ) {
599+ const index = this .children .indexOf (child)
600+ if (index !== - 1 ) {
601+ this .children .splice (index, 1 )
602+ }
603+ },
546604 },
547605}
548606< / script>
0 commit comments