--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_skin/resources/js/ext/tinymce/dev/plugins/spellchecker/plugin.js Wed Jun 17 10:00:10 2015 +0200
@@ -0,0 +1,996 @@
+/**
+ * Compiled inline version. (Library mode)
+ */
+
+/*jshint smarttabs:true, undef:true, latedef:true, curly:true, bitwise:true, camelcase:true */
+/*globals $code */
+
+(function(exports, undefined) {
+ "use strict";
+
+ var modules = {};
+
+ function require(ids, callback) {
+ var module, defs = [];
+
+ for (var i = 0; i < ids.length; ++i) {
+ module = modules[ids[i]] || resolve(ids[i]);
+ if (!module) {
+ throw 'module definition dependecy not found: ' + ids[i];
+ }
+
+ defs.push(module);
+ }
+
+ callback.apply(null, defs);
+ }
+
+ function define(id, dependencies, definition) {
+ if (typeof id !== 'string') {
+ throw 'invalid module definition, module id must be defined and be a string';
+ }
+
+ if (dependencies === undefined) {
+ throw 'invalid module definition, dependencies must be specified';
+ }
+
+ if (definition === undefined) {
+ throw 'invalid module definition, definition function must be specified';
+ }
+
+ require(dependencies, function() {
+ modules[id] = definition.apply(null, arguments);
+ });
+ }
+
+ function defined(id) {
+ return !!modules[id];
+ }
+
+ function resolve(id) {
+ var target = exports;
+ var fragments = id.split(/[.\/]/);
+
+ for (var fi = 0; fi < fragments.length; ++fi) {
+ if (!target[fragments[fi]]) {
+ return;
+ }
+
+ target = target[fragments[fi]];
+ }
+
+ return target;
+ }
+
+ function expose(ids) {
+ for (var i = 0; i < ids.length; i++) {
+ var target = exports;
+ var id = ids[i];
+ var fragments = id.split(/[.\/]/);
+
+ for (var fi = 0; fi < fragments.length - 1; ++fi) {
+ if (target[fragments[fi]] === undefined) {
+ target[fragments[fi]] = {};
+ }
+
+ target = target[fragments[fi]];
+ }
+
+ target[fragments[fragments.length - 1]] = modules[id];
+ }
+ }
+
+// Included from: js/tinymce/plugins/spellchecker/classes/DomTextMatcher.js
+
+/**
+ * DomTextMatcher.js
+ *
+ * Copyright, Moxiecode Systems AB
+ * Released under LGPL License.
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/*eslint no-labels:0, no-constant-condition: 0 */
+
+/**
+ * This class logic for filtering text and matching words.
+ *
+ * @class tinymce.spellcheckerplugin.TextFilter
+ * @private
+ */
+define("tinymce/spellcheckerplugin/DomTextMatcher", [], function() {
+ // Based on work developed by: James Padolsey http://james.padolsey.com
+ // released under UNLICENSE that is compatible with LGPL
+ // TODO: Handle contentEditable edgecase:
+ // <p>text<span contentEditable="false">text<span contentEditable="true">text</span>text</span>text</p>
+ return function(node, editor) {
+ var m, matches = [], text, dom = editor.dom;
+ var blockElementsMap, hiddenTextElementsMap, shortEndedElementsMap;
+
+ blockElementsMap = editor.schema.getBlockElements(); // H1-H6, P, TD etc
+ hiddenTextElementsMap = editor.schema.getWhiteSpaceElements(); // TEXTAREA, PRE, STYLE, SCRIPT
+ shortEndedElementsMap = editor.schema.getShortEndedElements(); // BR, IMG, INPUT
+
+ function createMatch(m, data) {
+ if (!m[0]) {
+ throw 'findAndReplaceDOMText cannot handle zero-length matches';
+ }
+
+ return {
+ start: m.index,
+ end: m.index + m[0].length,
+ text: m[0],
+ data: data
+ };
+ }
+
+ function getText(node) {
+ var txt;
+
+ if (node.nodeType === 3) {
+ return node.data;
+ }
+
+ if (hiddenTextElementsMap[node.nodeName] && !blockElementsMap[node.nodeName]) {
+ return '';
+ }
+
+ txt = '';
+
+ if (blockElementsMap[node.nodeName] || shortEndedElementsMap[node.nodeName]) {
+ txt += '\n';
+ }
+
+ if ((node = node.firstChild)) {
+ do {
+ txt += getText(node);
+ } while ((node = node.nextSibling));
+ }
+
+ return txt;
+ }
+
+ function stepThroughMatches(node, matches, replaceFn) {
+ var startNode, endNode, startNodeIndex,
+ endNodeIndex, innerNodes = [], atIndex = 0, curNode = node,
+ matchLocation, matchIndex = 0;
+
+ matches = matches.slice(0);
+ matches.sort(function(a, b) {
+ return a.start - b.start;
+ });
+
+ matchLocation = matches.shift();
+
+ out: while (true) {
+ if (blockElementsMap[curNode.nodeName] || shortEndedElementsMap[curNode.nodeName]) {
+ atIndex++;
+ }
+
+ if (curNode.nodeType === 3) {
+ if (!endNode && curNode.length + atIndex >= matchLocation.end) {
+ // We've found the ending
+ endNode = curNode;
+ endNodeIndex = matchLocation.end - atIndex;
+ } else if (startNode) {
+ // Intersecting node
+ innerNodes.push(curNode);
+ }
+
+ if (!startNode && curNode.length + atIndex > matchLocation.start) {
+ // We've found the match start
+ startNode = curNode;
+ startNodeIndex = matchLocation.start - atIndex;
+ }
+
+ atIndex += curNode.length;
+ }
+
+ if (startNode && endNode) {
+ curNode = replaceFn({
+ startNode: startNode,
+ startNodeIndex: startNodeIndex,
+ endNode: endNode,
+ endNodeIndex: endNodeIndex,
+ innerNodes: innerNodes,
+ match: matchLocation.text,
+ matchIndex: matchIndex
+ });
+
+ // replaceFn has to return the node that replaced the endNode
+ // and then we step back so we can continue from the end of the
+ // match:
+ atIndex -= (endNode.length - endNodeIndex);
+ startNode = null;
+ endNode = null;
+ innerNodes = [];
+ matchLocation = matches.shift();
+ matchIndex++;
+
+ if (!matchLocation) {
+ break; // no more matches
+ }
+ } else if ((!hiddenTextElementsMap[curNode.nodeName] || blockElementsMap[curNode.nodeName]) && curNode.firstChild) {
+ // Move down
+ curNode = curNode.firstChild;
+ continue;
+ } else if (curNode.nextSibling) {
+ // Move forward:
+ curNode = curNode.nextSibling;
+ continue;
+ }
+
+ // Move forward or up:
+ while (true) {
+ if (curNode.nextSibling) {
+ curNode = curNode.nextSibling;
+ break;
+ } else if (curNode.parentNode !== node) {
+ curNode = curNode.parentNode;
+ } else {
+ break out;
+ }
+ }
+ }
+ }
+
+ /**
+ * Generates the actual replaceFn which splits up text nodes
+ * and inserts the replacement element.
+ */
+ function genReplacer(callback) {
+ function makeReplacementNode(fill, matchIndex) {
+ var match = matches[matchIndex];
+
+ if (!match.stencil) {
+ match.stencil = callback(match);
+ }
+
+ var clone = match.stencil.cloneNode(false);
+ clone.setAttribute('data-mce-index', matchIndex);
+
+ if (fill) {
+ clone.appendChild(dom.doc.createTextNode(fill));
+ }
+
+ return clone;
+ }
+
+ return function(range) {
+ var before, after, parentNode, startNode = range.startNode,
+ endNode = range.endNode, matchIndex = range.matchIndex,
+ doc = dom.doc;
+
+ if (startNode === endNode) {
+ var node = startNode;
+
+ parentNode = node.parentNode;
+ if (range.startNodeIndex > 0) {
+ // Add "before" text node (before the match)
+ before = doc.createTextNode(node.data.substring(0, range.startNodeIndex));
+ parentNode.insertBefore(before, node);
+ }
+
+ // Create the replacement node:
+ var el = makeReplacementNode(range.match, matchIndex);
+ parentNode.insertBefore(el, node);
+ if (range.endNodeIndex < node.length) {
+ // Add "after" text node (after the match)
+ after = doc.createTextNode(node.data.substring(range.endNodeIndex));
+ parentNode.insertBefore(after, node);
+ }
+
+ node.parentNode.removeChild(node);
+
+ return el;
+ } else {
+ // Replace startNode -> [innerNodes...] -> endNode (in that order)
+ before = doc.createTextNode(startNode.data.substring(0, range.startNodeIndex));
+ after = doc.createTextNode(endNode.data.substring(range.endNodeIndex));
+ var elA = makeReplacementNode(startNode.data.substring(range.startNodeIndex), matchIndex);
+ var innerEls = [];
+
+ for (var i = 0, l = range.innerNodes.length; i < l; ++i) {
+ var innerNode = range.innerNodes[i];
+ var innerEl = makeReplacementNode(innerNode.data, matchIndex);
+ innerNode.parentNode.replaceChild(innerEl, innerNode);
+ innerEls.push(innerEl);
+ }
+
+ var elB = makeReplacementNode(endNode.data.substring(0, range.endNodeIndex), matchIndex);
+
+ parentNode = startNode.parentNode;
+ parentNode.insertBefore(before, startNode);
+ parentNode.insertBefore(elA, startNode);
+ parentNode.removeChild(startNode);
+
+ parentNode = endNode.parentNode;
+ parentNode.insertBefore(elB, endNode);
+ parentNode.insertBefore(after, endNode);
+ parentNode.removeChild(endNode);
+
+ return elB;
+ }
+ };
+ }
+
+ function unwrapElement(element) {
+ var parentNode = element.parentNode;
+ parentNode.insertBefore(element.firstChild, element);
+ element.parentNode.removeChild(element);
+ }
+
+ function getWrappersByIndex(index) {
+ var elements = node.getElementsByTagName('*'), wrappers = [];
+
+ index = typeof index == "number" ? "" + index : null;
+
+ for (var i = 0; i < elements.length; i++) {
+ var element = elements[i], dataIndex = element.getAttribute('data-mce-index');
+
+ if (dataIndex !== null && dataIndex.length) {
+ if (dataIndex === index || index === null) {
+ wrappers.push(element);
+ }
+ }
+ }
+
+ return wrappers;
+ }
+
+ /**
+ * Returns the index of a specific match object or -1 if it isn't found.
+ *
+ * @param {Match} match Text match object.
+ * @return {Number} Index of match or -1 if it isn't found.
+ */
+ function indexOf(match) {
+ var i = matches.length;
+ while (i--) {
+ if (matches[i] === match) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Filters the matches. If the callback returns true it stays if not it gets removed.
+ *
+ * @param {Function} callback Callback to execute for each match.
+ * @return {DomTextMatcher} Current DomTextMatcher instance.
+ */
+ function filter(callback) {
+ var filteredMatches = [];
+
+ each(function(match, i) {
+ if (callback(match, i)) {
+ filteredMatches.push(match);
+ }
+ });
+
+ matches = filteredMatches;
+
+ /*jshint validthis:true*/
+ return this;
+ }
+
+ /**
+ * Executes the specified callback for each match.
+ *
+ * @param {Function} callback Callback to execute for each match.
+ * @return {DomTextMatcher} Current DomTextMatcher instance.
+ */
+ function each(callback) {
+ for (var i = 0, l = matches.length; i < l; i++) {
+ if (callback(matches[i], i) === false) {
+ break;
+ }
+ }
+
+ /*jshint validthis:true*/
+ return this;
+ }
+
+ /**
+ * Wraps the current matches with nodes created by the specified callback.
+ * Multiple clones of these matches might occur on matches that are on multiple nodex.
+ *
+ * @param {Function} callback Callback to execute in order to create elements for matches.
+ * @return {DomTextMatcher} Current DomTextMatcher instance.
+ */
+ function wrap(callback) {
+ if (matches.length) {
+ stepThroughMatches(node, matches, genReplacer(callback));
+ }
+
+ /*jshint validthis:true*/
+ return this;
+ }
+
+ /**
+ * Finds the specified regexp and adds them to the matches collection.
+ *
+ * @param {RegExp} regex Global regexp to search the current node by.
+ * @param {Object} [data] Optional custom data element for the match.
+ * @return {DomTextMatcher} Current DomTextMatcher instance.
+ */
+ function find(regex, data) {
+ if (text && regex.global) {
+ while ((m = regex.exec(text))) {
+ matches.push(createMatch(m, data));
+ }
+ }
+
+ return this;
+ }
+
+ /**
+ * Unwraps the specified match object or all matches if unspecified.
+ *
+ * @param {Object} [match] Optional match object.
+ * @return {DomTextMatcher} Current DomTextMatcher instance.
+ */
+ function unwrap(match) {
+ var i, elements = getWrappersByIndex(match ? indexOf(match) : null);
+
+ i = elements.length;
+ while (i--) {
+ unwrapElement(elements[i]);
+ }
+
+ return this;
+ }
+
+ /**
+ * Returns a match object by the specified DOM element.
+ *
+ * @param {DOMElement} element Element to return match object for.
+ * @return {Object} Match object for the specified element.
+ */
+ function matchFromElement(element) {
+ return matches[element.getAttribute('data-mce-index')];
+ }
+
+ /**
+ * Returns a DOM element from the specified match element. This will be the first element if it's split
+ * on multiple nodes.
+ *
+ * @param {Object} match Match element to get first element of.
+ * @return {DOMElement} DOM element for the specified match object.
+ */
+ function elementFromMatch(match) {
+ return getWrappersByIndex(indexOf(match))[0];
+ }
+
+ /**
+ * Adds match the specified range for example a grammar line.
+ *
+ * @param {Number} start Start offset.
+ * @param {Number} length Length of the text.
+ * @param {Object} data Custom data object for match.
+ * @return {DomTextMatcher} Current DomTextMatcher instance.
+ */
+ function add(start, length, data) {
+ matches.push({
+ start: start,
+ end: start + length,
+ text: text.substr(start, length),
+ data: data
+ });
+
+ return this;
+ }
+
+ /**
+ * Returns a DOM range for the specified match.
+ *
+ * @param {Object} match Match object to get range for.
+ * @return {DOMRange} DOM Range for the specified match.
+ */
+ function rangeFromMatch(match) {
+ var wrappers = getWrappersByIndex(indexOf(match));
+
+ var rng = editor.dom.createRng();
+ rng.setStartBefore(wrappers[0]);
+ rng.setEndAfter(wrappers[wrappers.length - 1]);
+
+ return rng;
+ }
+
+ /**
+ * Replaces the specified match with the specified text.
+ *
+ * @param {Object} match Match object to replace.
+ * @param {String} text Text to replace the match with.
+ * @return {DOMRange} DOM range produced after the replace.
+ */
+ function replace(match, text) {
+ var rng = rangeFromMatch(match);
+
+ rng.deleteContents();
+
+ if (text.length > 0) {
+ rng.insertNode(editor.dom.doc.createTextNode(text));
+ }
+
+ return rng;
+ }
+
+ /**
+ * Resets the DomTextMatcher instance. This will remove any wrapped nodes and remove any matches.
+ *
+ * @return {[type]} [description]
+ */
+ function reset() {
+ matches.splice(0, matches.length);
+ unwrap();
+
+ return this;
+ }
+
+ text = getText(node);
+
+ return {
+ text: text,
+ matches: matches,
+ each: each,
+ filter: filter,
+ reset: reset,
+ matchFromElement: matchFromElement,
+ elementFromMatch: elementFromMatch,
+ find: find,
+ add: add,
+ wrap: wrap,
+ unwrap: unwrap,
+ replace: replace,
+ rangeFromMatch: rangeFromMatch,
+ indexOf: indexOf
+ };
+ };
+});
+
+// Included from: js/tinymce/plugins/spellchecker/classes/Plugin.js
+
+/**
+ * Plugin.js
+ *
+ * Copyright, Moxiecode Systems AB
+ * Released under LGPL License.
+ *
+ * License: http://www.tinymce.com/license
+ * Contributing: http://www.tinymce.com/contributing
+ */
+
+/*jshint camelcase:false */
+
+/**
+ * This class contains all core logic for the spellchecker plugin.
+ *
+ * @class tinymce.spellcheckerplugin.Plugin
+ * @private
+ */
+define("tinymce/spellcheckerplugin/Plugin", [
+ "tinymce/spellcheckerplugin/DomTextMatcher",
+ "tinymce/PluginManager",
+ "tinymce/util/Tools",
+ "tinymce/ui/Menu",
+ "tinymce/dom/DOMUtils",
+ "tinymce/util/XHR",
+ "tinymce/util/URI",
+ "tinymce/util/JSON"
+], function(DomTextMatcher, PluginManager, Tools, Menu, DOMUtils, XHR, URI, JSON) {
+ PluginManager.add('spellchecker', function(editor, url) {
+ var languageMenuItems, self = this, lastSuggestions, started, suggestionsMenu, settings = editor.settings;
+ var hasDictionarySupport;
+
+ function getTextMatcher() {
+ if (!self.textMatcher) {
+ self.textMatcher = new DomTextMatcher(editor.getBody(), editor);
+ }
+
+ return self.textMatcher;
+ }
+
+ function buildMenuItems(listName, languageValues) {
+ var items = [];
+
+ Tools.each(languageValues, function(languageValue) {
+ items.push({
+ selectable: true,
+ text: languageValue.name,
+ data: languageValue.value
+ });
+ });
+
+ return items;
+ }
+
+ var languagesString = settings.spellchecker_languages ||
+ 'English=en,Danish=da,Dutch=nl,Finnish=fi,French=fr_FR,' +
+ 'German=de,Italian=it,Polish=pl,Portuguese=pt_BR,' +
+ 'Spanish=es,Swedish=sv';
+
+ languageMenuItems = buildMenuItems('Language',
+ Tools.map(languagesString.split(','), function(langPair) {
+ langPair = langPair.split('=');
+
+ return {
+ name: langPair[0],
+ value: langPair[1]
+ };
+ })
+ );
+
+ function isEmpty(obj) {
+ /*jshint unused:false*/
+ /*eslint no-unused-vars:0 */
+ for (var name in obj) {
+ return false;
+ }
+
+ return true;
+ }
+
+ function showSuggestions(word, spans) {
+ var items = [], suggestions = lastSuggestions[word];
+
+ Tools.each(suggestions, function(suggestion) {
+ items.push({
+ text: suggestion,
+ onclick: function() {
+ editor.insertContent(editor.dom.encode(suggestion));
+ editor.dom.remove(spans);
+ checkIfFinished();
+ }
+ });
+ });
+
+ items.push({text: '-'});
+
+ if (hasDictionarySupport) {
+ items.push({text: 'Add to Dictionary', onclick: function() {
+ addToDictionary(word, spans);
+ }});
+ }
+
+ items.push.apply(items, [
+ {text: 'Ignore', onclick: function() {
+ ignoreWord(word, spans);
+ }},
+
+ {text: 'Ignore all', onclick: function() {
+ ignoreWord(word, spans, true);
+ }}
+ ]);
+
+ // Render menu
+ suggestionsMenu = new Menu({
+ items: items,
+ context: 'contextmenu',
+ onautohide: function(e) {
+ if (e.target.className.indexOf('spellchecker') != -1) {
+ e.preventDefault();
+ }
+ },
+ onhide: function() {
+ suggestionsMenu.remove();
+ suggestionsMenu = null;
+ }
+ });
+
+ suggestionsMenu.renderTo(document.body);
+
+ // Position menu
+ var pos = DOMUtils.DOM.getPos(editor.getContentAreaContainer());
+ var targetPos = editor.dom.getPos(spans[0]);
+ var root = editor.dom.getRoot();
+
+ // Adjust targetPos for scrolling in the editor
+ if (root.nodeName == 'BODY') {
+ targetPos.x -= root.ownerDocument.documentElement.scrollLeft || root.scrollLeft;
+ targetPos.y -= root.ownerDocument.documentElement.scrollTop || root.scrollTop;
+ } else {
+ targetPos.x -= root.scrollLeft;
+ targetPos.y -= root.scrollTop;
+ }
+
+ pos.x += targetPos.x;
+ pos.y += targetPos.y;
+
+ suggestionsMenu.moveTo(pos.x, pos.y + spans[0].offsetHeight);
+ }
+
+ function getWordCharPattern() {
+ // Regexp for finding word specific characters this will split words by
+ // spaces, quotes, copy right characters etc. It's escaped with unicode characters
+ // to make it easier to output scripts on servers using different encodings
+ // so if you add any characters outside the 128 byte range make sure to escape it
+ return editor.getParam('spellchecker_wordchar_pattern') || new RegExp("[^" +
+ "\\s!\"#$%&()*+,-./:;<=>?@[\\]^_{|}`" +
+ "\u00a7\u00a9\u00ab\u00ae\u00b1\u00b6\u00b7\u00b8\u00bb" +
+ "\u00bc\u00bd\u00be\u00bf\u00d7\u00f7\u00a4\u201d\u201c\u201e\u00a0\u2002\u2003\u2009" +
+ "]+", "g");
+ }
+
+ function defaultSpellcheckCallback(method, text, doneCallback, errorCallback) {
+ var data = {method: method}, postData = '';
+
+ if (method == "spellcheck") {
+ data.text = text;
+ data.lang = settings.spellchecker_language;
+ }
+
+ if (method == "addToDictionary") {
+ data.word = text;
+ }
+
+ Tools.each(data, function(value, key) {
+ if (postData) {
+ postData += '&';
+ }
+
+ postData += key + '=' + encodeURIComponent(value);
+ });
+
+ XHR.send({
+ url: new URI(url).toAbsolute(settings.spellchecker_rpc_url),
+ type: "post",
+ content_type: 'application/x-www-form-urlencoded',
+ data: postData,
+ success: function(result) {
+ result = JSON.parse(result);
+
+ if (!result) {
+ errorCallback("Sever response wasn't proper JSON.");
+ } else if (result.error) {
+ errorCallback(result.error);
+ } else {
+ doneCallback(result);
+ }
+ },
+ error: function(type, xhr) {
+ errorCallback("Spellchecker request error: " + xhr.status);
+ }
+ });
+ }
+
+ function sendRpcCall(name, data, successCallback, errorCallback) {
+ var spellCheckCallback = settings.spellchecker_callback || defaultSpellcheckCallback;
+ spellCheckCallback.call(self, name, data, successCallback, errorCallback);
+ }
+
+ function spellcheck() {
+ if (started) {
+ finish();
+ return;
+ } else {
+ finish();
+ }
+
+ function errorCallback(message) {
+ editor.windowManager.alert(message);
+ editor.setProgressState(false);
+ finish();
+ }
+
+ editor.setProgressState(true);
+ sendRpcCall("spellcheck", getTextMatcher().text, markErrors, errorCallback);
+ editor.focus();
+ }
+
+ function checkIfFinished() {
+ if (!editor.dom.select('span.mce-spellchecker-word').length) {
+ finish();
+ }
+ }
+
+ function addToDictionary(word, spans) {
+ editor.setProgressState(true);
+
+ sendRpcCall("addToDictionary", word, function() {
+ editor.setProgressState(false);
+ editor.dom.remove(spans, true);
+ checkIfFinished();
+ }, function(message) {
+ editor.windowManager.alert(message);
+ editor.setProgressState(false);
+ });
+ }
+
+ function ignoreWord(word, spans, all) {
+ editor.selection.collapse();
+
+ if (all) {
+ Tools.each(editor.dom.select('span.mce-spellchecker-word'), function(span) {
+ if (span.getAttribute('data-mce-word') == word) {
+ editor.dom.remove(span, true);
+ }
+ });
+ } else {
+ editor.dom.remove(spans, true);
+ }
+
+ checkIfFinished();
+ }
+
+ function finish() {
+ getTextMatcher().reset();
+ self.textMatcher = null;
+
+ if (started) {
+ started = false;
+ editor.fire('SpellcheckEnd');
+ }
+ }
+
+ function getElmIndex(elm) {
+ var value = elm.getAttribute('data-mce-index');
+
+ if (typeof value == "number") {
+ return "" + value;
+ }
+
+ return value;
+ }
+
+ function findSpansByIndex(index) {
+ var nodes, spans = [];
+
+ nodes = Tools.toArray(editor.getBody().getElementsByTagName('span'));
+ if (nodes.length) {
+ for (var i = 0; i < nodes.length; i++) {
+ var nodeIndex = getElmIndex(nodes[i]);
+
+ if (nodeIndex === null || !nodeIndex.length) {
+ continue;
+ }
+
+ if (nodeIndex === index.toString()) {
+ spans.push(nodes[i]);
+ }
+ }
+ }
+
+ return spans;
+ }
+
+ editor.on('click', function(e) {
+ var target = e.target;
+
+ if (target.className == "mce-spellchecker-word") {
+ e.preventDefault();
+
+ var spans = findSpansByIndex(getElmIndex(target));
+
+ if (spans.length > 0) {
+ var rng = editor.dom.createRng();
+ rng.setStartBefore(spans[0]);
+ rng.setEndAfter(spans[spans.length - 1]);
+ editor.selection.setRng(rng);
+ showSuggestions(target.getAttribute('data-mce-word'), spans);
+ }
+ }
+ });
+
+ editor.addMenuItem('spellchecker', {
+ text: 'Spellcheck',
+ context: 'tools',
+ onclick: spellcheck,
+ selectable: true,
+ onPostRender: function() {
+ var self = this;
+
+ self.active(started);
+
+ editor.on('SpellcheckStart SpellcheckEnd', function() {
+ self.active(started);
+ });
+ }
+ });
+
+ function updateSelection(e) {
+ var selectedLanguage = settings.spellchecker_language;
+
+ e.control.items().each(function(ctrl) {
+ ctrl.active(ctrl.settings.data === selectedLanguage);
+ });
+ }
+
+ /**
+ * Find the specified words and marks them. It will also show suggestions for those words.
+ *
+ * @example
+ * editor.plugins.spellchecker.markErrors({
+ * dictionary: true,
+ * words: {
+ * "word1": ["suggestion 1", "Suggestion 2"]
+ * }
+ * });
+ * @param {Object} data Data object containing the words with suggestions.
+ */
+ function markErrors(data) {
+ var suggestions;
+
+ if (data.words) {
+ hasDictionarySupport = !!data.dictionary;
+ suggestions = data.words;
+ } else {
+ // Fallback to old format
+ suggestions = data;
+ }
+
+ editor.setProgressState(false);
+
+ if (isEmpty(suggestions)) {
+ editor.windowManager.alert('No misspellings found');
+ started = false;
+ return;
+ }
+
+ lastSuggestions = suggestions;
+
+ getTextMatcher().find(getWordCharPattern()).filter(function(match) {
+ return !!suggestions[match.text];
+ }).wrap(function(match) {
+ return editor.dom.create('span', {
+ "class": 'mce-spellchecker-word',
+ "data-mce-bogus": 1,
+ "data-mce-word": match.text
+ });
+ });
+
+ started = true;
+ editor.fire('SpellcheckStart');
+ }
+
+ var buttonArgs = {
+ tooltip: 'Spellcheck',
+ onclick: spellcheck,
+ onPostRender: function() {
+ var self = this;
+
+ editor.on('SpellcheckStart SpellcheckEnd', function() {
+ self.active(started);
+ });
+ }
+ };
+
+ if (languageMenuItems.length > 1) {
+ buttonArgs.type = 'splitbutton';
+ buttonArgs.menu = languageMenuItems;
+ buttonArgs.onshow = updateSelection;
+ buttonArgs.onselect = function(e) {
+ settings.spellchecker_language = e.control.settings.data;
+ };
+ }
+
+ editor.addButton('spellchecker', buttonArgs);
+ editor.addCommand('mceSpellCheck', spellcheck);
+
+ editor.on('remove', function() {
+ if (suggestionsMenu) {
+ suggestionsMenu.remove();
+ suggestionsMenu = null;
+ }
+ });
+
+ editor.on('change', checkIfFinished);
+
+ this.getTextMatcher = getTextMatcher;
+ this.getWordCharPattern = getWordCharPattern;
+ this.markErrors = markErrors;
+ this.getLanguage = function() {
+ return settings.spellchecker_language;
+ };
+
+ // Set default spellchecker language if it's not specified
+ settings.spellchecker_language = settings.spellchecker_language || settings.language || 'en';
+ });
+});
+
+expose(["tinymce/spellcheckerplugin/DomTextMatcher"]);
+})(this);
\ No newline at end of file