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