@@ -44,26 +44,29 @@ import { onMounted, type PropType, ref, useAttrs, watch , type HTMLAttributes }
44
44
import { getFallbackId, type LinkableByIdProps,type TailwindClassProp } from "../shared/props.js"
45
45
46
46
import { 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"
48
50
49
51
const fallbackId = getFallbackId()
50
52
// eslint-disable-next-line no-use-before-define
51
53
const props = withDefaults(defineProps<Props>(), {
52
54
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"],
55
58
onlyShiftIfOpen: false,
56
59
})
57
60
const $attrs = useAttrs()
58
61
defineOptions({ name: "lib-popup" })
59
62
60
- type ElementLike = { getBoundingClientRect: () => DOMRect }
63
+
61
64
// todo, can we have transitions?
62
65
const 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)
65
68
66
- const pos = ref<{ x: number, y: number, maxWidth?: number, maxHeight?: number } >({} as any)
69
+ const pos = ref<PopupPosition >({} as any)
67
70
const modelValue = defineModel<boolean>({ default: false })
68
71
let isOpen = false
69
72
@@ -87,20 +90,29 @@ const getVeilBoundingRect = (el: HTMLElement): Omit<DOMRect, "toJSON"> => {
87
90
right: 0,
88
91
}
89
92
}
90
- let lastButtonElPos: DOMRect | undefined
93
+ let lastButtonElPos: ReturnType<IPopupReference["getBoundingClientRect"]> | undefined
91
94
const recompute = (force: boolean = false): void => {
92
95
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)) {
95
106
pos.value = {} as any
96
107
return
97
108
}
98
- const finalPos: { x: number, y: number, maxWidth?: number, maxHeight?: number } = {} as any
99
-
100
109
const el = buttonEl.value?.getBoundingClientRect()
101
110
const veil = getVeilBoundingRect(props.useBackdrop ? dialogEl.value : document.body)
102
111
const popup = popupEl.value.getBoundingClientRect()
103
112
113
+ let finalPos: { x: number, y: number, maxWidth?: number, maxHeight?: number } = {} as any
114
+
115
+
104
116
if (!force && modelValue.value && props.onlyShiftIfOpen && buttonEl.value && lastButtonElPos) {
105
117
const shiftX = buttonEl.value.getBoundingClientRect().x - lastButtonElPos.x
106
118
const shiftY = buttonEl.value.getBoundingClientRect().y - lastButtonElPos.y
@@ -138,146 +150,157 @@ const recompute = (force: boolean = false): void => {
138
150
const { preferredHorizontal, preferredVertical } = props
139
151
let maxWidth: number | undefined
140
152
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) &&
157
172
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
169
193
} 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
171
202
}
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
- }
188
203
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
+ }
208
224
}
209
225
}
210
226
}
211
227
}
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
265
258
} else {
266
- finalPos.y = el.y + (el.height / 2) + space.bottomFromCenter - popup.height ; break outerloop
259
+ finalPos.y = 0 ; break outerloop
267
260
}
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 }
268
294
}
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 }
275
295
}
276
296
}
277
297
}
278
298
finalPos.maxWidth = maxWidth ?? undefined
279
299
finalPos.maxHeight = maxHeight ?? undefined
280
300
/* eslint-enable no-labels */
301
+ if (props.modifyPosition) {
302
+ finalPos = props.modifyPosition(finalPos, el, popup, veil, space)
303
+ }
281
304
pos.value = finalPos
282
305
lastButtonElPos = el
283
306
})
@@ -382,10 +405,14 @@ type RealProps =
382
405
* There is also the `center-screen` position, which centers the popup on the screen.
383
406
*
384
407
* 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.
385
412
*/
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
387
414
/** 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
389
416
/**
390
417
* When the user scrolls or resizes, normally the entire popup position is recomputed, taking into account the preferred positioning.
391
418
*
@@ -394,6 +421,10 @@ type RealProps =
394
421
* Set this to true to only shift the popup depending on how much the button element moved and avoid recalculating the best position.
395
422
*/
396
423
avoidRepositioning?: boolean
424
+ /**
425
+ * Allows modifying the calculated position, to for example, clamp it.
426
+ */
427
+ modifyPosition?: PopupPositionModifier
397
428
}
398
429
399
430
interface Props
0 commit comments