mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Make DOMRange stack traces more better
Switch all methods to `DOMRange.prototype.myMethod =` style. Maybe we should just switch JSClass to this style.
This commit is contained in:
@@ -99,301 +99,314 @@ DOMRange.forElement = function (elem) {
|
||||
return range;
|
||||
};
|
||||
|
||||
_.extend(DOMRange.prototype, {
|
||||
attach: function (parentElement, nextNode, _isMove) {
|
||||
// This method is called to insert the DOMRange into the DOM for
|
||||
// the first time, but it's also used internally when
|
||||
// updating the DOM.
|
||||
//
|
||||
// If _isMove is true, move this attached range to a different
|
||||
// location under the same parentElement.
|
||||
if (_isMove) {
|
||||
if (! (this.parentElement === parentElement &&
|
||||
this.attached))
|
||||
throw new Error("Can only move an attached DOMRange, and only under the same parent element");
|
||||
}
|
||||
|
||||
var members = this.members;
|
||||
if (members.length) {
|
||||
this.emptyRangePlaceholder = null;
|
||||
for (var i = 0; i < members.length; i++) {
|
||||
DOMRange._insert(members[i], parentElement, nextNode, _isMove);
|
||||
}
|
||||
} else {
|
||||
var placeholder = document.createTextNode("");
|
||||
this.emptyRangePlaceholder = placeholder;
|
||||
parentElement.insertBefore(placeholder, nextNode || null);
|
||||
}
|
||||
this.attached = true;
|
||||
this.parentElement = parentElement;
|
||||
|
||||
if (! _isMove) {
|
||||
for(var i = 0; i < this.augmenters.length; i++)
|
||||
this.augmenters[i].attach(this, parentElement);
|
||||
}
|
||||
},
|
||||
setMembers: function (newNodeAndRangeArray) {
|
||||
var newMembers = newNodeAndRangeArray;
|
||||
if (! (newMembers && (typeof newMembers.length) === 'number'))
|
||||
throw new Error("Expected array");
|
||||
|
||||
var oldMembers = this.members;
|
||||
|
||||
|
||||
for (var i = 0; i < oldMembers.length; i++)
|
||||
this._memberOut(oldMembers[i]);
|
||||
for (var i = 0; i < newMembers.length; i++)
|
||||
this._memberIn(newMembers[i]);
|
||||
|
||||
if (! this.attached) {
|
||||
this.members = newMembers;
|
||||
} else {
|
||||
// don't do anything if we're going from empty to empty
|
||||
if (newMembers.length || oldMembers.length) {
|
||||
// detach the old members and insert the new members
|
||||
var nextNode = this.lastNode().nextSibling;
|
||||
var parentElement = this.parentElement;
|
||||
this.detach();
|
||||
this.members = newMembers;
|
||||
this.attach(parentElement, nextNode);
|
||||
}
|
||||
}
|
||||
},
|
||||
firstNode: function () {
|
||||
if (! this.attached)
|
||||
throw new Error("Must be attached");
|
||||
|
||||
if (! this.members.length)
|
||||
return this.emptyRangePlaceholder;
|
||||
|
||||
var m = this.members[0];
|
||||
return (m instanceof DOMRange) ? m.firstNode() : m;
|
||||
},
|
||||
lastNode: function () {
|
||||
if (! this.attached)
|
||||
throw new Error("Must be attached");
|
||||
|
||||
if (! this.members.length)
|
||||
return this.emptyRangePlaceholder;
|
||||
|
||||
var m = this.members[this.members.length - 1];
|
||||
return (m instanceof DOMRange) ? m.lastNode() : m;
|
||||
},
|
||||
detach: function () {
|
||||
if (! this.attached)
|
||||
throw new Error("Must be attached");
|
||||
|
||||
var oldParentElement = this.parentElement;
|
||||
var members = this.members;
|
||||
if (members.length) {
|
||||
for (var i = 0; i < members.length; i++) {
|
||||
DOMRange._remove(members[i]);
|
||||
}
|
||||
} else {
|
||||
var placeholder = this.emptyRangePlaceholder;
|
||||
this.parentElement.removeChild(placeholder);
|
||||
this.emptyRangePlaceholder = null;
|
||||
}
|
||||
this.attached = false;
|
||||
this.parentElement = null;
|
||||
|
||||
for(var i = 0; i < this.augmenters.length; i++)
|
||||
this.augmenters[i].detach(this, oldParentElement);
|
||||
},
|
||||
addMember: function (newMember, atIndex, _isMove) {
|
||||
var members = this.members;
|
||||
if (! (atIndex >= 0 && atIndex <= members.length))
|
||||
throw new Error("Bad index in range.addMember: " + atIndex);
|
||||
|
||||
if (! _isMove)
|
||||
this._memberIn(newMember);
|
||||
|
||||
if (! this.attached) {
|
||||
// currently detached; just updated members
|
||||
members.splice(atIndex, 0, newMember);
|
||||
} else if (members.length === 0) {
|
||||
// empty; use the empty-to-nonempty handling of setMembers
|
||||
this.setMembers([newMember]);
|
||||
} else {
|
||||
var nextNode;
|
||||
if (atIndex === members.length) {
|
||||
// insert at end
|
||||
nextNode = this.lastNode().nextSibling;
|
||||
} else {
|
||||
var m = members[atIndex];
|
||||
nextNode = (m instanceof DOMRange) ? m.firstNode() : m;
|
||||
}
|
||||
members.splice(atIndex, 0, newMember);
|
||||
DOMRange._insert(newMember, this.parentElement, nextNode, _isMove);
|
||||
}
|
||||
},
|
||||
removeMember: function (atIndex, _isMove) {
|
||||
var members = this.members;
|
||||
if (! (atIndex >= 0 && atIndex < members.length))
|
||||
throw new Error("Bad index in range.removeMember: " + atIndex);
|
||||
|
||||
if (_isMove) {
|
||||
members.splice(atIndex, 1);
|
||||
} else {
|
||||
var oldMember = members[atIndex];
|
||||
this._memberOut(oldMember);
|
||||
|
||||
if (members.length === 1) {
|
||||
// becoming empty; use the logic in setMembers
|
||||
this.setMembers(_emptyArray);
|
||||
} else {
|
||||
members.splice(atIndex, 1);
|
||||
if (this.attached)
|
||||
DOMRange._remove(oldMember);
|
||||
}
|
||||
}
|
||||
},
|
||||
moveMember: function (oldIndex, newIndex) {
|
||||
var member = this.members[oldIndex];
|
||||
this.removeMember(oldIndex, true /*_isMove*/);
|
||||
this.addMember(member, newIndex, true /*_isMove*/);
|
||||
},
|
||||
getMember: function (atIndex) {
|
||||
var members = this.members;
|
||||
if (! (atIndex >= 0 && atIndex < members.length))
|
||||
throw new Error("Bad index in range.getMember: " + atIndex);
|
||||
return this.members[atIndex];
|
||||
},
|
||||
stop: function () {
|
||||
var stopCallbacks = this.stopCallbacks;
|
||||
for (var i = 0; i < stopCallbacks.length; i++)
|
||||
stopCallbacks[i].call(this);
|
||||
this.stopCallbacks = _emptyArray;
|
||||
},
|
||||
onstop: function (cb) {
|
||||
if (this.stopCallbacks === _emptyArray)
|
||||
this.stopCallbacks = [];
|
||||
this.stopCallbacks.push(cb);
|
||||
},
|
||||
_memberIn: function (m) {
|
||||
if (m instanceof DOMRange)
|
||||
m.parentRange = this;
|
||||
else if (m.nodeType === 1) // DOM Element
|
||||
m.$blaze_range = this;
|
||||
},
|
||||
_memberOut: function (m) {
|
||||
// old members are almost always GCed immediately.
|
||||
// to avoid the potentialy performance hit of deleting
|
||||
// a property, we simple null it out.
|
||||
if (m instanceof DOMRange)
|
||||
m.parentRange = null;
|
||||
else if (m.nodeType === 1) // DOM Element
|
||||
m.$blaze_range = null;
|
||||
},
|
||||
containsElement: function (elem) {
|
||||
if (! this.attached)
|
||||
throw new Error("Must be attached");
|
||||
|
||||
// An element is contained in this DOMRange if it's possible to
|
||||
// reach it by walking parent pointers, first through the DOM and
|
||||
// then parentRange pointers. In other words, the element or some
|
||||
// ancestor of it is at our level of the DOM (a child of our
|
||||
// parentElement), and this element is one of our members or
|
||||
// is a member of a descendant Range.
|
||||
|
||||
if (! Blaze._elementContains(this.parentElement, elem))
|
||||
return false;
|
||||
|
||||
while (elem.parentNode !== this.parentElement)
|
||||
elem = elem.parentElement;
|
||||
|
||||
var range = elem.$blaze_range;
|
||||
while (range && range !== this)
|
||||
range = range.parentRange;
|
||||
|
||||
return range === this;
|
||||
},
|
||||
containsRange: function (range) {
|
||||
if (! this.attached)
|
||||
throw new Error("Must be attached");
|
||||
|
||||
if (! range.attached)
|
||||
return false;
|
||||
|
||||
// A DOMRange is contained in this DOMRange if it's possible
|
||||
// to reach this range by following parent pointers. If the
|
||||
// DOMRange has the same parentElement, then it should be
|
||||
// a member, or a member of a member etc. Otherwise, we must
|
||||
// contain its parentElement.
|
||||
|
||||
if (range.parentElement !== this.parentElement)
|
||||
return this.containsElement(range.parentElement);
|
||||
|
||||
if (range === this)
|
||||
return false; // don't contain self
|
||||
|
||||
while (range && range !== this)
|
||||
range = range.parentRange;
|
||||
|
||||
return range === this;
|
||||
},
|
||||
addDOMAugmenter: function (augmenter) {
|
||||
if (this.augmenters === _emptyArray)
|
||||
this.augmenters = [];
|
||||
this.augmenters.push(augmenter);
|
||||
},
|
||||
$: function (selector) {
|
||||
var self = this;
|
||||
|
||||
var parentNode = this.parentElement;
|
||||
if (! parentNode)
|
||||
throw new Error("Can't select in removed DomRange");
|
||||
|
||||
// Strategy: Find all selector matches under parentNode,
|
||||
// then filter out the ones that aren't in this DomRange
|
||||
// using `DOMRange#containsElement`. This is
|
||||
// asymptotically slow in the presence of O(N) sibling
|
||||
// content that is under parentNode but not in our range,
|
||||
// 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 an offscreen range");
|
||||
|
||||
var results = Blaze.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, so we use
|
||||
// `.filter`.
|
||||
|
||||
// Function that selects only elements that are actually
|
||||
// in this DomRange, rather than simply descending from
|
||||
// `parentNode`.
|
||||
var filterFunc = function (elem) {
|
||||
// handle jQuery's arguments to filter, where the node
|
||||
// is in `this` and the index is the first argument.
|
||||
if (typeof elem === 'number')
|
||||
elem = this;
|
||||
|
||||
return self.containsElement(elem);
|
||||
};
|
||||
|
||||
if (! results.filter) {
|
||||
// not a jQuery array, and not a browser with
|
||||
// Array.prototype.filter (e.g. IE <9)
|
||||
var newResults = [];
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
var x = results[i];
|
||||
if (filterFunc(x))
|
||||
newResults.push(x);
|
||||
}
|
||||
results = newResults;
|
||||
} else {
|
||||
// `results.filter` is either jQuery's or ECMAScript's `filter`
|
||||
results = results.filter(filterFunc);
|
||||
}
|
||||
|
||||
return results;
|
||||
DOMRange.prototype.attach = function (parentElement, nextNode, _isMove) {
|
||||
// This method is called to insert the DOMRange into the DOM for
|
||||
// the first time, but it's also used internally when
|
||||
// updating the DOM.
|
||||
//
|
||||
// If _isMove is true, move this attached range to a different
|
||||
// location under the same parentElement.
|
||||
if (_isMove) {
|
||||
if (! (this.parentElement === parentElement &&
|
||||
this.attached))
|
||||
throw new Error("Can only move an attached DOMRange, and only under the same parent element");
|
||||
}
|
||||
});
|
||||
|
||||
var members = this.members;
|
||||
if (members.length) {
|
||||
this.emptyRangePlaceholder = null;
|
||||
for (var i = 0; i < members.length; i++) {
|
||||
DOMRange._insert(members[i], parentElement, nextNode, _isMove);
|
||||
}
|
||||
} else {
|
||||
var placeholder = document.createTextNode("");
|
||||
this.emptyRangePlaceholder = placeholder;
|
||||
parentElement.insertBefore(placeholder, nextNode || null);
|
||||
}
|
||||
this.attached = true;
|
||||
this.parentElement = parentElement;
|
||||
|
||||
if (! _isMove) {
|
||||
for(var i = 0; i < this.augmenters.length; i++)
|
||||
this.augmenters[i].attach(this, parentElement);
|
||||
}
|
||||
};
|
||||
|
||||
DOMRange.prototype.setMembers = function (newNodeAndRangeArray) {
|
||||
var newMembers = newNodeAndRangeArray;
|
||||
if (! (newMembers && (typeof newMembers.length) === 'number'))
|
||||
throw new Error("Expected array");
|
||||
|
||||
var oldMembers = this.members;
|
||||
|
||||
for (var i = 0; i < oldMembers.length; i++)
|
||||
this._memberOut(oldMembers[i]);
|
||||
for (var i = 0; i < newMembers.length; i++)
|
||||
this._memberIn(newMembers[i]);
|
||||
|
||||
if (! this.attached) {
|
||||
this.members = newMembers;
|
||||
} else {
|
||||
// don't do anything if we're going from empty to empty
|
||||
if (newMembers.length || oldMembers.length) {
|
||||
// detach the old members and insert the new members
|
||||
var nextNode = this.lastNode().nextSibling;
|
||||
var parentElement = this.parentElement;
|
||||
this.detach();
|
||||
this.members = newMembers;
|
||||
this.attach(parentElement, nextNode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DOMRange.prototype.firstNode = function () {
|
||||
if (! this.attached)
|
||||
throw new Error("Must be attached");
|
||||
|
||||
if (! this.members.length)
|
||||
return this.emptyRangePlaceholder;
|
||||
|
||||
var m = this.members[0];
|
||||
return (m instanceof DOMRange) ? m.firstNode() : m;
|
||||
};
|
||||
|
||||
DOMRange.prototype.lastNode = function () {
|
||||
if (! this.attached)
|
||||
throw new Error("Must be attached");
|
||||
|
||||
if (! this.members.length)
|
||||
return this.emptyRangePlaceholder;
|
||||
|
||||
var m = this.members[this.members.length - 1];
|
||||
return (m instanceof DOMRange) ? m.lastNode() : m;
|
||||
};
|
||||
|
||||
DOMRange.prototype.detach = function () {
|
||||
if (! this.attached)
|
||||
throw new Error("Must be attached");
|
||||
|
||||
var oldParentElement = this.parentElement;
|
||||
var members = this.members;
|
||||
if (members.length) {
|
||||
for (var i = 0; i < members.length; i++) {
|
||||
DOMRange._remove(members[i]);
|
||||
}
|
||||
} else {
|
||||
var placeholder = this.emptyRangePlaceholder;
|
||||
this.parentElement.removeChild(placeholder);
|
||||
this.emptyRangePlaceholder = null;
|
||||
}
|
||||
this.attached = false;
|
||||
this.parentElement = null;
|
||||
|
||||
for(var i = 0; i < this.augmenters.length; i++)
|
||||
this.augmenters[i].detach(this, oldParentElement);
|
||||
};
|
||||
|
||||
DOMRange.prototype.addMember = function (newMember, atIndex, _isMove) {
|
||||
var members = this.members;
|
||||
if (! (atIndex >= 0 && atIndex <= members.length))
|
||||
throw new Error("Bad index in range.addMember: " + atIndex);
|
||||
|
||||
if (! _isMove)
|
||||
this._memberIn(newMember);
|
||||
|
||||
if (! this.attached) {
|
||||
// currently detached; just updated members
|
||||
members.splice(atIndex, 0, newMember);
|
||||
} else if (members.length === 0) {
|
||||
// empty; use the empty-to-nonempty handling of setMembers
|
||||
this.setMembers([newMember]);
|
||||
} else {
|
||||
var nextNode;
|
||||
if (atIndex === members.length) {
|
||||
// insert at end
|
||||
nextNode = this.lastNode().nextSibling;
|
||||
} else {
|
||||
var m = members[atIndex];
|
||||
nextNode = (m instanceof DOMRange) ? m.firstNode() : m;
|
||||
}
|
||||
members.splice(atIndex, 0, newMember);
|
||||
DOMRange._insert(newMember, this.parentElement, nextNode, _isMove);
|
||||
}
|
||||
};
|
||||
|
||||
DOMRange.prototype.removeMember = function (atIndex, _isMove) {
|
||||
var members = this.members;
|
||||
if (! (atIndex >= 0 && atIndex < members.length))
|
||||
throw new Error("Bad index in range.removeMember: " + atIndex);
|
||||
|
||||
if (_isMove) {
|
||||
members.splice(atIndex, 1);
|
||||
} else {
|
||||
var oldMember = members[atIndex];
|
||||
this._memberOut(oldMember);
|
||||
|
||||
if (members.length === 1) {
|
||||
// becoming empty; use the logic in setMembers
|
||||
this.setMembers(_emptyArray);
|
||||
} else {
|
||||
members.splice(atIndex, 1);
|
||||
if (this.attached)
|
||||
DOMRange._remove(oldMember);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
DOMRange.prototype.moveMember = function (oldIndex, newIndex) {
|
||||
var member = this.members[oldIndex];
|
||||
this.removeMember(oldIndex, true /*_isMove*/);
|
||||
this.addMember(member, newIndex, true /*_isMove*/);
|
||||
};
|
||||
|
||||
DOMRange.prototype.getMember = function (atIndex) {
|
||||
var members = this.members;
|
||||
if (! (atIndex >= 0 && atIndex < members.length))
|
||||
throw new Error("Bad index in range.getMember: " + atIndex);
|
||||
return this.members[atIndex];
|
||||
};
|
||||
|
||||
DOMRange.prototype.stop = function () {
|
||||
var stopCallbacks = this.stopCallbacks;
|
||||
for (var i = 0; i < stopCallbacks.length; i++)
|
||||
stopCallbacks[i].call(this);
|
||||
this.stopCallbacks = _emptyArray;
|
||||
};
|
||||
|
||||
DOMRange.prototype.onstop = function (cb) {
|
||||
if (this.stopCallbacks === _emptyArray)
|
||||
this.stopCallbacks = [];
|
||||
this.stopCallbacks.push(cb);
|
||||
};
|
||||
|
||||
DOMRange.prototype._memberIn = function (m) {
|
||||
if (m instanceof DOMRange)
|
||||
m.parentRange = this;
|
||||
else if (m.nodeType === 1) // DOM Element
|
||||
m.$blaze_range = this;
|
||||
};
|
||||
|
||||
DOMRange.prototype._memberOut = function (m) {
|
||||
// old members are almost always GCed immediately.
|
||||
// to avoid the potentialy performance hit of deleting
|
||||
// a property, we simple null it out.
|
||||
if (m instanceof DOMRange)
|
||||
m.parentRange = null;
|
||||
else if (m.nodeType === 1) // DOM Element
|
||||
m.$blaze_range = null;
|
||||
};
|
||||
|
||||
DOMRange.prototype.containsElement = function (elem) {
|
||||
if (! this.attached)
|
||||
throw new Error("Must be attached");
|
||||
|
||||
// An element is contained in this DOMRange if it's possible to
|
||||
// reach it by walking parent pointers, first through the DOM and
|
||||
// then parentRange pointers. In other words, the element or some
|
||||
// ancestor of it is at our level of the DOM (a child of our
|
||||
// parentElement), and this element is one of our members or
|
||||
// is a member of a descendant Range.
|
||||
|
||||
if (! Blaze._elementContains(this.parentElement, elem))
|
||||
return false;
|
||||
|
||||
while (elem.parentNode !== this.parentElement)
|
||||
elem = elem.parentElement;
|
||||
|
||||
var range = elem.$blaze_range;
|
||||
while (range && range !== this)
|
||||
range = range.parentRange;
|
||||
|
||||
return range === this;
|
||||
};
|
||||
|
||||
DOMRange.prototype.containsRange = function (range) {
|
||||
if (! this.attached)
|
||||
throw new Error("Must be attached");
|
||||
|
||||
if (! range.attached)
|
||||
return false;
|
||||
|
||||
// A DOMRange is contained in this DOMRange if it's possible
|
||||
// to reach this range by following parent pointers. If the
|
||||
// DOMRange has the same parentElement, then it should be
|
||||
// a member, or a member of a member etc. Otherwise, we must
|
||||
// contain its parentElement.
|
||||
|
||||
if (range.parentElement !== this.parentElement)
|
||||
return this.containsElement(range.parentElement);
|
||||
|
||||
if (range === this)
|
||||
return false; // don't contain self
|
||||
|
||||
while (range && range !== this)
|
||||
range = range.parentRange;
|
||||
|
||||
return range === this;
|
||||
};
|
||||
|
||||
DOMRange.prototype.addDOMAugmenter = function (augmenter) {
|
||||
if (this.augmenters === _emptyArray)
|
||||
this.augmenters = [];
|
||||
this.augmenters.push(augmenter);
|
||||
};
|
||||
|
||||
DOMRange.prototype.$ = function (selector) {
|
||||
var self = this;
|
||||
|
||||
var parentNode = this.parentElement;
|
||||
if (! parentNode)
|
||||
throw new Error("Can't select in removed DomRange");
|
||||
|
||||
// Strategy: Find all selector matches under parentNode,
|
||||
// then filter out the ones that aren't in this DomRange
|
||||
// using `DOMRange#containsElement`. This is
|
||||
// asymptotically slow in the presence of O(N) sibling
|
||||
// content that is under parentNode but not in our range,
|
||||
// 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 an offscreen range");
|
||||
|
||||
var results = Blaze.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, so we use
|
||||
// `.filter`.
|
||||
|
||||
// Function that selects only elements that are actually
|
||||
// in this DomRange, rather than simply descending from
|
||||
// `parentNode`.
|
||||
var filterFunc = function (elem) {
|
||||
// handle jQuery's arguments to filter, where the node
|
||||
// is in `this` and the index is the first argument.
|
||||
if (typeof elem === 'number')
|
||||
elem = this;
|
||||
|
||||
return self.containsElement(elem);
|
||||
};
|
||||
|
||||
if (! results.filter) {
|
||||
// not a jQuery array, and not a browser with
|
||||
// Array.prototype.filter (e.g. IE <9)
|
||||
var newResults = [];
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
var x = results[i];
|
||||
if (filterFunc(x))
|
||||
newResults.push(x);
|
||||
}
|
||||
results = newResults;
|
||||
} else {
|
||||
// `results.filter` is either jQuery's or ECMAScript's `filter`
|
||||
results = results.filter(filterFunc);
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
Blaze.DOMAugmenter = JSClass.create({
|
||||
attach: function (range, element) {},
|
||||
|
||||
Reference in New Issue
Block a user