Files
leanote/public/tinymce/classes/EditorCommands.js
lealife 6987a38820 v1.0
只读模式
group, 分享
评论更多问题
博客标签总是存在一个
2015-06-15 18:01:48 +08:00

1163 lines
33 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* EditorCommands.js
*
* Copyright, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
/**
* This class enables you to add custom editor commands and it contains
* overrides for native browser commands to address various bugs and issues.
*
* @class tinymce.EditorCommands
*/
define("tinymce/EditorCommands", [
"tinymce/html/Serializer",
"tinymce/Env",
"tinymce/util/Tools",
"tinymce/dom/ElementUtils",
"tinymce/dom/RangeUtils",
"tinymce/dom/TreeWalker"
], function(Serializer, Env, Tools, ElementUtils, RangeUtils, TreeWalker) {
// Added for compression purposes
var each = Tools.each, extend = Tools.extend;
var map = Tools.map, inArray = Tools.inArray, explode = Tools.explode;
var isGecko = Env.gecko, isIE = Env.ie, isOldIE = Env.ie && Env.ie < 11;
var TRUE = true, FALSE = false;
return function(editor) {
var dom, selection, formatter,
commands = {state: {}, exec: {}, value: {}},
settings = editor.settings,
bookmark;
editor.on('PreInit', function() {
dom = editor.dom;
selection = editor.selection;
settings = editor.settings;
formatter = editor.formatter;
});
/**
* Executes the specified command.
*
* @method execCommand
* @param {String} command Command to execute.
* @param {Boolean} ui Optional user interface state.
* @param {Object} value Optional value for command.
* @return {Boolean} true/false if the command was found or not.
*/
function execCommand(command, ui, value, args) {
var func, customCommand, state = 0;
if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint)$/.test(command) && (!args || !args.skip_focus)) {
// life ace
if(command != "toggleCode")
editor.focus();
}
args = extend({}, args);
args = editor.fire('BeforeExecCommand', {command: command, ui: ui, value: value});
if (args.isDefaultPrevented()) {
return false;
}
customCommand = command.toLowerCase();
if ((func = commands.exec[customCommand])) {
func(customCommand, ui, value);
editor.fire('ExecCommand', {command: command, ui: ui, value: value});
return true;
}
// Plugin commands
each(editor.plugins, function(p) {
if (p.execCommand && p.execCommand(command, ui, value)) {
editor.fire('ExecCommand', {command: command, ui: ui, value: value});
state = true;
return false;
}
});
if (state) {
return state;
}
// Theme commands
if (editor.theme && editor.theme.execCommand && editor.theme.execCommand(command, ui, value)) {
editor.fire('ExecCommand', {command: command, ui: ui, value: value});
return true;
}
// Browser commands
try {
state = editor.getDoc().execCommand(command, ui, value);
} catch (ex) {
// Ignore old IE errors
}
if (state) {
editor.fire('ExecCommand', {command: command, ui: ui, value: value});
return true;
}
return false;
}
/**
* Queries the current state for a command for example if the current selection is "bold".
*
* @method queryCommandState
* @param {String} command Command to check the state of.
* @return {Boolean/Number} true/false if the selected contents is bold or not, -1 if it's not found.
*/
function queryCommandState(command) {
var func;
// Is hidden then return undefined
if (editor._isHidden()) {
return;
}
command = command.toLowerCase();
if ((func = commands.state[command])) {
return func(command);
}
// Browser commands
try {
return editor.getDoc().queryCommandState(command);
} catch (ex) {
// Fails sometimes see bug: 1896577
}
return false;
}
/**
* Queries the command value for example the current fontsize.
*
* @method queryCommandValue
* @param {String} command Command to check the value of.
* @return {Object} Command value of false if it's not found.
*/
function queryCommandValue(command) {
var func;
// Is hidden then return undefined
if (editor._isHidden()) {
return;
}
command = command.toLowerCase();
if ((func = commands.value[command])) {
return func(command);
}
// Browser commands
try {
return editor.getDoc().queryCommandValue(command);
} catch (ex) {
// Fails sometimes see bug: 1896577
}
}
/**
* Adds commands to the command collection.
*
* @method addCommands
* @param {Object} command_list Name/value collection with commands to add, the names can also be comma separated.
* @param {String} type Optional type to add, defaults to exec. Can be value or state as well.
*/
function addCommands(command_list, type) {
type = type || 'exec';
each(command_list, function(callback, command) {
each(command.toLowerCase().split(','), function(command) {
commands[type][command] = callback;
});
});
}
function addCommand(command, callback, scope) {
command = command.toLowerCase();
commands.exec[command] = function(command, ui, value, args) {
return callback.call(scope || editor, ui, value, args);
};
}
/**
* Returns true/false if the command is supported or not.
*
* @method queryCommandSupported
* @param {String} cmd Command that we check support for.
* @return {Boolean} true/false if the command is supported or not.
*/
function queryCommandSupported(command) {
command = command.toLowerCase();
if (commands.exec[command]) {
return true;
}
// Browser commands
try {
return editor.getDoc().queryCommandSupported(command);
} catch (ex) {
// Fails sometimes see bug: 1896577
}
return false;
}
function addQueryStateHandler(command, callback, scope) {
command = command.toLowerCase();
commands.state[command] = function() {
return callback.call(scope || editor);
};
}
function addQueryValueHandler(command, callback, scope) {
command = command.toLowerCase();
commands.value[command] = function() {
return callback.call(scope || editor);
};
}
function hasCustomCommand(command) {
command = command.toLowerCase();
return !!commands.exec[command];
}
// Expose public methods
extend(this, {
execCommand: execCommand,
queryCommandState: queryCommandState,
queryCommandValue: queryCommandValue,
queryCommandSupported: queryCommandSupported,
addCommands: addCommands,
addCommand: addCommand,
addQueryStateHandler: addQueryStateHandler,
addQueryValueHandler: addQueryValueHandler,
hasCustomCommand: hasCustomCommand
});
// Private methods
function execNativeCommand(command, ui, value) {
if (ui === undefined) {
ui = FALSE;
}
if (value === undefined) {
value = null;
}
return editor.getDoc().execCommand(command, ui, value);
}
function isFormatMatch(name) {
return formatter.match(name);
}
function toggleFormat(name, value) {
formatter.toggle(name, value ? {value: value} : undefined);
editor.nodeChanged();
}
function storeSelection(type) {
bookmark = selection.getBookmark(type);
}
function restoreSelection() {
selection.moveToBookmark(bookmark);
}
// Add execCommand overrides
addCommands({
// Ignore these, added for compatibility
'mceResetDesignMode,mceBeginUndoLevel': function() {},
// Add undo manager logic
'mceEndUndoLevel,mceAddUndoLevel': function() {
editor.undoManager.add();
},
'Cut,Copy,Paste': function(command) {
var doc = editor.getDoc(), failed;
// Try executing the native command
try {
execNativeCommand(command);
} catch (ex) {
// Command failed
failed = TRUE;
}
// Present alert message about clipboard access not being available
if (failed || !doc.queryCommandSupported(command)) {
var msg = editor.translate(
"Your browser doesn't support direct access to the clipboard. " +
"Please use the Ctrl+X/C/V keyboard shortcuts instead."
);
if (Env.mac) {
msg = msg.replace(/Ctrl\+/g, '\u2318+');
}
editor.windowManager.alert(msg);
}
},
// Override unlink command
unlink: function() {
if (selection.isCollapsed()) {
var elm = selection.getNode();
if (elm.tagName == 'A') {
editor.dom.remove(elm, true);
}
return;
}
formatter.remove("link");
},
// Override justify commands to use the text formatter engine
'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull': function(command) {
var align = command.substring(7);
if (align == 'full') {
align = 'justify';
}
// Remove all other alignments first
each('left,center,right,justify'.split(','), function(name) {
if (align != name) {
formatter.remove('align' + name);
}
});
toggleFormat('align' + align);
execCommand('mceRepaint');
},
// Override list commands to fix WebKit bug
'InsertUnorderedList,InsertOrderedList': function(command) {
var listElm, listParent;
execNativeCommand(command);
// WebKit produces lists within block elements so we need to split them
// we will replace the native list creation logic to custom logic later on
// TODO: Remove this when the list creation logic is removed
listElm = dom.getParent(selection.getNode(), 'ol,ul');
if (listElm) {
listParent = listElm.parentNode;
// If list is within a text block then split that block
if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) {
storeSelection();
dom.split(listParent, listElm);
restoreSelection();
}
}
},
// Override commands to use the text formatter engine
'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function(command) {
toggleFormat(command);
},
// Override commands to use the text formatter engine
'ForeColor,HiliteColor,FontName': function(command, ui, value) {
toggleFormat(command, value);
},
FontSize: function(command, ui, value) {
var fontClasses, fontSizes;
// Convert font size 1-7 to styles
if (value >= 1 && value <= 7) {
fontSizes = explode(settings.font_size_style_values);
fontClasses = explode(settings.font_size_classes);
if (fontClasses) {
value = fontClasses[value - 1] || value;
} else {
value = fontSizes[value - 1] || value;
}
}
toggleFormat(command, value);
},
RemoveFormat: function(command) {
formatter.remove(command);
},
mceBlockQuote: function() {
toggleFormat('blockquote');
},
FormatBlock: function(command, ui, value) {
return toggleFormat(value || 'p');
},
mceCleanup: function() {
var bookmark = selection.getBookmark();
editor.setContent(editor.getContent({cleanup: TRUE}), {cleanup: TRUE});
selection.moveToBookmark(bookmark);
},
mceRemoveNode: function(command, ui, value) {
var node = value || selection.getNode();
// Make sure that the body node isn't removed
if (node != editor.getBody()) {
storeSelection();
editor.dom.remove(node, TRUE);
restoreSelection();
}
},
mceSelectNodeDepth: function(command, ui, value) {
var counter = 0;
dom.getParent(selection.getNode(), function(node) {
if (node.nodeType == 1 && counter++ == value) {
selection.select(node);
return FALSE;
}
}, editor.getBody());
},
mceSelectNode: function(command, ui, value) {
selection.select(value);
},
mceInsertContent: function(command, ui, value) {
var parser, serializer, parentNode, rootNode, fragment, args;
var marker, rng, node, node2, bookmarkHtml, merge;
var textInlineElements = editor.schema.getTextInlineElements();
function trimOrPaddLeftRight(html) {
var rng, container, offset;
rng = selection.getRng(true);
container = rng.startContainer;
offset = rng.startOffset;
function hasSiblingText(siblingName) {
return container[siblingName] && container[siblingName].nodeType == 3;
}
if (container.nodeType == 3) {
if (offset > 0) {
html = html.replace(/^&nbsp;/, ' ');
} else if (!hasSiblingText('previousSibling')) {
html = html.replace(/^ /, '&nbsp;');
}
if (offset < container.length) {
html = html.replace(/&nbsp;(<br>|)$/, ' ');
} else if (!hasSiblingText('nextSibling')) {
html = html.replace(/(&nbsp;| )(<br>|)$/, '&nbsp;');
}
}
return html;
}
// Removes &nbsp; from a [b] c -> a &nbsp;c -> a c
function trimNbspAfterDeleteAndPaddValue() {
var rng, container, offset;
rng = selection.getRng(true);
container = rng.startContainer;
offset = rng.startOffset;
if (container.nodeType == 3 && rng.collapsed) {
if (container.data[offset] === '\u00a0') {
container.deleteData(offset, 1);
if (!/[\u00a0| ]$/.test(value)) {
value += ' ';
}
} else if (container.data[offset - 1] === '\u00a0') {
container.deleteData(offset - 1, 1);
if (!/[\u00a0| ]$/.test(value)) {
value = ' ' + value;
}
}
}
}
function markInlineFormatElements(fragment) {
if (merge) {
for (node = fragment.firstChild; node; node = node.walk(true)) {
if (textInlineElements[node.name]) {
node.attr('data-mce-new', "true");
}
}
}
}
function reduceInlineTextElements() {
if (merge) {
var root = editor.getBody(), elementUtils = new ElementUtils(dom);
each(dom.select('*[data-mce-new]'), function(node) {
node.removeAttribute('data-mce-new');
for (var testNode = node.parentNode; testNode && testNode != root; testNode = testNode.parentNode) {
if (elementUtils.compare(testNode, node)) {
dom.remove(node, true);
}
}
});
}
}
if (typeof value != 'string') {
merge = value.merge;
value = value.content;
}
// Check for whitespace before/after value
if (/^ | $/.test(value)) {
value = trimOrPaddLeftRight(value);
}
// Setup parser and serializer
parser = editor.parser;
serializer = new Serializer({}, editor.schema);
bookmarkHtml = '<span id="mce_marker" data-mce-type="bookmark">&#xFEFF;&#x200B;</span>';
// Run beforeSetContent handlers on the HTML to be inserted
args = {content: value, format: 'html', selection: true};
editor.fire('BeforeSetContent', args);
value = args.content;
// Add caret at end of contents if it's missing
if (value.indexOf('{$caret}') == -1) {
value += '{$caret}';
}
// Replace the caret marker with a span bookmark element
value = value.replace(/\{\$caret\}/, bookmarkHtml);
// If selection is at <body>|<p></p> then move it into <body><p>|</p>
rng = selection.getRng();
var caretElement = rng.startContainer || (rng.parentElement ? rng.parentElement() : null);
var body = editor.getBody();
if (caretElement === body && selection.isCollapsed()) {
if (dom.isBlock(body.firstChild) && dom.isEmpty(body.firstChild)) {
rng = dom.createRng();
rng.setStart(body.firstChild, 0);
rng.setEnd(body.firstChild, 0);
selection.setRng(rng);
}
}
// Insert node maker where we will insert the new HTML and get it's parent
if (!selection.isCollapsed()) {
editor.getDoc().execCommand('Delete', false, null);
trimNbspAfterDeleteAndPaddValue();
}
parentNode = selection.getNode();
// Parse the fragment within the context of the parent node
var parserArgs = {context: parentNode.nodeName.toLowerCase()};
fragment = parser.parse(value, parserArgs);
markInlineFormatElements(fragment);
// Move the caret to a more suitable location
node = fragment.lastChild;
if (node.attr('id') == 'mce_marker') {
marker = node;
for (node = node.prev; node; node = node.walk(true)) {
if (node.type == 3 || !dom.isBlock(node.name)) {
if (editor.schema.isValidChild(node.parent.name, 'span')) {
node.parent.insert(marker, node, node.name === 'br');
}
break;
}
}
}
// If parser says valid we can insert the contents into that parent
if (!parserArgs.invalid) {
value = serializer.serialize(fragment);
// Check if parent is empty or only has one BR element then set the innerHTML of that parent
node = parentNode.firstChild;
node2 = parentNode.lastChild;
if (!node || (node === node2 && node.nodeName === 'BR')) {
dom.setHTML(parentNode, value);
} else {
selection.setContent(value);
}
} else {
// If the fragment was invalid within that context then we need
// to parse and process the parent it's inserted into
// Insert bookmark node and get the parent
selection.setContent(bookmarkHtml);
parentNode = selection.getNode();
rootNode = editor.getBody();
// Opera will return the document node when selection is in root
if (parentNode.nodeType == 9) {
parentNode = node = rootNode;
} else {
node = parentNode;
}
// Find the ancestor just before the root element
while (node !== rootNode) {
parentNode = node;
node = node.parentNode;
}
// Get the outer/inner HTML depending on if we are in the root and parser and serialize that
value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode);
value = serializer.serialize(
parser.parse(
// Need to replace by using a function since $ in the contents would otherwise be a problem
value.replace(/<span (id="mce_marker"|id=mce_marker).+?<\/span>/i, function() {
return serializer.serialize(fragment);
})
)
);
// life ace
// 插入的时候把<pre>内的标签全清掉
// life 把<pre></pre>间的代码拿出, 去掉标签<span>之类的
// console.log(value);
// <p> </p><pre>xxx<span id="mce_marker" data-mce-type="bookmark"></span></pre>
// <p> </p><pre >xxx</pre>
// console.log('life');
// console.log(value);
value = value.replace(/<pre([^>]*?)>([\s\S]*?)<\/pre>/g, function(v, v1, v2) {
// v == "<pre>a, b, c</pre>"
var hasBookmark = false;
var b = '<span id="mce_marker" data-mce-type="bookmark"></span>';
if(v2.indexOf('id="mce_marker') != -1) { // mini后不是b了
hasBookmark = true;
}
v2 = v2.replace(/(<([^>]+)>)/gi, '').replace(/\s+$/, ''); // 把最后一个换行去掉
if(hasBookmark) {
v2 += b;
}
return "<pre " + v1 + ">" + v2 + "</pre>";
});
// console.log(value);
// Set the inner/outer HTML depending on if we are in the root or not
if (parentNode == rootNode) {
dom.setHTML(rootNode, value);
} else {
dom.setOuterHTML(parentNode, value);
}
}
reduceInlineTextElements();
marker = dom.get('mce_marker');
selection.scrollIntoView(marker);
// Move selection before marker and remove it
rng = dom.createRng();
// If previous sibling is a text node set the selection to the end of that node
node = marker.previousSibling;
if (node && node.nodeType == 3) {
rng.setStart(node, node.nodeValue.length);
// TODO: Why can't we normalize on IE
if (!isIE) {
node2 = marker.nextSibling;
if (node2 && node2.nodeType == 3) {
node.appendData(node2.data);
node2.parentNode.removeChild(node2);
}
}
} else {
// If the previous sibling isn't a text node or doesn't exist set the selection before the marker node
rng.setStartBefore(marker);
rng.setEndBefore(marker);
}
// Remove the marker node and set the new range
dom.remove(marker);
selection.setRng(rng);
// Dispatch after event and add any visual elements needed
editor.fire('SetContent', args);
editor.addVisual();
},
// life ace
// life 修改
// 之前不是这个版本, 为什么要改, 因为考虑到在pre下粘贴后全部内容会修改(ace不友好)
// 这里不是改全部的内容, 对ace好
mceInsertRawHTML: function(command, ui, value) {
var parser, serializer, parentNode, rootNode, fragment, args;
var marker, rng, node, node2, bookmarkHtml;
// Setup parser and serializer
parser = editor.parser;
serializer = new Serializer({}, editor.schema);
bookmarkHtml = '<span id="mce_marker" data-mce-type="bookmark">&#xFEFF;</span>';
// Run beforeSetContent handlers on the HTML to be inserted
args = {content: value, format: 'html', selection: true};
editor.fire('BeforeSetContent', args);
value = args.content;
// Add caret at end of contents if it's missing
if (value.indexOf('{$caret}') == -1) {
value += '{$caret}';
}
// Replace the caret marker with a span bookmark element
value = value.replace(/\{\$caret\}/, bookmarkHtml);
// If selection is at <body>|<p></p> then move it into <body><p>|</p>
var body = editor.getBody();
if (dom.isBlock(body.firstChild) && dom.isEmpty(body.firstChild)) {
body.firstChild.appendChild(dom.doc.createTextNode('\u00a0'));
selection.select(body.firstChild, true);
dom.remove(body.firstChild.lastChild);
}
// Insert node maker where we will insert the new HTML and get it's parent
if (!selection.isCollapsed()) {
editor.getDoc().execCommand('Delete', false, null);
}
parentNode = selection.getNode();
// Parse the fragment within the context of the parent node
var parserArgs = {context: parentNode.nodeName.toLowerCase()};
fragment = parser.parse(value, parserArgs);
// Move the caret to a more suitable location
node = fragment.lastChild;
if (node.attr('id') == 'mce_marker') {
marker = node;
for (node = node.prev; node; node = node.walk(true)) {
if (node.type == 3 || !dom.isBlock(node.name)) {
node.parent.insert(marker, node, node.name === 'br');
break;
}
}
}
// If parser says valid we can insert the contents into that parent
if (!parserArgs.invalid) {
// Check if parent is empty or only has one BR element then set the innerHTML of that parent
node = parentNode.firstChild;
node2 = parentNode.lastChild;
if (!node || (node === node2 && node.nodeName === 'BR')) {
dom.setHTML(parentNode, value);
} else {
selection.setContent(value);
}
} else {
// If the fragment was invalid within that context then we need
// to parse and process the parent it's inserted into
// Insert bookmark node and get the parent
selection.setContent(bookmarkHtml);
parentNode = selection.getNode();
rootNode = editor.getBody();
// Opera will return the document node when selection is in root
if (parentNode.nodeType == 9) {
parentNode = node = rootNode;
} else {
node = parentNode;
}
// Find the ancestor just before the root element
while (node !== rootNode) {
parentNode = node;
node = node.parentNode;
}
// Get the outer/inner HTML depending on if we are in the root and parser and serialize that
value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode);
// Set the inner/outer HTML depending on if we are in the root or not
if (parentNode == rootNode) {
dom.setHTML(rootNode, value);
} else {
dom.setOuterHTML(parentNode, value);
}
}
marker = dom.get('mce_marker');
selection.scrollIntoView(marker);
// Move selection before marker and remove it
rng = dom.createRng();
// If previous sibling is a text node set the selection to the end of that node
node = marker.previousSibling;
if (node && node.nodeType == 3) {
rng.setStart(node, node.nodeValue.length);
// TODO: Why can't we normalize on IE
if (!isIE) {
node2 = marker.nextSibling;
if (node2 && node2.nodeType == 3) {
node.appendData(node2.data);
node2.parentNode.removeChild(node2);
}
}
} else {
// If the previous sibling isn't a text node or doesn't exist set the selection before the marker node
rng.setStartBefore(marker);
rng.setEndBefore(marker);
}
// Remove the marker node and set the new range
dom.remove(marker);
selection.setRng(rng);
// Dispatch after event and add any visual elements needed
editor.fire('SetContent', args);
editor.addVisual()
},
/* life
mceInsertRawHTML: function(command, ui, value) {
selection.setContent('tiny_mce_marker');
editor.setContent(
editor.getContent().replace(/tiny_mce_marker/g, function() {
return value;
})
);
},
*/
mceToggleFormat: function(command, ui, value) {
toggleFormat(value);
},
mceSetContent: function(command, ui, value) {
editor.setContent(value);
},
'Indent,Outdent': function(command) {
var intentValue, indentUnit, value;
// Setup indent level
intentValue = settings.indentation;
indentUnit = /[a-z%]+$/i.exec(intentValue);
intentValue = parseInt(intentValue, 10);
if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) {
// If forced_root_blocks is set to false we don't have a block to indent so lets create a div
if (!settings.forced_root_block && !dom.getParent(selection.getNode(), dom.isBlock)) {
formatter.apply('div');
}
each(selection.getSelectedBlocks(), function(element) {
if (element.nodeName != "LI") {
var indentStyleName = editor.getParam('indent_use_margin', false) ? 'margin' : 'padding';
indentStyleName += dom.getStyle(element, 'direction', true) == 'rtl' ? 'Right' : 'Left';
if (command == 'outdent') {
value = Math.max(0, parseInt(element.style[indentStyleName] || 0, 10) - intentValue);
dom.setStyle(element, indentStyleName, value ? value + indentUnit : '');
} else {
value = (parseInt(element.style[indentStyleName] || 0, 10) + intentValue) + indentUnit;
dom.setStyle(element, indentStyleName, value);
}
}
});
} else {
execNativeCommand(command);
}
},
mceRepaint: function() {
if (isGecko) {
try {
storeSelection(TRUE);
if (selection.getSel()) {
selection.getSel().selectAllChildren(editor.getBody());
}
selection.collapse(TRUE);
restoreSelection();
} catch (ex) {
// Ignore
}
}
},
InsertHorizontalRule: function() {
editor.execCommand('mceInsertContent', false, '<hr />');
},
mceToggleVisualAid: function() {
editor.hasVisual = !editor.hasVisual;
editor.addVisual();
},
mceReplaceContent: function(command, ui, value) {
editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({format: 'text'})));
},
mceInsertLink: function(command, ui, value) {
var anchor;
if (typeof value == 'string') {
value = {href: value};
}
anchor = dom.getParent(selection.getNode(), 'a');
// Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here.
value.href = value.href.replace(' ', '%20');
// Remove existing links if there could be child links or that the href isn't specified
if (!anchor || !value.href) {
formatter.remove('link');
}
// Apply new link to selection
if (value.href) {
formatter.apply('link', value, anchor);
}
},
selectAll: function() {
var root = dom.getRoot(), rng;
if (selection.getRng().setStart) {
rng = dom.createRng();
rng.setStart(root, 0);
rng.setEnd(root, root.childNodes.length);
selection.setRng(rng);
} else {
// IE will render it's own root level block elements and sometimes
// even put font elements in them when the user starts typing. So we need to
// move the selection to a more suitable element from this:
// <body>|<p></p></body> to this: <body><p>|</p></body>
rng = selection.getRng();
if (!rng.item) {
rng.moveToElementText(root);
rng.select();
}
}
},
"delete": function() {
execNativeCommand("Delete");
// Check if body is empty after the delete call if so then set the contents
// to an empty string and move the caret to any block produced by that operation
// this fixes the issue with root blocks not being properly produced after a delete call on IE
var body = editor.getBody();
if (dom.isEmpty(body)) {
editor.setContent('');
if (body.firstChild && dom.isBlock(body.firstChild)) {
editor.selection.setCursorLocation(body.firstChild, 0);
} else {
editor.selection.setCursorLocation(body, 0);
}
}
},
mceNewDocument: function() {
editor.setContent('');
},
InsertLineBreak: function(command, ui, value) {
// We load the current event in from EnterKey.js when appropriate to heed
// certain event-specific variations such as ctrl-enter in a list
var evt = value;
var brElm, extraBr, marker;
var rng = selection.getRng(true);
new RangeUtils(dom).normalize(rng);
var offset = rng.startOffset;
var container = rng.startContainer;
// Resolve node index
if (container.nodeType == 1 && container.hasChildNodes()) {
var isAfterLastNodeInContainer = offset > container.childNodes.length - 1;
container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container;
if (isAfterLastNodeInContainer && container.nodeType == 3) {
offset = container.nodeValue.length;
} else {
offset = 0;
}
}
var parentBlock = dom.getParent(container, dom.isBlock);
var parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
var containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null;
var containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5
// Enter inside block contained within a LI then split or insert before/after LI
var isControlKey = evt && evt.ctrlKey;
if (containerBlockName == 'LI' && !isControlKey) {
parentBlock = containerBlock;
parentBlockName = containerBlockName;
}
// Walks the parent block to the right and look for BR elements
function hasRightSideContent() {
var walker = new TreeWalker(container, parentBlock), node;
var nonEmptyElementsMap = editor.schema.getNonEmptyElements();
while ((node = walker.next())) {
if (nonEmptyElementsMap[node.nodeName.toLowerCase()] || node.length > 0) {
return true;
}
}
}
if (container && container.nodeType == 3 && offset >= container.nodeValue.length) {
// Insert extra BR element at the end block elements
if (!isOldIE && !hasRightSideContent()) {
brElm = dom.create('br');
rng.insertNode(brElm);
rng.setStartAfter(brElm);
rng.setEndAfter(brElm);
extraBr = true;
}
}
brElm = dom.create('br');
rng.insertNode(brElm);
// Rendering modes below IE8 doesn't display BR elements in PRE unless we have a \n before it
var documentMode = dom.doc.documentMode;
if (isOldIE && parentBlockName == 'PRE' && (!documentMode || documentMode < 8)) {
brElm.parentNode.insertBefore(dom.doc.createTextNode('\r'), brElm);
}
// Insert temp marker and scroll to that
marker = dom.create('span', {}, '&nbsp;');
brElm.parentNode.insertBefore(marker, brElm);
selection.scrollIntoView(marker);
dom.remove(marker);
if (!extraBr) {
rng.setStartAfter(brElm);
rng.setEndAfter(brElm);
} else {
rng.setStartBefore(brElm);
rng.setEndBefore(brElm);
}
selection.setRng(rng);
editor.undoManager.add();
return TRUE;
}
});
// Add queryCommandState overrides
addCommands({
// Override justify commands
'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull': function(command) {
var name = 'align' + command.substring(7);
var nodes = selection.isCollapsed() ? [dom.getParent(selection.getNode(), dom.isBlock)] : selection.getSelectedBlocks();
var matches = map(nodes, function(node) {
return !!formatter.matchNode(node, name);
});
return inArray(matches, TRUE) !== -1;
},
'Bold,Italic,Underline,Strikethrough,Superscript,Subscript': function(command) {
return isFormatMatch(command);
},
mceBlockQuote: function() {
return isFormatMatch('blockquote');
},
Outdent: function() {
var node;
if (settings.inline_styles) {
if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) {
return TRUE;
}
if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft, 10) > 0) {
return TRUE;
}
}
return (
queryCommandState('InsertUnorderedList') ||
queryCommandState('InsertOrderedList') ||
(!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE'))
);
},
'InsertUnorderedList,InsertOrderedList': function(command) {
var list = dom.getParent(selection.getNode(), 'ul,ol');
return list &&
(
command === 'insertunorderedlist' && list.tagName === 'UL' ||
command === 'insertorderedlist' && list.tagName === 'OL'
);
}
}, 'state');
// Add queryCommandValue overrides
addCommands({
'FontSize,FontName': function(command) {
var value = 0, parent;
if ((parent = dom.getParent(selection.getNode(), 'span'))) {
if (command == 'fontsize') {
value = parent.style.fontSize;
} else {
value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase();
}
}
return value;
}
}, 'value');
// Add undo manager logic
addCommands({
Undo: function() {
editor.undoManager.undo();
},
Redo: function() {
editor.undoManager.redo();
}
});
};
});