/** * 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);