一步步做组件-学校选择器(5)
发布在一步步做组件-学校选择器2015年4月11日view:1797BrettBatWeb组件,模块化
在文章任何区域双击击即可给文章添加【评注】!浮到评注点上可以查看详情。

上一篇中我们简单实现了搜索框的功能,这节中要为它添加按键事件,“上”“下”键选择匹配的结果,“回车”键来进入下一步,以使它使用起来更加人性化。

键盘事件入口

在搜索框keyup事件那里,针对特殊的按键做拦截(不触发搜索)。

var initSearchSchool = function(instance){
    // 以上省略...

    // 事件
    $searchInput.bind('keyup', function(event){
        // 特殊按键(动作键)
        if(event.keyCode == KEY_ENTER){
            searchSchoolChosen($searchList);
            return preventDefault(event);
        }
        if(event.keyCode == KEY_UP){
            searchListScrollPrev($searchDiv, $searchList);
            return preventDefault(event);
        }
        if(event.keyCode == KEY_DOWN){
            searchListScrollNext($searchDiv, $searchList);
            return preventDefault(event);
        }

        var keywords = $.trim($(this).val());
        // 空格or拼音没输完时暂不search
        if(keywords.length == 0 || keywords.indexOf("'") > -1){
            $searchDiv.hide();
            return false;
        }

        searchSchool(keywords, $searchDiv, $searchList, $searchEmpty);
    });

    // 以下省略...
};

这里定义了几个按键keyCode的全局变量和一个阻止浏览器默认事件的方法,如下。

// Constants
var KEY_ENTER = 13;
var KEY_UP = 38;
var KEY_DOWN = 40;

// Utils
var preventDefault = function(event){
    if(event && event.preventDefault)
        event.preventDefault();
    else
        window.event.returnValue = false;
    return false;
};

这里用自己写的preventDefault是为了能够兼容不同的浏览器,event.preventDefault()是标准浏览器提供的,而window.event.returnValue = false是IE下的写法。

searchSchoolChosen是选择当前项,searchListScrollPrev是选中上一项,而searchListScrollNext是选中下一项,我们将在后面详细讲。

动画效果

有了上面的代码结构,接下来要做的就是为特殊按键添加效果,这里涉及到动画,又是一个蛋疼的话题。

enter image description here

画了一张示意图,sDiv是父元素searchDivsList就是元素searchList,而target就是searchList中具体选中的那个子元素。根据这幅图,我们有:

Δoffset = tarTop - sDivTop + scrollTop

其中父元素sDiv上设置了height并且overflow-y: scroll,我们可以把sDiv视作一个窗口,只要保证target始终在这个窗口高度范围内即可。即随着我们按“上”“下”键,我们要保证目标子元素在这个视窗边界之内。

scrollTop <= Δoffset <= scrollTop + sDiv.height

于是我们有了控制searchDiv滚动条动画的方法。

var searchListScroll = function($searchDiv, $searchList){
    var scrollTop = $searchDiv.scrollTop();
    var viewMin = scrollTop;
    var viewMax = viewMin + $searchDiv.height();

    var $target = $searchList.children('li.active');
    var deltaOffset = $target.offset().top - $searchDiv.offset().top + scrollTop;

    // deltaOffset要在视窗范围里
    if(deltaOffset > viewMax){
        $searchDiv.animate({scrollTop: scrollTop + deltaOffset - viewMax}, 'fast');
    }
    else if(deltaOffset < viewMin){
        $searchDiv.animate({scrollTop: scrollTop - (viewMin - deltaOffset)}, 'fast');
    }
};

大体看上去没有问题,但是注意到当向“下”选中时,其实是Δoffset + target.height要在视窗范围内。因此我们作如下修正。

var searchListScroll = function(isDown, $searchDiv, $searchList){
    // 以上省略...

    var deltaOffset = $target.offset().top - $searchDiv.offset().top + scrollTop;
    isDown && (deltaOffset += $target.height());

    // 以下省略...
};

有了这个滚动条动画的方法,上面提到的searchListScrollPrevsearchListScrollNext也就信手拈来了。

var searchListScrollPrev = function($searchDiv, $searchList){
    var $cur = $searchList.children('li.active');
    $cur.removeClass && $cur.removeClass('active');

    if($cur.length == 0 || $cur.index() == 0){
        $searchList.children('li').last().addClass('active');
        searchListScroll(true, $searchDiv, $searchList);
    }
    else{
        $searchList.children('li').eq($cur.index() - 1).addClass('active');
        searchListScroll(false, $searchDiv, $searchList);
    }
};

