editor_plugin.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. /*
  2. * TinyMCE Writing Improvement Tool Plugin
  3. * Author: Raphael Mudge (raffi@automattic.com)
  4. *
  5. * http://www.afterthedeadline.com
  6. *
  7. * Distributed under the LGPL
  8. *
  9. * Derived from:
  10. * $Id: editor_plugin_src.js 425 2007-11-21 15:17:39Z spocke $
  11. *
  12. * @author Moxiecode
  13. * @copyright Copyright (C) 2004-2008, Moxiecode Systems AB, All rights reserved.
  14. *
  15. * Moxiecode Spell Checker plugin released under the LGPL with TinyMCE
  16. */
  17. /* jshint onevar: false, sub: true, devel: true, loopfunc: true */
  18. /* global tinymce, AtDCore, AtD_proofread_click_count */
  19. (function()
  20. {
  21. var each = tinymce.each, DOM = tinymce.DOM, core;
  22. function getLang( key, defaultStr ) {
  23. return ( window.AtD_l10n_r0ar && window.AtD_l10n_r0ar[key] ) || defaultStr;
  24. }
  25. tinymce.create('tinymce.plugins.AfterTheDeadlinePlugin',
  26. {
  27. getInfo : function()
  28. {
  29. return {
  30. longname : 'After The Deadline',
  31. author : 'Raphael Mudge',
  32. authorurl : 'http://blog.afterthedeadline.com',
  33. infourl : 'http://www.afterthedeadline.com',
  34. version : tinymce.majorVersion + '.' + tinymce.minorVersion
  35. };
  36. },
  37. /* initializes the functions used by the AtD Core UI Module */
  38. initAtDCore : function(editor/*, plugin*/)
  39. {
  40. var core = new AtDCore();
  41. core.map = each;
  42. core.getAttrib = function(node, key)
  43. {
  44. return editor.dom.getAttrib(node, key);
  45. };
  46. core.findSpans = function(parent)
  47. {
  48. if (!parent) {
  49. return editor.dom.select('span');
  50. } else {
  51. return editor.dom.select('span', parent);
  52. }
  53. };
  54. core.hasClass = function(node, className)
  55. {
  56. return editor.dom.hasClass(node, className);
  57. };
  58. core.contents = function(node)
  59. {
  60. return node.childNodes;
  61. };
  62. core.replaceWith = function(old_node, new_node)
  63. {
  64. return editor.dom.replace(new_node, old_node);
  65. };
  66. core.create = function(node_html)
  67. {
  68. return editor.dom.create('span', { 'class': 'mceItemHidden' }, node_html);
  69. };
  70. core.removeParent = function(node)
  71. {
  72. editor.dom.remove(node, 1);
  73. return node;
  74. };
  75. core.remove = function(node)
  76. {
  77. editor.dom.remove(node);
  78. };
  79. core.setIgnoreStrings(editor.getParam('atd_ignore_strings', [] ).join(','));
  80. core.showTypes(editor.getParam('atd_show_types', ''));
  81. return core;
  82. },
  83. /* called when the plugin is initialized */
  84. init : function(ed, url)
  85. {
  86. if ( typeof(AtDCore) === 'undefined' ) {
  87. return;
  88. }
  89. var plugin = this;
  90. var editor = ed;
  91. this.url = url;
  92. this.editor = ed;
  93. core = ed.core = this.initAtDCore(editor, plugin);
  94. /* look at the atd_ignore variable and put that stuff into a hash */
  95. var ignore = tinymce.util.Cookie.getHash('atd_ignore');
  96. if (!ignore) {
  97. ignore = {};
  98. }
  99. /* add a command to request a document check and process the results. */
  100. editor.addCommand('mceWritingImprovementTool', function(callback)
  101. {
  102. /* checks if a global var for click stats exists and increments it if it does... */
  103. if (typeof AtD_proofread_click_count !== 'undefined') {
  104. AtD_proofread_click_count++;
  105. }
  106. /* create the nifty spinny thing that says "hizzo, I'm doing something fo realz" */
  107. plugin.editor.setProgressState(1);
  108. /* remove the previous errors */
  109. plugin._removeWords();
  110. /* send request to our service */
  111. plugin.sendRequest('checkDocument', ed.getContent({ format: 'raw' }), function(data, request/*, someObject*/)
  112. {
  113. /* turn off the spinning thingie */
  114. plugin.editor.setProgressState(0);
  115. /* if the server is not accepting requests, let the user know */
  116. if ( request.status !== 200 || request.responseText.substr(1, 4) === 'html' || !request.responseXML )
  117. {
  118. ed.windowManager.alert(
  119. getLang( 'message_server_error', 'There was a problem communicating with the Proofreading service. Try again in one minute.' ),
  120. callback ? function() { callback( 0 ); } : function() {}
  121. );
  122. return;
  123. }
  124. /* check to see if things are broken first and foremost */
  125. if (request.responseXML.getElementsByTagName('message').item(0) != null)
  126. {
  127. ed.windowManager.alert(
  128. request.responseXML.getElementsByTagName('message').item(0).firstChild.data,
  129. callback ? function() { callback( 0 ); } : function() {}
  130. );
  131. return;
  132. }
  133. var results = core.processXML(request.responseXML);
  134. var ecount = 0;
  135. if (results.count > 0)
  136. {
  137. ecount = plugin.markMyWords(results.errors);
  138. ed.suggestions = results.suggestions;
  139. }
  140. if (ecount === 0 && (!callback || callback === undefined)) {
  141. ed.windowManager.alert( getLang('message_no_errors_found', 'No writing errors were found.') );
  142. } else if (callback) {
  143. callback(ecount);
  144. }
  145. });
  146. });
  147. /* load cascading style sheet for this plugin */
  148. editor.onInit.add(function()
  149. {
  150. /* loading the content.css file, why? I have no clue */
  151. if (editor.settings.content_css !== false)
  152. {
  153. editor.dom.loadCSS(editor.getParam('atd_css_url', url + '/css/content.css'));
  154. }
  155. });
  156. /* again showing a menu, I have no clue what */
  157. editor.onClick.add(plugin._showMenu, plugin);
  158. /* we're showing some sort of menu, no idea what */
  159. editor.onContextMenu.add(plugin._showMenu, plugin);
  160. /* strip out the markup before the contents is serialized (and do it on a copy of the markup so we don't affect the user experience) */
  161. editor.onPreProcess.add(function(sender, object)
  162. {
  163. var dom = sender.dom;
  164. each(dom.select('span', object.node).reverse(), function(n)
  165. {
  166. if (n && (dom.hasClass(n, 'hiddenGrammarError') || dom.hasClass(n, 'hiddenSpellError') || dom.hasClass(n, 'hiddenSuggestion') || dom.hasClass(n, 'mceItemHidden') || (!dom.getAttrib(n, 'class') && !dom.getAttrib(n, 'style') && !dom.getAttrib(n, 'id') && !dom.hasClass(n, 'Apple-style-span') && !dom.getAttrib(n, 'mce_name'))))
  167. {
  168. dom.remove(n, 1);
  169. }
  170. });
  171. });
  172. /* cleanup the HTML before executing certain commands */
  173. editor.onBeforeExecCommand.add(function(editor, command)
  174. {
  175. if (command === 'mceCodeEditor')
  176. {
  177. plugin._removeWords();
  178. }
  179. else if (command === 'mceFullScreen')
  180. {
  181. plugin._done();
  182. }
  183. });
  184. ed.addButton('AtD', {
  185. title: getLang( 'button_proofread_tooltip', 'Proofread Writing' ),
  186. image: ed.getParam('atd_button_url', url + '/atdbuttontr.gif'),
  187. cmd: 'mceWritingImprovementTool'
  188. });
  189. },
  190. _removeWords : function(w)
  191. {
  192. var ed = this.editor, dom = ed.dom, se = ed.selection, b = se.getBookmark();
  193. ed.core.removeWords(undefined, w);
  194. /* force a rebuild of the DOM... even though the right elements are stripped, the DOM is still organized
  195. as if the span were there and this breaks my code */
  196. dom.setHTML(dom.getRoot(), dom.getRoot().innerHTML);
  197. se.moveToBookmark(b);
  198. },
  199. markMyWords : function(errors)
  200. {
  201. var ed = this.editor;
  202. var se = ed.selection, b = se.getBookmark();
  203. var ecount = ed.core.markMyWords(ed.core.contents(this.editor.getBody()), errors);
  204. se.moveToBookmark(b);
  205. return ecount;
  206. },
  207. _showMenu : function(ed, e)
  208. {
  209. var t = this;
  210. ed = t.editor; // not clear why this overwrites the function parameter
  211. var m = t._menu, p1, dom = ed.dom, vp = dom.getViewPort(ed.getWin());
  212. if (!m)
  213. {
  214. p1 = DOM.getPos(ed.getContentAreaContainer());
  215. //p2 = DOM.getPos(ed.getContainer());
  216. m = ed.controlManager.createDropMenu('spellcheckermenu',
  217. {
  218. offset_x : p1.x,
  219. offset_y : p1.y,
  220. 'class' : 'mceNoIcons'
  221. });
  222. t._menu = m;
  223. }
  224. if (ed.core.isMarkedNode(e.target))
  225. {
  226. /* remove these other lame-o elements */
  227. m.removeAll();
  228. /* find the correct suggestions object */
  229. var errorDescription = ed.core.findSuggestion(e.target);
  230. if (!errorDescription)
  231. {
  232. m.add({ title: getLang( 'menu_title_no_suggestions', 'No suggestions' ), 'class': 'mceMenuItemTitle' }).setDisabled(1);
  233. }
  234. else if (errorDescription['suggestions'].length === 0)
  235. {
  236. m.add({ title: errorDescription['description'], 'class' : 'mceMenuItemTitle' }).setDisabled(1);
  237. }
  238. else
  239. {
  240. m.add({ title : errorDescription['description'], 'class' : 'mceMenuItemTitle' }).setDisabled(1);
  241. for (var i = 0; i < errorDescription['suggestions'].length; i++)
  242. {
  243. (function(sugg)
  244. {
  245. m.add({
  246. title : sugg,
  247. onclick : function()
  248. {
  249. ed.core.applySuggestion(e.target, sugg);
  250. t._checkDone();
  251. }
  252. });
  253. })(errorDescription['suggestions'][i]); // jshint ignore:line
  254. }
  255. m.addSeparator();
  256. }
  257. if (errorDescription && errorDescription['moreinfo'])
  258. {
  259. (function(url)
  260. {
  261. m.add({
  262. title : getLang( 'menu_option_explain', 'Explain...' ),
  263. onclick : function()
  264. {
  265. ed.windowManager.open({
  266. url : url,
  267. width : 480,
  268. height : 380,
  269. inline : true
  270. }, { theme_url : this.url });
  271. }
  272. });
  273. })(errorDescription['moreinfo']);
  274. m.addSeparator();
  275. }
  276. m.add({
  277. title : getLang( 'menu_option_ignore_once', 'Ignore suggestion' ),
  278. onclick : function()
  279. {
  280. dom.remove(e.target, 1);
  281. t._checkDone();
  282. }
  283. });
  284. if (String(this.editor.getParam('atd_ignore_enable', 'false')) === 'true')
  285. {
  286. m.add({
  287. title : getLang( 'menu_option_ignore_always', 'Ignore always' ),
  288. onclick : function()
  289. {
  290. var url = t.editor.getParam('atd_ignore_rpc_url', '{backend}');
  291. if (url === '{backend}')
  292. {
  293. /* Default scheme is to save ignore preferences in a cookie */
  294. var ignore = tinymce.util.Cookie.getHash('atd_ignore');
  295. if (!ignore) { ignore = {}; }
  296. ignore[e.target.innerHTML] = 1;
  297. tinymce.util.Cookie.setHash('atd_ignore', ignore, new Date( (new Date().getTime()) + 157680000000) );
  298. }
  299. else
  300. {
  301. /* Plugin is configured to send ignore preferences to server, do that */
  302. var id = t.editor.getParam('atd_rpc_id', '12345678');
  303. tinymce.util.XHR.send({
  304. url : url + encodeURI(e.target.innerHTML).replace(/&/g, '%26') + '&key=' + id,
  305. content_type : 'text/xml',
  306. async : true,
  307. type : 'GET',
  308. success : function(/* type, req, o */)
  309. {
  310. /* do nothing */
  311. },
  312. error : function( type, req, o )
  313. {
  314. alert( 'Ignore preference save failed\n' + type + '\n' + req.status + '\nAt: ' + o.url );
  315. }
  316. });
  317. /* update atd_ignore_strings with the new value */
  318. t.editor.core.setIgnoreStrings(e.target.innerHTML); /* this does an update */
  319. }
  320. t._removeWords(e.target.innerHTML);
  321. t._checkDone();
  322. }
  323. });
  324. }
  325. else
  326. {
  327. m.add({
  328. title : getLang( 'menu_option_ignore_all', 'Ignore all' ),
  329. onclick : function()
  330. {
  331. t._removeWords(e.target.innerHTML);
  332. t._checkDone();
  333. }
  334. });
  335. }
  336. /* show the menu please */
  337. ed.selection.select(e.target);
  338. p1 = dom.getPos(e.target);
  339. m.showMenu(p1.x, p1.y + e.target.offsetHeight - vp.y);
  340. return tinymce.dom.Event.cancel(e);
  341. }
  342. else
  343. {
  344. m.hideMenu();
  345. }
  346. },
  347. /* loop through editor DOM, call _done if no mce tags exist. */
  348. _checkDone : function()
  349. {
  350. var t = this, ed = t.editor, dom = ed.dom, o;
  351. each(dom.select('span'), function(n)
  352. {
  353. if (n && dom.hasClass(n, 'mceItemHidden'))
  354. {
  355. o = true;
  356. return false;
  357. }
  358. });
  359. if (!o)
  360. {
  361. t._done();
  362. }
  363. },
  364. /* remove all tags, hide the menu, and fire a dom change event */
  365. _done : function()
  366. {
  367. var plugin = this;
  368. plugin._removeWords();
  369. if (plugin._menu)
  370. {
  371. plugin._menu.hideMenu();
  372. }
  373. plugin.editor.nodeChanged();
  374. },
  375. sendRequest : function(file, data, success)
  376. {
  377. var id = this.editor.getParam('atd_rpc_id', '12345678');
  378. var url = this.editor.getParam('atd_rpc_url', '{backend}');
  379. var plugin = this;
  380. if (url === '{backend}' || id === '12345678')
  381. {
  382. this.editor.setProgressState(0);
  383. alert('Please specify: atd_rpc_url and atd_rpc_id');
  384. return;
  385. }
  386. tinymce.util.XHR.send({
  387. url : url + '/' + file,
  388. content_type : 'text/xml',
  389. type : 'POST',
  390. data : 'data=' + encodeURI(data).replace(/&/g, '%26') + '&key=' + id,
  391. async : true,
  392. success : success,
  393. error : function( type, req, o )
  394. {
  395. plugin.editor.setProgressState(0);
  396. alert( type + '\n' + req.status + '\nAt: ' + o.url );
  397. }
  398. });
  399. }
  400. });
  401. // Register plugin
  402. tinymce.PluginManager.add('AtD', tinymce.plugins.AfterTheDeadlinePlugin);
  403. })();