/**
* EditorCommands.js
*
* Copyright 2009, Moxiecode Systems AB
* Released under LGPL License.
*
* License: http://tinymce.moxiecode.com/license
* Contributing: http://tinymce.moxiecode.com/contributing
*/
(function(tinymce) {
// Added for compression purposes
var each = tinymce.each, undefined, TRUE = true, FALSE = false;
/**
* 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
*/
tinymce.EditorCommands = 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
tinymce.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);
};
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)) {
if (tinymce.isGecko) {
editor.windowManager.confirm(editor.getLang('clipboard_msg'), function(state) {
if (state)
open('http://www.mozilla.org/editor/midasdemo/securityprefs.html', '_blank');
});
} else
editor.windowManager.alert(editor.getLang('clipboard_no_support'));
}
},
// 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);
// Remove all other alignments first
each('left,center,right,full'.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 = tinymce.explode(settings.font_size_style_values);
fontClasses = tinymce.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(command) {
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,
marker, nodeRect, viewPortRect, rng, node, node2, bookmarkHtml, viewportBodyElement;
// Setup parser and serializer
parser = editor.parser;
serializer = new tinymce.html.Serializer({}, editor.schema);
bookmarkHtml = '\uFEFF';
// Run beforeSetContent handlers on the HTML to be inserted
args = {content: value, format: 'html'};
selection.onBeforeSetContent.dispatch(selection, 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);
// 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
args = {context : parentNode.nodeName.toLowerCase()};
fragment = parser.parse(value, args);
// 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 (!args.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 = editor.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');
// Scroll range into view scrollIntoView on element can't be used since it will scroll the main view port as well
nodeRect = dom.getRect(marker);
viewPortRect = dom.getViewPort(editor.getWin());
// Check if node is out side the viewport if it is then scroll to it
if ((nodeRect.y + nodeRect.h > viewPortRect.y + viewPortRect.h || nodeRect.y < viewPortRect.y) ||
(nodeRect.x > viewPortRect.x + viewPortRect.w || nodeRect.x < viewPortRect.x)) {
viewportBodyElement = tinymce.isIE ? editor.getDoc().documentElement : editor.getBody();
viewportBodyElement.scrollLeft = nodeRect.x;
viewportBodyElement.scrollTop = nodeRect.y - viewPortRect.h + 25;
}
// 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);
} 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
selection.onSetContent.dispatch(selection, args);
editor.addVisual();
},
mceInsertRawHTML : function(command, ui, value) {
selection.setContent('tiny_mce_marker');
editor.setContent(editor.getContent().replace(/tiny_mce_marker/g, function() { return 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);
if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) {
each(selection.getSelectedBlocks(), function(element) {
if (command == 'outdent') {
value = Math.max(0, parseInt(element.style.paddingLeft || 0) - intentValue);
dom.setStyle(element, 'paddingLeft', value ? value + indentUnit : '');
} else
dom.setStyle(element, 'paddingLeft', (parseInt(element.style.paddingLeft || 0) + intentValue) + indentUnit);
});
} else
execNativeCommand(command);
},
mceRepaint : function() {
var bookmark;
if (tinymce.isGecko) {
try {
storeSelection(TRUE);
if (selection.getSel())
selection.getSel().selectAllChildren(editor.getBody());
selection.collapse(TRUE);
restoreSelection();
} catch (ex) {
// Ignore
}
}
},
mceToggleFormat : function(command, ui, value) {
formatter.toggle(value);
},
InsertHorizontalRule : function() {
editor.execCommand('mceInsertContent', false, '
');
},
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 = dom.createRng();
rng.setStart(root, 0);
rng.setEnd(root, root.childNodes.length);
editor.selection.setRng(rng);
}
});
// Add queryCommandState overrides
addCommands({
// Override justify commands
'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) {
return isFormatMatch('align' + command.substring(7));
},
'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) > 0)
return TRUE;
if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0)
return TRUE;
}
return queryCommandState('InsertUnorderedList') || queryCommandState('InsertOrderedList') || (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE'));
},
'InsertUnorderedList,InsertOrderedList' : function(command) {
return dom.getParent(selection.getNode(), command == 'insertunorderedlist' ? 'UL' : '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
if (settings.custom_undo_redo) {
addCommands({
Undo : function() {
editor.undoManager.undo();
},
Redo : function() {
editor.undoManager.redo();
}
});
}
};
})(tinymce);