2 * Compiled inline version. (Library mode)
5 /*jshint smarttabs:true, undef:true, latedef:true, curly:true, bitwise:true, camelcase:true */
8 (function(exports, undefined) {
13 function require(ids, callback) {
14 var module, defs = [];
16 for (var i = 0; i < ids.length; ++i) {
17 module = modules[ids[i]] || resolve(ids[i]);
19 throw 'module definition dependecy not found: ' + ids[i];
25 callback.apply(null, defs);
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';
33 if (dependencies === undefined) {
34 throw 'invalid module definition, dependencies must be specified';
37 if (definition === undefined) {
38 throw 'invalid module definition, definition function must be specified';
41 require(dependencies, function() {
42 modules[id] = definition.apply(null, arguments);
46 function defined(id) {
50 function resolve(id) {
52 var fragments = id.split(/[.\/]/);
54 for (var fi = 0; fi < fragments.length; ++fi) {
55 if (!target[fragments[fi]]) {
59 target = target[fragments[fi]];
65 function expose(ids) {
66 for (var i = 0; i < ids.length; i++) {
69 var fragments = id.split(/[.\/]/);
71 for (var fi = 0; fi < fragments.length - 1; ++fi) {
72 if (target[fragments[fi]] === undefined) {
73 target[fragments[fi]] = {};
76 target = target[fragments[fi]];
79 target[fragments[fragments.length - 1]] = modules[id];
83 // Included from: js/tinymce/plugins/spellchecker/classes/DomTextMatcher.js
88 * Copyright, Moxiecode Systems AB
89 * Released under LGPL License.
91 * License: http://www.tinymce.com/license
92 * Contributing: http://www.tinymce.com/contributing
96 * This class logic for filtering text and matching words.
98 * @class tinymce.spellcheckerplugin.TextFilter
101 define("tinymce/spellcheckerplugin/DomTextMatcher", [], function() {
102 // Based on work developed by: James Padolsey http://james.padolsey.com
103 // released under UNLICENSE that is compatible with LGPL
104 // TODO: Handle contentEditable edgecase:
105 // <p>text<span contentEditable="false">text<span contentEditable="true">text</span>text</span>text</p>
106 return function(regex, node, schema) {
107 var m, matches = [], text, count = 0, doc;
108 var blockElementsMap, hiddenTextElementsMap, shortEndedElementsMap;
110 doc = node.ownerDocument;
111 blockElementsMap = schema.getBlockElements(); // H1-H6, P, TD etc
112 hiddenTextElementsMap = schema.getWhiteSpaceElements(); // TEXTAREA, PRE, STYLE, SCRIPT
113 shortEndedElementsMap = schema.getShortEndedElements(); // BR, IMG, INPUT
115 function getMatchIndexes(m) {
117 throw 'findAndReplaceDOMText cannot handle zero-length matches';
122 return [index, index + m[0].length, [m[0]]];
125 function getText(node) {
128 if (node.nodeType === 3) {
132 if (hiddenTextElementsMap[node.nodeName]) {
138 if (blockElementsMap[node.nodeName] || shortEndedElementsMap[node.nodeName]) {
142 if ((node = node.firstChild)) {
144 txt += getText(node);
145 } while ((node = node.nextSibling));
151 function stepThroughMatches(node, matches, replaceFn) {
152 var startNode, endNode, startNodeIndex,
153 endNodeIndex, innerNodes = [], atIndex = 0, curNode = node,
154 matchLocation = matches.shift(), matchIndex = 0;
157 if (blockElementsMap[curNode.nodeName] || shortEndedElementsMap[curNode.nodeName]) {
161 if (curNode.nodeType === 3) {
162 if (!endNode && curNode.length + atIndex >= matchLocation[1]) {
163 // We've found the ending
165 endNodeIndex = matchLocation[1] - atIndex;
166 } else if (startNode) {
168 innerNodes.push(curNode);
171 if (!startNode && curNode.length + atIndex > matchLocation[0]) {
172 // We've found the match start
174 startNodeIndex = matchLocation[0] - atIndex;
177 atIndex += curNode.length;
180 if (startNode && endNode) {
181 curNode = replaceFn({
182 startNode: startNode,
183 startNodeIndex: startNodeIndex,
185 endNodeIndex: endNodeIndex,
186 innerNodes: innerNodes,
187 match: matchLocation[2],
188 matchIndex: matchIndex
191 // replaceFn has to return the node that replaced the endNode
192 // and then we step back so we can continue from the end of the
194 atIndex -= (endNode.length - endNodeIndex);
198 matchLocation = matches.shift();
201 if (!matchLocation) {
202 break; // no more matches
204 } else if (!hiddenTextElementsMap[curNode.nodeName] && curNode.firstChild) {
206 curNode = curNode.firstChild;
208 } else if (curNode.nextSibling) {
210 curNode = curNode.nextSibling;
214 // Move forward or up:
216 if (curNode.nextSibling) {
217 curNode = curNode.nextSibling;
219 } else if (curNode.parentNode !== node) {
220 curNode = curNode.parentNode;
229 * Generates the actual replaceFn which splits up text nodes
230 * and inserts the replacement element.
232 function genReplacer(nodeName) {
233 var makeReplacementNode;
235 if (typeof nodeName != 'function') {
236 var stencilNode = nodeName.nodeType ? nodeName : doc.createElement(nodeName);
238 makeReplacementNode = function(fill, matchIndex) {
239 var clone = stencilNode.cloneNode(false);
241 clone.setAttribute('data-mce-index', matchIndex);
244 clone.appendChild(doc.createTextNode(fill));
250 makeReplacementNode = nodeName;
253 return function replace(range) {
254 var before, after, parentNode, startNode = range.startNode,
255 endNode = range.endNode, matchIndex = range.matchIndex;
257 if (startNode === endNode) {
258 var node = startNode;
260 parentNode = node.parentNode;
261 if (range.startNodeIndex > 0) {
262 // Add `before` text node (before the match)
263 before = doc.createTextNode(node.data.substring(0, range.startNodeIndex));
264 parentNode.insertBefore(before, node);
267 // Create the replacement node:
268 var el = makeReplacementNode(range.match[0], matchIndex);
269 parentNode.insertBefore(el, node);
270 if (range.endNodeIndex < node.length) {
271 // Add `after` text node (after the match)
272 after = doc.createTextNode(node.data.substring(range.endNodeIndex));
273 parentNode.insertBefore(after, node);
276 node.parentNode.removeChild(node);
280 // Replace startNode -> [innerNodes...] -> endNode (in that order)
281 before = doc.createTextNode(startNode.data.substring(0, range.startNodeIndex));
282 after = doc.createTextNode(endNode.data.substring(range.endNodeIndex));
283 var elA = makeReplacementNode(startNode.data.substring(range.startNodeIndex), matchIndex);
286 for (var i = 0, l = range.innerNodes.length; i < l; ++i) {
287 var innerNode = range.innerNodes[i];
288 var innerEl = makeReplacementNode(innerNode.data, matchIndex);
289 innerNode.parentNode.replaceChild(innerEl, innerNode);
290 innerEls.push(innerEl);
293 var elB = makeReplacementNode(endNode.data.substring(0, range.endNodeIndex), matchIndex);
295 parentNode = startNode.parentNode;
296 parentNode.insertBefore(before, startNode);
297 parentNode.insertBefore(elA, startNode);
298 parentNode.removeChild(startNode);
300 parentNode = endNode.parentNode;
301 parentNode.insertBefore(elB, endNode);
302 parentNode.insertBefore(after, endNode);
303 parentNode.removeChild(endNode);
310 text = getText(node);
311 if (text && regex.global) {
312 while ((m = regex.exec(text))) {
313 matches.push(getMatchIndexes(m));
317 function filter(callback) {
318 var filteredMatches = [];
320 each(function(match, i) {
321 if (callback(match, i)) {
322 filteredMatches.push(match);
326 matches = filteredMatches;
328 /*jshint validthis:true*/
332 function each(callback) {
333 for (var i = 0, l = matches.length; i < l; i++) {
334 if (callback(matches[i], i) === false) {
339 /*jshint validthis:true*/
343 function mark(replacementNode) {
344 if (matches.length) {
345 count = matches.length;
346 stepThroughMatches(node, matches, genReplacer(replacementNode));
349 /*jshint validthis:true*/
364 // Included from: js/tinymce/plugins/spellchecker/classes/Plugin.js
369 * Copyright, Moxiecode Systems AB
370 * Released under LGPL License.
372 * License: http://www.tinymce.com/license
373 * Contributing: http://www.tinymce.com/contributing
376 /*jshint camelcase:false */
379 * This class contains all core logic for the spellchecker plugin.
381 * @class tinymce.spellcheckerplugin.Plugin
384 define("tinymce/spellcheckerplugin/Plugin", [
385 "tinymce/spellcheckerplugin/DomTextMatcher",
386 "tinymce/PluginManager",
387 "tinymce/util/Tools",
389 "tinymce/dom/DOMUtils",
390 "tinymce/util/JSONRequest",
392 ], function(DomTextMatcher, PluginManager, Tools, Menu, DOMUtils, JSONRequest, URI) {
393 PluginManager.add('spellchecker', function(editor, url) {
394 var lastSuggestions, started, suggestionsMenu, settings = editor.settings;
396 function isEmpty(obj) {
397 /*jshint unused:false*/
398 for (var name in obj) {
405 function showSuggestions(target, word) {
406 var items = [], suggestions = lastSuggestions[word];
408 Tools.each(suggestions, function(suggestion) {
411 onclick: function() {
412 editor.insertContent(suggestion);
418 items.push.apply(items, [
421 {text: 'Ignore', onclick: function() {
422 ignoreWord(target, word);
425 {text: 'Ignore all', onclick: function() {
426 ignoreWord(target, word, true);
429 {text: 'Finish', onclick: finish}
433 suggestionsMenu = new Menu({
435 context: 'contextmenu',
436 onautohide: function(e) {
437 if (e.target.className.indexOf('spellchecker') != -1) {
442 suggestionsMenu.remove();
443 suggestionsMenu = null;
447 suggestionsMenu.renderTo(document.body);
450 var pos = DOMUtils.DOM.getPos(editor.getContentAreaContainer());
451 var targetPos = editor.dom.getPos(target);
453 pos.x += targetPos.x;
454 pos.y += targetPos.y;
456 suggestionsMenu.moveTo(pos.x, pos.y + target.offsetHeight);
459 function spellcheck() {
460 var textFilter, words = [], uniqueWords = {};
469 function doneCallback(suggestions) {
470 editor.setProgressState(false);
472 if (isEmpty(suggestions)) {
473 editor.windowManager.alert('No misspellings found');
478 lastSuggestions = suggestions;
480 textFilter.filter(function(match) {
481 return !!suggestions[match[2][0]];
482 }).mark(editor.dom.create('span', {
483 "class": 'mce-spellchecker-word',
488 editor.fire('SpellcheckStart');
491 // Regexp for finding word specific characters this will split words by
492 // spaces, quotes, copy right characters etc. It's escaped with unicode characters
493 // to make it easier to output scripts on servers using different encodings
494 // so if you add any characters outside the 128 byte range make sure to escape it
495 var nonWordSeparatorCharacters = editor.getParam('spellchecker_wordchar_pattern') || new RegExp("[^" +
496 "\\s!\"#$%&()*+,-./:;<=>?@[\\]^_{|}`" +
497 "\u00a7\u00a9\u00ab\u00ae\u00b1\u00b6\u00b7\u00b8\u00bb" +
498 "\u00bc\u00bd\u00be\u00bf\u00d7\u00f7\u00a4\u201d\u201c\u201e" +
501 // Find all words and make an unique words array
502 textFilter = new DomTextMatcher(nonWordSeparatorCharacters, editor.getBody(), editor.schema).each(function(match) {
503 var word = match[2][0];
505 // TODO: Fix so it remembers correctly spelled words
506 if (!uniqueWords[word]) {
507 // Ignore numbers and single character words
508 if (/^\d+$/.test(word) || word.length == 1) {
513 uniqueWords[word] = true;
517 function defaultSpellcheckCallback(method, words, doneCallback) {
518 JSONRequest.sendRPC({
519 url: new URI(url).toAbsolute(settings.spellchecker_rpc_url),
522 lang: settings.spellchecker_language || "en",
525 success: function(result) {
526 doneCallback(result);
528 error: function(error, xhr) {
529 if (error == "JSON Parse error.") {
530 error = "Non JSON response:" + xhr.responseText;
532 error = "Error: " + error;
535 editor.windowManager.alert(error);
536 editor.setProgressState(false);
543 editor.setProgressState(true);
545 var spellCheckCallback = settings.spellchecker_callback || defaultSpellcheckCallback;
546 spellCheckCallback("spellcheck", words, doneCallback);
549 function checkIfFinished() {
550 if (!editor.dom.select('span.mce-spellchecker-word').length) {
555 function unwrap(node) {
556 var parentNode = node.parentNode;
557 parentNode.insertBefore(node.firstChild, node);
558 node.parentNode.removeChild(node);
561 function ignoreWord(target, word, all) {
563 Tools.each(editor.dom.select('span.mce-spellchecker-word'), function(item) {
564 var text = item.innerText || item.textContent;
581 node = editor.getBody();
582 nodes = node.getElementsByTagName('span');
586 if (node.getAttribute('data-mce-index')) {
591 editor.fire('SpellcheckEnd');
594 function selectMatch(index) {
595 var nodes, i, spanElm, spanIndex = -1, startContainer, endContainer;
598 nodes = editor.getBody().getElementsByTagName("span");
599 for (i = 0; i < nodes.length; i++) {
601 if (spanElm.className == "mce-spellchecker-word") {
602 spanIndex = spanElm.getAttribute('data-mce-index');
603 if (spanIndex === index) {
606 if (!startContainer) {
607 startContainer = spanElm.firstChild;
610 endContainer = spanElm.firstChild;
613 if (spanIndex !== index && endContainer) {
619 var rng = editor.dom.createRng();
620 rng.setStart(startContainer, 0);
621 rng.setEnd(endContainer, endContainer.length);
622 editor.selection.setRng(rng);
627 editor.on('click', function(e) {
628 if (e.target.className == "mce-spellchecker-word") {
631 var rng = selectMatch(e.target.getAttribute('data-mce-index'));
632 showSuggestions(e.target, rng.toString());
636 editor.addMenuItem('spellchecker', {
641 onPostRender: function() {
644 editor.on('SpellcheckStart SpellcheckEnd', function() {
645 self.active(started);
650 editor.addButton('spellchecker', {
651 tooltip: 'Spellcheck',
653 onPostRender: function() {
656 editor.on('SpellcheckStart SpellcheckEnd', function() {
657 self.active(started);
662 editor.on('remove', function() {
663 if (suggestionsMenu) {
664 suggestionsMenu.remove();
665 suggestionsMenu = null;
671 expose(["tinymce/spellcheckerplugin/DomTextMatcher","tinymce/spellcheckerplugin/Plugin"]);