UNPKG

@stackoverfloweth/prefect-design

Version:

A collection of low-level Vue components.

193 lines (159 loc) 6.07 kB
/* eslint-disable max-params */ import { getCurrentInstance, onMounted, onUnmounted, reactive, ref, Ref, watch, watchEffect } from 'vue' import { Position, PositionMethod, PositionStyles, UsePosition, UsePositionRefs, UsePositionStyles } from '@/types/position' import { toPixels } from '@/utilities/units' export function usePosition( placement: Ref<PositionMethod> | PositionMethod, refs?: Partial<UsePositionRefs>, ): UsePosition { const target = refs?.target ?? ref<Element | undefined>() const content = refs?.content ?? ref<Element | undefined>() const container = refs?.container ?? ref<Element | undefined>() const placementRef = ref(placement) const position = reactive({} as Position) const observer = new ResizeObserver(update) function update(): void { if (target.value && content.value && container.value) { const newPosition = getPosition(target.value, content.value, container.value, placementRef.value) Object.assign(position, newPosition) } } useOnMountedIfComponentIsDetected(() => { watchEffect(update) observeTemplateRefs(observer, [target, content, container]) }) useOnUnmountedIfComponentIsDetected(() => observer.disconnect()) return { target, content, container, position, } } export function usePositionStyles( placement: Ref<PositionMethod> | PositionMethod, refs?: Partial<UsePositionRefs>, ): UsePositionStyles { const { target, content, container, position } = usePosition(placement, refs) const styles = reactive({} as PositionStyles) watchEffect(() => Object.assign(styles, mapPositionToPositionStyles(position))) return { target, content, container, styles, } } export function useMostVisiblePosition( placements: Ref<PositionMethod[]> | PositionMethod[], refs?: Partial<UsePositionRefs>, ): UsePosition { const target = refs?.target ?? ref<Element | undefined>() const content = refs?.content ?? ref<Element | undefined>() const container = refs?.container ?? ref<Element | undefined>() const placementsRef = ref(placements) const position = reactive({} as Position) const observer = new ResizeObserver(update) function update(): void { if (target.value && content.value && container.value && placementsRef.value.length > 0) { const positions = placementsRef.value.map(placement => usePosition(placement, { target, content, container })) // eslint-disable-next-line id-length const positionsSortedByVisibility = [...positions].sort((a, b) => sortPositionsByVisibility(content.value!, container.value!, a.position, b.position)) const [mostVisiblePosition] = positionsSortedByVisibility Object.assign(position, mostVisiblePosition.position) } } useOnMountedIfComponentIsDetected(() => { watchEffect(update) observeTemplateRefs(observer, [target, content, container]) }) useOnUnmountedIfComponentIsDetected(() => observer.disconnect()) return { target, content, container, position, } } export function useMostVisiblePositionStyles( placements: Ref<PositionMethod[]> | PositionMethod[], refs?: Partial<UsePositionRefs>, ): UsePositionStyles { const { target, content, container, position } = useMostVisiblePosition(placements, refs) const styles = reactive({} as PositionStyles) watchEffect(() => Object.assign(styles, mapPositionToPositionStyles(position))) return { target, content, container, styles, } } function getPositionVisibility(content: Element, container: Element, position: Position): number { const rect = getDomRectForPosition(content, container, position) const visibleWidth = Math.min(rect.right, window.innerWidth) - Math.max(rect.left, 0) const visibleHeight = Math.min(rect.bottom, window.innerHeight) - Math.max(rect.top, 0) const visibleArea = visibleWidth * visibleHeight const totalArea = rect.width * rect.height return visibleArea / totalArea } function getDomRectForPosition(content: Element, container: Element, position: Position): DOMRect { const { width, height } = content.getBoundingClientRect() const { left, top } = container.getBoundingClientRect() return new DOMRect(position.left + left, position.top + top, width, height) } function observerCallback(observer: ResizeObserver, newElement: Element | undefined, oldElement: Element | undefined): void { if (newElement) { observer.observe(newElement) } if (oldElement) { observer.unobserve(oldElement) } } function useOnMountedIfComponentIsDetected(callback: () => void): void { if (getCurrentInstance()) { onMounted(callback) } else { callback() } } function useOnUnmountedIfComponentIsDetected(callback: () => void): void { if (getCurrentInstance()) { onUnmounted(callback) } } function getPosition( target: Element, content: Element, container: Element, placement: PositionMethod, ): Position { const targetRect = target.getBoundingClientRect() const contentRect = content.getBoundingClientRect() const containerRect = container.getBoundingClientRect() const position = placement(targetRect, contentRect, containerRect) return position } function sortPositionsByVisibility(content: Element, container: Element, positionA: Position, positionB: Position): 0 | 1 | -1 { const aVisibility = getPositionVisibility(content, container, positionA) const bVisibility = getPositionVisibility(content, container, positionB) if (bVisibility === aVisibility) { return 0 } if (bVisibility > aVisibility) { return 1 } return -1 } function mapPositionToPositionStyles(position: Partial<Position>): PositionStyles { return { top: position.top ? toPixels(position.top) : undefined, left: position.left ? toPixels(position.left) : undefined, position: 'absolute', } } function observeTemplateRefs(observer: ResizeObserver, refs: Ref<Element | undefined>[]): void { refs.forEach(ref => { watch(ref, (newRef, oldRef) => observerCallback(observer, newRef, oldRef), { immediate: true }) }) }