Simple Note

选项卡的三步走

2016.01.06

第一步 | 实现功能

自己写js最常见的状态,仅仅实现了功能,没有考虑如何把它通用化,降低下一次开发类似功能的成本。有幸看到了nimojs/learn-js这个项目,才明白之前写的代码多糟糕。

(function() {
    var navBar = document.querySelector('nav');
    var descriptions = navBar.querySelectorAll('.description');
    var links = navBar.querySelectorAll('a');

    Array.prototype.forEach.call(links, function(value, index) {
        value.addEventListener('click', function() {
            // 先清除所有链接的选中类
            Array.prototype.forEach.call(links, function(value) {
                value.className = '';
            });
            // 给点击到的链接增加选中类
            value.className = 'clicked';
            // 先隐藏所有内容区
            Array.prototype.forEach.call(descriptions, function(value) {
                value.className = 'description hide';
            });
            // 单独显示点击的链接对应的内容区
            descriptions[index].className = 'description show';

            return false;
        })
    });
})();

第二步 | 抽象功能

上面的写法有两个大问题,第一个问题是在功能里固化了类名,万一选项卡并不在nav标签里呢?第二个问题,通过直接给元素的className属性赋值来增加或删除class名,可是元素的className并不一定只有这个功能函数中用到的那几个,也许在别的地方也会为它们增加新的class,这种直接重置的做法并不好。

改进如下:

function tab(options) {
    if (!(typeof options) || !(options.element && options.triggers && options.contents)) {
        throw 'please provide enough arguments!';
    };

    // 定义父级DOM 触发DOM 内容DOM
    var parentElement = document.querySelector(options.element);
    var triggerElements = parentElement.querySelectorAll(options.triggers);
    var contentElements = parentElement.querySelectorAll(options.contents);

    // 缓存类名
    var triggerClassName = options.triggers.slice(1);
    var contentClassName = options.contents.slice(1);

    parentElement.addEventListener('click', function(event) {
        var target = event.target;

        Array.prototype.forEach.call(triggerElements, function(value, index) {
            if (target === value) {

                // 先清除所有链接的选中类
                Array.prototype.forEach.call(triggerElements, function(value) {
                    removeClass(value, 'clicked');
                });

                // 给点击到的链接增加选中类
                addClass(value, 'clicked');

                // 先隐藏所有内容区
                Array.prototype.forEach.call(contentElements, function(value) {
                    removeClass(value, 'show');
                });

                // 单独显示点击的链接对应的内容区
                addClass(contentElements[index], 'show');
            }
        })
    });
}

把阶段一的功能封装成一个tab函数,三个参数分别指定了父级DOM、触发DOM、内容DOM:

tab({
    element: '.new-tab',
    triggers: '.triggers',
    contents: '.description'
});

其中用到了辅助函数:

function addClass(element, value) {
    if (!element.className) {
        element.className = value;
    } else {
        var newClassName = element.className;
        newClassName += " ";
        newClassName += value;
        element.className = newClassName;
    }
}
function removeClass(element, className) {
    if (element.className && element.className.indexOf(className) >= 0) {
        var oldClass = ' ' + element.className + ' ';
        var newClass = oldClass.replace(' ' + className + ' ', ' ');
        newClass = newClass.replace(/(^\s+)|(\s+$)/g, '');
        element.className = newClass;
    }
}

第三步 | 丰富功能

第二步这一改动,看起来好像好多了,但是tab函数里依然有一些不应该被写死的东西,比如clicked与show,这些东西也应该可配置。

添加:triggers和contents被选中时的class

