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 parser, serializer, parentNode, rootNode, fragment, args,
289 marker, nodeRect, viewPortRect, rng, node, node2, bookmarkHtml, viewportBodyElement;
291 // Setup parser and serializer
292 parser = editor.parser;
293 serializer = new tinymce.html.Serializer({}, editor.schema);
294 bookmarkHtml = '<span id="mce_marker" data-mce-type="bookmark">\uFEFF</span>';
296 // Run beforeSetContent handlers on the HTML to be inserted
297 args = {content: value, format: 'html'};
298 selection.onBeforeSetContent.dispatch(selection, args);
299 value = args.content;
301 // Add caret at end of contents if it's missing
302 if (value.indexOf('{$caret}') == -1)
305 // Replace the caret marker with a span bookmark element
306 value = value.replace(/\{\$caret\}/, bookmarkHtml);
308 // Insert node maker where we will insert the new HTML and get it's parent
309 if (!selection.isCollapsed())
310 editor.getDoc().execCommand('Delete', false, null);
312 parentNode = selection.getNode();
314 // Parse the fragment within the context of the parent node
315 args = {context : parentNode.nodeName.toLowerCase()};
316 fragment = parser.parse(value, args);
318 // Move the caret to a more suitable location
319 node = fragment.lastChild;
320 if (node.attr('id') == 'mce_marker') {
323 for (node = node.prev; node; node = node.walk(true)) {
324 if (node.type == 3 || !dom.isBlock(node.name)) {
325 node.parent.insert(marker, node, node.name === 'br');
331 // If parser says valid we can insert the contents into that parent
333 value = serializer.serialize(fragment);
335 // Check if parent is empty or only has one BR element then set the innerHTML of that parent
336 node = parentNode.firstChild;
337 node2 = parentNode.lastChild;
338 if (!node || (node === node2 && node.nodeName === 'BR'))
339 dom.setHTML(parentNode, value);
341 selection.setContent(value);
343 // If the fragment was invalid within that context then we need
344 // to parse and process the parent it's inserted into
346 // Insert bookmark node and get the parent
347 selection.setContent(bookmarkHtml);
348 parentNode = editor.selection.getNode();
349 rootNode = editor.getBody();
351 // Opera will return the document node when selection is in root
352 if (parentNode.nodeType == 9)
353 parentNode = node = rootNode;
357 // Find the ancestor just before the root element
358 while (node !== rootNode) {
360 node = node.parentNode;
363 // Get the outer/inner HTML depending on if we are in the root and parser and serialize that
364 value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode);
365 value = serializer.serialize(
367 // Need to replace by using a function since $ in the contents would otherwise be a problem
368 value.replace(/<span (id="mce_marker"|id=mce_marker).+?<\/span>/i, function() {
369 return serializer.serialize(fragment);
374 // Set the inner/outer HTML depending on if we are in the root or not
375 if (parentNode == rootNode)
376 dom.setHTML(rootNode, value);
378 dom.setOuterHTML(parentNode, value);
381 marker = dom.get('mce_marker');
383 // Scroll range into view scrollIntoView on element can't be used since it will scroll the main view port as well
384 nodeRect = dom.getRect(marker);
385 viewPortRect = dom.getViewPort(editor.getWin());
387 // Check if node is out side the viewport if it is then scroll to it
388 if ((nodeRect.y + nodeRect.h > viewPortRect.y + viewPortRect.h || nodeRect.y < viewPortRect.y) ||
389 (nodeRect.x > viewPortRect.x + viewPortRect.w || nodeRect.x < viewPortRect.x)) {
390 viewportBodyElement = tinymce.isIE ? editor.getDoc().documentElement : editor.getBody();
391 viewportBodyElement.scrollLeft = nodeRect.x;
392 viewportBodyElement.scrollTop = nodeRect.y - viewPortRect.h + 25;
395 // Move selection before marker and remove it
396 rng = dom.createRng();
398 // If previous sibling is a text node set the selection to the end of that node
399 node = marker.previousSibling;
400 if (node && node.nodeType == 3) {
401 rng.setStart(node, node.nodeValue.length);
403 // If the previous sibling isn't a text node or doesn't exist set the selection before the marker node
404 rng.setStartBefore(marker);
405 rng.setEndBefore(marker);
408 // Remove the marker node and set the new range
410 selection.setRng(rng);
412 // Dispatch after event and add any visual elements needed
413 selection.onSetContent.dispatch(selection, args);
417 mceInsertRawHTML : function(command, ui, value) {
418 selection.setContent('tiny_mce_marker');
419 editor.setContent(editor.getContent().replace(/tiny_mce_marker/g, function() { return value }));
422 mceSetContent : function(command, ui, value) {
423 editor.setContent(value);
426 'Indent,Outdent' : function(command) {
427 var intentValue, indentUnit, value;
429 // Setup indent level
430 intentValue = settings.indentation;
431 indentUnit = /[a-z%]+$/i.exec(intentValue);
432 intentValue = parseInt(intentValue);
434 if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) {
435 each(selection.getSelectedBlocks(), function(element) {
436 if (command == 'outdent') {
437 value = Math.max(0, parseInt(element.style.paddingLeft || 0) - intentValue);
438 dom.setStyle(element, 'paddingLeft', value ? value + indentUnit : '');
440 dom.setStyle(element, 'paddingLeft', (parseInt(element.style.paddingLeft || 0) + intentValue) + indentUnit);
443 execNativeCommand(command);
446 mceRepaint : function() {
449 if (tinymce.isGecko) {
451 storeSelection(TRUE);
453 if (selection.getSel())
454 selection.getSel().selectAllChildren(editor.getBody());
456 selection.collapse(TRUE);
464 mceToggleFormat : function(command, ui, value) {
465 editor.formatter.toggle(value);
468 InsertHorizontalRule : function() {
469 editor.execCommand('mceInsertContent', false, '<hr />');
472 mceToggleVisualAid : function() {
473 editor.hasVisual = !editor.hasVisual;
477 mceReplaceContent : function(command, ui, value) {
478 editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({format : 'text'})));
481 mceInsertLink : function(command, ui, value) {
482 var link = dom.getParent(selection.getNode(), 'a'), img, style, cls;
484 if (tinymce.is(value, 'string'))
485 value = {href : value};
487 // Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here.
488 value.href = value.href.replace(' ', '%20');
491 // WebKit can't create links on floated images for some odd reason
492 // So, just remove styles and restore it later
493 if (tinymce.isWebKit) {
494 img = dom.getParent(selection.getNode(), 'img');
497 style = img.style.cssText;
499 img.style.cssText = null;
500 img.className = null;
504 execNativeCommand('CreateLink', FALSE, 'javascript:mctmp(0);');
508 img.style.cssText = style;
512 each(dom.select("a[href='javascript:mctmp(0);']"), function(link) {
513 dom.setAttribs(link, value);
517 dom.setAttribs(link, value);
519 editor.dom.remove(link, TRUE);
523 selectAll : function() {
524 var root = dom.getRoot(), rng = dom.createRng();
526 rng.setStart(root, 0);
527 rng.setEnd(root, root.childNodes.length);
529 editor.selection.setRng(rng);
533 // Add queryCommandState overrides
535 // Override justify commands
536 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) {
537 return isFormatMatch('align' + command.substring(7));
540 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) {
541 return isFormatMatch(command);
544 mceBlockQuote : function() {
545 return isFormatMatch('blockquote');
548 Outdent : function() {
551 if (settings.inline_styles) {
552 if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0)
555 if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0)
559 return queryCommandState('InsertUnorderedList') || queryCommandState('InsertOrderedList') || (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE'));
562 'InsertUnorderedList,InsertOrderedList' : function(command) {
563 return dom.getParent(selection.getNode(), command == 'insertunorderedlist' ? 'UL' : 'OL');
567 // Add queryCommandValue overrides
569 'FontSize,FontName' : function(command) {
570 var value = 0, parent;
572 if (parent = dom.getParent(selection.getNode(), 'span')) {
573 if (command == 'fontsize')
574 value = parent.style.fontSize;
576 value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase();
583 // Add undo manager logic
584 if (settings.custom_undo_redo) {
587 editor.undoManager.undo();
591 editor.undoManager.redo();