var searchListScrollNext = function($searchDiv, $searchList){
    var $cur = $searchList.children('li.active');
    $cur.removeClass && $cur.removeClass('active');

    if($cur.length == 0 || $cur.index() == $searchList.children().length-1){
        $searchList.children('li').first().addClass('active');
        searchListScroll(false, $searchDiv, $searchList);
    }
    else{
        $searchList.children('li').eq($cur.index() + 1).addClass('active');
        searchListScroll(true, $searchDiv, $searchList);
    }
};

这两个方法就是用来响应“上”“下”键,控制searchList当前选中的子元素,为之添加class,并保证选中的元素在searchDiv的可见范围内。

注意这里代码$cur.removeClass && $cur.removeClass('active');这样写是因为可能找不到$cur元素,那么$cur.removeClass就肯定是false了,就不会执行$cur.removeClass('active')了。

<!-- 还有一点要注意的是,`$cur.index()`值的范围并不是`0 ~ length-1`,实际上值为`-1`时表示找不到元素,而超过`length-1`时又会从头开始找,即`$cur.index()`等于`length`时其实是第一个子元素。所以这里的代码中当`$cur.index() == $searchList.children().length-1`时要即时为第一个元素添加class,以保证`$cur.index()`的值范围在`0 ~ length-1`中。 -->

锦上添花

1.当通过“上”“下”键来选中时,我们已经为目标子元素添加了active的样式,那么这时如果鼠标再来捣乱该怎么办?我们只好再为鼠标添加hover效果,以抹去上下键的选中效果。

var initSearchSchool = function(instance){
    // 以上省略...

    $searchList.find('li').live('mouseenter', function(){
        $searchList.find('li.active').removeClass('active');
        $(this).addClass('hover');
    }).live('mouseleave', function(){
        $searchList.find('li.hover').removeClass('hover');
    });

    // 以下省略...
};

2.至于“回车”键的响应方法,我们用最简单的办法,相当于选中的子元素click一下。

var searchSchoolChosen = function($searchList){
    // 转向click event
    $searchList.children('li.active').click();
};

3.我们发现当我们输入关键字搜索时,其实每按一次键都执行了一次搜索和更新元素。而大多数情况下,我们输入一个关键字需要进行多次按键,比如搜索“江苏”,其实按键依次输入了“jiangsu”和最后拼音选择汉字的数字键或空格键。我们应该对此做些优化,以减少搜索执行,若使用Ajax搜索的话,可以减少很多次网络开销。

var initSearchSchool = function(instance){
    // 以上省略...

    // when正常输入
    initSearchSchool.currentTime = (new Date()).getTime();
    // 持续快速输入时不触发搜索
    if(initSearchSchool.currentTime - initSearchSchool.lastKeypressTime > KEY_PRESS_INTERVAL){
        initSearchSchool.lastKeypressTime = initSearchSchool.currentTime;
        searchSchool(keywords, $searchDiv, $searchList, $searchEmpty);
    }

    // 以下省略...
};

这里在全局定义常量var KEY_PRESS_INTERVAL = 300;毫秒即可。虽然不能面面俱到,但是已经可以减少大部分按键情况的执行开销了。

学校选择器v6 Demo

我的博客

原文链接:http://fuxiaode.cn/blog/2015/01/26/step-by-step-js-component-schoolbox-5/

合集链接:http://fuxiaode.cn/blog/2015/02/11/step-by-step-js-component-schoolbox-collections/

评论
发表评论
暂无评论
PUBLISHED IN
一步步做组件-学校选择器

这学期来一直在忙项目,整整一个学期都在做,自己的看书计划也没能实施。不过还是有不少收获的,是对以前看过的 JS Patterns 系列的综合运用,所以光看是不够的,一定要能应用到实际的业务中,并根据具体业务相应调整。趁着现在这段时间,想把以前写过的代码重新review一遍,并抽出可复用的功能把它们改写成通用组件,既是自己总结和提升的机会,也把它们作为以后的代码积累。原文博客合集链接:http://fuxiaode.cn/blog/2015/02/11/step-by-step-js-component-schoolbox-collections/

我的收藏