/**
* ExpandRange.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
define(
'tinymce.core.fmt.ExpandRange',
[
'tinymce.core.dom.Bookmarks',
'tinymce.core.dom.TreeWalker',
'tinymce.core.fmt.FormatUtils',
'tinymce.core.selection.RangeNodes'
],
function (Bookmarks, TreeWalker, FormatUtils, RangeNodes) {
var isBookmarkNode = Bookmarks.isBookmarkNode;
var getParents = FormatUtils.getParents, isWhiteSpaceNode = FormatUtils.isWhiteSpaceNode, isTextBlock = FormatUtils.isTextBlock;
// This function walks down the tree to find the leaf at the selection.
// The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node.
var findLeaf = function (node, offset) {
if (typeof offset === 'undefined') {
offset = node.nodeType === 3 ? node.length : node.childNodes.length;
}
while (node && node.hasChildNodes()) {
node = node.childNodes[offset];
if (node) {
offset = node.nodeType === 3 ? node.length : node.childNodes.length;
}
}
return { node: node, offset: offset };
};
var excludeTrailingWhitespace = function (endContainer, endOffset) {
// Avoid applying formatting to a trailing space,
// but remove formatting from trailing space
var leaf = findLeaf(endContainer, endOffset);
if (leaf.node) {
while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) {
leaf = findLeaf(leaf.node.previousSibling);
}
if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 &&
leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') {
if (leaf.offset > 1) {
endContainer = leaf.node;
endContainer.splitText(leaf.offset - 1);
}
}
}
return endContainer;
};
var isBogusBr = function (node) {
return node.nodeName === "BR" && node.getAttribute('data-mce-bogus') && !node.nextSibling;
};
// Expands the node to the closes contentEditable false element if it exists
var findParentContentEditable = function (dom, node) {
var parent = node;
while (parent) {
if (parent.nodeType === 1 && dom.getContentEditable(parent)) {
return dom.getContentEditable(parent) === "false" ? parent : node;
}
parent = parent.parentNode;
}
return node;
};
var findSpace = function (start, remove, node, offset) {
var pos, pos2, str = node.nodeValue;
if (typeof offset === "undefined") {
offset = start ? str.length : 0;
}
if (start) {
pos = str.lastIndexOf(' ', offset);
pos2 = str.lastIndexOf('\u00a0', offset);
pos = pos > pos2 ? pos : pos2;
// Include the space on remove to avoid tag soup
if (pos !== -1 && !remove) {
pos++;
}
} else {
pos = str.indexOf(' ', offset);
pos2 = str.indexOf('\u00a0', offset);
pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2;
}
return pos;
};
var findWordEndPoint = function (dom, body, container, offset, start, remove) {
var walker, node, pos, lastTextNode;
if (container.nodeType === 3) {
pos = findSpace(start, remove, container, offset);
if (pos !== -1) {
return { container: container, offset: pos };
}
lastTextNode = container;
}
// Walk the nodes inside the block
walker = new TreeWalker(container, dom.getParent(container, dom.isBlock) || body);
while ((node = walker[start ? 'prev' : 'next']())) {
if (node.nodeType === 3) {
lastTextNode = node;
pos = findSpace(start, remove, node);
if (pos !== -1) {
return { container: node, offset: pos };
}
} else if (dom.isBlock(node)) {
break;
}
}
if (lastTextNode) {
if (start) {
offset = 0;
} else {
offset = lastTextNode.length;
}
return { container: lastTextNode, offset: offset };
}
};
var findSelectorEndPoint = function (dom, format, rng, container, siblingName) {
var parents, i, y, curFormat;
if (container.nodeType === 3 && container.nodeValue.length === 0 && container[siblingName]) {
container = container[siblingName];
}
parents = getParents(dom, container);
for (i = 0; i < parents.length; i++) {
for (y = 0; y < format.length; y++) {
curFormat = format[y];
// If collapsed state is set then skip formats that doesn't match that
if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) {
continue;
}
if (dom.is(parents[i], curFormat.selector)) {
return parents[i];
}
}
}
return container;
};
var findBlockEndPoint = function (editor, format, container, siblingName) {
var node, dom = editor.dom, root = dom.getRoot();
// Expand to block of similar type
if (!format[0].wrapper) {
node = dom.getParent(container, format[0].block, root);
}
// Expand to first wrappable block element or any block element
if (!node) {
var scopeRoot = dom.getParent(container, 'LI,TD,TH');
node = dom.getParent(container.nodeType === 3 ? container.parentNode : container, function (node) {
// Fixes #6183 where it would expand to editable parent element in inline mode
return node !== root && isTextBlock(editor, node);
}, scopeRoot);
}
// Exclude inner lists from wrapping
if (node && format[0].wrapper) {
node = getParents(dom, node, 'ul,ol').reverse()[0] || node;
}
// Didn't find a block element look for first/last wrappable element
if (!node) {
node = container;
while (node[siblingName] && !dom.isBlock(node[siblingName])) {
node = node[siblingName];
// Break on BR but include it will be removed later on
// we can't remove it now since we need to check if it can be wrapped
if (FormatUtils.isEq(node, 'br')) {
break;
}
}
}
return node || container;
};
// This function walks up the tree if there is no siblings before/after the node
var findParentContainer = function (dom, format, startContainer, startOffset, endContainer, endOffset, start) {
var container, parent, sibling, siblingName, root;
container = parent = start ? startContainer : endContainer;
siblingName = start ? 'previousSibling' : 'nextSibling';
root = dom.getRoot();
// If it's a text node and the offset is inside the text
if (container.nodeType === 3 && !isWhiteSpaceNode(container)) {
if (start ? startOffset > 0 : endOffset < container.nodeValue.length) {
return container;
}
}
/*eslint no-constant-condition:0 */
while (true) {
// Stop expanding on block elements
if (!format[0].block_expand && dom.isBlock(parent)) {
return parent;
}
// Walk left/right
for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) {
if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling) && !isBogusBr(sibling)) {
return parent;
}
}
// Check if we can move up are we at root level or body level
if (parent === root || parent.parentNode === root) {
container = parent;
break;
}
parent = parent.parentNode;
}
return container;
};
var expandRng = function (editor, rng, format, remove) {
var endPoint,
startContainer = rng.startContainer,
startOffset = rng.startOffset,
endContainer = rng.endContainer,
endOffset = rng.endOffset,
dom = editor.dom;
// If index based start position then resolve it
if (startContainer.nodeType === 1 && startContainer.hasChildNodes()) {
startContainer = RangeNodes.getNode(startContainer, startOffset);
if (startContainer.nodeType === 3) {
startOffset = 0;
}
}
// If index based end position then resolve it
if (endContainer.nodeType === 1 && endContainer.hasChildNodes()) {
endContainer = RangeNodes.getNode(endContainer, rng.collapsed ? endOffset : endOffset - 1);
if (endContainer.nodeType === 3) {
endOffset = endContainer.nodeValue.length;
}
}
// Expand to closest contentEditable element
startContainer = findParentContentEditable(dom, startContainer);
endContainer = findParentContentEditable(dom, endContainer);
// Exclude bookmark nodes if possible
if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) {
startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode;
startContainer = startContainer.nextSibling || startContainer;
if (startContainer.nodeType === 3) {
startOffset = 0;
}
}
if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) {
endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode;
endContainer = endContainer.previousSibling || endContainer;
if (endContainer.nodeType === 3) {
endOffset = endContainer.length;
}
}
if (format[0].inline) {
if (rng.collapsed) {
// Expand left to closest word boundary
endPoint = findWordEndPoint(dom, editor.getBody(), startContainer, startOffset, true, remove);
if (endPoint) {
startContainer = endPoint.container;
startOffset = endPoint.offset;
}
// Expand right to closest word boundary
endPoint = findWordEndPoint(dom, editor.getBody(), endContainer, endOffset, false, remove);
if (endPoint) {
endContainer = endPoint.container;
endOffset = endPoint.offset;
}
}
endContainer = remove ? endContainer : excludeTrailingWhitespace(endContainer, endOffset);
}
// Move start/end point up the tree if the leaves are sharp and if we are in different containers
// Example * becomes !: !<p><b><i>*text</i><i>text*</i></b></p>!
// This will reduce the number of wrapper elements that needs to be created
// Move start point up the tree
if (format[0].inline || format[0].block_expand) {
if (!format[0].inline || (startContainer.nodeType !== 3 || startOffset === 0)) {
startContainer = findParentContainer(dom, format, startContainer, startOffset, endContainer, endOffset, true);
}
if (!format[0].inline || (endContainer.nodeType !== 3 || endOffset === endContainer.nodeValue.length)) {
endContainer = findParentContainer(dom, format, startContainer, startOffset, endContainer, endOffset, false);
}
}
// Expand start/end container to matching selector
if (format[0].selector && format[0].expand !== false && !format[0].inline) {
// Find new startContainer/endContainer if there is better one
startContainer = findSelectorEndPoint(dom, format, rng, startContainer, 'previousSibling');
endContainer = findSelectorEndPoint(dom, format, rng, endContainer, 'nextSibling');
}
// Expand start/end container to matching block element or text node
if (format[0].block || format[0].selector) {
// Find new startContainer/endContainer if there is better one
startContainer = findBlockEndPoint(editor, format, startContainer, 'previousSibling');
endContainer = findBlockEndPoint(editor, format, endContainer, 'nextSibling');
// Non block element then try to expand up the leaf
if (format[0].block) {
if (!dom.isBlock(startContainer)) {
startContainer = findParentContainer(dom, format, startContainer, startOffset, endContainer, endOffset, true);
}
if (!dom.isBlock(endContainer)) {
endContainer = findParentContainer(dom, format, startContainer, startOffset, endContainer, endOffset, false);
}
}
}
// Setup index for startContainer
if (startContainer.nodeType === 1) {
startOffset = dom.nodeIndex(startContainer);
startContainer = startContainer.parentNode;
}
// Setup index for endContainer
if (endContainer.nodeType === 1) {
endOffset = dom.nodeIndex(endContainer) + 1;
endContainer = endContainer.parentNode;
}
// Return new range like object
return {
startContainer: startContainer,
startOffset: startOffset,
endContainer: endContainer,
endOffset: endOffset
};
};
return {
expandRng: expandRng
};
}
);
|