123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380 |
- // 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<Element, [number, number]>()
- // 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')
- /* 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
|