(function () {
    var defs = {}; // id -> {dependencies, definition, instance (possibly undefined)}

// Used when there is no 'main' module.
// The name is probably (hopefully) unique so minification removes for releases.
    var register_3795 = function (id) {
        var module = dem(id);
        var fragments = id.split('.');
        var target = Function('return this;')();
        for (var i = 0; i < fragments.length - 1; ++i) {
            if (target[fragments[i]] === undefined) { target[fragments[i]] = {}; }
            target = target[fragments[i]];
        }
        target[fragments[fragments.length - 1]] = module;
    };

    var instantiate = function (id) {
        var actual = defs[id];
        var dependencies = actual.deps;
        var definition = actual.defn;
        var len = dependencies.length;
        var instances = new Array(len);
        for (var i = 0; i < len; ++i) { instances[i] = dem(dependencies[i]); }
        var defResult = definition.apply(null, instances);
        if (defResult === undefined) { throw 'module [' + id + '] returned undefined'; }
        actual.instance = defResult;
    };

    var def = function (id, dependencies, definition) {
        if (typeof id !== 'string') { throw 'module id must be a string'; } else if (dependencies === undefined) { throw 'no dependencies for ' + id; } else if (definition === undefined) { throw 'no definition function for ' + id; }
        defs[id] = {
            deps: dependencies,
            defn: definition,
            instance: undefined
        };
    };

    var dem = function (id) {
        var actual = defs[id];
        if (actual === undefined) { throw 'module [' + id + '] was undefined'; } else if (actual.instance === undefined) { instantiate(id); }
        return actual.instance;
    };

    var req = function (ids, callback) {
        var len = ids.length;
        var instances = new Array(len);
        for (var i = 0; i < len; ++i) { instances[i] = dem(ids[i]); }
        callback.apply(null, instances);
    };

    var ephox = {};

    ephox.bolt = {
        module: {
            api: {
                define: def,
                require: req,
                demand: dem
            }
        }
    };

    var define = def;
    var require = req;
    var demand = dem;
// this helps with minification when using a lot of global references
    var defineGlobal = function (id, ref) {
        define(id, [], function () { return ref; });
    };
/* jsc
["tinymce.plugins.spellchecker.Plugin","ephox.katamari.api.Cell","tinymce.core.PluginManager","tinymce.plugins.spellchecker.alien.DetectProPlugin","tinymce.plugins.spellchecker.api.Api","tinymce.plugins.spellchecker.api.Commands","tinymce.plugins.spellchecker.api.Settings","tinymce.plugins.spellchecker.ui.Buttons","tinymce.plugins.spellchecker.ui.SuggestionsMenu","global!tinymce.util.Tools.resolve","global!window","tinymce.plugins.spellchecker.core.Actions","tinymce.core.util.Tools","global!document","tinymce.core.dom.DOMUtils","tinymce.core.ui.Factory","tinymce.core.util.URI","tinymce.core.util.XHR","tinymce.plugins.spellchecker.api.Events","tinymce.plugins.spellchecker.core.DomTextMatcher"]
jsc */
    define(
  'ephox.katamari.api.Cell',

        [
        ],

  function () {
      var Cell = function (initial) {
          var value = initial;

          var get = function () {
              return value;
          };

          var set = function (v) {
              value = v;
          };

          var clone = function () {
              return Cell(get());
          };

          return {
              get: get,
              set: set,
              clone: clone
          };
      };

      return Cell;
  }
);

    defineGlobal('global!tinymce.util.Tools.resolve', tinymce.util.Tools.resolve);
/**
 * ResolveGlobal.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
  'tinymce.core.PluginManager',
        [
            'global!tinymce.util.Tools.resolve'
        ],
  function (resolve) {
      return resolve('tinymce.PluginManager');
  }
);

    defineGlobal('global!window', window);
/**
 * DetectProPlugin.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
  'tinymce.plugins.spellchecker.alien.DetectProPlugin',
        [
            'global!window',
            'tinymce.core.PluginManager'
        ],
  function (window, PluginManager) {
      var hasProPlugin = function (editor) {
      // draw back if power version is requested and registered
          if (/(^|[ ,])tinymcespellchecker([, ]|$)/.test(editor.settings.plugins) && PluginManager.get('tinymcespellchecker')) {
        /* eslint no-console:0 */
              if (typeof window.console !== 'undefined' && window.console.log) {
                  window.console.log(
            'Spell Checker Pro is incompatible with Spell Checker plugin! ' +
            'Remove \'spellchecker\' from the \'plugins\' option.'
          );
              }
              return true;
          } else {
              return false;
          }
      };

      return {
          hasProPlugin: hasProPlugin
      };
  }
);

