/**
* 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"
], function(Serializer, Env, Tools) {
// 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;
var TRUE = true, FALSE = false;
return function(editor) {
var dom = editor.dom,
selection = editor.selection,
commands = {state: {}, exec: {}, value: {}},
settings = editor.settings,
formatter = editor.formatter,
bookmark;
/**
* 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) {
var func;
command = command.toLowerCase();
if ((func = commands.exec[command])) {
func(command, ui, 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;
command = command.toLowerCase();
if ((func = commands.state[command])) {
return func(command);
}
return -1;
}
/**
* 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;
command = command.toLowerCase();
if ((func = commands.value[command])) {
return func(command);
}
return FALSE;
}
/**
* 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;
});
});
}
// Expose public methods
extend(this, {
execCommand: execCommand,
queryCommandState: queryCommandState,
queryCommandValue: queryCommandValue,
addCommands: addCommands
});
// 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)) {
editor.windowManager.alert(
"Your browser doesn't support direct access to the clipboard. " +
"Please use the Ctrl+X/C/V keyboard shortcuts instead."
);
}
},
// Override unlink command
unlink: function(command) {
if (selection.isCollapsed()) {
selection.select(selection.getNode());
}
execNativeCommand(command);
selection.collapse(FALSE);
},
// 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;
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(/^ /, ' ');
} else if (!hasSiblingText('previousSibling')) {
html = html.replace(/^ /, ' ');
}
if (offset < container.length) {
html = html.replace(/ (
|)$/, ' ');
} else if (!hasSiblingText('nextSibling')) {
html = html.replace(/( | )(
|)$/, ' ');
}
}
return html;
}
// 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 = '';
// 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
|
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) { 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(//i, function() { return serializer.serialize(fragment); }) ) ); // 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'); // start---------- // 为了定位到粘贴后 var bookmarkHtml = ''; if (value.indexOf('{$caret}') == -1) { value += '{$caret}'; } // Replace the caret marker with a span bookmark element value = value.replace(/\{\$caret\}/, bookmarkHtml); editor.setContent( editor.getContent().replace(/tiny_mce_marker/g, function() { return value; }) ); var marker = dom.get('mce_marker'); var rng = dom.createRng(); rng.setStartBefore(marker); rng.setEndBefore(marker); // Remove the marker node and set the new range dom.remove(marker); selection.setRng(rng); //--------end }, 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) { var indentStyleName; if (element.nodeName != "LI") { indentStyleName = dom.getStyle(element, 'direction', true) == 'rtl' ? 'paddingRight' : 'paddingLeft'; 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, '