- html 结构:
<div class="container">
<ul class="menu" id="menu">
<li>menu-1</li>
<li>menu-2</li>
<li>menu-3</li>
<li>menu-4</li>
<li>menu-5</li>
</ul>
<ul class="sub-menu " id="sub-menu">
<li>submenu-1</li>
<li>submenu-2</li>
<li>submenu-3</li>
<li>submenu-4</li>
<li>submenu-5</li>
</ul>
</div>
- JS
const menu = document.getElementById('menu');
const menuLIs = Array.from(menu.getElementsByTagName('li'));
const subMenu = document.getElementById('sub-menu');
const subMenuLIs = Array.from(subMenu.getElementsByTagName('li'));
const container = document.querySelector('.container');
// 存储鼠标移动时的坐标
let mouseLocs = [];
// container 右上角和右下角坐标
const menuTopRight = {x: menu.offsetWidth, y: 0}, menuBottomRight = {x: menu.offsetWidth, y: menu.offsetHeight};
menuLIs.forEach((item, index) => item.index = index);
menu.addEventListener('mouseover', handleToggleTabs);
menu.addEventListener('mousemove', storeMouseLocation);
menu.addEventListener('mouseout', clearTimeouter);
function handleToggleTabs(e) {
if (e.target.nodeName.toUpperCase() === 'LI') {
let currentMenu = e.target;
let isInTriRange;
// subMenu 添加 show-block 类,让其显示
subMenu.className = 'sub-menu show-block';
// 设置一个定时器
currentMenu.timeouter = null;
try {
isInTriRange = isTriangleRange(mouseLocs[0], mouseLocs[2], menuTopRight, menuBottomRight);
} catch (err) {
}
// 结果为 true,说明在三角区域内
if (isInTriRange) {
currentMenu.timeouter = setTimeout(function () {
toggle(subMenuLIs, currentMenu.index);
}, 300)
} else {
toggle(subMenuLIs, currentMenu.index)
}
}
}
/**
* 存储鼠标在当前选项卡移动时的最后三个坐标
* @param e
*/
function storeMouseLocation(e) {
if (e.target.nodeName.toUpperCase() === 'LI') {
// 坐标原点在 container 右上角
const x = e.clientX - container.offsetLeft, y = e.clientY - container.offsetTop;
mouseLocs.push({x, y});
if (mouseLocs.length > 3) {
mouseLocs.shift();
}
}
}
/**
* 鼠标移出当前选项卡时,如果当前选项卡设置了定时器,说明判断 isInTriangle 为 true,
* 定时器内设置的是切换选项卡的 function,这时,清除定时器,便不会触发 toggle 切换选项卡
* @param e
*/
function clearTimeouter(e) {
if (e.target.nodeName.toUpperCase() === 'LI') {
if (e.target.timeouter) {
clearTimeout(e.target.timeouter);
}
}
}
/**
* 切换选项卡和内容样式
* @param eleArr subMenuLIs
* @param id 当前选择的选项卡 ID
*/
function toggle(eleArr, id) {
eleArr.forEach((item, index) => {
menuLIs[index].className = '';
item.className = '';
});
menuLIs[id].className = 'active-menu';
eleArr[id].className = 'show-block';
}
/**
* 计算三角形区域的方法
* @param t1 开始鼠标坐标位置
* @param t2 结束时鼠标坐标位置
* @param p1 container 右上角坐标
* @param p2 container 右下角坐标
* @returns {boolean}
*/
function isTriangleRange(t1, t2, p1, p2) {
const x = t2.x,
y = t2.y,
x1 = t1.x,
y1 = t1.y,
x2 = p1.x,
y2 = p1.y,
x3 = p2.x,
y3 = p2.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);
return (r1 * r2 * r3 < 0) && (r1 > 0);
}
小结:
- 将 ul 下的所有 li 的 nodeList 集合获取到并使用
Array.from()
的方法转化为数组对象 - 遍历选项卡,为每个 li 添加一个
index
属性,值为索引值 - 为每个选项卡添加
mouseover
、mousemove
、mouseout
的方法 -
mouseover
的时候会触发选项卡切换,会有对应的内容和样式的变化,这不是重点,重点是,当鼠标从左侧的选项卡斜着划入右侧的内容区域时,会触发其他的选项卡的切换,这时,我们想进入内容区域,并不想切换选项卡。 - 亚马逊左侧导航区域的解决办法是,判断一个三角区域,具体看代码注释。
-
mousemove
的时候记录在当前选项卡移动的最后三个位置,只是为了取第一个和最后一个位置,第一个数值作为三角形的一个点,最后一个数值就用来判断是否在三角形内。 - 如果在三角形内,鼠标在当前选项卡
mouseout
的时候会清除设置的定时器,导致不会切换样式。