/**
* @ignore
* Range implementation across browsers for kissy editor.
* @author yiminghe@gmail.com
*/
/*
Copyright (c) 2003-2010, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
KISSY.add("editor/range", function (S, Editor, Utils, Walker, ElementPath) {
/**
* Enum for range
* @enum {number} KISSY.Editor.RangeType
*/
Editor.RangeType = {
POSITION_AFTER_START: 1, // <element>^contents</element> "^text"
POSITION_BEFORE_END: 2, // <element>contents^</element> "text^"
POSITION_BEFORE_START: 3, // ^<element>contents</element> ^"text"
POSITION_AFTER_END: 4, // <element>contents</element>^ "text"^
ENLARGE_ELEMENT: 1,
ENLARGE_BLOCK_CONTENTS: 2,
ENLARGE_LIST_ITEM_CONTENTS: 3,
START: 1,
END: 2,
SHRINK_ELEMENT: 1,
SHRINK_TEXT: 2
};
var TRUE = true,
FALSE = false,
NULL = null,
KER = Editor.RangeType,
KEP = Editor.PositionType,
Dom = S.DOM,
UA = S.UA,
dtd = Editor.XHTML_DTD,
Node = S.Node,
$ = Node.all,
UN_REMOVABLE = {
'td': 1
},
EMPTY = {
"area": 1, "base": 1, "br": 1,
"col": 1, "hr": 1, "img": 1,
"input": 1, "link": 1, "meta": 1,
"param": 1
};
var isWhitespace = new Walker.whitespaces(),
isBookmark = new Walker.bookmark(),
isNotWhitespaces = Walker.whitespaces(TRUE),
isNotBookmarks = Walker.bookmark(false, true);
var inlineChildReqElements = {
"abbr": 1, "acronym": 1, "b": 1, "bdo": 1,
"big": 1, "cite": 1, "code": 1, "del": 1, "dfn": 1,
"em": 1, "font": 1, "i": 1, "ins": 1, "label": 1,
"kbd": 1, "q": 1, "samp": 1, "small": 1, "span": 1,
"strike": 1, "strong": 1, "sub": 1, "sup": 1,
"tt": 1, "u": 1, 'var': 1
};
// Evaluator for checkBoundaryOfElement, reject any
// text node and non-empty elements unless it's being bookmark text.
function elementBoundaryEval(node) {
// Reject any text node unless it's being bookmark
// OR it's spaces. (#3883)
// 如果不是文本节点并且是空的,可以继续取下一个判断边界
var c1 = node.nodeType != Dom.NodeType.TEXT_NODE &&
Dom.nodeName(node) in dtd.$removeEmpty,
// 文本为空,可以继续取下一个判断边界
c2 = node.nodeType == Dom.NodeType.TEXT_NODE && !S.trim(node.nodeValue),
// 恩,进去了书签,可以继续取下一个判断边界
c3 = !!node.parentNode.getAttribute('_ke_bookmark');
return c1 || c2 || c3;
}
function nonWhitespaceOrIsBookmark(node) {
// Whitespaces and bookmark nodes are to be ignored.
return !isWhitespace(node) && !isBookmark(node);
}
function getCheckStartEndBlockEvalFunction(isStart) {
var hadBr = FALSE;
return function (node) {
// First ignore bookmark nodes.
if (isBookmark(node))
return TRUE;
if (node.nodeType == Dom.NodeType.TEXT_NODE) {
// If there's any visible text, then we're not at the start.
if (S.trim(node.nodeValue).length) {
return FALSE;
}
} else if (node.nodeType == Dom.NodeType.ELEMENT_NODE) {
var nodeName = Dom.nodeName(node);
// If there are non-empty inline elements (e.g. <img />), then we're not
// at the start.
if (!inlineChildReqElements[ nodeName ]) {
// If we're working at the end-of-block, forgive the first <br /> in non-IE
// browsers.
if (!isStart && !UA['ie'] && nodeName == 'br' && !hadBr) {
hadBr = TRUE;
} else {
return FALSE;
}
}
}
return TRUE;
};
}
/*
Extract html content within range.
0 : delete
1 : extract
2 : clone
*/
function execContentsAction(self, action) {
var startNode = self.startContainer,
endNode = self.endContainer,
startOffset = self.startOffset,
endOffset = self.endOffset,
removeStartNode,
hasSplitStart = FALSE,
hasSplitEnd = FALSE,
t,
docFrag = undefined,
doc = self.document,
removeEndNode;
if (action > 0) {
docFrag = doc.createDocumentFragment();
}
if (self.collapsed) {
return docFrag;
}
// 将 bookmark 包含在选区内
self.optimizeBookmark();
// endNode -> end guard , not included in range
// For text containers, we must simply split the node and point to the
// second part. The removal will be handled by the rest of the code .
//最关键:一般起始都是在文字节点中,得到起点选择右边的文字节点,只对节点处理!
if (endNode[0].nodeType == Dom.NodeType.TEXT_NODE) {
hasSplitEnd = TRUE;
endNode = endNode._4e_splitText(endOffset);
} else {
// If the end container has children and the offset is pointing
// to a child, then we should start from it.
if (endNode[0].childNodes.length > 0) {
// If the offset points after the last node.
if (endOffset >= endNode[0].childNodes.length) {
// Let's create a temporary node and mark it for removal.
endNode = new Node(
endNode[0].appendChild(doc.createTextNode(""))
);
removeEndNode = TRUE;
} else {
endNode = new Node(endNode[0].childNodes[endOffset]);
}
}
}
// startNode -> start guard , not included in range
// For text containers, we must simply split the node. The removal will
// be handled by the rest of the code .
if (startNode[0].nodeType == Dom.NodeType.TEXT_NODE) {
hasSplitStart = TRUE;
startNode._4e_splitText(startOffset);
} else {
// If the start container has children and the offset is pointing
// to a child, then we should start from its previous sibling.
// If the offset points to the first node, we don't have a
// sibling, so let's use the first one, but mark it for removal.
if (!startOffset) {
// Let's create a temporary node and mark it for removal.
t = new Node(doc.createTextNode(""));
startNode.prepend(t);
startNode = t;
removeStartNode = TRUE;
}
else if (startOffset >= startNode[0].childNodes.length) {
// Let's create a temporary node and mark it for removal.
startNode = new Node(startNode[0]
.appendChild(doc.createTextNode('')));
removeStartNode = TRUE;
} else
startNode = new Node(
startNode[0].childNodes[startOffset].previousSibling
);
}
// Get the parent nodes tree for the start and end boundaries.
//从根到自己
var startParents = startNode._4e_parents(),
endParents = endNode._4e_parents();
startParents.each(function (n, i) {
startParents[i] = n;
});
endParents.each(function (n, i) {
endParents[i] = n;
});
// Compare them, to find the top most siblings.
var i, topStart, topEnd;
for (i = 0; i < startParents.length; i++) {
topStart = startParents[ i ];
topEnd = endParents[ i ];
// The compared nodes will match until we find the top most
// siblings (different nodes that have the same parent).
// "i" will hold the index in the parents array for the top
// most element.
if (!topStart.equals(topEnd)) {
break;
}
}
var clone = docFrag,
levelStartNode,
levelClone,
currentNode,
currentSibling;
// Remove all successive sibling nodes for every node in the
// startParents tree.
for (var j = i; j < startParents.length; j++) {
levelStartNode = startParents[j];
// For Extract and Clone, we must clone this level.
if (action > 0 && !levelStartNode.equals(startNode)) {
// action = 0 = Delete
levelClone = clone.appendChild(levelStartNode.clone()[0]);
} else {
levelClone = null;
}
// 开始节点的路径所在父节点不能 clone(TRUE),其他节点(结束节点路径左边的节点)可以直接 clone(true)
currentNode = levelStartNode[0].nextSibling;
var endParentJ = endParents[ j ],
domEndNode = endNode[0],
domEndParentJ = endParentJ && endParentJ[0];
while (currentNode) {
// Stop processing when the current node matches a node in the
// endParents tree or if it is the endNode.
if (domEndParentJ == currentNode || domEndNode == currentNode) {
break;
}
// Cache the next sibling.
currentSibling = currentNode.nextSibling;
// If cloning, just clone it.
if (action == 2) {
// 2 = Clone
clone.appendChild(currentNode.cloneNode(TRUE));
} else {
// https://github.com/kissyteam/kissy/issues/418
// in case table structure is destroyed
if (UN_REMOVABLE[currentNode.nodeName.toLowerCase()]) {
var tmp = currentNode.cloneNode(TRUE);
currentNode.innerHTML='';
currentNode = tmp;
} else {
// Both Delete and Extract will remove the node.
Dom._4e_remove(currentNode);
}
// When Extracting, move the removed node to the docFrag.
if (action == 1) {
// 1 = Extract
clone.appendChild(currentNode);
}
}
currentNode = currentSibling;
}
// 开始节点的路径所在父节点不能 clone(TRUE),要在后面深入子节点处理
if (levelClone) {
clone = levelClone;
}
}
clone = docFrag;
// Remove all previous sibling nodes for every node in the
// endParents tree.
for (var k = i; k < endParents.length; k++) {
levelStartNode = endParents[ k ];
// For Extract and Clone, we must clone this level.
if (action > 0 && !levelStartNode.equals(endNode)) {
// action = 0 = Delete
// 浅复制
levelClone = clone.appendChild(levelStartNode.clone()[0]);
} else {
levelClone = null;
}
// The processing of siblings may have already been done by the parent.
if (
!startParents[ k ] ||
// 前面 startParents 循环已经处理过了
!levelStartNode._4e_sameLevel(startParents[ k ])
) {
currentNode = levelStartNode[0].previousSibling;
while (currentNode) {
// Cache the next sibling.
currentSibling = currentNode.previousSibling;
// If cloning, just clone it.
if (action == 2) { // 2 = Clone
clone.insertBefore(currentNode.cloneNode(TRUE),
clone.firstChild);
} else {
// Both Delete and Extract will remove the node.
Dom._4e_remove(currentNode);
// When Extracting, mode the removed node to the docFrag.
if (action == 1) {
// 1 = Extract
clone.insertBefore(currentNode, clone.firstChild);
}
}
currentNode = currentSibling;
}
}
if (levelClone) {
clone = levelClone;
}
}
// 2 = Clone.
if (action == 2) {
// No changes in the Dom should be done, so fix the split text (if any).
if (hasSplitStart) {
var startTextNode = startNode[0];
if (startTextNode.nodeType == Dom.NodeType.TEXT_NODE
&& startTextNode.nextSibling
// careful, next sibling should be text node
&& startTextNode.nextSibling.nodeType == Dom.NodeType.TEXT_NODE) {
startTextNode.data += startTextNode.nextSibling.data;
startTextNode.parentNode.removeChild(startTextNode.nextSibling);
}
}
if (hasSplitEnd) {
var endTextNode = endNode[0];
if (endTextNode.nodeType == Dom.NodeType.TEXT_NODE &&
endTextNode.previousSibling &&
endTextNode.previousSibling.nodeType == Dom.NodeType.TEXT_NODE) {
endTextNode.previousSibling.data += endTextNode.data;
endTextNode.parentNode.removeChild(endTextNode);
}
}
} else {
// Collapse the range.
// If a node has been partially selected, collapse the range between
// topStart and topEnd. Otherwise, simply collapse it to the start.
// (W3C specs).
if (
topStart && topEnd &&
(
!startNode._4e_sameLevel(topStart)
|| !endNode._4e_sameLevel(topEnd)
)
) {
var startIndex = topStart._4e_index();
// If the start node is to be removed, we must correct the
// index to reflect the removal.
if (removeStartNode &&
// startNode 和 topStart 同级
(topStart._4e_sameLevel(startNode))) {
startIndex--;
}
self.setStart(topStart.parent(), startIndex + 1);
}
// Collapse it to the start.
self.collapse(TRUE);
}
// Cleanup any marked node.
if (removeStartNode) {
startNode.remove();
}
if (removeEndNode) {
endNode.remove();
}
return docFrag;
}
function updateCollapsed(self) {
self.collapsed = (
self.startContainer &&
self.endContainer &&
self.startContainer[0] == self.endContainer[0] &&
self.startOffset == self.endOffset );
}
/**
* Range implementation across browsers.
* @class KISSY.Editor.Range
* @param document {Document}
*/
function KERange(document) {
var self = this;
self.startContainer = NULL;
self.startOffset = NULL;
self.endContainer = NULL;
self.endOffset = NULL;
self.collapsed = TRUE;
self.document = document;
}
S.augment(KERange, {
/**
* Range string representation.
*/
toString: function () {
var s = [],
self = this,
startContainer = self.startContainer[0],
endContainer = self.endContainer[0];
s.push((startContainer.id || startContainer.nodeName) + ":" + self.startOffset);
s.push((endContainer.id || endContainer.nodeName) + ":" + self.endOffset);
return s.join("<br/>");
},
/**
* Transforms the startContainer and endContainer properties from text
* nodes to element nodes, whenever possible. This is actually possible
* if either of the boundary containers point to a text node, and its
* offset is set to zero, or after the last char in the node.
*/
optimize: function () {
var self = this,
container = self.startContainer,
offset = self.startOffset;
if (container[0].nodeType != Dom.NodeType.ELEMENT_NODE) {
if (!offset) {
self.setStartBefore(container);
} else if (offset >= container[0].nodeValue.length) {
self.setStartAfter(container);
}
}
container = self.endContainer;
offset = self.endOffset;
if (container[0].nodeType != Dom.NodeType.ELEMENT_NODE) {
if (!offset) {
self.setEndBefore(container);
} else if (offset >= container[0].nodeValue.length) {
self.setEndAfter(container);
}
}
},
/**
* Set range start after node
* @param {KISSY.NodeList} node
*/
setStartAfter: function (node) {
this.setStart(node.parent(), node._4e_index() + 1);
},
/**
* Set range start before node
* @param {KISSY.NodeList} node
*/
setStartBefore: function (node) {
this.setStart(node.parent(), node._4e_index());
},
/**
* Set range end after node
* @param {KISSY.NodeList} node
*/
setEndAfter: function (node) {
this.setEnd(node.parent(), node._4e_index() + 1);
},
/**
* Set range end before node
* @param {KISSY.NodeList} node
*/
setEndBefore: function (node) {
this.setEnd(node.parent(), node._4e_index());
},
/**
* Make edge bookmarks included in current range.
*/
optimizeBookmark: function () {
var self = this,
startNode = self.startContainer,
endNode = self.endContainer;
if (startNode &&
startNode.nodeName() == 'span' &&
startNode.attr('_ke_bookmark')) {
self.setStartBefore(startNode);
}
if (endNode &&
endNode.nodeName() == 'span' &&
endNode.attr('_ke_bookmark')) {
self.setEndAfter(endNode);
}
},
/**
* Sets the start position of a Range.
* @param {KISSY.NodeList} startNode The node to start the range.
* @param {Number} startOffset An integer greater than or equal to zero
* representing the offset for the start of the range from the start
* of startNode.
*/
setStart: function (startNode, startOffset) {
// W3C requires a check for the new position. If it is after the end
// boundary, the range should be collapsed to the new start. It seams
// we will not need this check for our use of this class so we can
// ignore it for now.
// Fixing invalid range start inside dtd empty elements.
var self = this;
if (startNode[0].nodeType == Dom.NodeType.ELEMENT_NODE && EMPTY[ startNode.nodeName() ]) {
startNode = startNode.parent();
startOffset = startNode._4e_index();
}
self.startContainer = startNode;
self.startOffset = startOffset;
if (!self.endContainer) {
self.endContainer = startNode;
self.endOffset = startOffset;
}
updateCollapsed(self);
},
/**
* Sets the end position of a Range.
* @param {KISSY.NodeList} endNode The node to end the range.
* @param {Number} endOffset An integer greater than or equal to zero
* representing the offset for the end of the range from the start
* of endNode.
*/
setEnd: function (endNode, endOffset) {
// W3C requires a check for the new position. If it is before the start
// boundary, the range should be collapsed to the new end. It seams we
// will not need this check for our use of this class so we can ignore
// it for now.
// Fixing invalid range end inside dtd empty elements.
var self = this;
if (endNode[0].nodeType == Dom.NodeType.ELEMENT_NODE && EMPTY[ endNode.nodeName() ]) {
endNode = endNode.parent();
endOffset = endNode._4e_index() + 1;
}
self.endContainer = endNode;
self.endOffset = endOffset;
if (!self.startContainer) {
self.startContainer = endNode;
self.startOffset = endOffset;
}
updateCollapsed(self);
},
/**
* Sets the start position of a Range by specified rules.
* @param {KISSY.NodeList} node
* @param {Number} position
*/
setStartAt: function (node, position) {
var self = this;
switch (position) {
case KER.POSITION_AFTER_START :
self.setStart(node, 0);
break;
case KER.POSITION_BEFORE_END :
if (node[0].nodeType == Dom.NodeType.TEXT_NODE) {
self.setStart(node, node[0].nodeValue.length);
} else {
self.setStart(node, node[0].childNodes.length);
}
break;
case KER.POSITION_BEFORE_START :
self.setStartBefore(node);
break;
case KER.POSITION_AFTER_END :
self.setStartAfter(node);
}
updateCollapsed(self);
},
/**
* Sets the end position of a Range by specified rules.
* @param {KISSY.NodeList} node
* @param {Number} position
*/
setEndAt: function (node, position) {
var self = this;
switch (position) {
case KER.POSITION_AFTER_START :
self.setEnd(node, 0);
break;
case KER.POSITION_BEFORE_END :
if (node[0].nodeType == Dom.NodeType.TEXT_NODE) {
self.setEnd(node, node[0].nodeValue.length);
} else {
self.setEnd(node, node[0].childNodes.length);
}
break;
case KER.POSITION_BEFORE_START :
self.setEndBefore(node);
break;
case KER.POSITION_AFTER_END :
self.setEndAfter(node);
}
updateCollapsed(self);
},
/**
* Clone html content within range
*/
cloneContents: function () {
return execContentsAction(this, 2);
},
/**
* Remove html content within range
*/
deleteContents: function () {
return execContentsAction(this, 0);
},
* Extract html content within range.
*/
extractContents: function () {
return execContentsAction(this, 1);
},
/**
* collapse current range
* @param {Boolean} toStart
*/
collapse: function (toStart) {
var self = this;
if (toStart) {
self.endContainer = self.startContainer;
self.endOffset = self.startOffset;
} else {
self.startContainer = self.endContainer;
self.startOffset = self.endOffset;
}
self.collapsed = TRUE;
},
/**
* Clone current range.
* @return {KISSY.Editor.Range}
*/
clone: function () {
var self = this,
clone = new KERange(self.document);
clone.startContainer = self.startContainer;
clone.startOffset = self.startOffset;
clone.endContainer = self.endContainer;
clone.endOffset = self.endOffset;
clone.collapsed = self.collapsed;
return clone;
},
/**
* Get node which is enclosed by range.
*
* @example
* ^<book/><span/><book/>^
* <!-- => -->
* ^<span/>^
*/
getEnclosedNode: function () {
var walkerRange = this.clone();
// Optimize and analyze the range to avoid Dom destructive nature of walker.
walkerRange.optimize();
if (walkerRange.startContainer[0].nodeType != Dom.NodeType.ELEMENT_NODE ||
walkerRange.endContainer[0].nodeType != Dom.NodeType.ELEMENT_NODE) {
return NULL;
}
var walker = new Walker(walkerRange),
node, pre;
walker.evaluator = function (node) {
return isNotWhitespaces(node) && isNotBookmarks(node);
};
//深度优先遍历的第一个元素
// x
// y z
// x->y ,return y
node = walker.next();
walker.reset();
pre = walker.previous();
//前后相等,则脱一层皮 :)
return node && node.equals(pre) ? node : NULL;
},
/**
* Shrink range to its innermost element.(make sure text content is unchanged)
* @param mode
* @param {Boolean} [selectContents]
*/
shrink: function (mode, selectContents) {
// Unable to shrink a collapsed range.
var self = this;
if (!self.collapsed) {
mode = mode || KER.SHRINK_TEXT;
var walkerRange = self.clone(),
startContainer = self.startContainer,
endContainer = self.endContainer,
startOffset = self.startOffset,
endOffset = self.endOffset,
// Whether the start/end boundary is movable.
moveStart = TRUE,
currentElement,
walker,
moveEnd = TRUE;
if (startContainer &&
startContainer[0].nodeType == Dom.NodeType.TEXT_NODE) {
if (!startOffset) {
walkerRange.setStartBefore(startContainer);
} else if (startOffset >= startContainer[0].nodeValue.length) {
walkerRange.setStartAfter(startContainer);
} else {
// Enlarge the range properly to avoid walker making
// Dom changes caused by trimming the text nodes later.
walkerRange.setStartBefore(startContainer);
moveStart = FALSE;
}
}
if (endContainer &&
endContainer[0].nodeType == Dom.NodeType.TEXT_NODE) {
if (!endOffset) {
walkerRange.setEndBefore(endContainer);
} else if (endOffset >= endContainer[0].nodeValue.length) {
walkerRange.setEndAfter(endContainer);
} else {
walkerRange.setEndAfter(endContainer);
moveEnd = FALSE;
}
}
if (moveStart || moveEnd) {
walker = new Walker(walkerRange);
walker.evaluator = function (node) {
return node.nodeType == ( mode == KER.SHRINK_ELEMENT ?
Dom.NodeType.ELEMENT_NODE : Dom.NodeType.TEXT_NODE );
};
walker.guard = function (node, movingOut) {
// Stop when we're shrink in element mode while encountering a text node.
if (mode == KER.SHRINK_ELEMENT &&
node.nodeType == Dom.NodeType.TEXT_NODE) {
return FALSE;
}
// Stop when we've already walked "through" an element.
if (movingOut && node == currentElement) {
return FALSE;
}
if (!movingOut && node.nodeType == Dom.NodeType.ELEMENT_NODE) {
currentElement = node;
}
return TRUE;
};
}
if (moveStart) {
var textStart = walker[mode == KER.SHRINK_ELEMENT ? 'lastForward' : 'next']();
if (textStart) {
self.setStartAt(textStart, selectContents ? KER.POSITION_AFTER_START : KER.POSITION_BEFORE_START);
}
}
if (moveEnd) {
walker.reset();
var textEnd = walker[mode == KER.SHRINK_ELEMENT ? 'lastBackward' : 'previous']();
if (textEnd) {
self.setEndAt(textEnd, selectContents ? KER.POSITION_BEFORE_END : KER.POSITION_AFTER_END);
}
}
return moveStart || moveEnd;
}
},
/**
* Create virtual bookmark by remeber its position index.
* @param normalized
*/
createBookmark2: function (normalized) {
var self = this,
startContainer = self.startContainer,
endContainer = self.endContainer,
startOffset = self.startOffset,
endOffset = self.endOffset,
child, previous;
// If there is no range then get out of here.
// It happens on initial load in Safari #962 and if the editor it's
// hidden also in Firefox
if (!startContainer || !endContainer) {
return {
start: 0,
end: 0
};
}
if (normalized) {
// Find out if the start is pointing to a text node that will
// be normalized.
if (startContainer[0].nodeType == Dom.NodeType.ELEMENT_NODE) {
child = new Node(startContainer[0].childNodes[startOffset]);
// In this case, move the start information to that text
// node.
if (child && child[0] && child[0].nodeType == Dom.NodeType.TEXT_NODE
&& startOffset > 0 && child[0].previousSibling.nodeType == Dom.NodeType.TEXT_NODE) {
startContainer = child;
startOffset = 0;
}
}
// Normalize the start.
while (startContainer[0].nodeType == Dom.NodeType.TEXT_NODE
&& ( previous = startContainer.prev(undefined, 1) )
&& previous[0].nodeType == Dom.NodeType.TEXT_NODE) {
startContainer = previous;
startOffset += previous[0].nodeValue.length;
}
// Process the end only if not normalized.
if (!self.collapsed) {
// Find out if the start is pointing to a text node that
// will be normalized.
if (endContainer[0].nodeType == Dom.NodeType.ELEMENT_NODE) {
child = new Node(endContainer[0].childNodes[endOffset]);
// In this case, move the start information to that
// text node.
if (child && child[0] &&
child[0].nodeType == Dom.NodeType.TEXT_NODE && endOffset > 0 &&
child[0].previousSibling.nodeType == Dom.NodeType.TEXT_NODE) {
endContainer = child;
endOffset = 0;
}
}
// Normalize the end.
while (endContainer[0].nodeType == Dom.NodeType.TEXT_NODE
&& ( previous = endContainer.prev(undefined, 1) )
&& previous[0].nodeType == Dom.NodeType.TEXT_NODE) {
endContainer = previous;
endOffset += previous[0].nodeValue.length;
}
}
}
return {
start: startContainer._4e_address(normalized),
end: self.collapsed ? NULL : endContainer._4e_address(normalized),
startOffset: startOffset,
endOffset: endOffset,
normalized: normalized,
is2: TRUE // It's a createBookmark2 bookmark.
};
},
/**
* Create bookmark by create bookmark node.
* @param {Boolean} [serializable]
*/
createBookmark: function (serializable) {
var startNode,
endNode,
baseId,
clone,
self = this,
collapsed = self.collapsed;
startNode = new Node("<span>", NULL, self.document);
startNode.attr('_ke_bookmark', 1);
startNode.css('display', 'none');
// For IE, it must have something inside, otherwise it may be
// removed during Dom operations.
startNode.html(' ');
if (serializable) {
baseId = S.guid('ke_bm_');
startNode.attr('id', baseId + 'S');
}
// If collapsed, the endNode will not be created.
if (!collapsed) {
endNode = startNode.clone();
endNode.html(' ');
if (serializable) {
endNode.attr('id', baseId + 'E');
}
clone = self.clone();
clone.collapse();
clone.insertNode(endNode);
}
clone = self.clone();
clone.collapse(TRUE);
clone.insertNode(startNode);
// Update the range position.
if (endNode) {
self.setStartAfter(startNode);
self.setEndBefore(endNode);
} else {
self.moveToPosition(startNode, KER.POSITION_AFTER_END);
}
return {
startNode: serializable ? baseId + 'S' : startNode,
endNode: serializable ? baseId + 'E' : endNode,
serializable: serializable,
collapsed: collapsed
};
},
/**
* Set the start position and then collapse range.
* @param {KISSY.NodeList} node
* @param {Number} position
*/
moveToPosition: function (node, position) {
var self = this;
self.setStartAt(node, position);
self.collapse(TRUE);
},
/**
* Pull range out of text edge and split text node if range is in the middle of text node.
* @param {Boolean} ignoreStart
* @param {Boolean} ignoreEnd
*/
trim: function (ignoreStart, ignoreEnd) {
var self = this,
startContainer = self.startContainer,
startOffset = self.startOffset,
collapsed = self.collapsed;
if (( !ignoreStart || collapsed ) &&
startContainer[0] &&
startContainer[0].nodeType == Dom.NodeType.TEXT_NODE) {
// If the offset is zero, we just insert the new node before
// the start.
if (!startOffset) {
startOffset = startContainer._4e_index();
startContainer = startContainer.parent();
}
// If the offset is at the end, we'll insert it after the text
// node.
else if (startOffset >= startContainer[0].nodeValue.length) {
startOffset = startContainer._4e_index() + 1;
startContainer = startContainer.parent();
}
// In other case, we split the text node and insert the new
// node at the split point.
else {
var nextText = startContainer._4e_splitText(startOffset);
startOffset = startContainer._4e_index() + 1;
startContainer = startContainer.parent();
// Check all necessity of updating the end boundary.
if (Dom.equals(self.startContainer, self.endContainer)) {
self.setEnd(nextText, self.endOffset - self.startOffset);
} else if (Dom.equals(startContainer, self.endContainer)) {
self.endOffset += 1;
}
}
self.setStart(startContainer, startOffset);
if (collapsed) {
self.collapse(TRUE);
return;
}
}
var endContainer = self.endContainer,
endOffset = self.endOffset;
if (!( ignoreEnd || collapsed ) &&
endContainer[0] && endContainer[0].nodeType == Dom.NodeType.TEXT_NODE) {
// If the offset is zero, we just insert the new node before
// the start.
if (!endOffset) {
endOffset = endContainer._4e_index();
endContainer = endContainer.parent();
}
// If the offset is at the end, we'll insert it after the text
// node.
else if (endOffset >= endContainer[0].nodeValue.length) {
endOffset = endContainer._4e_index() + 1;
endContainer = endContainer.parent();
}
// In other case, we split the text node and insert the new
// node at the split point.
else {
endContainer._4e_splitText(endOffset);
endOffset = endContainer._4e_index() + 1;
endContainer = endContainer.parent();
}
self.setEnd(endContainer, endOffset);
}
},
/**
* Insert a new node at start position of current range
* @param {KISSY.NodeList} node
*/
insertNode: function (node) {
var self = this;
self.optimizeBookmark();
self.trim(FALSE, TRUE);
var startContainer = self.startContainer,
startOffset = self.startOffset,
nextNode = startContainer[0].childNodes[startOffset] || null;
startContainer[0].insertBefore(node[0], nextNode);
// Check if we need to update the end boundary.
if (startContainer[0] == self.endContainer[0]) {
self.endOffset++;
}
// Expand the range to embrace the new node.
self.setStartBefore(node);
},
/**
* Move range to previous saved bookmark.
* @param bookmark
*/
moveToBookmark: function (bookmark) {
var self = this,
doc = $(self.document);
if (bookmark.is2) {
// Get the start information.
var startContainer = doc._4e_getByAddress(bookmark.start, bookmark.normalized),
startOffset = bookmark.startOffset,
endContainer = bookmark.end && doc._4e_getByAddress(bookmark.end, bookmark.normalized),
endOffset = bookmark.endOffset;
// Set the start boundary.
self.setStart(startContainer, startOffset);
// Set the end boundary. If not available, collapse it.
if (endContainer) {
self.setEnd(endContainer, endOffset);
} else {
self.collapse(TRUE);
}
} else {
// Created with createBookmark().
var serializable = bookmark.serializable,
startNode = serializable ? S.one("#" + bookmark.startNode,
doc) : bookmark.startNode,
endNode = serializable ? S.one("#" + bookmark.endNode,
doc) : bookmark.endNode;
// Set the range start at the bookmark start node position.
self.setStartBefore(startNode);
// Remove it, because it may interfere in the setEndBefore call.
startNode._4e_remove();
// Set the range end at the bookmark end node position, or simply
// collapse it if it is not available.
if (endNode && endNode[0]) {
self.setEndBefore(endNode);
endNode._4e_remove();
} else {
self.collapse(TRUE);
}
}
},
/**
* Find the node which contains current range completely.
* @param {Boolean} includeSelf whether to return the only element with in range
* @param {Boolean} ignoreTextNode whether to return text node's parent node.
*/
getCommonAncestor: function (includeSelf, ignoreTextNode) {
var self = this,
start = self.startContainer,
end = self.endContainer,
ancestor;
if (start[0] == end[0]) {
if (includeSelf &&
start[0].nodeType == Dom.NodeType.ELEMENT_NODE &&
self.startOffset == self.endOffset - 1) {
ancestor = new Node(start[0].childNodes[self.startOffset]);
} else {
ancestor = start;
}
} else {
ancestor = start._4e_commonAncestor(end);
}
return ignoreTextNode && ancestor[0].nodeType == Dom.NodeType.TEXT_NODE
? ancestor.parent() : ancestor;
},
/**
* Enlarge the range as mush as possible
* @param {Number} unit
* @method
*
*
* <div><span><span>^1</span>2^</span>x</div>
* =>
* <div>^<span><span>1</span>2</span>^x</div>
*/
enlarge: (function () {
function enlargeElement(self, left, stop, commonAncestor) {
var container = self[left ? 'startContainer' : 'endContainer'],
enlarge,
sibling,
index = left ? 0 : 1,
commonReached = 0,
direction = left ? "previousSibling" : "nextSibling",
offset = self[left ? 'startOffset' : 'endOffset'];
if (container[0].nodeType == Dom.NodeType.TEXT_NODE) {
if (left) {
// 不在字的开头,立即结束
if (offset) {
return;
}
} else {
if (offset < container[0].nodeValue.length) {
return
}
}
// 文字节点的兄弟
sibling = container[0][direction];
// 可能会扩展到到的容器节点
enlarge = container[0].parentNode;
} else {
// 开始节点的兄弟节点
sibling = container[0].childNodes[offset + (left ? -1 : 1)] || null;
// 可能会扩展到到的容器节点
enlarge = container[0];
}
while (enlarge) {
// 兄弟节点是否都是空节点?
while (sibling) {
if (isWhitespace(sibling) || isBookmark(sibling)) {
sibling = sibling[direction];
} else {
break;
}
}
// 一个兄弟节点阻止了扩展
if (sibling) {
// 如果没有超过公共祖先
if (!commonReached) {
// 仅仅扩展到兄弟
self[left ? 'setStartAfter' : 'setEndBefore']($(sibling));
}
return;
}
// 没有兄弟节点阻止
// 超过了公共祖先,先记下来,最终不能 partly 选择某个节点,要完全选中
enlarge = $(enlarge);
if (enlarge.nodeName() == "body") {
return;
}
if (commonReached || enlarge.equals(commonAncestor)) {
stop[index] = enlarge;
commonReached = 1;
} else {
// 扩展到容器外边
self[left ? 'setStartBefore' : 'setEndAfter'](enlarge);
}
sibling = enlarge[0][direction];
enlarge = enlarge[0].parentNode;
}
}
return function (unit) {
var self = this;
switch (unit) {
case KER.ENLARGE_ELEMENT :
if (self.collapsed) {
return;
}
var commonAncestor = self.getCommonAncestor(),
stop = [];
enlargeElement(self, 1, stop, commonAncestor);
enlargeElement(self, 0, stop, commonAncestor);
if (stop[0] && stop[1]) {
var commonStop = stop[0].contains(stop[1]) ? stop[1] : stop[0];
self.setStartBefore(commonStop);
self.setEndAfter(commonStop);
}
break;
case KER.ENLARGE_BLOCK_CONTENTS:
case KER.ENLARGE_LIST_ITEM_CONTENTS:
// Enlarging the start boundary.
var walkerRange = new KERange(self.document);
var body = new Node(self.document.body);
walkerRange.setStartAt(body, KER.POSITION_AFTER_START);
walkerRange.setEnd(self.startContainer, self.startOffset);
var walker = new Walker(walkerRange),
blockBoundary, // The node on which the enlarging should stop.
tailBr, //
defaultGuard = Walker.blockBoundary(
( unit == KER.ENLARGE_LIST_ITEM_CONTENTS ) ?
{ br: 1 } : NULL),
// Record the encountered 'blockBoundary' for later use.
boundaryGuard = function (node) {
var retVal = defaultGuard(node);
if (!retVal) {
blockBoundary = $(node);
}
return retVal;
},
// Record the encountered 'tailBr' for later use.
tailBrGuard = function (node) {
var retVal = boundaryGuard(node);
if (!retVal && Dom.nodeName(node) == 'br') {
tailBr = $(node);
}
return retVal;
};
walker.guard = boundaryGuard;
enlargeable = walker.lastBackward();
// It's the body which stop the enlarging if no block boundary found.
blockBoundary = blockBoundary || body;
// Start the range at different position by comparing
// the document position of it with 'enlargeable' node.
self.setStartAt(
blockBoundary,
blockBoundary.nodeName() != 'br' &&
// <table></table> <span>1234^56</span> <table></table>
// =>
// <table></table> ^<span>123456</span>$ <table></table>
// <p> <span>123^456</span> </p>
// =>
// <p> ^<span>123456</span>$ </p>
( !enlargeable && self.checkStartOfBlock()
|| enlargeable && blockBoundary.contains(enlargeable) ) ?
KER.POSITION_AFTER_START :
KER.POSITION_AFTER_END);
// Enlarging the end boundary.
walkerRange = self.clone();
walkerRange.collapse();
walkerRange.setEndAt(body, KER.POSITION_BEFORE_END);
walker = new Walker(walkerRange);
// tailBrGuard only used for on range end.
walker.guard = ( unit == KER.ENLARGE_LIST_ITEM_CONTENTS ) ?
tailBrGuard : boundaryGuard;
blockBoundary = NULL;
// End the range right before the block boundary node.
var enlargeable = walker.lastForward();
// It's the body which stop the enlarging if no block boundary found.
blockBoundary = blockBoundary || body;
// Start the range at different position by comparing
// the document position of it with 'enlargeable' node.
self.setEndAt(
blockBoundary,
( !enlargeable && self.checkEndOfBlock()
|| enlargeable && blockBoundary.contains(enlargeable) ) ?
KER.POSITION_BEFORE_END :
KER.POSITION_BEFORE_START);
// We must include the <br> at the end of range if there's
// one and we're expanding list item contents
if (tailBr) {
self.setEndAfter(tailBr);
}
}
}
})(),
/**
* Check whether current range 's start position is at the start of a block (visible)
* @return Boolean
*/
checkStartOfBlock: function () {
var self = this,
startContainer = self.startContainer,
startOffset = self.startOffset;
// If the starting node is a text node, and non-empty before the offset,
// then we're surely not at the start of block.
if (startOffset && startContainer[0].nodeType == Dom.NodeType.TEXT_NODE) {
var textBefore = S.trim(startContainer[0].nodeValue.substring(0, startOffset));
if (textBefore.length) {
return FALSE;
}
}
// Anticipate the trim() call here, so the walker will not make
// changes to the Dom, which would not get reflected into this
// range otherwise.
self.trim();
// We need to grab the block element holding the start boundary, so
// let's use an element path for it.
var path = new ElementPath(self.startContainer);
// Creates a range starting at the block start until the range start.
var walkerRange = self.clone();
walkerRange.collapse(TRUE);
walkerRange.setStartAt(path.block || path.blockLimit, KER.POSITION_AFTER_START);
var walker = new Walker(walkerRange);
walker.evaluator = getCheckStartEndBlockEvalFunction(TRUE);
return walker.checkBackward();
},
/**
* Check whether current range 's end position is at the end of a block (visible)
* @return Boolean
*/
checkEndOfBlock: function () {
var self = this, endContainer = self.endContainer,
endOffset = self.endOffset;
// If the ending node is a text node, and non-empty after the offset,
// then we're surely not at the end of block.
if (endContainer[0].nodeType == Dom.NodeType.TEXT_NODE) {
var textAfter = S.trim(endContainer[0].nodeValue.substring(endOffset));
if (textAfter.length) {
return FALSE;
}
}
// Anticipate the trim() call here, so the walker will not make
// changes to the Dom, which would not get reflected into this
// range otherwise.
self.trim();
// We need to grab the block element holding the start boundary, so
// let's use an element path for it.
var path = new ElementPath(self.endContainer);
// Creates a range starting at the block start until the range start.
var walkerRange = self.clone();
walkerRange.collapse(FALSE);
walkerRange.setEndAt(path.block || path.blockLimit, KER.POSITION_BEFORE_END);
var walker = new Walker(walkerRange);
walker.evaluator = getCheckStartEndBlockEvalFunction(FALSE);
return walker.checkForward();
},
/**
* Check whether current range is on the inner edge of the specified element.
* @param {Number} checkType The checking side.
* @param {KISSY.NodeList} element The target element to check.
*/
checkBoundaryOfElement: function (element, checkType) {
var walkerRange = this.clone();
// Expand the range to element boundary.
walkerRange[ checkType == KER.START ?
'setStartAt' : 'setEndAt' ]
(element, checkType == KER.START ?
KER.POSITION_AFTER_START
: KER.POSITION_BEFORE_END);
var walker = new Walker(walkerRange);
walker.evaluator = elementBoundaryEval;
return walker[ checkType == KER.START ?
'checkBackward' : 'checkForward' ]();
},
/**
* Get two node which are at the edge of current range.
* @return {Object} Map with startNode and endNode as key/value.
*/
getBoundaryNodes: function () {
var self = this,
startNode = self.startContainer,
endNode = self.endContainer,
startOffset = self.startOffset,
endOffset = self.endOffset,
childCount;
if (startNode[0].nodeType == Dom.NodeType.ELEMENT_NODE) {
childCount = startNode[0].childNodes.length;
if (childCount > startOffset) {
startNode = $(startNode[0].childNodes[startOffset]);
} else if (childCount == 0) {
// ?? startNode
startNode = startNode._4e_previousSourceNode();
} else {
// startOffset >= childCount but childCount is not 0
// Try to take the node just after the current position.
startNode = startNode[0];
while (startNode.lastChild) {
startNode = startNode.lastChild;
}
startNode = $(startNode);
// Normally we should take the next node in DFS order. But it
// is also possible that we've already reached the end of
// document.
startNode = startNode._4e_nextSourceNode() || startNode;
}
}
if (endNode[0].nodeType == Dom.NodeType.ELEMENT_NODE) {
childCount = endNode[0].childNodes.length;
if (childCount > endOffset) {
endNode = $(endNode[0].childNodes[endOffset])
// in case endOffset == 0
._4e_previousSourceNode(TRUE);
} else if (childCount == 0) {
endNode = endNode._4e_previousSourceNode();
} else {
// endOffset > childCount but childCount is not 0
// Try to take the node just before the current position.
endNode = endNode[0];
while (endNode.lastChild)
endNode = endNode.lastChild;
endNode = $(endNode);
}
}
// Sometimes the endNode will come right before startNode for collapsed
// ranges. Fix it. (#3780)
if (startNode._4e_position(endNode) & KEP.POSITION_FOLLOWING) {
startNode = endNode;
}
return { startNode: startNode, endNode: endNode };
},
/**
* Wrap the content in range which is block-enlarged
* at the start or end of current range into a block element.
* @param {Boolean} isStart Start or end of current range tobe enlarged.
* @param {String} blockTag Block element's tag name.
* @return {KISSY.NodeList} Newly generated block element.
*/
fixBlock: function (isStart, blockTag) {
var self = this,
bookmark = self.createBookmark(),
fixedBlock = $(self.document.createElement(blockTag));
self.collapse(isStart);
self.enlarge(KER.ENLARGE_BLOCK_CONTENTS);
fixedBlock[0].appendChild(self.extractContents());
fixedBlock._4e_trim();
if (!UA['ie']) {
fixedBlock._4e_appendBogus();
}
self.insertNode(fixedBlock);
self.moveToBookmark(bookmark);
return fixedBlock;
},
/**
* Split current block which current range into two if current range is in the same block.
* Fix block at the start and end position of range if necessary.
* @param {String} blockTag Block tag if need fixBlock
*/
splitBlock: function (blockTag) {
var self = this,
startPath = new ElementPath(self.startContainer),
endPath = new ElementPath(self.endContainer),
startBlockLimit = startPath.blockLimit,
endBlockLimit = endPath.blockLimit,
startBlock = startPath.block,
endBlock = endPath.block,
elementPath = NULL;
// Do nothing if the boundaries are in different block limits.
if (!startBlockLimit.equals(endBlockLimit)) {
return NULL;
}
// Get or fix current blocks.
if (blockTag != 'br') {
if (!startBlock) {
startBlock = self.fixBlock(TRUE, blockTag);
endBlock = new ElementPath(self.endContainer).block;
}
if (!endBlock) {
endBlock = self.fixBlock(FALSE, blockTag);
}
}
// Get the range position.
var isStartOfBlock = startBlock && self.checkStartOfBlock(),
isEndOfBlock = endBlock && self.checkEndOfBlock();
// Delete the current contents.
self.deleteContents();
if (startBlock && startBlock[0] == endBlock[0]) {
if (isEndOfBlock) {
elementPath = new ElementPath(self.startContainer);
self.moveToPosition(endBlock, KER.POSITION_AFTER_END);
endBlock = NULL;
}
else if (isStartOfBlock) {
elementPath = new ElementPath(self.startContainer);
self.moveToPosition(startBlock, KER.POSITION_BEFORE_START);
startBlock = NULL;
}
else {
endBlock = self.splitElement(startBlock);
// In Gecko, the last child node must be a bogus <br>.
// Note: bogus <br> added under <ul> or <ol> would cause
// lists to be incorrectly rendered.
if (!UA['ie'] && !S.inArray(startBlock.nodeName(), ['ul', 'ol'])) {
startBlock._4e_appendBogus();
}
}
}
return {
previousBlock: startBlock,
nextBlock: endBlock,
wasStartOfBlock: isStartOfBlock,
wasEndOfBlock: isEndOfBlock,
elementPath: elementPath
};
},
/**
* Split toSplit element into two parts at current range's start position.
* @param {KISSY.NodeList} toSplit Element to split.
* @return {KISSY.NodeList} The second newly generated element.
*/
splitElement: function (toSplit) {
var self = this;
if (!self.collapsed)
return NULL;
// Extract the contents of the block from the selection point to the end
// of its contents.
self.setEndAt(toSplit, KER.POSITION_BEFORE_END);
var documentFragment = self.extractContents(),
// Duplicate the element after it.
clone = toSplit.clone(FALSE);
// Place the extracted contents into the duplicated element.
clone[0].appendChild(documentFragment);
clone.insertAfter(toSplit);
self.moveToPosition(toSplit, KER.POSITION_AFTER_END);
return clone;
},
/**
* Move the range to the depth-first start/end editing point inside
* an element.
* @param {KISSY.NodeList} el The element to find edit point into.
* @param {Boolean} [isMoveToEnd] Find start or end editing point.
* Set true to find end editing point.
* @return {Boolean} Whether find edit point
*/
moveToElementEditablePosition: function (el, isMoveToEnd) {
function nextDFS(node, childOnly) {
var next;
if (node[0].nodeType == Dom.NodeType.ELEMENT_NODE &&
node._4e_isEditable()) {
next = node[ isMoveToEnd ? 'last' : 'first' ](nonWhitespaceOrIsBookmark, 1);
}
if (!childOnly && !next) {
next = node[ isMoveToEnd ? 'prev' : 'next' ](nonWhitespaceOrIsBookmark, 1);
}
return next;
}
var found = 0, self = this;
while (el) {
// Stop immediately if we've found a text node.
if (el[0].nodeType == Dom.NodeType.TEXT_NODE) {
self.moveToPosition(el, isMoveToEnd ?
KER.POSITION_AFTER_END :
KER.POSITION_BEFORE_START);
found = 1;
break;
}
// If an editable element is found, move inside it, but not stop the searching.
if (el[0].nodeType == Dom.NodeType.ELEMENT_NODE && el._4e_isEditable()) {
self.moveToPosition(el, isMoveToEnd ?
KER.POSITION_BEFORE_END :
KER.POSITION_AFTER_START);
found = 1;
}
el = nextDFS(el, found);
}
return !!found;
},
/**
* Set range surround current node 's content.
* @param {KISSY.NodeList} node
*/
selectNodeContents: function (node) {
var self = this, domNode = node[0];
self.setStart(node, 0);
self.setEnd(node, domNode.nodeType == Dom.NodeType.TEXT_NODE ?
domNode.nodeValue.length :
domNode.childNodes.length);
},
/**
* Insert node by dtd.(not invalidate dtd convention)
* @param {KISSY.NodeList} element
*/
insertNodeByDtd: function (element) {
var current,
self = this,
tmpDtd,
last,
elementName = element['nodeName'](),
isBlock = dtd['$block'][ elementName ];
self.deleteContents();
if (isBlock) {
current = self.getCommonAncestor(FALSE, TRUE);
while (( tmpDtd = dtd[ current.nodeName() ] ) && !( tmpDtd && tmpDtd [ elementName ] )) {
var parent = current.parent();
// If we're in an empty block which indicate a new paragraph,
// simply replace it with the inserting block.(#3664)
if (self.checkStartOfBlock() && self.checkEndOfBlock()) {
self.setStartBefore(current);
self.collapse(TRUE);
current.remove();
} else {
last = current;
}
current = parent;
}
if (last) {
self.splitElement(last);
}
}
// Insert the new node.
self.insertNode(element);
}
});
Utils.injectDom({
_4e_breakParent: function (el, parent) {
parent = $(parent);
el = $(el);
var KERange = Editor.Range,
docFrag,
range = new KERange(el[0].ownerDocument);
// We'll be extracting part of this element, so let's use our
// range to get the correct piece.
range.setStartAfter(el);
range.setEndAfter(parent);
// Extract it.
docFrag = range.extractContents();
// Move the element outside the broken element.
range.insertNode(el.remove());
// Re-insert the extracted piece after the element.
el.after(docFrag);
}
});
Editor.Range = KERange;
return KERange;
}, {
requires: ['./base', './utils', './walker', './elementPath', './dom', 'node']
});