/**
 * Settings.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
  'tinymce.plugins.spellchecker.api.Settings',
        [
        ],
  function () {
      var getLanguages = function (editor) {
          var defaultLanguages = 'English=en,Danish=da,Dutch=nl,Finnish=fi,French=fr_FR,German=de,Italian=it,Polish=pl,Portuguese=pt_BR,Spanish=es,Swedish=sv';
          return editor.getParam('spellchecker_languages', defaultLanguages);
      };

      var getLanguage = function (editor) {
          var defaultLanguage = editor.getParam('language', 'en');
          return editor.getParam('spellchecker_language', defaultLanguage);
      };

      var getRpcUrl = function (editor) {
          return editor.getParam('spellchecker_rpc_url');
      };

      var getSpellcheckerCallback = function (editor) {
          return editor.getParam('spellchecker_callback');
      };

      var getSpellcheckerWordcharPattern = function (editor) {
          var defaultPattern = 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');
          return editor.getParam('spellchecker_wordchar_pattern', defaultPattern);
      };

      return {
          getLanguages: getLanguages,
          getLanguage: getLanguage,
          getRpcUrl: getRpcUrl,
          getSpellcheckerCallback: getSpellcheckerCallback,
          getSpellcheckerWordcharPattern: getSpellcheckerWordcharPattern
      };
  }
);
/**
 * ResolveGlobal.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
  'tinymce.core.util.Tools',
        [
            'global!tinymce.util.Tools.resolve'
        ],
  function (resolve) {
      return resolve('tinymce.util.Tools');
  }
);

/**
 * ResolveGlobal.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
  'tinymce.core.util.URI',
        [
            'global!tinymce.util.Tools.resolve'
        ],
  function (resolve) {
      return resolve('tinymce.util.URI');
  }
);

/**
 * ResolveGlobal.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
  'tinymce.core.util.XHR',
        [
            'global!tinymce.util.Tools.resolve'
        ],
  function (resolve) {
      return resolve('tinymce.util.XHR');
  }
);

/**
 * Events.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
  'tinymce.plugins.spellchecker.api.Events',
        [
        ],
  function () {
      var fireSpellcheckStart = function (editor) {
          return editor.fire('SpellcheckStart');
      };

      var fireSpellcheckEnd = function (editor) {
          return editor.fire('SpellcheckEnd');
      };

      return {
          fireSpellcheckStart: fireSpellcheckStart,
          fireSpellcheckEnd: fireSpellcheckEnd
      };
  }
);
/**
 * DomTextMatcher.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
  'tinymce.plugins.spellchecker.core.DomTextMatcher',
        [
        ],
  function () {
      function isContentEditableFalse (node) {
          return node && node.nodeType === 1 && node.contentEditable === 'false';
      }

    // 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 '';
              }

              if (isContentEditableFalse(node)) {
                  return '\n';
              }

              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) { // eslint-disable-line no-constant-condition
                  if (blockElementsMap[curNode.nodeName] || shortEndedElementsMap[curNode.nodeName] || isContentEditableFalse(curNode)) {
                      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) {
                      if (!isContentEditableFalse(curNode)) {
              // Move down
                          curNode = curNode.firstChild;
                          continue;
                      }
                  } else if (curNode.nextSibling) {
            // Move forward:
                      curNode = curNode.nextSibling;
                      continue;
                  }

          // Move forward or up:
                  while (true) { // eslint-disable-line no-constant-condition
                      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;
                  }

          // 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 hasClass (elm) {
              return elm.className.indexOf('mce-spellchecker-word') !== -1;
          }

          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 && hasClass(element)) {
                      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
          };
      };
  }
);
/**
 * Actions.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
  'tinymce.plugins.spellchecker.core.Actions',
        [
            'tinymce.core.util.Tools',
            'tinymce.core.util.URI',
            'tinymce.core.util.XHR',
            'tinymce.plugins.spellchecker.api.Events',
            'tinymce.plugins.spellchecker.api.Settings',
            'tinymce.plugins.spellchecker.core.DomTextMatcher'
        ],
  function (Tools, URI, XHR, Events, Settings, DomTextMatcher) {
      var getTextMatcher = function (editor, textMatcherState) {
          if (!textMatcherState.get()) {
              var textMatcher = new DomTextMatcher(editor.getBody(), editor);
              textMatcherState.set(textMatcher);
          }

          return textMatcherState.get();
      };

      var isEmpty = function (obj) {
          for (var name in obj) {
              return false;
          }

          return true;
      };

      var defaultSpellcheckCallback = function (editor, pluginUrl, currentLanguageState) {
          return function (method, text, doneCallback, errorCallback) {
              var data = { method: method, lang: currentLanguageState.get() }, postData = '';

              data[method === 'addToDictionary' ? 'word' : 'text'] = text;

              Tools.each(data, function (value, key) {
                  if (postData) {
                      postData += '&';
                  }

                  postData += key + '=' + encodeURIComponent(value);
              });

              XHR.send({
                  url: new URI(pluginUrl).toAbsolute(Settings.getRpcUrl(editor)),
                  type: 'post',
                  content_type: 'application/x-www-form-urlencoded',
                  data: postData,
                  success: function (result) {
                      result = JSON.parse(result);

                      if (!result) {
                          var message = editor.translate('Server response wasn\'t proper JSON.');
                          errorCallback(message);
                      } else if (result.error) {
                          errorCallback(result.error);
                      } else {
                          doneCallback(result);
                      }
                  },
                  error: function () {
                      var message = editor.translate('The spelling service was not found: (') +
              Settings.getRpcUrl(editor) +
              editor.translate(')');
                      errorCallback(message);
                  }
              });
          };
      };

      var sendRpcCall = function (editor, pluginUrl, currentLanguageState, name, data, successCallback, errorCallback) {
          var userSpellcheckCallback = Settings.getSpellcheckerCallback(editor);
          var spellCheckCallback = userSpellcheckCallback || defaultSpellcheckCallback(editor, pluginUrl, currentLanguageState);
          spellCheckCallback.call(editor.plugins.spellchecker, name, data, successCallback, errorCallback);
      };

      var spellcheck = function (editor, pluginUrl, startedState, textMatcherState, lastSuggestionsState, currentLanguageState) {
          if (finish(editor, startedState, textMatcherState)) {
              return;
          }

          var errorCallback = function (message) {
              editor.notificationManager.open({ text: message, type: 'error' });
              editor.setProgressState(false);
              finish(editor, startedState, textMatcherState);
          };

          var successCallback = function (data) {
              markErrors(editor, startedState, textMatcherState, lastSuggestionsState, data);
          };

          editor.setProgressState(true);
          sendRpcCall(editor, pluginUrl, currentLanguageState, 'spellcheck', getTextMatcher(editor, textMatcherState).text, successCallback, errorCallback);
          editor.focus();
      };

      var checkIfFinished = function (editor, startedState, textMatcherState) {
          if (!editor.dom.select('span.mce-spellchecker-word').length) {
              finish(editor, startedState, textMatcherState);
          }
      };

      var addToDictionary = function (editor, pluginUrl, startedState, textMatcherState, word, spans) {
          editor.setProgressState(true);

          sendRpcCall(editor, pluginUrl, 'addToDictionary', word, function () {
              editor.setProgressState(false);
              editor.dom.remove(spans, true);
              checkIfFinished(editor, startedState, textMatcherState);
          }, function (message) {
              editor.notificationManager.open({ text: message, type: 'error' });
              editor.setProgressState(false);
          });
      };

      var ignoreWord = function (editor, startedState, textMatcherState, 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(editor, startedState, textMatcherState);
      };

      var finish = function (editor, startedState, textMatcherState) {
          getTextMatcher(editor, textMatcherState).reset();
          textMatcherState.set(null);

          if (startedState.get()) {
              startedState.set(false);
              Events.fireSpellcheckEnd(editor);
              return true;
          }
      };

      var getElmIndex = function (elm) {
          var value = elm.getAttribute('data-mce-index');

          if (typeof value === 'number') {
              return '' + value;
          }

          return value;
      };

      var findSpansByIndex = function (editor, 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;
      };

      var markErrors = function (editor, startedState, textMatcherState, lastSuggestionsState, data) {
          var suggestions, hasDictionarySupport;

          if (data.words) {
              hasDictionarySupport = !!data.dictionary;
              suggestions = data.words;
          } else {
        // Fallback to old format
              suggestions = data;
          }

          editor.setProgressState(false);

          if (isEmpty(suggestions)) {
              var message = editor.translate('No misspellings found.');
              editor.notificationManager.open({ text: message, type: 'info' });
              startedState.set(false);
              return;
          }

          lastSuggestionsState.set({
              suggestions: suggestions,
              hasDictionarySupport: hasDictionarySupport
          });

          getTextMatcher(editor, textMatcherState).find(Settings.getSpellcheckerWordcharPattern(editor)).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
              });
          });

          startedState.set(true);
          Events.fireSpellcheckStart(editor);
      };

      return {
          spellcheck: spellcheck,
          checkIfFinished: checkIfFinished,
          addToDictionary: addToDictionary,
          ignoreWord: ignoreWord,
          findSpansByIndex: findSpansByIndex,
          getElmIndex: getElmIndex,
          markErrors: markErrors
      };
  }
);
/**
 * Api.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
  'tinymce.plugins.spellchecker.api.Api',
        [
            'tinymce.plugins.spellchecker.api.Settings',
            'tinymce.plugins.spellchecker.core.Actions'
        ],
  function (Settings, Actions) {
      var get = function (editor, startedState, lastSuggestionsState, textMatcherState, url) {
          var getLanguage = function () {
              return Settings.getLanguage(editor);
          };

          var getWordCharPattern = function () {
              return Settings.getSpellcheckerWordcharPattern(editor);
          };

          var markErrors = function (data) {
              Actions.markErrors(editor, startedState, textMatcherState, lastSuggestionsState, data);
          };

          var getTextMatcher = function () {
              return textMatcherState.get();
          };

          return {
              getTextMatcher: getTextMatcher,
              getWordCharPattern: getWordCharPattern,
              markErrors: markErrors,
              getLanguage: getLanguage
          };
      };

      return {
          get: get
      };
  }
);
/**
 * Commands.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
  'tinymce.plugins.spellchecker.api.Commands',
        [
            'tinymce.plugins.spellchecker.core.Actions'
        ],
  function (Actions) {
      var register = function (editor, pluginUrl, startedState, textMatcherState, lastSuggestionsState, currentLanguageState) {
          editor.addCommand('mceSpellCheck', function () {
              Actions.spellcheck(editor, pluginUrl, startedState, textMatcherState, lastSuggestionsState, currentLanguageState);
          });
      };

      return {
          register: register
      };
  }
);
/**
 * Buttons.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
  'tinymce.plugins.spellchecker.ui.Buttons',
        [
            'tinymce.core.util.Tools',
            'tinymce.plugins.spellchecker.api.Settings',
            'tinymce.plugins.spellchecker.core.Actions'
        ],
  function (Tools, Settings, Actions) {
      var buildMenuItems = function (listName, languageValues) {
          var items = [];

          Tools.each(languageValues, function (languageValue) {
              items.push({
                  selectable: true,
                  text: languageValue.name,
                  data: languageValue.value
              });
          });

          return items;
      };

      var updateSelection = function (editor) {
          return function (e) {
              var selectedLanguage = Settings.getLanguage(editor);

              e.control.items().each(function (ctrl) {
                  ctrl.active(ctrl.settings.data === selectedLanguage);
              });
          };
      };

      var getItems = function (editor) {
          return Tools.map(Settings.getLanguages(editor).split(','), function (langPair) {
              langPair = langPair.split('=');

              return {
                  name: langPair[0],
                  value: langPair[1]
              };
          });
      };

      var register = function (editor, pluginUrl, startedState, textMatcherState, currentLanguageState, lastSuggestionsState) {
          var languageMenuItems = buildMenuItems('Language', getItems(editor));
          var startSpellchecking = function () {
              Actions.spellcheck(editor, pluginUrl, startedState, textMatcherState, lastSuggestionsState, currentLanguageState);
          };

          var buttonArgs = {
              tooltip: 'Spellcheck',
              onclick: startSpellchecking,
              onPostRender: function (e) {
                  var ctrl = e.control;

                  editor.on('SpellcheckStart SpellcheckEnd', function () {
                      ctrl.active(startedState.get());
                  });
              }
          };

          if (languageMenuItems.length > 1) {
              buttonArgs.type = 'splitbutton';
              buttonArgs.menu = languageMenuItems;
              buttonArgs.onshow = updateSelection(editor);
              buttonArgs.onselect = function (e) {
                  currentLanguageState.set(e.control.settings.data);
              };
          }

          editor.addButton('spellchecker', buttonArgs);

          editor.addMenuItem('spellchecker', {
              text: 'Spellcheck',
              context: 'tools',
              onclick: startSpellchecking,
              selectable: true,
              onPostRender: function () {
                  var self = this;

                  self.active(startedState.get());

                  editor.on('SpellcheckStart SpellcheckEnd', function () {
                      self.active(startedState.get());
                  });
              }
          });
      };

      return {
          register: register
      };
  }
);
    defineGlobal('global!document', document);
/**
 * ResolveGlobal.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
  'tinymce.core.dom.DOMUtils',
        [
            'global!tinymce.util.Tools.resolve'
        ],
  function (resolve) {
      return resolve('tinymce.dom.DOMUtils');
  }
);

/**
 * ResolveGlobal.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
  'tinymce.core.ui.Factory',
        [
            'global!tinymce.util.Tools.resolve'
        ],
  function (resolve) {
      return resolve('tinymce.ui.Factory');
  }
);

/**
 * SuggestionsMenu.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
  'tinymce.plugins.spellchecker.ui.SuggestionsMenu',
        [
            'global!document',
            'tinymce.core.dom.DOMUtils',
            'tinymce.core.ui.Factory',
            'tinymce.core.util.Tools',
            'tinymce.plugins.spellchecker.api.Settings',
            'tinymce.plugins.spellchecker.core.Actions'
        ],
  function (document, DOMUtils, Factory, Tools, Settings, Actions) {
      var suggestionsMenu;

      var showSuggestions = function (editor, pluginUrl, lastSuggestionsState, startedState, textMatcherState, word, spans) {
          var items = [], suggestions = lastSuggestionsState.get().suggestions[word];

          Tools.each(suggestions, function (suggestion) {
              items.push({
                  text: suggestion,
                  onclick: function () {
                      editor.insertContent(editor.dom.encode(suggestion));
                      editor.dom.remove(spans);
                      Actions.checkIfFinished(editor, startedState, textMatcherState);
                  }
              });
          });

          items.push({ text: '-' });

          var hasDictionarySupport = lastSuggestionsState.get().hasDictionarySupport;
          if (hasDictionarySupport) {
              items.push({
                  text: 'Add to Dictionary',
                  onclick: function () {
                      Actions.addToDictionary(editor, pluginUrl, startedState, textMatcherState, word, spans);
                  }
              });
          }

          items.push.apply(items, [
              {
                  text: 'Ignore',
                  onclick: function () {
                      Actions.ignoreWord(editor, startedState, textMatcherState, word, spans);
                  }
              },

              {
                  text: 'Ignore all',
                  onclick: function () {
                      Actions.ignoreWord(editor, startedState, textMatcherState, word, spans, true);
                  }
              }
          ]);

      // Render menu
          suggestionsMenu = Factory.create('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);
      };

      var setup = function (editor, pluginUrl, lastSuggestionsState, startedState, textMatcherState) {
          editor.on('click', function (e) {
              var target = e.target;

              if (target.className === 'mce-spellchecker-word') {
                  e.preventDefault();

                  var spans = Actions.findSpansByIndex(editor, Actions.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(editor, pluginUrl, lastSuggestionsState, startedState, textMatcherState, target.getAttribute('data-mce-word'), spans);
                  }
              }
          });
      };

      return {
          setup: setup
      };
  }
);

/**
 * Plugin.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

    define(
  'tinymce.plugins.spellchecker.Plugin',
        [
            'ephox.katamari.api.Cell',
            'tinymce.core.PluginManager',
            'tinymce.plugins.spellchecker.alien.DetectProPlugin',
            'tinymce.plugins.spellchecker.api.Api',
            'tinymce.plugins.spellchecker.api.Commands',
            'tinymce.plugins.spellchecker.api.Settings',
            'tinymce.plugins.spellchecker.ui.Buttons',
            'tinymce.plugins.spellchecker.ui.SuggestionsMenu'
        ],
  function (Cell, PluginManager, DetectProPlugin, Api, Commands, Settings, Buttons, SuggestionsMenu) {
      PluginManager.add('spellchecker', function (editor, pluginUrl) {
          var startedState = Cell(false);
          var currentLanguageState = Cell(Settings.getLanguage(editor));
          var textMatcherState = Cell(null);
          var lastSuggestionsState = Cell({});

          if (DetectProPlugin.hasProPlugin(editor) === false) {
              Buttons.register(editor, pluginUrl, startedState, textMatcherState, currentLanguageState, lastSuggestionsState);
              SuggestionsMenu.setup(editor, pluginUrl, lastSuggestionsState, startedState, textMatcherState);
              Commands.register(editor, pluginUrl, startedState, textMatcherState, lastSuggestionsState, currentLanguageState);
          }

          return Api.get(editor, startedState, lastSuggestionsState, textMatcherState, pluginUrl);
      });

      return function () { };
  }
);
    dem('tinymce.plugins.spellchecker.Plugin')();
})();
