quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
381 lines (334 loc) • 9.68 kB
JavaScript
import { getScrollbarWidth } from '../scroll/scroll.js'
import { client } from '../../plugins/platform/Platform.js'
let vpLeft, vpTop
export function validatePosition(pos) {
const parts = pos.split(' ')
if (parts.length !== 2) {
return false
}
if (['top', 'center', 'bottom'].includes(parts[0]) !== true) {
console.error(
'Anchor/Self position must start with one of top/center/bottom'
)
return false
}
if (['left', 'middle', 'right', 'start', 'end'].includes(parts[1]) !== true) {
console.error(
'Anchor/Self position must end with one of left/middle/right/start/end'
)
return false
}
return true
}
export function validateOffset(val) {
if (!val) {
return true
}
if (val.length !== 2) {
return false
}
if (typeof val[0] !== 'number' || typeof val[1] !== 'number') {
return false
}
return true
}
const horizontalPos = {
'start#ltr': 'left',
'start#rtl': 'right',
'end#ltr': 'right',
'end#rtl': 'left'
}
;['left', 'middle', 'right'].forEach(pos => {
horizontalPos[`${pos}#ltr`] = pos
horizontalPos[`${pos}#rtl`] = pos
})
export function parsePosition(pos, rtl) {
const parts = pos.split(' ')
return {
vertical: parts[0],
horizontal: horizontalPos[`${parts[1]}#${rtl === true ? 'rtl' : 'ltr'}`]
}
}
export function getAnchorProps(el, offset) {
let { top, left, right, bottom, width, height } = el.getBoundingClientRect()
if (offset !== void 0) {
top -= offset[1]
left -= offset[0]
bottom += offset[1]
right += offset[0]
width += offset[0]
height += offset[1]
}
return {
top,
bottom,
height,
left,
right,
width,
middle: left + (right - left) / 2,
center: top + (bottom - top) / 2
}
}
function getAbsoluteAnchorProps(el, absoluteOffset, offset) {
let { top, left } = el.getBoundingClientRect()
top += absoluteOffset.top
left += absoluteOffset.left
if (offset !== void 0) {
top += offset[1]
left += offset[0]
}
return {
top,
bottom: top + 1,
height: 1,
left,
right: left + 1,
width: 1,
middle: left,
center: top
}
}
function getTargetProps(width, height) {
return {
top: 0,
center: height / 2,
bottom: height,
left: 0,
middle: width / 2,
right: width
}
}
function getTopLeftProps(anchorProps, targetProps, anchorOrigin, selfOrigin) {
return {
top: anchorProps[anchorOrigin.vertical] - targetProps[selfOrigin.vertical],
left:
anchorProps[anchorOrigin.horizontal] - targetProps[selfOrigin.horizontal]
}
}
export function setPosition(cfg, retryNumber = 0) {
if (
cfg.targetEl === null ||
cfg.anchorEl === null ||
retryNumber > 5 // we should try only a few times
) {
return
}
// some browsers report zero height or width because
// we are trying too early to get these dimensions
if (cfg.targetEl.offsetHeight === 0 || cfg.targetEl.offsetWidth === 0) {
setTimeout(() => {
setPosition(cfg, retryNumber + 1)
}, 10)
return
}
const {
targetEl,
offset,
anchorEl,
anchorOrigin,
selfOrigin,
absoluteOffset,
fit,
cover,
maxHeight,
maxWidth
} = cfg
if (client.is.ios === true && window.visualViewport !== void 0) {
// uses the q-position-engine CSS class
const el = document.body.style
const { offsetLeft: left, offsetTop: top } = window.visualViewport
if (left !== vpLeft) {
el.setProperty('--q-pe-left', left + 'px')
vpLeft = left
}
if (top !== vpTop) {
el.setProperty('--q-pe-top', top + 'px')
vpTop = top
}
}
// scroll position might change
// if max-height/-width changes, so we
// need to restore it after we calculate
// the new positioning
const { scrollLeft, scrollTop } = targetEl
const anchorProps =
absoluteOffset === void 0
? getAnchorProps(anchorEl, cover === true ? [0, 0] : offset)
: getAbsoluteAnchorProps(anchorEl, absoluteOffset, offset)
/**
* We "reset" the critical CSS properties
* so we can take an accurate measurement.
*
* Ensure that targetEl has a max-width & max-height
* set in CSS and that the value does NOT exceeds 100vw/vh.
* All users of the position-engine (currently QMenu & QTooltip)
* have CSS for this.
*/
Object.assign(targetEl.style, {
top: 0,
left: 0,
minWidth: null,
minHeight: null,
maxWidth,
maxHeight,
visibility: 'visible'
})
const { offsetWidth: origElWidth, offsetHeight: origElHeight } = targetEl
const { elWidth, elHeight } =
fit === true || cover === true
? {
elWidth: Math.max(anchorProps.width, origElWidth),
elHeight:
cover === true
? Math.max(anchorProps.height, origElHeight)
: origElHeight
}
: { elWidth: origElWidth, elHeight: origElHeight }
let elStyle = { maxWidth, maxHeight }
if (fit === true || cover === true) {
elStyle.minWidth = anchorProps.width + 'px'
if (cover === true) {
elStyle.minHeight = anchorProps.height + 'px'
}
}
Object.assign(targetEl.style, elStyle)
const targetProps = getTargetProps(elWidth, elHeight)
let props = getTopLeftProps(
anchorProps,
targetProps,
anchorOrigin,
selfOrigin
)
if (absoluteOffset === void 0 || offset === void 0) {
applyBoundaries(props, anchorProps, targetProps, anchorOrigin, selfOrigin)
} else {
// we have touch position or context menu with offset
const { top, left } = props // cache initial values
// apply initial boundaries
applyBoundaries(props, anchorProps, targetProps, anchorOrigin, selfOrigin)
let hasChanged = false
// did it flip vertically?
if (props.top !== top) {
hasChanged = true
const offsetY = 2 * offset[1]
anchorProps.center = anchorProps.top -= offsetY
anchorProps.bottom -= offsetY + 2
}
// did it flip horizontally?
if (props.left !== left) {
hasChanged = true
const offsetX = 2 * offset[0]
anchorProps.middle = anchorProps.left -= offsetX
anchorProps.right -= offsetX + 2
}
if (hasChanged === true) {
// re-calculate props with the new anchor
props = getTopLeftProps(
anchorProps,
targetProps,
anchorOrigin,
selfOrigin
)
// and re-apply boundaries
applyBoundaries(props, anchorProps, targetProps, anchorOrigin, selfOrigin)
}
}
elStyle = {
top: props.top + 'px',
left: props.left + 'px'
}
if (props.maxHeight !== void 0) {
elStyle.maxHeight = props.maxHeight + 'px'
if (anchorProps.height > props.maxHeight) {
elStyle.minHeight = elStyle.maxHeight
}
}
if (props.maxWidth !== void 0) {
elStyle.maxWidth = props.maxWidth + 'px'
if (anchorProps.width > props.maxWidth) {
elStyle.minWidth = elStyle.maxWidth
}
}
Object.assign(targetEl.style, elStyle)
// restore scroll position
if (targetEl.scrollTop !== scrollTop) {
targetEl.scrollTop = scrollTop
}
if (targetEl.scrollLeft !== scrollLeft) {
targetEl.scrollLeft = scrollLeft
}
}
function applyBoundaries(
props,
anchorProps,
targetProps,
anchorOrigin,
selfOrigin
) {
const currentHeight = targetProps.bottom,
currentWidth = targetProps.right,
margin = getScrollbarWidth(),
innerHeight = window.innerHeight - margin,
innerWidth = document.body.clientWidth
if (props.top < 0 || props.top + currentHeight > innerHeight) {
if (selfOrigin.vertical === 'center') {
props.top =
anchorProps[anchorOrigin.vertical] > innerHeight / 2
? Math.max(0, innerHeight - currentHeight)
: 0
props.maxHeight = Math.min(currentHeight, innerHeight)
} else if (anchorProps[anchorOrigin.vertical] > innerHeight / 2) {
const anchorY = Math.min(
innerHeight,
anchorOrigin.vertical === 'center'
? anchorProps.center
: anchorOrigin.vertical === selfOrigin.vertical
? anchorProps.bottom
: anchorProps.top
)
props.maxHeight = Math.min(currentHeight, anchorY)
props.top = Math.max(0, anchorY - currentHeight)
} else {
props.top = Math.max(
0,
anchorOrigin.vertical === 'center'
? anchorProps.center
: anchorOrigin.vertical === selfOrigin.vertical
? anchorProps.top
: anchorProps.bottom
)
props.maxHeight = Math.min(currentHeight, innerHeight - props.top)
}
}
if (props.left < 0 || props.left + currentWidth > innerWidth) {
props.maxWidth = Math.min(currentWidth, innerWidth)
if (selfOrigin.horizontal === 'middle') {
props.left =
anchorProps[anchorOrigin.horizontal] > innerWidth / 2
? Math.max(0, innerWidth - currentWidth)
: 0
} else if (anchorProps[anchorOrigin.horizontal] > innerWidth / 2) {
const anchorX = Math.min(
innerWidth,
anchorOrigin.horizontal === 'middle'
? anchorProps.middle
: anchorOrigin.horizontal === selfOrigin.horizontal
? anchorProps.right
: anchorProps.left
)
props.maxWidth = Math.min(currentWidth, anchorX)
props.left = Math.max(0, anchorX - props.maxWidth)
} else {
props.left = Math.max(
0,
anchorOrigin.horizontal === 'middle'
? anchorProps.middle
: anchorOrigin.horizontal === selfOrigin.horizontal
? anchorProps.left
: anchorProps.right
)
props.maxWidth = Math.min(currentWidth, innerWidth - props.left)
}
}
}