src/pyams_skin/resources/js/ext/tinymce/dev/plugins/spellchecker/plugin.js
changeset 557 bca7a7e058a3
equal deleted inserted replaced
-1:000000000000 557:bca7a7e058a3
       
     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);