DomRange.contains, tests

This commit is contained in:
David Greenspan
2013-08-30 22:51:35 -07:00
parent f0c5fe1313
commit faa60df20e
3 changed files with 182 additions and 26 deletions

View File

@@ -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.
//

View File

@@ -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;

View File

@@ -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("<span>Foo</span>"),
function (r) {
@@ -662,6 +662,78 @@ Tinytest.add("ui - DomRange - basic events", function (test) {
arrayEqual(buf, [['click', span, span]]);
});
inDocument(
htmlRange('<div id="yeah"><span>Foo</span></div>' +
'<div id="no">Bar</div>'),
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('<div id="one"></div>'));
r.add(htmlRange('<div id="two"></div>'));
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('<div id="one"><span>Foo</span></div>');
var t = new DomRange;
t.add(s);
r.add(t);
r.add(htmlRange('<div id="two"></div>'));
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: