diff --git a/packages/ui/backend.js b/packages/ui/backend.js index d8bc5b5c49..469e3d6849 100644 --- a/packages/ui/backend.js +++ b/packages/ui/backend.js @@ -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); } diff --git a/packages/ui/domrange.js b/packages/ui/domrange.js index f6f3a91ff1..3b0447786e 100644 --- a/packages/ui/domrange.js +++ b/packages/ui/domrange.js @@ -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); } };