EditorCommands.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. /**
  2. * EditorCommands.js
  3. *
  4. * Copyright 2009, Moxiecode Systems AB
  5. * Released under LGPL License.
  6. *
  7. * License: http://tinymce.moxiecode.com/license
  8. * Contributing: http://tinymce.moxiecode.com/contributing
  9. */
  10. (function(tinymce) {
  11. // Added for compression purposes
  12. var each = tinymce.each, undefined, TRUE = true, FALSE = false;
  13. /**
  14. * This class enables you to add custom editor commands and it contains
  15. * overrides for native browser commands to address various bugs and issues.
  16. *
  17. * @class tinymce.EditorCommands
  18. */
  19. tinymce.EditorCommands = function(editor) {
  20. var dom = editor.dom,
  21. selection = editor.selection,
  22. commands = {state: {}, exec : {}, value : {}},
  23. settings = editor.settings,
  24. formatter = editor.formatter,
  25. bookmark;
  26. /**
  27. * Executes the specified command.
  28. *
  29. * @method execCommand
  30. * @param {String} command Command to execute.
  31. * @param {Boolean} ui Optional user interface state.
  32. * @param {Object} value Optional value for command.
  33. * @return {Boolean} true/false if the command was found or not.
  34. */
  35. function execCommand(command, ui, value) {
  36. var func;
  37. command = command.toLowerCase();
  38. if (func = commands.exec[command]) {
  39. func(command, ui, value);
  40. return TRUE;
  41. }
  42. return FALSE;
  43. };
  44. /**
  45. * Queries the current state for a command for example if the current selection is "bold".
  46. *
  47. * @method queryCommandState
  48. * @param {String} command Command to check the state of.
  49. * @return {Boolean/Number} true/false if the selected contents is bold or not, -1 if it's not found.
  50. */
  51. function queryCommandState(command) {
  52. var func;
  53. command = command.toLowerCase();
  54. if (func = commands.state[command])
  55. return func(command);
  56. return -1;
  57. };
  58. /**
  59. * Queries the command value for example the current fontsize.
  60. *
  61. * @method queryCommandValue
  62. * @param {String} command Command to check the value of.
  63. * @return {Object} Command value of false if it's not found.
  64. */
  65. function queryCommandValue(command) {
  66. var func;
  67. command = command.toLowerCase();
  68. if (func = commands.value[command])
  69. return func(command);
  70. return FALSE;
  71. };
  72. /**
  73. * Adds commands to the command collection.
  74. *
  75. * @method addCommands
  76. * @param {Object} command_list Name/value collection with commands to add, the names can also be comma separated.
  77. * @param {String} type Optional type to add, defaults to exec. Can be value or state as well.
  78. */
  79. function addCommands(command_list, type) {
  80. type = type || 'exec';
  81. each(command_list, function(callback, command) {
  82. each(command.toLowerCase().split(','), function(command) {
  83. commands[type][command] = callback;
  84. });
  85. });
  86. };
  87. // Expose public methods
  88. tinymce.extend(this, {
  89. execCommand : execCommand,
  90. queryCommandState : queryCommandState,
  91. queryCommandValue : queryCommandValue,
  92. addCommands : addCommands
  93. });
  94. // Private methods
  95. function execNativeCommand(command, ui, value) {
  96. if (ui === undefined)
  97. ui = FALSE;
  98. if (value === undefined)
  99. value = null;
  100. return editor.getDoc().execCommand(command, ui, value);
  101. };
  102. function isFormatMatch(name) {
  103. return formatter.match(name);
  104. };
  105. function toggleFormat(name, value) {
  106. formatter.toggle(name, value ? {value : value} : undefined);
  107. };
  108. function storeSelection(type) {
  109. bookmark = selection.getBookmark(type);
  110. };
  111. function restoreSelection() {
  112. selection.moveToBookmark(bookmark);
  113. };
  114. // Add execCommand overrides
  115. addCommands({
  116. // Ignore these, added for compatibility
  117. 'mceResetDesignMode,mceBeginUndoLevel' : function() {},
  118. // Add undo manager logic
  119. 'mceEndUndoLevel,mceAddUndoLevel' : function() {
  120. editor.undoManager.add();
  121. },
  122. 'Cut,Copy,Paste' : function(command) {
  123. var doc = editor.getDoc(), failed;
  124. // Try executing the native command
  125. try {
  126. execNativeCommand(command);
  127. } catch (ex) {
  128. // Command failed
  129. failed = TRUE;
  130. }
  131. // Present alert message about clipboard access not being available
  132. if (failed || !doc.queryCommandSupported(command)) {
  133. if (tinymce.isGecko) {
  134. editor.windowManager.confirm(editor.getLang('clipboard_msg'), function(state) {
  135. if (state)
  136. open('http://www.mozilla.org/editor/midasdemo/securityprefs.html', '_blank');
  137. });
  138. } else
  139. editor.windowManager.alert(editor.getLang('clipboard_no_support'));
  140. }
  141. },
  142. // Override unlink command
  143. unlink : function(command) {
  144. if (selection.isCollapsed())
  145. selection.select(selection.getNode());
  146. execNativeCommand(command);
  147. selection.collapse(FALSE);
  148. },
  149. // Override justify commands to use the text formatter engine
  150. 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) {
  151. var align = command.substring(7);
  152. // Remove all other alignments first
  153. each('left,center,right,full'.split(','), function(name) {
  154. if (align != name)
  155. formatter.remove('align' + name);
  156. });
  157. toggleFormat('align' + align);
  158. execCommand('mceRepaint');
  159. },
  160. // Override list commands to fix WebKit bug
  161. 'InsertUnorderedList,InsertOrderedList' : function(command) {
  162. var listElm, listParent;
  163. execNativeCommand(command);
  164. // WebKit produces lists within block elements so we need to split them
  165. // we will replace the native list creation logic to custom logic later on
  166. // TODO: Remove this when the list creation logic is removed
  167. listElm = dom.getParent(selection.getNode(), 'ol,ul');
  168. if (listElm) {
  169. listParent = listElm.parentNode;
  170. // If list is within a text block then split that block
  171. if (/^(H[1-6]|P|ADDRESS|PRE)$/.test(listParent.nodeName)) {
  172. storeSelection();
  173. dom.split(listParent, listElm);
  174. restoreSelection();
  175. }
  176. }
  177. },
  178. // Override commands to use the text formatter engine
  179. 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) {
  180. toggleFormat(command);
  181. },
  182. // Override commands to use the text formatter engine
  183. 'ForeColor,HiliteColor,FontName' : function(command, ui, value) {
  184. toggleFormat(command, value);
  185. },
  186. FontSize : function(command, ui, value) {
  187. var fontClasses, fontSizes;
  188. // Convert font size 1-7 to styles
  189. if (value >= 1 && value <= 7) {
  190. fontSizes = tinymce.explode(settings.font_size_style_values);
  191. fontClasses = tinymce.explode(settings.font_size_classes);
  192. if (fontClasses)
  193. value = fontClasses[value - 1] || value;
  194. else
  195. value = fontSizes[value - 1] || value;
  196. }
  197. toggleFormat(command, value);
  198. },
  199. RemoveFormat : function(command) {
  200. formatter.remove(command);
  201. },
  202. mceBlockQuote : function(command) {
  203. toggleFormat('blockquote');
  204. },
  205. FormatBlock : function(command, ui, value) {
  206. return toggleFormat(value || 'p');
  207. },
  208. mceCleanup : function() {
  209. var bookmark = selection.getBookmark();
  210. editor.setContent(editor.getContent({cleanup : TRUE}), {cleanup : TRUE});
  211. selection.moveToBookmark(bookmark);
  212. },
  213. mceRemoveNode : function(command, ui, value) {
  214. var node = value || selection.getNode();
  215. // Make sure that the body node isn't removed
  216. if (node != editor.getBody()) {
  217. storeSelection();
  218. editor.dom.remove(node, TRUE);
  219. restoreSelection();
  220. }
  221. },
  222. mceSelectNodeDepth : function(command, ui, value) {
  223. var counter = 0;
  224. dom.getParent(selection.getNode(), function(node) {
  225. if (node.nodeType == 1 && counter++ == value) {
  226. selection.select(node);
  227. return FALSE;
  228. }
  229. }, editor.getBody());
  230. },
  231. mceSelectNode : function(command, ui, value) {
  232. selection.select(value);
  233. },
  234. mceInsertContent : function(command, ui, value) {
  235. var parser, serializer, parentNode, rootNode, fragment, args,
  236. marker, nodeRect, viewPortRect, rng, node, node2, bookmarkHtml, viewportBodyElement;
  237. // Setup parser and serializer
  238. parser = editor.parser;
  239. serializer = new tinymce.html.Serializer({}, editor.schema);
  240. bookmarkHtml = '<span id="mce_marker" data-mce-type="bookmark">\uFEFF</span>';
  241. // Run beforeSetContent handlers on the HTML to be inserted
  242. args = {content: value, format: 'html'};
  243. selection.onBeforeSetContent.dispatch(selection, args);
  244. value = args.content;
  245. // Add caret at end of contents if it's missing
  246. if (value.indexOf('{$caret}') == -1)
  247. value += '{$caret}';
  248. // Replace the caret marker with a span bookmark element
  249. value = value.replace(/\{\$caret\}/, bookmarkHtml);
  250. // Insert node maker where we will insert the new HTML and get it's parent
  251. if (!selection.isCollapsed())
  252. editor.getDoc().execCommand('Delete', false, null);
  253. parentNode = selection.getNode();
  254. // Parse the fragment within the context of the parent node
  255. args = {context : parentNode.nodeName.toLowerCase()};
  256. fragment = parser.parse(value, args);
  257. // Move the caret to a more suitable location
  258. node = fragment.lastChild;
  259. if (node.attr('id') == 'mce_marker') {
  260. marker = node;
  261. for (node = node.prev; node; node = node.walk(true)) {
  262. if (node.type == 3 || !dom.isBlock(node.name)) {
  263. node.parent.insert(marker, node, node.name === 'br');
  264. break;
  265. }
  266. }
  267. }
  268. // If parser says valid we can insert the contents into that parent
  269. if (!args.invalid) {
  270. value = serializer.serialize(fragment);
  271. // Check if parent is empty or only has one BR element then set the innerHTML of that parent
  272. node = parentNode.firstChild;
  273. node2 = parentNode.lastChild;
  274. if (!node || (node === node2 && node.nodeName === 'BR'))
  275. dom.setHTML(parentNode, value);
  276. else
  277. selection.setContent(value);
  278. } else {
  279. // If the fragment was invalid within that context then we need
  280. // to parse and process the parent it's inserted into
  281. // Insert bookmark node and get the parent
  282. selection.setContent(bookmarkHtml);
  283. parentNode = editor.selection.getNode();
  284. rootNode = editor.getBody();
  285. // Opera will return the document node when selection is in root
  286. if (parentNode.nodeType == 9)
  287. parentNode = node = rootNode;
  288. else
  289. node = parentNode;
  290. // Find the ancestor just before the root element
  291. while (node !== rootNode) {
  292. parentNode = node;
  293. node = node.parentNode;
  294. }
  295. // Get the outer/inner HTML depending on if we are in the root and parser and serialize that
  296. value = parentNode == rootNode ? rootNode.innerHTML : dom.getOuterHTML(parentNode);
  297. value = serializer.serialize(
  298. parser.parse(
  299. // Need to replace by using a function since $ in the contents would otherwise be a problem
  300. value.replace(/<span (id="mce_marker"|id=mce_marker).+?<\/span>/i, function() {
  301. return serializer.serialize(fragment);
  302. })
  303. )
  304. );
  305. // Set the inner/outer HTML depending on if we are in the root or not
  306. if (parentNode == rootNode)
  307. dom.setHTML(rootNode, value);
  308. else
  309. dom.setOuterHTML(parentNode, value);
  310. }
  311. marker = dom.get('mce_marker');
  312. // Scroll range into view scrollIntoView on element can't be used since it will scroll the main view port as well
  313. nodeRect = dom.getRect(marker);
  314. viewPortRect = dom.getViewPort(editor.getWin());
  315. // Check if node is out side the viewport if it is then scroll to it
  316. if ((nodeRect.y + nodeRect.h > viewPortRect.y + viewPortRect.h || nodeRect.y < viewPortRect.y) ||
  317. (nodeRect.x > viewPortRect.x + viewPortRect.w || nodeRect.x < viewPortRect.x)) {
  318. viewportBodyElement = tinymce.isIE ? editor.getDoc().documentElement : editor.getBody();
  319. viewportBodyElement.scrollLeft = nodeRect.x;
  320. viewportBodyElement.scrollTop = nodeRect.y - viewPortRect.h + 25;
  321. }
  322. // Move selection before marker and remove it
  323. rng = dom.createRng();
  324. // If previous sibling is a text node set the selection to the end of that node
  325. node = marker.previousSibling;
  326. if (node && node.nodeType == 3) {
  327. rng.setStart(node, node.nodeValue.length);
  328. } else {
  329. // If the previous sibling isn't a text node or doesn't exist set the selection before the marker node
  330. rng.setStartBefore(marker);
  331. rng.setEndBefore(marker);
  332. }
  333. // Remove the marker node and set the new range
  334. dom.remove(marker);
  335. selection.setRng(rng);
  336. // Dispatch after event and add any visual elements needed
  337. selection.onSetContent.dispatch(selection, args);
  338. editor.addVisual();
  339. },
  340. mceInsertRawHTML : function(command, ui, value) {
  341. selection.setContent('tiny_mce_marker');
  342. editor.setContent(editor.getContent().replace(/tiny_mce_marker/g, function() { return value }));
  343. },
  344. mceSetContent : function(command, ui, value) {
  345. editor.setContent(value);
  346. },
  347. 'Indent,Outdent' : function(command) {
  348. var intentValue, indentUnit, value;
  349. // Setup indent level
  350. intentValue = settings.indentation;
  351. indentUnit = /[a-z%]+$/i.exec(intentValue);
  352. intentValue = parseInt(intentValue);
  353. if (!queryCommandState('InsertUnorderedList') && !queryCommandState('InsertOrderedList')) {
  354. each(selection.getSelectedBlocks(), function(element) {
  355. if (command == 'outdent') {
  356. value = Math.max(0, parseInt(element.style.paddingLeft || 0) - intentValue);
  357. dom.setStyle(element, 'paddingLeft', value ? value + indentUnit : '');
  358. } else
  359. dom.setStyle(element, 'paddingLeft', (parseInt(element.style.paddingLeft || 0) + intentValue) + indentUnit);
  360. });
  361. } else
  362. execNativeCommand(command);
  363. },
  364. mceRepaint : function() {
  365. var bookmark;
  366. if (tinymce.isGecko) {
  367. try {
  368. storeSelection(TRUE);
  369. if (selection.getSel())
  370. selection.getSel().selectAllChildren(editor.getBody());
  371. selection.collapse(TRUE);
  372. restoreSelection();
  373. } catch (ex) {
  374. // Ignore
  375. }
  376. }
  377. },
  378. mceToggleFormat : function(command, ui, value) {
  379. formatter.toggle(value);
  380. },
  381. InsertHorizontalRule : function() {
  382. editor.execCommand('mceInsertContent', false, '<hr />');
  383. },
  384. mceToggleVisualAid : function() {
  385. editor.hasVisual = !editor.hasVisual;
  386. editor.addVisual();
  387. },
  388. mceReplaceContent : function(command, ui, value) {
  389. editor.execCommand('mceInsertContent', false, value.replace(/\{\$selection\}/g, selection.getContent({format : 'text'})));
  390. },
  391. mceInsertLink : function(command, ui, value) {
  392. var anchor;
  393. if (typeof(value) == 'string')
  394. value = {href : value};
  395. anchor = dom.getParent(selection.getNode(), 'a');
  396. // Spaces are never valid in URLs and it's a very common mistake for people to make so we fix it here.
  397. value.href = value.href.replace(' ', '%20');
  398. // Remove existing links if there could be child links or that the href isn't specified
  399. if (!anchor || !value.href) {
  400. formatter.remove('link');
  401. }
  402. // Apply new link to selection
  403. if (value.href) {
  404. formatter.apply('link', value, anchor);
  405. }
  406. },
  407. selectAll : function() {
  408. var root = dom.getRoot(), rng = dom.createRng();
  409. rng.setStart(root, 0);
  410. rng.setEnd(root, root.childNodes.length);
  411. editor.selection.setRng(rng);
  412. }
  413. });
  414. // Add queryCommandState overrides
  415. addCommands({
  416. // Override justify commands
  417. 'JustifyLeft,JustifyCenter,JustifyRight,JustifyFull' : function(command) {
  418. return isFormatMatch('align' + command.substring(7));
  419. },
  420. 'Bold,Italic,Underline,Strikethrough,Superscript,Subscript' : function(command) {
  421. return isFormatMatch(command);
  422. },
  423. mceBlockQuote : function() {
  424. return isFormatMatch('blockquote');
  425. },
  426. Outdent : function() {
  427. var node;
  428. if (settings.inline_styles) {
  429. if ((node = dom.getParent(selection.getStart(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0)
  430. return TRUE;
  431. if ((node = dom.getParent(selection.getEnd(), dom.isBlock)) && parseInt(node.style.paddingLeft) > 0)
  432. return TRUE;
  433. }
  434. return queryCommandState('InsertUnorderedList') || queryCommandState('InsertOrderedList') || (!settings.inline_styles && !!dom.getParent(selection.getNode(), 'BLOCKQUOTE'));
  435. },
  436. 'InsertUnorderedList,InsertOrderedList' : function(command) {
  437. return dom.getParent(selection.getNode(), command == 'insertunorderedlist' ? 'UL' : 'OL');
  438. }
  439. }, 'state');
  440. // Add queryCommandValue overrides
  441. addCommands({
  442. 'FontSize,FontName' : function(command) {
  443. var value = 0, parent;
  444. if (parent = dom.getParent(selection.getNode(), 'span')) {
  445. if (command == 'fontsize')
  446. value = parent.style.fontSize;
  447. else
  448. value = parent.style.fontFamily.replace(/, /g, ',').replace(/[\'\"]/g, '').toLowerCase();
  449. }
  450. return value;
  451. }
  452. }, 'value');
  453. // Add undo manager logic
  454. if (settings.custom_undo_redo) {
  455. addCommands({
  456. Undo : function() {
  457. editor.undoManager.undo();
  458. },
  459. Redo : function() {
  460. editor.undoManager.redo();
  461. }
  462. });
  463. }
  464. };
  465. })(tinymce);