editor_plugin_src.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  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, r = se.getRng(true);
  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.setRng(r);
  179. },
  180. _markWords : function(wl) {
  181. var ed = this.editor, dom = ed.dom, doc = ed.getDoc(), se = ed.selection, r = se.getRng(true), 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.setRng(r);
  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. if (ed.getParam('show_ignore_words', true)) {
  260. ignoreRpc = t.editor.getParam("spellchecker_enable_ignore_rpc", '');
  261. m.add({
  262. title : 'spellchecker.ignore_word',
  263. onclick : function() {
  264. var word = wordSpan.innerHTML;
  265. dom.remove(wordSpan, 1);
  266. t._checkDone();
  267. // tell the server if we need to
  268. if (ignoreRpc) {
  269. ed.setProgressState(1);
  270. t._sendRPC('ignoreWord', [t.selectedLang, word], function(r) {
  271. ed.setProgressState(0);
  272. });
  273. }
  274. }
  275. });
  276. m.add({
  277. title : 'spellchecker.ignore_words',
  278. onclick : function() {
  279. var word = wordSpan.innerHTML;
  280. t._removeWords(dom.decode(word));
  281. t._checkDone();
  282. // tell the server if we need to
  283. if (ignoreRpc) {
  284. ed.setProgressState(1);
  285. t._sendRPC('ignoreWords', [t.selectedLang, word], function(r) {
  286. ed.setProgressState(0);
  287. });
  288. }
  289. }
  290. });
  291. }
  292. if (t.editor.getParam("spellchecker_enable_learn_rpc")) {
  293. m.add({
  294. title : 'spellchecker.learn_word',
  295. onclick : function() {
  296. var word = wordSpan.innerHTML;
  297. dom.remove(wordSpan, 1);
  298. t._checkDone();
  299. ed.setProgressState(1);
  300. t._sendRPC('learnWord', [t.selectedLang, word], function(r) {
  301. ed.setProgressState(0);
  302. });
  303. }
  304. });
  305. }
  306. m.update();
  307. });
  308. p1 = DOM.getPos(ed.getContentAreaContainer());
  309. m.settings.offset_x = p1.x;
  310. m.settings.offset_y = p1.y;
  311. ed.selection.select(wordSpan);
  312. p1 = dom.getPos(wordSpan);
  313. m.showMenu(p1.x, p1.y + wordSpan.offsetHeight - vp.y);
  314. return tinymce.dom.Event.cancel(e);
  315. } else
  316. m.hideMenu();
  317. },
  318. _checkDone : function() {
  319. var t = this, ed = t.editor, dom = ed.dom, o;
  320. each(dom.select('span'), function(n) {
  321. if (n && dom.hasClass(n, 'mceItemHiddenSpellWord')) {
  322. o = true;
  323. return false;
  324. }
  325. });
  326. if (!o)
  327. t._done();
  328. },
  329. _done : function() {
  330. var t = this, la = t.active;
  331. if (t.active) {
  332. t.active = 0;
  333. t._removeWords();
  334. if (t._menu)
  335. t._menu.hideMenu();
  336. if (la)
  337. t.editor.nodeChanged();
  338. }
  339. },
  340. _sendRPC : function(m, p, cb) {
  341. var t = this;
  342. JSONRequest.sendRPC({
  343. url : t.rpcUrl,
  344. method : m,
  345. params : p,
  346. success : cb,
  347. error : function(e, x) {
  348. t.editor.setProgressState(0);
  349. t.editor.windowManager.alert(e.errstr || ('Error response: ' + x.responseText));
  350. }
  351. });
  352. }
  353. });
  354. // Register plugin
  355. tinymce.PluginManager.add('spellchecker', tinymce.plugins.SpellcheckerPlugin);
  356. })();