javascript 模拟亚马逊左侧导航算法的tab选项卡,支持四个方向,支持tab键切换,兼容各浏览器

Javascript tab选项卡(一切为了更好的体验, God in details!!!)

这原本只是一道简单的面试题,做出一个tab选项卡,然后要支持键盘TAB键控制。然后最近亚马逊的那个左侧菜单栏监听鼠标轨迹技术挺热门的,就把这功能添加进来了,而且还支持四个方向,用的都是原生js。如有问题,欢迎讨论

    • 1
    • 1
    • 1
    • 1
    • 1
  • 2222
    2222
  • 3333
  • 4444
    2222
  • 0001
  • 0002
  • 0003
  • 0004
  • 0005
  • 0006
    • 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
  • 0002
  • 0003
  • 0004
  • 0005
  • 0006
    • 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> 

 

 

posted @ 2013-03-05 11:19  LukeLin  阅读(1235)  评论(0编辑  收藏  举报