krafthaus/bauhaus

View on GitHub
bower_components/tinymce/plugins/spellchecker/plugin.js

Summary

Maintainability
F
1 wk
Test Coverage
/**
 * 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);
                }},

                {text: 'Finish', onclick: finish}
            ]);

            // 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" +
            "]+", "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();
            }

            started = true;

            function doneCallback(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
                    });
                });

                editor.fire('SpellcheckStart');
            }

            function errorCallback(message) {
                editor.windowManager.alert(message);
                editor.setProgressState(false);
                finish();
            }

            editor.setProgressState(true);
            sendRpcCall("spellcheck", getTextMatcher().text, doneCallback, 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;

                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);
            });
        }

        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.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);