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>

浙公网安备 33010602011771号