javascript 模拟亚马逊左侧导航算法的tab选项卡,支持四个方向,支持tab键切换,兼容各浏览器
Javascript tab选项卡(一切为了更好的体验, God in details!!!)
这原本只是一道简单的面试题,做出一个tab选项卡,然后要支持键盘TAB键控制。然后最近亚马逊的那个左侧菜单栏监听鼠标轨迹技术挺热门的,就把这功能添加进来了,而且还支持四个方向,用的都是原生js。如有问题,欢迎讨论
-
- 1
- 1
- 1
- 1
- 1
- 2222
2222 - 3333
- 4444
2222 - 0001
-
- 1
- 1
- 1
- 1
- 1
- 2222
2222 - 3333
- 4444
2222 - 0001
-
- 1
- 1
- 1
- 1
- 1
- 2222
2222 - 3333
- 4444
2222 - 0001
-
- 1
- 1
- 1
- 1
- 1
- 2222
2222 - 3333
- 4444
2222 - 0001
<!DOCTYPE HTML> <html lang="en-US"> <head> <meta charset="UTF-8"> <title>Javascript tab选项卡</title> <style> #menu { float: left; padding: 0; margin-left: 20px; } #menu3 { float: right; padding: 0; margin-left: 20px; } #menu2 { overflow: hidden; zoom: 1; } #menu li, #menu3 li { width: 12em; } .menu { list-style: none; background: #bf8d04; margin: 0; padding: 0; overflow: hidden; } #menu2 li, #menu4 li { width: 8em; float: left; } .menu li.on { background: #ff6600; } #menu li a, #menu3 li a { height: 3em; line-height: 3em; } .menu li a { display: block; text-align: center; } #menu2 li a, #menu4 li a { height: 5em; line-height: 5em; } #tabCon { float: left; display: inline; margin-left: 1em; } #tabCon2 { height: 50em; } #tabCon3 { float: right; } </style> </head> <body> <div style="overflow:hidden;zoom:1;"> <ul class="menu" id="menu"> <li><a href="javascript:" data-href="#tab1">1</a></li> <li><a href="javascript:" data-href="#tab2">2</a></li> <li><a href="javascript:" data-href="#tab3">3</a></li> <li><a href="javascript:" data-href="#tab4">4</a></li> <li><a data-href="#tab5" href="javascript:">5</a></li> <li><a data-href="#tab6" href="javascript:">6</a></li> <li><a data-href="#tab7" href="javascript:">7</a></li> <li><a data-href="#tab8" href="javascript:">8</a></li> <li><a data-href="#tab9" href="javascript:">9</a></li> <li><a data-href="#tab10" href="javascript:">10</a></li> <li><a href="javascript:" data-href="#tab11">11</a></li> <li><a href="javascript:" data-href="#tab12">12</a></li> <li><a href="javascript:" data-href="#tab13">13</a></li> <li><a href="javascript:" data-href="#tab14">14</a></li> <li><a data-href="#tab15" href="javascript:">15</a></li> </ul> <ul id="tabCon"> <li> <ul> <li>1</li> <li>1</li> <li>1</li> <li>1</li> <li>1</li> </ul> </li> <li>2222<br>2222</li> <li>3333</li> <li>4444<br>2222</li> <li>0001</li> <li>0002</li> <li>0003</li> <li>0004</li> <li>0005</li> <li>0006</li> <li> <ul> <li>1</li> <li>1</li> <li>1</li> <li>1</li> <li>1</li> </ul> </li> <li>2222<br>2222</li> <li>3333</li> <li>4444<br>2222</li> <li>0001</li> </ul> </div> <div> <ul class="menu" id="menu2"> <li><a href="javascript:" data-href="#tab1">1</a></li> <li><a href="javascript:" data-href="#tab2">2</a></li> <li><a href="javascript:" data-href="#tab3">3</a></li> <li><a href="javascript:" data-href="#tab4">4</a></li> <li><a data-href="#tab5" href="javascript:">5</a></li> <li><a data-href="#tab6" href="javascript:">6</a></li> <li><a data-href="#tab7" href="javascript:">7</a></li> <li><a data-href="#tab8" href="javascript:">8</a></li> <li><a data-href="#tab9" href="javascript:">9</a></li> <li><a data-href="#tab10" href="javascript:">10</a></li> </ul> <ul id="tabCon2"> <li> <ul> <li>1</li> <li>1</li> <li>1</li> <li>1</li> <li>1</li> </ul> </li> <li>2222<br>2222</li> <li>3333</li> <li>4444<br>2222</li> <li>0001</li> <li> <ul> <li>1</li> <li>1</li> <li>1</li> <li>1</li> <li>1</li> </ul> </li> <li>2222<br>2222</li> <li>3333</li> <li>4444<br>2222</li> <li>0001</li> </ul> </div> <div style="overflow:hidden;zoom:1;"> <ul class="menu" id="menu3"> <li><a href="javascript:" data-href="#tab1">1</a></li> <li><a href="javascript:" data-href="#tab2">2</a></li> <li><a href="javascript:" data-href="#tab3">3</a></li> <li><a href="javascript:" data-href="#tab4">4</a></li> <li><a data-href="#tab5" href="javascript:">5</a></li> <li><a data-href="#tab6" href="javascript:">6</a></li> <li><a data-href="#tab7" href="javascript:">7</a></li> <li><a data-href="#tab8" href="javascript:">8</a></li> <li><a data-href="#tab9" href="javascript:">9</a></li> <li><a data-href="#tab10" href="javascript:">10</a></li> <li><a href="javascript:" data-href="#tab11">11</a></li> <li><a href="javascript:" data-href="#tab12">12</a></li> <li><a href="javascript:" data-href="#tab13">13</a></li> <li><a href="javascript:" data-href="#tab14">14</a></li> <li><a data-href="#tab15" href="javascript:">15</a></li> </ul> <ul id="tabCon3"> <li> <ul> <li>1</li> <li>1</li> <li>1</li> <li>1</li> <li>1</li> </ul> </li> <li>2222<br>2222</li> <li>3333</li> <li>4444<br>2222</li> <li>0001</li> <li>0002</li> <li>0003</li> <li>0004</li> <li>0005</li> <li>0006</li> <li> <ul> <li>1</li> <li>1</li> <li>1</li> <li>1</li> <li>1</li> </ul> </li> <li>2222<br>2222</li> <li>3333</li> <li>4444<br>2222</li> <li>0001</li> </ul> </div> <div> <ul style="height:200px;" id="tabCon4"> <li> <ul> <li>1</li> <li>1</li> <li>1</li> <li>1</li> <li>1</li> </ul> </li> <li>2222<br>2222</li> <li>3333</li> <li>4444<br>2222</li> <li>0001</li> <li> <ul> <li>1</li> <li>1</li> <li>1</li> <li>1</li> <li>1</li> </ul> </li> <li>2222<br>2222</li> <li>3333</li> <li>4444<br>2222</li> <li>0001</li> </ul> <ul class="menu" id="menu4"> <li><a href="javascript:" data-href="#tab1">1</a></li> <li><a href="javascript:" data-href="#tab2">2</a></li> <li><a href="javascript:" data-href="#tab3">3</a></li> <li><a href="javascript:" data-href="#tab4">4</a></li> <li><a data-href="#tab5" href="javascript:">5</a></li> <li><a data-href="#tab6" href="javascript:">6</a></li> <li><a data-href="#tab7" href="javascript:">7</a></li> <li><a data-href="#tab8" href="javascript:">8</a></li> <li><a data-href="#tab9" href="javascript:">9</a></li> <li><a data-href="#tab10" href="javascript:">10</a></li> </ul> </div> <div style="height:500px;"></div> <script> (function(window){ /** * tab选项类 * @param {String} eventType 事件类型,可以指定'click', 'mouseover'事件 * @param {Element} menu 菜单包裹器元素,必须是ul或者ol的标签元素 * @param {Element} container 选项卡包裹器内容,必须是ul或者ol的标签元素 * @param {Boolean} TabKeyCtrl 是否开启键盘tab键控制,缺省为true,开启 */ var TabChange = function (eventType, menu, container, TabKeyCtrl) { this.eventType = eventType; this.menu = menu; this.container = container; // 菜单元素的链接数组 this.menuLinks = menu.getElementsByTagName('a'); // 设置延迟定时器,防止鼠标移到tab内容经过菜单时的切换 this.timeout = null; // 记录鼠标移动时的坐标数组 this.mouseLocs = []; // 菜单栏的固定点1, 根据菜单栏和内容的位置而改变 this.firstSlope = null; // 菜单栏的固定点2, 根据菜单栏和内容的位置而改变 this.secondSlope = null; // 记录内容栏相对于菜单栏的位置 this.relatedPos = ''; // 返回内容栏相对于菜单栏的位置, 保存在this.relatedPos属性里 this.contentPosRelate(); // 根据内容栏相对于菜单栏的位置, 返回菜单栏的固定点1,和固定点2 this.ensureTriangleDots(); // 默认开启TAB键控制 TabKeyCtrl = TabKeyCtrl || true; if (TabKeyCtrl) { // 启动tab键切换 this.tabKeyCtrl(this.menuLinks); } // 初始化事件 this.initEvent(); }; TabChange.prototype = (function(){ /** * 显示隐藏切换方法 * @param {Array} elementArray 元素集数组,是tab内容li元素集 * @param {Number} index 当前序号 */ var toggleDisplay = function (elementArray, index) { // 显示当前序号的元素 elementArray[index].style.display = ''; // 隐藏非当前序号的元素 for (var i = 0, len = elementArray.length; i < len; i++) { if (i === index) { continue; } elementArray[i].style.display = 'none'; } }; /** * 获取元素相对于浏览器左上角的坐标位置,为正值 * @param element * @return {{x: Number, y: Number}} * @constructor */ var LocFromdoc = function (element) { var left = element.offsetLeft, top = element.offsetTop; element = element.offsetParent; while (element !== null) { left += element.offsetLeft; top += element.offsetTop; element = element.offsetParent; } return { x: left, y: top }; }; /** * 类名转换方法,确保当前序号的类名,删除其他序号的类名 * @param {Array} elementArray 元素集数组 存放tab菜单li元素集 * @param {Number} index 当前序号 * @param {String} className 要添加的类名 */ var toggleClass = function (elementArray, index, className) { var pattern = new RegExp(' ' + className + ' '); for (var i = 0, len = elementArray.length; i < len; i++) { // 前后添加空格,且确保不能有连续空格 var temp = ' ' + elementArray[i].className.replace(/^\s+|\s+$/, '').replace(/\s+/, ' ') + ' '; // 当前序号 if (i === index) { // 如果当前序号的元素没有该类名,则添加类名 if (temp.indexOf(' ' + className + ' ') === -1) { elementArray[i].className += (elementArray[i].className ? ' ' : '') + 'on'; } // 跳到下一个循环 continue; } // 非当前序号,删除类名,去掉多余空格 elementArray[i].className = temp.replace(pattern, '').replace(/^\s+|\s+$/, '').replace(/\s+/, ' '); } }; return { /** * 根据内容栏相对于菜单栏的位置,判断移动过程中的点是否在三角形内 * @param {Object} p1 开始位置 * @param {Object} p2 菜单栏固定点1 * @param {Object} p3 菜单栏固定点2 * @param {Object} m 结束位置 * @return {*} */ proPosInTriangle: function (p1, p2, p3, m) { // 结束时鼠标坐标位置 var x = m.x, y = m.y, // 开始鼠标坐标位置 x1 = p1.x, y1 = p1.y, // 菜单栏包裹层右上角坐标 x2 = p2.x, y2 = p2.y, // 右下角坐标 x3 = p3.x, y3 = p3.y, // (y2 - y1) / (x2 - x1)为两坐标连成直线的斜率 // 因为直线的公式为y=kx+b;当斜率相同时,只要比较 // b1和b2的差值就可以知道该点是在 // (x1,y1),(x2,y2)的直线的哪个方向 // 当r1大于0,说明该点在直线右侧,其它以此类推 r1 = y - y1 - (y2 - y1) / (x2 - x1) * (x - x1), r2 = y - y2 - (y3 - y2) / (x3 - x2) * (x - x2), r3 = y - y3 - (y1 - y3) / (x1 - x3) * (x - x3), compare; // 根据位置不同,判定公式也不同,因为找不到通用的办法 // 只好各个对比 switch (this.relatedPos) { case 'left': compare = (r1 * r2 * r3 > 0) && (r1 > 0); break; case 'right': case 'down': compare = (r1 * r2 * r3 < 0) && (r1 > 0); break; case 'up': compare = (r1 * r2 * r3 > 0) && (r1 < 0); break; default: break; } // 返回是否在三角形内的结果 return compare; }, /** * 记录元素的位置信息 * @param element * @return {{top: *, topAndHeight: number, left: *, leftAndWidth: number}} */ info: function (element) { var location = LocFromdoc(element); return { top: location.y, topAndHeight: location.y + element.offsetHeight, left: location.x, leftAndWidth: location.x + element.offsetWidth }; }, /** * 记录内容栏相对于菜单栏的位置,因为后面还要用刀,使用this.relatedPos记录信息 * @param {Number} deviation 容许的差值,或者偏差 */ contentPosRelate: function (deviation) { deviation = deviation || 0; var ele1 = this.info(this.menu), ele2 = this.info(this.container); // ele2左边距离的浏览器左侧的距离大于 // ele1左边距离的浏览器左侧的距离加上本身的宽 // 说明ele2在ele1的右侧,其他以此类推 if (ele2.left >= (ele1.leftAndWidth + deviation)) { this.relatedPos = 'right'; } else if (ele1.left >= (ele2.leftAndWidth + deviation)) { this.relatedPos = 'left'; } else if (ele2.top >= (ele1.topAndHeight + deviation)) { this.relatedPos = 'down'; } else if(ele1.top >= (ele2.topAndHeight + deviation)) { this.relatedPos = 'up'; } }, /** * 根据内容栏相对于菜单栏的位置, 返回菜单栏的固定点1,和固定点2,保存在this.firstSlope和this.secondSlope对象里 */ ensureTriangleDots: function () { var x1, y1, x2, y2; var info = this.info(this.menu); switch (this.relatedPos) { case 'right': x1 = info.leftAndWidth; y1 = info.top; x2 = x1; y2 = info.topAndHeight; break; case 'left': x1 = info.left; y1 = info.top; x2 = x1; y2 = info.topAndHeight; break; case 'down': x1 = info.left; y1 = info.topAndHeight; x2 = info.leftAndWidth; y2 = y1; break; case 'up': x1 = info.left; y1 = info.top; x2 = info.leftAndWidth; y2 = y1; break; default: break; } this.firstSlope = { x: x1, y: y1 }; this.secondSlope = { x: x2, y: y2 }; }, /** * 键盘tab键控制tab菜单切换 * @param {Element} menuLinks tab菜单的链接数组,因为只有链接可以被键盘tab切换 */ tabKeyCtrl: function (menuLinks) { var that = this; // 侦听document的keyup事件 EventUtil.addEvent(document, 'keyup', function (event) { event = EventUtil.getEvent(event); // 获取当前元素 var target = EventUtil.getTarget(event), // 获取当前元素的data-href属性 href = target.getAttribute('data-href'); // 当按下的是tab键时且data-href属性值包含"#tab"字符 if ((event.keyCode === 9) && href && (href.indexOf('#tab') > -1)) { // 获取对应数字,表示显示的tab菜单的相应序号 var index = parseInt(href.split('#tab')[1], 10) - 1; // 触发当前序号的tab菜单事件,切换tab EventUtil.triggerEvent(menuLinks[index], that.eventType); } }); }, /** * 初始化事件,主要是注册tab菜单的事件侦听器 */ initEvent: function () { var that = this, menuLinks = this.menuLinks, clickRegistration; // 给tab菜单的链接注册侦听器 function toggle(array1, array2, index, className) { // 类名切换 toggleClass(array1, index, className); // 显示切换 toggleDisplay(array2, index); } for (var i = 0, len = menuLinks.length; i < len; i++) { // 当为mouseover事件时,给mouseout注册事件侦听器 // 防止鼠标移到tab内容经过菜单时的切换 if (this.eventType === 'mouseover') { EventUtil.addEvent(menuLinks[i], 'mouseover', (function (index) { return function (event) { // 捕捉第一次初始化的错误 try { // 是否在指定三角形内 var diff = that.proPosInTriangle(that.mouseLocs[0], that.firstSlope, that.secondSlope, that.mouseLocs[2]); } catch (ex) { } // 是就启动延迟显示, // 否则不延迟 if (diff) { that.timeout = setTimeout(function () { toggle(that.menu.children, that.container.children, index, 'on'); }, 300); } else { toggle(that.menu.children, that.container.children, index, 'on'); } }; })(i)); // 记录鼠标在菜单栏中移动的最后三个坐标位置 EventUtil.addEvent(menuLinks[i], 'mousemove', function (event) { that.mouseLocs.push({ x: EventUtil.eventPage(event).x, y: EventUtil.eventPage(event).y }); if (that.mouseLocs.length > 3) { // 移除超过三项的数据 that.mouseLocs.shift(); } }); // 鼠标移出的时候,清除延时器 EventUtil.addEvent(menuLinks[i], 'mouseout', function (event) { if (that.timeout) { clearTimeout(that.timeout); } }); } else { // 其它事件侦听器,例如“click” EventUtil.addEvent(menuLinks[i], this.eventType, clickRegistration = (function (index) { return function (event) { clearTimeout(that.timeout); // 启动延时显示 that.timeout = setTimeout(function () { toggle(that.menu.children, that.container.children, index, 'on'); }, 80); // 阻止默认行为 EventUtil.preventDefault(event); }; })(i)); } } // 触发第一个菜单链接的事件 EventUtil.triggerEvent(menuLinks[0], this.eventType); } }; }()); /** * 跨浏览器event对象处理对象 * @type {{addEvent: Function, getEvent: Function, getTarget: Function, preventDefault: Function, triggerEvent: Function}} */ var EventUtil = { /** * 注册事件侦听器 * @param element * @param type * @param listener */ addEvent: function (element, type, listener) { if (element.addEventListener) { element.addEventListener(type, listener, false); } else { element['e' + type + listener] = listener; element[type + listener] = function () { element['e' + type + listener](window.event); }; element.attachEvent('on' + type, element[type + listener]); } }, removeEvent: function (element, type, listener) { if (element.removeEventListener) { //W3C 方法 element.removeEventListener(type, listener, false); return true; } else if (element.detachEvent) { //MSIE方法 element.detachEvent("on" + type, element[type + listener]); element['e' + type + listener] = null; element[type + listener] = null; return true; } //若两种方法都不具备则返回false return false; }, /** * 获取event对象 * @param event * @return {*|Event} */ getEvent: function (event) { return event || window.event; }, /** * 获取目标元素 * @param event * @return {EventTarget|Object} */ getTarget: function (event) { event = this.getEvent(event); return event.target || event.srcElement; }, /** * 阻止默认行为 * @param event */ preventDefault: function (event) { event = this.getEvent(event); if (event.preventDefault) { event.preventDefault(); } else { event.returnValue = false; } }, /** * 触发事件 * @param element * @param type */ triggerEvent: function (element, type) { var ua = navigator.userAgent; if (ua.indexOf('MSIE') && parseInt(ua.split('MSIE')[1], 10) < 9) { element.fireEvent("on" + type); } else { var e = document.createEvent('MouseEvent'); e.initEvent(type, false, false); element.dispatchEvent(e); } }, eventPage: function (event) { event = this.getEvent(event); return { x: event.pageX || (event.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft)), y: event.pageY || (event.clientY + (document.documentElement.scrollTop || document.body.scrollTop)) }; } }; var TabFactory = { create: function (eventType, menu, container, TabKeyCtrl) { return new TabChange(eventType, menu, container, TabKeyCtrl); } }; window.TabFactory = TabFactory; }(window)); var menu = document.getElementById('menu'), container = document.getElementById('tabCon'), menu2 = document.getElementById('menu2'), container2 = document.getElementById('tabCon2'), menu3 = document.getElementById('menu3'), container3 = document.getElementById('tabCon3'), menu4 = document.getElementById('menu4'), container4 = document.getElementById('tabCon4'); // 实例化对象 TabFactory.create('mouseover', menu, container); TabFactory.create('mouseover', menu2, container2); TabFactory.create('mouseover', menu3, container3); TabFactory.create('mouseover', menu4, container4); </script> </body> </html>