diff --git a/packages/liveui/liveevents_w3c.js b/packages/liveui/liveevents_w3c.js index 45a8269d16..58a2d0e970 100644 --- a/packages/liveui/liveevents_w3c.js +++ b/packages/liveui/liveevents_w3c.js @@ -130,6 +130,8 @@ Meteor.ui._event._loadW3CImpl = function() { var eventsCaptured = {}; Meteor.ui._event.registerEventType = function(eventType, subtreeRoot) { + // We capture on the entire document, so don't actually care + // about subtreeRoot! installCapturer(eventType); }; diff --git a/packages/liveui/liveui.js b/packages/liveui/liveui.js index d5cc53a191..11170784e5 100644 --- a/packages/liveui/liveui.js +++ b/packages/liveui/liveui.js @@ -582,6 +582,9 @@ Meteor.ui = Meteor.ui || {}; // without taking enclosing ranges into account, so additional event // handlers need to be attached. var attach_secondary_events = function(range) { + // Implementations of LiveEvents that use whole-document event capture + // (all except old IE) don't actually need any of this; this function + // could be a no-op. for(var r = range; r; r = r.findParent()) { if (r === range) continue; @@ -598,6 +601,12 @@ Meteor.ui = Meteor.ui || {}; } }; + // Handle a currently-propagating event on a particular node. + // We walk all enclosing liveranges of the node, from the inside out, + // looking for matching handlers. If the app calls stopPropagation(), + // we don't stop immediately (within an event map), but we DO stop + // between ranges (i.e. templates), in keeping with the idea that + // ranges are like invisible container nodes. Meteor.ui._handleEvent = function(event) { var curNode = event.currentTarget; if (! curNode) @@ -609,7 +618,7 @@ Meteor.ui = Meteor.ui || {}; var type = event.type; - for(var range = innerRange; range; range = range.findParent(true)) { + for(var range = innerRange; range; range = range.findParent()) { if (! range.event_handlers) continue; @@ -626,6 +635,8 @@ Meteor.ui = Meteor.ui || {}; } var returnValue = h.callback.call(range.event_data, event); + // allow app to `return false` from event handler, just like + // you can in a jquery event handler if (returnValue === false) { event.stopPropagation(); event.preventDefault(); diff --git a/packages/liveui/liveui_tests.js b/packages/liveui/liveui_tests.js index faa09f649d..db30488398 100644 --- a/packages/liveui/liveui_tests.js +++ b/packages/liveui/liveui_tests.js @@ -1314,7 +1314,8 @@ Tinytest.add("liveui - basic events", function(test) { // "deep reach" from high node down to replaced low node. // Tests that attach_secondary_events actually does the - // right thing in IE. Also tests change event bubbling. + // right thing in IE. Also tests change event bubbling + // and proper interpretation of event maps. event_buf.length = 0; R = ReactiveVar('foo'); div = OnscreenDiv(Meteor.ui.render(function() { @@ -1323,13 +1324,34 @@ Tinytest.add("liveui - basic events", function(test) { return ''+R.get(); }, {events: eventmap('click input'), event_data:event_buf}) + '

'; - }, { events: eventmap('change b'), event_data:event_buf })); + }, { events: eventmap('change b', 'change input'), event_data:event_buf })); R.set('bar'); Meteor.flush(); // click on input clickElement(div.node().getElementsByTagName('input')[0]); event_buf.sort(); // don't care about order - test.equal(event_buf, ['change b', 'click input']); + test.equal(event_buf, ['change b', 'change input', 'click input']); + event_buf.length = 0; + div.kill(); + Meteor.flush(); + + // test that 'click *' fires on bubble + event_buf.length = 0; + R = ReactiveVar('foo'); + div = OnscreenDiv(Meteor.ui.render(function() { + return '

'+ + Meteor.ui.chunk(function() { + return ''+R.get(); + }, {events: eventmap('click input'), event_data:event_buf}) + + '

'; + }, { events: eventmap('click *'), event_data:event_buf })); + R.set('bar'); + Meteor.flush(); + // click on input + clickElement(div.node().getElementsByTagName('input')[0]); + test.equal( + event_buf, + ['click input', 'click *', 'click *', 'click *', 'click *', 'click *']); event_buf.length = 0; div.kill(); Meteor.flush();