/** * EditorCommands.js * * Released under LGPL License. * Copyright (c) 1999-2017 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ import Env from './Env'; import InsertContent from '../InsertContent'; import DeleteCommands from '../delete/DeleteCommands'; import * as FontCommands from '../commands/FontCommands'; import NodeType from '../dom/NodeType'; import InsertBr from '../newline/InsertBr'; import SelectionBookmark from '../selection/SelectionBookmark'; import Tools from './util/Tools'; import { Selection } from './dom/Selection'; import * as IndentOutdent from 'tinymce/core/commands/IndentOutdent'; import { Editor } from 'tinymce/core/api/Editor'; import { DOMUtils } from 'tinymce/core/api/dom/DOMUtils'; /** * 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 */ // Added for compression purposes const each = Tools.each, extend = Tools.extend; const map = Tools.map, inArray = Tools.inArray; export default function (editor: Editor) { let dom: DOMUtils, selection: Selection, formatter; const commands = { state: {}, exec: {}, value: {} }; let settings = editor.settings, bookmark; editor.on('PreInit', function () { dom = editor.dom; selection = editor.selection; settings = editor.settings; formatter = editor.formatter; }); /** * 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. * @param {Object} args Optional extra arguments to the execCommand. * @return {Boolean} true/false if the command was found or not. */ const execCommand = function (command, ui, value, args) { let func, customCommand, state = false; if (editor.removed) { return; } if (!/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint)$/.test(command) && (!args || !args.skip_focus)) { editor.focus(); } else { SelectionBookmark.restore(editor); } args = editor.fire('BeforeExecCommand', { command, ui, value }); if (args.isDefaultPrevented()) { return false; } customCommand = command.toLowerCase(); if ((func = commands.exec[customCommand])) { func(customCommand, ui, value); editor.fire('ExecCommand', { command, ui, value }); return true; } // Plugin commands each(editor.plugins, function (p) { if (p.execCommand && p.execCommand(command, ui, value)) { editor.fire('ExecCommand', { command, ui, value }); state = true; return false; } }); if (state) { return state; } // Theme commands if (editor.theme && editor.theme.execCommand && editor.theme.execCommand(command, ui, value)) { editor.fire('ExecCommand', { command, ui, value }); return true; } // Browser commands try { state = editor.getDoc().execCommand(command, ui, value); } catch (ex) { // Ignore old IE errors } if (state) { editor.fire('ExecCommand', { 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. */ const queryCommandState = function (command) { let func; if (editor.quirks.isHidden() || editor.removed) { return; } command = command.toLowerCase(); if ((func = commands.state[command])) { return func(command); } // Browser commands try { return editor.getDoc().queryCommandState(command); } catch (ex) { // Fails sometimes see bug: 1896577 } return false; }; /** * 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. */ const queryCommandValue = function (command) { let func; if (editor.quirks.isHidden() || editor.removed) { return; } command = command.toLowerCase(); if ((func = commands.value[command])) { return func(command); } // Browser commands try { return editor.getDoc().queryCommandValue(command); } catch (ex) { // Fails sometimes see bug: 1896577 } }; /** * Adds commands to the command collection. * * @method addCommands * @param {Object} commandList 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. */ const addCommands = function (commandList, type?) { type = type || 'exec'; each(commandList, function (callback, command) { each(command.toLowerCase().split(','), function (command) { commands[type][command] = callback; }); }); }; const addCommand = function (command, callback, scope) { command = command.toLowerCase(); commands.exec[command] = function (command, ui, value, args) { return callback.call(scope || editor, ui, value, args); }; }; /** * Returns true/false if the command is supported or not. * * @method queryCommandSupported * @param {String} command Command that we check support for. * @return {Boolean} true/false if the command is supported or not. */ const queryCommandSupported = function (command) { command = command.toLowerCase(); if (commands.exec[command]) { return true; } // Browser commands try { return editor.getDoc().queryCommandSupported(command); } catch (ex) { // Fails sometimes see bug: 1896577 } return false; }; const addQueryStateHandler = function (command, callback, scope) { command = command.toLowerCase(); commands.state[command] = function () { return callback.call(scope || editor); }; }; const addQueryValueHandler = function (command, callback, scope) { command = command.toLowerCase(); commands.value[command] = function () { return callback.call(scope || editor); }; }; const hasCustomCommand = function (command) { command = command.toLowerCase(); return !!commands.exec[command]; }; // Expose public methods extend(this, { execCommand, queryCommandState, queryCommandValue, queryCommandSupported, addCommands, addCommand, addQueryStateHandler, addQueryValueHandler, hasCustomCommand }); // Private methods const execNativeCommand = function (command, ui?, value?) { if (ui === undefined) { ui = false; } if (value === undefined) { value = null; } return editor.getDoc().execCommand(command, ui, value); }; const isFormatMatch = function (name) { return formatter.match(name); }; const toggleFormat = function (name, value?) { formatter.toggle(name, value ? { value } : undefined); editor.nodeChanged(); }; const storeSelection = function (type?) { bookmark = selection.getBookmark(type); }; const restoreSelection = function () { selection.moveToBookmark(bookmark); }; // Add execCommand overrides addCommands({ // Ignore these, added for compatibility 'mceResetDesignMode,mceBeginUndoLevel' () { }, // Add undo manager logic 'mceEndUndoLevel,mceAddUndoLevel' () { editor.undoManager.add(); }, 'Cut,Copy,Paste' (command) { const doc = editor.getDoc(); let failed; // Try executing the native command try { execNativeCommand(command); } catch (ex) { // Command failed failed = true; } // Chrome reports the paste command as supported however older IE:s will return false for cut/paste if (command === 'paste' && !doc.queryCommandEnabled(command)) { failed = true; } // Present alert message about clipboard access not being available if (failed || !doc.queryCommandSupported(command)) { let msg = editor.translate( 'Your browser doesn\'t support direct access to the clipboard. ' + 'Please use the Ctrl+X/C/V keyboard shortcuts instead.' ); if (Env.mac) { msg = msg.replace(/Ctrl\+/g, '\u2318+'); } editor.notificationManager.open({ text: msg, type: 'error' }); } }, // Override unlink command 'unlink' () { if (selection.isCollapsed()) { const elm = editor.dom.getParent(editor.selection.getStart(), 'a'); if (elm) { editor.dom.remove(elm, true); } return; } formatter.remove('link'); }, // Override justify commands to use the text formatter engine 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull,JustifyNone' (command) { let 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); } }); if (align !== 'none') { toggleFormat('align' + align); } }, // Override list commands to fix WebKit bug 'InsertUnorderedList,InsertOrderedList' (command) { let 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' (command) { toggleFormat(command); }, // Override commands to use the text formatter engine 'ForeColor,HiliteColor' (command, ui, value) { toggleFormat(command, value); }, 'FontName' (command, ui, value) { FontCommands.fontNameAction(editor, value); }, 'FontSize' (command, ui, value) { FontCommands.fontSizeAction(editor, value); }, 'RemoveFormat' (command) { formatter.remove(command); }, 'mceBlockQuote' () { toggleFormat('blockquote'); }, 'FormatBlock' (command, ui, value) { return toggleFormat(value || 'p'); }, 'mceCleanup' () { const bookmark = selection.getBookmark(); editor.setContent(editor.getContent()); selection.moveToBookmark(bookmark); }, 'mceRemoveNode' (command, ui, value) { const 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' (command, ui, value) { let counter = 0; dom.getParent(selection.getNode(), function (node) { if (node.nodeType === 1 && counter++ === value) { selection.select(node); return false; } }, editor.getBody()); }, 'mceSelectNode' (command, ui, value) { selection.select(value); }, 'mceInsertContent' (command, ui, value) { InsertContent.insertAtCaret(editor, value); }, 'mceInsertRawHTML' (command, ui, value) { const content = editor.getContent() as string; selection.setContent('tiny_mce_marker'); editor.setContent(content.replace(/tiny_mce_marker/g, () => value)); }, 'mceToggleFormat' (command, ui, value) { toggleFormat(value); }, 'mceSetContent' (command, ui, value) { editor.setContent(value); }, 'Indent,Outdent' (command) { IndentOutdent.handle(editor, command); }, 'mceRepaint' () { }, 'InsertHorizontalRule' () { editor.execCommand('mceInsertContent', false, '