blob: b2371bc3804c9eae59bcf5a6ba7d3ca9f6ecdbc8 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
// Helper functions
var DOM_assignNodeIds;
// Primitive node creation operations
var DOM_createElement;
var DOM_createElementNS;
var DOM_createTextNode;
var DOM_createComment;
var DOM_cloneNode;
// Primitive and high-level node mutation operations
var DOM_appendChild;
var DOM_insertBefore;
var DOM_deleteNode;
var DOM_setAttribute;
var DOM_setAttributeNS;
var DOM_removeAttribute;
var DOM_removeAttributeNS;
var DOM_setStyleProperties;
var DOM_insertCharacters;
var DOM_moveCharacters;
var DOM_deleteCharacters;
var DOM_setNodeValue;
// High-level DOM operations
var DOM_getAttribute;
var DOM_getAttributeNS;
var DOM_getStringAttribute;
var DOM_getStringAttributeNS;
var DOM_getStyleProperties;
var DOM_deleteAllChildren;
var DOM_shallowCopyElement;
var DOM_replaceElement;
var DOM_wrapNode;
var DOM_wrapSiblings;
var DOM_mergeWithNextSibling;
var DOM_nodesMergeable;
var DOM_replaceCharacters;
var DOM_addTrackedPosition;
var DOM_removeTrackedPosition;
var DOM_removeAdjacentWhitespace;
var DOM_documentHead;
var DOM_ensureUniqueIds;
var DOM_nodeOffset;
var DOM_maxChildOffset;
var DOM_ignoreMutationsWhileExecuting;
var DOM_getIgnoreMutations;
var DOM_addListener;
var DOM_removeListener;
var DOM_Listener;
(function() {
var nextNodeId = 0;
var nodeData = new Object();
var ignoreMutations = 0;
////////////////////////////////////////////////////////////////////////////////////////////////
// //
// DOM Helper Functions //
// //
////////////////////////////////////////////////////////////////////////////////////////////////
function addUndoAction()
{
if (window.undoSupported)
UndoManager_addAction.apply(null,arrayCopy(arguments));
}
function assignNodeId(node)
{
if (node._nodeId != null)
throw new Error(node+" already has id");
node._nodeId = nextNodeId++;
node._type = ElementTypes[node.nodeName];
return node;
}
function checkNodeId(node)
{
if (node._nodeId == null)
throw new Error(node.nodeName+" lacks _nodeId");
}
// public
DOM_assignNodeIds = function(root)
{
if (root._nodeId != null)
throw new Error(root+" already has id");
recurse(root);
return;
function recurse(node) {
node._nodeId = nextNodeId++;
node._type = ElementTypes[node.nodeName];
for (var child = node.firstChild; child != null; child = child.nextSibling)
recurse(child);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// //
// Primitive DOM Operations //
// //
////////////////////////////////////////////////////////////////////////////////////////////////
/*
The following functions are considered "primitive", in that they are the core functions
through which all manipulation of the DOM ultimately occurs. All other DOM functions call
these, either directly or indirectly, instead of making direct method calls on node objects.
These functions are divided into two categories: node creation and mode mutation.
The creation functions are as follows:
* createElement(document,elementName)
* createElementNS(document,namespaceURI,qualifiedName)
* createTextNode(document,data)
* createComment(document,data)
* cloneNode(original,deep,noIdAttr)
The purpose of these is to ensure that a unique _nodeId value is assigned to each node object,
which is needed for using the NodeSet and NodeMap classes. All nodes in a document must have
this set; we use our own functions for this because DOM provides no other way of uniquely
identifying nodes in a way that allows them to be stored in a hash table.
The mutation functions are as follows:
* insertBeforeInternal(parent,newChild,refChild)
* deleteNodeInternal(node,deleteDescendantData)
* setAttribute(element,name,value)
* setAttributeNS(element,namespaceURI,qualifiedName,value)
* setStyleProperties(element,properties)
* insertCharacters(textNode,offset,characters)
* deleteCharacters(textNode,startOffset,endOffset)
* moveCharacters(srcTextNode,srcStartOffset,srcEndOffset,destTextNode,destOffset)
* setNodeValue(textNode,value)
These functions exist to allow us to record undo information. We can't use DOM mutation events
for this purpose they're not fully supported in WebKit.
Every time a mutation operation is performed on a node, we add an action to the undo stack
corresponding to the inverse of that operaton, i.e. an action that undoes the operaton. It
is absolutely critical that all changes to a DOM node go through these functions, regardless
of whether or not the node currently resides in the tree. This ensures that the undo history
is able to correctly revert the tree to the same state that it was in at the relevant point
in time.
By routing all DOM modifications through these few functions, virtually all of the other
javascript code can be ignorant of the undo manager, provided the only state they change is
in the DOM. Parts of the code which maintain their own state about the document, such as the
style manager, must implement their own undo-compliant state manipulation logic.
*** IMPORTANT ***
Just in case it isn't already clear, you must *never* make direct calls to methods like
appendChild() and createElement() on the node objects themselves. Doing so will result in
subtle and probably hard-to-find bugs. As far as all javascript code for UX Write is
concerned, consider the public functions defined in this file to be the DOM API. You can use
check-dom-methods.sh to search for any cases where this rule has been violated.
*/
// public
DOM_createElement = function(document,elementName)
{
return assignNodeId(document.createElement(elementName)); // check-ok
}
// public
DOM_createElementNS = function(document,namespaceURI,qualifiedName)
{
return assignNodeId(document.createElementNS(namespaceURI,qualifiedName)); // check-ok
}
// public
DOM_createTextNode = function(document,data)
{
return assignNodeId(document.createTextNode(data)); // check-ok
}
// public
DOM_createComment = function(document,data)
{
return assignNodeId(document.createComment(data)); // check-ok
}
// public
DOM_cloneNode = function(original,deep,noIdAttr)
{
var clone = original.cloneNode(deep); // check-ok
DOM_assignNodeIds(clone);
if (noIdAttr)
clone.removeAttribute("id"); // check-ok
return clone;
}
function insertBeforeInternal(parent,newChild,refChild)
{
if (newChild.parentNode == null) {
addUndoAction(deleteNodeInternal,newChild)
}
else {
var oldParent = newChild.parentNode;
var oldNext = newChild.nextSibling;
addUndoAction(insertBeforeInternal,oldParent,newChild,oldNext);
}
parent.insertBefore(newChild,refChild); // check-ok
}
function deleteNodeInternal(node,deleteDescendantData)
{
checkNodeId(node);
addUndoAction(insertBeforeInternal,node.parentNode,node,node.nextSibling);
if (node.parentNode == null)
throw new Error("Undo delete "+nodeString(node)+": parent is null");
node.parentNode.removeChild(node); // check-ok
// Delete all data associated with the node. This is not preserved across undo/redo;
// currently the only thing we are using this data for is tracked positions, and we
// are going to be recording undo information for the selection separately, so this is
// not a problem.
if (deleteDescendantData)
deleteNodeDataRecursive(node);
else
deleteNodeData(node);
return;
function deleteNodeData(current)
{
delete nodeData[current._nodeId];
}
function deleteNodeDataRecursive(current)
{
deleteNodeData(current);
for (var child = current.firstChild; child != null; child = child.nextSibling)
deleteNodeDataRecursive(child);
}
}
// public
DOM_setAttribute = function(element,name,value)
{
if (element.hasAttribute(name))
addUndoAction(DOM_setAttribute,element,name,element.getAttribute(name));
else
addUndoAction(DOM_setAttribute,element,name,null);
if (value == null)
element.removeAttribute(name); // check-ok
else
element.setAttribute(name,value); // check-ok
}
// public
DOM_setAttributeNS = function(element,namespaceURI,qualifiedName,value)
{
var localName = qualifiedName.replace(/^.*:/,"");
if (element.hasAttributeNS(namespaceURI,localName)) {
var oldValue = element.getAttributeNS(namespaceURI,localName);
var oldQName = element.getAttributeNodeNS(namespaceURI,localName).nodeName; // check-ok
addUndoAction(DOM_setAttributeNS,element,namespaceURI,oldQName,oldValue)
}
else {
addUndoAction(DOM_setAttributeNS,element,namespaceURI,localName,null);
}
if (value == null)
element.removeAttributeNS(namespaceURI,localName); // check-ok
else
element.setAttributeNS(namespaceURI,qualifiedName,value); // check-ok
}
// public
DOM_setStyleProperties = function(element,properties)
{
if (Object.getOwnPropertyNames(properties).length == 0)
return;
if (element.hasAttribute("style"))
addUndoAction(DOM_setAttribute,element,"style",element.getAttribute("style"));
else
addUndoAction(DOM_setAttribute,element,"style",null);
for (var name in properties)
element.style.setProperty(name,properties[name]); // check-ok
if (element.getAttribute("style") == "")
element.removeAttribute("style"); // check-ok
}
// public
DOM_insertCharacters = function(textNode,offset,characters)
{
if (textNode.nodeType != Node.TEXT_NODE)
throw new Error("DOM_insertCharacters called on non-text node");
if ((offset < 0) || (offset > textNode.nodeValue.length))
throw new Error("DOM_insertCharacters called with invalid offset");
trackedPositionsForNode(textNode).forEach(function (position) {
if (position.offset > offset)
position.offset += characters.length;
});
textNode.nodeValue = textNode.nodeValue.slice(0,offset) +
characters +
textNode.nodeValue.slice(offset);
var startOffset = offset;
var endOffset = offset + characters.length;
addUndoAction(DOM_deleteCharacters,textNode,startOffset,endOffset);
}
// public
DOM_deleteCharacters = function(textNode,startOffset,endOffset)
{
if (textNode.nodeType != Node.TEXT_NODE)
throw new Error("DOM_deleteCharacters called on non-text node "+nodeString(textNode));
if (endOffset == null)
endOffset = textNode.nodeValue.length;
if (endOffset < startOffset)
throw new Error("DOM_deleteCharacters called with invalid start/end offset");
trackedPositionsForNode(textNode).forEach(function (position) {
var deleteCount = endOffset - startOffset;
if ((position.offset > startOffset) && (position.offset < endOffset))
position.offset = startOffset;
else if (position.offset >= endOffset)
position.offset -= deleteCount;
});
var removed = textNode.nodeValue.slice(startOffset,endOffset);
addUndoAction(DOM_insertCharacters,textNode,startOffset,removed);
textNode.nodeValue = textNode.nodeValue.slice(0,startOffset) +
textNode.nodeValue.slice(endOffset);
}
// public
DOM_moveCharacters = function(srcTextNode,srcStartOffset,srcEndOffset,destTextNode,destOffset,
excludeStartPos,excludeEndPos)
{
if (srcTextNode == destTextNode)
throw new Error("src and dest text nodes cannot be the same");
if (srcStartOffset > srcEndOffset)
throw new Error("Invalid src range "+srcStartOffset+" - "+srcEndOffset);
if (srcStartOffset < 0)
throw new Error("srcStartOffset < 0");
if (srcEndOffset > srcTextNode.nodeValue.length)
throw new Error("srcEndOffset beyond end of src length");
if (destOffset < 0)
throw new Error("destOffset < 0");
if (destOffset > destTextNode.nodeValue.length)
throw new Error("destOffset beyond end of dest length");
var length = srcEndOffset - srcStartOffset;
addUndoAction(DOM_moveCharacters,destTextNode,destOffset,destOffset+length,
srcTextNode,srcStartOffset,excludeStartPos,excludeEndPos);
trackedPositionsForNode(destTextNode).forEach(function (pos) {
var startMatch = excludeStartPos ? (pos.offset > destOffset)
: (pos.offset >= destOffset);
if (startMatch)
pos.offset += length;
});
trackedPositionsForNode(srcTextNode).forEach(function (pos) {
var startMatch = excludeStartPos ? (pos.offset > srcStartOffset)
: (pos.offset >= srcStartOffset);
var endMatch = excludeEndPos ? (pos.offset < srcEndOffset)
: (pos.offset <= srcEndOffset);
if (startMatch && endMatch) {
pos.node = destTextNode;
pos.offset = destOffset + (pos.offset - srcStartOffset);
}
else if (pos.offset >= srcEndOffset) {
pos.offset -= length;
}
});
var extract = srcTextNode.nodeValue.substring(srcStartOffset,srcEndOffset);
srcTextNode.nodeValue = srcTextNode.nodeValue.slice(0,srcStartOffset) +
srcTextNode.nodeValue.slice(srcEndOffset);
destTextNode.nodeValue = destTextNode.nodeValue.slice(0,destOffset) +
extract +
destTextNode.nodeValue.slice(destOffset);
}
// public
DOM_setNodeValue = function(textNode,value)
{
if (textNode.nodeType != Node.TEXT_NODE)
throw new Error("DOM_setNodeValue called on non-text node");
trackedPositionsForNode(textNode).forEach(function (position) {
position.offset = 0;
});
var oldValue = textNode.nodeValue;
addUndoAction(DOM_setNodeValue,textNode,oldValue);
textNode.nodeValue = value;
}
////////////////////////////////////////////////////////////////////////////////////////////////
// //
// High-level DOM Operations //
// //
////////////////////////////////////////////////////////////////////////////////////////////////
function appendChildInternal(parent,newChild)
{
insertBeforeInternal(parent,newChild,null);
}
// public
DOM_appendChild = function(node,child)
{
return DOM_insertBefore(node,child,null);
}
// public
DOM_insertBefore = function(parent,child,nextSibling)
{
var newOffset;
if (nextSibling != null)
newOffset = DOM_nodeOffset(nextSibling);
else
newOffset = parent.childNodes.length;
var oldParent = child.parentNode;
if (oldParent != null) { // already in tree
var oldOffset = DOM_nodeOffset(child);
if ((oldParent == parent) && (newOffset > oldOffset))
newOffset--;
trackedPositionsForNode(oldParent).forEach(function (position) {
if (position.offset > oldOffset) {
position.offset--;
}
else if (position.offset == oldOffset) {
position.node = parent;
position.offset = newOffset;
}
});
}
var result = insertBeforeInternal(parent,child,nextSibling);
trackedPositionsForNode(parent).forEach(function (position) {
if (position.offset > newOffset)
position.offset++;
});
return result;
}
// public
DOM_deleteNode = function(node)
{
if (node.parentNode == null) // already deleted
return;
adjustPositionsRecursive(node);
deleteNodeInternal(node,true);
function adjustPositionsRecursive(current)
{
for (var child = current.firstChild; child != null; child = child.nextSibling)
adjustPositionsRecursive(child);
trackedPositionsForNode(current.parentNode).forEach(function (position) {
var offset = DOM_nodeOffset(current);
if (offset < position.offset) {
position.offset--;
}
});
trackedPositionsForNode(current).forEach(function (position) {
var offset = DOM_nodeOffset(current);
position.node = current.parentNode;
position.offset = offset;
});
}
}
// public
DOM_removeAttribute = function(element,name,value)
{
DOM_setAttribute(element,name,null);
}
// public
DOM_removeAttributeNS = function(element,namespaceURI,localName)
{
DOM_setAttributeNS(element,namespaceURI,localName,null)
}
// public
DOM_getAttribute = function(element,name)
{
if (element.hasAttribute(name))
return element.getAttribute(name);
else
return null;
}
// public
DOM_getAttributeNS = function(element,namespaceURI,localName)
{
if (element.hasAttributeNS(namespaceURI,localName))
return element.getAttributeNS(namespaceURI,localName);
else
return null;
}
// public
DOM_getStringAttribute = function(element,name)
{
var value = element.getAttribute(name);
return (value == null) ? "" : value;
}
// public
DOM_getStringAttributeNS = function(element,namespaceURI,localName)
{
var value = element.getAttributeNS(namespaceURI,localName);
return (value == null) ? "" : value;
}
// public
DOM_getStyleProperties = function(node)
{
var properties = new Object();
if (node.nodeType == Node.ELEMENT_NODE) {
for (var i = 0; i < node.style.length; i++) {
var name = node.style[i];
var value = node.style.getPropertyValue(name);
properties[name] = value;
}
}
return properties;
}
// public
DOM_deleteAllChildren = function(parent)
{
while (parent.firstChild != null)
DOM_deleteNode(parent.firstChild);
}
// public
DOM_shallowCopyElement = function(element)
{
return DOM_cloneNode(element,false,true);
}
// public
DOM_removeNodeButKeepChildren = function(node)
{
if (node.parentNode == null)
throw new Error("Node "+nodeString(node)+" has no parent");
var offset = DOM_nodeOffset(node);
var childCount = node.childNodes.length;
trackedPositionsForNode(node.parentNode).forEach(function (position) {
if (position.offset > offset)
position.offset += childCount-1;
});
trackedPositionsForNode(node).forEach(function (position) {
position.node = node.parentNode;
position.offset += offset;
});
var parent = node.parentNode;
var nextSibling = node.nextSibling;
deleteNodeInternal(node,false);
while (node.firstChild != null) {
var child = node.firstChild;
insertBeforeInternal(parent,child,nextSibling);
}
}
// public
DOM_replaceElement = function(oldElement,newName)
{
var listeners = listenersForNode(oldElement);
var newElement = DOM_createElement(document,newName);
for (var i = 0; i < oldElement.attributes.length; i++) {
var name = oldElement.attributes[i].nodeName; // check-ok
var value = oldElement.getAttribute(name);
DOM_setAttribute(newElement,name,value);
}
var positions = arrayCopy(trackedPositionsForNode(oldElement));
if (positions != null) {
for (var i = 0; i < positions.length; i++) {
if (positions[i].node != oldElement)
throw new Error("replaceElement: position with wrong node");
positions[i].node = newElement;
}
}
var parent = oldElement.parentNode;
var nextSibling = oldElement.nextSibling;
while (oldElement.firstChild != null)
appendChildInternal(newElement,oldElement.firstChild);
// Deletion must be done first so if it's a heading, the outline code picks up the change
// correctly. Otherwise, there could be two elements in the document with the same id at
// the same time.
deleteNodeInternal(oldElement,false);
insertBeforeInternal(parent,newElement,nextSibling);
for (var i = 0; i < listeners.length; i++)
listeners[i].afterReplaceElement(oldElement,newElement);
return newElement;
}
// public
DOM_wrapNode = function(node,elementName)
{
return DOM_wrapSiblings(node,node,elementName);
}
DOM_wrapSiblings = function(first,last,elementName)
{
var parent = first.parentNode;
var wrapper = DOM_createElement(document,elementName);
if (first.parentNode != last.parentNode)
throw new Error("first and last are not siblings");
if (parent != null) {
var firstOffset = DOM_nodeOffset(first);
var lastOffset = DOM_nodeOffset(last);
var nodeCount = lastOffset - firstOffset + 1;
trackedPositionsForNode(parent).forEach(function (position) {
if ((position.offset >= firstOffset) && (position.offset <= lastOffset+1)) {
position.node = wrapper;
position.offset -= firstOffset;
}
else if (position.offset > lastOffset+1) {
position.offset -= (nodeCount-1);
}
});
insertBeforeInternal(parent,wrapper,first);
}
var end = last.nextSibling;
var current = first;
while (current != end) {
var next = current.nextSibling;
appendChildInternal(wrapper,current);
current = next;
}
return wrapper;
}
// public
DOM_mergeWithNextSibling = function(current,whiteList)
{
var parent = current.parentNode;
var next = current.nextSibling;
if ((next == null) || !DOM_nodesMergeable(current,next,whiteList))
return;
var currentLength = DOM_maxChildOffset(current);
var nextOffset = DOM_nodeOffset(next);
var lastChild = null;
if (current.nodeType == Node.ELEMENT_NODE) {
lastChild = current.lastChild;
DOM_insertBefore(current,next,null);
DOM_removeNodeButKeepChildren(next);
}
else {
DOM_insertCharacters(current,current.nodeValue.length,next.nodeValue);
trackedPositionsForNode(next).forEach(function (position) {
position.node = current;
position.offset = position.offset+currentLength;
});
trackedPositionsForNode(current.parentNode).forEach(function (position) {
if (position.offset == nextOffset) {
position.node = current;
position.offset = currentLength;
}
});
DOM_deleteNode(next);
}
if (lastChild != null)
DOM_mergeWithNextSibling(lastChild,whiteList);
}
// public
DOM_nodesMergeable = function(a,b,whiteList)
{
if ((a.nodeType == Node.TEXT_NODE) && (b.nodeType == Node.TEXT_NODE))
return true;
else if ((a.nodeType == Node.ELEMENT_NODE) && (b.nodeType == Node.ELEMENT_NODE))
return elementsMergableTypes(a,b);
else
return false;
function elementsMergableTypes(a,b)
{
if (whiteList["force"] && isParagraphNode(a) && isParagraphNode(b))
return true;
if ((a._type == b._type) &&
whiteList[a._type] &&
(a.attributes.length == b.attributes.length)) {
for (var i = 0; i < a.attributes.length; i++) {
var attrName = a.attributes[i].nodeName; // check-ok
if (a.getAttribute(attrName) != b.getAttribute(attrName))
return false;
}
return true;
}
return false;
}
}
function getDataForNode(node,create)
{
if (node._nodeId == null)
throw new Error("getDataForNode: node "+node.nodeName+" has no _nodeId property");
if ((nodeData[node._nodeId] == null) && create)
nodeData[node._nodeId] = new Object();
return nodeData[node._nodeId];
}
function trackedPositionsForNode(node)
{
var data = getDataForNode(node,false);
if ((data != null) && (data.trackedPositions != null)) {
// Sanity check
for (var i = 0; i < data.trackedPositions.length; i++) {
if (data.trackedPositions[i].node != node)
throw new Error("Position "+data.trackedPositions[i]+" has wrong node");
}
return arrayCopy(data.trackedPositions);
}
else {
return [];
}
}
function listenersForNode(node)
{
var data = getDataForNode(node,false);
if ((data != null) && (data.listeners != null))
return data.listeners;
else
return [];
}
// public
DOM_replaceCharacters = function(textNode,startOffset,endOffset,replacement)
{
// Note that we do the insertion *before* the deletion so that the position is properly
// maintained, and ends up at the end of the replacement (unless it was previously at
// startOffset, in which case it will stay the same)
DOM_insertCharacters(textNode,startOffset,replacement);
DOM_deleteCharacters(textNode,startOffset+replacement.length,endOffset+replacement.length);
}
// public
DOM_addTrackedPosition = function(position)
{
var data = getDataForNode(position.node,true);
if (data.trackedPositions == null)
data.trackedPositions = new Array();
data.trackedPositions.push(position);
}
// public
DOM_removeTrackedPosition = function(position)
{
var data = getDataForNode(position.node,false);
if ((data == null) || (data.trackedPositions == null))
throw new Error("DOM_removeTrackedPosition: no registered positions for this node "+
"("+position.node.nodeName+")");
for (var i = 0; i < data.trackedPositions.length; i++) {
if (data.trackedPositions[i] == position) {
data.trackedPositions.splice(i,1);
return;
}
}
throw new Error("DOM_removeTrackedPosition: position is not registered ("+
data.trackedPositions.length+" others)");
}
// public
DOM_removeAdjacentWhitespace = function(node)
{
while ((node.previousSibling != null) && (isWhitespaceTextNode(node.previousSibling)))
DOM_deleteNode(node.previousSibling);
while ((node.nextSibling != null) && (isWhitespaceTextNode(node.nextSibling)))
DOM_deleteNode(node.nextSibling);
}
// public
DOM_documentHead = function(document)
{
var html = document.documentElement;
for (var child = html.firstChild; child != null; child = child.nextSibling) {
if (child._type == HTML_HEAD)
return child;
}
throw new Error("Document contains no HEAD element");
}
// public
DOM_ensureUniqueIds = function(root)
{
var ids = new Object();
var duplicates = new Array();
discoverDuplicates(root);
renameDuplicates();
return;
function discoverDuplicates(node)
{
if (node.nodeType != Node.ELEMENT_NODE)
return;
var id = node.getAttribute("id");
if ((id != null) && (id != "")) {
if (ids[id])
duplicates.push(node);
else
ids[id] = true;
}
for (var child = node.firstChild; child != null; child = child.nextSibling)
discoverDuplicates(child);
}
function renameDuplicates()
{
var nextNumberForPrefix = new Object();
for (var i = 0; i < duplicates.length; i++) {
var id = duplicates[i].getAttribute("id");
var prefix = id.replace(/[0-9]+$/,"");
var num = nextNumberForPrefix[prefix] ? nextNumberForPrefix[prefix] : 1;
var candidate;
do {
candidate = prefix + num;
num++;
} while (ids[candidate]);
DOM_setAttribute(duplicates[i],"id",candidate);
ids[candidate] = true;
nextNumberForPrefix[prefix] = num;
}
}
}
// public
DOM_nodeOffset = function(node,parent)
{
if ((node == null) && (parent != null))
return DOM_maxChildOffset(parent);
var offset = 0;
for (var n = node.parentNode.firstChild; n != node; n = n.nextSibling)
offset++;
return offset;
}
// public
DOM_maxChildOffset = function(node)
{
if (node.nodeType == Node.TEXT_NODE)
return node.nodeValue.length;
else if (node.nodeType == Node.ELEMENT_NODE)
return node.childNodes.length;
else
throw new Error("maxOffset: invalid node type ("+node.nodeType+")");
}
function incIgnoreMutations()
{
UndoManager_addAction(decIgnoreMutations);
ignoreMutations++;
}
function decIgnoreMutations()
{
UndoManager_addAction(incIgnoreMutations);
ignoreMutations--;
if (ignoreMutations < 0)
throw new Error("ignoreMutations is now negative");
}
// public
DOM_ignoreMutationsWhileExecuting = function(fun)
{
incIgnoreMutations();
try {
return fun();
}
finally {
decIgnoreMutations();
}
}
// public
DOM_getIgnoreMutations = function()
{
return ignoreMutations;
}
// public
DOM_addListener = function(node,listener)
{
var data = getDataForNode(node,true);
if (data.listeners == null)
data.listeners = [listener];
else
data.listeners.push(listener);
}
// public
DOM_removeListener = function(node,listener)
{
var list = listenersForNode(node);
var index = list.indexOf(listener);
if (index >= 0)
list.splice(index,1);
}
// public
function Listener()
{
}
Listener.prototype.afterReplaceElement = function(oldElement,newElement) {}
DOM_Listener = Listener;
})();