// Composables import { useToggleScope } from "../../composables/toggleScope.mjs"; // Utilities import { computed, nextTick, onScopeDispose, ref, watch } from 'vue'; import { anchorToPoint, getOffset } from "./util/point.mjs"; import { clamp, consoleError, convertToUnit, destructComputed, flipAlign, flipCorner, flipSide, getAxis, getScrollParents, IN_BROWSER, isFixedPosition, nullifyTransforms, parseAnchor, propsFactory } from "../../util/index.mjs"; import { Box, getOverflow, getTargetBox } from "../../util/box.mjs"; // Types const locationStrategies = { static: staticLocationStrategy, // specific viewport position, usually centered connected: connectedLocationStrategy // connected to a certain element }; export const makeLocationStrategyProps = propsFactory({ locationStrategy: { type: [String, Function], default: 'static', validator: val => typeof val === 'function' || val in locationStrategies }, location: { type: String, default: 'bottom' }, origin: { type: String, default: 'auto' }, offset: [Number, String, Array] }, 'VOverlay-location-strategies'); export function useLocationStrategies(props, data) { const contentStyles = ref({}); const updateLocation = ref(); if (IN_BROWSER) { useToggleScope(() => !!(data.isActive.value && props.locationStrategy), reset => { watch(() => props.locationStrategy, reset); onScopeDispose(() => { window.removeEventListener('resize', onResize); updateLocation.value = undefined; }); window.addEventListener('resize', onResize, { passive: true }); if (typeof props.locationStrategy === 'function') { updateLocation.value = props.locationStrategy(data, props, contentStyles)?.updateLocation; } else { updateLocation.value = locationStrategies[props.locationStrategy](data, props, contentStyles)?.updateLocation; } }); } function onResize(e) { updateLocation.value?.(e); } return { contentStyles, updateLocation }; } function staticLocationStrategy() { // TODO } /** Get size of element ignoring max-width/max-height */ function getIntrinsicSize(el, isRtl) { // const scrollables = new Map() // el.querySelectorAll('*').forEach(el => { // const x = el.scrollLeft // const y = el.scrollTop // if (x || y) { // scrollables.set(el, [x, y]) // } // }) // const initialMaxWidth = el.style.maxWidth // const initialMaxHeight = el.style.maxHeight // el.style.removeProperty('max-width') // el.style.removeProperty('max-height') if (isRtl) { el.style.removeProperty('left'); } else { el.style.removeProperty('right'); } /* eslint-disable-next-line sonarjs/prefer-immediate-return */ const contentBox = nullifyTransforms(el); if (isRtl) { contentBox.x += parseFloat(el.style.right || 0); } else { contentBox.x -= parseFloat(el.style.left || 0); } contentBox.y -= parseFloat(el.style.top || 0); // el.style.maxWidth = initialMaxWidth // el.style.maxHeight = initialMaxHeight // scrollables.forEach((position, el) => { // el.scrollTo(...position) // }) return contentBox; } function connectedLocationStrategy(data, props, contentStyles) { const activatorFixed = Array.isArray(data.target.value) || isFixedPosition(data.target.value); if (activatorFixed) { Object.assign(contentStyles.value, { position: 'fixed', top: 0, [data.isRtl.value ? 'right' : 'left']: 0 }); } const { preferredAnchor, preferredOrigin } = destructComputed(() => { const parsedAnchor = parseAnchor(props.location, data.isRtl.value); const parsedOrigin = props.origin === 'overlap' ? parsedAnchor : props.origin === 'auto' ? flipSide(parsedAnchor) : parseAnchor(props.origin, data.isRtl.value); // Some combinations of props may produce an invalid origin if (parsedAnchor.side === parsedOrigin.side && parsedAnchor.align === flipAlign(parsedOrigin).align) { return { preferredAnchor: flipCorner(parsedAnchor), preferredOrigin: flipCorner(parsedOrigin) }; } else { return { preferredAnchor: parsedAnchor, preferredOrigin: parsedOrigin }; } }); const [minWidth, minHeight, maxWidth, maxHeight] = ['minWidth', 'minHeight', 'maxWidth', 'maxHeight'].map(key => { return computed(() => { const val = parseFloat(props[key]); return isNaN(val) ? Infinity : val; }); }); const offset = computed(() => { if (Array.isArray(props.offset)) { return props.offset; } if (typeof props.offset === 'string') { const offset = props.offset.split(' ').map(parseFloat); if (offset.length < 2) offset.push(0); return offset; } return typeof props.offset === 'number' ? [props.offset, 0] : [0, 0]; }); let observe = false; const observer = new ResizeObserver(() => { if (observe) updateLocation(); }); watch([data.target, data.contentEl], (_ref, _ref2) => { let [newTarget, newContentEl] = _ref; let [oldTarget, oldContentEl] = _ref2; if (oldTarget && !Array.isArray(oldTarget)) observer.unobserve(oldTarget); if (newTarget && !Array.isArray(newTarget)) observer.observe(newTarget); if (oldContentEl) observer.unobserve(oldContentEl); if (newContentEl) observer.observe(newContentEl); }, { immediate: true }); onScopeDispose(() => { observer.disconnect(); }); // eslint-disable-next-line max-statements function updateLocation() { observe = false; requestAnimationFrame(() => observe = true); if (!data.target.value || !data.contentEl.value) return; const targetBox = getTargetBox(data.target.value); const contentBox = getIntrinsicSize(data.contentEl.value, data.isRtl.value); const scrollParents = getScrollParents(data.contentEl.value); const viewportMargin = 12; if (!scrollParents.length) { scrollParents.push(document.documentElement); if (!(data.contentEl.value.style.top && data.contentEl.value.style.left)) { contentBox.x -= parseFloat(document.documentElement.style.getPropertyValue('--v-body-scroll-x') || 0); contentBox.y -= parseFloat(document.documentElement.style.getPropertyValue('--v-body-scroll-y') || 0); } } const viewport = scrollParents.reduce((box, el) => { const rect = el.getBoundingClientRect(); const scrollBox = new Box({ x: el === document.documentElement ? 0 : rect.x, y: el === document.documentElement ? 0 : rect.y, width: el.clientWidth, height: el.clientHeight }); if (box) { return new Box({ x: Math.max(box.left, scrollBox.left), y: Math.max(box.top, scrollBox.top), width: Math.min(box.right, scrollBox.right) - Math.max(box.left, scrollBox.left), height: Math.min(box.bottom, scrollBox.bottom) - Math.max(box.top, scrollBox.top) }); } return scrollBox; }, undefined); viewport.x += viewportMargin; viewport.y += viewportMargin; viewport.width -= viewportMargin * 2; viewport.height -= viewportMargin * 2; let placement = { anchor: preferredAnchor.value, origin: preferredOrigin.value }; function checkOverflow(_placement) { const box = new Box(contentBox); const targetPoint = anchorToPoint(_placement.anchor, targetBox); const contentPoint = anchorToPoint(_placement.origin, box); let { x, y } = getOffset(targetPoint, contentPoint); switch (_placement.anchor.side) { case 'top': y -= offset.value[0]; break; case 'bottom': y += offset.value[0]; break; case 'left': x -= offset.value[0]; break; case 'right': x += offset.value[0]; break; } switch (_placement.anchor.align) { case 'top': y -= offset.value[1]; break; case 'bottom': y += offset.value[1]; break; case 'left': x -= offset.value[1]; break; case 'right': x += offset.value[1]; break; } box.x += x; box.y += y; box.width = Math.min(box.width, maxWidth.value); box.height = Math.min(box.height, maxHeight.value); const overflows = getOverflow(box, viewport); return { overflows, x, y }; } let x = 0; let y = 0; const available = { x: 0, y: 0 }; const flipped = { x: false, y: false }; let resets = -1; while (true) { if (resets++ > 10) { consoleError('Infinite loop detected in connectedLocationStrategy'); break; } const { x: _x, y: _y, overflows } = checkOverflow(placement); x += _x; y += _y; contentBox.x += _x; contentBox.y += _y; // flip { const axis = getAxis(placement.anchor); const hasOverflowX = overflows.x.before || overflows.x.after; const hasOverflowY = overflows.y.before || overflows.y.after; let reset = false; ['x', 'y'].forEach(key => { if (key === 'x' && hasOverflowX && !flipped.x || key === 'y' && hasOverflowY && !flipped.y) { const newPlacement = { anchor: { ...placement.anchor }, origin: { ...placement.origin } }; const flip = key === 'x' ? axis === 'y' ? flipAlign : flipSide : axis === 'y' ? flipSide : flipAlign; newPlacement.anchor = flip(newPlacement.anchor); newPlacement.origin = flip(newPlacement.origin); const { overflows: newOverflows } = checkOverflow(newPlacement); if (newOverflows[key].before <= overflows[key].before && newOverflows[key].after <= overflows[key].after || newOverflows[key].before + newOverflows[key].after < (overflows[key].before + overflows[key].after) / 2) { placement = newPlacement; reset = flipped[key] = true; } } }); if (reset) continue; } // shift if (overflows.x.before) { x += overflows.x.before; contentBox.x += overflows.x.before; } if (overflows.x.after) { x -= overflows.x.after; contentBox.x -= overflows.x.after; } if (overflows.y.before) { y += overflows.y.before; contentBox.y += overflows.y.before; } if (overflows.y.after) { y -= overflows.y.after; contentBox.y -= overflows.y.after; } // size { const overflows = getOverflow(contentBox, viewport); available.x = viewport.width - overflows.x.before - overflows.x.after; available.y = viewport.height - overflows.y.before - overflows.y.after; x += overflows.x.before; contentBox.x += overflows.x.before; y += overflows.y.before; contentBox.y += overflows.y.before; } break; } const axis = getAxis(placement.anchor); Object.assign(contentStyles.value, { '--v-overlay-anchor-origin': `${placement.anchor.side} ${placement.anchor.align}`, transformOrigin: `${placement.origin.side} ${placement.origin.align}`, // transform: `translate(${pixelRound(x)}px, ${pixelRound(y)}px)`, top: convertToUnit(pixelRound(y)), left: data.isRtl.value ? undefined : convertToUnit(pixelRound(x)), right: data.isRtl.value ? convertToUnit(pixelRound(-x)) : undefined, minWidth: convertToUnit(axis === 'y' ? Math.min(minWidth.value, targetBox.width) : minWidth.value), maxWidth: convertToUnit(pixelCeil(clamp(available.x, minWidth.value === Infinity ? 0 : minWidth.value, maxWidth.value))), maxHeight: convertToUnit(pixelCeil(clamp(available.y, minHeight.value === Infinity ? 0 : minHeight.value, maxHeight.value))) }); return { available, contentBox }; } watch(() => [preferredAnchor.value, preferredOrigin.value, props.offset, props.minWidth, props.minHeight, props.maxWidth, props.maxHeight], () => updateLocation()); nextTick(() => { const result = updateLocation(); // TODO: overflowing content should only require a single updateLocation call // Icky hack to make sure the content is positioned consistently if (!result) return; const { available, contentBox } = result; if (contentBox.height > available.y) { requestAnimationFrame(() => { updateLocation(); requestAnimationFrame(() => { updateLocation(); }); }); } }); return { updateLocation }; } function pixelRound(val) { return Math.round(val * devicePixelRatio) / devicePixelRatio; } function pixelCeil(val) { return Math.ceil(val * devicePixelRatio) / devicePixelRatio; } //# sourceMappingURL=locationStrategies.mjs.map