diff --git a/packages/ui/backend.js b/packages/ui/backend.js index 9df448b4a9..d8bc5b5c49 100644 --- a/packages/ui/backend.js +++ b/packages/ui/backend.js @@ -12,6 +12,18 @@ if (Meteor.isClient) { var $ = Package.jquery.jQuery; var DomBackend = { + // Must use jQuery semantics for `context`, not + // querySelectorAll's. In other words, all the parts + // of `selector` must be found under `context`. + findBySelector: function (selector, context) { + return jQuery.find(selector, context); + }, + newFragment: function (nodeArray) { + // jQuery fragments are built specially in + // IE<9 so that they can safely hold HTML5 + // elements. + return $.buildFragment(nodeArray, document); + }, parseHTML: function (html) { // Return an array of nodes. // diff --git a/packages/ui/domrange.js b/packages/ui/domrange.js index b3bcb3f4d8..160df9877c 100644 --- a/packages/ui/domrange.js +++ b/packages/ui/domrange.js @@ -1,3 +1,4 @@ +var DomBackend = UI.DomBackend; var removeNode = function (n) { // if (n.nodeType === 1 && @@ -25,13 +26,6 @@ var moveNode = function (n, parent, next) { parent.insertBefore(n, next || null); }; -var newFragment = function (nodeArray) { - // jQuery fragments are built specially in - // IE<9 so that they can safely hold HTML5 - // elements. - return $.buildFragment(nodeArray, document); -}; - // A very basic operation like Underscore's `_.extend` that // copies `src`'s own, enumerable properties onto `tgt` and // returns `tgt`. @@ -74,7 +68,7 @@ var DomRange = function (component) { // detection too. var start = document.createTextNode(""); var end = document.createTextNode(""); - var fragment = newFragment([start, end]); + var fragment = DomBackend.newFragment([start, end]); if (component) { this.component = component; @@ -212,6 +206,8 @@ _extend(DomRange.prototype, { if (typeof newMember.nodeType !== 'number') throw new Error("Expected Component or Node"); var node = newMember; + // can't attach `$ui` to a TextNode in IE 8, so + // don't bother on any browser. if (node.nodeType !== 3) node.$ui = this.component; @@ -455,6 +451,8 @@ _extend(DomRange.prototype, { n && ! n.$ui; n = n.previousSibling) { this.members[this.nextMemberId++] = n; + // can't attach `$ui` to a TextNode in IE 8, so + // don't bother on any browser. if (n.nodeType !== 3) n.$ui = this.component; } @@ -652,6 +650,52 @@ var moveWithOwnersIntoTbody = function (range) { ///// FIND BY SELECTOR +DomRange.prototype.contains = function (compOrNode) { + if (! compOrNode) + throw new Error("Expected Component or Node"); + + var parentNode = this.parentNode(); + if (! parentNode) + return false; + + var range; + if (! compOrNode) + debugger; + if ('dom' in compOrNode) { + // Component + range = compOrNode.dom; + var pn = range.parentNode(); + if (! pn) + return false; + // If parentNode is different, it must be a node + // we contain. + if (pn !== parentNode) + return this.contains(pn); + if (range === this) + return false; // don't contain self + // Ok, `range` is a same-parent range to see if we + // contain. + } else { + // Node + var node = compOrNode; + if (! elementContains(parentNode, node)) + return false; + + while (node.parentNode !== parentNode) + node = node.parentNode; + + range = node.$ui && node.$ui.dom; + } + + // Now see if `range` is truthy and either `this` + // or an immediate subrange + + while (range && range !== this) + range = range.owner && range.owner.dom; + + return range === this; +}; + DomRange.prototype.$ = function (selector) { var self = this; @@ -667,10 +711,18 @@ DomRange.prototype.$ = function (selector) { // so if performance is an issue, the selector should be // run on a child element. + // Since jQuery can't run selectors on a DocumentFragment, + // we don't expect findBySelector to work. + if (parentNode.nodeType === 11) // DocumentFragment + throw new Error("Can't use $ on a detached component"); + + var results = DomBackend.findBySelector(selector, parentNode); + // We don't assume `results` has jQuery API; a plain array // should do just as well. However, if we do have a jQuery - // array, we want to end up with one also. - var results = $(selector, parentNode); + // array, we want to end up with one also, so we use + // `.filter`. + // Function that selects only elements that are actually // in this DomRange, rather than simply descending from @@ -681,14 +733,7 @@ DomRange.prototype.$ = function (selector) { if (typeof elem === 'number') elem = this; - while (elem.parentNode !== parentNode) - elem = elem.parentNode; - - var range = elem.$ui && elem.$ui.dom; - while (range && range !== self) - range = range.owner && range.owner.dom; - - return range === self; + return self.contains(elem); }; if (! results.filter) { @@ -764,6 +809,8 @@ DomRange.prototype.on = function (events, selector, handler) { if ((! selector) && evt.currentTarget !== evt.target) // no selector means only fire on target return; + if (! h.$ui.dom.contains(evt.currentTarget)) + return; return h.handler.call(h.$ui, evt); }; })(handlerRecord); @@ -775,4 +822,29 @@ DomRange.prototype.on = function (events, selector, handler) { } }; + // Returns true if element a contains node b and is not node b. + var elementContains = function (a, b) { + if (a.nodeType !== 1) // ELEMENT + return false; + if (a === b) + return false; + + if (a.compareDocumentPosition) { + return a.compareDocumentPosition(b) & 0x10; + } else { + // Should be only old IE and maybe other old browsers here. + // Modern Safari has both functions but seems to get contains() wrong. + // IE can't handle b being a text node. We work around this + // by doing a direct parent test now. + b = b.parentNode; + if (! (b && b.nodeType === 1)) // ELEMENT + return false; + if (a === b) + return true; + + return a.contains(b); + } + }; + + UI.DomRange = DomRange; \ No newline at end of file diff --git a/packages/ui/domrange_tests.js b/packages/ui/domrange_tests.js index f2ebb8065c..de42360058 100644 --- a/packages/ui/domrange_tests.js +++ b/packages/ui/domrange_tests.js @@ -29,6 +29,14 @@ var inDocument = function (range, func) { } }; +var htmlRange = function (html) { + var r = new DomRange; + _.each(parseHTML(html), function (node) { + r.add(node); + }); + return r; +}; + Tinytest.add("ui - DomRange - basic", function (test) { var r = new DomRange; r.which = 'R'; @@ -622,14 +630,6 @@ Tinytest.add("ui - DomRange - basic events", function (test) { } }; - var htmlRange = function (html) { - var r = new DomRange; - _.each(parseHTML(html), function (node) { - r.add(node); - }); - return r; - }; - inDocument( htmlRange("Foo"), function (r) { @@ -662,6 +662,78 @@ Tinytest.add("ui - DomRange - basic events", function (test) { arrayEqual(buf, [['click', span, span]]); }); + inDocument( + htmlRange('
Foo
' + + '
Bar
'), + function (r) { + var buf = []; + + // test click on particular div, which is + // not the target or the bound element + r.on('click', '#yeah', function (evt) { + buf.push([evt.type, evt.target, evt.currentTarget]); + }); + + arrayEqual(buf, []); + r.$('#no')[0].click(); + arrayEqual(buf, []); + var yeah = r.$('#yeah')[0]; + yeah.click(); + arrayEqual(buf, [['click', yeah, yeah]]); + }); + + inDocument( + new DomRange, + function (r) { + var s; + r.add(s = htmlRange('
')); + r.add(htmlRange('
')); + var one = r.$('#one')[0]; + var two = r.$('#two')[0]; + + var buf = []; + + // test that click must be in range to fire + // event handler + s.on('click', 'div', function (evt) { + buf.push([evt.type, evt.target, evt.currentTarget]); + }); + + arrayEqual(buf, []); + two.click(); + arrayEqual(buf, []); + one.click(); + arrayEqual(buf, [['click', one, one]]); + }); + +}); + +Tinytest.add("ui - DomRange - contains", function (test) { + inDocument(new DomRange, function (r) { + var s = htmlRange('
Foo
'); + var t = new DomRange; + t.add(s); + r.add(t); + r.add(htmlRange('
')); + var one = r.$('#one')[0]; + var two = r.$('#two')[0]; + var span = r.$('span')[0]; + + test.isFalse(r.contains(r)); + test.isTrue(r.contains(s)); + test.isTrue(r.contains(t)); + test.isTrue(r.contains(one)); + test.isTrue(s.contains(one)); + test.isTrue(t.contains(one)); + test.isTrue(r.contains(two)); + test.isFalse(s.contains(two)); + test.isFalse(t.contains(two)); + test.isTrue(r.contains(span)); + test.isTrue(s.contains(span)); + test.isTrue(t.contains(span)); + test.isFalse(r.contains(r.parentNode)); + test.isFalse(r.contains(document.createElement("DIV"))); + }); }); // TO TEST STILL: