lu2
Version:
Simple and flexible UI component library based on native HTML and JavaScript
1,512 lines (1,299 loc) • 490 kB
JavaScript
/**
* @Keyboard.js
* @author zhangxinxu
* @version
* Created: 17-06-13
*/
(function (global, factory) {
factory();
// eslint-disable-next-line
}((typeof global !== 'undefined') ? global
// eslint-disable-next-line
: ((typeof window !== 'undefined') ? window
: ((typeof self !== 'undefined') ? self : this)), function () {
var doc = document;
var win = window;
if (win.isKeyEventBind || !doc.addEventListener) {
return {};
}
/*
** HTML accesskey辅助增强脚本
** 作用包括:
** 1. 统一IE浏览器和其它浏览器的快捷访问行为;
** 2. 增加单独accesskey属性值对应按键按下的focus行为;
** 3. 增加accesskey生效的前置键按下的提示行为,例如window上Alt键提示,Mac下的Alt + control组合提示等;
** 4. 增加shift + '?'(keyCode=191)键的提示行为支持;
*/
// 操作系统和浏览器设备检测
var ua = navigator.userAgent;
var system = 'windows';
if (/Mac\sOS\sX/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: browser == 'moz' ? {
ctrlKey: false,
altKey: true,
shiftKey: true
} : {
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;
};
// IE9+
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.className = ele.className + ' ' + className;
},
remove: function (ele) {
if (ele.className && ele.className.split) {
ele.className = ele.className.split(/\s+/).filter(function (cl) {
if (cl != className) {
return cl;
}
}).join(' ');
}
},
removeAll: function () {
[].slice.call(doc.querySelectorAll('.' + className)).forEach(function (ele) {
classList.remove(ele);
});
},
has: function (ele) {
if (!ele.className || !ele.className.split) {
return false;
}
return ele.className.split(/\s+/).filter(function (cl) {
if (cl == className) {
return cl;
}
}).length > 0;
}
};
win.isKeyEvent = false;
//键盘事件
doc.addEventListener('keydown', function (event) {
win.isKeyEvent = true;
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 (/^radio|checkbox$/i.test(trigger.type) && keyName == 'enter') {
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('a[data-target="' + idEsc + '"],input[data-target="' + idEsc + '"],button[data-target="' + idEsc + '"]');
if (eleFirstMatchAttrTarget && eleEsc.style.display !== 'none' && eleEsc.clientHeight > 0) {
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出现)
if (target && target == eleActive && (/^radio|checkbox$/i.test(eleActive.type) || tabindex >= 0) && win.isKeyEvent == 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 (objStyleTarget.outlineStyle == 'none') {
classList.add(target);
}
});
doc.addEventListener('focusout', function (event) {
var target = event.target;
if (target) {
classList.remove(target);
}
});
// 防止多次重复绑定键盘事件
win.isKeyEventBind = true;
return {};
}));
/**
* @Follow.js
* @author zhangxinxu
* @version
* @Created: 15-06-25
* @edited: 17-06-19
*/
(function (global, factory) {
global.Follow = factory();
// eslint-disable-next-line
}((typeof global !== 'undefined') ? global
// eslint-disable-next-line
: ((typeof window !== 'undefined') ? window
: ((typeof self !== 'undefined') ? self : this)), function () {
/**
* 绝对定位元素的定位效果
* 针对所有浏览器
* 自动含边界判断
* 可用在DropDown, Tips等组件上
* 支持链式调用和模块化调用
* @example
* new Follow(eleTrigger, eleTarget, options);
* 文档见:https://www.zhangxinxu.com/wordpress/?p=1328 position()方法
**/
var Follow = function (eleTrigger, eleTarget, options) {
// 默认参数值
var 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];
}
}
var objParams = Object.assign({}, defaults, options);
// eleTarget 非必须,可 eleTrigger 元素 html 属性指定
if (!eleTarget) {
var strTarget = eleTrigger.getAttribute('is-follow') || eleTrigger.dataset.target;
if (!strTarget) {
return;
}
eleTarget = document.getElementById(strTarget) || document.querySelector('.' + strTarget) || document.querySelector(strTarget);
if (!eleTarget) {
return;
}
}
// 合法的位置关系数据
var 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
var dataOffsets = eleTrigger.dataset.offsets;
var 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];
}
var dataOffsetX = eleTrigger.dataset.offsetX;
var dataOffsetY = eleTrigger.dataset.offsetY;
if (dataOffsetX) {
objParams.offsets.x = dataOffsetX;
}
if (dataOffsetY) {
objParams.offsets.y = dataOffsetY;
}
// 转数值
objParams.offsets.x *= 1;
objParams.offsets.y *= 1;
// position
var dataPosition = eleTrigger.dataset.position;
var dataAlign = eleTrigger.dataset.align;
// data-align是否符合合法位置关系
var isDataAlignMatch = arrLegalPosition.some(function (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
var dataEdgeAdjust = eleTrigger.dataset.edgeAdjust || objParams.edgeAdjust;
// data-edge-adjust 字符串为 0、none、false 认为是 false,其他都是 true
var isEdgeAdjust = !((dataEdgeAdjust === '0') || (dataEdgeAdjust === 'none') || (dataEdgeAdjust === 'false') || (dataEdgeAdjust === false));
if (typeof dataEdgeAdjust == 'string' && typeof objParams.edgeAdjust != 'boolean') {
objParams.edgeAdjust = isEdgeAdjust;
}
// 先绝对定位,以便获取更准确的尺寸
var strOriginPosition = eleTarget.style.position;
if (strOriginPosition != 'absolute') {
eleTarget.style.position = 'absolute';
}
// 触发元素和目标元素的坐标数据
var objBoundTrigger = eleTrigger.getBoundingClientRect();
var objBoundTarget = eleTarget.getBoundingClientRect();
// 如果目标元素隐藏,则不处理
if (objBoundTarget.width * objBoundTarget.height === 0) {
eleTarget.style.position = strOriginPosition || '';
window.console.warn((eleTarget.id ? 'id为' + eleTarget.id + '的' : '') + '目前元素尺寸为0,无法定位');
return;
}
// 页面的水平和垂直滚动距离
var numScrollTop = window.pageYOffset;
var numScrollLeft = window.pageXOffset;
// 浏览器窗口的尺寸
var numWinWidth = window.innerWidth;
var 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的包含块祖先元素,也就是定位元素
var eleOffsetParent = eleTarget.offsetParent;
var objBoundOffsetParent = eleOffsetParent.getBoundingClientRect();
// 暴露给实例
var element = {
follow: eleTarget,
target: eleTarget,
trigger: eleTrigger
};
this.element = this.element ? Object.assign(this.element, element) : element;
this.params = this.params ? Object.assign(this.params, objParams) : objParams;
// 参数中设置的偏移位置
var objOffsets = objParams.offsets;
// target元素所在的offset偏移
var numOffsetTop = objBoundOffsetParent.top + numScrollTop;
var numOffsetLeft = objBoundOffsetParent.left + numScrollLeft;
// 如果是body元素,同时没有设置定位属性的话,忽略
// 因为此时margin有值或者margin重叠时候会有定位bug
if (eleOffsetParent === document.body && window.getComputedStyle(eleOffsetParent).position === 'static') {
numOffsetTop = 0;
numOffsetLeft = 0;
}
// 直接嫁接在offsets对象上,可以大大简化后续处理的逻辑
// 减去包含块元素的偏移位置,这样的objOffsets尺寸是精准的定位尺寸
// objOffsets.x -= numOffsetLeft;
// objOffsets.y -= numOffsetTop;
// 这是指定位置
// 支持具体坐标值
var strPosition = objParams.position;
// 最终定位的left/top坐标
var numTargetLeft, numTargetTop;
// 如果直接指定了坐标
if (typeof strPosition !== 'string' && strPosition.length === 2) {
var 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.setAttribute('data-align', '3-1');
// z-index自动最高
this.zIndex();
return;
}
// 是否对齐匹配的标志量
// 遍历,以确定设定的对齐是否有匹配
var isAlignMatch = arrLegalPosition.some(function (strLegalPosition) {
return strLegalPosition === strPosition;
});
// 如果没有匹配的对齐方式,使用默认的对齐方式
if (isAlignMatch === false) {
strPosition = defaults.position;
}
// 自动调整距离边缘的安全距离
var arrSafeArea = eleTrigger.dataset.safeArea || 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]);
}
// 是否超出边界的判断
// 只考虑在视区的情况,页面的滚动距离不处理
var 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
};
var strDirection = 'bottom';
var funGetPosition = function () {
// 定位的处理
// 有别于之前的逻辑
// 直接枚举处理,覆盖所有的情况,之前是方位调整比较粗放
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.setAttribute('data-align', strPosition);
eleTarget.setAttribute('data-direction', strDirection);
// z-index自动最高
this.zIndex();
};
/**
* eleTarget元素zIndex实时最大化
* @return {[type]} [description]
*/
Follow.prototype.zIndex = function () {
var eleTarget = this.element.target;
// 返回eleTarget才是的样式计算对象
var objStyleTarget = window.getComputedStyle(eleTarget);
// 此时元素的层级
var numZIndexTarget = objStyleTarget.zIndex;
// 用来对比的层级,也是最小层级
var numZIndexNew = 19;
// 只对<body>子元素进行层级最大化计算处理
document.body.childNodes.forEach(function (eleChild) {
if (eleChild.nodeType != 1) {
return;
}
var objStyleChild = window.getComputedStyle(eleChild);
var numZIndexChild = objStyleChild.zIndex * 1;
if (numZIndexChild && eleTarget != eleChild && objStyleChild.display != 'none') {
numZIndexNew = Math.max(numZIndexChild + 1, numZIndexNew);
}
});
if (numZIndexNew != numZIndexTarget) {
eleTarget.style.zIndex = numZIndexNew;
}
};
return Follow;
}));
/**
* @Tab.js
* @author zhangxinxu
* @version
* @created: 15-06-12
* @edit: 19-09-21 native js rewrite by lennonover
* @review 19-09-25 by zhangxinxu
*/
(function (global, factory) {
global.Tab = factory();
// eslint-disable-next-line
}((typeof global !== 'undefined') ? global
// eslint-disable-next-line
: ((typeof window !== 'undefined') ? window
: ((typeof self !== 'undefined') ? self : this)), function () {
/**
* 可定制的极简选项卡切换效果
* 状态类名使用.active
* @使用示例
* new Tab(document.querySelectorAll('#tabView > a'), {
* onSwitch: function() {}
* });
*/
var STATE = 'active';
var CL = {
add: function () {
return ['ui-tab'].concat([].slice.call(arguments)).join('-');
},
toString: function () {
return 'ui-tab';
}
};
var Tab = function (elements, options) {
// 元素的获取与判断
var eleTabs = elements;
// 如果元素没有,打道回府
if (!eleTabs) {
return;
}
if (typeof elements == 'string') {
eleTabs = document.querySelectorAll(elements);
}
// 参数获取
options = options || {};
if (typeof options == 'function') {
options = {
onSwitch: options
};
}
var defaults = {
// 事件名称,还可以是mouseenter或者mouseover
eventType: 'click',
// 'auto' 表示根据选项卡上的状态类名获取index值
index: 'auto',
// 是否使用HTML5 history在URL上标记当前选项卡位置,for IE10+
history: true,
// tab 切换时 滑动效果
slide: false,
// 自动播放,0 或 false 表示不自动播放,单位毫秒
autoplay: 0,
// 发生状态变化时候的回调
onSwitch: function () {}
};
// 参数合并
var objParams = Object.assign({}, defaults, options);
// 很重要的索引
var numIndexTab = objParams.index;
// 所有对应的面板元素
var elePanels = [];
// 首先,获得索引值
eleTabs.forEach(function (eleTab, index) {
if (typeof numIndexTab != 'number' && eleTab.classList.contains(STATE)) {
numIndexTab = index;
}
eleTab.setAttribute('data-index', index);
eleTab.setAttribute('role', 'tab');
eleTab.setAttribute('aria-selected', 'false');
// 乘机获取所有对应的面板元素
var elePanel = this.getPanel(eleTab);
elePanels.push(elePanel);
if (elePanel) {
elePanel.setAttribute('role', 'tabpanel');
}
}.bind(this));
// 如果没有匹配的选项卡索引,认为第一个选中
if (typeof numIndexTab != 'number') {
numIndexTab = 0;
}
// 暴露元素
this.element = {
tabs: eleTabs,
tab: eleTabs[numIndexTab],
panels: elePanels,
panel: elePanels[numIndexTab]
};
// 参数暴露
objParams.index = numIndexTab;
this.params = objParams;
// 回调暴露
this.callback = {
switch: objParams.onSwitch
};
// 事件绑定
this.events();
// 默认就来一发
// 以便回调可以触发
// 为了等业务事件绑定,这里的触发设置在DOM准备结束之后
window.addEventListener('DOMContentLoaded', function () {
eleTabs[numIndexTab].dispatchEvent(new CustomEvent(objParams.eventType));
objParams.onSwitch.call(this, eleTabs[numIndexTab], elePanels[numIndexTab], null, null);
}.bind(this));
// 设置父元素的无障碍访问信息
if (eleTabs.length > 1 && eleTabs[0].parentElement == eleTabs[1].parentElement) {
eleTabs[0].parentElement.setAttribute('role', 'tablist');
}
};
/**
* 基于tab元素获取panel元素的方法
* @param {[type]} eleTab [description]
* @return {[type]} [description]
*/
Tab.prototype.getPanel = function (eleTab) {
if (!eleTab) {
eleTab = this.element.tab;
}
// 获得属性对应的值
var strAttr = eleTab.getAttribute('data-rel') || eleTab.getAttribute('href');
if (!strAttr) {
return null;
}
// 如果是href的锚点匹配
if (/^#/.test(strAttr)) {
return document.querySelector(strAttr);
}
// 如果是data-rel设置
return document.querySelector('#' + strAttr) || document.querySelector('.' + strAttr);
};
/**
* 播放的处理
* @return {[type]} [description]
*/
Tab.prototype.show = function (eleTab) {
// 一些必须的暴露的参数
var numIndexTab = this.index;
var eleTabs = this.element.tabs;
var objParams = this.params;
// 对应选项卡显示的处理
eleTab = eleTab || this.element.tab;
if (!eleTab) {
return;
}
var elePanel = this.getPanel(eleTab);
var eleParent = eleTab.parentElement;
// 改变当前选中tab与panel元素
this.element.tab = eleTab;
this.element.panel = elePanel;
var strHref = eleTab.getAttribute('href');
// 需要非选中状态才行执行切换
if (eleTab.classList.contains(STATE)) {
return;
}
// 选中元素的索引值
numIndexTab = eleParent.querySelector('.' + STATE).getAttribute('data-index');
// 选项卡样式变变变
// 类名变化
eleTab.classList.add(STATE);
// 要移除类名的选项卡和选项面板
var eleResetTab = eleTabs[numIndexTab];
var eleResetPanel = this.getPanel(eleResetTab);
// 类名移除
eleResetTab.classList.remove(STATE);
// 面板的变化
elePanel.classList.add(STATE);
eleResetPanel.classList.remove(STATE);
// 索引参数变化
numIndexTab = eleTab.getAttribute('data-index');
this.params.index = numIndexTab * 1;
// line 滑动效果 IE10+
var eleLine;
// 需要参数配合
if (objParams.slide == true && history.pushState) {
eleLine = eleParent.querySelector('.' + CL.add('line'));
if (!eleLine) {
eleLine = document.createElement('i');
eleLine.className = CL.add('line');
eleParent.insertBefore(eleLine, eleTabs[0]);
}
eleLine.style.display = 'block';
eleLine.style.width = eleTab.clientWidth + 'px';
eleLine.style.left = eleTab.offsetLeft + 'px';
}
// 回调方法
this.callback.switch.call(this, eleTab, eleResetTab, elePanel, eleResetPanel);
// 历史记录的处理
if (/^(:?javas|#)/.test(strHref)) {
var strAttr = (eleTab.getAttribute('data-rel') || strHref || '').replace(/^#/, '');
if (objParams.history == true) {
// url地址查询键值对替换
var objURLParams = new URLSearchParams(location.search);
// 改变查询键值,有则替换,无则新增
objURLParams.set('tab', strAttr);
// hash优化,去除重复的hash
var strHash = location.hash;
if (strHref == strHash) {
location.hash = strHash = '';
}
// 改变当前URL
if (history.replaceState) {
history.replaceState(null, document.title, location.href.split('?')[0] + '?' + objURLParams.toString() + strHash);
} else {
location.replace('#tab=' + strAttr);
}
}
}
};
/**
* 相关事件
* @return {[type]} [description]
*/
Tab.prototype.events = function () {
// 获得选项卡对应的面板元素
// 各个元素
var objElement = this.element;
var eleTabs = objElement.tabs;
var objParams = this.params;
// 单个监听事件,综合考虑不做委托
eleTabs.forEach(function (eleTab) {
eleTab.addEventListener(objParams.eventType, function (event) {
if (/^(:?javas|#)/.test(event.target.getAttribute('href'))) {
event.preventDefault();
}
this.show(event.target);
}.bind(this), false);
}.bind(this));
// 定时播放
if (objParams.autoplay && objParams.autoplay > 0) {
this.timerSlide = setInterval(function () {
this.show(eleTabs[this.params.index + 1] || eleTabs[0]);
}.bind(this), objParams.autoplay);
// 鼠标经过停止自动播放
eleTabs.forEach(function (eleTab) {
var elePanel = this.getPanel(eleTab);
[eleTab, elePanel].forEach(function (eleTarget) {
if (!eleTarget) {
return;
}
eleTarget.addEventListener('mouseenter', function () {
clearInterval(this.timerSlide);
}.bind(this), false);
eleTarget.addEventListener('mouseleave', function () {
this.timerSlide = setInterval(function () {
this.show(eleTabs[this.params.index + 1] || eleTabs[0]);
}.bind(this), objParams.autoplay);
}.bind(this), false);
}.bind(this));
}.bind(this));
}
};
return Tab;
}));
/**
* @Select.js
* @author zhangxinxu
* @version
* @Created: 15-06-18
* @edit: 07-06-15 rewrite
* @edit: 09-08-28 native js rewrite
* @edit: 21-12-02 append to body for re-position
**/
/* global module, global */
(function (global, factory) {
global.Select = factory();
// eslint-disable-next-line
}((typeof global !== 'undefined') ? global
// eslint-disable-next-line
: ((typeof window !== 'undefined') ? window
: ((typeof self !== 'undefined') ? self : this)), function () {
/**
* 模拟下拉框效果
* 针对所有浏览器进行处理
* 基于原生的<select>元素生成
*
*/
// 常量变量
var SELECT = 'select';
var SELECTED = 'selected';
var DISABLED = 'disabled';
var ACTIVE = 'active';
var REVERSE = 'reverse';
var MULTIPLE = 'multiple';
// 样式类名统一处理
var CL = {
add: function () {
return ['ui', SELECT].concat([].slice.call(arguments)).join('-');
},
toString: function () {
return 'ui-' + SELECT;
}
};
/**
* 基于原生下拉框生成的下拉组件
* @param {Object} element 原生select元素
*/
var Select = function (element) {
if (!element) {
return this;
}
var eleSelect = element;
// 避免重复初始化
if (eleSelect.data && eleSelect.data.select) {
return;
}
var strAttrWidth = eleSelect.style.width || eleSelect.getAttribute('width');
if (!strAttrWidth) {
strAttrWidth = eleSelect.offsetWidth;
}
this.offsetWidth = strAttrWidth;
// 构造元素
// 1. 得到关联id
var strId = eleSelect.id;
if (!strId) {
strId = ('lulu_' + Math.random()).replace('0.', '');
} else {
strId = 'lulu_' + strId;
}
this.id = strId;
// 2. 是否多选
var isMultiple = (typeof eleSelect.getAttribute(MULTIPLE) == 'string');
this.multiple = isMultiple;
// 3. 创建下拉组合框元素
var eleCombobox = document.createElement('div');
// 4. 创建下拉点击按钮元素
var eleButton = document.createElement('a');
if (!eleSelect[DISABLED]) {
eleButton.setAttribute('href', 'javascript:');
}
eleButton.setAttribute('data-target', strId);
// 下面3个aria无障碍访问需要
eleButton.setAttribute('role', 'button');
eleButton.setAttribute('aria-expanded', 'false');
eleButton.setAttribute('aria-owns', strId);
// 样式类名
eleButton.classList.add(CL.add('button'));
// 5. 创建下拉列表元素
var eleDatalist = document.createElement('ui-select-list');
eleDatalist.id = strId;
eleDatalist.setAttribute('role', 'listbox');
eleDatalist.setAttribute('aria-expanded', 'true');
eleDatalist.classList.add(CL.add('datalist'));
// 6. 元素组装
// multiple没有button
if (isMultiple == false) {
eleCombobox.appendChild(eleButton);
// 插入到下拉框的后面
eleSelect.style.display = 'none';
eleSelect.insertAdjacentElement('afterend', eleCombobox);
} else {
eleCombobox.appendChild(eleDatalist);
// 绝对定位隐藏,以便可以响应键盘focus
eleSelect.style.position = 'absolute';
eleSelect.style.zIndex = 1;
eleSelect.insertAdjacentElement('afterend', eleCombobox);
// 视觉列表不参与设置为不可访问
eleDatalist.setAttribute('aria-hidden', 'true');
}
// 暴露给其他方法
this.element = {
select: eleSelect,
combobox: eleCombobox,
button: eleButton,
datalist: eleDatalist
};
// 刷新内容
this.refresh();
// 事件绑定
this.events();
// 存储
if (!eleSelect.data) {
eleSelect.data = {
select: this
};
} else {
eleSelect.data.select = this;
}
};
/**
* 下拉相关的事件处理
* @return {[type]} [description]
*/
Select.prototype.events = function () {
// 各个元素
var objElement = this.element;
// 主要的几个元素
var eleSelect = objElement.select;
var eleCombobox = objElement.combobox;
var eleButton = objElement.button;
var eleDatalist = objElement.datalist;
// 单选下拉框的事件
if (this.multiple == false) {
// 点击页面空白要隐藏
// 测试表明,这里优化下可以提高40~50%性能
// 原本是绑定对应元素上,现在改成委托
if (!document.isSelectMouseEvent) {
document.addEventListener('click', function (event) {
var target = event.target;
if (!target || !target.closest) {
return;
}
// 获取下拉元素是关键,因为存储了实例对象
// 元素什么的都可以直接匹配
eleCombobox = target.closest('.' + CL);
// 如果点击的是 body 定位的下拉列表元素
if (!eleCombobox) {
eleDatalist = target.closest('ui-select-list');
if (eleDatalist && eleDatalist.id) {
eleButton = document.querySelector('a[data-target="' + eleDatalist.id + '"]');
if (eleButton) {
eleCombobox = eleButton.closest('.' + CL);
}
}
}
eleSelect = eleCombobox && eleCombobox.previousElementSibling;
if (!eleSelect || !eleSelect.data || !eleSelect.data.select) {
return;
}
// 按钮和列表元素就知道了
objElement = eleSelect.data.select.element;
eleButton = objElement.button;
eleDatalist = objElement.datalist;
// 下面判断点击的是按钮还是列表了
if (eleButton.contains(target)) {
if (eleSelect[DISABLED]) {
return false;
}
// 显示与隐藏
eleCombobox.classList.toggle(ACTIVE);
if (eleCombobox.classList.contains(ACTIVE)) {
// 1. 为防止 overflow 剪裁下拉列表,改为 body 定位
// 2. 为防止默认状态下,大量的 DOM 作为 <body> 子元素呈现,改为交互后 append 处理
if (!document.body.contains(eleDatalist)) {
document.body.appendChild(eleDatalist);
}
// 使用 JS 计算定位,为减少依赖,