图片导航设计方案

2010-7-29 Update:

在第一种实现方案中,如果图片的父元素设置了padding、margin之后,就会使得FF、Chrome等逻辑判断失误,而IE还完好。

———————-纠错分界线————————–

说“图片导航”或许很迷糊,我也迷糊不知道怎么描述,就是类似于Google、QQ空间、百度图片搜索中的在图片中出现上下箭头或者左右箭头来指示下一个和上一个图片的功能。昨天同事问到我这方面的实现方式,也就暂时想了两种。

之前查看过Google的方法是在图片上添加两个HTML元素作为逻辑操作对象(包括切换鼠标样式、点击事件等),这样就显得比较复杂了,需要确定图片的在页面中的位置、图片的宽高、之后还要判断鼠标的位置……QQ空间的相册导航,是在图片本身来操作逻辑的,这同我第一种方案很类似,而百度图片搜索结果页中图片导航是在图片的父元素中做逻辑的,这样就避免了图片大小对导航的干扰的问题(主要是如果图片太小,在图片上做导航就显得迷糊了,特别是鼠标的样式切换)。下面来说说我的方法。

第一种,直接在图片上做逻辑。事件参数e有这么两个参数,在FF等浏览器下是layerX,layerY,在IE下是offsetX,offsetY,用于在一个DOM元素触发事件的时候,鼠标在该元素上的偏移量。这个是好方法啊,在结合offsetHeight和offsetWidth,就可以直接在图片上做判断了,给图片添加mousemove、onclick事件来处理鼠标样式切换、点击事件逻辑等等。《测试用例1》,可是这种方法的一个缺点是:在IE6下,当鼠标移动到鼠标的时候,会默认的弹出一个框,很杯具。

第二中方式就是根据上面的技巧,在图片上覆盖一个div(或者其他的能覆盖的元素都成),覆盖就得获取到图片在页面中的坐标以及宽高了,通过offsetLeft和offsetTop可以方便的得到,兼容性也还好,对于不是非常复杂的DOM结构和CSS布局,应该都是没啥问题的。《测试用例》,这样就避免了IE6下弹出框的问题。不过这样代码就得写多几行了。

具体的实现方式可以查看上面给出了两个实例。

delegate事件

这几天在负责帮助中心改版的项目,其中左栏(浏览下面的测试页面)有一个显示隐藏导航的功能,这个跟scrollTab不同了,它只涉及到一个ul显示隐藏,并且还是li的childNodes,所以可以给li添加事件,来触发ul的显示隐藏。

由于公司内使用YUI作为开发的基础库,所以开始的时候也就从YUI的delegate事件来实现功能了:《效果页面》,并且下面的代码有一个优化效果:记录前面点击过的元素,在点击下一个tab的时候隐藏之前点击过的tab。

<script src="http://k.kbcdn.com/global/utilities/utilities_1_4_24.js"></script>
<script>
   (function(){
      var _E=YK.E,_D=YK.D,tempEl=null;
	  YK.load(["event-delegate"],function(){
	    if(_D.inDocument("question-catagroy")){
		  _E.delegate("question-catagroy","click",function(e,matchEl,root){
		    if(matchEl.className === "catagroy-title"){
			  var temp=null;
			  temp=matchEl.parentNode;
		      if(!tempEl){
			     tempEl=_D.getFirstChild(root);
			  }
			  //判断是否重复点击了当前tab,从而来切换当前tab的显示隐藏
			  if(temp == tempEl){
			     if(temp.className.indexOf("catagroy-active")!=-1){
				    temp.className=tempEl.className.replace("catagroy-active","");
				 }else{
				    temp.className+=" catagroy-active";
				 }
			  }else{
			     tempEl.className=tempEl.className.replace("catagroy-active","");
				 temp.className+=" catagroy-active";
			  }
			  tempEl=temp;
			  temp=null;
			  _E.preventDefault(e);
		    }
		 },"a");
		}
	  });
   })();
</script>

delegate事件对给列表添加事件有了非常好的优化作用,这样就不需要给每一个li都添加事件,而是在它的父元素添加相关的事件,来监听哪个子元素触发了该事件。

既然delegate事件强大的很,我当然也就会心痒痒的想要自己实现个delegate函数啦,下面是我编写的delega事件函数:

/*
 * @author Shanpeng
 * @copyright: Shanpeng,All rights reserved.
 * @website:http://www.ilovejs.net
 * @e-mail:supersha@foxmail/shanpeng@taobao.com/supershafeng@gmail.com
 * @QQ:770104121
 * @my word:Here,believe what browsers display,and believe yourself
 */
