4 * Copyright 2009, Moxiecode Systems AB
5 * Released under LGPL License.
7 * License: http://tinymce.moxiecode.com/license
8 * Contributing: http://tinymce.moxiecode.com/contributing
12 // Added for compression purposes
13 var each = tinymce.each, undefined, TRUE = true, FALSE = false;
16 * This class enables you to add custom editor commands and it contains
17 * overrides for native browser commands to address various bugs and issues.
19 * @class tinymce.EditorCommands
21 tinymce.EditorCommands = function(editor) {
23 selection = editor.selection,
24 commands = {state: {}, exec : {}, value : {}},
25 settings = editor.settings,
29 * Executes the specified command.
32 * @param {String} command Command to execute.
33 * @param {Boolean} ui Optional user interface state.
34 * @param {Object} value Optional value for command.
35 * @return {Boolean} true/false if the command was found or not.
37 function execCommand(command, ui, value) {
40 command = command.toLowerCase();
41 if (func = commands.exec[command]) {
42 func(command, ui, value);
50 * Queries the current state for a command for example if the current selection is "bold".
52 * @method queryCommandState
53 * @param {String} command Command to check the state of.
54 * @return {Boolean/Number} true/false if the selected contents is bold or not, -1 if it's not found.
56 function queryCommandState(command) {
59 command = command.toLowerCase();
60 if (func = commands.state[command])
67 * Queries the command value for example the current fontsize.
69 * @method queryCommandValue
70 * @param {String} command Command to check the value of.
71 * @return {Object} Command value of false if it's not found.
73 function queryCommandValue(command) {
76 command = command.toLowerCase();
77 if (func = commands.value[command])
84 * Adds commands to the command collection.
87 * @param {Object} command_list Name/value collection with commands to add, the names can also be comma separated.
88 * @param {String} type Optional type to add, defaults to exec. Can be value or state as well.
90 function addCommands(command_list, type) {
91 type = type || 'exec';
93 each(command_list, function(callback, command) {
94 each(command.toLowerCase().split(','), function(command) {
95 commands[type][command] = callback;
100 // Expose public methods
101 tinymce.extend(this, {
102 execCommand : execCommand,
103 queryCommandState : queryCommandState,
104 queryCommandValue : queryCommandValue,
105 addCommands : addCommands
110 function execNativeCommand(command, ui, value) {
111 if (ui === undefined)
114 if (value === undefined)
117 return editor.getDoc().execCommand(command, ui, value);
120 function isFormatMatch(name) {
121 return editor.formatter.match(name);
124 function toggleFormat(name, value) {
125 editor.formatter.toggle(name, value ? {value : value} : undefined);
128 function storeSelection(type) {
129 bookmark = selection.getBookmark(type);
132 function restoreSelection() {
133 selection.moveToBookmark(bookmark);
136 // Add execCommand overrides
138 // Ignore these, added for compatibility
139 'mceResetDesignMode,mceBeginUndoLevel' : function() {},
141 // Add undo manager logic
142 'mceEndUndoLevel,mceAddUndoLevel' : function() {
143 editor.undoManager.add();
146 'Cut,Copy,Paste' : function(command) {
147 var doc = editor.getDoc(), failed;
149 // Try executing the native command
151 execNativeCommand(command);
157 // Present alert message about clipboard access not being available
158 if (failed || !doc.queryCommandSupported(command)) {
159 if (tinymce.isGecko) {
160 editor.windowManager.confirm(editor.getLang('clipboard_msg'), function(state) {
162 open('http://www.mozilla.org/editor/midasdemo/securityprefs.html', '_blank');
165 editor.windowManager.alert(editor.getLang('clipboard_no_support'));
169 // Override unlink command
170 unlink : function(command) {
171 if (selection.isCollapsed())
172 selection.select(selection.getNode());
174 execNativeCommand(command);
175 selection.collapse(FALSE);
178 // Override justify commands to use the text formatter engine
179 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) {
180 var align = command.substring(7);
182 // Remove all other alignments first
183 each('left,center,right,full'.split(','), function(name) {
185 editor.formatter.remove('align' + name);
188 toggleFormat('align' + align);
189 execCommand('mceRepaint');
192 // Override list commands to fix WebKit bug
193 'InsertUnorderedList,InsertOrderedList' : function(command) {
194 var listElm, listParent;
196 execNativeCommand(command);
198 // WebKit produces lists within block elements so we need to split them
199 // we will replace the native list creation logic to custom logic later on
200 // TODO: Remove this when the list creation logic is removed
201 listElm = dom.getParent(selection.getNode(), 'ol,ul');
203 listParent = listElm.parentNode;
205 // If list is within a text block then split that block
206 if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) {
208 dom.split(listParent, listElm);
214 // Override commands to use the text formatter engine
215 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) {
216 toggleFormat(command);
219 // Override commands to use the text formatter engine
220 'ForeColor,HiliteColor,FontName' : function(command, ui, value) {
221 toggleFormat(command, value);
224 FontSize : function(command, ui, value) {
225 var fontClasses, fontSizes;
227 // Convert font size 1-7 to styles
228 if (value >= 1 && value <= 7) {
229 fontSizes = tinymce.explode(settings.font_size_style_values);
230 fontClasses = tinymce.explode(settings.font_size_classes);
233 value = fontClasses[value - 1] || value;
235 value = fontSizes[value - 1] || value;
238 toggleFormat(command, value);
241 RemoveFormat : function(command) {
242 editor.formatter.remove(command);
245 mceBlockQuote : function(command) {
246 toggleFormat('blockquote');
249 FormatBlock : function(command, ui, value) {
250 return toggleFormat(value || 'p');
253 mceCleanup : function() {
254 var bookmark = selection.getBookmark();
256 editor.setContent(editor.getContent({cleanup : TRUE}), {cleanup : TRUE});
258 selection.moveToBookmark(bookmark);
261 mceRemoveNode : function(command, ui, value) {
262 var node = value || selection.getNode();
264 // Make sure that the body node isn't removed
265 if (node != editor.getBody()) {
267 editor.dom.remove(node, TRUE);
272 mceSelectNodeDepth : function(command, ui, value) {
275 dom.getParent(selection.getNode(), function(node) {
276 if (node.nodeType == 1 && counter++ == value) {
277 selection.select(node);
280 }, editor.getBody());
283 mceSelectNode : function(command, ui, value) {
284 selection.select(value);
287 mceInsertContent : function(command, ui, value) {
288 var caretNode, rng, rootNode, parent, node, rng, nodeRect, viewPortRect, args;
290 function findSuitableCaretNode(node, root_node, next) {
291 var walker = new tinymce.dom.TreeWalker(next ? node.nextSibling : node.previousSibling, root_node);
293 while ((node = walker.current())) {
294 if ((node.nodeType == 3 && tinymce.trim(node.nodeValue).length) || node.nodeName == 'BR' || node.nodeName == 'IMG')
304 args = {content: value, format: 'html'};
305 selection.onBeforeSetContent.dispatch(selection, args);
306 value = args.content;
308 // Add caret at end of contents if it's missing
309 if (value.indexOf('{$caret}') == -1)
312 // Set the content at selection to a span and replace it's contents with the value
313 selection.setContent('<span id="__mce">\uFEFF</span>', {no_events : false});
314 dom.setOuterHTML('__mce', value.replace(/\{\$caret\}/, '<span data-mce-type="bookmark" id="__mce">\uFEFF</span>'));
316 caretNode = dom.select('#__mce')[0];
317 rootNode = dom.getRoot();
319 // Move the caret into the last suitable location within the previous sibling if it's a block since the block might be split
320 if (caretNode.previousSibling && dom.isBlock(caretNode.previousSibling) || caretNode.parentNode == rootNode) {
321 node = findSuitableCaretNode(caretNode, rootNode);
323 if (node.nodeName == 'BR')
324 node.parentNode.insertBefore(caretNode, node);
326 dom.insertAfter(caretNode, node);
330 // Find caret root parent and clean it up using the serializer to avoid nesting
332 if (caretNode === rootNode) {
333 // Clean up the parent element by parsing and serializing it
334 // This will remove invalid elements/attributes and fix nesting issues
335 dom.setOuterHTML(parent,
336 new tinymce.html.Serializer({}, editor.schema).serialize(
337 editor.parser.parse(dom.getOuterHTML(parent))
345 caretNode = caretNode.parentNode;
348 // Find caret after cleanup and move selection to that location
349 caretNode = dom.select('#__mce')[0];
351 node = findSuitableCaretNode(caretNode, rootNode) || findSuitableCaretNode(caretNode, rootNode, true);
352 dom.remove(caretNode);
355 rng = dom.createRng();
357 if (node.nodeType == 3) {
358 rng.setStart(node, node.length);
359 rng.setEnd(node, node.length);
361 if (node.nodeName == 'BR') {
362 rng.setStartBefore(node);
363 rng.setEndBefore(node);
365 rng.setStartAfter(node);
366 rng.setEndAfter(node);
370 selection.setRng(rng);
372 // Scroll range into view scrollIntoView on element can't be used since it will scroll the main view port as well
374 node = dom.create('span', null, '\u00a0');
375 rng.insertNode(node);
376 nodeRect = dom.getRect(node);
377 viewPortRect = dom.getViewPort(editor.getWin());
379 // Check if node is out side the viewport if it is then scroll to it
380 if ((nodeRect.y > viewPortRect.y + viewPortRect.h || nodeRect.y < viewPortRect.y) ||
381 (nodeRect.x > viewPortRect.x + viewPortRect.w || nodeRect.x < viewPortRect.x)) {
382 editor.getBody().scrollLeft = nodeRect.x;
383 editor.getBody().scrollTop = nodeRect.y;
389 // Make sure that the selection is collapsed after we removed the node fixes a WebKit bug
390 // where WebKit would place the endContainer/endOffset at a different location than the startContainer/startOffset
391 selection.collapse(true);
395 selection.onSetContent.dispatch(selection, args);
399 mceInsertRawHTML : function(command, ui, value) {
400 selection.setContent('tiny_mce_marker');
401 editor.setContent(editor.getContent().replace(/tiny_mce_marker/g, function() { return value }));
404 mceSetContent : function(command, ui, value) {
405 editor.setContent(value);
408 'Indent,Outdent' : function(command) {
409 var intentValue, indentUnit, value;
411 // Setup indent level
412 intentValue = settings.indentation;
413 indentUnit = /[a-z%]+$/i.exec(intentValue);
414 intentValue = parseInt(intentValue);
416 if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) {
417 each(selection.getSelectedBlocks(), function(element) {
418 if (command == 'outdent') {
419 value = Math.max(0, parseInt(element.style.paddingLeft || 0) - intentValue);
420 dom.setStyle(element, 'paddingLeft', value ? value + indentUnit : '');
422 dom.setStyle(element, 'paddingLeft', (parseInt(element.style.paddingLeft || 0) + intentValue) + indentUnit);
425 execNativeCommand(command);
428 mceRepaint : function() {
431 if (tinymce.isGecko) {
433 storeSelection(TRUE);
435 if (selection.getSel())
436 selection.getSel().selectAllChildren(editor.getBody());
438 selection.collapse(TRUE);
446 mceToggleFormat : function(command, ui, value) {
447 editor.formatter.toggle(value);
450 InsertHorizontalRule : function() {
451 editor.execCommand('mceInsertContent', false, '<hr />');
454 mceToggleVisualAid : function() {
455 editor.hasVisual = !editor.hasVisual;
459 mceReplaceContent : function(command, ui, value) {
460 editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({format : 'text'})));
463 mceInsertLink : function(command, ui, value) {
464 var link = dom.getParent(selection.getNode(), 'a'), img, floatVal;
466 if (tinymce.is(value, 'string'))
467 value = {href : value};
469 // Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here.
470 value.href = value.href.replace(' ', '%20');
473 // WebKit can't create links on float images for some odd reason so just remove it and restore it later
474 if (tinymce.isWebKit) {
475 img = dom.getParent(selection.getNode(), 'img');
478 floatVal = img.style.cssFloat;
479 img.style.cssFloat = null;
483 execNativeCommand('CreateLink', FALSE, 'javascript:mctmp(0);');
485 // Restore float value
487 img.style.cssFloat = floatVal;
489 each(dom.select("a[href='javascript:mctmp(0);']"), function(link) {
490 dom.setAttribs(link, value);
494 dom.setAttribs(link, value);
496 editor.dom.remove(link, TRUE);
500 selectAll : function() {
501 var root = dom.getRoot(), rng = dom.createRng();
503 rng.setStart(root, 0);
504 rng.setEnd(root, root.childNodes.length);
506 editor.selection.setRng(rng);
510 // Add queryCommandState overrides
512 // Override justify commands
513 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) {
514 return isFormatMatch('align' + command.substring(7));
517 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) {
518 return isFormatMatch(command);
521 mceBlockQuote : function() {
522 return isFormatMatch('blockquote');
525 Outdent : function() {
528 if (settings.inline_styles) {
529 if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0)
532 if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0)
536 return queryCommandState('InsertUnorderedList') || queryCommandState('InsertOrderedList') || (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE'));
539 'InsertUnorderedList,InsertOrderedList' : function(command) {
540 return dom.getParent(selection.getNode(), command == 'insertunorderedlist' ? 'UL' : 'OL');
544 // Add queryCommandValue overrides
546 'FontSize,FontName' : function(command) {
547 var value = 0, parent;
549 if (parent = dom.getParent(selection.getNode(), 'span')) {
550 if (command == 'fontsize')
551 value = parent.style.fontSize;
553 value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase();
560 // Add undo manager logic
561 if (settings.custom_undo_redo) {
564 editor.undoManager.undo();
568 editor.undoManager.redo();