From d2e8596bb2519e4ffe9da6d4956ba968c0f79beb Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Mon, 23 Jun 2014 17:54:01 -0700 Subject: [PATCH] Make DOMRange stack traces more better Switch all methods to `DOMRange.prototype.myMethod =` style. Maybe we should just switch JSClass to this style. --- packages/blaze/domrange.js | 601 +++++++++++++++++++------------------ 1 file changed, 307 insertions(+), 294 deletions(-) diff --git a/packages/blaze/domrange.js b/packages/blaze/domrange.js index 2708ee11d1..2ca3dea1c5 100644 --- a/packages/blaze/domrange.js +++ b/packages/blaze/domrange.js @@ -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) {},