UNPKG

lu2

Version:

Simple and flexible UI component library based on native HTML and JavaScript

1,512 lines (1,299 loc) 490 kB
/** * @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 计算定位,为减少依赖,