lu2
Version:
Simple and flexible UI component library based on native HTML and JavaScript
1,523 lines (1,319 loc) • 569 kB
JavaScript
/**
* @Keyboard.js
* @author zhangxinxu
* @version
* Created: 17-06-13
*/
(function (doc, win) {
if (win.isKeyEventBind || !doc.addEventListener) {
return {};
}
/*
** HTML accesskey辅助增强脚本
** 作用包括:
** 1. 统一IE浏览器和其它浏览器的快捷访问行为;
** 2. 增加单独accesskey属性值对应按键按下的focus行为;
** 3. windows系统下Firefox也支持 Alt + key的访问能力;
** 4. 增加shift + '?'(keyCode=191)键的提示行为支持;
*/
// 操作系统和浏览器设备检测
var ua = navigator.platform || navigator.userAgent;
var system = 'windows';
if (/mac/i.test(ua)) {
system = 'mac';
}
// 浏览器判断
var browser = 'chrome';
if (typeof doc.mozFullScreen != 'undefined') {
browser = 'moz';
} else if (typeof doc.msHidden != 'undefined' || typeof doc.hidden == 'undefined') {
browser = 'ie';
}
// 快捷键组合
var keyPrefix = ({
windows: {
ctrlKey: false,
altKey: true,
shiftKey: false
},
mac: {
ctrlKey: true,
altKey: true,
shiftKey: false
}
})[system];
// 获取字符Unicode值方法
var U = function (a, b) {
if (!a) {
return '';
}
b = b || 'x';
var c = '';
var d = 0;
var e;
for (d; d < a.length; d += 1) a.charCodeAt(d) >= 55296 && a.charCodeAt(d) <= 56319 ? (e = (65536 + 1024 * (Number(a.charCodeAt(d)) - 55296) + Number(a.charCodeAt(d + 1)) - 56320).toString(16), d += 1) : e = a.charCodeAt(d).toString(16),
c += b + e;
return c.substr(b.length);
};
// 提示当前元素快捷键的方法
var timerTips = null;
var tips = function (arrEles) {
// 已经显示中,忽略
if (doc.hasTipsShow) {
return;
}
// 页面的滚动高度
var scrollTop = doc.documentElement.scrollTop || doc.body.scrollTop;
var scrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft;
// 遍历创建提示元素
arrEles.forEach(function (ele) {
// 如果元素隐藏,也忽略
if (ele.clientHeight * ele.clientWidth == 0) {
return;
}
var accesskey = ele.getAttribute('accesskey');
var arrAccesskey = [];
for (var key in keyPrefix) {
if (keyPrefix[key]) {
arrAccesskey.push(key);
}
}
arrAccesskey.push(accesskey);
// 当前元素相对于文档的偏移
var bounding = ele.getBoundingClientRect();
// 创建tips提示元素
var div = doc.createElement('div');
div.className = 'ui-kbd-tips AK_Tips';
div.setAttribute('style', 'top:' + (bounding.top + scrollTop) + 'px;left:' + (bounding.left + scrollLeft) + 'px;');
div.innerHTML = arrAccesskey.map(function (key) {
return '<kbd>' + key.replace('Key', '') + '</kbd>';
}).join('+');
doc.body.appendChild(div);
div.fromTarget = ele;
});
// 标记,避免重复生成
doc.hasTipsShow = true;
// 一段时间隐藏
timerTips = setTimeout(function () {
removeTips();
}, 3000);
};
// 隐藏tips
var removeTips = function () {
clearTimeout(timerTips);
// 移除所有的快捷键提示
var elesTips = doc.querySelectorAll('.AK_Tips');
[].slice.call(elesTips).forEach(function (ele) {
if (ele.fromTarget) {
ele.fromTarget.hasTipsShow = null;
}
doc.body.removeChild(ele);
});
doc.hasTipsShow = null;
};
// accesskey键盘处理
doc.addEventListener('keydown', function (event) {
// 当前元素是否是可输入的input或者textarea
var isTargetInputable = false;
var eleTarget = event.target || doc.activeElement;
var tagName = eleTarget.tagName.toLowerCase();
if (tagName == 'textarea' || (tagName == 'input' && /checkbox|radio|select|file|button|image|hidden/i.test(eleTarget.type) == false)) {
isTargetInputable = true;
}
// 遍历所有设置了accesskey的符合HTML4.0.1规范的元素
// 包括<a>, <area>, <button>, <input>, <label>, <legend>以及<textarea>
var elesOwnAccesskey = doc.querySelectorAll('a[accesskey],area[accesskey],button[accesskey],input[accesskey],label[accesskey],legend[accesskey],textarea[accesskey]');
if (elesOwnAccesskey.length == 0) {
return;
}
// 对象列表转换成数组
var arrElesOwnAccesskey = [].slice.call(elesOwnAccesskey);
// 进行遍历
var arrAcceeekey = arrElesOwnAccesskey.map(function (ele) {
return ele.getAttribute('accesskey');
});
// windows下图下直接event.key就是按下的键对应的内容,但老的OS X系统却没有key属性,有的是event.keyIdentifier,表示字符的Unicode值
// 根据这个判断按键是否有对应的匹配
var indexMatch = -1;
arrAcceeekey.forEach(function (accesskey, index) {
if ((event.key && event.key == accesskey) || (event.keyIdentifier && parseInt(event.keyIdentifier.toLowerCase().replace('u+', ''), 16) == parseInt(U(accesskey), 16))) {
indexMatch = index;
return false;
}
});
// 1. 单独按下某个键的匹配支持
if (event.altKey == false && event.shiftKey == false && event.ctrlKey == false) {
if (isTargetInputable) {
return;
}
// focus高亮
if (arrElesOwnAccesskey[indexMatch]) {
// 延时目的是让后面的键盘高亮逻辑可以顺利执行
setTimeout(function () {
arrElesOwnAccesskey[indexMatch].focus();
}, 1);
// 阻止内容输入
event.preventDefault();
}
// 2. shift + '?'(keyCode=191)键的提示行为支持
} else if (event.altKey == false && event.shiftKey == true && event.ctrlKey == false) {
if (event.keyCode == 191 && !isTargetInputable) {
doc.hasTipsShow ? removeTips() : tips(arrElesOwnAccesskey);
}
} else if (arrElesOwnAccesskey[indexMatch] && !isTargetInputable && (browser == 'ie' || browser == 'moz') && event.altKey && !event.shiftKey && !event.ctrlKey) {
arrElesOwnAccesskey[indexMatch].click();
}
});
doc.addEventListener('mousedown', function () {
removeTips();
});
/*
** CSS :focus或者JS的focus事件让下拉或浮层元素显示的时候键盘交互支持
** 基于data-target属性进行关联
*/
var keycode = {
37: 'left',
38: 'up',
39: 'right',
40: 'down',
13: 'enter',
9: 'tab',
27: 'esc'
};
// 键盘高亮类名
var className = 'ui-outline';
// 高亮类名的添加与删除
var classList = {
add: function (ele) {
ele.classList.add(className);
},
remove: function (ele) {
ele.classList.remove(className);
},
removeAll: function () {
[].slice.call(doc.querySelectorAll('.' + className)).forEach(function (ele) {
classList.remove(ele);
});
},
has: function (ele) {
ele.classList.contains(className);
}
};
// 是否是键盘事件
var timerKeyEvent = null;
win.isKeyEvent = false;
doc.addEventListener('keydown', function (event) {
win.isKeyEvent = true;
clearTimeout(timerKeyEvent);
timerKeyEvent = setTimeout(function () {
win.isKeyEvent = false;
}, 100);
// 是否是上下左右键
var keyName = keycode[event.keyCode];
if (!keyName) {
return;
}
// 当前激活元素
var trigger = doc.activeElement;
if (!trigger || /body/i.test(trigger.tagName)) {
return;
}
// 单复选框的增加回车选中,原生是空格键选中
if (keyName == 'enter' && (/^radio|checkbox$/i.test(trigger.type) || trigger.getAttribute('tabindex') == '0')) {
trigger.click();
// 阻止默认的回车行为,主要是表单提交
event.preventDefault();
return;
}
// 如果是ESC退出
var eleFirstMatchAttrTarget = null;
var eleEscAll = doc.querySelectorAll('.ESC');
if (keyName == 'esc' && eleEscAll.length) {
[].slice.call(eleEscAll).forEach(function (eleEsc) {
var idEsc = eleEsc.id;
eleFirstMatchAttrTarget = idEsc && doc.querySelector('[data-target="' + idEsc + '"],[data-target2="' + idEsc + '"],ui-drop[target="' + idEsc + '"]');
if (eleFirstMatchAttrTarget && eleEsc.style.display !== 'none' && eleEsc.clientHeight > 0) {
if (eleFirstMatchAttrTarget.hide) {
eleFirstMatchAttrTarget.hide();
} else if (eleFirstMatchAttrTarget['ui-drop']) {
eleFirstMatchAttrTarget['ui-drop'].hide();
} else {
eleFirstMatchAttrTarget.click();
}
} else if (eleEsc == document.activeElement && eleEsc.click) {
eleEsc.click();
}
});
}
// 对应的面板
// 1. data-target 点击出现的面板
// 2. data-focus focus出现的面板
var attrFocus = trigger.getAttribute('data-focus');
var attrTarget = trigger.getAttribute('data-target');
var target = null;
if (!attrFocus && !attrTarget) {
return;
}
if (attrFocus) {
target = doc.getElementById(attrFocus);
} else if (attrTarget) {
target = doc.getElementById(attrTarget);
}
// 目标元素存在,并且是显示状态
if (!target || (target.clientWidth == 0 && target.clientHeight == 0)) {
return;
}
// 如果是Tab键
if (keyName == 'tab') {
classList.removeAll();
var eleFirstFocusable = target;
if (!target.getAttribute('tabindex')) {
eleFirstFocusable = target.querySelector('a[href], button:not(:disabled), input:not(:disabled)');
}
if (eleFirstFocusable) {
eleFirstFocusable.focus();
}
return;
}
// 如果是回车事件
if (keyName == 'enter') {
var eleFocus = target.querySelector('.' + className);
if (eleFocus && attrFocus) {
// 阻止默认的回车
event.preventDefault();
eleFocus.click();
return;
}
if (attrTarget) {
return;
}
}
// ESC退出
if (keyName == 'esc') {
eleFirstMatchAttrTarget = doc.querySelector('a[data-target="' + attrTarget + '"],input[data-target="' + attrTarget + '"]');
if (attrFocus) {
trigger.blur();
} else if (eleFirstMatchAttrTarget && /ESC/.test(eleFirstMatchAttrTarget.className) == false) {
eleFirstMatchAttrTarget.click();
}
return;
}
// 如果都符合,同时有目标子元素
var arrEleFocusable = [].slice.call(target.querySelectorAll('a[href], button:not(:disabled), input:not(:disabled)'));
var index = -1;
if (arrEleFocusable.length == 0) {
return;
}
// 计算出当前索引
arrEleFocusable.forEach(function (ele, indexFocus) {
if (attrFocus) {
if (classList.has(ele)) {
index = indexFocus;
}
} else if (trigger == ele) {
index = indexFocus;
}
// 先全部清除focus态
classList.remove(ele);
});
// 阻止默认的上下键滚屏
event.preventDefault();
// 索引加加减减
if (keyName == 'left' || keyName == 'up') {
index--;
if (index < 0) {
index = -1;
}
} else if (keyName == 'right' || keyName == 'down') {
index++;
if (index > arrEleFocusable.length - 1) {
index = arrEleFocusable.length;
}
}
// 如果有对应的索引元素
if (arrEleFocusable[index]) {
// 高亮对应的控件元素
if (attrFocus) {
classList.add(arrEleFocusable[index]);
} else {
arrEleFocusable[index].focus();
}
}
});
doc.addEventListener('mousedown', function (event) {
var target = event.target;
if (target && !classList.has(target)) {
classList.removeAll();
}
});
doc.addEventListener('click', function (event) {
var target = event.target;
var eleActive = doc.activeElement;
var tabindex = target.getAttribute('tabindex') || '-1';
// 单复选框点击不focus
// tabindex>=0 元素点击也不focus(避免outline出现)
// 当然,本身outline不是none
if (target && target == eleActive && (/^radio|checkbox$/i.test(eleActive.type) || tabindex >= 0) && win.isKeyEvent == false && /none/.test(getComputedStyle(target).outline) == false) {
eleActive.blur();
}
});
// 全局对a标签按钮进行无障碍角色设置
doc.addEventListener('focusin', function (event) {
var target = event.target;
if (!target) {
return;
}
// 无障碍增强
if (/^javascript/.test(target.href) && !target.getAttribute('role')) {
target.setAttribute('role', 'button');
}
if (!win.isKeyEvent) {
return;
}
var objStyleTarget = window.getComputedStyle(target);
// 键盘高亮
if (/none|auto/.test(objStyleTarget.outlineStyle) && (!event.path || event.path[0] === target)) {
classList.add(target);
}
});
doc.addEventListener('focusout', function (event) {
var target = event.target;
if (target) {
classList.remove(target);
}
});
// 防止多次重复绑定键盘事件
win.isKeyEventBind = true;
return {};
})(document, window);
/**
* @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);
}
});
};
});
/**
* @Pagination.js
* @author sunmeiye
* @version
* @Created: 20-06-07
* @edit: 20-06-07
*/
// ui tab custom HTML
class Tab extends HTMLElement {
static get observedAttributes () {
return ['open'];
}
static get defaults () {
return {
eventType: 'click',
history: false,
autoplay: 3000
};
}
constructor (trigger, options) {
super();
// 元素的获取
let eleTrigger = null;
if (typeof trigger == 'string') {
eleTrigger = document.getElementById(trigger);
} else if (typeof trigger == 'object') {
if (trigger.tagName) {
eleTrigger = trigger;
} else if (!options) {
options = trigger;
}
}
options = options || {};
if (eleTrigger) {
// is-tab处理
let strIsTab = eleTrigger.getAttribute('is-tab');
// 必须有data-name值
const strName = eleTrigger.dataset.name;
// 上一项下一项
if (strName && (strIsTab == 'prev' || strIsTab == 'next')) {
eleTrigger.addEventListener('click', () => {
const eleTabGroup = document.querySelectorAll('ui-tab[name="' + strName + '"]');
const indexOpen = [...eleTabGroup].findIndex(eleTab => {
return eleTab.open;
});
if (strIsTab == 'prev') {
(eleTabGroup[indexOpen - 1] || eleTabGroup[eleTabGroup.length - 1]).switch();
} else {
(eleTabGroup[indexOpen + 1] || eleTabGroup[0]).switch();
}
});
// 如果是定时播放,暂停
eleTrigger.addEventListener('mouseenter', () => {
document.querySelectorAll('ui-tab[name="' + strName + '"][autoplay]').forEach(eleTab => {
clearTimeout(eleTab.timer);
})
});
eleTrigger.addEventListener('mouseout', () => {
[...document.querySelectorAll('ui-tab[name="' + strName + '"][autoplay]')].some(eleTab => {
if (eleTab.open) {
eleTab.autoSwitch();
return true;
}
})
});
return;
}
let strTriggerId = eleTrigger.id;
if (!strTriggerId) {
strTriggerId = ('lulu_' + Math.random()).replace('0.', '');
eleTrigger.id = strTriggerId;
// for属性设置
this.htmlFor = strTriggerId;
}
// 参数转移
if (eleTrigger.getAttribute('name')) {
this.name = eleTrigger.getAttribute('name');
}
if (eleTrigger.hasAttribute('open')) {
this.open = true;
}
for (let paramsKey in Tab.defaults) {
if (typeof eleTrigger.dataset[paramsKey.toLowerCase()] != 'undefined') {
this[paramsKey] = eleTrigger.dataset[paramsKey.toLowerCase()];
} else if (typeof options[paramsKey] != 'undefined') {
this[paramsKey] = eleTrigger.dataset[paramsKey.toLowerCase()] = options[paramsKey];
}
}
// target值也可以使用is-tab属性设置
if (strIsTab && !eleTrigger.dataset.target) {
eleTrigger.dataset.target = strIsTab;
}
// 非控件元素的键盘访问支持
if (eleTrigger.tabIndex == -1) {
eleTrigger.setAttribute('tabindex', 0);
}
}
// 参数和元素设置的处理
let objElement = this.element || {};
// trigger和target元素的设置与获取
this.element = new Proxy(objElement, {
get: (target, prop) => {
if (prop == 'target') {
let strIdTarget = this.target;
let eleTarget = null;
let eleTrigger = this.element.trigger;
if (!strIdTarget && eleTrigger) {
strIdTarget = eleTrigger.dataset.target || eleTrigger.getAttribute('href');
if (strIdTarget && /#/.test(strIdTarget)) {
strIdTarget = strIdTarget.split('#')[1];
}
}
// 对应的目标元素获取
if (strIdTarget) {
eleTarget = document.getElementById(strIdTarget);
}
return eleTarget;
}
if (prop == 'trigger') {
return (this.htmlFor && document.getElementById(this.htmlFor)) || this;
}
return target[prop];
},
set: (target, prop, value) => {
if (typeof value == 'string') {
value = document.getElementById(value) || document.querySelector(value);
}
// 只有当value是节点元素时候才赋值
if (value && typeof value.nodeType != 'number') {
return false;
}
// 元素赋值
target[prop] = value;
// target元素设置时候同时需要赋值
if (prop == 'target' && value) {
let eleTarget = value;
let strId = eleTarget.id;
if (!strId) {
strId = ('lulu_' + Math.random()).replace('0.', '');
eleTarget.id = strId;
}
// 如果用户直接使用this.element.target赋值,则需要同步target属性值
if (this.element.trigger == this) {
// 此if判断可以避免死循环
if (this.target != strId) {
this.target = strId;
}
} else if (this.element.trigger) {
this.element.trigger.setAttribute('data-target', strId);
}
}
return true;
}
});
// 参数设置
// 在本组件中,作用较弱
this.setParams(options);
// 载入到页面
if (eleTrigger && !this.parentElement && eleTrigger != this) {
// 使用专门的div包裹,避免暴露过多的细节
let eleHidden = document.querySelector('body > div[hidden="tab"]');
if (!eleHidden) {
eleHidden = document.createElement('div');
eleHidden.setAttribute('hidden', 'tab');
document.body.append(eleHidden);
}
eleHidden.append(this);
}
}
get eventType () {
let strEventType = this.getAttribute('eventtype') || Tab.defaults.eventType;
if (strEventType == 'hover') {
strEventType = 'mouseenter';
}
return strEventType;
}
set eventType (value) {
this.setAttribute('eventtype', value);
}
get history () {
return this.hasAttribute('history') || Tab.defaults.history;
}
set history (value) {
this.toggleAttribute('history', value);
}
get autoplay () {
let strAttrAutoplay = this.getAttribute('autoplay');
if (typeof strAttrAutoplay !== 'string') {
return false;
}
if (/^\d+$/.test(strAttrAutoplay)) {
return strAttrAutoplay * 1;
}
return Tab.defaults.autoplay;
}
set autoplay (value) {
if (!value && value !== '') {
this.removeAttribute('autoplay');
} else {
this.setAttribute('autoplay', value);
}
}
get name () {
return this.getAttribute('name');
}
set name (value) {
this.setAttribute('name', value);
}
get htmlFor () {
return this.getAttribute('for');
}
set htmlFor (v) {
// 设置属性
this.setAttribute('for', v);
}
// 目标元素的设置与获取
get target () {
return this.getAttribute('target');
}
set target (value) {
this.setAttribute('target', value);
}
get open () {
return this.hasAttribute('open');
}
set open (value) {
this.toggleAttribute('open', value);
}
// 参数批量设置
setParams (options) {
this.params = this.params || {};
options = options || {};
Object.assign(this.params, options);
}
switch () {
let strName = this.name;
let eleTabGroup = [];
// 对应选项卡显示的处理
if (strName) {
eleTabGroup = document.querySelectorAll('ui-tab[name="' + strName + '"]');
if (!this.open) {
eleTabGroup.forEach(tab => {
if (tab.open) {
tab.open = false;
}
});
this.open = true;
}
} else {
this.open = !this.open;
}
const location = window.location;
// 历史记录的处理
if (this.history == true && strName && /tab\d{10,16}/.test(strName) == false) {
const strId = this.target;
// url地址查询键值对替换
const objURLParams = new URLSearchParams(location.search);
// 改变查询键值,有则替换,无则新增
objURLParams.set(strName, strId);
// hash优化,去除重复的hash
let strHash = location.hash;
if ('#' + strId == strHash) {
location.hash = strHash = '';
}
// 改变当前URL
window.history.replaceState(null, document.title, location.href.split('?')[0] + '?' + objURLParams.toString() + strHash);
}
}
// 自动切换
autoSwitch () {
// 自动播放
let numTimeAutoplay = this.autoplay;
let strName = this.name;
if (numTimeAutoplay && strName) {
let eleTabGroup = document.querySelectorAll('ui-tab[name="' + strName + '"]');
clearTimeout(this.timer);
if (numTimeAutoplay && strName && eleTabGroup.length > 1) {
let indexTab = [].slice.call(eleTabGroup).findIndex(tab => {
return tab == this;
});
indexTab++;
if (indexTab >= eleTabGroup.length) {
indexTab = 0;
}
this.timer = setTimeout(() => {
eleTabGroup[indexTab].switch();
// 自动播放
eleTabGroup[indexTab].autoSwitch();
}, numTimeAutoplay);
}
}
}
// 初始化tab事件
events () {
// hover事件的键盘访问支持
if (this.eventType == 'mouseover' || this.eventType == 'mouseenter') {
// 增加延时判断,避免误经过产生的不好体验
this.element.trigger.addEventListener(this.eventType, () => {
Tab.hoverTimer = setTimeout(() => {
this.switch();
}, 150);
}, false);
// 如果快速移出,则不执行切换
this.element.trigger.addEventListener(this.eventType.replace('over', 'out').replace('enter', 'leave'), () => {
clearTimeout(Tab.hoverTimer);
});
}
// 无论什么事件类型,都需要click事件兜底
this.element.trigger.addEventListener('click', (event) => {
if (/^(:?javas|#)/.test(event.target.getAttribute('href'))) {
event.preventDefault();
}
this.switch();
}, false);
// 如果定时播放,鼠标经过暂停
if (this.autoplay && this.name) {
[this.element.trigger, this.element.target].forEach((ele) => {
ele.addEventListener('mouseenter', () => {
clearTimeout(this.timer);
});
ele.addEventListener('mouseleave', () => {
this.autoSwitch();
});
});
// 当前open选项卡开始准备自动播放
let eleFirstOpenTab = document.querySelector('ui-tab[name="' + this.name + '"][open]');
if (eleFirstOpenTab) {
eleFirstOpenTab.autoSwitch();
}
}
}
// ui-tab元素在页面出现的时候
connectedCallback () {
if (!this.closest('a, button') && !this.querySelector('a, button')) {
this.setAttribute('tabindex', '0');
}
const eleTarget = this.element.target;
const eleTrigger = this.element.trigger;
if (eleTrigger) {
eleTrigger.setAttribute('role', 'tab');
}
if (eleTarget) {
eleTarget.setAttribute('role', 'tabpanel');
}
// 事件
this.events();
// URL查询看看能不能获取到记录的选项卡状态信息
const objURLParams = new URLSearchParams(window.location.search);
objURLParams.forEach((value, key) => {
if (eleTrigger && this.name == key && this.target == value && !eleTrigger.hasAttribute('open')) {
eleTrigger.click();
}
});
// 全局事件
this.dispatchEvent(new CustomEvent('connected', {
detail: {
type: 'ui-tab'
}
}));
this.isConnectedCallback = true;
this.dispatchEvent(new CustomEvent('DOMContentLoaded'));
// is-tab等类型初始化完毕标志事件
if (eleTrigger != this) {
eleTrigger.dispatchEvent(new CustomEvent('DOMContentLoaded'));
if (eleTrigger.hasAttribute('is-tab')) {
eleTrigger.dispatchEvent(new CustomEvent('connected', {
detail: {
type: 'ui-tab'
}
}));
// 设置定义完毕标志量
eleTrigger.setAttribute('defined', '');
}
}
}
/**
* open属性变化时候的变化
* @param {*} name
* @param {*} newValue
* @param {*} oldValue
*/
attributeChangedCallback (name, newValue, oldValue) {
if (this.element && name === 'open' && typeof newValue != typeof oldValue) {
const elePanel = this.element.target;
if (!elePanel) {
return;
}
if (this.open) {
elePanel.classList.add('active');
this.setAttribute('aria-selected', 'true');
this.dispatchEvent(new CustomEvent('show', {
detail: {
type: 'ui-tab'
}
}));
} else {
elePanel.classList.remove('active');
this.setAttribute('aria-selected', 'false');
this.dispatchEvent(new CustomEvent('hide', {
detail: {
type: 'ui-tab'
}
}));