lu2
Version:
Simple and flexible UI component library based on native HTML and JavaScript
611 lines (528 loc) • 23.6 kB
JavaScript
/**
* @Follow.js
* @author zhangxinxu
* @version
* @Created: 15-06-25
* @edited: 17-06-19
* @edited: by littleLionGuoQing: 20-05-14 ES6
* @edited: 2020-11-29 rewrite by zhangxinxu
*/
/**
* 绝对定位元素的定位效果
* 自动含边界判断
* 可用在Drop, Tips等组件上
* @example
* 文档见:https://www.zhangxinxu.com/wordpress/?p=1328 position()方法
**/
HTMLElement.prototype.follow = function (eleTarget, options) {
// 默认参数值
let defaults = {
offsets: {
x: 0,
y: 0
},
safeArea: [0, 0, 0, 0],
// eleTrigger-eleTarget
position: '4-1',
// 边缘位置自动调整
edgeAdjust: true
};
// 判断第一个参数是否是DOM元素,不是的话判断是否是对象,是对象则将第一个参数当作options
if (eleTarget && Object.prototype.toString.call(eleTarget) === '[object Object]') {
options = eleTarget;
eleTarget = null;
}
for (var keyOption in (options || (options = {}))) {
if (typeof options[keyOption] == 'undefined') {
delete options[keyOption];
}
}
const objParams = Object.assign({}, defaults, options);
// eleTarget 非必须,可 eleTrigger 元素 html 属性指定
if (!eleTarget) {
let strTarget = this.getAttribute('is-follow') || this.dataset.target;
if (!strTarget) {
return;
}
eleTarget = document.getElementById(strTarget) || document.querySelector('.' + strTarget) || document.querySelector(strTarget);
if (!eleTarget) {
return;
}
}
// 合法的位置关系数据
const arrLegalPosition = ['4-1', '1-4', '5-7', '2-3', '2-1', '6-8', '3-4', '4-3', '8-6', '1-2', '7-5', '3-2'];
// eleTrigger 元素属性指定 options,传入的 options 参数优先级更高
// offsets
let dataOffsets = this.dataset.offsets;
let arrOffsets = [];
if (objParams.offsets.map && objParams.offsets.length) {
arrOffsets = objParams.offsets;
} else if (typeof objParams.offsets == 'string') {
arrOffsets = objParams.offsets.trim().split(/,\s*|\s+/);
}
if (dataOffsets && !options.offsets) {
arrOffsets = dataOffsets.trim().split(/,\s*|\s+/);
}
// 如果arrOffsets有值
if (arrOffsets.length) {
objParams.offsets = {};
objParams.offsets.x = arrOffsets[0];
objParams.offsets.y = arrOffsets[1] || arrOffsets[0];
}
let dataOffsetX = this.dataset.offsetX;
let dataOffsetY = this.dataset.offsetY;
if (dataOffsetX) {
objParams.offsets.x = dataOffsetX;
}
if (dataOffsetY) {
objParams.offsets.y = dataOffsetY;
}
// 转数值
objParams.offsets.x *= 1;
objParams.offsets.y *= 1;
// position
let dataPosition = this.dataset.position;
let dataAlign = this.dataset.align;
// data-align是否符合合法位置关系
let isDataAlignMatch = arrLegalPosition.some((strLegalPosition) => {
return strLegalPosition === dataAlign;
});
// 若没有设置 data-position,设置了 data-align 也行,若都设置了以 data-position 的值为准
if (!dataPosition && dataAlign && isDataAlignMatch) {
dataPosition = dataAlign;
}
if (dataPosition && (!options || !options.position)) {
objParams.position = dataPosition;
}
// edge-adjust
const dataEdgeAdjust = this.dataset.edgeAdjust;
// data-edge-adjust 字符串为 0、none、false 认为是 false,其他都是 true
let isEdgeAdjust = !((dataEdgeAdjust === '0') || (dataEdgeAdjust === 'none') || (dataEdgeAdjust === 'false'));
if (typeof dataEdgeAdjust == 'string') {
objParams.edgeAdjust = isEdgeAdjust;
}
// 使用CSS锚点定位
if (CSS.supports('position-try-fallbacks', 'flip-block') && objParams.position?.includes('-')) {
this.element = this.element ? Object.assign(this.element, {
follow: eleTarget
}) : {
follow: eleTarget
};
this.params = this.params ? Object.assign(this.params, objParams) : objParams;
// 创建anchor-name
let strAnchorName = window.getComputedStyle(this).anchorName;
if (strAnchorName === 'none') {
strAnchorName = '--ui-anchor-' + (this.id || Math.random().toString(32).slice(2)).toLocaleLowerCase();
}
// 如果有id,优先使用id作为锚点名称
this.style.setProperty('anchor-name', strAnchorName);
eleTarget.style.positionAnchor = strAnchorName;
if (objParams.edgeAdjust) {
eleTarget.style.positionTryFallbacks = 'flip-block, flip-inline';
} else {
eleTarget.style.positionTryFallbacks = '';
}
// 根据arrLegalPosition设置inset-area的值
const mapInsetArea = {
'4-1': 'bottom span-right',
'1-4': 'top span-right',
'5-7': 'top',
'2-3': 'top span-left',
'2-1': 'right span-bottom',
'6-8': 'right',
'3-4': 'right span-top',
'4-3': 'left span-top',
'8-6': 'left',
'1-2': 'left span-bottom',
'7-5': 'bottom',
'3-2': 'right span-top'
};
eleTarget.style.positionArea = mapInsetArea[objParams.position] || mapInsetArea[arrLegalPosition[0]];
// 使用fixed定位
eleTarget.style.position = 'fixed';
// 设置为popover
eleTarget.setAttribute('popover', '');
eleTarget.style.inset = 'auto';
eleTarget.dataset.align = objParams.position;
if (objParams.offsets.x) {
eleTarget.style.marginInline = objParams.offsets.x + 'px';
} else {
eleTarget.style.marginInline = '';
}
if (objParams.offsets.y) {
eleTarget.style.marginBlock = objParams.offsets.y + 'px';
} else {
eleTarget.style.marginBlock = '';
}
if (eleTarget.showPopover) {
eleTarget.showPopover();
}
return;
}
// 先绝对定位,以便获取更准确的尺寸
let strOriginPosition = eleTarget.style.position;
if (strOriginPosition != 'absolute') {
eleTarget.style.position = 'absolute';
}
// 触发元素和目标元素的坐标数据
let objBoundTrigger = this.getBoundingClientRect();
let objBoundTarget = eleTarget.getBoundingClientRect();
// 如果目标元素隐藏,则不处理
if (objBoundTarget.width * objBoundTarget.height === 0) {
eleTarget.style.position = strOriginPosition || '';
window.console.warn((eleTarget.id ? 'id为' + eleTarget.id + '的' : '') + '目前元素尺寸为0,无法定位');
return;
}
// 页面的水平和垂直滚动距离
const selectorScroller = this.dataset.scroller;
// 自定义滚动容器
let customScroller = null;
// 最终的滚动容器
let scroller;
if (selectorScroller) {
customScroller = this.closest(selectorScroller) || this.closest('#' + selectorScroller);
// 滚动容器设置为自定义的滚动容器
scroller = customScroller;
// 必须是包含关系
// 否则认为是不合法
if (scroller && !scroller.contains(eleTarget)) {
scroller = null;
}
}
// 如果没有自定义滚动容器,那么就是默认的滚动容器
if (!scroller) {
scroller = document.scrollingElement || document.documentElement;
}
let numScrollTop = scroller.scrollTop;
let numScrollLeft = scroller.scrollLeft;
// 浏览器窗口的尺寸
const numWinWidth = window.innerWidth;
const numWinHeight = window.innerHeight;
// 如果trigger元素全部都在屏幕外,则不进行边缘调整
if ((objBoundTrigger.left < 0 && objBoundTrigger.right < 0) || (objBoundTrigger.top < 0 && objBoundTrigger.bottom < 0) || (objBoundTrigger.left > numWinWidth && objBoundTrigger.right > numWinWidth) || (objBoundTrigger.top > numWinHeight && objBoundTrigger.bottom > numWinHeight)) {
objParams.edgeAdjust = isEdgeAdjust = false;
}
// target的包含块祖先元素,也就是定位元素
const eleOffsetParent = eleTarget.offsetParent || scroller;
const objBoundOffsetParent = eleOffsetParent.getBoundingClientRect();
// 暴露给实例
const element = {
follow: eleTarget
};
this.element = this.element ? Object.assign(this.element, element) : element;
this.params = this.params ? Object.assign(this.params, objParams) : objParams;
// 参数中设置的偏移位置
let objOffsets = objParams.offsets;
// target元素所在的offset偏移
let numOffsetTop = objBoundOffsetParent.top + numScrollTop;
let numOffsetLeft = objBoundOffsetParent.left + numScrollLeft;
// 如果是body元素,同时没有设置定位属性的话,忽略
// 因为此时margin有值或者margin重叠时候会有定位bug
if (eleOffsetParent === document.body && window.getComputedStyle(eleOffsetParent).position === 'static') {
numOffsetTop = 0;
numOffsetLeft = 0;
} else if (eleOffsetParent == customScroller && customScroller) {
// 如果触发按钮和浮层使用同一个自定义的滚动容器,则忽略滚动容器的滚动距离
numOffsetTop = objBoundOffsetParent.top;
numOffsetLeft = objBoundOffsetParent.left;
}
// 直接嫁接在offsets对象上,可以大大简化后续处理的逻辑
// 减去包含块元素的偏移位置,这样的objOffsets尺寸是精准的定位尺寸
// objOffsets.x -= numOffsetLeft;
// objOffsets.y -= numOffsetTop;
// 这是指定位置
// 支持具体坐标值
let strPosition = objParams.position;
// 最终定位的left/top坐标
let numTargetLeft, numTargetTop;
// eleTarget元素zIndex实时最大化
const zIndex = function () {
// 返回eleTarget才是的样式计算对象
let objStyleTarget = window.getComputedStyle(eleTarget);
// 此时元素的层级
let numZIndexTarget = Number(objStyleTarget.zIndex);
// 用来对比的层级,也是最小层级
let numZIndexNew = 19;
// 只对同级子元素进行层级最大化计算处理
eleOffsetParent.childNodes.forEach((eleChild) => {
if (eleChild.nodeType !== 1) return;
let objStyleChild = window.getComputedStyle(eleChild);
let numZIndexChild = objStyleChild.zIndex * 1;
if (numZIndexChild && eleTarget !== eleChild && objStyleChild.display !== 'none') {
numZIndexNew = Math.max(numZIndexChild + 1, numZIndexNew);
}
});
if (numZIndexNew !== numZIndexTarget) {
eleTarget.style.zIndex = numZIndexNew;
}
};
// 如果直接指定了坐标
if (typeof strPosition !== 'string' && strPosition.length === 2) {
let arrPosition = strPosition;
numTargetLeft = arrPosition[0] + objOffsets.x;
numTargetTop = arrPosition[1] + objOffsets.y;
// 边缘检测
if (objParams.edgeAdjust === true) {
if (numTargetLeft + objBoundTarget.width > numWinWidth + numScrollLeft) {
numTargetLeft = numWinWidth + numScrollLeft - objBoundTarget.width - objOffsets.x;
}
if (numTargetTop + objBoundTarget.height > numWinHeight + numScrollTop) {
numTargetTop = numWinHeight + numScrollTop - objBoundTarget.height - objOffsets.y;
}
}
// 浮动框的定位与显示
eleTarget.style.left = `${ numTargetLeft }px`;
eleTarget.style.top = `${ numTargetTop }px`;
// 记住定位标识码
eleTarget.dataset.align = '3-1';
// z-index自动最高
zIndex();
return;
}
// 是否对齐匹配的标志量
// 遍历,以确定设定的对齐是否有匹配
let isAlignMatch = arrLegalPosition.some((strLegalPosition) => {
return strLegalPosition === strPosition;
});
// 如果没有匹配的对齐方式,使用默认的对齐方式
if (isAlignMatch === false) {
strPosition = defaults.position;
}
// 自动调整距离边缘的安全距离
let arrSafeArea = this.dataset.safeArea || getComputedStyle(eleTarget).getPropertyValue('--safe-area') || objParams.safeArea;
// 字符串转数组
if (typeof arrSafeArea == 'string') {
arrSafeArea = arrSafeArea.trim().split(/(?:,\s*|\s+)/);
}
arrSafeArea = arrSafeArea.map(function (val) {
return parseFloat(val) || 0;
});
// 数量的处理
if (arrSafeArea.length == 1) {
arrSafeArea = arrSafeArea.concat(arrSafeArea[0], arrSafeArea[0], arrSafeArea[0]);
} else if (arrSafeArea.length == 2) {
arrSafeArea.push(arrSafeArea[0]);
arrSafeArea.push(arrSafeArea[1]);
} else if (arrSafeArea.length == 3) {
arrSafeArea.push(arrSafeArea[1]);
}
// 是否超出边界的判断
// 只考虑在视区的情况,页面的滚动距离不处理
const objIsOverflow = {
// 键使用trigger-target方位表示
// 例如'left-right'表示trigger元素的左边缘和target元素右边缘对齐时候是否溢出
'left-right': objBoundTarget.width + objOffsets.x + arrSafeArea[3] > objBoundTrigger.left,
'top-bottom': objBoundTrigger.top - (objBoundTarget.height + objOffsets.y + arrSafeArea[0]) < 0,
'right-left': objBoundTrigger.right + objBoundTarget.width + objOffsets.x + arrSafeArea[1] > numWinWidth,
'bottom-top': objBoundTrigger.bottom + objBoundTarget.height + objOffsets.y + arrSafeArea[2] > numWinHeight,
// 新增4个方位
'right-right': objBoundTarget.width + objOffsets.x + arrSafeArea[3] > objBoundTrigger.right,
'left-left': objBoundTrigger.left + objBoundTarget.width + objOffsets.x + arrSafeArea[1] > numWinWidth,
'bottom-bottom': objBoundTarget.height + objOffsets.y + arrSafeArea[0] > objBoundTrigger.bottom,
'top-top': objBoundTrigger.top + objBoundTarget.height + objOffsets.y + arrSafeArea[2] > numWinHeight
};
let strDirection = 'bottom';
var funGetPosition = () => {
// 定位的处理
// 有别于之前的逻辑
// 直接枚举处理,覆盖所有的情况,之前是方位调整比较粗放
switch (strPosition) {
case '1-4': case '5-7': case '2-3': {
// 如果在上方显示
// top坐标是确定的
numTargetTop = objBoundTrigger.top - objBoundTarget.height;
// left坐标确定
if (strPosition === '1-4') {
numTargetLeft = objBoundTrigger.left;
} else if (strPosition === '5-7') {
numTargetLeft = objBoundTrigger.left - (objBoundTarget.width - objBoundTrigger.width) / 2;
} else {
numTargetLeft = objBoundTrigger.left - (objBoundTarget.width - objBoundTrigger.width);
}
strDirection = 'top';
// 如果上方超出,则看看下方有没有空间
if (isEdgeAdjust && objIsOverflow['top-bottom']) {
if (!objIsOverflow['bottom-top']) {
strPosition = ({
'1-4': '4-1',
'5-7': '7-5',
'2-3': '3-2'
})[strPosition];
// 再执行一次
funGetPosition();
} else if (!objIsOverflow['left-right'] || !objIsOverflow['right-left']) {
// 上下无空间,但是左侧或右侧有空间
// 随便给个水平方向就好
strPosition = ({
'1-4': '2-1',
'5-7': '6-8',
'2-3': '3-4'
})[strPosition];
funGetPosition();
}
}
break;
}
case '2-1': case '6-8': case '3-4': {
// left坐标固定
numTargetLeft = objBoundTrigger.right;
// top坐标确定
if (strPosition === '2-1') {
numTargetTop = objBoundTrigger.top;
} else if (strPosition === '6-8') {
numTargetTop = objBoundTrigger.top - (objBoundTarget.height - objBoundTrigger.height) / 2;
} else {
numTargetTop = objBoundTrigger.top - (objBoundTarget.height - objBoundTrigger.height);
}
strDirection = 'right';
// 如果右侧超出,则看看左方有没有空间
if (isEdgeAdjust && objIsOverflow['right-left']) {
if (!objIsOverflow['left-right']) {
strPosition = ({
'2-1': '1-2',
'6-8': '8-6',
'3-4': '4-3'
})[strPosition];
// 再执行一次
funGetPosition();
} else if (!objIsOverflow['top-bottom'] || !objIsOverflow['bottom-top']) {
strPosition = ({
'2-1': '1-4',
'6-8': '5-7',
'3-4': '2-3'
})[strPosition];
// 再执行一次
funGetPosition();
}
}
break;
}
case '4-1': case '7-5': case '3-2': {
// top坐标是确定的
numTargetTop = objBoundTrigger.bottom;
// left坐标确定
if (strPosition === '4-1') {
numTargetLeft = objBoundTrigger.left;
} else if (strPosition === '7-5') {
numTargetLeft = objBoundTrigger.left - (objBoundTarget.width - objBoundTrigger.width) / 2;
} else {
numTargetLeft = objBoundTrigger.left - (objBoundTarget.width - objBoundTrigger.width);
}
strDirection = 'bottom';
// 如果下方超出,则看看上方有没有空间
if (isEdgeAdjust && objIsOverflow['bottom-top']) {
if (!objIsOverflow['top-bottom']) {
strPosition = ({
'4-1': '1-4',
'7-5': '5-7',
'3-2': '2-3'
})[strPosition];
// 再执行一次
funGetPosition();
} else if (!objIsOverflow['left-right'] || !objIsOverflow['right-left']) {
// 上下无空间,但是左侧或右侧有空间
// 随便给个水平方向就好
strPosition = ({
'4-1': '2-1',
'7-5': '6-8',
'3-2': '3-4'
})[strPosition];
funGetPosition();
}
}
break;
}
case '1-2': case '8-6': case '4-3': {
// left坐标固定
numTargetLeft = objBoundTrigger.left - objBoundTarget.width;
// top坐标确定
if (strPosition === '1-2') {
numTargetTop = objBoundTrigger.top;
} else if (strPosition === '8-6') {
numTargetTop = objBoundTrigger.top - (objBoundTarget.height - objBoundTrigger.height) / 2;
} else {
numTargetTop = objBoundTrigger.top - (objBoundTarget.height - objBoundTrigger.height);
}
strDirection = 'left';
// 如果左侧超出,则看看右侧有没有空间
if (isEdgeAdjust && objIsOverflow['left-right']) {
if (!objIsOverflow['right-left']) {
strPosition = ({
'1-2': '2-1',
'8-6': '6-8',
'4-3': '3-4'
})[strPosition];
// 再执行一次
funGetPosition();
} else if (!objIsOverflow['top-bottom'] || !objIsOverflow['bottom-top']) {
strPosition = ({
'1-2': '1-4',
'8-6': '5-7',
'4-3': '2-3'
})[strPosition];
// 再执行一次
funGetPosition();
}
}
break;
}
}
};
funGetPosition();
numTargetLeft = numTargetLeft + objOffsets.x - numOffsetLeft;
numTargetTop = numTargetTop + objOffsets.y - numOffsetTop;
// 边界溢出,当前方位的安全举例处理
if (isEdgeAdjust) {
// 水平方向的微调
if (strDirection == 'top') {
numTargetTop = numTargetTop - arrSafeArea[2];
} else if (strDirection == 'bottom') {
numTargetTop = numTargetTop + arrSafeArea[0];
} else if (strDirection == 'left') {
numTargetLeft = numTargetLeft - arrSafeArea[1];
} else {
numTargetLeft = numTargetLeft + arrSafeArea[3];
}
}
// 加上滚动距离
numTargetTop += numScrollTop;
numTargetLeft += numScrollLeft;
//浮动框显示
eleTarget.style.left = `${ Math.round(numTargetLeft) }px`;
eleTarget.style.top = `${ Math.round(numTargetTop) }px`;
// // 此时的eleTarget位置
objBoundTarget = eleTarget.getBoundingClientRect();
// 对立分享水平方向的微调
if (isEdgeAdjust) {
if (strDirection == 'top' || strDirection == 'bottom') {
if (objBoundTarget.left < arrSafeArea[3]) {
numTargetLeft = numTargetLeft + (arrSafeArea[3] - objBoundTarget.left);
} else if (objBoundTarget.right + arrSafeArea[1] > numWinWidth) {
numTargetLeft = numTargetLeft - (objBoundTarget.right + arrSafeArea[1] - numWinWidth);
}
} else if (objBoundTarget.top < arrSafeArea[0]) {
numTargetTop += arrSafeArea[0] - objBoundTarget.top;
} else if (objBoundTarget.bottom + arrSafeArea[2] > numWinHeight) {
numTargetTop -= (objBoundTarget.bottom + arrSafeArea[2] - numWinHeight);
}
//浮动框显示
eleTarget.style.left = `${ Math.round(numTargetLeft) }px`;
eleTarget.style.top = `${ Math.round(numTargetTop) }px`;
}
eleTarget.dataset.align = strPosition;
eleTarget.dataset.direction = strDirection;
// z-index自动最高
zIndex();
if (!eleTarget.zIndex) {
eleTarget.zIndex = zIndex;
}
};
[NodeList.prototype, HTMLCollection.prototype].forEach(prop => {
prop.follow = function () {
[...this].forEach(node => {
if (node.nodeType === 1) {
node.follow.apply(node, this.arguments);
}
});
};
});