jQuery.live っぽい実装
submit, focus, blur, change をクロスブラウザにする方法を追記しました。
最新版のコードを追記しました。
デモを追加しました。
jQuery.live() は
- document.addEventListener(type, fn, capture) で、天辺までバブルアップしてくるイベントを拾う
- 拾ったイベントが CSS のセレクタ式(expr) に一致するか比較
- 一致していれば登録されている fn をコールバック
という処理をやっているようです。
# 注意: jQuery のコードを見ずにしゃべってるよ
そんな感じのを実装してみた
var _livedb = {}; // { "expr\vnamespace.click": {...}, ... } uu = uumix(_uujamfactory, { // uu(expr, ctx) -> Instance(jam) // --- event.live --- live: uumix(uulive, { // uu.live("css > expr", "namespace.click", fn) has: uulivehas, // uu.live.has("css > expr", "namespace.click") -> Boolean unbind: uuliveunbind // uu.live.unbind("css > expr" = void 0, "namespace.click" = void 0) }) }); // uu.live function uulive(expr, // @param String: "css > expr" nstype, // @param String: "namespace.click" fn) { // @param Function: callback fn(evt, node, src) function _uuevliveclosure(evt) { evt = evt || win.event; var src = evt.srcElement || evt.target; src = (_webkit && src.nodeType === 3) ? src.parentNode : src; if (uuquerymatch(expr, src)) { // document.matchesSelector evt.node = doc; evt.code = (_EVCODE[evt.type] || 0) & 255; evt.src = src; evt.px = _ie ? evt.clientX + uupub.iebody.scrollLeft : evt.pageX; evt.py = _ie ? evt.clientY + uupub.iebody.scrollTop : evt.pageY; evt.ox = evt.offsetX || evt.layerX || 0; // [offsetX] IE, Opera, WebKit evt.oy = evt.offsetY || evt.layerY || 0; // [layerX] Gecko, WebKit handler.call(fn, evt, doc, src); } } if (!uulivehas(expr, nstype)) { var ary = nstype.split("."), // "namespace.click" -> ["namespace", "click"] type = ary.pop(), ns = ary.pop() || "", handler = uuisfunc(fn) ? fn : fn.handleEvent, closure = fn.uuevliveclosure = _uuevliveclosure; _livedb[expr + "\v" + nstype] = { expr: expr, ns: ns, type: type, nstype: nstype, fn: closure }; uuevattach(doc, _EVFIX[type] || type, closure); // document.addEvenetListener } } // uu.live.has function uulivehas(expr, // @param String: "css > expr" nstype) { // @param String: "namespace.click" var db = _livedb[expr + "\v" + nstype]; return db && expr === db.expr && nstype === db.nstype; } // uu.live.unbind // [1][unbind all] uu.live.unbind() // [2][unbind all] uu.live.unbind("expr") // [3][unbind one] uu.live.unbind("expr", "click") // [4][unbind namespace all] uu.live.unbind("expr", "namespace.*") // [5][unbind namespace one] uu.live.unbind("expr", "namespace.click") function uuliveunbind(expr, // @param String(= void 0): "css > expr" nstype) { // @param String(= void 0): "namespace.click" var ns, v, i, r, mode = !expr ? 1 : // [1] !nstype ? 2 : // [2] nstype.indexOf("*") < 0 ? 3 : // [3][5] (ns = nstype.slice(0, -2), 4); // [4] "namespace.*" -> "namespace" for (i in _livedb) { // i = "expr\vnamespace.click" v = _livedb[i]; // v = { expr, ns, type, nstype, closure } r = 1; switch (mode) { case 2: r = expr === v.expr; break; // [2] case 3: r = expr === v.expr && nstype === v.nstype; break; // [3][5] case 4: r = expr === v.expr && ns === v.ns; // [4] } if (r) { uuevdetach(doc, v.type, v.fn); delete _livedb[i]; } } }
どのイベントがバブルアップするのかを調査
DOM の仕様: http://www.w3.org/TR/DOM-Level-2-Events/events.html
IE のオレオレ仕様: http://msdn.microsoft.com/en-us/library/ms533051(VS.85).aspx
Bubbles
Event Type | DOM(IE) | uu.live() で利用可能なイベント |
abort | yes(no) | × |
blur | no(no) | × |
DOMFocusOut(onfocusout) | yes(yes) | ○ uu.live(expr, "blur") |
change | yes(no) | ○ uu.live(expr, "change") |
click | yes(yes) | ○ |
error | yes(no) | × |
focus | no(no) | × |
DOMFocusIn(onfocusin) | yes(yes) | ○ uu.live(expr, "focus") |
keydown | yes(yes) | ○ |
keypress | yes(yes) | ○ |
keyup | yes(yes) | ○ |
load | no(no) | × |
mousedown | yes(yes) | ○ |
mousemove | yes(yes) | ○ |
mouseout | yes(yes) | ○ |
mouseover | yes(yes) | ○ |
mouseup | yes(yes) | ○ |
mousewheel gecko:DOMMouseScroll |
yes(yes) | ○ |
reset | yes(no) | × |
resize | yes(no) | × |
scroll | yes(no) | × |
select | yes(no) | × |
submit | yes(no) | ○ uu.live(expr, "submit") |
unload | no(no) | × |
この表で yes(yes) となっているイベントがクロスブラウザであり、live() で利用できるイベントということになります。
# といっても実際にはそんなに甘くなかった
デモ
http://pigs.sourceforge.jp/blog/20091231/
- uu.live.submit.htm は uu.live(expr, "submit"); のデモです。
- uu.live.focus.htm は uu.live(expr, "focus"); と uu.live(expr, "blur") のデモです。
- uu.live.change.htm は uu.live(expr, "change"); のデモです。
- uu.ev.change.htm は通常のイベント uu.ev(expr, "change"); のデモです。uu.live 版との動作比較用です。
追記
submit を IE で拾えるようするには
IE の submit イベントはバブルアップしないので、
uu.live("form", "submit") を
内部的に、uu.live("form input[type=submit],form input[type=image]", "click") に変換して回避
if (uu.ie) { // uu.live("form", "submit") -> // uu.live("form input[type=submit],form input[type=image]", "click") if (/submit$/.test(type)) { _uulive(expr + " input[type=submit]," + expr + " input[type=image]", nstype.replace(/submit$/, "click"), fn, hash); } }
focus(DOMFocusIn), blur(DOMFocusOut) を gecko で拾えるようにするには
gecko は DOMFocusIn と DOMFocusOut を無視するので、
document.addEventListener("focus", fn, true); // capture
document.addEventListener("blur", fn, true); // capture
相当の処理を追加。要はキャプチャーフェーズで focus, blur イベントを拾って処理する
if (uu.gecko) { // http://help.dottoro.com/ljuoivsj.php // http://twitter.com/uupaa/status/7221096300 (type === "focus" || type === "blur") && (capt = 1); }
gecko 以外のブラウザは、イベントタイプを以下のように変換することで処理
var _LIVEFIX = uu.ie ? { focus: "focusin", blur: "focusout" } : uu.gecko ? {} : { focus: "DOMFocusIn", blur: "DOMFocusOut" }; uu.ev.attach(doc, _LIVEFIX[type] || type, closure, capt)
change イベントを IE で拾えるようにするには
IE の change イベントはバブルアップしません。
2時間悩んで…
- onfocusin, onfocusout はバブルアップするので、onfocusin と onfocusout を設定して待機
- onfocusin が発生したら event.srcElement に対し、change イベントを設定
- onfocusout が発生したら event.srcElement の change イベントを解除
というトリックを考えました。
if (uu.ie) { if (/change$/.test(type)) { _uulive(expr, nstype.replace(/change$/, "focus"), function(evt) { uu.ev(evt.srcElement, "uulivehook.change", fn); }, hash); _uulive(expr, nstype.replace(/change$/, "blur"), function(evt) { uu.ev.unbind(evt.srcElement, "uulivehook.change"); }, hash); } }
submit, focus, blur, change 実装後のソースコード
_uulive() に Gecko と IE 向けのコードが追加されています。
// === Live === // depend: uu.js // http://d.hatena.ne.jp/uupaa/20091231 uu.waste || (function(win, doc, uu) { var _livedb = {}, // { "expr\vnamespace.click": {...}, ... } _LIVEFIX = uu.ie ? { focus: "focusin", blur: "focusout" } : uu.gecko ? {} : { focus: "DOMFocusIn", blur: "DOMFocusOut" }; // uu.live uu.live = uu.mix(uulive, { // uu.live("css > expr", "namespace.click", fn) has: uulivehas, // uu.live.has("css > expr", "namespace.click") -> Boolean unbind: uuliveunbind // uu.live.unbind("css > expr" = void 0, "namespace.click" = void 0) }); // uu.match uu.match = uumatch; // uu.match("p > a", NodeArray/Node, rtype = 0) -> Boolean/NodeArray // uu.live function uulive(expr, // @param String: "css > expr" nstype, // @param String: "namespace.click" fn) { // @param Function: callback fn(evt, node, src) _uulive(expr, nstype, fn); } // inner - function _uulive(expr, nstype, fn, hash) { function _uuliveclosure(evt) { evt = evt || win.event; var src = evt.srcElement || evt.target; src = (uu.webkit && src.nodeType === 3) ? src.parentNode : src; if (uu.match(expr, src)) { evt.node = doc; evt.code = (uupub.EVCODE[evt.type] || 0) & 255; evt.src = src; evt.px = uu.ie ? evt.clientX + uupub.iebody.scrollLeft : evt.pageX; evt.py = uu.ie ? evt.clientY + uupub.iebody.scrollTop : evt.pageY; evt.ox = evt.offsetX || evt.layerX || 0; // [offsetX] IE, Opera, WebKit evt.oy = evt.offsetY || evt.layerY || 0; // [layerX] Gecko, WebKit handler.call(fn, evt, doc, src); } } if (!uulivehas(expr, nstype)) { var ary = nstype.split("."), // "namespace.click" -> ["namespace", "click"] type = ary.pop(), ns = ary.pop() || "", capt = 0, handler = uu.isfunc(fn) ? fn : fn.handleEvent, closure = fn.uuevliveclosure = _uuliveclosure; hash || (hash = _livedb[expr + "\v" + nstype] = { expr: expr, ns: ns, type: type, nstype: nstype, unbind: [] }); if (uu.gecko) { (type === "focus" || type === "blur") && (capt = 1); } hash.unbind.push(function() { uu.ev.detach(doc, _LIVEFIX[type] || type, closure, capt); }); uu.ev.attach(doc, _LIVEFIX[type] || type, closure, capt); if (uu.ie) { if (/submit$/.test(type)) { _uulive(expr + " input[type=submit]," + expr + " input[type=image]", nstype.replace(/submit$/, "click"), fn, hash); } else if (/change$/.test(type)) { _uulive(expr, nstype.replace(/change$/, "focus"), function(evt) { uu.ev(evt.srcElement, "uulivehook.change", fn); }, hash); _uulive(expr, nstype.replace(/change$/, "blur"), function(evt) { uu.ev.unbind(evt.srcElement, "uulivehook.change"); }, hash); } } } } // uu.live.has function uulivehas(expr, // @param String: "css > expr" nstype) { // @param String: "namespace.click" var db = _livedb[expr + "\v" + nstype]; return db && expr === db.expr && nstype === db.nstype; } // uu.live.unbind // [1][unbind all] uu.live.unbind() // [2][unbind all] uu.live.unbind("expr") // [3][unbind one] uu.live.unbind("expr", "click") // [4][unbind namespace all] uu.live.unbind("expr", "namespace.*") // [5][unbind namespace one] uu.live.unbind("expr", "namespace.click") function uuliveunbind(expr, // @param String(= void 0): "css > expr" nstype) { // @param String(= void 0): "namespace.click" var ns, v, i, r, mode = !expr ? 1 : // [1] !nstype ? 2 : // [2] nstype.indexOf("*") < 0 ? 3 : // [3][5] (ns = nstype.slice(0, -2), 4); // [4] "namespace.*" -> "namespace" for (i in _livedb) { // i = "expr\vnamespace.click" v = _livedb[i]; // v = { expr, ns, type, nstype, closure } r = 1; switch (mode) { case 2: r = expr === v.expr; break; // [2] case 3: r = expr === v.expr && nstype === v.nstype; break; // [3][5] case 4: r = expr === v.expr && ns === v.ns; // [4] } if (r) { v.unbind.forEach(function(v) { v(); }); delete _livedb[i]; } } } // uu.match - document.matchesSelector like function function uumatch(expr, // @param String: "css > expr" ctx, // @param NodeArray/Node: match context rtype) { // @param Number(= 0): result type, // 0 is Boolean result, matches all, // 1 is Boolean result, matches any, // 2 is NodeArray result, matches array // @return Boolean/NodeArray: ctx = ctx.nodeType ? [ctx] : ctx; var rv = [], hash = {}, v, w, i = 0, j = 0, ary = uu.query(expr, doc); if (ctx.length === 1) { v = ctx[0]; while ( (w = ary[i++]) ) { if (v === w) { rv.push(v); break; } } } else { while ( (v = ary[i++]) ) { hash[v.uuguid || uu.nodeid(v)] = 1; } while ( (v = ctx[j++]) ) { (v.uuguid || uu.nodeid(v)) in hash && rv.push(v); } } return !rtype ? rv.length === ctx.length : rtype < 2 ? !!rv.length : rv; } })(window, document, uu);
リンク
- DOM: http://www.w3.org/TR/DOM-Level-2-Events/events.html
- MSDN: http://msdn.microsoft.com/en-us/library/ms533051(VS.85).aspx
- focus: http://help.dottoro.com/ljuoivsj.php
- focus: http://twitter.com/uupaa/status/7221096300
問題が無ければ次回のリリースから利用可能になります。
反省会
- uu.live() は今朝の 04:00頃から実装を開始して、実装完了が 16:50
- 100行実装するのに約12時間(+ 休憩)。
- C や Java なら8時間で1000行ぐらい書ける事もあるけど、ブラウザが本来サポートしていない機能を攻略しつつだから、12時間で100行は悪くないと思うよ。
- 100行実装するのに約12時間(+ 休憩)。
おまけ
Cancelable 一覧
Event Type | DOM(IE) |
abort | no(yes) |
blur | no(no) |
change | no(no) |
click | yes(yes) |
error | no(yes) |
focus | no(no) |
keydown | 不明(yes) |
keypress | 不明(yes) |
keyup | 不明(no) |
load | no(no) |
mousedown | yes(yes) |
mousemove | no(no) |
mouseout | yes(no) |
mouseover | yes(yes) |
mouseup | yes(yes) |
mousewheel gecko:DOMMouseScroll |
yes(yes) |
reset | no(yes) |
resize | no(no) |
scroll | no(no) |
select | no(yes) |
submit | yes(yes) |
unload | no(no) |
DOMFocusIn(onfocusin) | no(no) |
DOMFocusOut(onfocusout) | no(no) |