@@ -44,26 +44,29 @@ import { onMounted, type PropType, ref, useAttrs, watch , type HTMLAttributes }
4444import { getFallbackId , type LinkableByIdProps ,type TailwindClassProp } from " ../shared/props.js"
4545
4646import { twMerge } from " ../../helpers/twMerge.js"
47- import { castType } from " @alanscodelog/utils"
47+ import { castType } from " @alanscodelog/utils/castType.js"
48+ import { isArray } from " @alanscodelog/utils/isArray.js"
49+ import type { IPopupReference , PopupPosition , PopupPositioner , PopupPositionModifier , PopupSpaceInfo } from " ../../types.js"
4850
4951const fallbackId = getFallbackId ()
5052// eslint-disable-next-line no-use-before-define
5153const props = withDefaults (defineProps <Props >(), {
5254 useBackdrop: true ,
53- preferredHorizontal : () => [" center" , " right" , " left" , " either" ],
54- preferredVertical : () => [" top" , " bottom" , " either" ],
55+ // vue is getting confused when the prop type can also be a function
56+ preferredHorizontal : () => [" center" , " right" , " left" , " either" ] as any as [" center" , " right" , " left" , " either" ],
57+ preferredVertical : () => [" top" , " bottom" , " either" ] as any as [" top" , " bottom" , " either" ],
5558 onlyShiftIfOpen: false ,
5659})
5760const $attrs = useAttrs ()
5861defineOptions ({ name: " lib-popup" })
5962
60- type ElementLike = { getBoundingClientRect : () => DOMRect }
63+
6164// todo, can we have transitions?
6265const dialogEl = ref <HTMLDialogElement | null >(null )
63- const popupEl = ref <ElementLike | null >(null )
64- const buttonEl = ref <ElementLike | null >(null )
66+ const popupEl = ref <IPopupReference | null >(null )
67+ const buttonEl = ref <IPopupReference | null >(null )
6568
66- const pos = ref <{ x : number , y : number , maxWidth ? : number , maxHeight ? : number } >({} as any )
69+ const pos = ref <PopupPosition >({} as any )
6770const modelValue = defineModel <boolean >({ default: false })
6871let isOpen = false
6972
@@ -87,20 +90,29 @@ const getVeilBoundingRect = (el: HTMLElement): Omit<DOMRect, "toJSON"> => {
8790 right: 0 ,
8891 }
8992}
90- let lastButtonElPos: DOMRect | undefined
93+ let lastButtonElPos: ReturnType < IPopupReference [ " getBoundingClientRect " ]> | undefined
9194const recompute = (force : boolean = false ): void => {
9295 requestAnimationFrame (() => {
93- const allAreCenterScreen = props .preferredHorizontal [0 ] === " center-screen" && props .preferredVertical [0 ] === " center-screen"
94- if ((! buttonEl .value && ! allAreCenterScreen ) || ! popupEl .value || ! dialogEl .value ) {
96+ const horzHasCenterScreen = isArray (props .preferredHorizontal )
97+ && props .preferredHorizontal [0 ] === " center-screen"
98+ const vertHasCenterScreen = isArray (props .preferredVertical )
99+ && props .preferredVertical [0 ] === " center-screen"
100+
101+ const canBePositionedWithoutButton =
102+ (horzHasCenterScreen || typeof props .preferredHorizontal === " function" )
103+ && (vertHasCenterScreen || typeof props .preferredVertical === " function" )
104+
105+ if (! popupEl .value || ! dialogEl .value || (! buttonEl .value && ! canBePositionedWithoutButton )) {
95106 pos .value = {} as any
96107 return
97108 }
98- const finalPos: { x: number , y: number , maxWidth? : number , maxHeight? : number } = {} as any
99-
100109 const el = buttonEl .value ?.getBoundingClientRect ()
101110 const veil = getVeilBoundingRect (props .useBackdrop ? dialogEl .value : document .body )
102111 const popup = popupEl .value .getBoundingClientRect ()
103112
113+ let finalPos: { x: number , y: number , maxWidth? : number , maxHeight? : number } = {} as any
114+
115+
104116 if (! force && modelValue .value && props .onlyShiftIfOpen && buttonEl .value && lastButtonElPos ) {
105117 const shiftX = buttonEl .value .getBoundingClientRect ().x - lastButtonElPos .x
106118 const shiftY = buttonEl .value .getBoundingClientRect ().y - lastButtonElPos .y
@@ -138,146 +150,157 @@ const recompute = (force: boolean = false): void => {
138150 const { preferredHorizontal, preferredVertical } = props
139151 let maxWidth: number | undefined
140152 let maxHeight: number | undefined
141- /* eslint-disable no-labels */
142- outerloop :
143- for (const type of preferredHorizontal ) {
144- switch (type ) {
145- case " center-screen" :
146- if (popup .width < veil .width ) {
147- finalPos .x = (veil .width / 2 ) - (popup .width / 2 )
148- } else {
149- finalPos .x = 0
150- maxWidth = finalPos .x
151- }
152- break
153- case " center-most" :
154- case " center" :
155- castType <DOMRect >(el )
156- if (space .leftFromCenter >= (popup .width / 2 ) &&
153+ if (typeof preferredHorizontal === " function" ) {
154+ finalPos .x = preferredHorizontal (el , popup , veil , space )
155+ } else {
156+ /* eslint-disable no-labels */
157+ outerloop :
158+ for (const type of preferredHorizontal ) {
159+ switch (type ) {
160+ case " center-screen" :
161+ if (popup .width < veil .width ) {
162+ finalPos .x = (veil .width / 2 ) - (popup .width / 2 )
163+ } else {
164+ finalPos .x = 0
165+ maxWidth = finalPos .x
166+ }
167+ break
168+ case " center-most" :
169+ case " center" :
170+ castType <DOMRect >(el )
171+ if (space .leftFromCenter >= (popup .width / 2 ) &&
157172 space .rightFromCenter >= (popup .width / 2 )) {
158- finalPos .x = el .x + (el .width / 2 ) - (popup .width / 2 )
159- break outerloop
160- }
161- // todo temp fix when it's too wide, will prefer left
162- if (((space .rightFromCenter + space .leftFromCenter ) <= popup .width )) {
163- finalPos .x = 0
164- break outerloop
165- }
166- if (type === " center-most" ) {
167- if (space .leftFromCenter < space .rightFromCenter ) {
168- finalPos .x = el .x + (el .width / 2 ) - space .leftFromCenter ; break outerloop
173+ finalPos .x = el .x + (el .width / 2 ) - (popup .width / 2 )
174+ break outerloop
175+ }
176+ // todo temp fix when it's too wide, will prefer left
177+ if (((space .rightFromCenter + space .leftFromCenter ) <= popup .width )) {
178+ finalPos .x = 0
179+ break outerloop
180+ }
181+ if (type === " center-most" ) {
182+ if (space .leftFromCenter < space .rightFromCenter ) {
183+ finalPos .x = el .x + (el .width / 2 ) - space .leftFromCenter ; break outerloop
184+ } else {
185+ finalPos .x = el .x + (el .width / 2 ) + space .rightFromCenter - popup .width ; break outerloop
186+ }
187+ }
188+ break
189+ case " left-most" :
190+ castType <DOMRect >(el )
191+ if (space .left >= popup .width ) {
192+ finalPos .x = el .x - popup .width ; break outerloop
169193 } else {
170- finalPos .x = el .x + (el .width / 2 ) + space .rightFromCenter - popup .width ; break outerloop
194+ finalPos .x = 0 ; break outerloop
195+ }
196+ case " right-most" :
197+ castType <DOMRect >(el )
198+ if (space .right >= popup .width ) {
199+ finalPos .x = el .x + el .width ; break outerloop
200+ } else {
201+ finalPos .x = veil .x + veil .width - popup .width ; break outerloop
171202 }
172- }
173- break
174- case " left-most" :
175- castType <DOMRect >(el )
176- if (space .left >= popup .width ) {
177- finalPos .x = el .x - popup .width ; break outerloop
178- } else {
179- finalPos .x = 0 ; break outerloop
180- }
181- case " right-most" :
182- castType <DOMRect >(el )
183- if (space .right >= popup .width ) {
184- finalPos .x = el .x + el .width ; break outerloop
185- } else {
186- finalPos .x = veil .x + veil .width - popup .width ; break outerloop
187- }
188203
189- case " right" :
190- castType <DOMRect >(el )
191- if (space .right >= popup .width ) {
192- finalPos .x = el .x ; break outerloop
193- }
194- break
195- case " left" :
196- castType <DOMRect >(el )
197- if (space .left >= popup .width ) {
198- finalPos .x = (el .x + el .width ) - popup .width ; break outerloop
199- }
200- break
201- case " either" : {
202- castType <DOMRect >(el )
203- if (space .right >= space .left ) {
204- finalPos .x = el .x ; break outerloop
205- } else {
206- finalPos .x = (el .x + el .width ) - popup .width
207- break outerloop
204+ case " right" :
205+ castType <DOMRect >(el )
206+ if (space .right >= popup .width ) {
207+ finalPos .x = el .x ; break outerloop
208+ }
209+ break
210+ case " left" :
211+ castType <DOMRect >(el )
212+ if (space .left >= popup .width ) {
213+ finalPos .x = (el .x + el .width ) - popup .width ; break outerloop
214+ }
215+ break
216+ case " either" : {
217+ castType <DOMRect >(el )
218+ if (space .right >= space .left ) {
219+ finalPos .x = el .x ; break outerloop
220+ } else {
221+ finalPos .x = (el .x + el .width ) - popup .width
222+ break outerloop
223+ }
208224 }
209225 }
210226 }
211227 }
212- outerloop :
213- for (const type of preferredVertical ) {
214- switch (type ) {
215- case " center-screen" :
216- if (popup .height < veil .height ) {
217- finalPos .y = (veil .height / 2 ) - (popup .height / 2 )
218- } else {
219- finalPos .y = 0
220- maxHeight = finalPos .y
221- }
222- break
223- case " top" :
224- castType <DOMRect >(el )
225- if (space .top >= popup .height ) {
226- finalPos .y = el .y - popup .height ; break outerloop
227- }
228- break
229- case " bottom" :
230- castType <DOMRect >(el )
231- if (space .bottom >= popup .height ) {
232- finalPos .y = el .y + el .height ; break outerloop
233- }
234- break
235- case " top-most" :
236- castType <DOMRect >(el )
237- if (space .top >= popup .height ) {
238- finalPos .y = el .y - popup .height ; break outerloop
239- } else {
240- finalPos .y = 0 ; break outerloop
241- }
242- case " bottom-most" :
243- castType <DOMRect >(el )
244- if (space .bottom >= popup .height ) {
245- finalPos .y = el .y + el .height ; break outerloop
246- } else {
247- finalPos .y = veil .y + veil .height - popup .height ; break outerloop
248- }
249- case " center-most" :
250- case " center" :
251- castType <DOMRect >(el )
252- if (space .topFromCenter >= (popup .height / 2 ) &&
253- space .bottomFromCenter >= (popup .height / 2 )) {
254- finalPos .y = el .y + (el .height / 2 ) - (popup .height / 2 )
255- break outerloop
256- }
257- // todo temp fix when it's too wide, will prefer the top
258- if (((space .bottomFromCenter + space .topFromCenter ) <= popup .height )) {
259- finalPos .y = 0
260- break outerloop
261- }
262- if (type === " center-most" ) {
263- if (space .topFromCenter < space .bottomFromCenter ) {
264- finalPos .y = el .y + (el .height / 2 ) - space .topFromCenter ; break outerloop
228+ if (typeof preferredVertical === " function" ) {
229+ finalPos .y = preferredVertical (el , popup , veil , space )
230+ } else {
231+ outerloop :
232+ for (const type of preferredVertical ) {
233+ switch (type ) {
234+ case " center-screen" :
235+ if (popup .height < veil .height ) {
236+ finalPos .y = (veil .height / 2 ) - (popup .height / 2 )
237+ } else {
238+ finalPos .y = 0
239+ maxHeight = finalPos .y
240+ }
241+ break
242+ case " top" :
243+ castType <DOMRect >(el )
244+ if (space .top >= popup .height ) {
245+ finalPos .y = el .y - popup .height ; break outerloop
246+ }
247+ break
248+ case " bottom" :
249+ castType <DOMRect >(el )
250+ if (space .bottom >= popup .height ) {
251+ finalPos .y = el .y + el .height ; break outerloop
252+ }
253+ break
254+ case " top-most" :
255+ castType <DOMRect >(el )
256+ if (space .top >= popup .height ) {
257+ finalPos .y = el .y - popup .height ; break outerloop
265258 } else {
266- finalPos .y = el . y + ( el . height / 2 ) + space . bottomFromCenter - popup . height ; break outerloop
259+ finalPos .y = 0 ; break outerloop
267260 }
261+ case " bottom-most" :
262+ castType <DOMRect >(el )
263+ if (space .bottom >= popup .height ) {
264+ finalPos .y = el .y + el .height ; break outerloop
265+ } else {
266+ finalPos .y = veil .y + veil .height - popup .height ; break outerloop
267+ }
268+ case " center-most" :
269+ case " center" :
270+ castType <DOMRect >(el )
271+ if (space .topFromCenter >= (popup .height / 2 ) &&
272+ space .bottomFromCenter >= (popup .height / 2 )) {
273+ finalPos .y = el .y + (el .height / 2 ) - (popup .height / 2 )
274+ break outerloop
275+ }
276+ // todo temp fix when it's too wide, will prefer the top
277+ if (((space .bottomFromCenter + space .topFromCenter ) <= popup .height )) {
278+ finalPos .y = 0
279+ break outerloop
280+ }
281+ if (type === " center-most" ) {
282+ if (space .topFromCenter < space .bottomFromCenter ) {
283+ finalPos .y = el .y + (el .height / 2 ) - space .topFromCenter ; break outerloop
284+ } else {
285+ finalPos .y = el .y + (el .height / 2 ) + space .bottomFromCenter - popup .height ; break outerloop
286+ }
287+ }
288+ break
289+ case " either" : {
290+ castType <DOMRect >(el )
291+ if (space .top >= space .bottom ) {
292+ finalPos .y = el .y - popup .height ; break outerloop
293+ } else { finalPos .y = el .y + el .height ; break outerloop }
268294 }
269- break
270- case " either" : {
271- castType <DOMRect >(el )
272- if (space .top >= space .bottom ) {
273- finalPos .y = el .y - popup .height ; break outerloop
274- } else { finalPos .y = el .y + el .height ; break outerloop }
275295 }
276296 }
277297 }
278298 finalPos .maxWidth = maxWidth ?? undefined
279299 finalPos .maxHeight = maxHeight ?? undefined
280300 /* eslint-enable no-labels */
301+ if (props .modifyPosition ) {
302+ finalPos = props .modifyPosition (finalPos , el , popup , veil , space )
303+ }
281304 pos .value = finalPos
282305 lastButtonElPos = el
283306 })
@@ -382,10 +405,14 @@ type RealProps =
382405 * There is also the `center-screen` position, which centers the popup on the screen.
383406 *
384407 * These last two (`*-most` and `center-screen`) are greedy, they will always find a position that fits. Positions listed after are ignored.
408+ *
409+ * You can also specify a function instead which is given some additional information regarding the space around the button reference element. It should a number for the x position (or y, if preferredVertical).
410+ *
411+ * If you only need to slightn the position, you can use the `modifyPosition` option instead.
385412 */
386- preferredHorizontal? : (" center" | " right" | " left" | " either" | " center-screen" | " right-most" | " left-most" | " center-most" )[]
413+ preferredHorizontal? : (" center" | " right" | " left" | " either" | " center-screen" | " right-most" | " left-most" | " center-most" )[] | PopupPositioner
387414 /** See `preferredHorizontal`. */
388- preferredVertical? : (" top" | " bottom" | " center" | " either" | " center-screen" | " top-most" | " bottom-most" | " center-most" )[]
415+ preferredVertical? : (" top" | " bottom" | " center" | " either" | " center-screen" | " top-most" | " bottom-most" | " center-most" )[] | PopupPositioner
389416 /**
390417 * When the user scrolls or resizes, normally the entire popup position is recomputed, taking into account the preferred positioning.
391418 *
@@ -394,6 +421,10 @@ type RealProps =
394421 * Set this to true to only shift the popup depending on how much the button element moved and avoid recalculating the best position.
395422 */
396423 avoidRepositioning? : boolean
424+ /**
425+ * Allows modifying the calculated position, to for example, clamp it.
426+ */
427+ modifyPosition? : PopupPositionModifier
397428}
398429
399430interface Props
0 commit comments