1 /** |
|
2 * Compiled inline version. (Library mode) |
|
3 */ |
|
4 |
|
5 /*jshint smarttabs:true, undef:true, latedef:true, curly:true, bitwise:true, camelcase:true */ |
|
6 /*globals $code */ |
|
7 |
|
8 (function(exports, undefined) { |
|
9 "use strict"; |
|
10 |
|
11 var modules = {}; |
|
12 |
|
13 function require(ids, callback) { |
|
14 var module, defs = []; |
|
15 |
|
16 for (var i = 0; i < ids.length; ++i) { |
|
17 module = modules[ids[i]] || resolve(ids[i]); |
|
18 if (!module) { |
|
19 throw 'module definition dependecy not found: ' + ids[i]; |
|
20 } |
|
21 |
|
22 defs.push(module); |
|
23 } |
|
24 |
|
25 callback.apply(null, defs); |
|
26 } |
|
27 |
|
28 function define(id, dependencies, definition) { |
|
29 if (typeof id !== 'string') { |
|
30 throw 'invalid module definition, module id must be defined and be a string'; |
|
31 } |
|
32 |
|
33 if (dependencies === undefined) { |
|
34 throw 'invalid module definition, dependencies must be specified'; |
|
35 } |
|
36 |
|
37 if (definition === undefined) { |
|
38 throw 'invalid module definition, definition function must be specified'; |
|
39 } |
|
40 |
|
41 require(dependencies, function() { |
|
42 modules[id] = definition.apply(null, arguments); |
|
43 }); |
|
44 } |
|
45 |
|
46 function defined(id) { |
|
47 return !!modules[id]; |
|
48 } |
|
49 |
|
50 function resolve(id) { |
|
51 var target = exports; |
|
52 var fragments = id.split(/[.\/]/); |
|
53 |
|
54 for (var fi = 0; fi < fragments.length; ++fi) { |
|
55 if (!target[fragments[fi]]) { |
|
56 return; |
|
57 } |
|
58 |
|
59 target = target[fragments[fi]]; |
|
60 } |
|
61 |
|
62 return target; |
|
63 } |
|
64 |
|
65 function expose(ids) { |
|
66 for (var i = 0; i < ids.length; i++) { |
|
67 var target = exports; |
|
68 var id = ids[i]; |
|
69 var fragments = id.split(/[.\/]/); |
|
70 |
|
71 for (var fi = 0; fi < fragments.length - 1; ++fi) { |
|
72 if (target[fragments[fi]] === undefined) { |
|
73 target[fragments[fi]] = {}; |
|
74 } |
|
75 |
|
76 target = target[fragments[fi]]; |
|
77 } |
|
78 |
|
79 target[fragments[fragments.length - 1]] = modules[id]; |
|
80 } |
|
81 } |
|
82 |
|
83 // Included from: js/tinymce/plugins/spellchecker/classes/DomTextMatcher.js |
|
84 |
|
85 /** |
|
86 * DomTextMatcher.js |
|
87 * |
|
88 * Copyright, Moxiecode Systems AB |
|
89 * Released under LGPL License. |
|
90 * |
|
91 * License: http://www.tinymce.com/license |
|
92 * Contributing: http://www.tinymce.com/contributing |
|
93 */ |
|
94 |
|
95 /*eslint no-labels:0, no-constant-condition: 0 */ |
|
96 |
|
97 /** |
|
98 * This class logic for filtering text and matching words. |
|
99 * |
|
100 * @class tinymce.spellcheckerplugin.TextFilter |
|
101 * @private |
|
102 */ |
|
103 define("tinymce/spellcheckerplugin/DomTextMatcher", [], function() { |
|
104 // Based on work developed by: James Padolsey http://james.padolsey.com |
|
105 // released under UNLICENSE that is compatible with LGPL |
|
106 // TODO: Handle contentEditable edgecase: |
|
107 // <p>text<span contentEditable="false">text<span contentEditable="true">text</span>text</span>text</p> |
|
108 return function(node, editor) { |
|
109 var m, matches = [], text, dom = editor.dom; |
|
110 var blockElementsMap, hiddenTextElementsMap, shortEndedElementsMap; |
|
111 |
|
112 blockElementsMap = editor.schema.getBlockElements(); // H1-H6, P, TD etc |
|
113 hiddenTextElementsMap = editor.schema.getWhiteSpaceElements(); // TEXTAREA, PRE, STYLE, SCRIPT |
|
114 shortEndedElementsMap = editor.schema.getShortEndedElements(); // BR, IMG, INPUT |
|
115 |
|
116 function createMatch(m, data) { |
|
117 if (!m[0]) { |
|
118 throw 'findAndReplaceDOMText cannot handle zero-length matches'; |
|
119 } |
|
120 |
|
121 return { |
|
122 start: m.index, |
|
123 end: m.index + m[0].length, |
|
124 text: m[0], |
|
125 data: data |
|
126 }; |
|
127 } |
|
128 |
|
129 function getText(node) { |
|
130 var txt; |
|
131 |
|
132 if (node.nodeType === 3) { |
|
133 return node.data; |
|
134 } |
|
135 |
|
136 if (hiddenTextElementsMap[node.nodeName] && !blockElementsMap[node.nodeName]) { |
|
137 return ''; |
|
138 } |
|
139 |
|
140 txt = ''; |
|
141 |
|
142 if (blockElementsMap[node.nodeName] || shortEndedElementsMap[node.nodeName]) { |
|
143 txt += '\n'; |
|
144 } |
|
145 |
|
146 if ((node = node.firstChild)) { |
|
147 do { |
|
148 txt += getText(node); |
|
149 } while ((node = node.nextSibling)); |
|
150 } |
|
151 |
|
152 return txt; |
|
153 } |
|
154 |
|
155 function stepThroughMatches(node, matches, replaceFn) { |
|
156 var startNode, endNode, startNodeIndex, |
|
157 endNodeIndex, innerNodes = [], atIndex = 0, curNode = node, |
|
158 matchLocation, matchIndex = 0; |
|
159 |
|
160 matches = matches.slice(0); |
|
161 matches.sort(function(a, b) { |
|
162 return a.start - b.start; |
|
163 }); |
|
164 |
|
165 matchLocation = matches.shift(); |
|
166 |
|
167 out: while (true) { |
|
168 if (blockElementsMap[curNode.nodeName] || shortEndedElementsMap[curNode.nodeName]) { |
|
169 atIndex++; |
|
170 } |
|
171 |
|
172 if (curNode.nodeType === 3) { |
|
173 if (!endNode && curNode.length + atIndex >= matchLocation.end) { |
|
174 // We've found the ending |
|
175 endNode = curNode; |
|
176 endNodeIndex = matchLocation.end - atIndex; |
|
177 } else if (startNode) { |
|
178 // Intersecting node |
|
179 innerNodes.push(curNode); |
|
180 } |
|
181 |
|
182 if (!startNode && curNode.length + atIndex > matchLocation.start) { |
|
183 // We've found the match start |
|
184 startNode = curNode; |
|
185 startNodeIndex = matchLocation.start - atIndex; |
|
186 } |
|
187 |
|
188 atIndex += curNode.length; |
|
189 } |
|
190 |
|
191 if (startNode && endNode) { |
|
192 curNode = replaceFn({ |
|
193 startNode: startNode, |
|
194 startNodeIndex: startNodeIndex, |
|
195 endNode: endNode, |
|
196 endNodeIndex: endNodeIndex, |
|
197 innerNodes: innerNodes, |
|
198 match: matchLocation.text, |
|
199 matchIndex: matchIndex |
|
200 }); |
|
201 |
|
202 // replaceFn has to return the node that replaced the endNode |
|
203 // and then we step back so we can continue from the end of the |
|
204 // match: |
|
205 atIndex -= (endNode.length - endNodeIndex); |
|
206 startNode = null; |
|
207 endNode = null; |
|
208 innerNodes = []; |
|
209 matchLocation = matches.shift(); |
|
210 matchIndex++; |
|
211 |
|
212 if (!matchLocation) { |
|
213 break; // no more matches |
|
214 } |
|
215 } else if ((!hiddenTextElementsMap[curNode.nodeName] || blockElementsMap[curNode.nodeName]) && curNode.firstChild) { |
|
216 // Move down |
|
217 curNode = curNode.firstChild; |
|
218 continue; |
|
219 } else if (curNode.nextSibling) { |
|
220 // Move forward: |
|
221 curNode = curNode.nextSibling; |
|
222 continue; |
|
223 } |
|
224 |
|
225 // Move forward or up: |
|
226 while (true) { |
|
227 if (curNode.nextSibling) { |
|
228 curNode = curNode.nextSibling; |
|
229 break; |
|
230 } else if (curNode.parentNode !== node) { |
|
231 curNode = curNode.parentNode; |
|
232 } else { |
|
233 break out; |
|
234 } |
|
235 } |
|
236 } |
|
237 } |
|
238 |
|
239 /** |
|
240 * Generates the actual replaceFn which splits up text nodes |
|
241 * and inserts the replacement element. |
|
242 */ |
|
243 function genReplacer(callback) { |
|
244 function makeReplacementNode(fill, matchIndex) { |
|
245 var match = matches[matchIndex]; |
|
246 |
|
247 if (!match.stencil) { |
|
248 match.stencil = callback(match); |
|
249 } |
|
250 |
|
251 var clone = match.stencil.cloneNode(false); |
|
252 clone.setAttribute('data-mce-index', matchIndex); |
|
253 |
|
254 if (fill) { |
|
255 clone.appendChild(dom.doc.createTextNode(fill)); |
|
256 } |
|
257 |
|
258 return clone; |
|
259 } |
|
260 |
|
261 return function(range) { |
|
262 var before, after, parentNode, startNode = range.startNode, |
|
263 endNode = range.endNode, matchIndex = range.matchIndex, |
|
264 doc = dom.doc; |
|
265 |
|
266 if (startNode === endNode) { |
|
267 var node = startNode; |
|
268 |
|
269 parentNode = node.parentNode; |
|
270 if (range.startNodeIndex > 0) { |
|
271 // Add "before" text node (before the match) |
|
272 before = doc.createTextNode(node.data.substring(0, range.startNodeIndex)); |
|
273 parentNode.insertBefore(before, node); |
|
274 } |
|
275 |
|
276 // Create the replacement node: |
|
277 var el = makeReplacementNode(range.match, matchIndex); |
|
278 parentNode.insertBefore(el, node); |
|
279 if (range.endNodeIndex < node.length) { |
|
280 // Add "after" text node (after the match) |
|
281 after = doc.createTextNode(node.data.substring(range.endNodeIndex)); |
|
282 parentNode.insertBefore(after, node); |
|
283 } |
|
284 |
|
285 node.parentNode.removeChild(node); |
|
286 |
|
287 return el; |
|
288 } else { |
|
289 // Replace startNode -> [innerNodes...] -> endNode (in that order) |
|
290 before = doc.createTextNode(startNode.data.substring(0, range.startNodeIndex)); |
|
291 after = doc.createTextNode(endNode.data.substring(range.endNodeIndex)); |
|
292 var elA = makeReplacementNode(startNode.data.substring(range.startNodeIndex), matchIndex); |
|
293 var innerEls = []; |
|
294 |
|
295 for (var i = 0, l = range.innerNodes.length; i < l; ++i) { |
|
296 var innerNode = range.innerNodes[i]; |
|
297 var innerEl = makeReplacementNode(innerNode.data, matchIndex); |
|
298 innerNode.parentNode.replaceChild(innerEl, innerNode); |
|
299 innerEls.push(innerEl); |
|
300 } |
|
301 |
|
302 var elB = makeReplacementNode(endNode.data.substring(0, range.endNodeIndex), matchIndex); |
|
303 |
|
304 parentNode = startNode.parentNode; |
|
305 parentNode.insertBefore(before, startNode); |
|
306 parentNode.insertBefore(elA, startNode); |
|
307 parentNode.removeChild(startNode); |
|
308 |
|
309 parentNode = endNode.parentNode; |
|
310 parentNode.insertBefore(elB, endNode); |
|
311 parentNode.insertBefore(after, endNode); |
|
312 parentNode.removeChild(endNode); |
|
313 |
|
314 return elB; |
|
315 } |
|
316 }; |
|
317 } |
|
318 |
|
319 function unwrapElement(element) { |
|
320 var parentNode = element.parentNode; |
|
321 parentNode.insertBefore(element.firstChild, element); |
|
322 element.parentNode.removeChild(element); |
|
323 } |
|
324 |
|
325 function getWrappersByIndex(index) { |
|
326 var elements = node.getElementsByTagName('*'), wrappers = []; |
|
327 |
|
328 index = typeof index == "number" ? "" + index : null; |
|
329 |
|
330 for (var i = 0; i < elements.length; i++) { |
|
331 var element = elements[i], dataIndex = element.getAttribute('data-mce-index'); |
|
332 |
|
333 if (dataIndex !== null && dataIndex.length) { |
|
334 if (dataIndex === index || index === null) { |
|
335 wrappers.push(element); |
|
336 } |
|
337 } |
|
338 } |
|
339 |
|
340 return wrappers; |
|
341 } |
|
342 |
|
343 /** |
|
344 * Returns the index of a specific match object or -1 if it isn't found. |
|
345 * |
|
346 * @param {Match} match Text match object. |
|
347 * @return {Number} Index of match or -1 if it isn't found. |
|
348 */ |
|
349 function indexOf(match) { |
|
350 var i = matches.length; |
|
351 while (i--) { |
|
352 if (matches[i] === match) { |
|
353 return i; |
|
354 } |
|
355 } |
|
356 |
|
357 return -1; |
|
358 } |
|
359 |
|
360 /** |
|
361 * Filters the matches. If the callback returns true it stays if not it gets removed. |
|
362 * |
|
363 * @param {Function} callback Callback to execute for each match. |
|
364 * @return {DomTextMatcher} Current DomTextMatcher instance. |
|
365 */ |
|
366 function filter(callback) { |
|
367 var filteredMatches = []; |
|
368 |
|
369 each(function(match, i) { |
|
370 if (callback(match, i)) { |
|
371 filteredMatches.push(match); |
|
372 } |
|
373 }); |
|
374 |
|
375 matches = filteredMatches; |
|
376 |
|
377 /*jshint validthis:true*/ |
|
378 return this; |
|
379 } |
|
380 |
|
381 /** |
|
382 * Executes the specified callback for each match. |
|
383 * |
|
384 * @param {Function} callback Callback to execute for each match. |
|
385 * @return {DomTextMatcher} Current DomTextMatcher instance. |
|
386 */ |
|
387 function each(callback) { |
|
388 for (var i = 0, l = matches.length; i < l; i++) { |
|
389 if (callback(matches[i], i) === false) { |
|
390 break; |
|
391 } |
|
392 } |
|
393 |
|
394 /*jshint validthis:true*/ |
|
395 return this; |
|
396 } |
|
397 |
|
398 /** |
|
399 * Wraps the current matches with nodes created by the specified callback. |
|
400 * Multiple clones of these matches might occur on matches that are on multiple nodex. |
|
401 * |
|
402 * @param {Function} callback Callback to execute in order to create elements for matches. |
|
403 * @return {DomTextMatcher} Current DomTextMatcher instance. |
|
404 */ |
|
405 function wrap(callback) { |
|
406 if (matches.length) { |
|
407 stepThroughMatches(node, matches, genReplacer(callback)); |
|
408 } |
|
409 |
|
410 /*jshint validthis:true*/ |
|
411 return this; |
|
412 } |
|
413 |
|
414 /** |
|
415 * Finds the specified regexp and adds them to the matches collection. |
|
416 * |
|
417 * @param {RegExp} regex Global regexp to search the current node by. |
|
418 * @param {Object} [data] Optional custom data element for the match. |
|
419 * @return {DomTextMatcher} Current DomTextMatcher instance. |
|
420 */ |
|
421 function find(regex, data) { |
|
422 if (text && regex.global) { |
|
423 while ((m = regex.exec(text))) { |
|
424 matches.push(createMatch(m, data)); |
|
425 } |
|
426 } |
|
427 |
|
428 return this; |
|
429 } |
|
430 |
|
431 /** |
|
432 * Unwraps the specified match object or all matches if unspecified. |
|
433 * |
|
434 * @param {Object} [match] Optional match object. |
|
435 * @return {DomTextMatcher} Current DomTextMatcher instance. |
|
436 */ |
|
437 function unwrap(match) { |
|
438 var i, elements = getWrappersByIndex(match ? indexOf(match) : null); |
|
439 |
|
440 i = elements.length; |
|
441 while (i--) { |
|
442 unwrapElement(elements[i]); |
|
443 } |
|
444 |
|
445 return this; |
|
446 } |
|
447 |
|
448 /** |
|
449 * Returns a match object by the specified DOM element. |
|
450 * |
|
451 * @param {DOMElement} element Element to return match object for. |
|
452 * @return {Object} Match object for the specified element. |
|
453 */ |
|
454 function matchFromElement(element) { |
|
455 return matches[element.getAttribute('data-mce-index')]; |
|
456 } |
|
457 |
|
458 /** |
|
459 * Returns a DOM element from the specified match element. This will be the first element if it's split |
|
460 * on multiple nodes. |
|
461 * |
|
462 * @param {Object} match Match element to get first element of. |
|
463 * @return {DOMElement} DOM element for the specified match object. |
|
464 */ |
|
465 function elementFromMatch(match) { |
|
466 return getWrappersByIndex(indexOf(match))[0]; |
|
467 } |
|
468 |
|
469 /** |
|
470 * Adds match the specified range for example a grammar line. |
|
471 * |
|
472 * @param {Number} start Start offset. |
|
473 * @param {Number} length Length of the text. |
|
474 * @param {Object} data Custom data object for match. |
|
475 * @return {DomTextMatcher} Current DomTextMatcher instance. |
|
476 */ |
|
477 function add(start, length, data) { |
|
478 matches.push({ |
|
479 start: start, |
|
480 end: start + length, |
|
481 text: text.substr(start, length), |
|
482 data: data |
|
483 }); |
|
484 |
|
485 return this; |
|
486 } |
|
487 |
|
488 /** |
|
489 * Returns a DOM range for the specified match. |
|
490 * |
|
491 * @param {Object} match Match object to get range for. |
|
492 * @return {DOMRange} DOM Range for the specified match. |
|
493 */ |
|
494 function rangeFromMatch(match) { |
|
495 var wrappers = getWrappersByIndex(indexOf(match)); |
|
496 |
|
497 var rng = editor.dom.createRng(); |
|
498 rng.setStartBefore(wrappers[0]); |
|
499 rng.setEndAfter(wrappers[wrappers.length - 1]); |
|
500 |
|
501 return rng; |
|
502 } |
|
503 |
|
504 /** |
|
505 * Replaces the specified match with the specified text. |
|
506 * |
|
507 * @param {Object} match Match object to replace. |
|
508 * @param {String} text Text to replace the match with. |
|
509 * @return {DOMRange} DOM range produced after the replace. |
|
510 */ |
|
511 function replace(match, text) { |
|
512 var rng = rangeFromMatch(match); |
|
513 |
|
514 rng.deleteContents(); |
|
515 |
|
516 if (text.length > 0) { |
|
517 rng.insertNode(editor.dom.doc.createTextNode(text)); |
|
518 } |
|
519 |
|
520 return rng; |
|
521 } |
|
522 |
|
523 /** |
|
524 * Resets the DomTextMatcher instance. This will remove any wrapped nodes and remove any matches. |
|
525 * |
|
526 * @return {[type]} [description] |
|
527 */ |
|
528 function reset() { |
|
529 matches.splice(0, matches.length); |
|
530 unwrap(); |
|
531 |
|
532 return this; |
|
533 } |
|
534 |
|
535 text = getText(node); |
|
536 |
|
537 return { |
|
538 text: text, |
|
539 matches: matches, |
|
540 each: each, |
|
541 filter: filter, |
|
542 reset: reset, |
|
543 matchFromElement: matchFromElement, |
|
544 elementFromMatch: elementFromMatch, |
|
545 find: find, |
|
546 add: add, |
|
547 wrap: wrap, |
|
548 unwrap: unwrap, |
|
549 replace: replace, |
|
550 rangeFromMatch: rangeFromMatch, |
|
551 indexOf: indexOf |
|
552 }; |
|
553 }; |
|
554 }); |
|
555 |
|
556 // Included from: js/tinymce/plugins/spellchecker/classes/Plugin.js |
|
557 |
|
558 /** |
|
559 * Plugin.js |
|
560 * |
|
561 * Copyright, Moxiecode Systems AB |
|
562 * Released under LGPL License. |
|
563 * |
|
564 * License: http://www.tinymce.com/license |
|
565 * Contributing: http://www.tinymce.com/contributing |
|
566 */ |
|
567 |
|
568 /*jshint camelcase:false */ |
|
569 |
|
570 /** |
|
571 * This class contains all core logic for the spellchecker plugin. |
|
572 * |
|
573 * @class tinymce.spellcheckerplugin.Plugin |
|
574 * @private |
|
575 */ |
|
576 define("tinymce/spellcheckerplugin/Plugin", [ |
|
577 "tinymce/spellcheckerplugin/DomTextMatcher", |
|
578 "tinymce/PluginManager", |
|
579 "tinymce/util/Tools", |
|
580 "tinymce/ui/Menu", |
|
581 "tinymce/dom/DOMUtils", |
|
582 "tinymce/util/XHR", |
|
583 "tinymce/util/URI", |
|
584 "tinymce/util/JSON" |
|
585 ], function(DomTextMatcher, PluginManager, Tools, Menu, DOMUtils, XHR, URI, JSON) { |
|
586 PluginManager.add('spellchecker', function(editor, url) { |
|
587 var languageMenuItems, self = this, lastSuggestions, started, suggestionsMenu, settings = editor.settings; |
|
588 var hasDictionarySupport; |
|
589 |
|
590 function getTextMatcher() { |
|
591 if (!self.textMatcher) { |
|
592 self.textMatcher = new DomTextMatcher(editor.getBody(), editor); |
|
593 } |
|
594 |
|
595 return self.textMatcher; |
|
596 } |
|
597 |
|
598 function buildMenuItems(listName, languageValues) { |
|
599 var items = []; |
|
600 |
|
601 Tools.each(languageValues, function(languageValue) { |
|
602 items.push({ |
|
603 selectable: true, |
|
604 text: languageValue.name, |
|
605 data: languageValue.value |
|
606 }); |
|
607 }); |
|
608 |
|
609 return items; |
|
610 } |
|
611 |
|
612 var languagesString = settings.spellchecker_languages || |
|
613 'English=en,Danish=da,Dutch=nl,Finnish=fi,French=fr_FR,' + |
|
614 'German=de,Italian=it,Polish=pl,Portuguese=pt_BR,' + |
|
615 'Spanish=es,Swedish=sv'; |
|
616 |
|
617 languageMenuItems = buildMenuItems('Language', |
|
618 Tools.map(languagesString.split(','), function(langPair) { |
|
619 langPair = langPair.split('='); |
|
620 |
|
621 return { |
|
622 name: langPair[0], |
|
623 value: langPair[1] |
|
624 }; |
|
625 }) |
|
626 ); |
|
627 |
|
628 function isEmpty(obj) { |
|
629 /*jshint unused:false*/ |
|
630 /*eslint no-unused-vars:0 */ |
|
631 for (var name in obj) { |
|
632 return false; |
|
633 } |
|
634 |
|
635 return true; |
|
636 } |
|
637 |
|
638 function showSuggestions(word, spans) { |
|
639 var items = [], suggestions = lastSuggestions[word]; |
|
640 |
|
641 Tools.each(suggestions, function(suggestion) { |
|
642 items.push({ |
|
643 text: suggestion, |
|
644 onclick: function() { |
|
645 editor.insertContent(editor.dom.encode(suggestion)); |
|
646 editor.dom.remove(spans); |
|
647 checkIfFinished(); |
|
648 } |
|
649 }); |
|
650 }); |
|
651 |
|
652 items.push({text: '-'}); |
|
653 |
|
654 if (hasDictionarySupport) { |
|
655 items.push({text: 'Add to Dictionary', onclick: function() { |
|
656 addToDictionary(word, spans); |
|
657 }}); |
|
658 } |
|
659 |
|
660 items.push.apply(items, [ |
|
661 {text: 'Ignore', onclick: function() { |
|
662 ignoreWord(word, spans); |
|
663 }}, |
|
664 |
|
665 {text: 'Ignore all', onclick: function() { |
|
666 ignoreWord(word, spans, true); |
|
667 }} |
|
668 ]); |
|
669 |
|
670 // Render menu |
|
671 suggestionsMenu = new Menu({ |
|
672 items: items, |
|
673 context: 'contextmenu', |
|
674 onautohide: function(e) { |
|
675 if (e.target.className.indexOf('spellchecker') != -1) { |
|
676 e.preventDefault(); |
|
677 } |
|
678 }, |
|
679 onhide: function() { |
|
680 suggestionsMenu.remove(); |
|
681 suggestionsMenu = null; |
|
682 } |
|
683 }); |
|
684 |
|
685 suggestionsMenu.renderTo(document.body); |
|
686 |
|
687 // Position menu |
|
688 var pos = DOMUtils.DOM.getPos(editor.getContentAreaContainer()); |
|
689 var targetPos = editor.dom.getPos(spans[0]); |
|
690 var root = editor.dom.getRoot(); |
|
691 |
|
692 // Adjust targetPos for scrolling in the editor |
|
693 if (root.nodeName == 'BODY') { |
|
694 targetPos.x -= root.ownerDocument.documentElement.scrollLeft || root.scrollLeft; |
|
695 targetPos.y -= root.ownerDocument.documentElement.scrollTop || root.scrollTop; |
|
696 } else { |
|
697 targetPos.x -= root.scrollLeft; |
|
698 targetPos.y -= root.scrollTop; |
|
699 } |
|
700 |
|
701 pos.x += targetPos.x; |
|
702 pos.y += targetPos.y; |
|
703 |
|
704 suggestionsMenu.moveTo(pos.x, pos.y + spans[0].offsetHeight); |
|
705 } |
|
706 |
|
707 function getWordCharPattern() { |
|
708 // Regexp for finding word specific characters this will split words by |
|
709 // spaces, quotes, copy right characters etc. It's escaped with unicode characters |
|
710 // to make it easier to output scripts on servers using different encodings |
|
711 // so if you add any characters outside the 128 byte range make sure to escape it |
|
712 return editor.getParam('spellchecker_wordchar_pattern') || new RegExp("[^" + |
|
713 "\\s!\"#$%&()*+,-./:;<=>?@[\\]^_{|}`" + |
|
714 "\u00a7\u00a9\u00ab\u00ae\u00b1\u00b6\u00b7\u00b8\u00bb" + |
|
715 "\u00bc\u00bd\u00be\u00bf\u00d7\u00f7\u00a4\u201d\u201c\u201e\u00a0\u2002\u2003\u2009" + |
|
716 "]+", "g"); |
|
717 } |
|
718 |
|
719 function defaultSpellcheckCallback(method, text, doneCallback, errorCallback) { |
|
720 var data = {method: method}, postData = ''; |
|
721 |
|
722 if (method == "spellcheck") { |
|
723 data.text = text; |
|
724 data.lang = settings.spellchecker_language; |
|
725 } |
|
726 |
|
727 if (method == "addToDictionary") { |
|
728 data.word = text; |
|
729 } |
|
730 |
|
731 Tools.each(data, function(value, key) { |
|
732 if (postData) { |
|
733 postData += '&'; |
|
734 } |
|
735 |
|
736 postData += key + '=' + encodeURIComponent(value); |
|
737 }); |
|
738 |
|
739 XHR.send({ |
|
740 url: new URI(url).toAbsolute(settings.spellchecker_rpc_url), |
|
741 type: "post", |
|
742 content_type: 'application/x-www-form-urlencoded', |
|
743 data: postData, |
|
744 success: function(result) { |
|
745 result = JSON.parse(result); |
|
746 |
|
747 if (!result) { |
|
748 errorCallback("Sever response wasn't proper JSON."); |
|
749 } else if (result.error) { |
|
750 errorCallback(result.error); |
|
751 } else { |
|
752 doneCallback(result); |
|
753 } |
|
754 }, |
|
755 error: function(type, xhr) { |
|
756 errorCallback("Spellchecker request error: " + xhr.status); |
|
757 } |
|
758 }); |
|
759 } |
|
760 |
|
761 function sendRpcCall(name, data, successCallback, errorCallback) { |
|
762 var spellCheckCallback = settings.spellchecker_callback || defaultSpellcheckCallback; |
|
763 spellCheckCallback.call(self, name, data, successCallback, errorCallback); |
|
764 } |
|
765 |
|
766 function spellcheck() { |
|
767 if (started) { |
|
768 finish(); |
|
769 return; |
|
770 } else { |
|
771 finish(); |
|
772 } |
|
773 |
|
774 function errorCallback(message) { |
|
775 editor.windowManager.alert(message); |
|
776 editor.setProgressState(false); |
|
777 finish(); |
|
778 } |
|
779 |
|
780 editor.setProgressState(true); |
|
781 sendRpcCall("spellcheck", getTextMatcher().text, markErrors, errorCallback); |
|
782 editor.focus(); |
|
783 } |
|
784 |
|
785 function checkIfFinished() { |
|
786 if (!editor.dom.select('span.mce-spellchecker-word').length) { |
|
787 finish(); |
|
788 } |
|
789 } |
|
790 |
|
791 function addToDictionary(word, spans) { |
|
792 editor.setProgressState(true); |
|
793 |
|
794 sendRpcCall("addToDictionary", word, function() { |
|
795 editor.setProgressState(false); |
|
796 editor.dom.remove(spans, true); |
|
797 checkIfFinished(); |
|
798 }, function(message) { |
|
799 editor.windowManager.alert(message); |
|
800 editor.setProgressState(false); |
|
801 }); |
|
802 } |
|
803 |
|
804 function ignoreWord(word, spans, all) { |
|
805 editor.selection.collapse(); |
|
806 |
|
807 if (all) { |
|
808 Tools.each(editor.dom.select('span.mce-spellchecker-word'), function(span) { |
|
809 if (span.getAttribute('data-mce-word') == word) { |
|
810 editor.dom.remove(span, true); |
|
811 } |
|
812 }); |
|
813 } else { |
|
814 editor.dom.remove(spans, true); |
|
815 } |
|
816 |
|
817 checkIfFinished(); |
|
818 } |
|
819 |
|
820 function finish() { |
|
821 getTextMatcher().reset(); |
|
822 self.textMatcher = null; |
|
823 |
|
824 if (started) { |
|
825 started = false; |
|
826 editor.fire('SpellcheckEnd'); |
|
827 } |
|
828 } |
|
829 |
|
830 function getElmIndex(elm) { |
|
831 var value = elm.getAttribute('data-mce-index'); |
|
832 |
|
833 if (typeof value == "number") { |
|
834 return "" + value; |
|
835 } |
|
836 |
|
837 return value; |
|
838 } |
|
839 |
|
840 function findSpansByIndex(index) { |
|
841 var nodes, spans = []; |
|
842 |
|
843 nodes = Tools.toArray(editor.getBody().getElementsByTagName('span')); |
|
844 if (nodes.length) { |
|
845 for (var i = 0; i < nodes.length; i++) { |
|
846 var nodeIndex = getElmIndex(nodes[i]); |
|
847 |
|
848 if (nodeIndex === null || !nodeIndex.length) { |
|
849 continue; |
|
850 } |
|
851 |
|
852 if (nodeIndex === index.toString()) { |
|
853 spans.push(nodes[i]); |
|
854 } |
|
855 } |
|
856 } |
|
857 |
|
858 return spans; |
|
859 } |
|
860 |
|
861 editor.on('click', function(e) { |
|
862 var target = e.target; |
|
863 |
|
864 if (target.className == "mce-spellchecker-word") { |
|
865 e.preventDefault(); |
|
866 |
|
867 var spans = findSpansByIndex(getElmIndex(target)); |
|
868 |
|
869 if (spans.length > 0) { |
|
870 var rng = editor.dom.createRng(); |
|
871 rng.setStartBefore(spans[0]); |
|
872 rng.setEndAfter(spans[spans.length - 1]); |
|
873 editor.selection.setRng(rng); |
|
874 showSuggestions(target.getAttribute('data-mce-word'), spans); |
|
875 } |
|
876 } |
|
877 }); |
|
878 |
|
879 editor.addMenuItem('spellchecker', { |
|
880 text: 'Spellcheck', |
|
881 context: 'tools', |
|
882 onclick: spellcheck, |
|
883 selectable: true, |
|
884 onPostRender: function() { |
|
885 var self = this; |
|
886 |
|
887 self.active(started); |
|
888 |
|
889 editor.on('SpellcheckStart SpellcheckEnd', function() { |
|
890 self.active(started); |
|
891 }); |
|
892 } |
|
893 }); |
|
894 |
|
895 function updateSelection(e) { |
|
896 var selectedLanguage = settings.spellchecker_language; |
|
897 |
|
898 e.control.items().each(function(ctrl) { |
|
899 ctrl.active(ctrl.settings.data === selectedLanguage); |
|
900 }); |
|
901 } |
|
902 |
|
903 /** |
|
904 * Find the specified words and marks them. It will also show suggestions for those words. |
|
905 * |
|
906 * @example |
|
907 * editor.plugins.spellchecker.markErrors({ |
|
908 * dictionary: true, |
|
909 * words: { |
|
910 * "word1": ["suggestion 1", "Suggestion 2"] |
|
911 * } |
|
912 * }); |
|
913 * @param {Object} data Data object containing the words with suggestions. |
|
914 */ |
|
915 function markErrors(data) { |
|
916 var suggestions; |
|
917 |
|
918 if (data.words) { |
|
919 hasDictionarySupport = !!data.dictionary; |
|
920 suggestions = data.words; |
|
921 } else { |
|
922 // Fallback to old format |
|
923 suggestions = data; |
|
924 } |
|
925 |
|
926 editor.setProgressState(false); |
|
927 |
|
928 if (isEmpty(suggestions)) { |
|
929 editor.windowManager.alert('No misspellings found'); |
|
930 started = false; |
|
931 return; |
|
932 } |
|
933 |
|
934 lastSuggestions = suggestions; |
|
935 |
|
936 getTextMatcher().find(getWordCharPattern()).filter(function(match) { |
|
937 return !!suggestions[match.text]; |
|
938 }).wrap(function(match) { |
|
939 return editor.dom.create('span', { |
|
940 "class": 'mce-spellchecker-word', |
|
941 "data-mce-bogus": 1, |
|
942 "data-mce-word": match.text |
|
943 }); |
|
944 }); |
|
945 |
|
946 started = true; |
|
947 editor.fire('SpellcheckStart'); |
|
948 } |
|
949 |
|
950 var buttonArgs = { |
|
951 tooltip: 'Spellcheck', |
|
952 onclick: spellcheck, |
|
953 onPostRender: function() { |
|
954 var self = this; |
|
955 |
|
956 editor.on('SpellcheckStart SpellcheckEnd', function() { |
|
957 self.active(started); |
|
958 }); |
|
959 } |
|
960 }; |
|
961 |
|
962 if (languageMenuItems.length > 1) { |
|
963 buttonArgs.type = 'splitbutton'; |
|
964 buttonArgs.menu = languageMenuItems; |
|
965 buttonArgs.onshow = updateSelection; |
|
966 buttonArgs.onselect = function(e) { |
|
967 settings.spellchecker_language = e.control.settings.data; |
|
968 }; |
|
969 } |
|
970 |
|
971 editor.addButton('spellchecker', buttonArgs); |
|
972 editor.addCommand('mceSpellCheck', spellcheck); |
|
973 |
|
974 editor.on('remove', function() { |
|
975 if (suggestionsMenu) { |
|
976 suggestionsMenu.remove(); |
|
977 suggestionsMenu = null; |
|
978 } |
|
979 }); |
|
980 |
|
981 editor.on('change', checkIfFinished); |
|
982 |
|
983 this.getTextMatcher = getTextMatcher; |
|
984 this.getWordCharPattern = getWordCharPattern; |
|
985 this.markErrors = markErrors; |
|
986 this.getLanguage = function() { |
|
987 return settings.spellchecker_language; |
|
988 }; |
|
989 |
|
990 // Set default spellchecker language if it's not specified |
|
991 settings.spellchecker_language = settings.spellchecker_language || settings.language || 'en'; |
|
992 }); |
|
993 }); |
|
994 |
|
995 expose(["tinymce/spellcheckerplugin/DomTextMatcher"]); |
|
996 })(this); |
|