function tab(options) {
    if (!(typeof options) || !(options.element && options.triggers && options.contents)) {
        throw new ReferenceError('please provide enough arguments!');
    };

    // 定义默认参数
    // triggers激活时的默认类为ui-tab-active
    options.activeTriggersClass = options.activeTriggersClass ? options.activeTriggersClass : 'ui-tab-active';
    // contents激活时的默认类为ui-tab-content-active
    options.activeContentsClass = options.activeContentsClass ? options.activeContentsClass : 'ui-tab-content-active';

    // 定义父级DOM 触发DOM 内容DOM
    var parentElement = document.querySelector(options.element);
    var triggerElements = parentElement.querySelectorAll(options.triggers);
    var contentElements = parentElement.querySelectorAll(options.contents);

    // 事件绑定
    parentElement.addEventListener('click', function(event) {
        var target = event.target;

        Array.prototype.forEach.call(triggerElements, function(value, index) {
            if (target === value) {

                // 先清除所有链接的选中类
                Array.prototype.forEach.call(triggerElements, function(value) {
                    removeClass(value, options.activeTriggersClass);
                });

                // 给点击到的链接增加选中类
                addClass(value, options.activeTriggersClass);

                // 先隐藏所有内容区
                Array.prototype.forEach.call(contentElements, function(value) {
                    removeClass(value, options.activeContentsClass);
                });

                // 单独显示点击的链接对应的内容区
                addClass(contentElements[index], options.activeContentsClass);
            }
        })
    });
}

现在tab函数应该这么用:

tab({
    element: '.new-tab',
    triggers: '.triggers',
    contents: '.description',
    activeTriggersClass: 'clicked',
    activeContentsClass: 'show'
});

tab函数本身好像没有什么问题了,还可以继续丰富它的功能,比如:

  • 初始化的时候我需要选项卡显示指定的面板怎么办?
  • 不是点击,我要鼠标移到选项卡上面板就能激活怎么办?
  • 我想面板切换的页面另外一个地方也产生响应怎么办?

于是,我们得到了一个更好的tab函数:

function tab(options) {
    if (!(typeof options) || !(options.element && options.triggers && options.contents)) {
        throw 'please provide enough arguments!';
    };
    if (options.triggerType && !(options.triggerType === 'click' || options.triggerType === 'hover')) {
        throw 'triggerType must be "click" or "hover"!';
    }

    // 定义默认参数
    // 默认初始化显示的标签为第一个
    options.activeIndex = options.activeIndex ? options.activeIndex : 0;
    // triggers激活时的默认类为ui-tab-active
    options.activeTriggersClass = options.activeTriggersClass ? options.activeTriggersClass : 'ui-tab-active';
    // contents激活时的默认类为ui-tab-content-active
    options.activeContentsClass = options.activeContentsClass ? options.activeContentsClass : 'ui-tab-content-active';
    // triggerType默认为click,可选hover(mouseenter + mousele)
    options.triggerType = options.triggerType === 'hover' ? 'mouseover' : 'click';
    // 点击切换是默认触发的函数为空函数
    options.onSwitch = options.onSwitch ? options.onSwitch : function() {};

    // 定义父级DOM 触发DOM 内容DOM
    var parentElement = document.querySelector(options.element);
    var triggerElements = parentElement.querySelectorAll(options.triggers);
    var contentElements = parentElement.querySelectorAll(options.contents);

    // 缓存类名
    var triggerClassName = options.triggers.slice(1);
    var contentClassName = options.contents.slice(1);

    // 初始化
    triggerElements[options.activeIndex].className = options.activeTriggersClass + ' ' + triggerClassName;
    contentElements[options.activeIndex].className = options.activeContentsClass + ' ' + contentClassName;

    // 事件绑定
    parentElement.addEventListener(options.triggerType, function(event) {
        var target = event.target;

        Array.prototype.forEach.call(triggerElements, function(value, index) {
            if (target === value) {

                // 先清除所有链接的选中类
                Array.prototype.forEach.call(triggerElements, function(value) {
                    removeClass(value, options.activeTriggersClass);
                });

                // 给点击到的链接增加选中类
                addClass(value, options.activeTriggersClass);

                // 先隐藏所有内容区
                Array.prototype.forEach.call(contentElements, function(value) {
                    removeClass(value, options.activeContentsClass);
                });

                // 单独显示点击的链接对应的内容区
                addClass(contentElements[index], options.activeContentsClass);

                // 触发onSwitcher函数
                options.onSwitch.call(null, index, triggerElements.length);
            }
        })
    });
}

现在,tab函数的用法:

tab({
    element: '.new-tab',
    triggers: '.triggers',
    contents: '.description',
    activeIndex: 1,
    activeTriggersClass: 'clicked',
    activeContentsClass: 'show',
    triggerType: 'hover',
    onSwitch: function(index, count){}
});

click或hover、trigger时onSwitch会执行,其中,index是当前trigger的索引,count是trigger的总数。

Comments
Write a Comment