|
const hasOwn = function(obj, key) { |
|
return Object.prototype.hasOwnProperty.call(obj, key); |
|
}; |
|
|
|
const isNum = function(num) { |
|
if (typeof num !== 'number' || isNaN(num)) { |
|
return false; |
|
} |
|
const isInvalid = function(n) { |
|
if (n === Number.MAX_VALUE || n === Number.MIN_VALUE || n === Number.NEGATIVE_INFINITY || n === Number.POSITIVE_INFINITY) { |
|
return true; |
|
} |
|
return false; |
|
}; |
|
if (isInvalid(num)) { |
|
return false; |
|
} |
|
return true; |
|
}; |
|
|
|
const toNum = (num) => { |
|
if (typeof (num) !== 'number') { |
|
num = parseFloat(num); |
|
} |
|
if (isNaN(num)) { |
|
num = 0; |
|
} |
|
num = Math.round(num); |
|
return num; |
|
}; |
|
|
|
const clamp = function(value, min, max) { |
|
return Math.max(min, Math.min(max, value)); |
|
}; |
|
|
|
const isWindow = (obj) => { |
|
return Boolean(obj && obj === obj.window); |
|
}; |
|
|
|
const isDocument = (obj) => { |
|
return Boolean(obj && obj.nodeType === 9); |
|
}; |
|
|
|
const isElement = (obj) => { |
|
return Boolean(obj && obj.nodeType === 1); |
|
}; |
|
|
|
|
|
|
|
export const toRect = (obj) => { |
|
if (obj) { |
|
return { |
|
left: toNum(obj.left || obj.x), |
|
top: toNum(obj.top || obj.y), |
|
width: toNum(obj.width), |
|
height: toNum(obj.height) |
|
}; |
|
} |
|
return { |
|
left: 0, |
|
top: 0, |
|
width: 0, |
|
height: 0 |
|
}; |
|
}; |
|
|
|
export const getElement = (selector) => { |
|
if (typeof selector === 'string' && selector) { |
|
if (selector.startsWith('#')) { |
|
return document.getElementById(selector.slice(1)); |
|
} |
|
return document.querySelector(selector); |
|
} |
|
|
|
if (isDocument(selector)) { |
|
return selector.body; |
|
} |
|
if (isElement(selector)) { |
|
return selector; |
|
} |
|
}; |
|
|
|
export const getRect = (target, fixed) => { |
|
if (!target) { |
|
return toRect(); |
|
} |
|
|
|
if (isWindow(target)) { |
|
return { |
|
left: 0, |
|
top: 0, |
|
width: window.innerWidth, |
|
height: window.innerHeight |
|
}; |
|
} |
|
|
|
const elem = getElement(target); |
|
if (!elem) { |
|
return toRect(target); |
|
} |
|
|
|
const br = elem.getBoundingClientRect(); |
|
const rect = toRect(br); |
|
|
|
|
|
if (!fixed) { |
|
rect.left += window.scrollX; |
|
rect.top += window.scrollY; |
|
} |
|
|
|
rect.width = elem.offsetWidth; |
|
rect.height = elem.offsetHeight; |
|
|
|
return rect; |
|
}; |
|
|
|
|
|
|
|
const calculators = { |
|
|
|
bottom: (info, containerRect, targetRect) => { |
|
info.space = containerRect.top + containerRect.height - targetRect.top - targetRect.height - info.height; |
|
info.top = targetRect.top + targetRect.height; |
|
info.left = Math.round(targetRect.left + targetRect.width * 0.5 - info.width * 0.5); |
|
}, |
|
|
|
top: (info, containerRect, targetRect) => { |
|
info.space = targetRect.top - info.height - containerRect.top; |
|
info.top = targetRect.top - info.height; |
|
info.left = Math.round(targetRect.left + targetRect.width * 0.5 - info.width * 0.5); |
|
}, |
|
|
|
right: (info, containerRect, targetRect) => { |
|
info.space = containerRect.left + containerRect.width - targetRect.left - targetRect.width - info.width; |
|
info.top = Math.round(targetRect.top + targetRect.height * 0.5 - info.height * 0.5); |
|
info.left = targetRect.left + targetRect.width; |
|
}, |
|
|
|
left: (info, containerRect, targetRect) => { |
|
info.space = targetRect.left - info.width - containerRect.left; |
|
info.top = Math.round(targetRect.top + targetRect.height * 0.5 - info.height * 0.5); |
|
info.left = targetRect.left - info.width; |
|
} |
|
}; |
|
|
|
|
|
export const getDefaultPositions = () => { |
|
return Object.keys(calculators); |
|
}; |
|
|
|
const calculateSpace = (info, containerRect, targetRect) => { |
|
const calculator = calculators[info.position]; |
|
calculator(info, containerRect, targetRect); |
|
if (info.space >= 0) { |
|
info.passed += 1; |
|
} |
|
}; |
|
|
|
|
|
|
|
const calculateAlignOffset = (info, containerRect, targetRect, alignType, sizeType) => { |
|
|
|
const popoverStart = info[alignType]; |
|
const popoverSize = info[sizeType]; |
|
|
|
const containerStart = containerRect[alignType]; |
|
const containerSize = containerRect[sizeType]; |
|
|
|
const targetStart = targetRect[alignType]; |
|
const targetSize = targetRect[sizeType]; |
|
|
|
const targetCenter = targetStart + targetSize * 0.5; |
|
|
|
|
|
if (popoverSize > containerSize) { |
|
const overflow = (popoverSize - containerSize) * 0.5; |
|
info[alignType] = containerStart - overflow; |
|
info.offset = targetCenter - containerStart + overflow; |
|
return; |
|
} |
|
|
|
const space1 = popoverStart - containerStart; |
|
const space2 = (containerStart + containerSize) - (popoverStart + popoverSize); |
|
|
|
|
|
if (space1 >= 0 && space2 >= 0) { |
|
if (info.passed) { |
|
info.passed += 2; |
|
} |
|
info.offset = popoverSize * 0.5; |
|
return; |
|
} |
|
|
|
|
|
if (info.passed) { |
|
info.passed += 1; |
|
} |
|
|
|
if (space1 < 0) { |
|
const min = containerStart; |
|
info[alignType] = min; |
|
info.offset = targetCenter - min; |
|
return; |
|
} |
|
|
|
|
|
const max = containerStart + containerSize - popoverSize; |
|
info[alignType] = max; |
|
info.offset = targetCenter - max; |
|
|
|
}; |
|
|
|
const calculateHV = (info, containerRect) => { |
|
if (['top', 'bottom'].includes(info.position)) { |
|
info.top = clamp(info.top, containerRect.top, containerRect.top + containerRect.height - info.height); |
|
return ['left', 'width']; |
|
} |
|
info.left = clamp(info.left, containerRect.left, containerRect.left + containerRect.width - info.width); |
|
return ['top', 'height']; |
|
}; |
|
|
|
const calculateOffset = (info, containerRect, targetRect) => { |
|
|
|
const [alignType, sizeType] = calculateHV(info, containerRect); |
|
|
|
calculateAlignOffset(info, containerRect, targetRect, alignType, sizeType); |
|
|
|
info.offset = clamp(info.offset, 0, info[sizeType]); |
|
|
|
}; |
|
|
|
|
|
|
|
const calculateDistance = (info, previousPositionInfo) => { |
|
if (!previousPositionInfo) { |
|
return; |
|
} |
|
|
|
if (info.position === previousPositionInfo.position) { |
|
return; |
|
} |
|
const ax = info.left + info.width * 0.5; |
|
const ay = info.top + info.height * 0.5; |
|
const bx = previousPositionInfo.left + previousPositionInfo.width * 0.5; |
|
const by = previousPositionInfo.top + previousPositionInfo.height * 0.5; |
|
const dx = Math.abs(ax - bx); |
|
const dy = Math.abs(ay - by); |
|
info.distance = Math.round(Math.sqrt(dx * dx + dy * dy)); |
|
}; |
|
|
|
|
|
|
|
const calculatePositionInfo = (info, containerRect, targetRect, previousPositionInfo) => { |
|
calculateSpace(info, containerRect, targetRect); |
|
calculateOffset(info, containerRect, targetRect); |
|
calculateDistance(info, previousPositionInfo); |
|
}; |
|
|
|
|
|
|
|
const calculateBestPosition = (containerRect, targetRect, infoMap, withOrder, previousPositionInfo) => { |
|
|
|
|
|
|
|
|
|
|
|
|
|
const safePassed = 3; |
|
|
|
if (previousPositionInfo) { |
|
const prevInfo = infoMap[previousPositionInfo.position]; |
|
if (prevInfo) { |
|
calculatePositionInfo(prevInfo, containerRect, targetRect); |
|
if (prevInfo.passed >= safePassed) { |
|
return prevInfo; |
|
} |
|
prevInfo.calculated = true; |
|
} |
|
} |
|
|
|
const positionList = []; |
|
Object.values(infoMap).forEach((info) => { |
|
if (!info.calculated) { |
|
calculatePositionInfo(info, containerRect, targetRect, previousPositionInfo); |
|
} |
|
positionList.push(info); |
|
}); |
|
|
|
positionList.sort((a, b) => { |
|
if (a.passed !== b.passed) { |
|
return b.passed - a.passed; |
|
} |
|
|
|
if (withOrder && a.passed >= safePassed && b.passed >= safePassed) { |
|
return a.index - b.index; |
|
} |
|
|
|
if (a.space !== b.space) { |
|
return b.space - a.space; |
|
} |
|
|
|
return a.index - b.index; |
|
}); |
|
|
|
|
|
|
|
return positionList[0]; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const getAllowPositions = (positions, defaultAllowPositions) => { |
|
if (!positions) { |
|
return; |
|
} |
|
if (Array.isArray(positions)) { |
|
positions = positions.join(','); |
|
} |
|
positions = String(positions).split(',').map((it) => it.trim().toLowerCase()).filter((it) => it); |
|
positions = positions.filter((it) => defaultAllowPositions.includes(it)); |
|
if (!positions.length) { |
|
return; |
|
} |
|
return positions; |
|
}; |
|
|
|
const isPositionChanged = (info, previousPositionInfo) => { |
|
if (!previousPositionInfo) { |
|
return true; |
|
} |
|
|
|
if (info.left !== previousPositionInfo.left) { |
|
return true; |
|
} |
|
|
|
if (info.top !== previousPositionInfo.top) { |
|
return true; |
|
} |
|
|
|
return false; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const getBestPosition = (containerRect, targetRect, popoverRect, positions, previousPositionInfo) => { |
|
|
|
const defaultAllowPositions = getDefaultPositions(); |
|
let withOrder = true; |
|
let allowPositions = getAllowPositions(positions, defaultAllowPositions); |
|
if (!allowPositions) { |
|
allowPositions = defaultAllowPositions; |
|
withOrder = false; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
const infoMap = {}; |
|
allowPositions.forEach((k, i) => { |
|
infoMap[k] = { |
|
position: k, |
|
index: i, |
|
|
|
top: 0, |
|
left: 0, |
|
width: popoverRect.width, |
|
height: popoverRect.height, |
|
|
|
space: 0, |
|
|
|
offset: 0, |
|
passed: 0, |
|
|
|
distance: 0 |
|
}; |
|
}); |
|
|
|
|
|
|
|
|
|
const bestPosition = calculateBestPosition(containerRect, targetRect, infoMap, withOrder, previousPositionInfo); |
|
|
|
|
|
bestPosition.changed = isPositionChanged(bestPosition, previousPositionInfo); |
|
|
|
return bestPosition; |
|
}; |
|
|
|
|
|
|
|
const getTemplatePath = (width, height, arrowOffset, arrowSize, borderRadius) => { |
|
const p = (px, py) => { |
|
return [px, py].join(','); |
|
}; |
|
|
|
const px = function(num, alignEnd) { |
|
const floor = Math.floor(num); |
|
let n = num < floor + 0.5 ? floor + 0.5 : floor + 1.5; |
|
if (alignEnd) { |
|
n -= 1; |
|
} |
|
return n; |
|
}; |
|
|
|
const pxe = function(num) { |
|
return px(num, true); |
|
}; |
|
|
|
const ls = []; |
|
|
|
const innerLeft = px(arrowSize); |
|
const innerRight = pxe(width - arrowSize); |
|
arrowOffset = clamp(arrowOffset, innerLeft, innerRight); |
|
|
|
const innerTop = px(arrowSize); |
|
const innerBottom = pxe(height - arrowSize); |
|
|
|
const startPoint = p(innerLeft, innerTop + borderRadius); |
|
const arrowPoint = p(arrowOffset, 1); |
|
|
|
const LT = p(innerLeft, innerTop); |
|
const RT = p(innerRight, innerTop); |
|
|
|
const AOT = p(arrowOffset - arrowSize, innerTop); |
|
const RRT = p(innerRight - borderRadius, innerTop); |
|
|
|
ls.push(`M${startPoint}`); |
|
ls.push(`V${innerBottom - borderRadius}`); |
|
ls.push(`Q${p(innerLeft, innerBottom)} ${p(innerLeft + borderRadius, innerBottom)}`); |
|
ls.push(`H${innerRight - borderRadius}`); |
|
ls.push(`Q${p(innerRight, innerBottom)} ${p(innerRight, innerBottom - borderRadius)}`); |
|
ls.push(`V${innerTop + borderRadius}`); |
|
|
|
if (arrowOffset < innerLeft + arrowSize + borderRadius) { |
|
ls.push(`Q${RT} ${RRT}`); |
|
ls.push(`H${arrowOffset + arrowSize}`); |
|
ls.push(`L${arrowPoint}`); |
|
if (arrowOffset < innerLeft + arrowSize) { |
|
ls.push(`L${LT}`); |
|
ls.push(`L${startPoint}`); |
|
} else { |
|
ls.push(`L${AOT}`); |
|
ls.push(`Q${LT} ${startPoint}`); |
|
} |
|
} else if (arrowOffset > innerRight - arrowSize - borderRadius) { |
|
if (arrowOffset > innerRight - arrowSize) { |
|
ls.push(`L${RT}`); |
|
} else { |
|
ls.push(`Q${RT} ${p(arrowOffset + arrowSize, innerTop)}`); |
|
} |
|
ls.push(`L${arrowPoint}`); |
|
ls.push(`L${AOT}`); |
|
ls.push(`H${innerLeft + borderRadius}`); |
|
ls.push(`Q${LT} ${startPoint}`); |
|
} else { |
|
ls.push(`Q${RT} ${RRT}`); |
|
ls.push(`H${arrowOffset + arrowSize}`); |
|
ls.push(`L${arrowPoint}`); |
|
ls.push(`L${AOT}`); |
|
ls.push(`H${innerLeft + borderRadius}`); |
|
ls.push(`Q${LT} ${startPoint}`); |
|
} |
|
return ls.join(''); |
|
}; |
|
|
|
const getPathData = function(position, width, height, arrowOffset, arrowSize, borderRadius) { |
|
|
|
const handlers = { |
|
|
|
bottom: () => { |
|
const d = getTemplatePath(width, height, arrowOffset, arrowSize, borderRadius); |
|
return { |
|
d, |
|
transform: '' |
|
}; |
|
}, |
|
|
|
top: () => { |
|
const d = getTemplatePath(width, height, width - arrowOffset, arrowSize, borderRadius); |
|
return { |
|
d, |
|
transform: `rotate(180,${width * 0.5},${height * 0.5})` |
|
}; |
|
}, |
|
|
|
left: () => { |
|
const d = getTemplatePath(height, width, arrowOffset, arrowSize, borderRadius); |
|
const x = (width - height) * 0.5; |
|
const y = (height - width) * 0.5; |
|
return { |
|
d, |
|
transform: `translate(${x} ${y}) rotate(90,${height * 0.5},${width * 0.5})` |
|
}; |
|
}, |
|
|
|
right: () => { |
|
const d = getTemplatePath(height, width, height - arrowOffset, arrowSize, borderRadius); |
|
const x = (width - height) * 0.5; |
|
const y = (height - width) * 0.5; |
|
return { |
|
d, |
|
transform: `translate(${x} ${y}) rotate(-90,${height * 0.5},${width * 0.5})` |
|
}; |
|
} |
|
}; |
|
|
|
return handlers[position](); |
|
}; |
|
|
|
|
|
|
|
|
|
const styleCache = { |
|
|
|
|
|
|
|
|
|
|
|
}; |
|
|
|
export const getPositionStyle = (info, options = {}) => { |
|
|
|
const o = { |
|
bgColor: '#fff', |
|
borderColor: '#ccc', |
|
borderRadius: 5, |
|
arrowSize: 10 |
|
}; |
|
Object.keys(o).forEach((k) => { |
|
|
|
if (hasOwn(options, k)) { |
|
const d = o[k]; |
|
const v = options[k]; |
|
|
|
if (typeof d === 'string') { |
|
|
|
if (typeof v === 'string' && v) { |
|
o[k] = v; |
|
} |
|
} else { |
|
|
|
if (isNum(v) && v >= 0) { |
|
o[k] = v; |
|
} |
|
|
|
} |
|
|
|
} |
|
}); |
|
|
|
const key = [ |
|
info.width, |
|
info.height, |
|
info.offset, |
|
o.arrowSize, |
|
o.borderRadius, |
|
o.bgColor, |
|
o.borderColor |
|
].join('-'); |
|
|
|
const positionCache = styleCache[info.position]; |
|
if (positionCache && key === positionCache.key) { |
|
const st = positionCache.style; |
|
st.changed = styleCache.position !== info.position; |
|
styleCache.position = info.position; |
|
return st; |
|
} |
|
|
|
|
|
|
|
const data = getPathData(info.position, info.width, info.height, info.offset, o.arrowSize, o.borderRadius); |
|
|
|
|
|
const viewBox = [0, 0, info.width, info.height].join(' '); |
|
const svg = [ |
|
`<svg viewBox="${viewBox}" xmlns="http://www.w3.org/2000/svg">`, |
|
`<path d="${data.d}" fill="${o.bgColor}" stroke="${o.borderColor}" transform="${data.transform}" />`, |
|
'</svg>' |
|
].join(''); |
|
|
|
|
|
const backgroundImage = `url("data:image/svg+xml;charset=utf8,${encodeURIComponent(svg)}")`; |
|
|
|
const background = `${backgroundImage} center no-repeat`; |
|
|
|
const padding = `${o.arrowSize + o.borderRadius}px`; |
|
|
|
const style = { |
|
background, |
|
backgroundImage, |
|
padding, |
|
changed: true |
|
}; |
|
|
|
styleCache.position = info.position; |
|
styleCache[info.position] = { |
|
key, |
|
style |
|
}; |
|
|
|
return style; |
|
}; |
|
|