2014-05-07 13:06:24 +08:00
|
|
|
/**
|
2015-06-15 18:01:48 +08:00
|
|
|
* RangeUtils.js
|
2014-05-07 13:06:24 +08:00
|
|
|
*
|
|
|
|
* Copyright, Moxiecode Systems AB
|
|
|
|
* Released under LGPL License.
|
|
|
|
*
|
|
|
|
* License: http://www.tinymce.com/license
|
|
|
|
* Contributing: http://www.tinymce.com/contributing
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
2015-06-15 18:01:48 +08:00
|
|
|
* This class contains a few utility methods for ranges.
|
2014-05-07 13:06:24 +08:00
|
|
|
*
|
|
|
|
* @class tinymce.dom.RangeUtils
|
|
|
|
*/
|
|
|
|
define("tinymce/dom/RangeUtils", [
|
2015-06-15 18:01:48 +08:00
|
|
|
"tinymce/util/Tools",
|
|
|
|
"tinymce/dom/TreeWalker"
|
|
|
|
], function(Tools, TreeWalker) {
|
2014-05-07 13:06:24 +08:00
|
|
|
var each = Tools.each;
|
|
|
|
|
2015-06-15 18:01:48 +08:00
|
|
|
function getEndChild(container, index) {
|
|
|
|
var childNodes = container.childNodes;
|
|
|
|
|
|
|
|
index--;
|
|
|
|
|
|
|
|
if (index > childNodes.length - 1) {
|
|
|
|
index = childNodes.length - 1;
|
|
|
|
} else if (index < 0) {
|
|
|
|
index = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
return childNodes[index] || container;
|
|
|
|
}
|
|
|
|
|
2014-05-07 13:06:24 +08:00
|
|
|
function RangeUtils(dom) {
|
|
|
|
/**
|
|
|
|
* Walks the specified range like object and executes the callback for each sibling collection it finds.
|
|
|
|
*
|
|
|
|
* @method walk
|
|
|
|
* @param {Object} rng Range like object.
|
|
|
|
* @param {function} callback Callback function to execute for each sibling collection.
|
|
|
|
*/
|
|
|
|
this.walk = function(rng, callback) {
|
|
|
|
var startContainer = rng.startContainer,
|
|
|
|
startOffset = rng.startOffset,
|
|
|
|
endContainer = rng.endContainer,
|
|
|
|
endOffset = rng.endOffset,
|
|
|
|
ancestor, startPoint,
|
|
|
|
endPoint, node, parent, siblings, nodes;
|
|
|
|
|
|
|
|
// Handle table cell selection the table plugin enables
|
|
|
|
// you to fake select table cells and perform formatting actions on them
|
|
|
|
nodes = dom.select('td.mce-item-selected,th.mce-item-selected');
|
|
|
|
if (nodes.length > 0) {
|
|
|
|
each(nodes, function(node) {
|
|
|
|
callback([node]);
|
|
|
|
});
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Excludes start/end text node if they are out side the range
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {Array} nodes Nodes to exclude items from.
|
|
|
|
* @return {Array} Array with nodes excluding the start/end container if needed.
|
|
|
|
*/
|
|
|
|
function exclude(nodes) {
|
|
|
|
var node;
|
|
|
|
|
|
|
|
// First node is excluded
|
|
|
|
node = nodes[0];
|
|
|
|
if (node.nodeType === 3 && node === startContainer && startOffset >= node.nodeValue.length) {
|
|
|
|
nodes.splice(0, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Last node is excluded
|
|
|
|
node = nodes[nodes.length - 1];
|
|
|
|
if (endOffset === 0 && nodes.length > 0 && node === endContainer && node.nodeType === 3) {
|
|
|
|
nodes.splice(nodes.length - 1, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
return nodes;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Collects siblings
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {Node} node Node to collect siblings from.
|
|
|
|
* @param {String} name Name of the sibling to check for.
|
|
|
|
* @return {Array} Array of collected siblings.
|
|
|
|
*/
|
|
|
|
function collectSiblings(node, name, end_node) {
|
|
|
|
var siblings = [];
|
|
|
|
|
|
|
|
for (; node && node != end_node; node = node[name]) {
|
|
|
|
siblings.push(node);
|
|
|
|
}
|
|
|
|
|
|
|
|
return siblings;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find an end point this is the node just before the common ancestor root.
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {Node} node Node to start at.
|
|
|
|
* @param {Node} root Root/ancestor element to stop just before.
|
|
|
|
* @return {Node} Node just before the root element.
|
|
|
|
*/
|
|
|
|
function findEndPoint(node, root) {
|
|
|
|
do {
|
|
|
|
if (node.parentNode == root) {
|
|
|
|
return node;
|
|
|
|
}
|
|
|
|
|
|
|
|
node = node.parentNode;
|
2015-06-15 18:01:48 +08:00
|
|
|
} while (node);
|
2014-05-07 13:06:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
function walkBoundary(start_node, end_node, next) {
|
|
|
|
var siblingName = next ? 'nextSibling' : 'previousSibling';
|
|
|
|
|
|
|
|
for (node = start_node, parent = node.parentNode; node && node != end_node; node = parent) {
|
|
|
|
parent = node.parentNode;
|
|
|
|
siblings = collectSiblings(node == start_node ? node : node[siblingName], siblingName);
|
|
|
|
|
|
|
|
if (siblings.length) {
|
|
|
|
if (!next) {
|
|
|
|
siblings.reverse();
|
|
|
|
}
|
|
|
|
|
|
|
|
callback(exclude(siblings));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If index based start position then resolve it
|
|
|
|
if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) {
|
|
|
|
startContainer = startContainer.childNodes[startOffset];
|
|
|
|
}
|
|
|
|
|
|
|
|
// If index based end position then resolve it
|
|
|
|
if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) {
|
2015-06-15 18:01:48 +08:00
|
|
|
endContainer = getEndChild(endContainer, endOffset);
|
2014-05-07 13:06:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Same container
|
|
|
|
if (startContainer == endContainer) {
|
|
|
|
return callback(exclude([startContainer]));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find common ancestor and end points
|
|
|
|
ancestor = dom.findCommonAncestor(startContainer, endContainer);
|
|
|
|
|
|
|
|
// Process left side
|
|
|
|
for (node = startContainer; node; node = node.parentNode) {
|
|
|
|
if (node === endContainer) {
|
|
|
|
return walkBoundary(startContainer, ancestor, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (node === ancestor) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Process right side
|
|
|
|
for (node = endContainer; node; node = node.parentNode) {
|
|
|
|
if (node === startContainer) {
|
|
|
|
return walkBoundary(endContainer, ancestor);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (node === ancestor) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find start/end point
|
|
|
|
startPoint = findEndPoint(startContainer, ancestor) || startContainer;
|
|
|
|
endPoint = findEndPoint(endContainer, ancestor) || endContainer;
|
|
|
|
|
|
|
|
// Walk left leaf
|
|
|
|
walkBoundary(startContainer, startPoint, true);
|
|
|
|
|
|
|
|
// Walk the middle from start to end point
|
|
|
|
siblings = collectSiblings(
|
|
|
|
startPoint == startContainer ? startPoint : startPoint.nextSibling,
|
|
|
|
'nextSibling',
|
|
|
|
endPoint == endContainer ? endPoint.nextSibling : endPoint
|
|
|
|
);
|
|
|
|
|
|
|
|
if (siblings.length) {
|
|
|
|
callback(exclude(siblings));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Walk right leaf
|
|
|
|
walkBoundary(endContainer, endPoint);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Splits the specified range at it's start/end points.
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {Range/RangeObject} rng Range to split.
|
|
|
|
* @return {Object} Range position object.
|
|
|
|
*/
|
|
|
|
this.split = function(rng) {
|
|
|
|
var startContainer = rng.startContainer,
|
|
|
|
startOffset = rng.startOffset,
|
|
|
|
endContainer = rng.endContainer,
|
|
|
|
endOffset = rng.endOffset;
|
|
|
|
|
|
|
|
function splitText(node, offset) {
|
|
|
|
return node.splitText(offset);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle single text node
|
|
|
|
if (startContainer == endContainer && startContainer.nodeType == 3) {
|
|
|
|
if (startOffset > 0 && startOffset < startContainer.nodeValue.length) {
|
|
|
|
endContainer = splitText(startContainer, startOffset);
|
|
|
|
startContainer = endContainer.previousSibling;
|
|
|
|
|
|
|
|
if (endOffset > startOffset) {
|
|
|
|
endOffset = endOffset - startOffset;
|
|
|
|
startContainer = endContainer = splitText(endContainer, endOffset).previousSibling;
|
|
|
|
endOffset = endContainer.nodeValue.length;
|
|
|
|
startOffset = 0;
|
|
|
|
} else {
|
|
|
|
endOffset = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Split startContainer text node if needed
|
|
|
|
if (startContainer.nodeType == 3 && startOffset > 0 && startOffset < startContainer.nodeValue.length) {
|
|
|
|
startContainer = splitText(startContainer, startOffset);
|
|
|
|
startOffset = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Split endContainer text node if needed
|
|
|
|
if (endContainer.nodeType == 3 && endOffset > 0 && endOffset < endContainer.nodeValue.length) {
|
|
|
|
endContainer = splitText(endContainer, endOffset).previousSibling;
|
|
|
|
endOffset = endContainer.nodeValue.length;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
startContainer: startContainer,
|
|
|
|
startOffset: startOffset,
|
|
|
|
endContainer: endContainer,
|
|
|
|
endOffset: endOffset
|
|
|
|
};
|
|
|
|
};
|
2015-06-15 18:01:48 +08:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Normalizes the specified range by finding the closest best suitable caret location.
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {Range} rng Range to normalize.
|
|
|
|
* @return {Boolean} True/false if the specified range was normalized or not.
|
|
|
|
*/
|
|
|
|
this.normalize = function(rng) {
|
|
|
|
var normalized, collapsed;
|
|
|
|
|
|
|
|
function normalizeEndPoint(start) {
|
|
|
|
var container, offset, walker, body = dom.getRoot(), node, nonEmptyElementsMap;
|
|
|
|
var directionLeft, isAfterNode;
|
|
|
|
|
|
|
|
function hasBrBeforeAfter(node, left) {
|
|
|
|
var walker = new TreeWalker(node, dom.getParent(node.parentNode, dom.isBlock) || body);
|
|
|
|
|
|
|
|
while ((node = walker[left ? 'prev' : 'next']())) {
|
|
|
|
if (node.nodeName === "BR") {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function isPrevNode(node, name) {
|
|
|
|
return node.previousSibling && node.previousSibling.nodeName == name;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Walks the dom left/right to find a suitable text node to move the endpoint into
|
|
|
|
// It will only walk within the current parent block or body and will stop if it hits a block or a BR/IMG
|
|
|
|
function findTextNodeRelative(left, startNode) {
|
|
|
|
var walker, lastInlineElement, parentBlockContainer;
|
|
|
|
|
|
|
|
startNode = startNode || container;
|
|
|
|
parentBlockContainer = dom.getParent(startNode.parentNode, dom.isBlock) || body;
|
|
|
|
|
|
|
|
// Lean left before the BR element if it's the only BR within a block element. Gecko bug: #6680
|
|
|
|
// This: <p><br>|</p> becomes <p>|<br></p>
|
|
|
|
if (left && startNode.nodeName == 'BR' && isAfterNode && dom.isEmpty(parentBlockContainer)) {
|
|
|
|
container = startNode.parentNode;
|
|
|
|
offset = dom.nodeIndex(startNode);
|
|
|
|
normalized = true;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Walk left until we hit a text node we can move to or a block/br/img
|
|
|
|
walker = new TreeWalker(startNode, parentBlockContainer);
|
|
|
|
while ((node = walker[left ? 'prev' : 'next']())) {
|
|
|
|
// Break if we hit a non content editable node
|
|
|
|
if (dom.getContentEditableParent(node) === "false") {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Found text node that has a length
|
|
|
|
if (node.nodeType === 3 && node.nodeValue.length > 0) {
|
|
|
|
container = node;
|
|
|
|
offset = left ? node.nodeValue.length : 0;
|
|
|
|
normalized = true;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Break if we find a block or a BR/IMG/INPUT etc
|
|
|
|
if (dom.isBlock(node) || nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
lastInlineElement = node;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only fetch the last inline element when in caret mode for now
|
|
|
|
if (collapsed && lastInlineElement) {
|
|
|
|
container = lastInlineElement;
|
|
|
|
normalized = true;
|
|
|
|
offset = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
container = rng[(start ? 'start' : 'end') + 'Container'];
|
|
|
|
offset = rng[(start ? 'start' : 'end') + 'Offset'];
|
|
|
|
isAfterNode = container.nodeType == 1 && offset === container.childNodes.length;
|
|
|
|
nonEmptyElementsMap = dom.schema.getNonEmptyElements();
|
|
|
|
directionLeft = start;
|
|
|
|
|
|
|
|
if (container.nodeType == 1 && offset > container.childNodes.length - 1) {
|
|
|
|
directionLeft = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the container is a document move it to the body element
|
|
|
|
if (container.nodeType === 9) {
|
|
|
|
container = dom.getRoot();
|
|
|
|
offset = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the container is body try move it into the closest text node or position
|
|
|
|
if (container === body) {
|
|
|
|
// If start is before/after a image, table etc
|
|
|
|
if (directionLeft) {
|
|
|
|
node = container.childNodes[offset > 0 ? offset - 1 : 0];
|
|
|
|
if (node) {
|
|
|
|
if (nonEmptyElementsMap[node.nodeName] || node.nodeName == "TABLE") {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Resolve the index
|
|
|
|
if (container.hasChildNodes()) {
|
|
|
|
offset = Math.min(!directionLeft && offset > 0 ? offset - 1 : offset, container.childNodes.length - 1);
|
|
|
|
container = container.childNodes[offset];
|
|
|
|
offset = 0;
|
|
|
|
|
|
|
|
// Don't walk into elements that doesn't have any child nodes like a IMG
|
|
|
|
if (container.hasChildNodes() && !/TABLE/.test(container.nodeName)) {
|
|
|
|
// Walk the DOM to find a text node to place the caret at or a BR
|
|
|
|
node = container;
|
|
|
|
walker = new TreeWalker(container, body);
|
|
|
|
|
|
|
|
do {
|
|
|
|
// Found a text node use that position
|
|
|
|
if (node.nodeType === 3 && node.nodeValue.length > 0) {
|
|
|
|
offset = directionLeft ? 0 : node.nodeValue.length;
|
|
|
|
container = node;
|
|
|
|
normalized = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Found a BR/IMG element that we can place the caret before
|
|
|
|
if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) {
|
|
|
|
offset = dom.nodeIndex(node);
|
|
|
|
container = node.parentNode;
|
|
|
|
|
|
|
|
// Put caret after image when moving the end point
|
|
|
|
if (node.nodeName == "IMG" && !directionLeft) {
|
|
|
|
offset++;
|
|
|
|
}
|
|
|
|
|
|
|
|
normalized = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} while ((node = (directionLeft ? walker.next() : walker.prev())));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Lean the caret to the left if possible
|
|
|
|
if (collapsed) {
|
|
|
|
// So this: <b>x</b><i>|x</i>
|
|
|
|
// Becomes: <b>x|</b><i>x</i>
|
|
|
|
// Seems that only gecko has issues with this
|
|
|
|
if (container.nodeType === 3 && offset === 0) {
|
|
|
|
findTextNodeRelative(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Lean left into empty inline elements when the caret is before a BR
|
|
|
|
// So this: <i><b></b><i>|<br></i>
|
|
|
|
// Becomes: <i><b>|</b><i><br></i>
|
|
|
|
// Seems that only gecko has issues with this.
|
|
|
|
// Special edge case for <p><a>x</a>|<br></p> since we don't want <p><a>x|</a><br></p>
|
|
|
|
if (container.nodeType === 1) {
|
|
|
|
node = container.childNodes[offset];
|
|
|
|
|
|
|
|
// Offset is after the containers last child
|
|
|
|
// then use the previous child for normalization
|
|
|
|
if (!node) {
|
|
|
|
node = container.childNodes[offset - 1];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (node && node.nodeName === 'BR' && !isPrevNode(node, 'A') &&
|
|
|
|
!hasBrBeforeAfter(node) && !hasBrBeforeAfter(node, true)) {
|
|
|
|
findTextNodeRelative(true, node);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Lean the start of the selection right if possible
|
|
|
|
// So this: x[<b>x]</b>
|
|
|
|
// Becomes: x<b>[x]</b>
|
|
|
|
if (directionLeft && !collapsed && container.nodeType === 3 && offset === container.nodeValue.length) {
|
|
|
|
findTextNodeRelative(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set endpoint if it was normalized
|
|
|
|
if (normalized) {
|
|
|
|
rng['set' + (start ? 'Start' : 'End')](container, offset);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
collapsed = rng.collapsed;
|
|
|
|
|
|
|
|
normalizeEndPoint(true);
|
|
|
|
|
|
|
|
if (!collapsed) {
|
|
|
|
normalizeEndPoint();
|
|
|
|
}
|
|
|
|
|
|
|
|
// If it was collapsed then make sure it still is
|
|
|
|
if (normalized && collapsed) {
|
|
|
|
rng.collapse(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
return normalized;
|
|
|
|
};
|
2014-05-07 13:06:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Compares two ranges and checks if they are equal.
|
|
|
|
*
|
|
|
|
* @static
|
|
|
|
* @method compareRanges
|
|
|
|
* @param {DOMRange} rng1 First range to compare.
|
|
|
|
* @param {DOMRange} rng2 First range to compare.
|
|
|
|
* @return {Boolean} true/false if the ranges are equal.
|
|
|
|
*/
|
|
|
|
RangeUtils.compareRanges = function(rng1, rng2) {
|
|
|
|
if (rng1 && rng2) {
|
|
|
|
// Compare native IE ranges
|
|
|
|
if (rng1.item || rng1.duplicate) {
|
|
|
|
// Both are control ranges and the selected element matches
|
|
|
|
if (rng1.item && rng2.item && rng1.item(0) === rng2.item(0)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Both are text ranges and the range matches
|
|
|
|
if (rng1.isEqual && rng2.isEqual && rng2.isEqual(rng1)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Compare w3c ranges
|
|
|
|
return rng1.startContainer == rng2.startContainer && rng1.startOffset == rng2.startOffset;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
2015-06-15 18:01:48 +08:00
|
|
|
/**
|
|
|
|
* Gets the caret range for the given x/y location.
|
|
|
|
*
|
|
|
|
* @static
|
|
|
|
* @method getCaretRangeFromPoint
|
|
|
|
* @param {Number} x X coordinate for range
|
|
|
|
* @param {Number} y Y coordinate for range
|
|
|
|
* @param {Document} doc Document that x/y are relative to
|
|
|
|
* @returns {Range} caret range
|
|
|
|
*/
|
|
|
|
RangeUtils.getCaretRangeFromPoint = function(x, y, doc) {
|
|
|
|
var rng, point;
|
|
|
|
|
|
|
|
if (doc.caretPositionFromPoint) {
|
|
|
|
point = doc.caretPositionFromPoint(x, y);
|
|
|
|
rng = doc.createRange();
|
|
|
|
rng.setStart(point.offsetNode, point.offset);
|
|
|
|
rng.collapse(true);
|
|
|
|
} else if (doc.caretRangeFromPoint) {
|
|
|
|
rng = doc.caretRangeFromPoint(x, y);
|
|
|
|
} else if (doc.body.createTextRange) {
|
|
|
|
rng = doc.body.createTextRange();
|
|
|
|
|
|
|
|
try {
|
|
|
|
rng.moveToPoint(x, y);
|
|
|
|
rng.collapse(true);
|
|
|
|
} catch (ex) {
|
|
|
|
// Append to top or bottom depending on drop location
|
|
|
|
rng.collapse(y < doc.body.clientHeight);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return rng;
|
|
|
|
};
|
|
|
|
|
|
|
|
RangeUtils.getNode = function(container, offset) {
|
|
|
|
if (container.nodeType == 1 && container.hasChildNodes()) {
|
|
|
|
if (offset >= container.childNodes.length) {
|
|
|
|
offset = container.childNodes.length - 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
container = container.childNodes[offset];
|
|
|
|
}
|
|
|
|
|
|
|
|
return container;
|
|
|
|
};
|
|
|
|
|
2014-05-07 13:06:24 +08:00
|
|
|
return RangeUtils;
|
|
|
|
});
|