////////////////////// Javascript's code below ///////////////////////
/*
 * @type:event type.
 * @fn:handle function,argument[0] is target Element,"this" reference to @elem.
 * @elem:the aim DOM Element to add an event(elem || id).
 * @subTag: sub DOM Elements's tagName.
 */
var delegate = (function(){
   var $ = function(id){
      return typeof id === "string" ? document.getElementById(id) : id;
   }
   var addListener = function(el, type, fn){
      if (el.addEventListener) {
         el.addEventListener(type, fn, false);
      }
      else {
         el.attachEvent("on" + type, function(e){
            fn.call(el, e);
         });
      }
      return false;
   }
   return function(type, fn, elem, subTag){
      addListener($(elem), type, function(e){
         var e = e || window.event;
         var el = e.srcElement || e.target;
         var nodeName = el.nodeName.toLowerCase();
         if (nodeName === subTag) {
            fn.call(elem, el);
            //if subTag is a "A" element,preventDefault;
            if (nodeName === "a") {
               try {
                  e.preventDefault();
               }
               catch (o) {
                  e.returnValue = false;
               }
            }
         }
      });
   }
})();

上面的delegate函数跟YUI的delegate编写方式一样,只是没有了YUI强大的selector筛选功能而已啦,还有如果需要监听的是A超链接元素,会阻止超链接的默认行为。具体说明在源代码的注释中已有说明,下面就是一个测试页面:《效果页面》。

监听键盘事件类KeyEvent

今天在重新看《javascript高级程序设计》的时候,当翻到关于事件的那章,那里讲述了键盘的几个事件(keydown,keyuup,keypress)以及IE跟其他主流浏览器在事件属性方面的异同,另一方面也想到了YUI中也有这么监听键盘事件功能的类,所以自己也就简单尝试去实现另外一种操作方式的类:KeyEvent。示例如下:

var Event = {
    type: "keydown",
    data: {
        name: "Supersha"
    }
}
onload = function(){
    //表示需要同时按下shift、ctrl以及1键keydown事件程序才会生效。
    var key = new KeyEvent([49],{"shiftKey":true,"ctrlKey":true});
    key.keydown(function(e){
        alert(e.keyCode);
    });

    var key2 = new KeyEvent([50]);
    key2.keydown(function(e){
        alert(this.data.name);
    }, Event);
}

可以同时声明几个监听键盘事件的实例对象,互不干扰。上面的示例说明了keydown的使用方法,keyup和keypress也是类似的。《测试页面

下面是KeyEvent类的源码:

var KeyEvent = function(aKeyCode, oFlag){
    this.keyCodes = aKeyCode || [];
    this.key = oFlag || {};
}
KeyEvent.prototype._addListener = function(elem, type, handle, context){
    var that = this;
    var _handle = function(e){
        e = e || window.event;
        var codeFlag = true;
        var keyFlag = true;
        //检查参数指明的keyCode有没有按下
        for (var i = 0, l = that.keyCodes.length; i < l; i++) {
            if (e.keyCode !== that.keyCodes[i]) {
                codeFlag = false;
            }
        }
        //检查参数指明的ctrl,shift,alt键是否按下。
        for (var k in that.key) {
            if (!e[k]) {
                keyFlag = false;
            }
        }
        if (keyFlag && codeFlag) {
            handle.call(context || elem, e);
        }
    }
    if (elem.addEventListener) {
        elem.addEventListener(type, _handle, false);
    }
    else if (elem.attachEvent) {
        elem.attachEvent("on" + type, _handle);
    }
    else {
        elem["on" + type] = _handle;
    }
}
//context是handle函数内this的上下文,不指明的话默认是当前的DOM元素(document)
KeyEvent.prototype.keydown = function(handle, context){
    this._addListener(document, "keydown", handle, context);
}
KeyEvent.prototype.keyup = function(handle, context){
    this._addListener(document, "keyup", handle, context);
}
KeyEvent.prototype.keypress = function(handle, context){
	this._addListener(document, "keypress", handle, context);
}

//////////////////// 2010-01-24 Update /////////////////////

由于考虑到键盘keyCode难以记忆,所以使得通过keyCode编码来触发key event的时候有些不方便。基于这一点,我重新修改了一下KeyEvent类的实现方式,提供了直接写数字键和字母键支持,比如:

