/**
* CaretFormat.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.CaretFormat',
[
'ephox.katamari.api.Arr',
'ephox.sugar.api.dom.Insert',
'ephox.sugar.api.dom.Remove',
'ephox.sugar.api.node.Element',
'ephox.sugar.api.node.Node',
'ephox.sugar.api.properties.Attr',
'tinymce.core.caret.CaretPosition',
'tinymce.core.dom.NodeType',
'tinymce.core.dom.PaddingBr',
'tinymce.core.dom.TreeWalker',
'tinymce.core.fmt.ExpandRange',
'tinymce.core.fmt.FormatUtils',
'tinymce.core.fmt.MatchFormat',
'tinymce.core.selection.SplitRange',
'tinymce.core.text.Zwsp',
'tinymce.core.util.Fun'
],
function (Arr, Insert, Remove, Element, Node, Attr, CaretPosition, NodeType, PaddingBr, TreeWalker, ExpandRange, FormatUtils, MatchFormat, SplitRange, Zwsp, Fun) {
var ZWSP = Zwsp.ZWSP, CARET_ID = '_mce_caret';
var importNode = function (ownerDocument, node) {
return ownerDocument.importNode(node, true);
};
var isCaretNode = function (node) {
return node.nodeType === 1 && node.id === CARET_ID;
};
var getEmptyCaretContainers = function (node) {
var nodes = [];
while (node) {
if ((node.nodeType === 3 && node.nodeValue !== ZWSP) || node.childNodes.length > 1) {
return [];
}
// Collect nodes
if (node.nodeType === 1) {
nodes.push(node);
}
node = node.firstChild;
}
return nodes;
};
var isCaretContainerEmpty = function (node) {
return getEmptyCaretContainers(node).length > 0;
};
var findFirstTextNode = function (node) {
var walker;
if (node) {
walker = new TreeWalker(node, node);
for (node = walker.current(); node; node = walker.next()) {
if (node.nodeType === 3) {
return node;
}
}
}
return null;
};
var createCaretContainer = function (fill) {
var caretContainer = Element.fromTag('span');
Attr.setAll(caretContainer, {
//style: 'color:red',
id: CARET_ID,
'data-mce-bogus': '1',
'data-mce-type': 'format-caret'
});
if (fill) {
Insert.append(caretContainer, Element.fromText(ZWSP));
}
return caretContainer;
};
var getParentCaretContainer = function (body, node) {
while (node && node !== body) {
if (node.id === CARET_ID) {
return node;
}
node = node.parentNode;
}
return null;
};
var trimZwspFromCaretContainer = function (caretContainerNode) {
var textNode = findFirstTextNode(caretContainerNode);
if (textNode && textNode.nodeValue.charAt(0) === ZWSP) {
textNode.deleteData(0, 1);
}
return textNode;
};
var removeCaretContainerNode = function (dom, selection, node, moveCaret) {
var rng, block, textNode;
rng = selection.getRng(true);
block = dom.getParent(node, dom.isBlock);
if (isCaretContainerEmpty(node)) {
if (moveCaret !== false) {
rng.setStartBefore(node);
rng.setEndBefore(node);
}
dom.remove(node);
} else {
textNode = trimZwspFromCaretContainer(node);
if (rng.startContainer === textNode && rng.startOffset > 0) {
rng.setStart(textNode, rng.startOffset - 1);
}
if (rng.endContainer === textNode && rng.endOffset > 0) {
rng.setEnd(textNode, rng.endOffset - 1);
}
dom.remove(node, true);
}
if (block && dom.isEmpty(block)) {
PaddingBr.fillWithPaddingBr(Element.fromDom(block));
}
selection.setRng(rng);
};
// Removes the caret container for the specified node or all on the current document
var removeCaretContainer = function (body, dom, selection, node, moveCaret) {
if (!node) {
node = getParentCaretContainer(body, selection.getStart());
if (!node) {
while ((node = dom.get(CARET_ID))) {
removeCaretContainerNode(dom, selection, node, false);
}
}
} else {
removeCaretContainerNode(dom, selection, node, moveCaret);
}
};
var insertCaretContainerNode = function (editor, caretContainer, formatNode) {
var dom = editor.dom, block = dom.getParent(formatNode, Fun.curry(FormatUtils.isTextBlock, editor));
if (block && dom.isEmpty(block)) {
// Replace formatNode with caretContainer when removing format from empty block like <p><b>|</b></p>
formatNode.parentNode.replaceChild(caretContainer, formatNode);
} else {
PaddingBr.removeTrailingBr(Element.fromDom(formatNode));
if (dom.isEmpty(formatNode)) {
formatNode.parentNode.replaceChild(caretContainer, formatNode);
} else {
dom.insertAfter(caretContainer, formatNode);
}
}
};
var appendNode = function (parentNode, node) {
parentNode.appendChild(node);
return node;
};
var insertFormatNodesIntoCaretContainer = function (formatNodes, caretContainer) {
var innerMostFormatNode = Arr.foldr(formatNodes, function (parentNode, formatNode) {
return appendNode(parentNode, formatNode.cloneNode(false));
}, caretContainer);
return appendNode(innerMostFormatNode, innerMostFormatNode.ownerDocument.createTextNode(ZWSP));
};
var applyCaretFormat = function (editor, name, vars) {
var rng, caretContainer, textNode, offset, bookmark, container, text;
var selection = editor.selection;
rng = selection.getRng(true);
offset = rng.startOffset;
container = rng.startContainer;
text = container.nodeValue;
caretContainer = getParentCaretContainer(editor.getBody(), selection.getStart());
if (caretContainer) {
textNode = findFirstTextNode(caretContainer);
}
// Expand to word if caret is in the middle of a text node and the char before/after is a alpha numeric character
var wordcharRegex = /[^\s\u00a0\u00ad\u200b\ufeff]/;
if (text && offset > 0 && offset < text.length &&
wordcharRegex.test(text.charAt(offset)) && wordcharRegex.test(text.charAt(offset - 1))) {
// Get bookmark of caret position
bookmark = selection.getBookmark();
// Collapse bookmark range (WebKit)
rng.collapse(true);
// Expand the range to the closest word and split it at those points
rng = ExpandRange.expandRng(editor, rng, editor.formatter.get(name));
rng = SplitRange.split(rng);
// Apply the format to the range
editor.formatter.apply(name, vars, rng);
// Move selection back to caret position
selection.moveToBookmark(bookmark);
} else {
if (!caretContainer || textNode.nodeValue !== ZWSP) {
// Need to import the node into the document on IE or we get a lovely WrongDocument exception
caretContainer = importNode(editor.getDoc(), createCaretContainer(true).dom());
textNode = caretContainer.firstChild;
rng.insertNode(caretContainer);
offset = 1;
editor.formatter.apply(name, vars, caretContainer);
} else {
editor.formatter.apply(name, vars, caretContainer);
}
// Move selection to text node
selection.setCursorLocation(textNode, offset);
}
};
var removeCaretFormat = function (editor, name, vars, similar) {
var dom = editor.dom, selection = editor.selection;
var rng = selection.getRng(true), container, offset, bookmark;
var hasContentAfter, node, formatNode, parents = [], caretContainer;
container = rng.startContainer;
offset = rng.startOffset;
node = container;
if (container.nodeType === 3) {
if (offset !== container.nodeValue.length) {
hasContentAfter = true;
}
node = node.parentNode;
}
while (node) {
if (MatchFormat.matchNode(editor, node, name, vars, similar)) {
formatNode = node;
break;
}
if (node.nextSibling) {
hasContentAfter = true;
}
parents.push(node);
node = node.parentNode;
}
// Node doesn't have the specified format
if (!formatNode) {
return;
}
// Is there contents after the caret then remove the format on the element
if (hasContentAfter) {
bookmark = selection.getBookmark();
// Collapse bookmark range (WebKit)
rng.collapse(true);
// Expand the range to the closest word and split it at those points
rng = ExpandRange.expandRng(editor, rng, editor.formatter.get(name), true);
rng = SplitRange.split(rng);
editor.formatter.remove(name, vars, rng);
selection.moveToBookmark(bookmark);
} else {
caretContainer = getParentCaretContainer(editor.getBody(), formatNode);
var newCaretContainer = createCaretContainer(false).dom();
var caretNode = insertFormatNodesIntoCaretContainer(parents, newCaretContainer);
if (caretContainer) {
insertCaretContainerNode(editor, newCaretContainer, caretContainer);
} else {
insertCaretContainerNode(editor, newCaretContainer, formatNode);
}
removeCaretContainerNode(dom, selection, caretContainer, false);
selection.setCursorLocation(caretNode, 1);
if (dom.isEmpty(formatNode)) {
dom.remove(formatNode);
}
}
};
var disableCaretContainer = function (body, dom, selection, keyCode) {
removeCaretContainer(body, dom, selection, null, false);
// Remove caret container if it's empty
if (keyCode === 8 && selection.isCollapsed() && selection.getStart().innerHTML === ZWSP) {
removeCaretContainer(body, dom, selection, getParentCaretContainer(body, selection.getStart()));
}
// Remove caret container on keydown and it's left/right arrow keys
if (keyCode === 37 || keyCode === 39) {
removeCaretContainer(body, dom, selection, getParentCaretContainer(body, selection.getStart()));
}
};
var setup = function (editor) {
var dom = editor.dom, selection = editor.selection;
var body = editor.getBody();
editor.on('mouseup keydown', function (e) {
disableCaretContainer(body, dom, selection, e.keyCode);
});
};
var replaceWithCaretFormat = function (targetNode, formatNodes) {
var caretContainer = createCaretContainer(false);
var innerMost = insertFormatNodesIntoCaretContainer(formatNodes, caretContainer.dom());
Insert.before(Element.fromDom(targetNode), caretContainer);
Remove.remove(Element.fromDom(targetNode));
return CaretPosition(innerMost, 0);
};
var isFormatElement = function (editor, element) {
var inlineElements = editor.schema.getTextInlineElements();
return inlineElements.hasOwnProperty(Node.name(element)) && !isCaretNode(element.dom()) && !NodeType.isBogus(element.dom());
};
return {
setup: setup,
applyCaretFormat: applyCaretFormat,
removeCaretFormat: removeCaretFormat,
isCaretNode: isCaretNode,
getParentCaretContainer: getParentCaretContainer,
replaceWithCaretFormat: replaceWithCaretFormat,
isFormatElement: isFormatElement
};
}
);
|