|
1 /** |
|
2 * Plugin.js |
|
3 * |
|
4 * Copyright, Moxiecode Systems AB |
|
5 * Released under LGPL License. |
|
6 * |
|
7 * License: http://www.tinymce.com/license |
|
8 * Contributing: http://www.tinymce.com/contributing |
|
9 */ |
|
10 |
|
11 /*jshint camelcase:false */ |
|
12 |
|
13 /** |
|
14 * This class contains all core logic for the spellchecker plugin. |
|
15 * |
|
16 * @class tinymce.spellcheckerplugin.Plugin |
|
17 * @private |
|
18 */ |
|
19 define("tinymce/spellcheckerplugin/Plugin", [ |
|
20 "tinymce/spellcheckerplugin/DomTextMatcher", |
|
21 "tinymce/PluginManager", |
|
22 "tinymce/util/Tools", |
|
23 "tinymce/ui/Menu", |
|
24 "tinymce/dom/DOMUtils", |
|
25 "tinymce/util/XHR", |
|
26 "tinymce/util/URI", |
|
27 "tinymce/util/JSON" |
|
28 ], function(DomTextMatcher, PluginManager, Tools, Menu, DOMUtils, XHR, URI, JSON) { |
|
29 PluginManager.add('spellchecker', function(editor, url) { |
|
30 var languageMenuItems, self = this, lastSuggestions, started, suggestionsMenu, settings = editor.settings; |
|
31 var hasDictionarySupport; |
|
32 |
|
33 function getTextMatcher() { |
|
34 if (!self.textMatcher) { |
|
35 self.textMatcher = new DomTextMatcher(editor.getBody(), editor); |
|
36 } |
|
37 |
|
38 return self.textMatcher; |
|
39 } |
|
40 |
|
41 function buildMenuItems(listName, languageValues) { |
|
42 var items = []; |
|
43 |
|
44 Tools.each(languageValues, function(languageValue) { |
|
45 items.push({ |
|
46 selectable: true, |
|
47 text: languageValue.name, |
|
48 data: languageValue.value |
|
49 }); |
|
50 }); |
|
51 |
|
52 return items; |
|
53 } |
|
54 |
|
55 var languagesString = settings.spellchecker_languages || |
|
56 'English=en,Danish=da,Dutch=nl,Finnish=fi,French=fr_FR,' + |
|
57 'German=de,Italian=it,Polish=pl,Portuguese=pt_BR,' + |
|
58 'Spanish=es,Swedish=sv'; |
|
59 |
|
60 languageMenuItems = buildMenuItems('Language', |
|
61 Tools.map(languagesString.split(','), function(langPair) { |
|
62 langPair = langPair.split('='); |
|
63 |
|
64 return { |
|
65 name: langPair[0], |
|
66 value: langPair[1] |
|
67 }; |
|
68 }) |
|
69 ); |
|
70 |
|
71 function isEmpty(obj) { |
|
72 /*jshint unused:false*/ |
|
73 /*eslint no-unused-vars:0 */ |
|
74 for (var name in obj) { |
|
75 return false; |
|
76 } |
|
77 |
|
78 return true; |
|
79 } |
|
80 |
|
81 function showSuggestions(word, spans) { |
|
82 var items = [], suggestions = lastSuggestions[word]; |
|
83 |
|
84 Tools.each(suggestions, function(suggestion) { |
|
85 items.push({ |
|
86 text: suggestion, |
|
87 onclick: function() { |
|
88 editor.insertContent(editor.dom.encode(suggestion)); |
|
89 editor.dom.remove(spans); |
|
90 checkIfFinished(); |
|
91 } |
|
92 }); |
|
93 }); |
|
94 |
|
95 items.push({text: '-'}); |
|
96 |
|
97 if (hasDictionarySupport) { |
|
98 items.push({text: 'Add to Dictionary', onclick: function() { |
|
99 addToDictionary(word, spans); |
|
100 }}); |
|
101 } |
|
102 |
|
103 items.push.apply(items, [ |
|
104 {text: 'Ignore', onclick: function() { |
|
105 ignoreWord(word, spans); |
|
106 }}, |
|
107 |
|
108 {text: 'Ignore all', onclick: function() { |
|
109 ignoreWord(word, spans, true); |
|
110 }} |
|
111 ]); |
|
112 |
|
113 // Render menu |
|
114 suggestionsMenu = new Menu({ |
|
115 items: items, |
|
116 context: 'contextmenu', |
|
117 onautohide: function(e) { |
|
118 if (e.target.className.indexOf('spellchecker') != -1) { |
|
119 e.preventDefault(); |
|
120 } |
|
121 }, |
|
122 onhide: function() { |
|
123 suggestionsMenu.remove(); |
|
124 suggestionsMenu = null; |
|
125 } |
|
126 }); |
|
127 |
|
128 suggestionsMenu.renderTo(document.body); |
|
129 |
|
130 // Position menu |
|
131 var pos = DOMUtils.DOM.getPos(editor.getContentAreaContainer()); |
|
132 var targetPos = editor.dom.getPos(spans[0]); |
|
133 var root = editor.dom.getRoot(); |
|
134 |
|
135 // Adjust targetPos for scrolling in the editor |
|
136 if (root.nodeName == 'BODY') { |
|
137 targetPos.x -= root.ownerDocument.documentElement.scrollLeft || root.scrollLeft; |
|
138 targetPos.y -= root.ownerDocument.documentElement.scrollTop || root.scrollTop; |
|
139 } else { |
|
140 targetPos.x -= root.scrollLeft; |
|
141 targetPos.y -= root.scrollTop; |
|
142 } |
|
143 |
|
144 pos.x += targetPos.x; |
|
145 pos.y += targetPos.y; |
|
146 |
|
147 suggestionsMenu.moveTo(pos.x, pos.y + spans[0].offsetHeight); |
|
148 } |
|
149 |
|
150 function getWordCharPattern() { |
|
151 // Regexp for finding word specific characters this will split words by |
|
152 // spaces, quotes, copy right characters etc. It's escaped with unicode characters |
|
153 // to make it easier to output scripts on servers using different encodings |
|
154 // so if you add any characters outside the 128 byte range make sure to escape it |
|
155 return editor.getParam('spellchecker_wordchar_pattern') || new RegExp("[^" + |
|
156 "\\s!\"#$%&()*+,-./:;<=>?@[\\]^_{|}`" + |
|
157 "\u00a7\u00a9\u00ab\u00ae\u00b1\u00b6\u00b7\u00b8\u00bb" + |
|
158 "\u00bc\u00bd\u00be\u00bf\u00d7\u00f7\u00a4\u201d\u201c\u201e\u00a0\u2002\u2003\u2009" + |
|
159 "]+", "g"); |
|
160 } |
|
161 |
|
162 function defaultSpellcheckCallback(method, text, doneCallback, errorCallback) { |
|
163 var data = {method: method}, postData = ''; |
|
164 |
|
165 if (method == "spellcheck") { |
|
166 data.text = text; |
|
167 data.lang = settings.spellchecker_language; |
|
168 } |
|
169 |
|
170 if (method == "addToDictionary") { |
|
171 data.word = text; |
|
172 } |
|
173 |
|
174 Tools.each(data, function(value, key) { |
|
175 if (postData) { |
|
176 postData += '&'; |
|
177 } |
|
178 |
|
179 postData += key + '=' + encodeURIComponent(value); |
|
180 }); |
|
181 |
|
182 XHR.send({ |
|
183 url: new URI(url).toAbsolute(settings.spellchecker_rpc_url), |
|
184 type: "post", |
|
185 content_type: 'application/x-www-form-urlencoded', |
|
186 data: postData, |
|
187 success: function(result) { |
|
188 result = JSON.parse(result); |
|
189 |
|
190 if (!result) { |
|
191 errorCallback("Sever response wasn't proper JSON."); |
|
192 } else if (result.error) { |
|
193 errorCallback(result.error); |
|
194 } else { |
|
195 doneCallback(result); |
|
196 } |
|
197 }, |
|
198 error: function(type, xhr) { |
|
199 errorCallback("Spellchecker request error: " + xhr.status); |
|
200 } |
|
201 }); |
|
202 } |
|
203 |
|
204 function sendRpcCall(name, data, successCallback, errorCallback) { |
|
205 var spellCheckCallback = settings.spellchecker_callback || defaultSpellcheckCallback; |
|
206 spellCheckCallback.call(self, name, data, successCallback, errorCallback); |
|
207 } |
|
208 |
|
209 function spellcheck() { |
|
210 if (started) { |
|
211 finish(); |
|
212 return; |
|
213 } else { |
|
214 finish(); |
|
215 } |
|
216 |
|
217 function errorCallback(message) { |
|
218 editor.windowManager.alert(message); |
|
219 editor.setProgressState(false); |
|
220 finish(); |
|
221 } |
|
222 |
|
223 editor.setProgressState(true); |
|
224 sendRpcCall("spellcheck", getTextMatcher().text, markErrors, errorCallback); |
|
225 editor.focus(); |
|
226 } |
|
227 |
|
228 function checkIfFinished() { |
|
229 if (!editor.dom.select('span.mce-spellchecker-word').length) { |
|
230 finish(); |
|
231 } |
|
232 } |
|
233 |
|
234 function addToDictionary(word, spans) { |
|
235 editor.setProgressState(true); |
|
236 |
|
237 sendRpcCall("addToDictionary", word, function() { |
|
238 editor.setProgressState(false); |
|
239 editor.dom.remove(spans, true); |
|
240 checkIfFinished(); |
|
241 }, function(message) { |
|
242 editor.windowManager.alert(message); |
|
243 editor.setProgressState(false); |
|
244 }); |
|
245 } |
|
246 |
|
247 function ignoreWord(word, spans, all) { |
|
248 editor.selection.collapse(); |
|
249 |
|
250 if (all) { |
|
251 Tools.each(editor.dom.select('span.mce-spellchecker-word'), function(span) { |
|
252 if (span.getAttribute('data-mce-word') == word) { |
|
253 editor.dom.remove(span, true); |
|
254 } |
|
255 }); |
|
256 } else { |
|
257 editor.dom.remove(spans, true); |
|
258 } |
|
259 |
|
260 checkIfFinished(); |
|
261 } |
|
262 |
|
263 function finish() { |
|
264 getTextMatcher().reset(); |
|
265 self.textMatcher = null; |
|
266 |
|
267 if (started) { |
|
268 started = false; |
|
269 editor.fire('SpellcheckEnd'); |
|
270 } |
|
271 } |
|
272 |
|
273 function getElmIndex(elm) { |
|
274 var value = elm.getAttribute('data-mce-index'); |
|
275 |
|
276 if (typeof value == "number") { |
|
277 return "" + value; |
|
278 } |
|
279 |
|
280 return value; |
|
281 } |
|
282 |
|
283 function findSpansByIndex(index) { |
|
284 var nodes, spans = []; |
|
285 |
|
286 nodes = Tools.toArray(editor.getBody().getElementsByTagName('span')); |
|
287 if (nodes.length) { |
|
288 for (var i = 0; i < nodes.length; i++) { |
|
289 var nodeIndex = getElmIndex(nodes[i]); |
|
290 |
|
291 if (nodeIndex === null || !nodeIndex.length) { |
|
292 continue; |
|
293 } |
|
294 |
|
295 if (nodeIndex === index.toString()) { |
|
296 spans.push(nodes[i]); |
|
297 } |
|
298 } |
|
299 } |
|
300 |
|
301 return spans; |
|
302 } |
|
303 |
|
304 editor.on('click', function(e) { |
|
305 var target = e.target; |
|
306 |
|
307 if (target.className == "mce-spellchecker-word") { |
|
308 e.preventDefault(); |
|
309 |
|
310 var spans = findSpansByIndex(getElmIndex(target)); |
|
311 |
|
312 if (spans.length > 0) { |
|
313 var rng = editor.dom.createRng(); |
|
314 rng.setStartBefore(spans[0]); |
|
315 rng.setEndAfter(spans[spans.length - 1]); |
|
316 editor.selection.setRng(rng); |
|
317 showSuggestions(target.getAttribute('data-mce-word'), spans); |
|
318 } |
|
319 } |
|
320 }); |
|
321 |
|
322 editor.addMenuItem('spellchecker', { |
|
323 text: 'Spellcheck', |
|
324 context: 'tools', |
|
325 onclick: spellcheck, |
|
326 selectable: true, |
|
327 onPostRender: function() { |
|
328 var self = this; |
|
329 |
|
330 self.active(started); |
|
331 |
|
332 editor.on('SpellcheckStart SpellcheckEnd', function() { |
|
333 self.active(started); |
|
334 }); |
|
335 } |
|
336 }); |
|
337 |
|
338 function updateSelection(e) { |
|
339 var selectedLanguage = settings.spellchecker_language; |
|
340 |
|
341 e.control.items().each(function(ctrl) { |
|
342 ctrl.active(ctrl.settings.data === selectedLanguage); |
|
343 }); |
|
344 } |
|
345 |
|
346 /** |
|
347 * Find the specified words and marks them. It will also show suggestions for those words. |
|
348 * |
|
349 * @example |
|
350 * editor.plugins.spellchecker.markErrors({ |
|
351 * dictionary: true, |
|
352 * words: { |
|
353 * "word1": ["suggestion 1", "Suggestion 2"] |
|
354 * } |
|
355 * }); |
|
356 * @param {Object} data Data object containing the words with suggestions. |
|
357 */ |
|
358 function markErrors(data) { |
|
359 var suggestions; |
|
360 |
|
361 if (data.words) { |
|
362 hasDictionarySupport = !!data.dictionary; |
|
363 suggestions = data.words; |
|
364 } else { |
|
365 // Fallback to old format |
|
366 suggestions = data; |
|
367 } |
|
368 |
|
369 editor.setProgressState(false); |
|
370 |
|
371 if (isEmpty(suggestions)) { |
|
372 editor.windowManager.alert('No misspellings found'); |
|
373 started = false; |
|
374 return; |
|
375 } |
|
376 |
|
377 lastSuggestions = suggestions; |
|
378 |
|
379 getTextMatcher().find(getWordCharPattern()).filter(function(match) { |
|
380 return !!suggestions[match.text]; |
|
381 }).wrap(function(match) { |
|
382 return editor.dom.create('span', { |
|
383 "class": 'mce-spellchecker-word', |
|
384 "data-mce-bogus": 1, |
|
385 "data-mce-word": match.text |
|
386 }); |
|
387 }); |
|
388 |
|
389 started = true; |
|
390 editor.fire('SpellcheckStart'); |
|
391 } |
|
392 |
|
393 var buttonArgs = { |
|
394 tooltip: 'Spellcheck', |
|
395 onclick: spellcheck, |
|
396 onPostRender: function() { |
|
397 var self = this; |
|
398 |
|
399 editor.on('SpellcheckStart SpellcheckEnd', function() { |
|
400 self.active(started); |
|
401 }); |
|
402 } |
|
403 }; |
|
404 |
|
405 if (languageMenuItems.length > 1) { |
|
406 buttonArgs.type = 'splitbutton'; |
|
407 buttonArgs.menu = languageMenuItems; |
|
408 buttonArgs.onshow = updateSelection; |
|
409 buttonArgs.onselect = function(e) { |
|
410 settings.spellchecker_language = e.control.settings.data; |
|
411 }; |
|
412 } |
|
413 |
|
414 editor.addButton('spellchecker', buttonArgs); |
|
415 editor.addCommand('mceSpellCheck', spellcheck); |
|
416 |
|
417 editor.on('remove', function() { |
|
418 if (suggestionsMenu) { |
|
419 suggestionsMenu.remove(); |
|
420 suggestionsMenu = null; |
|
421 } |
|
422 }); |
|
423 |
|
424 editor.on('change', checkIfFinished); |
|
425 |
|
426 this.getTextMatcher = getTextMatcher; |
|
427 this.getWordCharPattern = getWordCharPattern; |
|
428 this.markErrors = markErrors; |
|
429 this.getLanguage = function() { |
|
430 return settings.spellchecker_language; |
|
431 }; |
|
432 |
|
433 // Set default spellchecker language if it's not specified |
|
434 settings.spellchecker_language = settings.spellchecker_language || settings.language || 'en'; |
|
435 }); |
|
436 }); |