var key=new KeyEvent(["a","5"],{"shiftKey":true,"ctrlKey":true});
key.keydown(function(e)){
    alert("you keydown 'a');
}

在源码里只是修改了KeyEvent构造函数而已,其他不变:

var KeyEvent = function(aKeyCode, oFlag){
    this.keyCodes = [];
	//由于本人还没有了解到字符键跟对应的键盘的keyCode直接转换的javascript内置函数,只能使用这种方式。
	var codes_swidth={"a":65,"b":66,"c":67,"d":68,"e":69,"f":70,"g":71,"h":72,"i":73,"j":74,"k":75,
"l":76,"m":77,"n":78,"o":79,"p":80,"q":81,"r":82,"s":83,"t":84,"u":85,
"v":86,"w":87,"x":88,"y":89,"z":90}
	for(var i=0,l=aKeyCode.length;i<l;i++){
		this.keyCodes[i]=typeof aKeyCode[i] === "string" ? codes_swidth[aKeyCode[i]] || aKeyCode[i].charCodeAt(0) : aKeyCode[i];
	}
    this.key = oFlag || {};
}

解析模拟事件

昨晚在《Javascript权威指南》书本中看到了“合成事件”这样的一个名词,开始怎么都不明白,看了它提供的例子,也都是糊里糊涂,不知道这样使用有什么作用,直到今天,上网几番查找,都没有讨论它的用处,都是简单介绍了createEvent,createEventObject等等方法的语法,都没有现成Demo来说明它的用途。时间从上午到了下午,最后还是回到了书本中,又重新看了一下《Javascript权威指南》中提供的例子,照着书本写例子来测试,第一次发现可以动态声明事件,类似于addEventListener和attachEvent的用途,发现的端倪,继续深入。之后继续上网查资料,发现了“模拟事件”,这个名词,哟和!!突然有些豁然开朗,对,“模拟事件”,因此就尝试在HTML元素行内声明事件,之后用createEvent和createEventObject生成新事件,在window.onload回调函数下测试,模拟成功!终于明白了它的这一点用途了,也开始想到了jQuery中的trigger函数的原理了!

下面来看看我写的一个封装的imitateEvent对象:

/*
elem:是DOM元素引用,将要添加模拟事件的DOM元素
eventType:需要模拟的事件的类型
handler:事件回调函数,这个函数可有可无。
*/
var imitateEvent = {};
imitateEvent.imitate = function(elem, eventType, handler){
    elem = typeof elem !== "object" ? document.getElementById(elem) : elem;
    //处理W3C下createEvent的第一个参数的设置,它有三种类型:Events,MouseEvents以及UIEvents
    //不过UIEvents非常少用,在此就不提供模拟这些事件
    //在这里声明Events和MouseEvents对象的目的是确定适用Events类型还是MouseEvents类型
    var Events = {
        "click": "click",
        "blur": "blure",
        "change": "change",
        "dblclick": "dblclick",
        "error": "error",
        "focus": "focus",
        "keypress": "keypress",
        "keydown": "keydown",
        "keyup": "keyup",
        "load": "load",
        "resize": "resize",
        "scroll": "scroll",
        "select": "select",
        "submit": "submit",
        "unload": "unload"
    };
    var MouseEvents = {
        "mousedown": "mousedown",
        "mousemove": "mousemove",
        "mouseout": "mouseout",
        "mouseover": "mouseover",
        "mouseup": "mouseup"
    };
    var evt = null;
    if (document.createEvent) { // W3C DOM
        //通过createEvent创建一个新事件,默认是Events
        evt = document.createEvent(Events[eventType] ? "Events" : MouseEvents[eventType] ? "MouseEvents" : "Events");
        //初始化事件
        evt.initEvent(eventType, true, false);
        //分派事件
        elem.dispatchEvent(evt);
		//如果没有提供handler函数,则默认用一个空函数代替,下面IE中也是这样设置
        elem.addEventListener(eventType, handler ? handler : function(){}, false);
    } else if (document.createEventObject) { // IE
        //通过createEventObject创建一个新事件
        evt = document.createEventObject();
        //分派事件
        elem.fireEvent("on" + eventType, evt);
      elem.attachEvent("on" + eventType, handler ? function(){ handler.call(elem);} : function(){});
    }
}

如果在DOM元素已经通过其他方式添加了事件的时候,在模拟事件对象中又提供了handler回调函数,则事件则会叠加,可能这就是所谓的“合成事件”吧。

下面是提供的Demo:

<div id="div" onclick="test();">
       Click me please.
</div>
<script type="text/javascript">
    //添加事件模拟,这样在window.onload的时候就调用了div标签的onclick事件
     //在这里提供了handler回调函数,如果没有提供的话,就只会执行其他方式声明的事件。
    imitateEvent.imitate(document.getElementById("div"), "click", function(){
       alert(this.nodeName);
    });
    function test(){
       alert("You have mouseovered me already.");
    }
</script>

上面我对于模拟事件的一点点见解,如果有错误之处,还请高手指点一二,指正错误,不胜感激!

Javascript的Lazy Definition Pattern

Lazy Definition Pattern是这样的一个模式:根据浏览器之间的解析javascript的差异性,使得创建封装的对象或者是函数的时候都需要使用浏览器的嗅探技术来做判断,而且对象或者方法每调用一次就需要去嗅探,这是一个非常不好的额外功。而解决这个瓶颈的方法之一就是Lazy Definition了。它会在浏览器第一次执行该对象或者函数的时候就记住这次的操作,以至于下面的重复调用不会再去执行浏览器的嗅探。让我们先从一个简单的addListener封装函数来一步步分析。

我们通常封装浏览器的添加事件函数的方式是使用下面的方式:

var addListener=function(el, type, handle) {
   if (el.addEventListener) {
      el.addEventListener(type, handle, false);
   }else if (el.attachEvent) {
      el.attachEvent('on' + type, handle);
   } else {
      el['on' + type] = handle;
   }
}

上面是一种使用很普遍的封装方式,它的问题之一就是口头说的,每次addListener函数调用的时候,都需要去执行if判断,以至于使用正确的方式,讨厌的if的执行次数跟调用次数相同了,不爽!

为了解决上面讨厌的if的反复执行,我们使用Lazy Definition来改造addListener方法:

var addListener = function(el, type, handle){
    if (el.addEventListener) {
        //重新定义addListener的方法体,使得下次调用addListener方法的时候就直接调用这个方法体,下同
        addListener = function(el, type, handle){
            el.addEventListener(type, handle, false);
        }
    } else
        if (el.attachEvent) {
            addListener = function(el, type, handle){
                el.attachEvent("on" + type, function(e){
                    handle.call(el, e);
                });
            }
        } else {
            addListener = function(el, type, handle){
                el["on" + type] = handle;
            }
        }
    //第一次执行addListener方法
    addListener(el, type, handle);
}

上面的addListener定义方式是YAHOO提出的一种Lazy Definition方式,它还有一种变体,我把它改成如下的方式(有点类似于分支技术):

var addListener = function(el, type, handle){
	//在这里也是重新定义addListener方法
    addListener = (function(){
        if (el.addEventListener) {
            return function(el, type, handle){
                el.addEventListener(type, handle, false);
            };
        } else if (el.attachEvent) {
                return function(el, type, handle){
                    el.attachEvent("on" + type, function(e){
                        handle.call(el, e);
                    });
                };
            } else {
                return function(el, type, handle){
                    el["on" + type] = handle;
                };
            }
    })();
    addListener(el, type, handle);
}

现在Lazy Definition的原理已经很清晰了:就是在函数内部重新自定义自己的方法体来实现。【测试页面】。下面再用一个简单的函数来表述:

var foo=function(){
    var t=new Date();
    foo=function(){
         return t;
    }
    return foo(); //用于第一次调用的时候执行
}

更多资料可以阅读:《Lazy Function Definition Pattern

EvtWithSelector模仿YUI的on方法

基于自己对YUI3中on方法的喜爱,忍不住想要去自己实现它,早期本人开发了一个cSelector库,这个库是用来通过selectors来查询DOM的,类似与jQuery的selectors功能。因此,想要实现on的操作方式就非常简单了,首先说明一下YUI中on方法的语法:

Y.on(type,fn,id,context);

id不单止是一个HTML中的id而已,它可以是css2、3中的selectors,组合查询DOM,将查询到的DOM元素添加type驱动事件fn。这种思想非常的不错。也是自己想要去模仿它的原因,一个是实践,另一个也是加深对它的思想的理解。

cSelector库中支持普通的selector,比如:

#id      .class      first-child      last-child      nth-child      >      tag      only-child     *=     $=     ^=     [attr]

这已经适合与普通的DOM查询了。EvtWithSelector里定义了一个Evt的对象,Evt有一个on方法,调用方式跟YUI3的是一样的。比如下面的一个实例:

onload=function(){
   Evt.on("click",function(){ alert(this.innerHTML);},"div");
}

最后声明:纯粹是为了实践和喜好之目的,只提供交流和漫骂。下载EvtWithSelector