/modules/mod_base/lib/js/modules/tinymce3.4.3.2/plugins/spellchecker/editor_plugin_src.js

http://github.com/zotonic/zotonic · JavaScript · 434 lines · 385 code · 32 blank · 17 comment · 28 complexity · 17febdebb1a6a3037f104dafe49f3860 MD5 · raw file

  1. /**
  2. * editor_plugin_src.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() {
  11. var JSONRequest = tinymce.util.JSONRequest, each = tinymce.each, DOM = tinymce.DOM;
  12. tinymce.create('tinymce.plugins.SpellcheckerPlugin', {
  13. getInfo : function() {
  14. return {
  15. longname : 'Spellchecker',
  16. author : 'Moxiecode Systems AB',
  17. authorurl : 'http://tinymce.moxiecode.com',
  18. infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/spellchecker',
  19. version : tinymce.majorVersion + "." + tinymce.minorVersion
  20. };
  21. },
  22. init : function(ed, url) {
  23. var t = this, cm;
  24. t.url = url;
  25. t.editor = ed;
  26. t.rpcUrl = ed.getParam("spellchecker_rpc_url", "{backend}");
  27. if (t.rpcUrl == '{backend}') {
  28. // Sniff if the browser supports native spellchecking (Don't know of a better way)
  29. if (tinymce.isIE)
  30. return;
  31. t.hasSupport = true;
  32. // Disable the context menu when spellchecking is active
  33. ed.onContextMenu.addToTop(function(ed, e) {
  34. if (t.active)
  35. return false;
  36. });
  37. }
  38. // Register commands
  39. ed.addCommand('mceSpellCheck', function() {
  40. if (t.rpcUrl == '{backend}') {
  41. // Enable/disable native spellchecker
  42. t.editor.getBody().spellcheck = t.active = !t.active;
  43. return;
  44. }
  45. if (!t.active) {
  46. ed.setProgressState(1);
  47. t._sendRPC('checkWords', [t.selectedLang, t._getWords()], function(r) {
  48. if (r.length > 0) {
  49. t.active = 1;
  50. t._markWords(r);
  51. ed.setProgressState(0);
  52. ed.nodeChanged();
  53. } else {
  54. ed.setProgressState(0);
  55. if (ed.getParam('spellchecker_report_no_misspellings', true))
  56. ed.windowManager.alert('spellchecker.no_mpell');
  57. }
  58. });
  59. } else
  60. t._done();
  61. });
  62. if (ed.settings.content_css !== false)
  63. ed.contentCSS.push(url + '/css/content.css');
  64. ed.onClick.add(t._showMenu, t);
  65. ed.onContextMenu.add(t._showMenu, t);
  66. ed.onBeforeGetContent.add(function() {
  67. if (t.active)
  68. t._removeWords();
  69. });
  70. ed.onNodeChange.add(function(ed, cm) {
  71. cm.setActive('spellchecker', t.active);
  72. });
  73. ed.onSetContent.add(function() {
  74. t._done();
  75. });
  76. ed.onBeforeGetContent.add(function() {
  77. t._done();
  78. });
  79. ed.onBeforeExecCommand.add(function(ed, cmd) {
  80. if (cmd == 'mceFullScreen')
  81. t._done();
  82. });
  83. // Find selected language
  84. t.languages = {};
  85. each(ed.getParam('spellchecker_languages', '+English=en,Danish=da,Dutch=nl,Finnish=fi,French=fr,German=de,Italian=it,Polish=pl,Portuguese=pt,Spanish=es,Swedish=sv', 'hash'), function(v, k) {
  86. if (k.indexOf('+') === 0) {
  87. k = k.substring(1);
  88. t.selectedLang = v;
  89. }
  90. t.languages[k] = v;
  91. });
  92. },
  93. createControl : function(n, cm) {
  94. var t = this, c, ed = t.editor;
  95. if (n == 'spellchecker') {
  96. // Use basic button if we use the native spellchecker
  97. if (t.rpcUrl == '{backend}') {
  98. // Create simple toggle button if we have native support
  99. if (t.hasSupport)
  100. c = cm.createButton(n, {title : 'spellchecker.desc', cmd : 'mceSpellCheck', scope : t});
  101. return c;
  102. }
  103. c = cm.createSplitButton(n, {title : 'spellchecker.desc', cmd : 'mceSpellCheck', scope : t});
  104. c.onRenderMenu.add(function(c, m) {
  105. m.add({title : 'spellchecker.langs', 'class' : 'mceMenuItemTitle'}).setDisabled(1);
  106. each(t.languages, function(v, k) {
  107. var o = {icon : 1}, mi;
  108. o.onclick = function() {
  109. if (v == t.selectedLang) {
  110. return;
  111. }
  112. mi.setSelected(1);
  113. t.selectedItem.setSelected(0);
  114. t.selectedItem = mi;
  115. t.selectedLang = v;
  116. };
  117. o.title = k;
  118. mi = m.add(o);
  119. mi.setSelected(v == t.selectedLang);
  120. if (v == t.selectedLang)
  121. t.selectedItem = mi;
  122. })
  123. });
  124. return c;
  125. }
  126. },
  127. // Internal functions
  128. _walk : function(n, f) {
  129. var d = this.editor.getDoc(), w;
  130. if (d.createTreeWalker) {
  131. w = d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, false);
  132. while ((n = w.nextNode()) != null)
  133. f.call(this, n);
  134. } else
  135. tinymce.walk(n, f, 'childNodes');
  136. },
  137. _getSeparators : function() {
  138. var re = '', i, str = this.editor.getParam('spellchecker_word_separator_chars', '\\s!"#$%&()*+,-./:;<=>?@[\]^_{|}§ŠŤŽąśˇ¸ťź˝žż×÷¤\u201d\u201c');
  139. // Build word separator regexp
  140. for (i=0; i<str.length; i++)
  141. re += '\\' + str.charAt(i);
  142. return re;
  143. },
  144. _getWords : function() {
  145. var ed = this.editor, wl = [], tx = '', lo = {}, rawWords = [];
  146. // Get area text
  147. this._walk(ed.getBody(), function(n) {
  148. if (n.nodeType == 3)
  149. tx += n.nodeValue + ' ';
  150. });
  151. // split the text up into individual words
  152. if (ed.getParam('spellchecker_word_pattern')) {
  153. // look for words that match the pattern
  154. rawWords = tx.match('(' + ed.getParam('spellchecker_word_pattern') + ')', 'gi');
  155. } else {
  156. // Split words by separator
  157. tx = tx.replace(new RegExp('([0-9]|[' + this._getSeparators() + '])', 'g'), ' ');
  158. tx = tinymce.trim(tx.replace(/(\s+)/g, ' '));
  159. rawWords = tx.split(' ');
  160. }
  161. // Build word array and remove duplicates
  162. each(rawWords, function(v) {
  163. if (!lo[v]) {
  164. wl.push(v);
  165. lo[v] = 1;
  166. }
  167. });
  168. return wl;
  169. },
  170. _removeWords : function(w) {
  171. var ed = this.editor, dom = ed.dom, se = ed.selection, b = se.getBookmark();
  172. each(dom.select('span').reverse(), function(n) {
  173. if (n && (dom.hasClass(n, 'mceItemHiddenSpellWord') || dom.hasClass(n, 'mceItemHidden'))) {
  174. if (!w || dom.decode(n.innerHTML) == w)
  175. dom.remove(n, 1);
  176. }
  177. });
  178. se.moveToBookmark(b);
  179. },
  180. _markWords : function(wl) {
  181. var ed = this.editor, dom = ed.dom, doc = ed.getDoc(), se = ed.selection, b = se.getBookmark(), nl = [],
  182. w = wl.join('|'), re = this._getSeparators(), rx = new RegExp('(^|[' + re + '])(' + w + ')(?=[' + re + ']|$)', 'g');
  183. // Collect all text nodes
  184. this._walk(ed.getBody(), function(n) {
  185. if (n.nodeType == 3) {
  186. nl.push(n);
  187. }
  188. });
  189. // Wrap incorrect words in spans
  190. each(nl, function(n) {
  191. var node, elem, txt, pos, v = n.nodeValue;
  192. if (rx.test(v)) {
  193. // Encode the content
  194. v = dom.encode(v);
  195. // Create container element
  196. elem = dom.create('span', {'class' : 'mceItemHidden'});
  197. // Following code fixes IE issues by creating text nodes
  198. // using DOM methods instead of innerHTML.
  199. // Bug #3124: <PRE> elements content is broken after spellchecking.
  200. // Bug #1408: Preceding whitespace characters are removed
  201. // @TODO: I'm not sure that both are still issues on IE9.
  202. if (tinymce.isIE) {
  203. // Enclose mispelled words with temporal tag
  204. v = v.replace(rx, '$1<mcespell>$2</mcespell>');
  205. // Loop over the content finding mispelled words
  206. while ((pos = v.indexOf('<mcespell>')) != -1) {
  207. // Add text node for the content before the word
  208. txt = v.substring(0, pos);
  209. if (txt.length) {
  210. node = doc.createTextNode(dom.decode(txt));
  211. elem.appendChild(node);
  212. }
  213. v = v.substring(pos+10);
  214. pos = v.indexOf('</mcespell>');
  215. txt = v.substring(0, pos);
  216. v = v.substring(pos+11);
  217. // Add span element for the word
  218. elem.appendChild(dom.create('span', {'class' : 'mceItemHiddenSpellWord'}, txt));
  219. }
  220. // Add text node for the rest of the content
  221. if (v.length) {
  222. node = doc.createTextNode(dom.decode(v));
  223. elem.appendChild(node);
  224. }
  225. } else {
  226. // Other browsers preserve whitespace characters on innerHTML usage
  227. elem.innerHTML = v.replace(rx, '$1<span class="mceItemHiddenSpellWord">$2</span>');
  228. }
  229. // Finally, replace the node with the container
  230. dom.replace(elem, n);
  231. }
  232. });
  233. se.moveToBookmark(b);
  234. },
  235. _showMenu : function(ed, e) {
  236. var t = this, ed = t.editor, m = t._menu, p1, dom = ed.dom, vp = dom.getViewPort(ed.getWin()), wordSpan = e.target;
  237. e = 0; // Fixes IE memory leak
  238. if (!m) {
  239. m = ed.controlManager.createDropMenu('spellcheckermenu', {'class' : 'mceNoIcons'});
  240. t._menu = m;
  241. }
  242. if (dom.hasClass(wordSpan, 'mceItemHiddenSpellWord')) {
  243. m.removeAll();
  244. m.add({title : 'spellchecker.wait', 'class' : 'mceMenuItemTitle'}).setDisabled(1);
  245. t._sendRPC('getSuggestions', [t.selectedLang, dom.decode(wordSpan.innerHTML)], function(r) {
  246. var ignoreRpc;
  247. m.removeAll();
  248. if (r.length > 0) {
  249. m.add({title : 'spellchecker.sug', 'class' : 'mceMenuItemTitle'}).setDisabled(1);
  250. each(r, function(v) {
  251. m.add({title : v, onclick : function() {
  252. dom.replace(ed.getDoc().createTextNode(v), wordSpan);
  253. t._checkDone();
  254. }});
  255. });
  256. m.addSeparator();
  257. } else
  258. m.add({title : 'spellchecker.no_sug', 'class' : 'mceMenuItemTitle'}).setDisabled(1);
  259. ignoreRpc = t.editor.getParam("spellchecker_enable_ignore_rpc", '');
  260. m.add({
  261. title : 'spellchecker.ignore_word',
  262. onclick : function() {
  263. var word = wordSpan.innerHTML;
  264. dom.remove(wordSpan, 1);
  265. t._checkDone();
  266. // tell the server if we need to
  267. if (ignoreRpc) {
  268. ed.setProgressState(1);
  269. t._sendRPC('ignoreWord', [t.selectedLang, word], function(r) {
  270. ed.setProgressState(0);
  271. });
  272. }
  273. }
  274. });
  275. m.add({
  276. title : 'spellchecker.ignore_words',
  277. onclick : function() {
  278. var word = wordSpan.innerHTML;
  279. t._removeWords(dom.decode(word));
  280. t._checkDone();
  281. // tell the server if we need to
  282. if (ignoreRpc) {
  283. ed.setProgressState(1);
  284. t._sendRPC('ignoreWords', [t.selectedLang, word], function(r) {
  285. ed.setProgressState(0);
  286. });
  287. }
  288. }
  289. });
  290. if (t.editor.getParam("spellchecker_enable_learn_rpc")) {
  291. m.add({
  292. title : 'spellchecker.learn_word',
  293. onclick : function() {
  294. var word = wordSpan.innerHTML;
  295. dom.remove(wordSpan, 1);
  296. t._checkDone();
  297. ed.setProgressState(1);
  298. t._sendRPC('learnWord', [t.selectedLang, word], function(r) {
  299. ed.setProgressState(0);
  300. });
  301. }
  302. });
  303. }
  304. m.update();
  305. });
  306. p1 = DOM.getPos(ed.getContentAreaContainer());
  307. m.settings.offset_x = p1.x;
  308. m.settings.offset_y = p1.y;
  309. ed.selection.select(wordSpan);
  310. p1 = dom.getPos(wordSpan);
  311. m.showMenu(p1.x, p1.y + wordSpan.offsetHeight - vp.y);
  312. return tinymce.dom.Event.cancel(e);
  313. } else
  314. m.hideMenu();
  315. },
  316. _checkDone : function() {
  317. var t = this, ed = t.editor, dom = ed.dom, o;
  318. each(dom.select('span'), function(n) {
  319. if (n && dom.hasClass(n, 'mceItemHiddenSpellWord')) {
  320. o = true;
  321. return false;
  322. }
  323. });
  324. if (!o)
  325. t._done();
  326. },
  327. _done : function() {
  328. var t = this, la = t.active;
  329. if (t.active) {
  330. t.active = 0;
  331. t._removeWords();
  332. if (t._menu)
  333. t._menu.hideMenu();
  334. if (la)
  335. t.editor.nodeChanged();
  336. }
  337. },
  338. _sendRPC : function(m, p, cb) {
  339. var t = this;
  340. JSONRequest.sendRPC({
  341. url : t.rpcUrl,
  342. method : m,
  343. params : p,
  344. success : cb,
  345. error : function(e, x) {
  346. t.editor.setProgressState(0);
  347. t.editor.windowManager.alert(e.errstr || ('Error response: ' + x.responseText));
  348. }
  349. });
  350. }
  351. });
  352. // Register plugin
  353. tinymce.PluginManager.add('spellchecker', tinymce.plugins.SpellcheckerPlugin);
  354. })();