event capturing

This commit is contained in:
David Greenspan
2013-09-02 13:14:25 -07:00
parent 92afed3922
commit 16d823dce5
2 changed files with 111 additions and 12 deletions

View File

@@ -9,7 +9,7 @@ if (Meteor.isClient) {
if (! Package.jquery)
throw new Error("Meteor UI jQuery adapter: jQuery not found.");
var $ = Package.jquery.jQuery;
var jQuery = Package.jquery.jQuery;
var DomBackend = {
// Must use jQuery semantics for `context`, not
@@ -22,7 +22,7 @@ if (Meteor.isClient) {
// jQuery fragments are built specially in
// IE<9 so that they can safely hold HTML5
// elements.
return $.buildFragment(nodeArray, document);
return jQuery.buildFragment(nodeArray, document);
},
parseHTML: function (html) {
// Return an array of nodes.
@@ -30,10 +30,50 @@ if (Meteor.isClient) {
// jQuery does fancy stuff like creating an appropriate
// container element and setting innerHTML on it, as well
// as working around various IE quirks.
return $.parseHTML(html);
return jQuery.parseHTML(html);
},
// `selector` is non-null. `type` is one type (but
// may be in backend-specific form, e.g. have namespaces).
delegateEvents: function (elem, type, selector, handler) {
$(elem).on(type, selector, handler);
},
undelegateEvents: function (elem, type, handler) {
$(elem).off(type, handler);
},
bindEventCapturer: function (elem, type, handler) {
var wrapper = function (event) {
event = jQuery.event.fix(event);
event.currentTarget = event.target;
// XXX maybe could fire more jQuery-specific stuff
// here, like special event hooks? At the end of the
// day, though, jQuery just can't bind capturing
// handlers, and if we're not putting the handler
// in jQuery's queue, we can't call high-level
// internal funcs like `dispatch`.
handler.call(elem, event);
};
handler._meteorui_wrapper = wrapper;
type = this.parseEventType(type);
// add *capturing* event listener
elem.addEventListener(type, wrapper, true);
},
unbindEventCapturer: function (elem, type, handler) {
type = this.parseEventType(type);
elem.removeEventListener(type, handler._meteorui_wrapper);
},
parseEventType: function (type) {
// strip off namespaces
var dotLoc = type.indexOf('.');
if (dotLoc >= 0)
return type.slice(0, dotLoc);
return type;
},
// XXX EVERYTHING BELOW THIS POINT IS A WORK IN PROGRESS XXX
watchElement: function (elem) {
$(elem).on('meteor_ui_domrange_gc', $.noop);
jQuery(elem).on('meteor_ui_domrange_gc', jQuery.noop);
},
// Called when an element is removed from the DOM using the
// back-end library directly, either by removing it directly
@@ -44,7 +84,7 @@ if (Meteor.isClient) {
};
// See http://bugs.jquery.com/ticket/12213#comment:23
$.event.special.meteor_ui_domrange_gc = {
jQuery.event.special.meteor_ui_domrange_gc = {
teardown: function() {
DomBackend.onRemoveElement(this);
}

View File

@@ -583,6 +583,8 @@ DomRange.prototype.elements = function (intoArray) {
// In this case, Sortable wants to call `refresh`
// on the div, not the each, so it would use this function.
DomRange.refresh = function (element) {
// note: this "top-level ranges" code could be its
// own API call.
var topLevelRanges = [];
for (var n = element.firstChild;
n; n = n.nextSibling) {
@@ -772,6 +774,23 @@ DomRange.prototype.$ = function (selector) {
///// EVENTS
// List of events to always delegate, never capture.
// Since jQuery fakes bubbling for certain events in
// certain browsers (like `submit`), we don't want to
// get in its way.
//
// We could list all known bubbling
// events here to avoid creating speculative capturers
// for them, but it would only be an optimization.
var eventsToDelegate = {
blur: 1, change: 1, click: 1, focus: 1, focusin: 1,
focusout: 1, reset: 1, submit: 1
};
var EVENT_MODE_TBD = 0;
var EVENT_MODE_BUBBLING = 1;
var EVENT_MODE_CAPTURING = 2;
// XXX could write the form of arguments for this function
// in several different ways, including simply as an event map.
DomRange.prototype.on = function (events, selector, handler) {
@@ -811,15 +830,20 @@ DomRange.prototype.on = function (events, selector, handler) {
type: type,
selector: selector,
$ui: this.component,
handler: handler
handler: handler,
mode: EVENT_MODE_TBD
};
// It's important that lowLevelHandler be a different
info.handlers.push(handlerRecord);
// It's important that delegatedHandler be a different
// instance for each handlerRecord, because its identity
// is used to remove it. Capture handlerRecord in a
// is used to remove it.
//
// Capture handlerRecord in a
// closure so that we have access to it, even when
// the var changes, and so we don't pull in the rest of
// the stack frame.
handlerRecord.lowLevelHandler = (function (h) {
handlerRecord.delegatedHandler = (function (h) {
return function (evt) {
if ((! selector) && evt.currentTarget !== evt.target)
// no selector means only fire on target
@@ -830,10 +854,45 @@ DomRange.prototype.on = function (events, selector, handler) {
};
})(handlerRecord);
info.handlers.push(handlerRecord);
var tryCapturing = (! eventsToDelegate.hasOwnProperty(
DomBackend.parseEventType(type)));
$(parentNode).on(type, selector || '*',
handlerRecord.lowLevelHandler);
if (tryCapturing) {
handlerRecord.capturingHandler = (function (h) {
return function (evt) {
if (h.mode === EVENT_MODE_TBD) {
// must be first time we're called.
if (evt.bubbles) {
// this type of event bubbles, so don't
// get called again.
h.mode = EVENT_MODE_BUBBLING;
DomBackend.unbindEventCapturer(
h.elem, h.type, h.capturingHandler);
return;
} else {
// this type of event doesn't bubble,
// so unbind the delegation, preventing
// it from ever firing.
h.mode = EVENT_MODE_CAPTURING;
DomBackend.undelegateEvents(
h.elem. hType, h.delegatedHandler);
}
}
h.delegatedHandler(evt);
};
})(handlerRecord);
DomBackend.bindEventCapturer(
parentNode, type,
handlerRecord.capturingHandler);
} else {
handlerRecord.mode = EVENT_MODE_BUBBLING;
}
DomBackend.delegateEvents(parentNode, type,
selector || '*',
handlerRecord.delegatedHandler);
}
};