/*!

* froala_editor v2.3.3 (https://www.froala.com/wysiwyg-editor)
* License https://froala.com/wysiwyg-editor/terms/
* Copyright 2014-2016 Froala Labs
*/

(function (factory) {

if (typeof define === 'function' && define.amd) {
    // AMD. Register as an anonymous module.
    define(['jquery'], factory);
} else if (typeof module === 'object' && module.exports) {
    // Node/CommonJS
    module.exports = function( root, jQuery ) {
        if ( jQuery === undefined ) {
            // require('jQuery') returns a factory that requires window to
            // build a jQuery instance, we normalize how we use modules
            // that require this pattern but the window provided is a noop
            // if it's defined (how jquery works)
            if ( typeof window !== 'undefined' ) {
                jQuery = require('jquery');
            }
            else {
                jQuery = require('jquery')(root);
            }
        }
        factory(jQuery);
        return jQuery;
    };
} else {
    // Browser globals
    factory(jQuery);
}

}(function ($) {

/*jslint browser: true, debug: true, vars: true, devel: true, expr: true, jQuery: true */
// EDITABLE CLASS DEFINITION
// =========================

'use strict';

var FE = function (element, options) {
  this.id = ++$.FE.ID;

  this.opts = $.extend(true, {}, $.extend({}, FE.DEFAULTS, typeof options == 'object' && options));
  var opts_string = JSON.stringify(this.opts);

  $.FE.OPTS_MAPPING[opts_string] = $.FE.OPTS_MAPPING[opts_string] || this.id;
  this.sid = $.FE.OPTS_MAPPING[opts_string];
  $.FE.SHARED[this.sid] = $.FE.SHARED[this.sid] || {};
  this.shared = $.FE.SHARED[this.sid];
  this.shared.count = (this.shared.count || 0) + 1;

  this.$oel = $(element);
  this.$oel.data('froala.editor', this);

  this.o_doc = element.ownerDocument;
  this.o_win = 'defaultView' in this.o_doc ? this.o_doc.defaultView :   this.o_doc.parentWindow;
  var c_scroll = $(this.o_win).scrollTop();

  this.$oel.on('froala.doInit', $.proxy(function () {
    this.$oel.off('froala.doInit');
    this.doc = this.$el.get(0).ownerDocument;
    this.win = 'defaultView' in this.doc ? this.doc.defaultView : this.doc.parentWindow;
    this.$doc = $(this.doc);
    this.$win = $(this.win);

    if (!this.opts.pluginsEnabled) this.opts.pluginsEnabled = Object.keys($.FE.PLUGINS);

    if (this.opts.initOnClick) {
      this.load($.FE.MODULES);

      // https://github.com/froala/wysiwyg-editor/issues/1207.
      this.$el.on('touchstart.init', function () {
        $(this).data('touched', true);
      });

      this.$el.on('touchmove.init', function () {
        $(this).removeData('touched');
      })

      this.$el.on('mousedown.init touchend.init dragenter.init focus.init', $.proxy(function (e) {
        if (e.type == 'touchend' && !this.$el.data('touched')) {
          return true;
        }

        if (e.which === 1 || e.which === 0) {
          this.$el.off('mousedown.init touchstart.init touchmove.init touchend.init dragenter.init focus.init');

          this.load($.FE.MODULES);
          this.load($.FE.PLUGINS);

          var target = e.originalEvent && e.originalEvent.originalTarget;
          if (target && target.tagName == 'IMG') $(target).trigger('mousedown');

          if (typeof this.ul == 'undefined') this.destroy();

          if (e.type == 'touchend' && this.image && e.originalEvent && e.originalEvent.target && $(e.originalEvent.target).is('img')) {
            setTimeout($.proxy(function () {
              this.image.edit($(e.originalEvent.target));
            }, this), 100);
          }

          this.events.trigger('initialized');
        }
      }, this));
    }
    else {
      this.load($.FE.MODULES);
      this.load($.FE.PLUGINS);

      $(this.o_win).scrollTop(c_scroll);

      if (typeof this.ul == 'undefined') this.destroy();

      this.events.trigger('initialized');
    }
  }, this));

  this._init();
};

FE.DEFAULTS = {
  initOnClick: false,
  pluginsEnabled: null
};

FE.MODULES = {};

FE.PLUGINS = {};

FE.VERSION = '2.3.3';

FE.INSTANCES = [];

FE.OPTS_MAPPING = {};

FE.SHARED = {};

FE.ID = 0;

FE.prototype._init = function () {
  // Get the tag name of the original element.
  var tag_name = this.$oel.prop('tagName');

  // Initialize on anything else.
  var initOnDefault = $.proxy(function () {
    this._original_html = (this._original_html || this.$oel.html());
    this.$box = this.$box || this.$oel;

    // Turn on iframe if fullPage is on.
    if (this.opts.fullPage) this.opts.iframe = true;

    if (!this.opts.iframe) {
      this.$el = $('<div></div>');
      this.$wp = $('<div></div>').append(this.$el);
      this.$box.html(this.$wp);
      this.$oel.trigger('froala.doInit');
    }
    else {
      this.$iframe = $('<iframe src="about:blank" frameBorder="0">');
      this.$wp = $('<div></div>');
      this.$box.html(this.$wp);
      this.$wp.append(this.$iframe);
      this.$iframe.get(0).contentWindow.document.open();
      this.$iframe.get(0).contentWindow.document.write('<!DOCTYPE html>');
      this.$iframe.get(0).contentWindow.document.write('<html><head></head><body></body></html>');
      this.$iframe.get(0).contentWindow.document.close();

      this.$el = this.$iframe.contents().find('body');
      this.$head = this.$iframe.contents().find('head');
      this.$html = this.$iframe.contents().find('html');
      this.iframe_document = this.$iframe.get(0).contentWindow.document;
      this.$oel.trigger('froala.doInit');
    }
  }, this);

  // Initialize on a TEXTAREA.
  var initOnTextarea = $.proxy(function () {
    this.$box = $('<div>');
    this.$oel.before(this.$box).hide();

    this._original_html = this.$oel.val();

    // Before submit textarea do a sync.
    this.$oel.parents('form').on('submit.' + this.id, $.proxy(function () {
      this.events.trigger('form.submit');
    }, this));

    this.$oel.parents('form').on('reset.' + this.id, $.proxy(function () {
      this.events.trigger('form.reset');
    }, this));

    initOnDefault();
  }, this);

  // Initialize on a Link.
  var initOnA = $.proxy(function () {
    this.$el = this.$oel;
    this.$el.attr('contenteditable', true).css('outline', 'none').css('display', 'inline-block');
    this.opts.multiLine = false;
    this.opts.toolbarInline = false;

    this.$oel.trigger('froala.doInit');
  }, this)

  // Initialize on an Image.
  var initOnImg = $.proxy(function () {
    this.$el = this.$oel;
    this.opts.toolbarInline = false;

    this.$oel.trigger('froala.doInit');
  }, this)

  var editInPopup = $.proxy(function () {
    this.$el = this.$oel;
    this.opts.toolbarInline = false;

    this.$oel.on('click.popup', function (e) {
      e.preventDefault();
    })
    this.$oel.trigger('froala.doInit');
  }, this);

  // Check on what element it was initialized.
  if (this.opts.editInPopup) editInPopup();
  else if (tag_name == 'TEXTAREA') initOnTextarea();
  else if (tag_name == 'A') initOnA();
  else if (tag_name == 'IMG') initOnImg();
  else if (tag_name == 'BUTTON' || tag_name == 'INPUT') {
    this.opts.editInPopup = true;
    this.opts.toolbarInline = false;
    editInPopup();
  }
  else {
    initOnDefault();
  }
}

FE.prototype.load = function (module_list) {
  // Bind modules to the current instance and tear them up.
  for (var m_name in module_list) {
    if (module_list.hasOwnProperty(m_name)) {
      if (this[m_name]) continue;

      // Do not include plugin.
      if ($.FE.PLUGINS[m_name] && this.opts.pluginsEnabled.indexOf(m_name) < 0) continue;

      this[m_name] = new module_list[m_name](this);
      if (this[m_name]._init) {
        this[m_name]._init();
        if (this.opts.initOnClick && m_name == 'core') {
          return false;
        }
      }
    }
  }
}

FE.prototype.reset = function () {
    this.$el.html('');
}

FE.prototype.refresh = function () {
    this.$el.html(this.$oel.val());
}
// Do destroy.
FE.prototype.destroy = function () {
  this.shared.count--;

  this.events.$off();

  // HTML.
  var html = this.html.get();

  this.events.trigger('destroy', [], true);
  this.events.trigger('shared.destroy', undefined, true);

  // Remove shared.
  if (this.shared.count === 0) {
    for (var k in this.shared) {
      if (this.shared.hasOwnProperty(k)) {
        this.shared[k] == null;
        $.FE.SHARED[this.sid][k] = null;
      }
    }

    $.FE.SHARED[this.sid] = {};
  }

  this.$oel.parents('form').off('.' + this.id);
  this.$oel.off('click.popup');
  this.$oel.removeData('froala.editor');

  // Destroy editor basic elements.
  this.core.destroy(html);

  $.FE.INSTANCES.splice($.FE.INSTANCES.indexOf(this), 1);
}

// FROALA EDITOR PLUGIN DEFINITION
// ==========================
$.fn.froalaEditor = function (option) {
  var arg_list = [];
  for (var i = 0; i < arguments.length; i++) {
    arg_list.push(arguments[i]);
  }
  if (typeof option == 'string') {
    var returns = [];

    this.each(function () {
      var $this = $(this);
      var editor = $this.data('froala.editor');

      if (!editor) {
        return console.warn('Editor should be initialized before calling the ' + option + ' method.');
      }

      var context;
      var nm;

      // Might do a module call.
      if (option.indexOf('.') > 0 && editor[option.split('.')[0]]) {
        if (editor[option.split('.')[0]]) {
          context = editor[option.split('.')[0]];
        }
        nm = option.split('.')[1];
      }
      else {
        context = editor;
        nm = option.split('.')[0]
      }
      if (context[nm]) {
        var returned_value = context[nm].apply(editor, arg_list.slice(1));
        if (returned_value === undefined) {
          returns.push(this);
        } else if (returns.length === 0) {
          returns.push(returned_value);
        }
      }
      else {
        return $.error('Method ' +  option + ' does not exist in Froala Editor.');
      }
    });

    return (returns.length == 1) ? returns[0] : returns;
  }
  else if (typeof option === 'object' || !option) {
    return this.each(function () {
      var editor = $(this).data('froala.editor');

      if (!editor) {
        var that = this;
        new FE(that, option);
      }
    });
  }
}

$.fn.froalaEditor.Constructor = FE;
$.FroalaEditor = FE;
$.FE = FE;

$.FE.MODULES.node = function (editor) {
  function getContents(node) {
    if (!node || node.tagName == 'IFRAME') return [];
    return $(node).contents();
  }

  /**
   * Determine if the node is a block tag.
   */
  function isBlock (node) {
    if (!node) return false;
    if (node.nodeType != Node.ELEMENT_NODE) return false;

    return $.FE.BLOCK_TAGS.indexOf(node.tagName.toLowerCase()) >= 0;
  }

  /**
   * Check if a DOM element is empty.
   */
  function isEmpty (el, ignore_markers) {
    if ($(el).find('table').length > 0) return false;

    // Look for void nodes.
    if (el.querySelectorAll($.FE.VOID_ELEMENTS.join(',')).length - el.querySelectorAll('br').length) return false;

    // Look for empty allowed tags.
    if (el.querySelectorAll(editor.opts.htmlAllowedEmptyTags.join(':not(.fr-marker),') + ':not(.fr-marker)').length) return false;

    // Look for block tags.
    if (el.querySelectorAll($.FE.BLOCK_TAGS.join(',')).length > 1) return false;

    // Look for do not wrap tags.
    if (el.querySelectorAll(editor.opts.htmlDoNotWrapTags.join(':not(.fr-marker),') + ':not(.fr-marker)').length) return false;

    // Get element contents.
    var contents = getContents(el);

    // Check if there is a block tag.
    if (contents.length == 1 && isBlock(contents[0])) {
      contents = getContents(contents[0]);
    }

    var has_br = false;
    for (var i = 0; i < contents.length; i++) {
      var node = contents[i];

      if (ignore_markers && $(node).hasClass('fr-marker')) continue;

      if (node.nodeType == Node.TEXT_NODE && node.textContent.length == 0) continue;

      if (node.tagName != 'BR' && (node.textContent || '').replace(/\u200B/gi, '').replace(/\n/g, '').length > 0) return false;

      if (has_br) {
        return false;
      }
      else if (node.tagName == 'BR') {
        has_br = true;
      }
    }

    return true;
  }

  /**
   * Get the block parent.
   */
  function blockParent (node) {
    while (node && node.parentNode !== editor.$el.get(0) && !(node.parentNode && $(node.parentNode).hasClass('fr-inner'))) {
      node = node.parentNode;
      if (isBlock(node)) {
        return node;
      }
    }

    return null;
  }

  /**
   * Get deepest parent till the element.
   */
  function deepestParent (node, until, simple_enter) {
    if (typeof until == 'undefined') until = [];
    if (typeof simple_enter == 'undefined') simple_enter = true;
    until.push(editor.$el.get(0));

    if (until.indexOf(node.parentNode) >= 0 || (node.parentNode && $(node.parentNode).hasClass('fr-inner')) || (node.parentNode && $.FE.SIMPLE_ENTER_TAGS.indexOf(node.parentNode.tagName) >= 0 && simple_enter)) {
      return null;
    }

    // 1. Before until.
    // 2. Parent node doesn't has class fr-inner.
    // 3. Parent node is not a simple enter tag or quote.
    // 4. Parent node is not a block tag
    while (until.indexOf(node.parentNode) < 0 && node.parentNode && !$(node.parentNode).hasClass('fr-inner') && ($.FE.SIMPLE_ENTER_TAGS.indexOf(node.parentNode.tagName) < 0 || !simple_enter) && (!(isBlock(node) && isBlock(node.parentNode)) || !simple_enter)) {
      node = node.parentNode;
    }

    return node;
  }

  function rawAttributes (node) {
    var attrs = {};

    var atts = node.attributes;
    if (atts) {
      for (var i = 0; i < atts.length; i++) {
        var att = atts[i];
        attrs[att.nodeName] = att.value;
      }
    }

    return attrs;
  }

  /**
   * Get attributes for a node as a string.
   */
  function attributes (node) {
    var str = '';
    var atts = rawAttributes(node);

    var keys = Object.keys(atts).sort();
    for (var i = 0; i < keys.length; i++) {
      var nodeName = keys[i];
      var value = atts[nodeName];

      // Make sure we don't break any HTML.
      if (value.indexOf('"') < 0) {
        str += ' ' + nodeName + '="' + value + '"';
      }
      else {
        str += ' ' + nodeName + '=\'' + value + '\'';
      }
    }

    return str;
  }

  function clearAttributes (node) {
    var atts = node.attributes;
    for (var i = 0; i < atts.length; i++) {
      var att = atts[i];
      node.removeAttribute(att.nodeName);
    }
  }

  /**
   * Open string for a node.
   */
  function openTagString (node) {
    return '<' + node.tagName.toLowerCase() + attributes(node) + '>';
  }

  /**
   * Close string for a node.
   */
  function closeTagString (node) {
    return '</' + node.tagName.toLowerCase() + '>';
  }

  /**
   * Determine if the node has any left sibling.
   */
  function isFirstSibling (node, ignore_markers) {
    if (typeof ignore_markers == 'undefined') ignore_markers = true;
    var sibling = node.previousSibling;

    while (sibling && ignore_markers && $(sibling).hasClass('fr-marker')) {
      sibling = sibling.previousSibling;
    }

    if (!sibling) return true;
    if (sibling.nodeType == Node.TEXT_NODE && sibling.textContent === '') return isFirstSibling(sibling);
    return false;
  }

  /**
   * Determine if the node has any right sibling.
   */
  function isLastSibling (node, ignore_markers) {
    if (typeof ignore_markers == 'undefined') ignore_markers = true;
    var sibling = node.nextSibling;

    while (sibling && ignore_markers && $(sibling).hasClass('fr-marker')) {
      sibling = sibling.nextSibling;
    }

    if (!sibling) return true;
    if (sibling.nodeType == Node.TEXT_NODE && sibling.textContent === '') return isLastSibling(sibling);
    return false;
  }

  function isVoid(node) {
    return node && node.nodeType == Node.ELEMENT_NODE && $.FE.VOID_ELEMENTS.indexOf((node.tagName || '').toLowerCase()) >= 0
  }

  /**
   * Check if the node is a list.
   */
  function isList (node) {
    if (!node) return false;
    return ['UL', 'OL'].indexOf(node.tagName) >= 0;
  }

  /**
   * Check if the node is the editable element.
   */
  function isElement (node) {
    return node === editor.$el.get(0);
  }

  /**
   * Check if the node is the editable element.
   */
  function isDeletable (node) {
    return node && node.className && (node.className || '').indexOf('fr-deletable') >= 0;
  }

  /**
   * Check if the node has focus.
   */
  function hasFocus (node) {
    return node === editor.doc.activeElement && (!editor.doc.hasFocus || editor.doc.hasFocus()) && !!(isElement(node) || node.type || node.href || ~node.tabIndex);;
  }

  function isEditable (node) {
    return (!node.getAttribute || node.getAttribute('contenteditable') != 'false')
              && ['STYLE', 'SCRIPT'].indexOf(node.tagName) < 0;
  }

  return {
    isBlock: isBlock,
    isEmpty: isEmpty,
    blockParent: blockParent,
    deepestParent: deepestParent,
    rawAttributes: rawAttributes,
    attributes: attributes,
    clearAttributes: clearAttributes,
    openTagString: openTagString,
    closeTagString: closeTagString,
    isFirstSibling: isFirstSibling,
    isLastSibling: isLastSibling,
    isList: isList,
    isElement: isElement,
    contents: getContents,
    isVoid: isVoid,
    hasFocus: hasFocus,
    isEditable: isEditable,
    isDeletable: isDeletable
  }
};

// Extend defaults.
$.extend($.FE.DEFAULTS, {
  // Tags that describe head from HEAD http://www.w3schools.com/html/html_head.asp.
  htmlAllowedTags: ['a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdi', 'bdo', 'blockquote', 'br', 'button', 'canvas', 'caption', 'cite', 'code', 'col', 'colgroup', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link', 'main', 'map', 'mark', 'menu', 'menuitem', 'meter', 'nav', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'pre', 'progress', 'queue', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'style', 'section', 'select', 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'u', 'ul', 'var', 'video', 'wbr'],
  htmlRemoveTags: ['script', 'style'],
  htmlAllowedAttrs: ['accept', 'accept-charset', 'accesskey', 'action', 'align', 'allowfullscreen', 'allowtransparency', 'alt', 'async', 'autocomplete', 'autofocus', 'autoplay', 'autosave', 'background', 'bgcolor', 'border', 'charset', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'color', 'cols', 'colspan', 'content', 'contenteditable', 'contextmenu', 'controls', 'coords', 'data', 'data-.*', 'datetime', 'default', 'defer', 'dir', 'dirname', 'disabled', 'download', 'draggable', 'dropzone', 'enctype', 'for', 'form', 'formaction', 'frameborder', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'http-equiv', 'icon', 'id', 'ismap', 'itemprop', 'keytype', 'kind', 'label', 'lang', 'language', 'list', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'mozallowfullscreen', 'multiple', 'name', 'novalidate', 'open', 'optimum', 'pattern', 'ping', 'placeholder', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'reversed', 'rows', 'rowspan', 'sandbox', 'scope', 'scoped', 'scrolling', 'seamless', 'selected', 'shape', 'size', 'sizes', 'span', 'src', 'srcdoc', 'srclang', 'srcset', 'start', 'step', 'summary', 'spellcheck', 'style', 'tabindex', 'target', 'title', 'type', 'translate', 'usemap', 'value', 'valign', 'webkitallowfullscreen', 'width', 'wrap'],
  htmlAllowComments: true,
  fullPage: false // Will also turn iframe on.
});

$.FE.HTML5Map = {
  'B': 'STRONG',
  'I': 'EM',
  'STRIKE': 'S'
},

$.FE.MODULES.clean = function (editor) {
  var $iframe, body;
  var allowedTagsRE, removeTagsRE, allowedAttrsRE;

  function _removeInvisible (node) {
    if (node.className && node.className.indexOf('fr-marker') >= 0) return false;

    // Get node contents.
    var contents = editor.node.contents(node);
    var markers = [];
    var i;

    // Loop through contents.
    for (i = 0; i < contents.length; i++) {
      // If node is not void.
      if (contents[i].nodeType == Node.ELEMENT_NODE && !editor.node.isVoid(contents[i])) {
        // There are invisible spaces.
        if (contents[i].textContent.replace(/\u200b/g, '').length != contents[i].textContent.length) {
          // Do remove invisible spaces.
          _removeInvisible(contents[i]);
        }
      }

      // If node is text node, replace invisible spaces.
      else if (contents[i].nodeType == Node.TEXT_NODE) {
        contents[i].textContent = contents[i].textContent.replace(/\u200b/g, '');
      }
    }

    // Reasess contents after cleaning invisible spaces.
    if (node.nodeType == Node.ELEMENT_NODE && !editor.node.isVoid(node)) {
      node.normalize();
      contents = editor.node.contents(node);
      markers = node.querySelectorAll('.fr-marker');

      // All we have left are markers.
      if (contents.length - markers.length == 0) {
        // Make sure contents are all markers.
        for (i = 0; i < contents.length; i++) {
          if ((contents[i].className || '').indexOf('fr-marker') < 0) {
            return false;
          }
        }

        for (i = 0; i < markers.length; i++) {
          node.parentNode.insertBefore(markers[i].cloneNode(true), node);
        }
        node.parentNode.removeChild(node);
        return false;
      }
    }
  }

  function _toHTML (el) {
    if (el.nodeType == Node.COMMENT_NODE) return '<!--' + el.nodeValue + '-->';
    if (el.nodeType == Node.TEXT_NODE) return el.textContent.replace(/\</g, '&lt;').replace(/\>/g, '&gt;').replace(/\u00A0/g, '&nbsp;');
    if (el.nodeType != Node.ELEMENT_NODE) return el.outerHTML;
    if (el.nodeType == Node.ELEMENT_NODE && ['STYLE', 'SCRIPT'].indexOf(el.tagName) >= 0) return el.outerHTML;
    if (el.tagName == 'IFRAME') return el.outerHTML;

    var contents = el.childNodes;

    if (contents.length === 0) return el.outerHTML;

    var str = '';
    for (var i = 0; i < contents.length; i++) {
      str += _toHTML(contents[i]);
    }

    return editor.node.openTagString(el) + str + editor.node.closeTagString(el);
  }

  var scripts = [];
  function _encode (dirty_html) {
    // Replace script tag with comments.
    scripts = [];
    dirty_html = dirty_html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, function (str) {
      scripts.push(str);
      return '[FROALA.EDITOR.SCRIPT ' + (scripts.length - 1) + ']';
    });

    dirty_html = dirty_html.replace(/<img((?:[\w\W]*?)) src="/g, '<img$1 data-fr-src="');

    return dirty_html;
  }

  function _decode (dirty_html) {
    // Replace script comments with the original script.
    dirty_html = dirty_html.replace(/\[FROALA\.EDITOR\.SCRIPT ([\d]*)\]/gi, function (str, a1) {
      if (editor.opts.htmlRemoveTags.indexOf('script') >= 0) {
        return '';
      }
      else {
        return scripts[parseInt(a1, 10)];
      }
    });

    dirty_html = dirty_html.replace(/<img((?:[\w\W]*?)) data-fr-src="/g, '<img$1 src="');

    return dirty_html;
  }

  function _cleanAttrs (attrs) {
    var nm;

    for (nm in attrs) {
      if (attrs.hasOwnProperty(nm)) {
        if (!nm.match(allowedAttrsRE)) {
          delete attrs[nm];
        }
      }
    }

    var str = '';
    var keys = Object.keys(attrs).sort();
    for (var i = 0; i < keys.length; i++) {
      nm = keys[i];

      // Make sure we don't break any HTML.
      if (attrs[nm].indexOf('"') < 0) {
        str += ' ' + nm + '="' + attrs[nm] + '"';
      }
      else {
        str += ' ' + nm + '=\'' + attrs[nm] + '\'';
      }
    }

    return str;
  }

  function _rebuild (body_html, head_html, original_html) {
    if (editor.opts.fullPage) {
      // Get DOCTYPE.
      var doctype = editor.html.extractDoctype(original_html);

      // Get HTML attributes.
      var html_attrs = _cleanAttrs(editor.html.extractNodeAttrs(original_html, 'html'));

      // Get HEAD data.
      head_html = head_html == null ? editor.html.extractNode(original_html, 'head') || '<title></title>' : head_html;
      var head_attrs = _cleanAttrs(editor.html.extractNodeAttrs(original_html, 'head'));

      // Get BODY attributes.
      var body_attrs = _cleanAttrs(editor.html.extractNodeAttrs(original_html, 'body'));

      return doctype + '<html' + html_attrs + '><head' + head_attrs + '>' + head_html + '</head><body' + body_attrs + '>' + body_html + '</body></html>';
    }

    return body_html;
  }

  function _process (html, func) {
    var $el = $('<div>' + html + '</div>');

    var new_html = '';
    if ($el) {
      var els = editor.node.contents($el.get(0));
      for (var i = 0; i < els.length; i++) {
        func(els[i]);
      }

      els = editor.node.contents($el.get(0));
      for (var i = 0; i < els.length; i++) {
        new_html += _toHTML(els[i]);
      }
    }

    return new_html;
  }

  function exec (html, func, parse_head) {
    html = _encode(html);

    var b_html = html;
    var h_html = null;
    if (editor.opts.fullPage) {
      // Get BODY data.
      var b_html = (editor.html.extractNode(html, 'body') || (html.indexOf('<body') >= 0 ? '' : html));

      if (parse_head) {
        h_html = (editor.html.extractNode(html, 'head') || '');
      }
    }

    b_html = _process(b_html, func);
    if (h_html) h_html = _process(h_html, func);

    var new_html = _rebuild(b_html, h_html, html);

    return _decode(new_html);
  }

  function invisibleSpaces (dirty_html) {
    if (dirty_html.replace(/\u200b/g, '').length == dirty_html.length) return dirty_html;

    return editor.clean.exec(dirty_html, _removeInvisible);
  }

  function toHTML5 () {
    var els = editor.$el.get(0).querySelectorAll(Object.keys($.FE.HTML5Map).join(','));
    if (els.length) {
      editor.selection.save();
      for (var i = 0; i < els.length; i++) {
        if (editor.node.attributes(els[i]) === '') {
           $(els[i]).replaceWith('<' + $.FE.HTML5Map[els[i].tagName] + '>' + els[i].innerHTML + '</' + $.FE.HTML5Map[els[i].tagName] + '>');
        }
      }
      editor.selection.restore();
    }
  }

  function _node (node) {
    // Skip when we're dealing with markers.
    if (node.tagName == 'SPAN' && (node.className || '').indexOf('fr-marker') >=0) return false;

    if (node.tagName == 'PRE') _cleanPre(node);

    if (node.nodeType == Node.ELEMENT_NODE) {
      if (node.getAttribute('data-fr-src')) node.setAttribute('data-fr-src', editor.helpers.sanitizeURL(node.getAttribute('data-fr-src')));
      if (node.getAttribute('href')) node.setAttribute('href', editor.helpers.sanitizeURL(node.getAttribute('href')));

      if (['TABLE', 'TBODY', 'TFOOT', 'TR'].indexOf(node.tagName) >= 0) {
        node.innerHTML = node.innerHTML.trim();
      }
    }

    // Remove local images if option they are not allowed.
    if (!editor.opts.pasteAllowLocalImages && node.nodeType == Node.ELEMENT_NODE && node.tagName == 'IMG' && node.getAttribute('data-fr-src') && node.getAttribute('data-fr-src').indexOf('file://') == 0) {
      node.parentNode.removeChild(node);
      return false;
    }

    if (node.nodeType == Node.ELEMENT_NODE && $.FE.HTML5Map[node.tagName] && editor.node.attributes(node) === '') {
      var tg = $.FE.HTML5Map[node.tagName];
      var new_node = '<' + tg + '>' + node.innerHTML + '</' + tg + '>';
      node.insertAdjacentHTML('beforebegin', new_node);
      node = node.previousSibling;
      node.parentNode.removeChild(node.nextSibling);
    }

    if (!editor.opts.htmlAllowComments && node.nodeType == Node.COMMENT_NODE) {
      // Do not remove FROALA.EDITOR comments.
      if (node.data.indexOf('[FROALA.EDITOR') !== 0) {
        node.parentNode.removeChild(node);
      }
    }

    // Remove completely tags in denied tags.
    else if (node.tagName && node.tagName.match(removeTagsRE)) {
      node.parentNode.removeChild(node);
    }

    // Unwrap tags not in allowed tags.
    else if (node.tagName && !node.tagName.match(allowedTagsRE)) {
      node.outerHTML = node.innerHTML;
    }

    // Check denied attributes.
    else {
      var attrs = node.attributes;
      if (attrs) {
        for (var i = attrs.length - 1; i >= 0; i--) {
          var attr = attrs[i];
          if (!attr.nodeName.match(allowedAttrsRE)) {
            node.removeAttribute(attr.nodeName);
          }
        }
      }
    }
  }

  function _run (node) {
    var contents = editor.node.contents(node);
    for (var i = 0; i < contents.length; i++) {
      if (contents[i].nodeType != Node.TEXT_NODE) {
        _run(contents[i]);
      }
    }

    _node(node);
  }

  /**
   * Clean pre.
   */
  function _cleanPre (pre) {
    var content = pre.innerHTML;
    if (content.indexOf('\n') >= 0) {
      pre.innerHTML = content.replace(/\n/g, '<br>');
    }
  }

  /**
   * Clean the html input.
   */
  var scripts = [];
  function html (dirty_html, denied_tags, denied_attrs, full_page) {
    if (typeof denied_tags == 'undefined') denied_tags = [];
    if (typeof denied_attrs == 'undefined') denied_attrs = [];
    if (typeof full_page == 'undefined') full_page = false;

    // Strip tabs.
    dirty_html = dirty_html.replace(/\u0009/g, '');

    // Build the allowed tags array.
    var allowed_tags = $.merge([], editor.opts.htmlAllowedTags);
    var i;
    for (i = 0; i < denied_tags.length; i++) {
      if (allowed_tags.indexOf(denied_tags[i]) >= 0) {
        allowed_tags.splice(allowed_tags.indexOf(denied_tags[i]), 1);
      }
    }

    // Build the allowed attrs array.
    var allowed_attrs = $.merge([], editor.opts.htmlAllowedAttrs);
    for (i = 0; i < denied_attrs.length; i++) {
      if (allowed_attrs.indexOf(denied_attrs[i]) >= 0) {
        allowed_attrs.splice(allowed_attrs.indexOf(denied_attrs[i]), 1);
      }
    }

    // We should allow data-fr.
    allowed_attrs.push('data-fr-.*');
    allowed_attrs.push('fr-.*');

    // Generate cleaning RegEx.
    allowedTagsRE = new RegExp('^' + allowed_tags.join('$|^') + '$', 'gi');
    allowedAttrsRE = new RegExp('^' + allowed_attrs.join('$|^') + '$', 'gi');
    removeTagsRE = new RegExp('^' + editor.opts.htmlRemoveTags.join('$|^') + '$', 'gi');

    dirty_html = exec(dirty_html, _run, true);

    return dirty_html;
  }

  /**
   * Clean quotes.
   */
  function quotes () {
    // Join quotes.
    var sibling_quotes = editor.$el.get(0).querySelectorAll('blockquote + blockquote');
    for (var k = 0; k < sibling_quotes.length; k++) {
      var quote = sibling_quotes[k];
      if (editor.node.attributes(quote) == editor.node.attributes(quote.previousSibling)) {
        $(quote).prev().append($(quote).html());
        $(quote).remove();
      }
    }
  }

  function _tablesWrapTHEAD () {
    var trs = editor.$el.get(0).querySelectorAll('tr');

    // Make sure the TH lives inside thead.
    for (var i = 0; i < trs.length; i++) {
      // Search for th inside tr.
      var children = trs[i].children;
      var ok = true;
      for (var j = 0; j < children.length; j++) {
        if (children[j].tagName != 'TH') {
          ok = false;
          break;
        }
      }

      // If there is something else than TH.
      if (ok == false || children.length == 0) continue;

      var tr = trs[i];

      while (tr && tr.tagName != 'TABLE' && tr.tagName != 'THEAD') {
        tr = tr.parentNode;
      }

      var thead = tr;
      if (thead.tagName != 'THEAD') {
        thead = editor.doc.createElement('THEAD');
        tr.insertBefore(thead, tr.firstChild);
      }

      thead.appendChild(trs[i]);
    }
  }

  function _tablesBRBefore () {
    // Make sure we have a br before tables.
    var tbls = editor.$el.get(0).querySelectorAll('table');
    for (var i = 0; i < tbls.length; i++) {
      var prev_node = tbls[i].previousSibling;

      // Get previous sibling.
      while (prev_node && prev_node.nodeType == Node.TEXT_NODE && prev_node.textContent.length == 0) {
        prev_node = prev_node.previousSibling;
      }

      if (prev_node && !editor.node.isBlock(prev_node) && prev_node.tagName != 'BR' && (prev_node.nodeType == Node.TEXT_NODE || prev_node.nodeType == Node.ELEMENT_NODE) && !$(prev_node).is(editor.opts.htmlDoNotWrapTags.join(','))) {
        tbls[i].parentNode.insertBefore(editor.doc.createElement('br'), tbls[i]);
      }
    }
  }

  function _tablesRemovePFromCell () {
    // Remove P from TH and TH.
    var default_tag = editor.html.defaultTag();
    if (default_tag) {
      var nodes = editor.$el.get(0).querySelectorAll('td > ' + default_tag + ', th > ' + default_tag);
      for (var i = 0; i < nodes.length; i++) {
        if (editor.node.attributes(nodes[i]) === '') {
          $(nodes[i]).replaceWith(nodes[i].innerHTML + '<br>');
        }
      }
    }
  }

  /**
   * Clean tables.
   */
  function tables () {
    _tablesWrapTHEAD();

    _tablesBRBefore();

    _tablesRemovePFromCell();
  }

  function _listsWrapMissplacedLI () {
    // Find missplaced list items.
    var lis = [];
    var filterListItem = function (li) {
      return !editor.node.isList(li.parentNode);
    };

    do {
      if (lis.length) {
        var li = lis[0];
        var ul = editor.doc.createElement('ul');
        li.parentNode.insertBefore(ul, li);
        do {
          var tmp = li;
          li = li.nextSibling;
          ul.appendChild(tmp);
        } while (li && li.tagName == 'LI');
      }

      lis = [];
      var li_sel = editor.$el.get(0).querySelectorAll('li');
      for (var i = 0; i < li_sel.length; i++) {
        if (filterListItem(li_sel[i])) lis.push(li_sel[i]);
      }
    } while (lis.length > 0);
  }

  function _listsJoinSiblings () {
    // Join lists.
    var sibling_lists = editor.$el.get(0).querySelectorAll('ol + ol, ul + ul');
    for (var k = 0; k < sibling_lists.length; k++) {
      var list = sibling_lists[k];
      if (editor.node.isList(list.previousSibling) && editor.node.openTagString(list) == editor.node.openTagString(list.previousSibling)) {
        var childs = editor.node.contents(list);
        for (var i = 0; i < childs.length; i++) {
          list.previousSibling.appendChild(childs[i]);
        }
        list.parentNode.removeChild(list);
      }
    }
  }

  function _listsRemoveEmpty () {
    // Remove empty lists.
    var do_remove;
    var removeEmptyList = function (lst) {
      if (lst.querySelectorAll('LI').length === 0) {
        do_remove = true;
        lst.parentNode.removeChild(lst);
      }
    };

    do {
      do_remove = false;

      // Remove empty li.
      var empty_lis = editor.$el.get(0).querySelectorAll('li:empty');
      for (var i = 0; i < empty_lis.length; i++) {
        empty_lis[i].parentNode.removeChild(empty_lis[i]);
      }

      // Remove empty ul and ol.
      var remaining_lists = editor.$el.get(0).querySelectorAll('ul, ol');
      for (var i = 0; i < remaining_lists.length; i++) {
        removeEmptyList(remaining_lists[i]);
      }
    } while (do_remove === true);
  }

  function _listsWrapLists () {
    // Do not allow list directly inside another list.
    var direct_lists = editor.$el.get(0).querySelectorAll('ul > ul, ol > ol, ul > ol, ol > ul');
    for (var i = 0; i < direct_lists.length; i++) {
      var list = direct_lists[i];
      var prev_li = list.previousSibling;
      if (prev_li) {
        if (prev_li.tagName == 'LI') {
          prev_li.appendChild(list);
        }
        else {
          $(list).wrap('<li></li>');
        }
      }
    }
  }

  function _listsNoTagAfterNested () {
    // Check if nested lists don't have HTML after them.
    var nested_lists = editor.$el.get(0).querySelectorAll('li > ul, li > ol');
    for (var i = 0; i < nested_lists.length; i++) {
      var lst = nested_lists[i];

      if (lst.nextSibling) {
        var node = lst.nextSibling;
        var $new_li = $('<li>');
        $(lst.parentNode).after($new_li);
        do {
          var tmp = node;
          node = node.nextSibling;
          $new_li.append(tmp);
        } while (node);
      }
    }
  }

  function _listsTypeInNested () {
    // Make sure we can type in nested list.
    var nested_lists = editor.$el.get(0).querySelectorAll('li > ul, li > ol');
    for (var i = 0; i < nested_lists.length; i++) {
      var lst = nested_lists[i];

      // List is the first in the LI.
      if (editor.node.isFirstSibling(lst)) {
        $(lst).before('<br/>');
      }

      // Make sure we don't leave BR before list.
      else if (lst.previousSibling && lst.previousSibling.tagName == 'BR') {
        var prev_node = lst.previousSibling.previousSibling;

        // Skip markers.
        while (prev_node && $(prev_node).hasClass('fr-marker')) {
          prev_node = prev_node.previousSibling;
        }

        // Remove BR only if there is something else than BR.
        if (prev_node && prev_node.tagName != 'BR') {
          $(lst.previousSibling).remove();
        }
      }
    }
  }

  function _listsRemoveEmptyLI () {
    // Remove empty li.
    var empty_lis = editor.$el.get(0).querySelectorAll('li:empty');
    for (var i = 0; i < empty_lis.length; i++) {
      $(empty_lis[i]).remove();
    }
  }

  function _listsFindMissplacedText () {
    var lists = editor.$el.get(0).querySelectorAll('ul, ol');
    for (var i = 0; i < lists.length; i++) {
      var contents = editor.node.contents(lists[i]);

      var $li = null;
      for (var j = contents.length - 1; j >=0 ; j--) {
        if (contents[j].tagName != 'LI') {
          if (!$li) {
            $li = $('<li>');
            $li.insertBefore(contents[j]);
          }

          $li.append(contents[j]);
        }
        else {
          $li = null;
        }
      }
    }
  }

  /**
   * Clean lists.
   */
  function lists () {
    _listsWrapMissplacedLI();

    _listsJoinSiblings();

    _listsRemoveEmpty();

    _listsWrapLists();

    _listsNoTagAfterNested();

    _listsTypeInNested();

    _listsFindMissplacedText();

    _listsRemoveEmptyLI();
  }

  /**
   * Initialize
   */
  function _init () {
    // If fullPage is on allow head and title.
    if (editor.opts.fullPage) {
      $.merge(editor.opts.htmlAllowedTags, ['head', 'title', 'style', 'link', 'base', 'body', 'html']);
    }
  }

  return {
    _init: _init,
    html: html,
    toHTML5: toHTML5,
    tables: tables,
    lists: lists,
    quotes: quotes,
    invisibleSpaces: invisibleSpaces,
    exec: exec
  }
};

$.FE.XS = 0;
$.FE.SM = 1;
$.FE.MD = 2;
$.FE.LG = 3;

$.FE.MODULES.helpers = function (editor) {
  /**
   * Get the IE version.
   */
  function _ieVersion () {
    /*global navigator */
    var rv = -1;
    var ua;
    var re;

    if (navigator.appName == 'Microsoft Internet Explorer') {
      ua = navigator.userAgent;
      re = new RegExp('MSIE ([0-9]{1,}[\\.0-9]{0,})');
      if (re.exec(ua) !== null)
        rv = parseFloat(RegExp.$1);
    } else if (navigator.appName == 'Netscape') {
      ua = navigator.userAgent;
      re = new RegExp('Trident/.*rv:([0-9]{1,}[\\.0-9]{0,})');
      if (re.exec(ua) !== null)
        rv = parseFloat(RegExp.$1);
    }
    return rv;
  }

  /**
   * Determine the browser.
   */
  function _browser () {
    var browser = {};

    var ie_version = _ieVersion();
    if (ie_version > 0) {
      browser.msie = true;
    } else {
      var ua = navigator.userAgent.toLowerCase();

      var match =
        /(edge)[ \/]([\w.]+)/.exec(ua) ||
        /(chrome)[ \/]([\w.]+)/.exec(ua) ||
        /(webkit)[ \/]([\w.]+)/.exec(ua) ||
        /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
        /(msie) ([\w.]+)/.exec(ua) ||
        ua.indexOf('compatible') < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
        [];

      var matched = {
        browser: match[1] || '',
        version: match[2] || '0'
      };

      if (match[1]) browser[matched.browser] = true;

      // Chrome is Webkit, but Webkit is also Safari.
      if (browser.chrome) {
        browser.webkit = true;
      } else if (browser.webkit) {
        browser.safari = true;
      }
    }

    if (browser.msie) browser.version = ie_version;

    return browser;
  }

  function isIOS () {
    return /(iPad|iPhone|iPod)/g.test(navigator.userAgent) && !isWindowsPhone();
  }

  function isAndroid () {
    return /(Android)/g.test(navigator.userAgent) && !isWindowsPhone();
  }

  function isBlackberry () {
    return /(Blackberry)/g.test(navigator.userAgent);
  }

  function isWindowsPhone () {
    return /(Windows Phone)/gi.test(navigator.userAgent);
  }

  function isMobile () {
    return isAndroid() || isIOS() || isBlackberry();
  }

  function requestAnimationFrame () {
    return window.requestAnimationFrame ||
            window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame    ||
            function (callback) {
              window.setTimeout(callback, 1000 / 60);
            };
  }

  function getPX (val) {
    return parseInt(val, 10) || 0;
  }

  function screenSize () {
    var $test = $('<div class="fr-visibility-helper"></div>').appendTo('body');
    var size = getPX($test.css('margin-left'));
    $test.remove();
    return size;
  }

  function isTouch () {
    return ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
  }

  function isURL (url) {
    if (!/^(https?:|ftps?:|)\/\//i.test(url)) return false;

    url = String(url)
            .replace(/</g, '%3C')
            .replace(/>/g, '%3E')
            .replace(/"/g, '%22')
            .replace(/ /g, '%20');

    var test_reg = /(http|ftp|https):\/\/[a-z\u00a1-\uffff0-9]+(\.[a-z\u00a1-\uffff0-9]*)*([a-z\u00a1-\uffff0-9.,@?^=%&amp;:\/~+#-]*[a-z\u00a1-\uffff0-9@?^=%&amp;\/~+#-])?/gi;

    return test_reg.test(url);
  }

  // Sanitize URL.
  function sanitizeURL (url) {
    if (/^(https?:|ftps?:|)\/\//i.test(url)) {
      if (!isURL(url) && !isURL('http:' + url)) {
        return '';
      }
    }
    else {
      url = encodeURIComponent(url)
                .replace(/%23/g, '#')
                .replace(/%2F/g, '/')
                .replace(/%25/g, '%')
                .replace(/mailto%3A/gi, 'mailto:')
                .replace(/file%3A/gi, 'file:')
                .replace(/sms%3A/gi, 'sms:')
                .replace(/tel%3A/gi, 'tel:')
                .replace(/notes%3A/gi, 'notes:')
                .replace(/data%3Aimage/gi, 'data:image')
                .replace(/webkit-fake-url%3A/gi, 'webkit-fake-url:')
                .replace(/%3F/g, '?')
                .replace(/%3D/g, '=')
                .replace(/%26/g, '&')
                .replace(/&amp;/g, '&')
                .replace(/%2C/g, ',')
                .replace(/%3B/g, ';')
                .replace(/%2B/g, '+')
                .replace(/%40/g, '@')
                .replace(/%5B/g, '[')
                .replace(/%5D/g, ']')
                .replace(/%7B/g, '{')
                .replace(/%7D/g, '}');
    }

    return url;
  }

  function isArray (obj) {
    return obj && !(obj.propertyIsEnumerable('length')) &&
            typeof obj === 'object' && typeof obj.length === 'number';
  }

  /*
   * Transform RGB color to hex value.
   */
  function RGBToHex (rgb) {
    function hex(x) {
      return ('0' + parseInt(x, 10).toString(16)).slice(-2);
    }

    try {
      if (!rgb || rgb === 'transparent') return '';

      if (/^#[0-9A-F]{6}$/i.test(rgb)) return rgb;

      rgb = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);

      return ('#' + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3])).toUpperCase();
    }
    catch (ex) {
      return null;
    }
  }

  function HEXtoRGB (hex) {
    // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
    var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
    hex = hex.replace(shorthandRegex, function (m, r, g, b) {
      return r + r + g + g + b + b;
    });

    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result ? 'rgb(' + parseInt(result[1], 16) + ', ' + parseInt(result[2], 16) + ', ' + parseInt(result[3], 16) + ')' : '';
  }

  /*
   * Get block alignment.
   */
  var default_alignment;
  function getAlignment ($block) {
    var alignment = ($block.css('text-align') || '').replace(/-(.*)-/g, '');

    // Detect rtl.
    if (['left', 'right', 'justify', 'center'].indexOf(alignment) < 0) {
      if (!default_alignment) {
        var $div = $('<div dir="auto" style="text-align: initial; position: fixed; left: -3000px;"><span id="s1">.</span><span id="s2">.</span></div>');
        $('body').append($div);

        var l1 = $div.find('#s1').get(0).getBoundingClientRect().left;
        var l2 = $div.find('#s2').get(0).getBoundingClientRect().left;

        $div.remove();

        default_alignment = l1 < l2 ? 'left' : 'right';
      }

      alignment = default_alignment;
    }

    return alignment;
  }

  /**
   * Check if is mac.
   */
  var is_mac = null;
  function isMac () {
    if (is_mac == null) {
      is_mac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;;
    }

    return is_mac;
  }

  /**
   * Tear up.
   */
  function _init () {
    editor.browser = _browser();
  }

  return {
    _init: _init,
    isIOS: isIOS,
    isMac: isMac,
    isAndroid: isAndroid,
    isBlackberry: isBlackberry,
    isWindowsPhone: isWindowsPhone,
    isMobile: isMobile,
    requestAnimationFrame: requestAnimationFrame,
    getPX: getPX,
    screenSize: screenSize,
    isTouch: isTouch,
    sanitizeURL: sanitizeURL,
    isArray: isArray,
    RGBToHex: RGBToHex,
    HEXtoRGB: HEXtoRGB,
    isURL: isURL,
    getAlignment: getAlignment
  }
}

$.FE.MODULES.events = function (editor) {
  var _events = {};
  var _do_blur;

  // function _refresh () {
  //
  // }

  function _assignEvent($el, evs, handler) {
    $on($el, evs, handler);
  }

  function _forPaste () {
    _assignEvent(editor.$el, 'cut copy paste beforepaste', function (e) {
      trigger(e.type, [e]);
    });
  }

  function _forElement() {
    _assignEvent(editor.$el, 'click mouseup mousedown touchstart touchend dragenter dragover dragleave dragend drop dragstart', function (e) {
      trigger(e.type, [e]);
    });

    on('mousedown', function () {
      for (var i = 0; i < $.FE.INSTANCES.length; i++) {
        if ($.FE.INSTANCES[i] != editor && $.FE.INSTANCES[i].popups && $.FE.INSTANCES[i].popups.areVisible()) {
          $.FE.INSTANCES[i].$el.find('.fr-marker').remove();
        }
      }
    })
  }

  function _forKeys () {
    // Map events.
    _assignEvent(editor.$el, 'keydown keypress keyup input', function (e) {
      trigger(e.type, [e]);
    });
  }

  function _forWindow () {
    _assignEvent(editor.$win, editor._mousedown, function (e) {
      trigger('window.mousedown', [e]);
      enableBlur();
    });

    _assignEvent(editor.$win, editor._mouseup, function (e) {
      trigger('window.mouseup', [e]);
    });

    _assignEvent(editor.$win, 'cut copy keydown keyup touchmove touchend', function (e) {
      trigger('window.' + e.type, [e]);
    });
  }

  function _forDocument() {
    _assignEvent(editor.$doc, 'dragend drop', function (e) {
      trigger('document.' + e.type, [e]);
    })
  }

  function focus (do_focus) {
    if (typeof do_focus == 'undefined') do_focus = true;
    if (!editor.$wp) return false;

    // Focus the editor window.
    if (editor.helpers.isIOS()) {
      editor.$win.get(0).focus();
    }

    // If there is no focus, then force focus.
    if (!editor.core.hasFocus() && do_focus) {
      var st = editor.$win.scrollTop();
      editor.$el.focus();
      if (st != editor.$win.scrollTop()) {
        editor.$win.scrollTop(st);
      }
      return false;
    }

    // Don't go further if we haven't focused or there are markers.
    if (!editor.core.hasFocus() || editor.$el.find('.fr-marker').length > 0) {
      return false;
    }

    var info = editor.selection.info(editor.$el.get(0));

    if (info.atStart && editor.selection.isCollapsed()) {
      if (editor.html.defaultTag() != null) {
        var marker = editor.markers.insert();
        if (marker && !editor.node.blockParent(marker)) {
          $(marker).remove();

          var element = editor.$el.find(editor.html.blockTagsQuery()).get(0);
          if (element) {
            $(element).prepend($.FE.MARKERS);
            editor.selection.restore();
          }
        }
        else if (marker) {
          $(marker).remove();
        }
      }
    }
  }

  var focused = false;

  function _forFocus () {
    _assignEvent(editor.$el, 'focus', function (e) {
      if (blurActive()) {
        focus(false);
        if (focused === false) {
          trigger(e.type, [e]);
        }
      }
    });

    _assignEvent(editor.$el, 'blur', function (e) {
      if (blurActive() /* && document.activeElement != this */) {
        if (focused === true) {
          trigger(e.type, [e]);
        }
      }
    });

    on('focus', function () {
      focused = true;
    });

    on('blur', function () {
      focused = false;
    });
  }

  function _forMouse () {
    if (editor.helpers.isMobile()) {
      editor._mousedown = 'touchstart';
      editor._mouseup = 'touchend';
      editor._move = 'touchmove';
      editor._mousemove = 'touchmove';
    }
    else {
      editor._mousedown = 'mousedown';
      editor._mouseup = 'mouseup';
      editor._move = '';
      editor._mousemove = 'mousemove';
    }
  }

  function _buttonMouseDown (e) {
    var $btn = $(e.currentTarget);
    if (editor.edit.isDisabled() || $btn.hasClass('fr-disabled')) {
      e.preventDefault();
      return false;
    }

    // Not click button.
    if (e.type === 'mousedown' && e.which !== 1) return true;

    // Scroll in list.
    if (!editor.helpers.isMobile()) {
      e.preventDefault();
    }

    if ((editor.helpers.isAndroid() || editor.helpers.isWindowsPhone()) && $btn.parents('.fr-dropdown-menu').length === 0) {
      e.preventDefault();
      e.stopPropagation();
    }

    // Simulate click.
    $btn.addClass('fr-selected');

    editor.events.trigger('commands.mousedown', [$btn]);
  }

  function _buttonMouseUp (e, handler) {
    var $btn = $(e.currentTarget);

    if (editor.edit.isDisabled() || $btn.hasClass('fr-disabled')) {
      e.preventDefault();
      return false;
    }

    if (e.type === 'mouseup' && e.which !== 1) return true;
    if (!$btn.hasClass('fr-selected')) return true;

    if (e.type != 'touchmove') {
      e.stopPropagation();
      e.stopImmediatePropagation();
      e.preventDefault();

      // Simulate click.
      if (!$btn.hasClass('fr-selected')) {
        $('.fr-selected').removeClass('fr-selected');
        return false;
      }
      $('.fr-selected').removeClass('fr-selected');

      if ($btn.data('dragging') || $btn.attr('disabled')) {
        $btn.removeData('dragging');
        return false;
      }

      var timeout = $btn.data('timeout');
      if (timeout) {
        clearTimeout(timeout);
        $btn.removeData('timeout');
      }

      handler.apply(editor, [e]);
    }
    else {
      if (!$btn.data('timeout')) {
        $btn.data('timeout', setTimeout(function () {
          $btn.data('dragging', true);
        }, 100));
      }
    }
  }

  function enableBlur () {
    _do_blur = true;
  }

  function disableBlur () {
    _do_blur = false;
  }

  function blurActive () {
    return _do_blur;
  }

  /**
   * Bind click on an element.
   */
  function bindClick ($element, selector, handler) {
    $on($element, editor._mousedown, selector, function (e) {
      if (!editor.edit.isDisabled()) _buttonMouseDown(e);
    }, true);

    $on($element, editor._mouseup + ' ' + editor._move, selector, function (e) {
      if (!editor.edit.isDisabled()) _buttonMouseUp(e, handler);
    }, true);

    $on($element, 'mousedown click mouseup', selector, function (e) {
      if (!editor.edit.isDisabled()) e.stopPropagation();
    }, true);

    on('window.mouseup', function () {
      if (!editor.edit.isDisabled()) {
        $element.find(selector).removeClass('fr-selected');
        enableBlur();
      }
    });
  }

  /**
   * Add event.
   */
  function on (name, callback, first) {
    var names = name.split(' ');
    if (names.length > 1) {
      for (var i = 0; i < names.length; i++) {
        on(names[i], callback, first);
      }

      return true;
    }

    if (typeof first == 'undefined') first = false;

    var callbacks;
    if (name.indexOf('shared.') != 0) {
      callbacks = (_events[name] = _events[name] || []);
    }
    else {
      callbacks = (editor.shared._events[name] = editor.shared._events[name] || []);
    }

    if (first) {
      callbacks.unshift(callback);
    }
    else {
      callbacks.push(callback);
    }
  }

              var $_events = [];
              function $on ($el, evs, selector, callback, shared) {
    if (typeof selector == 'function') {
      shared = callback;
      callback = selector;
      selector = false;
    }

    var ary = (!shared ? $_events : editor.shared.$_events);
    var id = (!shared ? editor.id : editor.sid);

    if (!selector) {
      $el.on(evs.split(' ').join('.ed' + id + ' ') + '.ed' + id, callback);
    }
    else {
      $el.on(evs.split(' ').join('.ed' + id + ' ') + '.ed' + id, selector, callback);
    }

    if (ary.indexOf($el.get(0)) < 0) ary.push($el.get(0));
              }

  function _$off (evs, id) {
                      for (var i = 0; i < evs.length; i++) {
                              $(evs[i]).off('.ed' + id);
                      }
  }

              function $off () {
    _$off($_events, editor.id);
    $_events = [];

    if (editor.shared.count == 0) {
                      _$off(editor.shared.$_events, editor.sid);
      editor.shared.$_events = null;
    }
              }

  /**
   * Trigger an event.
   */
  function trigger (name, args, force) {
    if (!editor.edit.isDisabled() || force) {
      var callbacks;

      if (name.indexOf('shared.') != 0) {
        callbacks = _events[name];
      }
      else {
        if (editor.shared.count > 0) return false;
        callbacks = editor.shared._events[name];
      }

      var val;

      if (callbacks) {
        for (var i = 0; i < callbacks.length; i++) {
          val = callbacks[i].apply(editor, args);
          if (val === false) return false;
        }
      }

      // Trigger event outside.
      val = editor.$oel.triggerHandler('froalaEditor.' + name, $.merge([editor], (args || [])));
      if (val === false) return false;

      return val;
    }
  }

  function chainTrigger (name, param, force) {
    if (!editor.edit.isDisabled() || force) {
      var callbacks;

      if (name.indexOf('shared.') != 0) {
        callbacks = _events[name];
      }
      else {
        if (editor.shared.count > 0) return false;
        callbacks = editor.shared._events[name];
      }

      var resp;

      if (callbacks) {
        for (var i = 0; i < callbacks.length; i++) {
          // Get the callback response.
          resp = callbacks[i].apply(editor, [param]);

          // If callback response is defined then assign it to param.
          if (typeof resp !== 'undefined') param = resp;
        }
      }

      // Trigger event outside.
      resp = editor.$oel.triggerHandler('froalaEditor.' + name, $.merge([editor], [param]));

      // If callback response is defined then assign it to param.
      if (typeof resp !== 'undefined') param = resp;

      return param;
    }
  }

  /**
   * Destroy
   */
  function _destroy () {
    // Clear the events list.
    for (var k in _events) {
      if (_events.hasOwnProperty(k)) {
        delete _events[k];
      }
    }
  }

  function _sharedDestroy () {
    for (var k in editor.shared._events) {
      if (editor.shared._events.hasOwnProperty(k)) {
        delete editor.shared._events[k];
      }
    }
  }

  /**
   * Tear up.
   */
  function _init () {
    editor.shared.$_events = editor.shared.$_events || [];
    editor.shared._events = {};

    _forMouse();

    _forElement();
    _forWindow();
    _forDocument();
    _forKeys();

    _forFocus();
    enableBlur();

    _forPaste();

    on('destroy', _destroy);
  //   on('refresh', _refresh);

    on('shared.destroy', _sharedDestroy);
  }

  return {
    _init: _init,
    on: on,
    trigger: trigger,
    bindClick: bindClick,
    disableBlur: disableBlur,
    enableBlur: enableBlur,
    blurActive: blurActive,
    focus: focus,
    chainTrigger: chainTrigger,
                      $on: $on,
                      $off: $off
  }
};

$.FE.INVISIBLE_SPACE = '&#8203;';
$.FE.START_MARKER = '<span class="fr-marker" data-id="0" data-type="true" style="display: none; line-height: 0;">' + $.FE.INVISIBLE_SPACE + '</span>';
$.FE.END_MARKER = '<span class="fr-marker" data-id="0" data-type="false" style="display: none; line-height: 0;">'  + $.FE.INVISIBLE_SPACE + '</span>';
$.FE.MARKERS = $.FE.START_MARKER + $.FE.END_MARKER;

$.FE.MODULES.markers = function (editor) {
  /**
   * Build marker element.
   */
  function _build (marker, id) {
    return $('<span class="fr-marker" data-id="' + id + '" data-type="' + marker + '" style="display: ' + (editor.browser.safari ? 'none' : 'inline-block') + '; line-height: 0;">' + $.FE.INVISIBLE_SPACE + '</span>', editor.doc)[0];
  }

  /**
   * Place marker.
   */
  function place (range, marker, id) {
    try {
      var boundary = range.cloneRange();
      boundary.collapse(marker);

      boundary.insertNode(_build(marker, id));

      if (marker === true && range.collapsed) {
        var mk = editor.$el.find('span.fr-marker[data-type="true"][data-id="' + id + '"]');
        var sibling = mk.get(0).nextSibling;
        while (sibling && sibling.nodeType === Node.TEXT_NODE && sibling.textContent.length === 0) {
          $(sibling).remove();
          sibling = mk.nextSibling;
        }
      }

      if (marker === true && !range.collapsed) {
        var mk = editor.$el.find('span.fr-marker[data-type="true"][data-id="' + id + '"]').get(0);
        var sibling = mk.nextSibling;
        if (sibling && sibling.nodeType === Node.ELEMENT_NODE && editor.node.isBlock(sibling)) {
          // Place the marker deep inside the block tags.
          var contents = [sibling];
          do {
            sibling = contents[0];
            contents = editor.node.contents(sibling);
          } while (contents[0] && editor.node.isBlock(contents[0]));

          $(sibling).prepend($(mk));
        }
      }

      if (marker === false && !range.collapsed) {
        var mk = editor.$el.find('span.fr-marker[data-type="false"][data-id="' + id + '"]').get(0);
        var sibling = mk.previousSibling;
        if (sibling && sibling.nodeType === Node.ELEMENT_NODE && editor.node.isBlock(sibling)) {
          // Place the marker deep inside the block tags.
          var contents = [sibling];
          do {
            sibling = contents[contents.length - 1];
            contents = editor.node.contents(sibling);
          } while (contents[contents.length - 1] && editor.node.isBlock(contents[contents.length - 1]));

          $(sibling).append($(mk));
        }

        // https://github.com/froala/wysiwyg-editor/issues/705
        if (mk.parentNode && ['TD', 'TH'].indexOf(mk.parentNode.tagName) >= 0) {
          if (mk.parentNode.previousSibling && !mk.previousSibling) {
            $(mk.parentNode.previousSibling).append(mk);
          }
        }
      }

      var dom_marker = editor.$el.find('span.fr-marker[data-type="' + marker + '"][data-id="' + id + '"]').get(0);

      // If image is at the top of the editor in an empty P
      // and floated to right, the text will be pushed down
      // when trying to insert an image.
      if (dom_marker) dom_marker.style.display = 'none';
      return dom_marker;
    }
    catch (ex) {
      return null;
    }
  }

  /**
   * Insert a single marker.
   */
  function insert () {
    if (!editor.$wp) return null;

    try {
      var range = editor.selection.ranges(0);
      var containter = range.commonAncestorContainer;

      // Check if selection is inside editor.
      if (containter != editor.$el.get(0) && editor.$el.find(containter).length == 0) return null;

      var boundary = range.cloneRange();
      var original_range = range.cloneRange();
      boundary.collapse(true);

      var mk = $('<span class="fr-marker" style="display: none; line-height: 0;">' + $.FE.INVISIBLE_SPACE + '</span>', editor.doc)[0];

      boundary.insertNode(mk);

      mk = editor.$el.find('span.fr-marker').get(0);

      if (mk) {
        var sibling = mk.nextSibling;
        while (sibling && sibling.nodeType === Node.TEXT_NODE && sibling.textContent.length === 0) {
          $(sibling).remove();
          sibling = editor.$el.find('span.fr-marker').get(0).nextSibling;
        }

        // Keep original selection.
        editor.selection.clear();
        editor.selection.get().addRange(original_range);

        return mk;
      }
      else {
        return null;
      }
    }
    catch (ex) {
      console.warn ('MARKER', ex)
    }
  }

  /**
   * Split HTML at the marker position.
   */
  function split () {
    if (!editor.selection.isCollapsed()) {
      editor.selection.remove();
    }

    var marker = editor.$el.find('.fr-marker').get(0);
    if (marker == null) marker = insert();
    if (marker == null) return null;

    var deep_parent;
    if ((deep_parent = editor.node.deepestParent(marker))) {
      if (editor.node.isBlock(deep_parent) && editor.node.isEmpty(deep_parent)) {
        $(deep_parent).replaceWith('<span class="fr-marker"></span>');
      }
      else {
        var node = marker;
        var close_str = '';
        var open_str = '';
        do {
          node = node.parentNode;
          close_str = close_str + editor.node.closeTagString(node);
          open_str = editor.node.openTagString(node) + open_str;
        } while (node != deep_parent);

        $(marker).replaceWith('<span id="fr-break"></span>');
        var h = editor.node.openTagString(deep_parent) + $(deep_parent).html() + editor.node.closeTagString(deep_parent);
        h = h.replace(/<span id="fr-break"><\/span>/g, close_str + '<span class="fr-marker"></span>' + open_str);

        $(deep_parent).replaceWith(h);
      }
    }

    return editor.$el.find('.fr-marker').get(0)
  }

  /**
   * Insert marker at point from event.
   *
   * http://stackoverflow.com/questions/11191136/set-a-selection-range-from-a-to-b-in-absolute-position
   * https://developer.mozilla.org/en-US/docs/Web/API/this.document.caretPositionFromPoint
   */
  function insertAtPoint (e) {
    var x = e.clientX;
    var y = e.clientY;

    // Clear markers.
    remove();

    var start;
    var range = null;

    // Default.
    if (typeof editor.doc.caretPositionFromPoint != 'undefined') {
      start = editor.doc.caretPositionFromPoint(x, y);
      range = editor.doc.createRange();

      range.setStart(start.offsetNode, start.offset);
      range.setEnd(start.offsetNode, start.offset);
    }

    // Webkit.
    else if (typeof editor.doc.caretRangeFromPoint != 'undefined') {
      start = editor.doc.caretRangeFromPoint(x, y);
      range = editor.doc.createRange();

      range.setStart(start.startContainer, start.startOffset);
      range.setEnd(start.startContainer, start.startOffset);
    }

    // Set ranges.
    if (range !== null && typeof editor.win.getSelection != 'undefined') {
      var sel = editor.win.getSelection();
      sel.removeAllRanges();
      sel.addRange(range);
    }

    // MSIE.
    else if (typeof editor.doc.body.createTextRange != 'undefined') {
      try {
        range = editor.doc.body.createTextRange();
        range.moveToPoint(x, y);
        var end_range = range.duplicate();
        end_range.moveToPoint(x, y);
        range.setEndPoint('EndToEnd', end_range);
        range.select();
      }
      catch (ex) {
        return false;
      }
    }

    insert();
  }

  /**
   * Remove markers.
   */
  function remove () {
    editor.$el.find('.fr-marker').remove();
  }

  return {
    place: place,
    insert: insert,
    split: split,
    insertAtPoint: insertAtPoint,
    remove: remove
  }
};

$.FE.MODULES.selection = function (editor) {
  /**
   * Get selection text.
   */
  function text () {
    var text = '';

    if (editor.win.getSelection) {
      text = editor.win.getSelection();
    } else if (editor.doc.getSelection) {
      text = editor.doc.getSelection();
    } else if (editor.doc.selection) {
      text = editor.doc.selection.createRange().text;
    }

    return text.toString();
  }

  /**
   * Get the selection object.
   */
  function get () {
    var selection = '';
    if (editor.win.getSelection) {
      selection = editor.win.getSelection();
    } else if (editor.doc.getSelection) {
      selection = editor.doc.getSelection();
    } else {
      selection = editor.doc.selection.createRange();
    }

    return selection;
  }

  /**
   * Get the selection ranges or a single range at a specified index.
  */
  function ranges (index) {
    var sel = get();
    var ranges = [];

    // Get ranges.
    if (sel && sel.getRangeAt && sel.rangeCount) {
      var ranges = [];
      for (var i = 0; i < sel.rangeCount; i++) {
        ranges.push(sel.getRangeAt(i));
      }
    }
    else {
      if (editor.doc.createRange) {
        ranges = [editor.doc.createRange()];
      } else {
        ranges = [];
      }
    }

    return (typeof index != 'undefined' ? ranges[index] : ranges);
  }

  /**
   * Clear selection.
   */
  function clear () {
    var sel = get();

    try {
      if (sel.removeAllRanges) {
        sel.removeAllRanges();
      } else if (sel.empty) {  // IE?
        sel.empty();
      } else if (sel.clear) {
        sel.clear();
      }
    }
    catch (ex) {}
  }

  /**
   * Selection element.
  */
  function element () {
    var sel = get();

    try {
      if (sel.rangeCount) {
        var range = ranges(0);
        var node = range.startContainer;

        // Get parrent if node type is not DOM.
        if (node.nodeType == Node.ELEMENT_NODE) {
          var node_found = false;

          // Search for node deeper.
          if (node.childNodes.length > 0 && node.childNodes[range.startOffset]) {
            var child = node.childNodes[range.startOffset];
            while (child && child.nodeType == Node.TEXT_NODE && child.textContent.length == 0) {
              child = child.nextSibling;
            }

            if (child && child.textContent.replace(/\u200B/g, '') === text().replace(/\u200B/g, '')) {
              node = child;
              node_found = true;
            }
          }
          // Selection starts just at the end of the node.
          else if (!range.collapsed && node.nextSibling && node.nextSibling.nodeType == Node.ELEMENT_NODE) {
            var child = node.nextSibling;

            if (child && child.textContent.replace(/\u200B/g, '') === text().replace(/\u200B/g, '')) {
              node = child;
              node_found = true;
            }
          }

          if (!node_found && node.childNodes.length > 0 && $(node.childNodes[0]).text().replace(/\u200B/g, '') === text().replace(/\u200B/g, '') && ['BR', 'IMG', 'HR'].indexOf(node.childNodes[0].tagName) < 0) {
            node = node.childNodes[0];
          }
        }

        while (node.nodeType != Node.ELEMENT_NODE && node.parentNode) {
          node = node.parentNode;
        }

        // Make sure the node is in editor.
        var p = node;
        while (p && p.tagName != 'HTML') {
          if (p == editor.$el.get(0)) {
            return node;
          }

          p = $(p).parent()[0];
        }
      }
    }
    catch (ex) {

    }

    return editor.$el.get(0);
  }

  /**
   * Selection element.
  */
  function endElement () {
    var sel = get();

    try {
      if (sel.rangeCount) {
        var range = ranges(0);
        var node = range.endContainer;

        // Get parrent if node type is not DOM.
        if (node.nodeType == Node.ELEMENT_NODE) {
          var node_found = false;

          // Search for node deeper.
          if (node.childNodes.length > 0 && node.childNodes[range.endOffset] && $(node.childNodes[range.endOffset]).text() === text()) {
            node = node.childNodes[range.endOffset];
            node_found = true;
          }
          // Selection starts just at the end of the node.
          else if (!range.collapsed && node.previousSibling && node.previousSibling.nodeType == Node.ELEMENT_NODE) {
            var child = node.previousSibling;

            if (child && child.textContent.replace(/\u200B/g, '') === text().replace(/\u200B/g, '')) {
              node = child;
              node_found = true;
            }
          }
          // Browser sees selection at the beginning of the next node.
          else if (!range.collapsed && node.childNodes.length > 0 && node.childNodes[range.endOffset]) {
            var child = node.childNodes[range.endOffset].previousSibling;

            if (child.nodeType == Node.ELEMENT_NODE) {
              if (child && child.textContent.replace(/\u200B/g, '') === text().replace(/\u200B/g, '')) {
                node = child;
                node_found = true;
              }
            }
          }

          if (!node_found && node.childNodes.length > 0 && $(node.childNodes[node.childNodes.length - 1]).text() === text() && ['BR', 'IMG', 'HR'].indexOf(node.childNodes[node.childNodes.length - 1].tagName) < 0) {
            node = node.childNodes[node.childNodes.length - 1];
          }
        }

        if (node.nodeType == Node.TEXT_NODE && range.endOffset == 0 && node.previousSibling && node.previousSibling.nodeType == Node.ELEMENT_NODE) {
          node = node.previousSibling;
        }

        while (node.nodeType != Node.ELEMENT_NODE && node.parentNode) {
          node = node.parentNode;
        }

        // Make sure the node is in editor.
        var p = node;
        while (p && p.tagName != 'HTML') {
          if (p == editor.$el.get(0)) {
            return node;
          }

          p = $(p).parent()[0];
        }
      }
    }
    catch (ex) {
    }

    return editor.$el.get(0);
  }

  /**
   * Get the ELEMENTS node where the selection starts.
   * By default TEXT node might be selected.
   */
  function rangeElement(rangeContainer, offset) {
    var node = rangeContainer;
    if (node.nodeType == Node.ELEMENT_NODE) {
      // Search for node deeper.
      if (node.childNodes.length > 0 && node.childNodes[offset]) {
        node = node.childNodes[offset];
      }
    }

    if (node.nodeType == Node.TEXT_NODE) {
      node = node.parentNode;
    }

    return node;
  }

  /**
   * Search for the current selected blocks.
   */
  function blocks () {
    var blks = [];

    var sel = get();

    // Selection must be inside editor.
    if (inEditor() && sel.rangeCount) {

      // Loop through ranges.
      var rngs = ranges();
      for (var i = 0; i < rngs.length; i++) {
        var range = rngs[i];

        // Get start node and end node for range.
        var start_node = rangeElement(range.startContainer, range.startOffset);
        var end_node  = rangeElement(range.endContainer, range.endOffset);

        // Add the start node.
        if (editor.node.isBlock(start_node) && blks.indexOf(start_node) < 0) blks.push(start_node);

        // Check for the parent node of the start node.
        var block_parent = editor.node.blockParent(start_node);
        if (block_parent && blks.indexOf(block_parent) < 0) {
          blks.push(block_parent);
        }

        // Do not add nodes where we've been once.
        var was_into = [];

        // Loop until we reach end.
        var next_node = start_node;
        while (next_node !== end_node && next_node !== editor.$el.get(0)) {
          // Get deeper into the current node.
          if (was_into.indexOf(next_node) < 0 && next_node.children && next_node.children.length) {
            was_into.push(next_node);
            next_node = next_node.children[0];
          }

          // Get next sibling.
          else if (next_node.nextSibling) {
            next_node = next_node.nextSibling;
          }

          // Get parent node.
          else if (next_node.parentNode) {
            next_node = next_node.parentNode;
            was_into.push(next_node);
          }

          // Add node to the list.
          if (editor.node.isBlock(next_node) && was_into.indexOf(next_node) < 0 && blks.indexOf(next_node) < 0) {
            if (next_node !== end_node || range.endOffset > 0) {
              blks.push(next_node);
            }
          }
        }

        // Add the end node.
        if (editor.node.isBlock(end_node) && blks.indexOf(end_node) < 0 && range.endOffset > 0) blks.push(end_node);

        // Check for the parent node of the end node.
        var block_parent = editor.node.blockParent(end_node);
        if (block_parent && blks.indexOf(block_parent) < 0) {
          blks.push(block_parent);
        }
      }
    }

    // Remove blocks that we don't need.
    for (var i = blks.length - 1; i > 0; i--) {
      // Nodes that contain another node. Don't do it for LI.
      if ($(blks[i]).find(blks).length && blks[i].tagName != 'LI') blks.splice(i, 1);
    }

    return blks;
  }

  /**
   * Save selection.
   */
  function save () {
    if (editor.$wp) {
      editor.markers.remove();

      var rgs = ranges();
      var new_ranges = [];

      for (var i = 0; i < rgs.length; i++) {
        if (rgs[i].startContainer !== editor.doc) {
          var range = rgs[i];
          var collapsed = range.collapsed;
          var start_m = editor.markers.place(range, true, i); // Start.
          var end_m = editor.markers.place(range, false, i); // End.

          if (editor.browser.safari && !collapsed) {
            var range = editor.doc.createRange();
            range.setStartAfter(start_m);
            range.setEndBefore(end_m);
            new_ranges.push(range);
          }
        }
      }

      if (editor.browser.safari && new_ranges.length) {
        editor.selection.clear();
        for (var i = 0; i < new_ranges.length; i++) {
          editor.selection.get().addRange(new_ranges[i]);
        }
      }
    }
  }

  /**
   * Restore selection.
   */
  function restore () {
    // Get markers.
    var markers = editor.$el.get(0).querySelectorAll('.fr-marker[data-type="true"]');

    if (!editor.$wp) {
      editor.markers.remove();
      return false;
    }

    // No markers.
    if (markers.length === 0) {
      return false;
    }

    if (editor.browser.msie || editor.browser.edge) {
      for (var i = 0; i < markers.length; i++) {
        markers[i].style.display = 'inline-block';
      }
    }

    // Focus.
    if (!editor.core.hasFocus() && !editor.browser.msie && !editor.browser.webkit) {
      editor.$el.focus();
    }

    clear();
    var sel = get();

    var parents = [];

    // Add ranges.
    for (var i = 0; i < markers.length; i++) {
      var id = $(markers[i]).data('id');
      var start_marker = markers[i];
      var range = editor.doc.createRange();
      var end_marker = editor.$el.find('.fr-marker[data-type="false"][data-id="' + id + '"]');

      if (editor.browser.msie || editor.browser.edge) end_marker.css('display', 'inline-block');

      var ghost = null;

      // Make sure there is an start marker.
      if (end_marker.length > 0) {
        end_marker = end_marker[0];

        try {
          // If we have markers one next to each other inside text, then we should normalize text by joining it.
          var special_case = false;

          // Clear empty text nodes.
          var s_node = start_marker.nextSibling;
          while (s_node && s_node.nodeType == Node.TEXT_NODE && s_node.textContent.length == 0) {
            var tmp = s_node;
            s_node = s_node.nextSibling;
            $(tmp).remove();
          }

          var e_node = end_marker.nextSibling;
          while (e_node && e_node.nodeType == Node.TEXT_NODE && e_node.textContent.length == 0) {
            var tmp = e_node;
            e_node = e_node.nextSibling;
            $(tmp).remove();
          }

          if (start_marker.nextSibling == end_marker || end_marker.nextSibling == start_marker) {
            // Decide which is first and which is last between markers.
            var first_node = (start_marker.nextSibling == end_marker) ? start_marker : end_marker;
            var last_node = (first_node == start_marker) ? end_marker : start_marker;

            // Previous node.
            var prev_node = first_node.previousSibling;
            while (prev_node && prev_node.nodeType == Node.TEXT_NODE && prev_node.length == 0) {
              var tmp = prev_node;
              prev_node = prev_node.previousSibling;
              $(tmp).remove();
            }

            // Normalize text before.
            if (prev_node && prev_node.nodeType == Node.TEXT_NODE) {
              while (prev_node && prev_node.previousSibling && prev_node.previousSibling.nodeType == Node.TEXT_NODE) {
                prev_node.previousSibling.textContent = prev_node.previousSibling.textContent + prev_node.textContent;
                prev_node = prev_node.previousSibling;
                $(prev_node.nextSibling).remove();
              }
            }

            // Next node.
            var next_node = last_node.nextSibling;
            while (next_node && next_node.nodeType == Node.TEXT_NODE && next_node.length == 0) {
              var tmp = next_node;
              next_node = next_node.nextSibling;
              $(tmp).remove();
            }

            // Normalize text after.
            if (next_node && next_node.nodeType == Node.TEXT_NODE) {
              while (next_node && next_node.nextSibling && next_node.nextSibling.nodeType == Node.TEXT_NODE) {
                next_node.nextSibling.textContent =  next_node.textContent + next_node.nextSibling.textContent;
                next_node = next_node.nextSibling;
                $(next_node.previousSibling).remove();
              }
            }

            if (prev_node && (editor.node.isVoid(prev_node) || editor.node.isBlock(prev_node))) prev_node = null;
            if (next_node && (editor.node.isVoid(next_node) || editor.node.isBlock(next_node))) next_node = null;

            // Previous node and next node are both text.
            if (prev_node && next_node && prev_node.nodeType == Node.TEXT_NODE && next_node.nodeType == Node.TEXT_NODE) {
              // Remove markers.
              $(start_marker).remove();
              $(end_marker).remove();

              // Save cursor position.
              var len = prev_node.textContent.length;
              prev_node.textContent = prev_node.textContent + next_node.textContent;
              $(next_node).remove();

              // Normalize spaces.
              editor.spaces.normalize(prev_node);

              // Restore position.
              range.setStart(prev_node, len);
              range.setEnd(prev_node, len);

              special_case = true;
            }
            else if (!prev_node && next_node && next_node.nodeType == Node.TEXT_NODE) {
              // Remove markers.
              $(start_marker).remove();
              $(end_marker).remove();

              // Normalize spaces.
              editor.spaces.normalize(next_node);

              ghost = $(editor.doc.createTextNode('\u200B'));
              $(next_node).before(ghost);

              // Restore position.
              range.setStart(next_node, 0);
              range.setEnd(next_node, 0);
              special_case = true;
            }
            else if (!next_node && prev_node && prev_node.nodeType == Node.TEXT_NODE) {
              // Remove markers.
              $(start_marker).remove();
              $(end_marker).remove();

              // Normalize spaces.
              editor.spaces.normalize(prev_node);

              ghost = $(editor.doc.createTextNode('\u200B'));
              $(prev_node).after(ghost);

              // Restore position.
              range.setStart(prev_node, prev_node.textContent.length);
              range.setEnd(prev_node, prev_node.textContent.length);

              special_case = true;
            }
          }

          if (!special_case) {
            var x, y;
            // DO NOT TOUCH THIS OR IT WILL BREAK!!!
            if (editor.browser.chrome && start_marker.nextSibling == end_marker) {
              x = _normalizedMarker(end_marker, range, true) || range.setStartAfter(end_marker);
              y = _normalizedMarker(start_marker, range, false) || range.setEndBefore(start_marker);
            }
            else {
              if (start_marker.previousSibling == end_marker) {
                start_marker = end_marker;
                end_marker = start_marker.nextSibling;
              }

              // https://github.com/froala/wysiwyg-editor/issues/759
              if (!(end_marker.nextSibling && end_marker.nextSibling.tagName === 'BR') &&
                  !(!end_marker.nextSibling && editor.node.isBlock(start_marker.previousSibling)) &&
                  !(start_marker.previousSibling && start_marker.previousSibling.tagName == 'BR')) {
                start_marker.style.display = 'inline';
                end_marker.style.display = 'inline';
                ghost = $(editor.doc.createTextNode('\u200B'));
              }

              // https://github.com/froala/wysiwyg-editor/issues/1120
              var p_node = start_marker.previousSibling;
              if (p_node && p_node.style && editor.win.getComputedStyle(p_node).display == 'block' && !editor.opts.enter == $.FE.ENTER_BR) {
                range.setEndAfter(p_node);
                range.setStartAfter(p_node);
              }
              else {
                x = _normalizedMarker(start_marker, range, true) || ($(start_marker).before(ghost) && range.setStartBefore(start_marker));
                y = _normalizedMarker(end_marker, range, false) || ($(end_marker).after(ghost) && range.setEndAfter(end_marker));
              }
            }

            if (typeof x == 'function') x();
            if (typeof y == 'function') y();
          }
        } catch (ex) {
          console.warn ('RESTORE RANGE', ex);
        }
      }

      if (ghost) {
        ghost.remove();
      }

      try {
        sel.addRange(range);
      }
      catch (ex) {
        console.warn ('ADD RANGE', ex);
      }
    }

    // Remove used markers.
    editor.markers.remove();
  }

  /**
   * Normalize marker when restoring selection.
   */
  function _normalizedMarker(marker, range, start) {
    var prev_node = marker.previousSibling;
    var next_node = marker.nextSibling;

    // Prev and next node are both text nodes.
    if (prev_node && next_node && prev_node.nodeType == Node.TEXT_NODE && next_node.nodeType == Node.TEXT_NODE) {
      var len = prev_node.textContent.length;

      if (start) {
       next_node.textContent = prev_node.textContent + next_node.textContent;
       $(prev_node).remove();
       $(marker).remove();

       editor.spaces.normalize(next_node);

       return function () {
         range.setStart(next_node, len);
       }
      }
      else {
        prev_node.textContent = prev_node.textContent + next_node.textContent;
        $(next_node).remove();
        $(marker).remove();

        editor.spaces.normalize(prev_node);

        return function () {
          range.setEnd(prev_node, len);
        }
      }
    }
    // Prev node is text node.
    else if (prev_node && !next_node && prev_node.nodeType == Node.TEXT_NODE) {
      var len = prev_node.textContent.length;

      if (start) {
        editor.spaces.normalize(prev_node);
        return function () {
          range.setStart(prev_node, len);
        }
      }
      else {
        editor.spaces.normalize(prev_node);
        return function () {
          range.setEnd(prev_node, len);
        }
      }
    }

    // Next node is text node.
    else if (next_node && !prev_node && next_node.nodeType == Node.TEXT_NODE) {
      if (start) {
        editor.spaces.normalize(next_node);
        return function () {
          range.setStart(next_node, 0);
        }
      }
      else {
        editor.spaces.normalize(next_node);
        return function () {
          range.setEnd(next_node, 0);
        }
      }
    }

    return false;
  }

  /**
   * Determine if we can do delete.
   */
  function _canDelete () {
    return true;
  }

  /**
   * Check if selection is collapsed.
   */
  function isCollapsed () {
    var rgs = ranges();
    for (var i = 0; i < rgs.length; i++) {
      if (!rgs[i].collapsed) return false;
    }

    return true;
  }

  // From: http://www.coderexception.com/0B1B33z1NyQxUQSJ/contenteditable-div-how-can-i-determine-if-the-cursor-is-at-the-start-or-end-of-the-content
  function info (el) {
    var atStart = false;
    var atEnd = false;
    var selRange;
    var testRange;

    if (editor.win.getSelection) {
      var sel = editor.win.getSelection();
      if (sel.rangeCount) {
        selRange = sel.getRangeAt(0);
        testRange = selRange.cloneRange();

        testRange.selectNodeContents(el);
        testRange.setEnd(selRange.startContainer, selRange.startOffset);
        atStart = (testRange.toString() === '');

        testRange.selectNodeContents(el);
        testRange.setStart(selRange.endContainer, selRange.endOffset);
        atEnd = (testRange.toString() === '');
      }
    } else if (editor.doc.selection && editor.doc.selection.type != 'Control') {
      selRange = editor.doc.selection.createRange();
      testRange = selRange.duplicate();

      testRange.moveToElementText(el);
      testRange.setEndPoint('EndToStart', selRange);
      atStart = (testRange.text === '');

      testRange.moveToElementText(el);
      testRange.setEndPoint('StartToEnd', selRange);
      atEnd = (testRange.text === '');
    }

    return { atStart: atStart, atEnd: atEnd };
  }

  /**
   * Check if everything is selected inside the editor.
   */
  function isFull () {
    if (isCollapsed()) return false;

    // https://github.com/froala/wysiwyg-editor/issues/710
    editor.$el.find('td').prepend('<span class="fr-mk">' + $.FE.INVISIBLE_SPACE + '</span>');
    editor.$el.find('img').append('<span class="fr-mk">' + $.FE.INVISIBLE_SPACE + '</span>');

    var full = false;
    var inf = info(editor.$el.get(0));
    if (inf.atStart && inf.atEnd) full = true;

    // https://github.com/froala/wysiwyg-editor/issues/710
    editor.$el.find('.fr-mk').remove();

    return full;
  }

  /**
   * Remove HTML from inner nodes when we deal with keepFormatOnDelete option.
   */
  function _emptyInnerNodes (node, first) {
    if (typeof first == 'undefined') first = true;

    // Remove invisible spaces.
    var h = $(node).html();
    if (h && h.replace(/\u200b/g, '').length != h.length) $(node).html(h.replace(/\u200b/g, ''));

    // Loop contents.
    var contents = editor.node.contents(node);
    for (var j = 0; j < contents.length; j++) {
      // Remove text nodes.
      if (contents[j].nodeType != Node.ELEMENT_NODE) {
        $(contents[j]).remove();
      }

      // Empty inner nodes further.
      else {
        // j == 0 determines if the node is the first one and we should keep format.
        _emptyInnerNodes(contents[j], j == 0);

        // There are inner nodes, ignore the current one.
        if (j == 0) first = false;
      }
    }

    // First node is a text node, so replace it with a span.
    if (node.nodeType == Node.TEXT_NODE) {
      $(node).replaceWith('<span data-first="true" data-text="true"></span>');
    }

    // Add the first node marker so that we add selection in it later on.
    else if (first) {
      $(node).attr('data-first', true);
    }
  }

  /**
   * Process deleting nodes.
   */
  function _processNodeDelete ($node, should_delete) {
    var contents = editor.node.contents($node.get(0));

    // Node is TD or TH.
    if (['TD', 'TH'].indexOf($node.get(0).tagName) >= 0 && $node.find('.fr-marker').length == 1 && $(contents[0]).hasClass('fr-marker')) {
      $node.attr('data-del-cell', true);
    }

    for (var i = 0; i < contents.length; i++) {
      var node = contents[i];

      // We found a marker.
      if ($(node).hasClass('fr-marker')) {
        should_delete = (should_delete + 1) % 2;
      }
      else if (should_delete) {
        // Check if we have a marker inside it.
        if ($(node).find('.fr-marker').length > 0) {
          should_delete = _processNodeDelete($(node), should_delete);
        }
        else {
          // TD, TH or inner, then go further.
          if (['TD', 'TH'].indexOf(node.tagName) < 0 && !$(node).hasClass('fr-inner')) {
            if (!editor.opts.keepFormatOnDelete || editor.$el.find('[data-first]').length > 0) {
              $(node).remove();
            }
            else {
              _emptyInnerNodes(node);
            }
          }
          else if ($(node).hasClass('fr-inner')) {
            if ($(node).find('.fr-inner').length == 0) {
              $(node).html('<br>');
            }
            else {
              $(node).find('.fr-inner').filter(function () {
                return $(this).find('fr-inner').length == 0;
              }).html('<br>');
            }
          }
          else {
            $(node).empty();
            $(node).attr('data-del-cell', true);
          }
        }
      }
      else {
        if ($(node).find('.fr-marker').length > 0) {
          should_delete = _processNodeDelete($(node), should_delete);
        }
      }
    }

    return should_delete;
  }

  /**
   * Determine if selection is inside the editor.
   */
  function inEditor () {
    try {
      if (!editor.$wp) return false;

      var range = ranges(0);
      var container = range.commonAncestorContainer;

      while (container && !editor.node.isElement(container)) {
        container = container.parentNode;
      }

      if (editor.node.isElement(container)) return true;
      return false;
    }
    catch (ex) {
      return false;
    }
  }

  /**
   * Remove the current selection html.
   */
  function remove () {
    if (isCollapsed()) return true;

    save();

    // Get the previous sibling normalized.
    var _prevSibling = function (node) {
      var prev_node = node.previousSibling;
      while (prev_node && prev_node.nodeType == Node.TEXT_NODE && prev_node.textContent.length == 0) {
        var tmp = prev_node;
        var prev_node = prev_node.previousSibling;
        $(tmp).remove();
      }

      return prev_node;
    }

    // Get the next sibling normalized.
    var _nextSibling = function (node) {
      var next_node = node.nextSibling;
      while (next_node && next_node.nodeType == Node.TEXT_NODE && next_node.textContent.length == 0) {
        var tmp = next_node;
        var next_node = next_node.nextSibling;
        $(tmp).remove();
      }

      return next_node;
    }

    // Normalize start markers.
    var start_markers = editor.$el.find('.fr-marker[data-type="true"]');
    for (var i = 0; i < start_markers.length; i++) {
      var sm = start_markers[i];
      while (!_prevSibling(sm) && !editor.node.isBlock(sm.parentNode) && !editor.$el.is(sm.parentNode)) {
        $(sm.parentNode).before(sm);
      }
    }

    // Normalize end markers.
    var end_markers = editor.$el.find('.fr-marker[data-type="false"]');
    for (var i = 0; i < end_markers.length; i++) {
      var em = end_markers[i];
      while (!_nextSibling(em) && !editor.node.isBlock(em.parentNode) && !editor.$el.is(em.parentNode)) {
        $(em.parentNode).after(em);
      }

      // Last node is empty and has a BR in it.
      if (em.parentNode && editor.node.isBlock(em.parentNode) && editor.node.isEmpty(em.parentNode) && !editor.$el.is(em.parentNode)) {
        $(em.parentNode).after(em);
      }
    }

    // Check if selection can be deleted.
    if (_canDelete()) {
      _processNodeDelete(editor.$el, 0);

      // Look for selection marker.
      var $first_node = editor.$el.find('[data-first="true"]');
      if ($first_node.length) {
        // Remove markers.
        editor.$el.find('.fr-marker').remove();

        // Add markers in the node that we marked as the first one.
        $first_node
          .append($.FE.INVISIBLE_SPACE + $.FE.MARKERS)
          .removeAttr('data-first');

        // Remove span with data-text if there is one.
        if ($first_node.attr('data-text')) {
          $first_node.replaceWith($first_node.html());
        }
      }
      else {
        // Remove tables.
        editor.$el.find('table').filter(function () {
          var ok = $(this).find('[data-del-cell]').length > 0 && $(this).find('[data-del-cell]').length == $(this).find('td, th').length;

          return ok;
        }).remove();
        editor.$el.find('[data-del-cell]').removeAttr('data-del-cell');

        // Merge contents between markers.
        var start_markers = editor.$el.find('.fr-marker[data-type="true"]');
        for (var i = 0; i < start_markers.length; i++) {
          // Get start marker.
          var start_marker = start_markers[i];

          // Get the next node after start marker.
          var next_node = start_marker.nextSibling;

          // Get the end node.
          var end_marker = editor.$el.find('.fr-marker[data-type="false"][data-id="' + $(start_marker).data('id') + '"]').get(0);

          if (end_marker) {
            // Markers are next to other.
            if (next_node && next_node == end_marker) {
              // Do nothing.
            }
            else if (start_marker) {
              // Get the parents of the nodes.
              var start_parent = editor.node.blockParent(start_marker);
              var end_parent = editor.node.blockParent(end_marker);

              // https://github.com/froala/wysiwyg-editor/issues/1233
              var list_start = false;
              var list_end = false;
              if (start_parent && ['UL', 'OL'].indexOf(start_parent.tagName) >= 0) {
                start_parent = null;
                list_start = true;
              }
              if (end_parent && ['UL', 'OL'].indexOf(end_parent.tagName) >= 0) {
                end_parent = null;
                list_end = true;
              }

              // Move end marker next to start marker.
              $(start_marker).after(end_marker);

              // We're in the same parent. Moving marker is enough.
              if (start_parent == end_parent) {
              }

              // The block parent of the start marker is the element itself.
              else if (start_parent == null && !list_start) {
                var deep_parent = editor.node.deepestParent(start_marker);

                // There is a parent for the marker. Move the end html to it.
                if (deep_parent) {
                  $(deep_parent).after($(end_parent).html());
                  $(end_parent).remove();
                }

                // There is no parent for the marker.
                else if ($(end_parent).parentsUntil(editor.$el, 'table').length == 0) {
                  $(start_marker).next().after($(end_parent).html());
                  $(end_parent).remove();
                }
              }

              // End marker is inside element. We don't merge in table.
              else if (end_parent == null && !list_end && $(start_parent).parentsUntil(editor.$el, 'table').length == 0) {
                // Get the node that has a next sibling.
                var next_node = start_parent;
                while (!next_node.nextSibling && next_node.parentNode != editor.$el.get(0)) {
                  next_node = next_node.parentNode;
                }
                next_node = next_node.nextSibling;

                // Join HTML inside the start node.
                while (next_node && next_node.tagName != 'BR') {
                  var tmp_node = next_node.nextSibling;
                  $(start_parent).append(next_node);
                  next_node = tmp_node;
                }

                if (next_node && next_node.tagName == 'BR') {
                  $(next_node).remove();
                }
              }

              // Join end block with start block.
              else if (start_parent && end_parent && $(start_parent).parentsUntil(editor.$el, 'table').length == 0 && $(end_parent).parentsUntil(editor.$el, 'table').length == 0) {
                $(start_parent).append($(end_parent).html());
                $(end_parent).remove();
              }
            }
          }

          else {
            end_marker = $(start_marker).clone().attr('data-type', false);
            $(start_marker).after(end_marker);
          }
        }
      }
    }

    if (!editor.opts.keepFormatOnDelete) {
      editor.html.fillEmptyBlocks();
    }

    editor.html.cleanEmptyTags(true);
    editor.clean.lists();

    editor.spaces.normalize();
    restore();
  }

  function setAtStart (node) {
    if ($(node).find('.fr-marker').length > 0) return false;

    var contents = editor.node.contents(node);
    while (contents.length && editor.node.isBlock(contents[0])) {
      node = contents[0];
      contents = editor.node.contents(node);
    }

    $(node).prepend($.FE.MARKERS);
  }

  function setAtEnd (node) {
    if ($(node).find('.fr-marker').length > 0) return false;

    var contents = editor.node.contents(node);
    while (contents.length && editor.node.isBlock(contents[contents.length - 1])) {
      node = contents[contents.length - 1];
      contents = editor.node.contents(node);
    }

    $(node).append($.FE.MARKERS);
  }

  function setBefore (node) {
    var prev_node = node.previousSibling;
    while (prev_node && prev_node.nodeType == Node.TEXT_NODE && prev_node.textContent.length == 0) {
      prev_node = prev_node.previousSibling;
    }

    if (prev_node) {
      if (editor.node.isBlock(prev_node)) {
        setAtEnd(prev_node);
      }
      else if (prev_node.tagName == 'BR') {
        $(prev_node).before($.FE.MARKERS);
      }
      else {
        $(prev_node).after($.FE.MARKERS);
      }

      return true;
    }
    else {
      return false;
    }
  }

  function setAfter (node) {
    var next_node = node.nextSibling;
    while (next_node && next_node.nodeType == Node.TEXT_NODE && next_node.textContent.length == 0) {
      next_node = next_node.nextSibling;
    }

    if (next_node) {
      if (editor.node.isBlock(next_node)) {
        setAtStart(next_node);
      }
      else {
        $(next_node).before($.FE.MARKERS);
      }

      return true;
    }
    else {
      return false;
    }
  }

  return {
    text: text,
    get: get,
    ranges: ranges,
    clear: clear,
    element: element,
    endElement: endElement,
    save: save,
    restore: restore,
    isCollapsed: isCollapsed,
    isFull: isFull,
    inEditor: inEditor,
    remove: remove,
    blocks: blocks,
    info: info,
    setAtEnd: setAtEnd,
    setAtStart: setAtStart,
    setBefore: setBefore,
    setAfter: setAfter,
    rangeElement: rangeElement
  }
};

$.FE.MODULES.spaces = function (editor) {
  function _remove (node) {
    var next = node.nextSibling || node.parentNode;

    node.parentNode.removeChild(node);

    return next;
  }

  function _next (prev, current) {
    if ((prev && prev.parentNode === current) || current.nodeName === 'PRE') {
      return current.nextSibling || current.parentNode;
    }

    return current.firstChild || current.nextSibling || current.parentNode;
  }

  // Adaptation of MIT https://github.com/lucthev/collapse-whitespace.
  function collapse (elem) {
    if (!elem.firstChild || elem.nodeName === 'PRE' || ['STYLE', 'SCRIPT'].indexOf(elem.tagName) >= 0) return;

    var prevText = null;

    var prev = null;
    var node = _next(prev, elem);

    // Go deep while current node is not elem.
    while (node !== elem && node.nodeName !== 'PRE' && ['STYLE', 'SCRIPT'].indexOf(node.tagName) < 0) {
      // Current node is text node.
      if (node.nodeType === Node.TEXT_NODE) {
        // Collapse spaces.
        var text = node.data.replace(/[ \r\n\t]+/g, ' ');

        // No previous text or previous text ends with space.
        // No previous voide.
        // Current text starts with space.
        if ((!prevText || / $/.test(prevText.data)) && text[0] === ' ') {
          // Remove starting space.
          text = text.substr(1);
        }

        // `text` might be empty at this point.
        if (!text || text.length == 0) {
          node = _remove(node);
          continue;
        }

        // Set new text.
        node.data = text;

        // Set previous text node as current node.
        prevText = node;
      }

      // Current node is element node.
      else if (node.nodeType === Node.ELEMENT_NODE) {
        // Block or BR.
        if (editor.node.isBlock(node) || editor.node.isVoid(node)) {
          // If there was a previous text collapse ending spaces;
          if (prevText && prevText.data) {
            prevText.data = prevText.data.replace(/ $/, '');
          }

          prevText = null;
        }
        else if (node.textContent.length == 0) {
          prevText = node;
        }
      }

      var nextNode = _next(prev, node);
      prev = node;
      node = nextNode;
    }

    // There is previous text.
    if (prevText && prevText.data) {
      // Remove ending.
      prevText.data = prevText.data.replace(/ $/, '')

      // If text is left empty, remove it.
      if (!prevText.data) {
        _remove(prevText);
      }
    }
  }

  function normalize (node, browser_way) {
    if (typeof node == 'undefined' || !node) node = editor.$el.get(0);
    if (typeof browser_way == 'undefined') browser_way = false;

    if (browser_way) {
      collapse(node);
    }

    // Ignore contenteditable.
    if (node.getAttribute && node.getAttribute('contenteditable') == 'false') return;

    if (node.nodeType == Node.ELEMENT_NODE && ['STYLE', 'SCRIPT', 'HEAD'].indexOf(node.tagName) < 0) {
      var contents = editor.node.contents(node);
      for (var i = contents.length - 1; i >= 0 ; i--) {
        if (contents[i].tagName != Node.ELEMENT_NODE || (contents[i].className || '').indexOf('fr-marker') < 0) {
          normalize(contents[i]);
        }
      }
    }
    else if (node.nodeType == Node.TEXT_NODE && node.textContent.length > 0) {
      var prev_node = node.previousSibling;
      var next_node = node.nextSibling;
      var txt = node.textContent;

      // Convert all non breaking to breaking spaces.
      txt = txt.replace(new RegExp($.FE.UNICODE_NBSP, 'g'), ' ');

      var new_text = '';
      for (var t = 0; t < txt.length; t++) {
        if (txt.charCodeAt(t) == 32 && (t === 0 || new_text.charCodeAt(t - 1) == 32)) {
          new_text += $.FE.UNICODE_NBSP;
        }
        else {
          new_text += txt[t];
        }
      }

      // Ending spaces should be NBSP or spaces before block tags.
      if (!node.nextSibling || editor.node.isBlock(node.nextSibling) || (node.nextSibling.nodeType == Node.ELEMENT_NODE && editor.win.getComputedStyle(node.nextSibling) && editor.win.getComputedStyle(node.nextSibling).display == 'block')) {
        new_text = new_text.replace(/ $/, $.FE.UNICODE_NBSP);
      }

      // Previous sibling is not void or block.
      if (node.previousSibling && !editor.node.isVoid(node.previousSibling) && !editor.node.isBlock(node.previousSibling)) {
        new_text = new_text.replace(/^\u00A0([^ $])/, ' $1');
      }

      // Convert middle nbsp to spaces.
      new_text = new_text.replace(/([^ \u00A0])\u00A0([^ \u00A0])/g, '$1 $2');

      if (node.textContent != new_text) {
        node.textContent = new_text;
      }
    }
  }

  return {
    normalize: normalize
  }
};

$.FE.UNICODE_NBSP = String.fromCharCode(160);

// Void Elements http://www.w3.org/html/wg/drafts/html/master/syntax.html#void-elements
$.FE.VOID_ELEMENTS = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'];

$.FE.BLOCK_TAGS = ['address', 'article', 'aside', 'audio', 'blockquote', 'canvas', 'dd', 'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'li', 'main', 'nav', 'noscript', 'ol', 'output', 'p', 'pre', 'section', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'ul', 'video'];

// Extend defaults.
$.extend($.FE.DEFAULTS, {
  htmlAllowedEmptyTags: ['textarea', 'a', 'iframe', 'object', 'video', 'style', 'script', '.fa', '.fr-emoticon'],
  htmlDoNotWrapTags: ['script', 'style'],
  htmlSimpleAmpersand: false
});

$.FE.MODULES.html = function (editor) {
  /**
   * Determine the default block tag.
   */
  function defaultTag () {
    if (editor.opts.enter == $.FE.ENTER_P) return 'p';
    if (editor.opts.enter == $.FE.ENTER_DIV) return 'div';
    if (editor.opts.enter == $.FE.ENTER_BR) return null;
  }

  /**
   * Get the empty blocs.
   */
  function emptyBlocks () {
    var empty_blocks = [];

    // Block tag elements.
    var els = editor.$el.get(0).querySelectorAll(blockTagsQuery());

    // Check if there are empty block tags with markers.
    for (var i = 0; i < els.length; i++) {
      // There are void elements tags.
      if (els[i].querySelectorAll($.FE.VOID_ELEMENTS.join(',')).length > 0) continue;

      // There are other empty elements.
      if (els[i].querySelectorAll(editor.opts.htmlAllowedEmptyTags.join(':not(.fr-marker),') + ':not(.fr-marker)').length > 0) continue;

      // There are other block tags.
      if (els[i].querySelectorAll(blockTagsQuery()).length > 0) continue;

      // We're checking text from here on.
      var contents = editor.node.contents(els[i]);

      var found = false;
      for (var j = 0; j < contents.length; j++) {
        if (contents[j].nodeType == Node.COMMENT_NODE) continue;

        // Text node that is not empty.
        if (contents[j].textContent && contents[j].textContent.replace(/\u200B/g, '').replace(/\n/g, '').length > 0) {
          found = true;
          break;
        }
      }

      // Make sure we don't add TABLE and TD at the same time for instance.
      if (!found) empty_blocks.push(els[i]);
    }

    return empty_blocks;
  }

  /**
   * Create jQuery query for empty block tags.
   */
  function emptyBlockTagsQuery () {
    return $.FE.BLOCK_TAGS.join(':empty, ') + ':empty';
  }

  /**
   * Create jQuery query for selecting block tags.
   */
  function blockTagsQuery () {
    return $.FE.BLOCK_TAGS.join(', ');
  }

  /**
   * Remove empty elements that are not VOID elements.
   */
  function cleanEmptyTags (remove_blocks) {
    var els = $.merge([], $.FE.VOID_ELEMENTS);
    els = $.merge(els, editor.opts.htmlAllowedEmptyTags);

    if (typeof remove_blocks == 'undefined') els = $.merge(els, $.FE.BLOCK_TAGS);

    var elms;
    var ok;
    do {
      ok = false;
      elms = editor.$el.get(0).querySelectorAll('*:empty:not(' + els.join('):not(') + '):not(.fr-marker)');

      // Remove those elements that have no attributes.
      for (var i = 0; i < elms.length; i++) {
        if (elms[i].attributes.length === 0 || typeof elms[i].getAttribute('href') !== 'undefined') {
          $(elms[i]).remove();
          ok = true;
        }
      }

      elms = editor.$el.get(0).querySelectorAll('*:empty:not(' + els.join('):not(') + '):not(.fr-marker)');
    } while (elms.length && ok);

  }

  /**
   * Wrap the content inside the element passed as argument.
   */
  function _wrapElement($el, temp) {
    var default_tag = defaultTag();
    if (temp) default_tag = 'div class="fr-temp-div"';

    if (default_tag) {
      var contents = editor.node.contents($el.get(0));
      var $anchor = null;

      // Loop through contents.
      for (var i = 0; i < contents.length; i++) {

        // Current node.
        var node = contents[i];

        // Current node is a block node.
        if (node.nodeType == Node.ELEMENT_NODE && (editor.node.isBlock(node) || ($(node).is(editor.opts.htmlDoNotWrapTags.join(',')) && !$(node).hasClass('fr-marker')))) {
          $anchor = null;
        }

        // Other node types than element and text.
        else if (node.nodeType != Node.ELEMENT_NODE && node.nodeType != Node.TEXT_NODE) {
          $anchor = null;
        }

        // Current node is BR.
        else if (node.nodeType == Node.ELEMENT_NODE && node.tagName == 'BR') {
          // There is no anchor.
          if ($anchor == null) {
            if (temp) {
              $(node).replaceWith('<' + default_tag + ' data-empty="true"><br></div>');
            }
            else {
              $(node).replaceWith('<' + default_tag + '><br></' + default_tag + '>');
            }
          }

          // There is anchor. Just remove BR.
          else {
            $(node).remove();

            // Check if in anchor we have something else than markers.
            var cnts = editor.node.contents($anchor);
            var found = false;
            for (var j = 0; j < cnts.length; j++) {
              if (!$(cnts[j]).hasClass('fr-marker') && !(cnts[j].nodeType == Node.TEXT_NODE && cnts[j].textContent.replace(/ /g, '').length === 0)) {
                found = true;
                break;
              }
            }

            if (found === false) {
              $anchor.append('<br>');
              $anchor.data('empty', true);
            }

            $anchor = null;
          }
        }

        // Text node or other node type.
        else {
          if (node.nodeType == Node.TEXT_NODE && $(node).text().trim().length == 0) {
            $(node).remove();
          }
          else {
            if ($anchor == null) {
              $anchor = $('<' + default_tag + '>');
              $(node).before($anchor);
            }

            if (node.nodeType == Node.TEXT_NODE && $(node).text().trim().length > 0) {
              $anchor.append($(node).clone());
              $(node).remove();
            }
            else {
              $anchor.append($(node));
            }
          }
        }
      }
    }
  }

  /**
   * Wrap the direct content inside the default block tag.
   */
  function _wrap (temp, tables, blockquote, inner) {
    if (!editor.$wp) return false;

    if (typeof temp == 'undefined') temp = false;
    if (typeof tables == 'undefined') tables = false;
    if (typeof blockquote == 'undefined') blockquote = false;
    if (typeof inner == 'undefined') inner = false;

    // Wrap element.
    _wrapElement(editor.$el, temp);

    if (inner) {
      editor.$el.find('.fr-inner').each (function () {
        _wrapElement($(this), temp);
      })
    }

    // Wrap table contents.
    if (tables) {
      editor.$el.find('td, th').each (function () {
        _wrapElement($(this), temp);
      })
    }

    // Wrap table contents.
    if (blockquote) {
      editor.$el.find('blockquote').each (function () {
        _wrapElement($(this), temp);
      })
    }
  }

  /**
   * Unwrap temporary divs.
   */
  function unwrap () {
    editor.$el.find('div.fr-temp-div').each(function () {
      if ($(this).data('empty') || this.parentNode.tagName == 'LI') {
        $(this).replaceWith($(this).html());
      }
      else {
        $(this).replaceWith($(this).html() + '<br>');
      }
    });

    // Remove temp class from other blocks.
    editor.$el.find('.fr-temp-div').removeClass('fr-temp-div').filter(function () {
      return $(this).attr('class') == '';
    }).removeAttr('class');
  }

  /**
   * Add BR inside empty elements.
   */
  function fillEmptyBlocks () {
    var blocks = emptyBlocks();
    for (var i = 0; i < blocks.length; i++) {
      var block = blocks[i];
      if (block.getAttribute('contenteditable') != "false" &&
          block.querySelectorAll(editor.opts.htmlAllowedEmptyTags.join(':not(.fr-marker),') + ':not(.fr-marker)').length == 0 &&
          !editor.node.isVoid(block)) {
        if (block.tagName != 'TABLE') block.appendChild(editor.doc.createElement('br'));
      }
    }

    // Fix for https://github.com/froala/wysiwyg-editor/issues/1166#issuecomment-204549406.
    if (editor.browser.msie && editor.opts.enter == $.FE.ENTER_BR) {
      var contents = editor.node.contents(editor.$el.get(0));
      if (contents.length && contents[contents.length - 1].nodeType == Node.TEXT_NODE) {
        editor.$el.append('<br>');
      }
    }
  }

  /**
   * Get the blocks inside the editable area.
   */
  function blocks () {
    return editor.$el.find(blockTagsQuery());
  }

  /**
   * Clean the blank spaces between the block tags.
   */
  function cleanBlankSpaces (node) {
    if (typeof node == 'undefined') node = editor.$el.get(0);
    if (node && ['SCRIPT', 'STYLE', 'PRE'].indexOf(node.tagName) >= 0) return false;

    var contents = editor.node.contents(node);

    // Loop contents.
    for (var i = contents.length - 1; i >= 0; i--) {
      // Content is text and node is block.
      if (contents[i].nodeType == Node.TEXT_NODE) {
        var len = -1;

        // Remove middle spaces.
        contents[i].textContent = contents[i].textContent.replace(/(?!^)( ){2,}(?!$)/g, ' ');

        // Replace new lines with spaces.
        contents[i].textContent = contents[i].textContent.replace(/\n/g, ' ');

        // Replace begin/end spaces.
        contents[i].textContent = contents[i].textContent.replace(/^[ ]{2,}/g, ' ');
        contents[i].textContent = contents[i].textContent.replace(/[ ]{2,}$/g, ' ');

        if (editor.node.isBlock(node) || editor.node.isElement(node)) {
          // No previous siblings.
          if (!contents[i].previousSibling) {
            contents[i].textContent = contents[i].textContent.replace(/^ */,'');
          }

          // No next siblings.
          if (!contents[i].nextSibling) {
            contents[i].textContent = contents[i].textContent.replace(/ *$/,'');
          }

          if (contents[i].previousSibling && contents[i].nextSibling && contents[i].textContent == ' ') {
            if (contents[i].previousSibling && contents[i].nextSibling && editor.node.isBlock(contents[i].previousSibling) && editor.node.isBlock(contents[i].nextSibling)) {
              contents[i].textContent = '';
            }
            else {
              contents[i].textContent = "\n";
            }
          }
        }
      }
      else {
        cleanBlankSpaces(contents[i]);
      }
    }
  }

  function _isBlock (node) {
    return node && (editor.node.isBlock(node) || ['STYLE', 'SCRIPT', 'HEAD', 'BR', 'HR'].indexOf(node.tagName) >= 0 || node.nodeType == Node.COMMENT_NODE);
  }

  function doNormalize (node) {
    if (typeof node == 'undefined') node = editor.$el.get(0);

    if (node.nodeType == Node.ELEMENT_NODE && ['STYLE', 'SCRIPT', 'HEAD'].indexOf(node.tagName) < 0) {
      var contents = editor.node.contents(node);
      for (var i = contents.length - 1; i >= 0 ; i--) {
        if (!$(contents[i]).hasClass('fr-marker')) {
          var r = doNormalize(contents[i]);
          if (r == true) return true;
        }
      }
    }
    else if (node.nodeType == Node.TEXT_NODE && node.textContent.length > 0) {
      var prev_node = node.previousSibling;
      var next_node = node.nextSibling;

      if (_isBlock(prev_node) && _isBlock(next_node) && node.textContent.trim().length === 0) {
        return true;
      }
      else {
        var txt = node.textContent;
        txt = txt.replace(new RegExp($.FE.UNICODE_NBSP, 'g'), ' ');

        var new_text = ''
        for (var t = 0; t < txt.length; t++) {
          if (txt.charCodeAt(t) == 32 && (t === 0 || new_text.charCodeAt(t - 1) == 32)) {
            new_text += $.FE.UNICODE_NBSP;
          }
          else {
            new_text += txt[t];
          }
        }

        if (!node.nextSibling) new_text = new_text.replace(/ $/, $.FE.UNICODE_NBSP);
        if (node.previousSibling && !editor.node.isVoid(node.previousSibling)) new_text = new_text.replace(/^\u00A0([^ $])/, ' $1');

        new_text = new_text.replace(/([^ \u00A0])\u00A0([^ \u00A0])/g, '$1 $2');

        if (node.textContent != new_text) {
          return true;
        }
      }
    }

    return false;
  }

  /**
   * Extract a specific match for a RegEx.
   */
  function _extractMatch (html, re, id) {
    var reg_exp = new RegExp(re, 'gi');
    var matches = reg_exp.exec(html);

    if (matches) {
      return matches[id];
    }

    return null;
  }

  /**
   * Create new doctype.
   */
  function _newDoctype (string, doc) {
    var matches = string.match(/<!DOCTYPE ?([^ ]*) ?([^ ]*) ?"?([^"]*)"? ?"?([^"]*)"?>/i);
    if (matches) {
      return doc.implementation.createDocumentType(
        matches[1],
        matches[3],
        matches[4]
      )
    }
    else {
      return doc.implementation.createDocumentType('html');
    }
  }

  /**
   * Get string doctype of a document.
   */
  function getDoctype (doc) {
    var node = doc.doctype;
    var doctype = '<!DOCTYPE html>';
    if (node) {
      doctype = '<!DOCTYPE '
                + node.name
                + (node.publicId ? ' PUBLIC "' + node.publicId + '"' : '')
                + (!node.publicId && node.systemId ? ' SYSTEM' : '')
                + (node.systemId ? ' "' + node.systemId + '"' : '')
                + '>';
    }

    return doctype;
  }

  /**
   * Normalize.
   */
  function _normalize () {
    // Wrap possible text.
    _wrap();

    // Clean blank spaces.
    cleanBlankSpaces();

    // Remove empty tags.
    cleanEmptyTags();

    // Normalize spaces.
    editor.spaces.normalize(null, true);

    // Add BR tag where it is necessary.
    editor.html.fillEmptyBlocks();

    // Clean quotes.
    editor.clean.quotes();

    // Clean lists.
    editor.clean.lists();

    // Clean tables.
    editor.clean.tables();

    // Convert to HTML5.
    editor.clean.toHTML5();

    // Restore selection.
    editor.selection.restore();

    // Check if editor is empty and add placeholder.
    checkIfEmpty();

    // Refresh placeholder.
    editor.placeholder.refresh();
  }

  function checkIfEmpty () {
    if (editor.core.isEmpty()) {
      if (defaultTag() != null) {
        // There is no block tag inside the editor.
        if (editor.$el.get(0).querySelectorAll(blockTagsQuery()).length === 0 &&
             editor.$el.get(0).querySelectorAll(editor.opts.htmlDoNotWrapTags.join(':not(.fr-marker),') + ':not(.fr-marker)').length === 0) {
          if (editor.core.hasFocus()) {
            editor.$el.html('<' + defaultTag() + '>' + $.FE.MARKERS + '<br/></' + defaultTag() + '>');
            editor.selection.restore();
          }
          else {
            editor.$el.html('<' + defaultTag() + '>' + '<br/></' + defaultTag() + '>');
          }
        }
      }
      else {
        // There is nothing in the editor.
        if (editor.$el.get(0).querySelectorAll('*:not(.fr-marker):not(br)').length === 0) {
          if (editor.core.hasFocus()) {
            editor.$el.html($.FE.MARKERS + '<br/>');
            editor.selection.restore();
          }
          else {
            editor.$el.html('<br/>');
          }
        }
      }
    }
  }

  function extractNode (html, tag) {
    return _extractMatch(html, '<' + tag +'[^>]*?>([\\w\\W]*)<\/' + tag + '>', 1);
  }

  function extractNodeAttrs (html, tag) {
    var $dv = $('<div ' + (_extractMatch(html, '<' + tag + '([^>]*?)>', 1) || '') + '>');
    return editor.node.rawAttributes($dv.get(0));
  }

  function extractDoctype (html) {
    return _extractMatch(html, '<!DOCTYPE([^>]*?)>', 0) || '<!DOCTYPE html>';
  }

  /**
   * Set HTML.
   */
  function set (html) {
    var clean_html = editor.clean.html(html || '', [], [], editor.opts.fullPage);

    if (!editor.opts.fullPage) {
      editor.$el.html(clean_html);
    }
    else {
      // Get BODY data.
      var body_html = (extractNode(clean_html, 'body') || (clean_html.indexOf('<body') >= 0 ? '' : clean_html));
      var body_attrs = extractNodeAttrs(clean_html, 'body');

      // Get HEAD data.
      var head_html = extractNode(clean_html, 'head') || '<title></title>';
      var head_attrs = extractNodeAttrs(clean_html, 'head');

      // Get HTML that might be in <head> other than meta tags.
      // https://github.com/froala/wysiwyg-editor/issues/1208
      var head_bad_html = $('<div>')
                            .append(head_html)
                            .find('base, link, meta, noscript, script, style, template, title')
                            .remove().end()
                            .html().trim();

      // Filter and keep only meta tags in <head>.
      // https://html.spec.whatwg.org/multipage/dom.html#metadata-content-2
      head_html = $('<div>')
                            .append(head_html)
                            .find('base, link, meta, noscript, script, style, template, title')
                            .map(function () {
                              return this.outerHTML;
                            }).toArray().join('');

      // Get DOCTYPE.
      var doctype = extractDoctype(clean_html);

      // Get HTML attributes.
      var html_attrs = extractNodeAttrs(clean_html, 'html');

      editor.$el.html(head_bad_html + '\n' + body_html);
      editor.node.clearAttributes(editor.$el.get(0));
      editor.$el.attr(body_attrs);

      editor.$head.html(head_html);
      editor.node.clearAttributes(editor.$head.get(0));
      editor.$head.attr(head_attrs);

      editor.node.clearAttributes(editor.$html.get(0));

      editor.$html.attr(html_attrs);

      editor.iframe_document.doctype.parentNode.replaceChild(
        _newDoctype(doctype, editor.iframe_document),
        editor.iframe_document.doctype
      );
    }

    // Make sure the content is editable.
    var disabled = editor.edit.isDisabled();
    editor.edit.on();

    editor.core.injectStyle(editor.opts.iframeStyle);

    _normalize();

    if (!editor.opts.useClasses) {
      // Restore orignal attributes if present.
      editor.$el.find('[fr-original-class]').each (function () {
        this.setAttribute('class', this.getAttribute('fr-original-class'));
        this.removeAttribute('fr-original-class');
      });

      editor.$el.find('[fr-original-style]').each (function () {
        this.setAttribute('style', this.getAttribute('fr-original-style'));
        this.removeAttribute('fr-original-style');
      });
    }

    if (disabled) editor.edit.off();

    editor.events.trigger('html.set');
  }

  /**
   * Get HTML.
   */
  function get (keep_markers, keep_classes) {
    if (!editor.$wp) {
      return editor.$oel.clone()
              .removeClass('fr-view')
              .removeAttr('contenteditable')
              .get(0).outerHTML;
    }

    var html = '';

    editor.events.trigger('html.beforeGet');

    var specifity = function (selector) {
      var idRegex = /(#[^\s\+>~\.\[:]+)/g;
      var attributeRegex = /(\[[^\]]+\])/g;
      var classRegex = /(\.[^\s\+>~\.\[:]+)/g;
      var pseudoElementRegex = /(::[^\s\+>~\.\[:]+|:first-line|:first-letter|:before|:after)/gi;
      var pseudoClassWithBracketsRegex = /(:[\w-]+\([^\)]*\))/gi;
                      // A regex for other pseudo classes, which don't have brackets
                      var pseudoClassRegex = /(:[^\s\+>~\.\[:]+)/g;
                      var elementRegex = /([^\s\+>~\.\[:]+)/g;

      // Remove the negation psuedo-class (:not) but leave its argument because specificity is calculated on its argument
              (function() {
                      var regex = /:not\(([^\)]*)\)/g;
                      if (regex.test(selector)) {
                              selector = selector.replace(regex, '     $1 ');
                      }
              }());

      var s = (selector.match(idRegex) || []).length * 100 +
              (selector.match(attributeRegex) || []).length * 10 +
              (selector.match(classRegex) || []).length * 10 +
              (selector.match(pseudoClassWithBracketsRegex) || []).length * 10 +
              (selector.match(pseudoClassRegex) || []).length * 10 +
              (selector.match(pseudoElementRegex) || []).length;

      // Remove universal selector and separator characters
              selector = selector.replace(/[\*\s\+>~]/g, ' ');

              // Remove any stray dots or hashes which aren't attached to words
              // These may be present if the user is live-editing this selector
              selector = selector.replace(/[#\.]/g, ' ');

      s += (selector.match(elementRegex) || []).length;

      return s;
    }

    // Convert STYLE from CSS files to inline style.
    var updated_elms = [];
    var elms_info = {};
    var i;
    if (!editor.opts.useClasses && !keep_classes) {
      for (i = 0; i < editor.doc.styleSheets.length; i++) {
        var rules;
        var head_style = 0;
        try {
          rules = editor.doc.styleSheets[i].cssRules;
          if (editor.doc.styleSheets[i].ownerNode && editor.doc.styleSheets[i].ownerNode.nodeType == 'STYLE') {
            head_style = 1;
          }
        }
        catch (ex) {
        }

        if (rules) {
          for (var idx = 0, len = rules.length; idx < len; idx++) {
            var class_selector = editor.opts.iframe ? 'body ' : '.fr-view ';

            if (rules[idx].selectorText && rules[idx].selectorText.indexOf(class_selector) === 0) {
              if (rules[idx].style.cssText.length > 0) {
                var selector = rules[idx].selectorText.replace(class_selector, '').replace(/::/g, ':');
                var elms = editor.$el.get(0).querySelectorAll(selector);

                for (var j = 0; j < elms.length; j++) {
                  // Save original style.
                  if (!elms[j].getAttribute('fr-original-style') && elms[j].getAttribute('style')) {
                    elms[j].setAttribute('fr-original-style', elms[j].getAttribute('style'));
                    updated_elms.push(elms[j]);
                  }
                  else if (!elms[j].getAttribute('fr-original-style')) {
                    updated_elms.push(elms[j]);
                  }

                  if (!elms_info[elms[j]]) {
                    elms_info[elms[j]] = {};
                  }

                  // Compute specification.
                  var spec = head_style * 1000 + specifity(rules[idx].selectorText);

                  // Get CSS text of the rule.
                  var css_text = rules[idx].style.cssText.split(';');

                  // Get each rule.
                  for (var k = 0; k < css_text.length; k++) {
                    // Rule.
                    var rule = css_text[k].trim().split(':')[0];
                    if (!elms_info[elms[j]][rule]) {
                      elms_info[elms[j]][rule] = 0;

                      if ((elms[j].getAttribute('fr-original-style') || '').indexOf(rule + ':') >= 0) {
                        elms_info[elms[j]][rule] = 10000;
                      }
                    }

                    // Current spec is higher than the existing one.
                    if (spec >= elms_info[elms[j]][rule]) {
                      elms_info[elms[j]][rule] = spec;

                      if (css_text[k].trim().length) {
                        elms[j].style[rule.trim()] = css_text[k].trim().split(':')[1].trim();
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }

      // Save original class.
      for (i = 0; i < updated_elms.length; i++) {
        if (updated_elms[i].getAttribute('class')) {
          updated_elms[i].setAttribute('fr-original-class', updated_elms[i].getAttribute('class'));
          updated_elms[i].removeAttribute('class');
        }
      }
    }

    // If editor is not empty.
    if (!editor.core.isEmpty()) {
      if (typeof keep_markers == 'undefined') keep_markers = false;

      if (!editor.opts.fullPage) {
        html = editor.$el.html();
      }
      else {
        html = getDoctype(editor.iframe_document);
        html += '<html' + editor.node.attributes(editor.$html.get(0)) + '>' + editor.$html.html() + '</html>';
      }
    }
    else if (editor.opts.fullPage) {
      html = getDoctype(editor.iframe_document);
      html += '<html' + editor.node.attributes(editor.$html.get(0)) + '>' + editor.$html.find('head').get(0).outerHTML + '<body></body></html>';
    }

    // Remove unwanted attributes.
    if (!editor.opts.useClasses && !keep_classes) {
      for (i = 0; i < updated_elms.length; i++) {
        if (updated_elms[i].getAttribute('fr-original-class')) {
          updated_elms[i].setAttribute('class', updated_elms[i].getAttribute('fr-original-class'));
          updated_elms[i].removeAttribute('fr-original-class');
        }

        if (updated_elms[i].getAttribute('fr-original-style')) {
          updated_elms[i].setAttribute('style', updated_elms[i].getAttribute('fr-original-style'));
          updated_elms[i].removeAttribute('fr-original-style');
        }
        else {
          updated_elms[i].removeAttribute('style');
        }
      }
    }

    // Clean helpers.
    if (editor.opts.fullPage) {
      html = html.replace(/<style data-fr-style="true">(?:[\w\W]*?)<\/style>/g, '');
      html = html.replace(/<link(?:[\w\W]*?)data-fr-style="true"(?:[\w\W]*?)>/g, '');
      html = html.replace(/<style(?:[\w\W]*?)class="firebugResetStyles"(?:[\w\W]*?)>(?:[\w\W]*?)<\/style>/g, '');
      html = html.replace(/<body((?:[\w\W]*?)) spellcheck="true"((?:[\w\W]*?))>((?:[\w\W]*?))<\/body>/g, '<body$1$2>$3</body>');
      html = html.replace(/<body((?:[\w\W]*?)) contenteditable="(true|false)"((?:[\w\W]*?))>((?:[\w\W]*?))<\/body>/g, '<body$1$3>$4</body>');

      html = html.replace(/<body((?:[\w\W]*?)) dir="([\w]*)"((?:[\w\W]*?))>((?:[\w\W]*?))<\/body>/g, '<body$1$3>$4</body>');
      html = html.replace(/<body((?:[\w\W]*?))class="([\w\W]*?)(fr-rtl|fr-ltr)([\w\W]*?)"((?:[\w\W]*?))>((?:[\w\W]*?))<\/body>/g, '<body$1class="$2$4"$5>$6</body>');
      html = html.replace(/<body((?:[\w\W]*?)) class=""((?:[\w\W]*?))>((?:[\w\W]*?))<\/body>/g, '<body$1$2>$3</body>');
    }

    // Ampersand fix.
    if (editor.opts.htmlSimpleAmpersand) {
      html = html.replace(/\&amp;/gi, '&');
    }

    editor.events.trigger('html.afterGet');

    // Remove markers.
    if (!keep_markers) {
      html = html.replace(/<span[^>]*? class\s*=\s*["']?fr-marker["']?[^>]+>\u200b<\/span>/gi, '');
    }

    html = editor.clean.invisibleSpaces(html);

    var new_html = editor.events.chainTrigger('html.get', html);
    if (typeof new_html == 'string') {
      html = new_html;
    }

    // Deal with pre.
    html = html.replace(/<pre(?:[\w\W]*?)>(?:[\w\W]*?)<\/pre>/g, function (str) {
      return str.replace(/<br>/g, '\n');
    });

    return html;
  }

  /**
   * Get selected HTML.
   */
  function getSelected () {
    var wrapSelection = function (container, node) {
      while (node && (node.nodeType == Node.TEXT_NODE || !editor.node.isBlock(node)) && !editor.node.isElement(node)) {
        if (node && node.nodeType != Node.TEXT_NODE) {
          $(container).wrapInner(editor.node.openTagString(node) + editor.node.closeTagString(node));
        }

        node = node.parentNode;
      }

      if (node && container.innerHTML == node.innerHTML) {
        container.innerHTML = node.outerHTML;
      }
    }

    var selectionParent = function () {
      var parent = null;
      var sel;

      if (editor.win.getSelection) {
        sel = editor.win.getSelection();
        if (sel && sel.rangeCount) {
          parent = sel.getRangeAt(0).commonAncestorContainer;
          if (parent.nodeType != Node.ELEMENT_NODE) {
            parent = parent.parentNode;
          }
        }
      } else if ((sel = editor.doc.selection) && sel.type != 'Control') {
        parent = sel.createRange().parentElement();
      }

      if (parent != null && ($.inArray(editor.$el.get(0), $(parent).parents()) >= 0 || parent == editor.$el.get(0))) {
        return parent;
      }
      else {
        return null;
      }
    }

    var html = '';
    if (typeof editor.win.getSelection != 'undefined') {

      // Multiple ranges hack.
      if (editor.browser.mozilla) {
        editor.selection.save();
        if (editor.$el.find('.fr-marker[data-type="false"]').length > 1) {
          editor.$el.find('.fr-marker[data-type="false"][data-id="0"]').remove();
          editor.$el.find('.fr-marker[data-type="false"]:last').attr('data-id', '0');
          editor.$el.find('.fr-marker').not('[data-id="0"]').remove();
        }
        editor.selection.restore();
      }

      var ranges = editor.selection.ranges();
      for (var i = 0; i < ranges.length; i++) {
        var container = document.createElement('div');
        container.appendChild(ranges[i].cloneContents());
        wrapSelection(container, selectionParent());

        // Fix for https://github.com/froala/wysiwyg-editor/issues/1010.
        if ($(container).find('.fr-element').length > 0) {
          container = editor.$el.get(0);
        }

        html += container.innerHTML;
      }
    }

    else if (typeof editor.doc.selection != 'undefined') {
      if (editor.doc.selection.type == 'Text') {
        html = editor.doc.selection.createRange().htmlText;
      }
    }
    return html;
  }

  function _hasBlockTags (html) {
    var $tmp = $('<div>').html(html);
    return $tmp.find(blockTagsQuery()).length > 0;
  }

  function _setCursorAtEnd (html) {
    var tmp = editor.doc.createElement('div');
    tmp.innerHTML = html;

    editor.selection.setAtEnd(tmp);

    return tmp.innerHTML;
  }

  function escapeEntities (str) {
    return str.replace(/</gi, '&lt;')
              .replace(/>/gi, '&gt;')
              .replace(/"/gi, '&quot;')
              .replace(/'/gi, '&apos;')
  }

  /**
   * Insert HTML.
   */
  function insert (dirty_html, clean, do_split) {
    // There is no selection.
    if (!editor.selection.isCollapsed()) {
      editor.selection.remove();
    }

    var clean_html;
    if (!clean) {
      clean_html = editor.clean.html(dirty_html);
    }
    else {
      clean_html = dirty_html;
    }

    clean_html = clean_html.replace(/\r|\n/g, ' ');

    if (dirty_html.indexOf('class="fr-marker"') < 0) {
      clean_html = _setCursorAtEnd(clean_html);
    }

    if (editor.core.isEmpty()) {
      editor.$el.html(clean_html);
    }
    else {
      // Insert a marker.
      var marker = editor.markers.insert();

      if (!marker) {
        editor.$el.append(clean_html);
      }
      else {
        // Check if HTML contains block tags and if so then break the current HTML.
        var deep_parent;
        if ((_hasBlockTags(clean_html) || do_split) && (deep_parent = editor.node.deepestParent(marker))) {
          var marker = editor.markers.split();
          if (!marker) return false;
          $(marker).replaceWith(clean_html);
        }
        else {
          $(marker).replaceWith(clean_html);
        }
      }
    }

    _normalize();

    editor.events.trigger('html.inserted');
  }

  /**
   * Clean those tags that have an invisible space inside.
   */
  function cleanWhiteTags (ignore_selection) {
    var current_el = null;
    if (typeof ignore_selection == 'undefined') {
      current_el = editor.selection.element();
    }

    var possible_elements;
    var removed;
    do {
      removed = false;
      possible_elements = editor.$el.get(0).querySelectorAll('*:not(.fr-marker)');
      for (var i = 0; i < possible_elements.length; i++) {
        var el = possible_elements[i];
        if (current_el == el) continue;

        var text = el.textContent;
        if (el.children.length === 0 && text.length === 1 && text.charCodeAt(0) == 8203) {
          $(el).remove();
          removed = true;
        }
      }
    } while (removed);
  }

  /**
   * Initialization.
   */
  function _init () {
    var cleanTags = function () {
      cleanWhiteTags();

      if (editor.placeholder) editor.placeholder.refresh();
    }

    editor.events.on('mouseup', cleanTags);
    editor.events.on('keydown', cleanTags);
    editor.events.on('contentChanged', checkIfEmpty);
  }

  return {
    defaultTag: defaultTag,
    emptyBlocks: emptyBlocks,
    emptyBlockTagsQuery: emptyBlockTagsQuery,
    blockTagsQuery: blockTagsQuery,
    fillEmptyBlocks: fillEmptyBlocks,
    cleanEmptyTags: cleanEmptyTags,
    cleanWhiteTags: cleanWhiteTags,
    doNormalize: doNormalize,
    cleanBlankSpaces: cleanBlankSpaces,
    blocks: blocks,
    getDoctype: getDoctype,
    set: set,
    get: get,
    getSelected: getSelected,
    insert: insert,
    wrap: _wrap,
    unwrap: unwrap,
    escapeEntities: escapeEntities,
    checkIfEmpty: checkIfEmpty,
    extractNode: extractNode,
    extractNodeAttrs: extractNodeAttrs,
    extractDoctype: extractDoctype,
    _init: _init
  }
}

// Extend defaults.
$.extend($.FE.DEFAULTS, {
  height: null,
  heightMax: null,
  heightMin: null,
  width: null
});

$.FE.MODULES.size = function (editor) {
  function syncIframe () {
    if (editor.opts.height) {
      editor.$el.css('minHeight', editor.opts.height - editor.helpers.getPX(editor.$el.css('padding-top')) - editor.helpers.getPX(editor.$el.css('padding-bottom')));
    }

    editor.$iframe.height(editor.$el.outerHeight(true));
  }

  function refresh () {
    if (editor.opts.heightMin) {
      editor.$el.css('minHeight', editor.opts.heightMin);
    }
    else {
      editor.$el.css('minHeight', '');
    }

    if (editor.opts.heightMax) {
      editor.$wp.css('maxHeight', editor.opts.heightMax);
      editor.$wp.css('overflow', 'auto');
    }
    else {
      editor.$wp.css('maxHeight', '');
      editor.$wp.css('overflow', '');
    }

    // Set height.
    if (editor.opts.height) {
      editor.$wp.height(editor.opts.height);
      editor.$el.css('minHeight', editor.opts.height - editor.helpers.getPX(editor.$el.css('padding-top')) - editor.helpers.getPX(editor.$el.css('padding-bottom')));
      editor.$wp.css('overflow', 'auto');
    }
    else {
      editor.$wp.css('height', '');
      if (!editor.opts.heightMin) editor.$el.css('minHeight', '');
      if (!editor.opts.heightMax) editor.$wp.css('overflow', '');
    }

    if (editor.opts.width) editor.$box.width(editor.opts.width);
  }

  function _init () {
    if (!editor.$wp) return false;

    refresh();

    // Sync iframe height.
    if (editor.opts.iframe) {
      editor.events.on('keyup', syncIframe);
      editor.events.on('commands.after', syncIframe);
      editor.events.on('html.set', syncIframe);
      editor.events.on('init', syncIframe);
      editor.events.on('initialized', syncIframe);
    }
  }

  return {
    _init: _init,
    syncIframe: syncIframe,
    refresh: refresh
  }
};

// Extend defaults.
$.extend($.FE.DEFAULTS, {
  language: null
});

$.FE.LANGUAGE = {};

$.FE.MODULES.language = function (editor) {
  var lang;

  /**
   * Translate.
   */
  function translate (str) {
    if (lang && lang.translation[str]) {
      return lang.translation[str];
    }
    else {
      return str;
    }
  }

  /* Initialize */
  function _init () {
    // Load lang.
    if ($.FE.LANGUAGE) {
      lang = $.FE.LANGUAGE[editor.opts.language];
    }

    // Set direction.
    if (lang && lang.direction) {
      editor.opts.direction = lang.direction;
    }
  }

  return {
    _init: _init,
    translate: translate
  }
};

// Extend defaults.
$.extend($.FE.DEFAULTS, {
  placeholderText: 'Type something'
});

$.FE.MODULES.placeholder = function (editor) {
  /* Show placeholder. */
  function show () {
    if (!editor.$placeholder) _add();

    // Determine the placeholder position based on the first element inside editor.
    var margin_top = 0;
    var margin_left = 0;
    var padding_top = 0;
    var padding_left = 0;
    var contents = editor.node.contents(editor.$el.get(0));
    if (contents.length && contents[0].nodeType == Node.ELEMENT_NODE) {
      var $first_node = $(contents[0]);
      if (!editor.opts.toolbarInline) {
        margin_top = editor.helpers.getPX($first_node.css('margin-top'));
        padding_top = editor.helpers.getPX($first_node.css('padding-top'));
        margin_left = editor.helpers.getPX($first_node.css('margin-left'));
        padding_left = editor.helpers.getPX($first_node.css('padding-left'));
      }

      editor.$placeholder.css('font-size', $first_node.css('font-size'));
      editor.$placeholder.css('line-height', $first_node.css('line-height'));
    }
    else {
      editor.$placeholder.css('font-size', editor.$el.css('font-size'));
      editor.$placeholder.css('line-height', editor.$el.css('line-height'));
    }

    editor.$wp.addClass('show-placeholder');
    editor.$placeholder
      .css({
          marginTop: Math.max(editor.helpers.getPX(editor.$el.css('margin-top')), margin_top),
          paddingTop: Math.max(editor.helpers.getPX(editor.$el.css('padding-top')), padding_top),
          paddingLeft: Math.max(editor.helpers.getPX(editor.$el.css('padding-left')), padding_left),
          marginLeft: Math.max(editor.helpers.getPX(editor.$el.css('margin-left')), margin_left)
        })
      .text(editor.language.translate(editor.opts.placeholderText || editor.$oel.attr('placeholder') || ''))
      .css('height', editor.$el.height() - Math.max(editor.helpers.getPX(editor.$el.css('margin-top')), margin_top) - Math.max(editor.helpers.getPX(editor.$el.css('padding-top')), padding_top));

    editor.$placeholder.html(editor.$placeholder.text().replace(/\n/g, '<br>'));
  }

  /* Hide placeholder. */
  function hide () {
    editor.$wp.removeClass('show-placeholder');
  }

  /* Check if placeholder is visible */
  function isVisible () {
    return !editor.$wp ? true : editor.$wp.hasClass('show-placeholder');
  }

  /* Refresh placeholder. */
  function refresh () {
    if (!editor.$wp) return false;

    if (editor.core.isEmpty()) {
      show();
    }
    else {
      hide();
    }
  }

  function _add () {
    editor.$placeholder = $('<span class="fr-placeholder"></span>');
    editor.$wp.append(editor.$placeholder);
  }

  /* Initialize. */
  function _init () {
    if (!editor.$wp) return false;

    editor.events.on('init input keydown keyup contentChanged initialized', refresh);
  }

  return {
    _init: _init,
    show: show,
    hide: hide,
    refresh: refresh,
    isVisible: isVisible
  }
};

$.FE.MODULES.edit = function (editor) {
  /**
   * Disable editing design.
   */
  function disableDesign () {
    if (editor.browser.mozilla) {
      try {
        editor.doc.execCommand('enableObjectResizing', false, 'false');
        editor.doc.execCommand('enableInlineTableEditing', false, 'false');
      }
      catch (ex) {

      }
    }

    if (editor.browser.msie) {
      try {
        editor.doc.body.addEventListener('mscontrolselect', function (e) {
          e.preventDefault();
          return false;
        });
      }
      catch (ex) {

      }
    }
  }

  var disabled = false;

  /**
   * Add contneteditable attribute.
   */
  function on () {
    if (editor.$wp) {
      editor.$el.attr('contenteditable', true);
      editor.$el.removeClass('fr-disabled');
      if (editor.$tb) editor.$tb.removeClass('fr-disabled');
      disableDesign();
    }
    else if (editor.$el.is('a')) {
      editor.$el.attr('contenteditable', true);
    }

    disabled = false;
  }

  /**
   * Remove contenteditable attribute.
   */
  function off () {
    if (editor.$wp) {
      editor.$el.attr('contenteditable', false);
      editor.$el.addClass('fr-disabled');
      if (editor.$tb) editor.$tb.addClass('fr-disabled');
    }
    else if (editor.$el.is('a')) {
      editor.$el.attr('contenteditable', false);
    }

    disabled = true;
  }

  function isDisabled () {
    return disabled;
  }

  return {
    on: on,
    off: off,
    disableDesign: disableDesign,
    isDisabled: isDisabled
  }
};

// Extend defaults.
$.extend($.FE.DEFAULTS, {
  editorClass: null,
  typingTimer: 500,
  iframe: false,
  requestWithCORS: true,
  requestHeaders: {},
  useClasses: true,
  spellcheck: true,
  iframeStyle: 'html{margin: 0px;}body{padding:10px;background:transparent;color:#000000;position:relative;z-index: 2;-webkit-user-select:auto;margin:0px;overflow:hidden;min-height:20px;}body:after{content:"";display:block;clear:both;}',
  iframeStyleFiles: [],
  direction: 'auto',
  zIndex: 1,
  disableRightClick: false,
  scrollableContainer: 'body',
  keepFormatOnDelete: false,
  theme: null
})

$.FE.MODULES.core = function(editor) {
  function injectStyle(style) {
    if (editor.opts.iframe) {
      editor.$head.find('style[data-fr-style], link[data-fr-style]').remove();
      editor.$head.append('<style data-fr-style="true">' + style + '</style>');

      for (var i = 0; i < editor.opts.iframeStyleFiles.length; i++) {
        editor.$head.append('<link data-fr-style="true" rel="stylesheet" href="' + editor.opts.iframeStyleFiles[i] + '">');
      }
    }
  }

  function _initElementStyle() {
    if (!editor.opts.iframe) {
      editor.$el.addClass('fr-element fr-view');
    }
  }

  /**
   * Init the editor style.
   */

  function _initStyle() {
    editor.$box.addClass('fr-box' + (editor.opts.editorClass ? ' ' + editor.opts.editorClass : ''));
    editor.$wp.addClass('fr-wrapper');

    _initElementStyle();

    if (editor.opts.iframe) {
      editor.$iframe.addClass('fr-iframe');
      editor.$html.find('body').addClass('fr-view');

      for (var i = 0; i < editor.o_doc.styleSheets.length; i++) {
        var rules;
        try {
          rules = editor.o_doc.styleSheets[i].cssRules;
        }
        catch (ex) {

        }

        if (rules) {
          for (var idx = 0, len = rules.length; idx < len; idx++) {
            if (rules[idx].selectorText && (rules[idx].selectorText.indexOf('.fr-view') === 0 || rules[idx].selectorText.indexOf('.fr-element') === 0)) {
              if (rules[idx].style.cssText.length > 0) {
                if (rules[idx].selectorText.indexOf('.fr-view') === 0) {
                  editor.opts.iframeStyle += rules[idx].selectorText.replace(/\.fr-view/g, 'body') + '{' + rules[idx].style.cssText + '}';
                }
                else {
                  editor.opts.iframeStyle += rules[idx].selectorText.replace(/\.fr-element/g, 'body') + '{' + rules[idx].style.cssText + '}';
                }
              }
            }
          }
        }
      }
    }

    if (editor.opts.direction != 'auto') {
      editor.$box.removeClass('fr-ltr fr-rtl').addClass('fr-' + editor.opts.direction);
    }
    editor.$el.attr('dir', editor.opts.direction);
    editor.$wp.attr('dir', editor.opts.direction);

    if (editor.opts.zIndex > 1) {
      editor.$box.css('z-index', editor.opts.zIndex);
    }

    if (editor.opts.theme) {
      editor.$box.addClass(editor.opts.theme + '-theme');
    }
  }

  /**
   * Determine if the editor is empty.
   */

  function isEmpty() {
    return editor.node.isEmpty(editor.$el.get(0));
  }

  /**
   * Check if the browser allows drag and init it.
   */

  function _initDrag() {
    // Drag and drop support.
    editor.drag_support = {
      filereader: typeof FileReader != 'undefined',
      formdata: !! editor.win.FormData,
      progress: 'upload' in new XMLHttpRequest()
    };
  }

  /**
   * Return an XHR object.
   */

  function getXHR(url, method) {
    var xhr = new XMLHttpRequest();

    // Make it async.
    xhr.open(method, url, true);

    // Set with credentials.
    if (editor.opts.requestWithCORS) {
      xhr.withCredentials = true;
    }

    // Set headers.
    for (var header in editor.opts.requestHeaders) {
      if (editor.opts.requestHeaders.hasOwnProperty(header)) {
        xhr.setRequestHeader(header, editor.opts.requestHeaders[header]);
      }
    }

    return xhr;
  }

  function _destroy (html) {
    if (editor.$oel.get(0).tagName == 'TEXTAREA') {
      editor.$oel.val(html);
    }

    if (editor.$wp) {
      if (editor.$oel.get(0).tagName == 'TEXTAREA') {
        editor.$el.html('');
        editor.$wp.html('');
        editor.$box.replaceWith(editor.$oel);
        editor.$oel.show();
      } else {
        editor.$wp.replaceWith(html);
        editor.$el.html('');
        editor.$box.removeClass('fr-view fr-ltr fr-box ' + (editor.opts.editorClass || ''));

        if (editor.opts.theme) {
          editor.$box.addClass(editor.opts.theme + '-theme');
        }
      }
    }

    this.$wp = null;
    this.$el = null;
    this.$box = null;
  }

  function hasFocus() {
    if (editor.browser.mozilla && editor.helpers.isMobile()) return editor.selection.inEditor();
    return editor.node.hasFocus(editor.$el.get(0)) || editor.$el.find('*:focus').length > 0;
  }

  function sameInstance ($obj) {
    if (!$obj) return false;

    var inst = $obj.data('instance');

    return (inst ? inst.id == editor.id : false);
  }

  /**
   * Tear up.
   */

  function _init () {
    $.FE.INSTANCES.push(editor);

    _initDrag();

    // Call initialization methods.
    if (editor.$wp) {
      _initStyle();
      editor.html.set(editor._original_html);

      // Set spellcheck.
      editor.$el.attr('spellcheck', editor.opts.spellcheck);

      // Disable autocomplete.
      if (editor.helpers.isMobile()) {
        editor.$el.attr('autocomplete', editor.opts.spellcheck ? 'on' : 'off');
        editor.$el.attr('autocorrect', editor.opts.spellcheck ? 'on' : 'off');
        editor.$el.attr('autocapitalize', editor.opts.spellcheck ? 'on' : 'off');
      }

      // Disable right click.
      if (editor.opts.disableRightClick) {
        editor.events.$on(editor.$el, 'contextmenu', function(e) {
          if (e.button == 2) {
            return false;
          }
        });
      }

      try {
        editor.doc.execCommand('styleWithCSS', false, false);
      } catch (ex) {

      }
    }

    // Do not allow drop inside the editor.
    editor.events.on('drop', function (e) {
      e.preventDefault();
      e.stopPropagation();
    });

    if (editor.$oel.get(0).tagName == 'TEXTAREA') {
      // Sync on contentChanged.
      editor.events.on('contentChanged', function() {
        editor.$oel.val(editor.html.get());
        // Donald modified: Add .change()
        editor.$oel.change();
      });

      // Set HTML on form submit.
      editor.events.on('form.submit', function() {
        editor.$oel.val(editor.html.get());
        // Donald modified: Add .change()
        editor.$oel.change();
      });

      editor.events.on('form.reset', function () {
        editor.html.set(editor._original_html);
        // Donald modified: Add .change()
        editor.$oel.change();
      })

      editor.$oel.val(editor.html.get());
    }

    // iOS focus fix.
    if (editor.helpers.isIOS()) {
      editor.events.$on(editor.$doc, 'selectionchange', function () {
        if (!editor.$doc.get(0).hasFocus()) {
          editor.$win.get(0).focus();
        }
      });
    }

    editor.events.trigger('init');
  }

  return {
    _init: _init,
    destroy: _destroy,
    isEmpty: isEmpty,
    getXHR: getXHR,
    injectStyle: injectStyle,
    hasFocus: hasFocus,
    sameInstance: sameInstance
  }
}

'use strict';

$.FE.MODULES.format = function (editor) {
  /**
   * Create open tag string.
   */
  function _openTag (tag, attrs) {
    var str = '<' + tag;

    for (var key in attrs) {
      if (attrs.hasOwnProperty(key)) {
        str += ' ' + key + '="' + attrs[key] + '"';
      }
    }

    str += '>';

    return str;
  }

  /**
   * Create close tag string.
   */
  function _closeTag (tag) {
    return '</' + tag + '>';
  }

  /**
   * Create query for the current format.
   */
  function _query (tag, attrs) {
    var selector = tag;

    for (var key in attrs) {
      if (attrs.hasOwnProperty(key)) {
        if (key == 'id') tag += '#' + attrs[key];
        else if (key == 'class') tag += '.' + attrs[key];
        else tag += '[' + key + '="' + attrs[key] + '"]';
      }
    }

    return selector;
  }

  /**
   * Test matching element.
   */
  function _matches (el, selector) {
    if (!el || el.nodeType != Node.ELEMENT_NODE) return false;

    return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector);
  }

  /**
   * Apply format to the current node till we find a marker.
   */
  function _processNodeFormat (start_node, tag, attrs) {
    // No start node.
    if (!start_node) return;

    // If we are in a block process starting with the first child.
    if (editor.node.isBlock(start_node)) {
      _processNodeFormat(start_node.firstChild, tag, attrs);
      return false;
    }

    // Create new element.
    var $span = $(_openTag(tag, attrs)).insertBefore(start_node);

    // Start with the next sibling of the current node.
    var node = start_node;

    // Search while there is a next node.
    // Next node is not marker.
    // Next node does not contain marker.
    while (node && !$(node).is('.fr-marker') && $(node).find('.fr-marker').length == 0) {
      var tmp = node;
      node = node.nextSibling;
      $span.append(tmp);
    }

    // If there is no node left at the right look at parent siblings.
    if (!node) {
      var p_node = $span.get(0).parentNode;
      while (p_node && !p_node.nextSibling && !editor.node.isElement(p_node)) {
        p_node = p_node.parentNode;
      }

      if (p_node) {
        var sibling = p_node.nextSibling;
        if (sibling) {
          // Parent sibling is block then look next.
          if (!editor.node.isBlock(sibling)) {
            _processNodeFormat(sibling, tag, attrs);
          }
          else {
            _processNodeFormat(sibling.firstChild, tag, attrs);
          }
        }
      }
    }
    // Start processing child nodes if there is a marker.
    else if ($(node).find('.fr-marker').length) {
      _processNodeFormat(node.firstChild, tag, attrs);
    }

    if ($span.is(':empty')) {
      $span.remove();
    }
  }

  /**
   * Apply tag format.
   */
  function apply (tag, attrs) {
    if (typeof attrs == 'undefined') attrs = {};
    if (attrs.style) {
      delete attrs.style;
    }

    // Selection is collapsed.
    if (editor.selection.isCollapsed()) {
      editor.markers.insert();
      var $marker = editor.$el.find('.fr-marker');
      $marker.replaceWith(_openTag(tag, attrs) + $.FE.INVISIBLE_SPACE + $.FE.MARKERS + _closeTag(tag));
      editor.selection.restore();
    }

    // Selection is not collapsed.
    else {
      editor.selection.save();

      // Check if selection can be deleted.
      var start_marker = editor.$el.find('.fr-marker[data-type="true"]').get(0).nextSibling;
      _processNodeFormat(start_marker, tag, attrs);

      // Clean inner spans.
      var inner_spans;
      do {
        inner_spans = editor.$el.find(_query(tag, attrs) + ' > ' + _query(tag, attrs));
        inner_spans.each (function () {
          $(this).replaceWith(this.innerHTML);
        });
      } while (inner_spans.length);

      editor.$el.get(0).normalize();

      // Have markers inside the new tag.
      var markers = editor.$el.get(0).querySelectorAll('.fr-marker');
      for (var i = 0; i < markers.length; i++) {
        var $mk = $(markers[i]);
        if ($mk.data('type') == true) {
          if (_matches($mk.get(0).nextSibling, _query(tag, attrs))) {
            $mk.next().prepend($mk);
          }
        }
        else {
          if (_matches($mk.get(0).previousSibling, _query(tag, attrs))) {
            $mk.prev().append($mk);
          }
        }
      }

      editor.selection.restore();
    }
  }

  /**
   * Split at current node the parents with tag.
   */
  function _split ($node, tag, attrs, collapsed) {
    if (!collapsed) {
      var changed = false;
      if ($node.data('type') === true) {
        while (editor.node.isFirstSibling($node.get(0)) && !$node.parent().is(editor.$el)) {
          $node.parent().before($node);
          changed = true;
        }
      }
      else if ($node.data('type') === false) {
        while (editor.node.isLastSibling($node.get(0)) && !$node.parent().is(editor.$el)) {
          $node.parent().after($node);
          changed = true;
        }
      }

      if (changed) return true;
    }

    // Check if current node has parents which match our tag.
    if ($node.parents(tag).length || typeof tag == 'undefined') {
      var close_str = '';
      var open_str = '';
      var $p_node = $node.parent();

      // Do not split when parent is block.
      if ($p_node.is(editor.$el) || editor.node.isBlock($p_node.get(0))) return false;

      // Check undefined so that we
      while ((typeof tag == 'undefined' && !editor.node.isBlock($p_node.parent().get(0))) || (typeof tag != 'undefined' && !_matches($p_node.get(0), _query(tag, attrs)))) {
        close_str = close_str + editor.node.closeTagString($p_node.get(0));
        open_str = editor.node.openTagString($p_node.get(0)) + open_str;
        $p_node = $p_node.parent();
      }

      // Node STR.
      var node_str = $node.get(0).outerHTML;

      // Replace node with marker.
      $node.replaceWith('<span id="mark"></span>');

      // Rebuild the HTML for the node.
      var p_html = $p_node.html().replace(/<span id="mark"><\/span>/, close_str + editor.node.closeTagString($p_node.get(0)) + open_str + node_str + close_str + editor.node.openTagString($p_node.get(0)) + open_str);
      $p_node.replaceWith(editor.node.openTagString($p_node.get(0)) + p_html + editor.node.closeTagString($p_node.get(0)));

      return true;
    }

    return false;
  }

  /**
   * Process node remove.
   */
  function _processNodeRemove ($node, should_remove, tag, attrs) {
    // Get contents.
    var contents = editor.node.contents($node.get(0));

    // Loop contents.
    for (var i = 0; i < contents.length; i++) {
      var node = contents[i];

      // We found a marker => change should_remove flag.
      if ($(node).hasClass('fr-marker')) {
        should_remove = (should_remove + 1) % 2;
      }
      // We should remove.
      else if (should_remove) {
        // Check if we have a marker inside it.
        if ($(node).find('.fr-marker').length > 0) {
          should_remove = _processNodeRemove($(node), should_remove, tag, attrs);
        }
        // Remove everything starting with the most inner nodes.
        else {
          $($(node).find(tag || '*').get().reverse()).each(function() {
            if (!editor.node.isBlock(this) && !editor.node.isVoid(this)) {
              $(this).replaceWith(this.innerHTML);
            }
          });

          // Check inner nodes.
          if ((typeof tag == 'undefined' && node.nodeType == Node.ELEMENT_NODE && !editor.node.isVoid(node) && !editor.node.isBlock(node)) || _matches(node, _query(tag, attrs))) {
            $(node).replaceWith(node.innerHTML);
          }
        }
      }
      else {
        // There is a marker.
        if ($(node).find('.fr-marker').length > 0) {
          should_remove = _processNodeRemove($(node), should_remove, tag, attrs);
        }
      }
    }

    return should_remove;
  }

  /**
   * Remove tag.
   */
  function remove (tag, attrs) {
    if (typeof attrs == 'undefined') attrs = {};
    if (attrs.style) {
      delete attrs.style;
    }

    var collapsed = editor.selection.isCollapsed();
    editor.selection.save();

    // Split at start and end marker.
    var reassess = true;
    while (reassess) {
      reassess = false;
      var markers = editor.$el.find('.fr-marker');
      for (var i = 0; i < markers.length; i++) {
        if (_split($(markers[i]), tag, attrs, collapsed)) {
          reassess = true;
          break;
        }
      }
    }

    // Remove format between markers.
    _processNodeRemove(editor.$el, 0, tag, attrs);

    // Selection is collapsed => add invisible spaces.
    if (collapsed) {
      editor.$el.find('.fr-marker').before($.FE.INVISIBLE_SPACE).after($.FE.INVISIBLE_SPACE);
    }

    editor.html.cleanEmptyTags();

    editor.$el.get(0).normalize();
    editor.selection.restore();
  }

  /**
   * Toggle format.
   */
  function toggle (tag, attrs) {
    if (is(tag, attrs)) {
      remove(tag, attrs);
    }
    else {
      apply(tag, attrs);
    }
  }

  /**
   * Clean format.
   */
  function _cleanFormat (elem, prop) {
    var $elem = $(elem);
    $elem.css(prop, '');

    if ($elem.attr('style') === '') {
      $elem.replaceWith($elem.html());
    }
  }

  /**
   * Filter spans with specific property.
   */
  function _filterSpans (elem, prop) {
    return $(elem).attr('style').indexOf(prop + ':') === 0 || $(elem).attr('style').indexOf(';' + prop + ':') >= 0 || $(elem).attr('style').indexOf('; ' + prop + ':') >= 0;
  };

  /**
   * Apply inline style.
   */
  function applyStyle (prop, val) {
    // Selection is collapsed.
    if (editor.selection.isCollapsed()) {
      editor.markers.insert();
      var $marker = editor.$el.find('.fr-marker');
      var $parent = $marker.parent();

      // https://github.com/froala/wysiwyg-editor/issues/1084
      if (editor.node.openTagString($parent.get(0)) == '<span style="' + prop + ': ' + $parent.css(prop) + ';">' && editor.node.isEmpty($parent.get(0))) {
        $parent.replaceWith('<span style="' + prop + ': ' + val + ';">' + $.FE.INVISIBLE_SPACE + $.FE.MARKERS + '</span>');
      }
      else if (editor.node.isEmpty($parent.get(0)) && $parent.is('span')) {
        $marker.replaceWith($.FE.MARKERS);
        $parent.css(prop, val);
      }
      else {
        $marker.replaceWith('<span style="' + prop + ': ' + val + ';">' + $.FE.INVISIBLE_SPACE + $.FE.MARKERS + '</span>');
      }

      editor.selection.restore();
    }
    else {
      editor.selection.save();

      // When removing selection we should make sure we have selection outside of the first/last parent node.
      if (val === null) {
        var markers = editor.$el.find('.fr-marker');
        for (var i = 0; i < markers.length; i++) {
          var $marker = $(markers[i]);
          if ($marker.data('type') === true) {
            while (editor.node.isFirstSibling($marker.get(0)) && !$marker.parent().is(editor.$el)) {
              $marker.parent().before($marker);
            }
          }
          else {
            while (editor.node.isLastSibling($marker.get(0)) && !$marker.parent().is(editor.$el)) {
              $marker.parent().after($marker);
            }
          }
        }
      }

      // Check if selection can be deleted.
      var start_marker = editor.$el.find('.fr-marker[data-type="true"]').get(0).nextSibling;

      var attrs = { 'class': 'fr-unprocessed' };
      if (val) attrs.style = prop + ': ' + val + ';'
      _processNodeFormat(start_marker, 'span', attrs);

      editor.$el.find('.fr-marker + .fr-unprocessed').each(function () {
        $(this).prepend($(this).prev());
      });

      editor.$el.find('.fr-unprocessed + .fr-marker').each(function () {
        $(this).prev().append(this);
      });

      while (editor.$el.find('span.fr-unprocessed').length > 0) {
        var $span = editor.$el.find('span.fr-unprocessed:first').removeClass('fr-unprocessed');

        // Look at parent node to see if we can merge with it.
        $span.parent().get(0).normalize();
        if ($span.parent().is('span') && $span.parent().get(0).childNodes.length == 1) {
          $span.parent().css(prop, val);
          var $child = $span;
          $span = $span.parent();
          $child.replaceWith($child.html());
        }

        // Replace in reverse order to take care of the inner spans first.
        var inner_spans = $span.find('span');
        for (var i = inner_spans.length - 1; i >= 0; i--) {
          _cleanFormat(inner_spans[i], prop);
        }

        // Look at parents with the same property.
        var $outer_span = $span.parentsUntil(editor.$el, 'span[style]').filter(function() {
          return _filterSpans(this, prop);
        });

        if ($outer_span.length) {
          var c_str = '';
          var o_str = '';
          var ic_str = '';
          var io_str = '';
          var c_node = $span.get(0);

          do {
            c_node = c_node.parentNode;
            c_str = c_str + editor.node.closeTagString(c_node);
            o_str = editor.node.openTagString($(c_node).clone().addClass('fr-split').get(0)) + o_str;

            // Inner close and open.
            if ($outer_span.get(0) != c_node) {
              ic_str = ic_str + editor.node.closeTagString(c_node);
              io_str = editor.node.openTagString($(c_node).clone().addClass('fr-split').get(0)) + io_str;
            }
          } while ($outer_span.get(0) != c_node);

          // Build breaking string.
          var str = c_str + editor.node.openTagString($outer_span.clone().css(prop, val || '').get(0)) + io_str + $span.html() + ic_str + '</span>' + o_str;
          $span.replaceWith('<span id="fr-break"></span>');
          var html = $outer_span.get(0).outerHTML;

          // Replace the outer node.
          $($outer_span.get(0)).replaceWith(html.replace(/<span id="fr-break"><\/span>/g, str));
        }
      }

      while (editor.$el.find('.fr-split:empty').length > 0) {
        editor.$el.find('.fr-split:empty').remove();
      }

      editor.$el.find('.fr-split').removeClass('fr-split');

      editor.$el.find('span[style=""]').removeAttr('style');
      editor.$el.find('span[class=""]').removeAttr('class');

      editor.html.cleanEmptyTags();

      editor.$el.find('span').each(function () {
        if (!this.attributes || this.attributes.length == 0) {
          $(this).replaceWith(this.innerHTML);
        }
      });

      editor.$el.get(0).normalize();

      // Join current spans together if they are one next to each other.
      var just_spans = editor.$el.find('span[style] + span[style]');
      for (i = 0; i < just_spans.length; i++) {
        var $x = $(just_spans[i]);
        var $p = $(just_spans[i]).prev();

        if ($x.get(0).previousSibling == $p.get(0) && editor.node.openTagString($x.get(0)) == editor.node.openTagString($p.get(0))) {
          $x.prepend($p.html());
          $p.remove();
        }
      }

      editor.$el.get(0).normalize();
      editor.selection.restore();
    }
  }

  /**
   * Remove inline style.
   */
  function removeStyle (prop) {
    applyStyle(prop, null);
  }

  /**
   * Get the current state.
   */
  function is (tag, attrs) {
    if (typeof attrs == 'undefined') attrs = {};
    if (attrs.style) {
      delete attrs.style;
    }

    var range = editor.selection.ranges(0);
    var el = range.startContainer;
    if (el.nodeType == Node.ELEMENT_NODE) {
      // Search for node deeper.
      if (el.childNodes.length > 0 && el.childNodes[range.startOffset]) {
        el = el.childNodes[range.startOffset];
      }
    }

    // Check first childs.
    var f_child = el;
    while (f_child && f_child.nodeType == Node.ELEMENT_NODE && !_matches(f_child, _query(tag, attrs))) {
      f_child = f_child.firstChild;
    }

    if (f_child && f_child.nodeType == Node.ELEMENT_NODE && _matches(f_child, _query(tag, attrs))) return true;

    // Check parents.
    var p_node = el;
    if (p_node && p_node.nodeType != Node.ELEMENT_NODE) p_node = p_node.parentNode;
    while (p_node && p_node.nodeType == Node.ELEMENT_NODE && p_node != editor.$el.get(0) && !_matches(p_node, _query(tag, attrs))) {
      p_node = p_node.parentNode;
    }

    if (p_node && p_node.nodeType == Node.ELEMENT_NODE && p_node != editor.$el.get(0) && _matches(p_node, _query(tag, attrs))) return true;

    return false;
  }

  return {
    is: is,
    toggle: toggle,
    apply: apply,
    remove: remove,
    applyStyle: applyStyle,
    removeStyle: removeStyle
  }
}

$.FE.COMMANDS = {
  bold: {
    title: 'Bold',
    refresh: function ($btn) {
      $btn.toggleClass('fr-active', this.format.is('strong'));
    }
  },
  italic: {
    title: 'Italic',
    refresh: function ($btn) {
      $btn.toggleClass('fr-active', this.format.is('em'));
    }
  },
  underline: {
    title: 'Underline',
    refresh: function ($btn) {
      $btn.toggleClass('fr-active', this.format.is('u'));
    }
  },
  strikeThrough: {
    title: 'Strikethrough',
    refresh: function ($btn) {
      $btn.toggleClass('fr-active', this.format.is('s'));
    }
  },
  subscript: {
    title: 'Subscript',
    refresh: function ($btn) {
      $btn.toggleClass('fr-active', this.format.is('sub'));
    }
  },
  superscript: {
    title: 'Superscript',
    refresh: function ($btn) {
      $btn.toggleClass('fr-active', this.format.is('sup'));
    }
  },
  outdent: {
    title: 'Decrease Indent'
  },
  indent: {
    title: 'Increase Indent'
  },
  undo: {
    title: 'Undo',
    undo: false,
    forcedRefresh: true,
    disabled: true
  },
  redo: {
    title: 'Redo',
    undo: false,
    forcedRefresh: true,
    disabled: true
  },
  insertHR: {
    title: 'Insert Horizontal Line'
  },
  clearFormatting: {
    title: 'Clear Formatting'
  },
  selectAll: {
    title: 'Select All',
    undo: false
  }
};

$.FE.RegisterCommand = function (name, info) {
  $.FE.COMMANDS[name] = info;
}

$.FE.MODULES.commands = function (editor) {
  var mapping = {
    bold: function () {
      _execCommand('bold', 'strong');
    },

    subscript: function () {
      _execCommand('subscript', 'sub');
    },

    superscript: function () {
      _execCommand('superscript', 'sup');
    },

    italic: function () {
      _execCommand('italic', 'em');
    },

    strikeThrough: function () {
      _execCommand('strikeThrough', 's');
    },

    underline: function () {
      _execCommand('underline', 'u');
    },

    undo: function () {
      editor.undo.run();
    },

    redo: function () {
      editor.undo.redo();
    },

    indent: function () {
      _processIndent(1);
    },

    outdent: function () {
      _processIndent(-1);
    },

    show: function () {
      if (editor.opts.toolbarInline) {
        editor.toolbar.showInline(null, true);
      }
    },

    insertHR: function () {
      editor.selection.remove();

      var empty = '';
      if (editor.core.isEmpty()) {
        empty = '<br>';
        if (editor.html.defaultTag()) {
          empty = '<' + editor.html.defaultTag() + '>' + empty + '</' + editor.html.defaultTag() + '>';
        }
      }

      editor.html.insert('<hr id="fr-just">' + empty);

      var $hr = editor.$el.find('hr#fr-just');
      $hr.removeAttr('id');

      editor.selection.setAfter($hr.get(0)) || editor.selection.setBefore($hr.get(0));

      editor.selection.restore();
    },

    clearFormatting: function () {
      editor.format.remove();
    },

    selectAll: function () {
      editor.doc.execCommand('selectAll', false, false);
    }
  }

  /**
   * Exec command.
   */
  function exec (cmd, params) {
    // Trigger before command to see if to execute the default callback.
    if (editor.events.trigger('commands.before', $.merge([cmd], params || [])) !== false) {
      // Get the callback.
      var callback = ($.FE.COMMANDS[cmd] && $.FE.COMMANDS[cmd].callback) || mapping[cmd];

      var focus = true;
      if ($.FE.COMMANDS[cmd] && typeof $.FE.COMMANDS[cmd].focus != 'undefined') {
        focus = $.FE.COMMANDS[cmd].focus;
      }

      // Make sure we have focus.
      if (!editor.core.hasFocus() && focus && !editor.popups.areVisible()) {
        // Focus in the editor at any position.
        editor.events.focus(true);
      }

      // Callback.
      // Save undo step.
      if ($.FE.COMMANDS[cmd] && $.FE.COMMANDS[cmd].undo !== false) {
        editor.undo.saveStep();
      }

      if (callback) {
        callback.apply(editor, $.merge([cmd], params || []));
      }

      // Trigger after command.
      editor.events.trigger('commands.after', $.merge([cmd], params || []));

      // Save undo step again.
      if ($.FE.COMMANDS[cmd] && $.FE.COMMANDS[cmd].undo !== false) editor.undo.saveStep();
    }
  }

  /**
   * Exex default.
   */
  function _execCommand(cmd, tag) {
    editor.format.toggle(tag);
  }

  function _processIndent(indent) {
    editor.selection.save();
    editor.html.wrap(true, true, true, true);
    editor.selection.restore();

    var blocks = editor.selection.blocks();

    for (var i = 0; i < blocks.length; i++) {
      if (blocks[i].tagName != 'LI' && blocks[i].parentNode.tagName != 'LI') {
        var $block = $(blocks[i]);

        var prop = (editor.opts.direction == 'rtl' || $block.css('direction') == 'rtl') ? 'margin-right' : 'margin-left';

        var margin_left = editor.helpers.getPX($block.css(prop));

        $block.css(prop, Math.max(margin_left + indent * 20, 0) || '');
        $block.removeClass('fr-temp-div');
      }
    }

    editor.selection.save();
    editor.html.unwrap();
    editor.selection.restore();
  }

  function callExec (k) {
    return function () {
      exec(k);
    }
  }

  var resp = {};
  for (var k in mapping) {
    if (mapping.hasOwnProperty(k)) {
      resp[k] = callExec(k);
    }
  }

  function _init () {
    // Prevent typing in HR.
    editor.events.on('keydown', function (e) {
      var el = editor.selection.element();
      if (el && el.tagName == 'HR') {
        e.preventDefault();
        return false;
      }
    });

    // Do not allow mousedown on HR.
    editor.events.on('mousedown', function (e) {
      if (e.target && e.target.tagName == 'HR') {
        e.preventDefault();
        e.stopPropagation();
        return false;
      }
    });

    // If somehow focus gets in HR remove it.
    editor.events.on('mouseup', function (e) {
      var s_el = editor.selection.element();
      var e_el = editor.selection.endElement();

      if (s_el == e_el && s_el && s_el.tagName == 'HR') {
        editor.selection.clear();
      }
    })
  }

  return $.extend(resp, {
    exec: exec,
    _init: _init
  });
};

$.FE.MODULES.cursorLists = function (editor) {
  /**
   * Find the first li parent.
   */
  function _firstParentLI (node) {
    var p_node = node;
    while (p_node.tagName != 'LI') {
      p_node = p_node.parentNode;
    }

    return p_node;
  }

  /**
   * Find the first list parent.
   */
  function _firstParentList (node) {
    var p_node = node;
    while (!editor.node.isList(p_node)) {
      p_node = p_node.parentNode;
    }

    return p_node;
  }

  /**
   * Do enter at the beginning of a list item.
   */
  function _startEnter (marker) {
    var li = _firstParentLI(marker);

    // Get previous and next siblings.
    var next_li = li.nextSibling;
    var prev_li = li.previousSibling;
    var default_tag = editor.html.defaultTag();

    var ul;

    // We are in a list item at the middle of the list or an list item that is not empty.
    if (editor.node.isEmpty(li, true) && next_li) {
      var o_str = '';
      var c_str = ''
      var p_node = marker.parentNode;

      // Create open / close string.
      while (!editor.node.isList(p_node) && p_node.parentNode && p_node.parentNode.tagName !== 'LI') {
        o_str = editor.node.openTagString(p_node) + o_str;
        c_str = c_str + editor.node.closeTagString(p_node);
        p_node = p_node.parentNode;
      }

      o_str = editor.node.openTagString(p_node) + o_str;
      c_str = c_str + editor.node.closeTagString(p_node);

      var str = ''
      if (p_node.parentNode && p_node.parentNode.tagName == 'LI') {
        str = c_str + '<li>' + $.FE.MARKERS + '<br>' + o_str;
      }
      else {
        if (default_tag) {
          str = c_str + '<' + default_tag + '>' + $.FE.MARKERS + '<br>' + '</' + default_tag + '>' + o_str;
        }
        else {
          str = c_str + $.FE.MARKERS + '<br>' + o_str;
        }
      }

      $(li).html('<span id="fr-break"></span>');

      while (['UL', 'OL'].indexOf(p_node.tagName) < 0 || (p_node.parentNode && p_node.parentNode.tagName === 'LI')) {
        p_node = p_node.parentNode;
      }
      var html = editor.node.openTagString(p_node) + $(p_node).html() + editor.node.closeTagString(p_node);
      html = html.replace(/<span id="fr-break"><\/span>/g, str);

      $(p_node).replaceWith(html);

      editor.$el.find('li:empty').remove();
    }
    else if ((prev_li && next_li) || !editor.node.isEmpty(li, true)) {
      $(li).before('<li><br></li>');
      $(marker).remove();
    }

    // There is no previous list item so transform the current list item to an empty line.
    else if (!prev_li) {
      ul = _firstParentList(li);

      // We are in a nested list so add a new li before it.
      if (ul.parentNode && ul.parentNode.tagName == 'LI') {
        $(ul.parentNode).before('<li>' + $.FE.MARKERS + '<br></li>');
      }

      // We are in a normal list. Add a new line before.
      else {
        if (default_tag) {
          $(ul).before('<' + default_tag + '>' + $.FE.MARKERS + '<br></' + default_tag + '>');
        }
        else {
          $(ul).before($.FE.MARKERS  + '<br>');
        }
      }

      // Remove the current li.
      $(li).remove();
    }

    // There is no next_li item so transform the current list item to an empty line.
    else {
      ul = _firstParentList(li);

      // We are in a nested lists so add a new li after it.
      if (ul.parentNode && ul.parentNode.tagName == 'LI') {
        $(ul.parentNode).after('<li>' + $.FE.MARKERS + '<br></li>');
      }

      // We are in a normal list. Add a new line after.
      else {
        if (default_tag) {
          $(ul).after('<' + default_tag + '>' + $.FE.MARKERS + '<br></' + default_tag + '>');
        }
        else {
          $(ul).after($.FE.MARKERS  + '<br>');
        }
      }

      // Remove the current li.
      $(li).remove();
    }
  }

  /**
   * Enter at the middle of a list.
   */
  function _middleEnter (marker) {
    var li = _firstParentLI(marker);

    // Build the closing / opening list item string.
    var str = '';
    var node = marker;
    var o_str = '';
    var c_str = '';
    while (node != li) {
      node = node.parentNode;

      var cls = (node.tagName == 'A' && editor.cursor.isAtEnd(marker, node)) ? 'fr-to-remove' : '';

      o_str = editor.node.openTagString($(node).clone().addClass(cls).get(0)) + o_str;
      c_str = editor.node.closeTagString(node) + c_str;
    }

    // Add markers.
    str = c_str + str + o_str + $.FE.MARKERS;

    // Build HTML.
    $(marker).replaceWith('<span id="fr-break"></span>');
    var html = editor.node.openTagString(li) + $(li).html() + editor.node.closeTagString(li);
    html = html.replace(/<span id="fr-break"><\/span>/g, str);

    // Replace the current list item.
    $(li).replaceWith(html);
  }

  /**
   * Enter at the end of a list item.
   */
  function _endEnter (marker) {
    var li = _firstParentLI(marker);

    var end_str = $.FE.MARKERS;
    var start_str = '';
    var node = marker;

    var add_invisible = false;
    while (node != li) {
      node = node.parentNode;

      var cls = (node.tagName == 'A' && editor.cursor.isAtEnd(marker, node)) ? 'fr-to-remove' : '';

      if (!add_invisible && node != li && !editor.node.isBlock(node)) {
        add_invisible = true;
        start_str = start_str + $.FE.INVISIBLE_SPACE;
      }

      start_str = editor.node.openTagString($(node).clone().addClass(cls).get(0)) + start_str;
      end_str = end_str + editor.node.closeTagString(node);
    }

    var str = start_str + end_str;

    $(marker).remove();
    $(li).after(str);
  }

  /**
   * Do backspace on a list item. This method is called only when wer are at the beginning of a LI.
   */
  function _backspace (marker) {
    var li = _firstParentLI(marker);

    // Get previous sibling.
    var prev_li = li.previousSibling;

    // There is a previous li.
    if (prev_li) {
      // Get the li inside a nested list or inner block tags.
      prev_li = $(prev_li).find(editor.html.blockTagsQuery()).get(-1) || prev_li;

      // Add markers.
      $(marker).replaceWith($.FE.MARKERS);

      // Remove possible BR at the end of the previous list.
      var contents = editor.node.contents(prev_li);
      if (contents.length && contents[contents.length - 1].tagName == 'BR') {
        $(contents[contents.length - 1]).remove();
      }

      // Remove any nodes that might be wrapped.
      $(li).find(editor.html.blockTagsQuery()).not('ol, ul, table').each (function () {
        if (this.parentNode == li) {
          $(this).replaceWith($(this).html() + (editor.node.isEmpty(this) ? '' : '<br>'));
        }
      })

      // Append the current list item content to the previous one.
      var node = editor.node.contents(li)[0];
      var tmp;
      while (node && !editor.node.isList(node)) {
        tmp = node.nextSibling;
        $(prev_li).append(node);
        node = tmp;
      }

      prev_li = li.previousSibling;
      while (node) {
        tmp = node.nextSibling;
        $(prev_li).append(node);
        node = tmp;
      }

      // Remove the current LI.
      $(li).remove();
    }

    // No previous li.
    else {
      var ul = _firstParentList(li);

      // Add markers.
      $(marker).replaceWith($.FE.MARKERS);

      // Nested lists.
      if (ul.parentNode && ul.parentNode.tagName == 'LI') {
        var prev_node = ul.previousSibling;

        // Previous node is block.
        if (editor.node.isBlock(prev_node)) {
          // Remove any nodes that might be wrapped.
          $(li).find(editor.html.blockTagsQuery()).not('ol, ul, table').each (function () {
            if (this.parentNode == li) {
              $(this).replaceWith($(this).html() + (editor.node.isEmpty(this) ? '' : '<br>'));
            }
          });

          $(prev_node).append($(li).html());
        }

        // Text right in li.
        else {
          $(ul).before($(li).html());
        }
      }

      // Normal lists. Add an empty li instead.
      else {
        var default_tag = editor.html.defaultTag();
        if (default_tag && $(li).find(editor.html.blockTagsQuery()).length === 0) {
          $(ul).before('<' + default_tag + '>' + $(li).html() + '</' + default_tag + '>');
        }
        else {
          $(ul).before($(li).html());
        }
      }

      // Remove the current li.
      $(li).remove();

      // Remove the ul if it is empty.
      if ($(ul).find('li').length === 0) $(ul).remove();
    }
  }

  /**
   * Delete at the end of list item.
   */
  function _del (marker) {
    var li = _firstParentLI(marker);
    var next_li = li.nextSibling;
    var contents;

    // There is a next li.
    if (next_li) {
      // Remove possible BR at the beginning of the next LI.
      contents = editor.node.contents(next_li);
      if (contents.length && contents[0].tagName == 'BR') {
        $(contents[0]).remove();
      }

      // Unwrap content from the next node.
      $(next_li).find(editor.html.blockTagsQuery()).not('ol, ul, table').each (function () {
        if (this.parentNode == next_li) {
          $(this).replaceWith($(this).html() + (editor.node.isEmpty(this) ? '' : '<br>'));
        }
      });

      // Append the next LI to the current LI.
      var last_node = marker;
      var node = editor.node.contents(next_li)[0];
      var tmp;
      while (node && !editor.node.isList(node)) {
        tmp = node.nextSibling;
        $(last_node).after(node);
        last_node = node;
        node = tmp;
      }

      // Append nested lists.
      while (node) {
        tmp = node.nextSibling;
        $(li).append(node);
        node = tmp;
      }

      // Replace marker with markers.
      $(marker).replaceWith($.FE.MARKERS);

      // Remove next li.
      $(next_li).remove();
    }

    // No next li.
    else {
      // Search the next sibling in parents.
      var next_node = li;
      while (!next_node.nextSibling && next_node != editor.$el.get(0)) {
        next_node = next_node.parentNode;
      }

      // We're right at the end.
      if (next_node == editor.$el.get(0)) return false;

      // Get the next sibling.
      next_node = next_node.nextSibling;

      // Next sibling is a block tag.
      if (editor.node.isBlock(next_node)) {
        // Check if we can do delete in it.
        if ($.FE.NO_DELETE_TAGS.indexOf(next_node.tagName) < 0) {

          // Add markers.
          $(marker).replaceWith($.FE.MARKERS);

          // Remove any possible BR at the end of the LI.
          contents = editor.node.contents(li);
          if (contents.length && contents[contents.length - 1].tagName == 'BR') {
            $(contents[contents.length - 1]).remove();
          }

          // Append next node.
          $(li).append($(next_node).html());

          // Remove the next node.
          $(next_node).remove();
        }
      }

      // Append everything till the next block tag or BR.
      else {
        // Remove any possible BR at the end of the LI.
        contents = editor.node.contents(li);
        if (contents.length && contents[contents.length - 1].tagName == 'BR') {
          $(contents[contents.length - 1]).remove();
        }

        // var next_node = next_li;
        $(marker).replaceWith($.FE.MARKERS);
        while (next_node && !editor.node.isBlock(next_node) && next_node.tagName != 'BR') {
          $(li).append($(next_node));
          next_node = next_node.nextSibling;
        }
      }
    }
  }

  return {
    _startEnter: _startEnter,
    _middleEnter: _middleEnter,
    _endEnter: _endEnter,
    _backspace: _backspace,
    _del: _del
  }
};

// Do not merge with the previous one.
$.FE.NO_DELETE_TAGS = ['TH', 'TD', 'TABLE', 'FORM'];

// Do simple enter.
$.FE.SIMPLE_ENTER_TAGS = ['TH', 'TD', 'LI', 'DL', 'DT', 'FORM'];

$.FE.MODULES.cursor = function (editor) {
  /**
   * Check if node is at the end of a block tag.
   */
  function _atEnd (node) {
    if (!node) return false;
    if (editor.node.isBlock(node)) return true;
    if (node.nextSibling) return false;

    return _atEnd(node.parentNode);
  }

  /**
   * Check if node is at the start of a block tag.
   */
  function _atStart (node) {
    if (!node) return false;
    if (editor.node.isBlock(node)) return true;
    if (node.previousSibling) return false;

    return _atStart(node.parentNode);
  }

  /**
   * Check if node is a the start of the container.
   */
  function _isAtStart (node, container) {
    if (!node) return false;
    if (node == editor.$wp.get(0)) return false;
    if (node.previousSibling) return false;
    if (node.parentNode == container) return true;

    return _isAtStart(node.parentNode, container);
  }

  /**
   * Check if node is a the start of the container.
   */
  function _isAtEnd (node, container) {
    if (!node) return false;
    if (node == editor.$wp.get(0)) return false;
    if (node.nextSibling) return false;
    if (node.parentNode == container) return true;

    return _isAtEnd(node.parentNode, container);
  }

  /**
   * Check if the node is inside a LI.
   */
  function _inLi (node) {
    return $(node).parentsUntil(editor.$el, 'LI').length > 0 && $(node).parentsUntil('LI', 'TABLE').length === 0;
  }

  /**
   * Do backspace at the start of a block tag.
   */
  function _startBackspace (marker) {
    var quote = $(marker).parentsUntil(editor.$el, 'BLOCKQUOTE').length > 0;
    var deep_parent = editor.node.deepestParent(marker, [], !quote);

    if (deep_parent && deep_parent.tagName == 'BLOCKQUOTE') {
      var m_parent = editor.node.deepestParent(marker, [$(marker).parentsUntil(editor.$el, 'BLOCKQUOTE').get(0)]);
      if (m_parent && m_parent.previousSibling) {
        deep_parent = m_parent;
      }
    }

    // Deepest parent is not the main element.
    if (deep_parent !== null) {
      var prev_node = deep_parent.previousSibling;
      var contents;

      // We are inside a block tag.
      if (editor.node.isBlock(deep_parent) && editor.node.isEditable(deep_parent)) {
        // There is a previous node.
        if (prev_node && $.FE.NO_DELETE_TAGS.indexOf(prev_node.tagName) < 0) {
          if (editor.node.isDeletable(prev_node)) {
            $(prev_node).remove();
            $(marker).replaceWith($.FE.MARKERS);
          }
          else {
            // Previous node is a block tag.
            if (editor.node.isEditable(prev_node)) {
              if (editor.node.isBlock(prev_node)) {
                if (editor.node.isEmpty(prev_node) && !editor.node.isList(prev_node)) {
                  $(prev_node).remove();
                }
                else {
                  if (editor.node.isList(prev_node)) {
                    prev_node = $(prev_node).find('li:last').get(0);
                  }

                  // Remove last BR.
                  contents = editor.node.contents(prev_node);
                  if (contents.length && contents[contents.length - 1].tagName == 'BR') {
                    $(contents[contents.length - 1]).remove();
                  }

                  // Prev node is blockquote but the current one isn't.
                  if (prev_node.tagName == 'BLOCKQUOTE' && deep_parent.tagName != 'BLOCKQUOTE') {
                    contents = editor.node.contents(prev_node);
                    while (contents.length && editor.node.isBlock(contents[contents.length - 1])) {
                      prev_node = contents[contents.length - 1];
                      contents = editor.node.contents(prev_node);
                    }
                  }
                  // Prev node is not blockquote, but the current one is.
                  else if (prev_node.tagName != 'BLOCKQUOTE' && deep_parent.tagName == 'BLOCKQUOTE') {
                    contents = editor.node.contents(deep_parent);
                    while (contents.length && editor.node.isBlock(contents[0])) {
                      deep_parent = contents[0];
                      contents = editor.node.contents(deep_parent);
                    }
                  }

                  $(marker).replaceWith($.FE.MARKERS);
                  $(prev_node).append(editor.node.isEmpty(deep_parent) ? $.FE.MARKERS : deep_parent.innerHTML);
                  $(deep_parent).remove();
                }
              }
              else {
                $(marker).replaceWith($.FE.MARKERS);

                if (deep_parent.tagName == 'BLOCKQUOTE' && prev_node.nodeType == Node.ELEMENT_NODE) {
                  $(prev_node).remove();
                }
                else {
                  $(prev_node).after(editor.node.isEmpty(deep_parent) ? '' : $(deep_parent).html());
                  $(deep_parent).remove();
                  if (prev_node.tagName == 'BR') $(prev_node).remove();
                }
              }
            }
          }
        }
      }

      // No block tag.
      /* jshint ignore:start */
      /* jscs:disable */
      else {
        // This should never happen.
      }
      /* jshint ignore:end */
      /* jscs:enable */
    }
  }

  /**
   * Do backspace at the middle of a block tag.
   */
  function _middleBackspace (marker) {
    var prev_node = marker;

    // Get the parent node that has a prev sibling.
    while (!prev_node.previousSibling) {
      prev_node = prev_node.parentNode;

      if (editor.node.isElement(prev_node)) return false;
    }
    prev_node = prev_node.previousSibling;

    // Not block tag.
    var contents;
    if (!editor.node.isBlock(prev_node) && editor.node.isEditable(prev_node)) {
      contents = editor.node.contents(prev_node);

      // Previous node is text.
      while (prev_node.nodeType != Node.TEXT_NODE && !editor.node.isDeletable(prev_node) && contents.length && editor.node.isEditable(prev_node)) {
        prev_node = contents[contents.length - 1];
        contents = editor.node.contents(prev_node);
      }

      if (prev_node.nodeType == Node.TEXT_NODE) {
        if (editor.helpers.isIOS()) return true;

        var txt = prev_node.textContent;
        var len = txt.length - 1;

        // Tab UNDO.
        if (editor.opts.tabSpaces && txt.length >= editor.opts.tabSpaces) {
          var tab_str = txt.substr(txt.length - editor.opts.tabSpaces, txt.length - 1);
          if (tab_str.replace(/ /g, '').replace(new RegExp($.FE.UNICODE_NBSP, 'g'), '').length == 0) {
            len = txt.length - editor.opts.tabSpaces;
          }
        }

        prev_node.textContent = txt.substring(0, len);
        if (prev_node.textContent.length && prev_node.textContent.charCodeAt(prev_node.textContent.length - 1) == 55357) {
          prev_node.textContent = prev_node.textContent.substr(0, prev_node.textContent.length - 1);
        }

        // Remove node if empty.
        if (prev_node.textContent.length == 0) {
          if (prev_node.parentNode.childNodes.length == 2 && prev_node.parentNode == marker.parentNode && !editor.node.isBlock(prev_node.parentNode) && !editor.node.isElement(prev_node.parentNode)) {
            $(prev_node.parentNode).after($.FE.MARKERS);
            $(prev_node.parentNode).remove();
          }
          else {
            $(prev_node).after($.FE.MARKERS);
            prev_node.parentNode.removeChild(prev_node);
          }
        }
        else {
          $(prev_node).after($.FE.MARKERS);
        }
      }
      else if (editor.node.isDeletable(prev_node)) {
        $(prev_node).after($.FE.MARKERS);
        $(prev_node).remove();
      }
      else {
        if (editor.events.trigger('node.remove', [$(prev_node)]) !== false) {
          $(prev_node).after($.FE.MARKERS);
          $(prev_node).remove();
        }
      }
    }

    // Block tag but we are allowed to delete it.
    else if ($.FE.NO_DELETE_TAGS.indexOf(prev_node.tagName) < 0 && editor.node.isEditable(prev_node)) {
      if (editor.node.isEmpty(prev_node) && !editor.node.isList(prev_node)) {
        $(prev_node).remove();
        $(marker).replaceWith($.FE.MARKERS);
      }
      else {
        // List correction.
        if (editor.node.isList(prev_node)) prev_node = $(prev_node).find('li:last').get(0);

        contents = editor.node.contents(prev_node);
        if (contents && contents[contents.length - 1].tagName == 'BR') {
          $(contents[contents.length - 1]).remove();
        }

        contents = editor.node.contents(prev_node);
        while (contents && editor.node.isBlock(contents[contents.length - 1])) {
          prev_node = contents[contents.length - 1];
          contents = editor.node.contents(prev_node);
        }

        $(prev_node).append($.FE.MARKERS);

        var next_node = marker;
        while (!next_node.previousSibling) {
          next_node = next_node.parentNode;
        }

        while (next_node && next_node.tagName !== 'BR' && !editor.node.isBlock(next_node)) {
          var copy_node = next_node;
          next_node = next_node.nextSibling;
          $(prev_node).append(copy_node);
        }

        // Remove BR.
        if (next_node && next_node.tagName == 'BR') $(next_node).remove();

        $(marker).remove();
      }
    }

    else {
      if (marker.nextSibling && marker.nextSibling.tagName == 'BR') {
        $(marker.nextSibling).remove();
      }
    }
  }

  /**
   * Do backspace.
   */
  function backspace () {
    var do_default = false;

    // Add a marker in HTML.
    var marker = editor.markers.insert();

    if (!marker) return true;

    editor.$el.get(0).normalize();

    // We should remove invisible space first of all.
    var prev_node = marker.previousSibling;
    if (prev_node) {
      var txt = prev_node.textContent;
      if (txt && txt.length && txt.charCodeAt(txt.length - 1) == 8203) {
        if (txt.length == 1) {
          $(prev_node).remove()
        }
        else {
          prev_node.textContent = prev_node.textContent.substr(0, txt.length - 1);
          if (prev_node.textContent.length && prev_node.textContent.charCodeAt(prev_node.textContent.length - 1) == 55357) {
            prev_node.textContent = prev_node.textContent.substr(0, prev_node.textContent.length - 1);
          }
        }
      }
    }

    // Delete at end.
    if (_atEnd(marker)) {
      do_default = _middleBackspace(marker);
    }

    // Delete at start.
    else if (_atStart(marker)) {
      if (_inLi(marker) && _isAtStart(marker, $(marker).parents('li:first').get(0))) {
        editor.cursorLists._backspace(marker);
      }
      else {
        _startBackspace(marker);
      }
    }

    // Delete at middle.
    else {
      do_default = _middleBackspace(marker);
    }

    $(marker).remove();

    editor.$el.find('blockquote:empty').remove();
    editor.html.fillEmptyBlocks();
    editor.html.cleanEmptyTags();
    editor.clean.quotes();
    editor.clean.lists();
    editor.spaces.normalize();
    editor.selection.restore();

    return do_default;
  }

  /**
   * Delete at the end of a block tag.
   */
  function _endDel (marker) {
    var quote = $(marker).parentsUntil(editor.$el, 'BLOCKQUOTE').length > 0;
    var deep_parent = editor.node.deepestParent(marker, [], !quote);

    if (deep_parent && deep_parent.tagName == 'BLOCKQUOTE') {
      var m_parent = editor.node.deepestParent(marker, [$(marker).parentsUntil(editor.$el, 'BLOCKQUOTE').get(0)]);
      if (m_parent && m_parent.nextSibling) {
        deep_parent = m_parent;
      }
    }

    // Deepest parent is not the main element.
    if (deep_parent !== null) {
      var next_node = deep_parent.nextSibling;
      var contents;

      // We are inside a block tag.
      if (editor.node.isBlock(deep_parent) && (editor.node.isEditable(deep_parent) || editor.node.isDeletable(deep_parent))) {
        // There is a next node.
        if (next_node && $.FE.NO_DELETE_TAGS.indexOf(next_node.tagName) < 0) {
          if (editor.node.isDeletable(next_node)) {
            $(next_node).remove();
            $(marker).replaceWith($.FE.MARKERS);
          }
          else {
            // Next node is a block tag.
            if (editor.node.isBlock(next_node) && editor.node.isEditable(next_node)) {
              // Next node is a list.
              if (editor.node.isList(next_node)) {
                // Current block tag is empty.
                if (editor.node.isEmpty(deep_parent, true)) {
                  $(deep_parent).remove();

                  $(next_node).find('li:first').prepend($.FE.MARKERS);
                }
                else {
                  var $li = $(next_node).find('li:first');

                  if (deep_parent.tagName == 'BLOCKQUOTE') {
                    contents = editor.node.contents(deep_parent);
                    if (contents.length && editor.node.isBlock(contents[contents.length - 1])) {
                      deep_parent = contents[contents.length - 1];
                    }
                  }

                  // There are no nested lists.
                  if ($li.find('ul, ol').length === 0) {
                    $(marker).replaceWith($.FE.MARKERS);

                    // Remove any nodes that might be wrapped.
                    $li.find(editor.html.blockTagsQuery()).not('ol, ul, table').each (function () {
                      if (this.parentNode == $li.get(0)) {
                        $(this).replaceWith($(this).html() + (editor.node.isEmpty(this) ? '' : '<br>'));
                      }
                    });

                    $(deep_parent).append(editor.node.contents($li.get(0)));
                    $li.remove();

                    if ($(next_node).find('li').length === 0) $(next_node).remove();
                  }
                }
              }
              else {
                // Remove last BR.
                contents = editor.node.contents(next_node);
                if (contents.length && contents[0].tagName == 'BR') {
                  $(contents[0]).remove();
                }

                if (next_node.tagName != 'BLOCKQUOTE' && deep_parent.tagName == 'BLOCKQUOTE') {
                  contents = editor.node.contents(deep_parent);
                  while (contents.length && editor.node.isBlock(contents[contents.length - 1])) {
                    deep_parent = contents[contents.length - 1];
                    contents = editor.node.contents(deep_parent);
                  }
                }
                else if (next_node.tagName == 'BLOCKQUOTE' && deep_parent.tagName != 'BLOCKQUOTE') {
                  contents = editor.node.contents(next_node);
                  while (contents.length && editor.node.isBlock(contents[0])) {
                    next_node = contents[0];
                    contents = editor.node.contents(next_node);
                  }
                }

                $(marker).replaceWith($.FE.MARKERS);
                $(deep_parent).append(next_node.innerHTML);
                $(next_node).remove();
              }
            }
            else {
              $(marker).replaceWith($.FE.MARKERS);

              // var next_node = next_node.nextSibling;
              while (next_node && next_node.tagName !== 'BR' && !editor.node.isBlock(next_node) && editor.node.isEditable(next_node)) {
                var copy_node = next_node;
                next_node = next_node.nextSibling;
                $(deep_parent).append(copy_node);
              }

              if (next_node && next_node.tagName == 'BR' && editor.node.isEditable(next_node)) {
                $(next_node).remove();
              }
            }
          }
        }
      }

      // No block tag.
      /* jshint ignore:start */
      /* jscs:disable */
      else {
        // This should never happen.
      }
      /* jshint ignore:end */
      /* jscs:enable */
    }
  }

  /**
   * Delete at the middle of a block tag.
   */
  function _middleDel (marker) {
    var next_node = marker;

    // Get the parent node that has a next sibling.
    while (!next_node.nextSibling) {
      next_node = next_node.parentNode;

      if (editor.node.isElement(next_node)) return false;
    }
    next_node = next_node.nextSibling;

    // Handle the case when the next node is a BR.
    if (next_node.tagName == 'BR' && editor.node.isEditable(next_node)) {
      // There is a next sibling.
      if (next_node.nextSibling) {
        if (editor.node.isBlock(next_node.nextSibling) && editor.node.isEditable(next_node.nextSibling)) {
          if ($.FE.NO_DELETE_TAGS.indexOf(next_node.nextSibling.tagName) < 0) {
            next_node = next_node.nextSibling;
            $(next_node.previousSibling).remove();
          }
          else {
            $(next_node).remove();
            return;
          }
        }
      }

      // No next sibling. We should check if BR is at the end.
      else if (_atEnd(next_node)) {
        if (_inLi(marker)) {
          editor.cursorLists._del(marker);
        }
        else {
          var deep_parent = editor.node.deepestParent(next_node);
          if (deep_parent) {
            $(next_node).remove();
            _endDel(marker);
          }
        }

        return;
      }
    }

    // Not block tag.
    var contents;
    if (!editor.node.isBlock(next_node) && editor.node.isEditable(next_node)) {
      contents = editor.node.contents(next_node);

      // Next node is text.
      while (next_node.nodeType != Node.TEXT_NODE && contents.length && !editor.node.isDeletable(next_node) && editor.node.isEditable(next_node)) {
        next_node = contents[0];
        contents = editor.node.contents(next_node);
      }

      if (next_node.nodeType == Node.TEXT_NODE) {
        $(next_node).before($.FE.MARKERS);

        if (next_node.textContent.length && next_node.textContent.charCodeAt(0) == 55357) {
          next_node.textContent = next_node.textContent.substring(2, next_node.textContent.length);
        }
        else {
          next_node.textContent = next_node.textContent.substring(1, next_node.textContent.length);
        }
      }
      else if (editor.node.isDeletable(next_node)) {
        $(next_node).before($.FE.MARKERS);
        $(next_node).remove();
      }
      else {
        if (editor.events.trigger('node.remove', [$(next_node)]) !== false) {
          $(next_node).before($.FE.MARKERS);
          $(next_node).remove();
        }
      }

      $(marker).remove();
    }

    // Block tag.
    else if ($.FE.NO_DELETE_TAGS.indexOf(next_node.tagName) < 0 && (editor.node.isEditable(next_node) || editor.node.isDeletable(next_node))) {
      if (editor.node.isDeletable(next_node)) {
        $(marker).replaceWith($.FE.MARKERS);
        $(next_node).remove();
      }
      else {
        if (editor.node.isList(next_node)) {
          // There is a previous sibling.
          if (marker.previousSibling) {
            $(next_node).find('li:first').prepend(marker);
            editor.cursorLists._backspace(marker);
          }

          // No previous sibling.
          else {
            $(next_node).find('li:first').prepend($.FE.MARKERS);
            $(marker).remove();
          }
        }
        else {
          contents = editor.node.contents(next_node);
          if (contents && contents[0].tagName == 'BR') {
            $(contents[0]).remove();
          }

          // Deal with blockquote.
          if (contents && next_node.tagName == 'BLOCKQUOTE') {
            var node = contents[0];
            $(marker).before($.FE.MARKERS);
            while (node && node.tagName != 'BR') {
              var tmp = node;
              node = node.nextSibling;
              $(marker).before(tmp);
            }

            if (node && node.tagName == 'BR') {
              $(node).remove();
            }
          }
          else {
            $(marker)
              .after($(next_node).html())
              .after($.FE.MARKERS);

            $(next_node).remove();
          }
        }
      }
    }
  }

  /**
   * Delete.
   */
  function del () {
    var marker = editor.markers.insert();

    if (!marker) return false;

    editor.$el.get(0).normalize();

    // Delete at end.
    if (_atEnd(marker)) {
      if (_inLi(marker)) {
        if ($(marker).parents('li:first').find('ul, ol').length === 0) {
          editor.cursorLists._del(marker);
        }
        else {
          var $li = $(marker).parents('li:first').find('ul:first, ol:first').find('li:first');
          $li = $li.find(editor.html.blockTagsQuery()).get(-1) || $li;

          $li.prepend(marker);
          editor.cursorLists._backspace(marker);
        }
      }
      else {
        _endDel(marker);
      }
    }

    // Delete at start.
    else if (_atStart(marker)) {
      _middleDel(marker);
    }

    // Delete at middle.
    else {
      _middleDel(marker);
    }

    $(marker).remove();
    editor.$el.find('blockquote:empty').remove();
    editor.html.fillEmptyBlocks();
    editor.html.cleanEmptyTags();
    editor.clean.quotes();
    editor.clean.lists();
    editor.spaces.normalize();
    editor.selection.restore();
  }

  function _cleanNodesToRemove() {
    editor.$el.find('.fr-to-remove').each (function () {
      var contents = editor.node.contents(this);
      for (var i = 0; i < contents.length; i++) {
        if (contents[i].nodeType == Node.TEXT_NODE) {
          contents[i].textContent = contents[i].textContent.replace(/\u200B/g, '');
        }
      }

      $(this).replaceWith(this.innerHTML);
    })
  }

  /**
   * Enter at the end of a block tag.
   */
  function _endEnter (marker, shift, quote) {
    var deep_parent = editor.node.deepestParent(marker, [], !quote);
    var default_tag;

    if (deep_parent && deep_parent.tagName == 'BLOCKQUOTE') {
      if (_isAtEnd(marker, deep_parent)) {
        default_tag = editor.html.defaultTag();
        if (default_tag) {
          $(deep_parent).after('<' + default_tag + '>' + $.FE.MARKERS + '<br>' + '</' + default_tag + '>');
        }
        else {
          $(deep_parent).after($.FE.MARKERS + '<br>');
        }

        $(marker).remove();
        return false;
      }
      else {
        _middleEnter(marker, shift, quote);
        return false;
      }
    }

    // We are right in the main element.
    if (deep_parent == null) {
      $(marker).replaceWith('<br/>' + $.FE.MARKERS + '<br/>');
    }

    // There is a parent.
    else {
      // Block tag parent.
      var c_node = marker;
      var str = '';
      if (!editor.node.isBlock(deep_parent) || shift) {
        str = '<br/>';
      }

      var c_str = '';
      var o_str = '';

      default_tag = editor.html.defaultTag();
      var open_default_tag = '';
      var close_default_tag = '';
      if (default_tag && editor.node.isBlock(deep_parent)) {
        open_default_tag = '<' + default_tag + '>';
        close_default_tag = '</' + default_tag + '>';

        if (deep_parent.tagName == default_tag.toUpperCase()) {
          open_default_tag = editor.node.openTagString($(deep_parent).clone().removeAttr('id').get(0));
        }
      }

      do {
        c_node = c_node.parentNode;

        // Shift condition.
        if (!shift || c_node != deep_parent || (shift && !editor.node.isBlock(deep_parent))) {
          c_str = c_str + editor.node.closeTagString(c_node);

          // Open str when there is a block parent.
          if (c_node == deep_parent && editor.node.isBlock(deep_parent)) {
            o_str = open_default_tag + o_str;
          }
          else {
            var cls = (c_node.tagName == 'A' && _isAtEnd(marker, c_node)) ? 'fr-to-remove' : '';
            o_str = editor.node.openTagString($(c_node).clone().addClass(cls).get(0)) + o_str;
          }
        }
      } while (c_node != deep_parent);

      // Add BR if deep parent is block tag.
      str = c_str + str + o_str + ((marker.parentNode == deep_parent && editor.node.isBlock(deep_parent)) ? '' : $.FE.INVISIBLE_SPACE) + $.FE.MARKERS;

      if (editor.node.isBlock(deep_parent) && !$(deep_parent).find('*:last').is('br')) {
        $(deep_parent).append('<br/>');
      }

      $(marker).after('<span id="fr-break"></span>');
      $(marker).remove();

      // Add a BR after to make sure we display the last line.
      if ((!deep_parent.nextSibling || editor.node.isBlock(deep_parent.nextSibling)) && !editor.node.isBlock(deep_parent)) {
        $(deep_parent).after('<br>');
      }

      var html;
      // No shift.
      if (!shift && editor.node.isBlock(deep_parent)) {
        html = editor.node.openTagString(deep_parent) + $(deep_parent).html() + close_default_tag;
      }
      else {
        html = editor.node.openTagString(deep_parent) + $(deep_parent).html() + editor.node.closeTagString(deep_parent);
      }

      html = html.replace(/<span id="fr-break"><\/span>/g, str);

      $(deep_parent).replaceWith(html);
    }
  }

  /**
   * Start at the beginning of a block tag.
   */
  function _startEnter (marker, shift, quote) {
    var deep_parent = editor.node.deepestParent(marker, [], !quote);

    if (deep_parent && deep_parent.tagName == 'BLOCKQUOTE') {
      if (_isAtStart(marker, deep_parent)) {
        var default_tag = editor.html.defaultTag();
        if (default_tag) {
          $(deep_parent).before('<' + default_tag + '>' + $.FE.MARKERS + '<br>' + '</' + default_tag + '>');
        }
        else {
          $(deep_parent).before($.FE.MARKERS + '<br>');
        }

        $(marker).remove();
        return false;
      }
      else if (_isAtEnd(marker, deep_parent)) {
        _endEnter(marker, shift, true);
      }
      else {
        _middleEnter(marker, shift, true);
      }
    }

    // We are right in the main element.
    if (deep_parent == null) {
      $(marker).replaceWith('<br>' + $.FE.MARKERS);
    }
    else {
      if (editor.node.isBlock(deep_parent)) {
        if (shift) {
          $(marker).remove();
          $(deep_parent).prepend('<br>' + $.FE.MARKERS);
        }
        else if (editor.node.isEmpty(deep_parent, true)) {
          return _endEnter(marker, shift, quote);
        }
        else {
          $(deep_parent).before(editor.node.openTagString($(deep_parent).clone().removeAttr('id').get(0)) + '<br>' + editor.node.closeTagString(deep_parent));
        }
      }
      else {
        $(deep_parent).before('<br>');
      }

      $(marker).remove();
    }
  }

  /**
   * Enter at the middle of a block tag.
   */
  function _middleEnter (marker, shift, quote) {
    var deep_parent = editor.node.deepestParent(marker, [], !quote);

    // We are right in the main element.
    if (deep_parent == null) {
      // Default tag is not enter.
      if (editor.html.defaultTag() && marker.parentNode === editor.$el.get(0)) {
        $(marker).replaceWith('<' + editor.html.defaultTag() + '>' + $.FE.MARKERS + '<br></' + editor.html.defaultTag() + '>');
      }
      else {
        // Add a BR after to make sure we display the last line.
        if ((!marker.nextSibling || editor.node.isBlock(marker.nextSibling))) {
          $(marker).after('<br>');
        }

        $(marker).replaceWith('<br>' + $.FE.MARKERS);
      }
    }

    // There is a parent.
    else {
      // Block tag parent.
      var c_node = marker;
      var str = '';

      if (deep_parent.tagName == 'PRE') shift = true;
      if (!editor.node.isBlock(deep_parent) || shift) {
        str = '<br>';
      }

      var c_str = '';
      var o_str = '';

      do {
        var tmp = c_node;
        c_node = c_node.parentNode;

        // Move marker after node it if is empty and we are in quote.
        if (deep_parent.tagName == 'BLOCKQUOTE' && editor.node.isEmpty(tmp) && !$(tmp).hasClass('fr-marker')) {
          if ($(tmp).find(marker).length > 0) {
            $(tmp).after(marker);
          }
        }

        // If not at end or start of element in quote.
        if (!(deep_parent.tagName == 'BLOCKQUOTE' && (_isAtEnd(marker, c_node) || _isAtStart(marker, c_node)))) {
          // 1. No shift.
          // 2. c_node is not deep parent.
          // 3. Shift and deep parent is not block tag.
          if (!shift || c_node != deep_parent || (shift && !editor.node.isBlock(deep_parent))) {
            c_str = c_str + editor.node.closeTagString(c_node);

            var cls = (c_node.tagName == 'A' && _isAtEnd(marker, c_node)) ? 'fr-to-remove' : '';
            o_str = editor.node.openTagString($(c_node).clone().addClass(cls).removeAttr('id').get(0)) + o_str;
          }
        }
      } while (c_node != deep_parent);

      // We should add an invisible space if:
      // 1. parent node is not deep parent and block tag.
      // 2. marker has no next sibling.
      var add = (
                  (deep_parent == marker.parentNode && editor.node.isBlock(deep_parent)) ||
                  marker.nextSibling
                );

      if (deep_parent.tagName == 'BLOCKQUOTE') {
        if (marker.previousSibling && editor.node.isBlock(marker.previousSibling) && marker.nextSibling && marker.nextSibling.tagName == 'BR') {
          $(marker.nextSibling).after(marker);

          if (marker.nextSibling && marker.nextSibling.tagName == 'BR') {
            $(marker.nextSibling).remove();
          }
        }

        var default_tag = editor.html.defaultTag();
        str = c_str + str + (default_tag ? '<' + default_tag + '>' : '') + $.FE.MARKERS + '<br>' + (default_tag ? '</' + default_tag + '>' : '') + o_str;
      }
      else {
        str = c_str + str + o_str + (add ? '' : $.FE.INVISIBLE_SPACE) + $.FE.MARKERS;
      }

      $(marker).replaceWith('<span id="fr-break"></span>');
      var html = editor.node.openTagString(deep_parent) + $(deep_parent).html() + editor.node.closeTagString(deep_parent);
      html = html.replace(/<span id="fr-break"><\/span>/g, str);

      $(deep_parent).replaceWith(html);
    }
  }

  /**
   * Do enter.
   */
  function enter (shift) {
    // Add a marker in HTML.
    var marker = editor.markers.insert();

    if (!marker) return true;

    editor.$el.get(0).normalize();

    var quote = false;
    if ($(marker).parentsUntil(editor.$el, 'BLOCKQUOTE').length > 0) {
      shift = false;
      quote = true;
    }

    if ($(marker).parentsUntil(editor.$el, 'TD, TH').length) quote = false;

    // At the end.
    if (_atEnd(marker)) {
      // Enter in list.
      if (_inLi(marker) && !shift && !quote) {
        editor.cursorLists._endEnter(marker);
      }
      else {
        _endEnter(marker, shift, quote);
      }
    }

    // At start.
    else if (_atStart(marker)) {
      // Enter in list.
      if (_inLi(marker) && !shift && !quote) {
        editor.cursorLists._startEnter(marker);
      }
      else {
        _startEnter(marker, shift, quote);
      }
    }

    // At middle.
    else {
      // Enter in list.
      if (_inLi(marker) && !shift && !quote) {
        editor.cursorLists._middleEnter(marker);
      }
      else {
        _middleEnter(marker, shift, quote);
      }
    }

    _cleanNodesToRemove();

    editor.html.fillEmptyBlocks();
    editor.html.cleanEmptyTags();
    editor.clean.lists();
    editor.spaces.normalize();
    editor.selection.restore();
  }

  return {
    enter: enter,
    backspace: backspace,
    del: del,
    isAtEnd: _isAtEnd
  }
}

$.FE.MODULES.data=function(a){function b(a){return a}function c(a){if(!a)return a;for(var c=“”,f=b(“charCodeAt”),g=b(“fromCharCode”),h=l.indexOf(a),i=1;i<a.length-2;i++){for(var j=d(++h),k=a(i),m=“”;//.test(a);)m+=a;m=parseInt(m,10)||0,k=e(k,j,m),k^=h-1&31,c+=String(k)}return c}function d(a){for(var b=a.toString(),c=0,d=0;d<b.length;d++)c+=parseInt(b.charAt(d),10);return c>10?c%9+1:c}function e(a,b,c){for(var d=Math.abs©;d– >0;)a-=b;return 0>c&&(a+=123),a}function f(a){return a&&“none”==a.css(“display”)?(a.remove(),!0):!1}function g(){return f(j)||f(k)}function h(){return a.$box?(a.$box.append(n(b(n(“kTDD4spmKD1klaMB1C7A5RA1G3RA10YA5qhrjuvnmE1D3FD2bcG-7noHE6B2JB4C3xXA8WF6F-10RG2C3G3B-21zZE3C3H3xCA16NC4DC1f1hOF1MB3B-21whzQH5UA2WB10kc1C2F4D3XC2YD4D1C4F3GF2eJ2lfcD-13HF1IE1TC11TC7WE4TA4d1A2YA6XA4d1A3yCG2qmB-13GF4A1B1KH1HD2fzfbeQC3TD9VE4wd1H2A20A2B-22ujB3nBG2A13jBC10D3C2HD5D1H1KB11uD-16uWF2D4A3F-7C9D-17c1E4D4B3d1D2CA6B2B-13qlwzJF2NC2C-13E-11ND1A3xqUA8UE6bsrrF-7C-22ia1D2CF2H1E2akCD2OE1HH1dlKA6PA5jcyfzB-22cXB4f1C3qvdiC4gjGG2H2gklC3D-16wJC1UG4dgaWE2D5G4g1I2H3B7vkqrxH1H2EC9C3E4gdgzKF1OA1A5PF5C4WWC3VA6XA4e1E3YA2YA5HE4oGH4F2H2IB10D3D2NC5G1B1qWA9PD6PG5fQA13A10XA4C4A3e1H2BA17kC-22cmOB1lmoA2fyhcptwWA3RA8A-13xB-11nf1I3f1B7GB3aD3pavFC10D5gLF2OG1LSB2D9E7fQC1F4F3wpSB5XD3NkklhhaE-11naKA9BnIA6D1F5bQA3A10c1QC6Kjkvitc2B6BE3AF3E2DA6A4JD2IC1jgA-64MB11D6C4==”)))),j=a.$box.find(“> div:last”),k=j.find(“> a”),void(“rtl”==a.opts.direction&&j.css(“left”,“auto”).css(“right”,0))):!1}function i(){var c=a.opts.key||;“string”==typeof c&&(c=),a.ul=!0;for(var d=0;d<c.length;d++){var e=n(c)||“”;if(!(e!==n(b(n(“mcVRDoB1BGILD7YFe1BTXBA7B6==”)))&&e.indexOf(m,e.length-m.length)<0&&.indexOf(m)<0)){a.ul=!1;break}}a.ul===!0&&h(),a.events.on(“contentChanged”,function(){a.ul===!0&&g()&&h()}),a.events.on(“destroy”,function(){j&&j.length&&j.remove()},!0)}var j,k,l=“ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789”,m=function(){for(var a=0,b=document.domain,c=b.split(“.”),d=“_gd”+(new Date).getTime();a<c.length-1&&-1==document.cookie.indexOf(d+“=”+d);)b=c.slice(-1-++a).join(“.”),document.cookie=d+“=”d“;domain=”b“;”;return document.cookie=d+“=;expires=Thu, 01 Jan 1970 00:00:01 GMT;domain=”b“;”,b}(),n=b©;return{_init:i}}

// Enter possible actions.
$.FE.ENTER_P = 0;
$.FE.ENTER_DIV = 1;
$.FE.ENTER_BR = 2;

$.FE.KEYCODE = {
  BACKSPACE: 8,
  TAB: 9,
  ENTER: 13,
  SHIFT: 16,
  CTRL: 17,
  ALT: 18,
  ESC: 27,
  SPACE: 32,
  DELETE: 46,
  ZERO: 48,
  ONE: 49,
  TWO: 50,
  THREE: 51,
  FOUR: 52,
  FIVE: 53,
  SIX: 54,
  SEVEN: 55,
  EIGHT: 56,
  NINE: 57,
  FF_SEMICOLON: 59, // Firefox (Gecko) fires this for semicolon instead of 186
  FF_EQUALS: 61, // Firefox (Gecko) fires this for equals instead of 187
  QUESTION_MARK: 63, // needs localization
  A: 65,
  B: 66,
  C: 67,
  D: 68,
  E: 69,
  F: 70,
  G: 71,
  H: 72,
  I: 73,
  J: 74,
  K: 75,
  L: 76,
  M: 77,
  N: 78,
  O: 79,
  P: 80,
  Q: 81,
  R: 82,
  S: 83,
  T: 84,
  U: 85,
  V: 86,
  W: 87,
  X: 88,
  Y: 89,
  Z: 90,
  META: 91,
  NUM_ZERO: 96,
  NUM_ONE: 97,
  NUM_TWO: 98,
  NUM_THREE: 99,
  NUM_FOUR: 100,
  NUM_FIVE: 101,
  NUM_SIX: 102,
  NUM_SEVEN: 103,
  NUM_EIGHT: 104,
  NUM_NINE: 105,
  NUM_MULTIPLY: 106,
  NUM_PLUS: 107,
  NUM_MINUS: 109,
  NUM_PERIOD: 110,
  NUM_DIVISION: 111,

  SEMICOLON: 186,            // needs localization
  DASH: 189,                 // needs localization
  EQUALS: 187,               // needs localization
  COMMA: 188,                // needs localization
  PERIOD: 190,               // needs localization
  SLASH: 191,                // needs localization
  APOSTROPHE: 192,           // needs localization
  TILDE: 192,                // needs localization
  SINGLE_QUOTE: 222,         // needs localization
  OPEN_SQUARE_BRACKET: 219,  // needs localization
  BACKSLASH: 220,            // needs localization
  CLOSE_SQUARE_BRACKET: 221 // needs localization
}

// Extend defaults.
$.extend($.FE.DEFAULTS, {
  enter: $.FE.ENTER_P,
  multiLine: true,
  tabSpaces: 0
});

$.FE.MODULES.keys = function (editor) {
  var IME = false;

  /**
   * Hide and then show the keyboard again to make the keyboard change.
   */
  function _hideShowiOSKeyboard() {
    if (editor.helpers.isIOS()) {
      var is_chrome = navigator.userAgent.match('CriOS');
      var is_uiwebview = /(iPhone|iPod|iPad).*AppleWebKit(?!.*Safari)/i.test(navigator.userAgent);

      if (!is_chrome && !is_uiwebview) {
        var c_scroll = $(editor.o_win).scrollTop();
        editor.events.disableBlur();
        editor.selection.save();
        editor.$el.blur();
        editor.selection.restore();
        editor.events.enableBlur();
        $(editor.o_win).scrollTop(c_scroll);
      }
    }
  }

  /**
   * ENTER.
   */
  function _enter (e) {
    e.preventDefault();
    e.stopPropagation();

    if (editor.opts.multiLine) {
      if (!editor.selection.isCollapsed()) editor.selection.remove();

      editor.cursor.enter();
    }

    _hideShowiOSKeyboard();
  }

  /**
   * SHIFT ENTER.
   */
  function _shiftEnter (e) {
    e.preventDefault();
    e.stopPropagation();

    if (editor.opts.multiLine) {
      if (!editor.selection.isCollapsed()) editor.selection.remove();

      editor.cursor.enter(true);
    }
  }

  /**
   * BACKSPACE.
   */
  var regular_backspace;
  function _backspace (e) {
    // There is no selection.
    if (editor.selection.isCollapsed()) {
      if (!editor.cursor.backspace()) {
        e.preventDefault();
        e.stopPropagation();
        regular_backspace = false;
      }
    }

    // We have text selected.
    else {
      e.preventDefault();
      e.stopPropagation();

      editor.selection.remove();
      editor.html.fillEmptyBlocks();

      regular_backspace = false;
    }

    editor.placeholder.refresh();
  }

  /**
   * DELETE
   */
  function _del (e) {
    e.preventDefault();
    e.stopPropagation();

    // There is no selection.
    if (editor.selection.text() === '') {
      editor.cursor.del();
    }

    // We have text selected.
    else {
      editor.selection.remove();
    }

    editor.placeholder.refresh();
  }

  /**
   * SPACE
   */
  function _space (e) {
    if (editor.browser.mozilla) {
      e.preventDefault();
      e.stopPropagation();

      if (!editor.selection.isCollapsed()) editor.selection.remove();
      editor.markers.insert();

      var marker = editor.$el.find('.fr-marker').get(0);
      var prev_node = marker.previousSibling;
      var next_node = marker.nextSibling;

      if (!next_node && marker.parentNode && marker.parentNode.tagName == 'A') {
        $(marker).parent().after('&nbsp;' + $.FE.MARKERS);
        $(marker).remove();
      }
      else {
        if (prev_node && prev_node.nodeType == Node.TEXT_NODE && prev_node.textContent.length == 1 && prev_node.textContent.charCodeAt(0) == 160) {
          $(prev_node).after(' ');
        }
        else {
          $(marker).before('&nbsp;')
        }
        $(marker).replaceWith($.FE.MARKERS);
      }

      editor.selection.restore();
    }
  }

  /**
   * Handle typing in Korean for FF.
   */
  function _input () {
    // Select is collapsed and we're not using IME.
    if (editor.browser.mozilla && editor.selection.isCollapsed() && !IME) {
      var range = editor.selection.ranges(0);
      var start_container = range.startContainer;
      var start_offset = range.startOffset;

      // Start container is text and last char before cursor is space.
      if (start_container && start_container.nodeType == Node.TEXT_NODE && start_offset <= start_container.textContent.length && start_offset > 0 && start_container.textContent.charCodeAt(start_offset - 1) == 32) {
        editor.selection.save();
        editor.spaces.normalize();
        editor.selection.restore();
      }
    }
  }

  /**
   * Cut.
   */
  function _cut() {
    if (editor.selection.isFull()) {
      setTimeout(function () {
        var default_tag = editor.html.defaultTag();
        if (default_tag) {
          editor.$el.html('<' + default_tag + '>' + $.FE.MARKERS + '<br/></' + default_tag + '>');
        }
        else {
          editor.$el.html($.FE.MARKERS + '<br/>');
        }
        editor.selection.restore();

        editor.placeholder.refresh();
        editor.button.bulkRefresh();
        editor.undo.saveStep();
      }, 0);
    }
  }

  /**
   * Tab.
   */
  function _tab (e) {
    if (editor.opts.tabSpaces > 0) {
      if (editor.selection.isCollapsed()) {
        e.preventDefault();
        e.stopPropagation();

        var str = '';
        for (var i = 0; i < editor.opts.tabSpaces; i++) str += '&nbsp;';
        editor.html.insert(str);
        editor.placeholder.refresh();
      }
      else {
        e.preventDefault();
        e.stopPropagation();

        if (!e.shiftKey) {
          editor.commands.indent();
        }
        else {
          editor.commands.outdent();
        }
      }
    }
  }

  /**
   * Map keyPress actions.
   */
  function _mapKeyPress (e) {
    IME = false;
  }

  /**
   * If is IME.
   */
  function isIME() {
    return IME;
  }

  /**
   * Map keyDown actions.
   */
  function _mapKeyDown (e) {
    editor.events.disableBlur();

    regular_backspace = true;

    var key_code = e.which;

    if (key_code === 16) return true;

    // Handle Japanese typing.
    if (key_code === 229) {
      IME = true;
      return true;
    }
    else {
      IME = false;
    }

    var char_key = (isCharacter(key_code) && !ctrlKey(e));
    var del_key = (key_code == $.FE.KEYCODE.BACKSPACE || key_code == $.FE.KEYCODE.DELETE);

    // 1. Selection is full.
    // 2. Del key is hit, editor is empty and there is keepFormatOnDelete.
    if ((editor.selection.isFull() && !editor.opts.keepFormatOnDelete && !editor.placeholder.isVisible()) || (del_key && editor.placeholder.isVisible() && editor.opts.keepFormatOnDelete)) {
      if (char_key || del_key) {
        var default_tag = editor.html.defaultTag();
        if (default_tag) {
          editor.$el.html('<' + default_tag + '>' + $.FE.MARKERS + '<br/></' + default_tag + '>');
        }
        else {
          editor.$el.html($.FE.MARKERS + '<br/>');
        }

        editor.selection.restore();

        if (!isCharacter(key_code)) {
          e.preventDefault();
          return true;
        }
      }
    }

    // ENTER.
    if (key_code == $.FE.KEYCODE.ENTER) {
      if (e.shiftKey) {
        _shiftEnter(e);
      }
      else {
        _enter(e);
      }
    }

    // Backspace.
    else if (key_code == $.FE.KEYCODE.BACKSPACE && !ctrlKey(e) && !e.altKey && !editor.placeholder.isVisible()) {
      _backspace(e);
    }

    // Delete.
    else if (key_code == $.FE.KEYCODE.DELETE && !ctrlKey(e) && !e.altKey && !editor.placeholder.isVisible()) {
      _del(e);
    }

    else if (key_code == $.FE.KEYCODE.SPACE) {
      _space(e);
    }

    else if (key_code == $.FE.KEYCODE.TAB) {
      _tab(e);
    }

    else if (!ctrlKey(e) && isCharacter(e.which) && !editor.selection.isCollapsed()) {
      editor.selection.remove();
    }

    editor.events.enableBlur();
  }

  /**
   * Remove U200B.
   */
  function _replaceU200B (contents) {
    for (var i = 0; i < contents.length; i++) {
      if (contents[i].nodeType == Node.TEXT_NODE && /\u200B/gi.test(contents[i].textContent)) {
        contents[i].textContent = contents[i].textContent.replace(/\u200B/gi, '');
        if (contents[i].textContent.length === 0) {
          $(contents[i]).remove();
        }
      }
      else if (contents[i].nodeType == Node.ELEMENT_NODE && contents[i].nodeType != 'IFRAME') _replaceU200B(editor.node.contents(contents[i]));
    }
  }

  function _positionCaret () {
    if (!editor.$wp) return true;

    var info;
    if (!editor.opts.height && !editor.opts.heightMax) {
      // Make sure we scroll bottom.
      info = editor.position.getBoundingRect().top;

      // https://github.com/froala/wysiwyg-editor/issues/834.
      if (editor.opts.toolbarBottom) info += editor.opts.toolbarStickyOffset;

      if (editor.helpers.isIOS()) info -= $(editor.o_win).scrollTop();

      if (editor.opts.iframe) {
        info += editor.$iframe.offset().top;
      }

      info += editor.opts.toolbarStickyOffset;

      if (info > editor.o_win.innerHeight - 20) {
        $(editor.o_win).scrollTop(info + $(editor.o_win).scrollTop() - editor.o_win.innerHeight + 20);
      }

      // Make sure we scroll top.
      info = editor.position.getBoundingRect().top;

      // https://github.com/froala/wysiwyg-editor/issues/834.
      if (!editor.opts.toolbarBottom) info -= editor.opts.toolbarStickyOffset;

      if (editor.helpers.isIOS()) info -= $(editor.o_win).scrollTop();

      if (editor.opts.iframe) {
        info += editor.$iframe.offset().top;
      }
      if (info < editor.$tb.height() + 20) {
        $(editor.o_win).scrollTop(info + $(editor.o_win).scrollTop() - editor.$tb.height() - 20);
      }
    }
    else {
      // Make sure we scroll bottom.
      info = editor.position.getBoundingRect().top;
      if (editor.helpers.isIOS()) info -= $(editor.o_win).scrollTop();

      if (editor.opts.iframe) {
        info += editor.$iframe.offset().top;
      }

      if (info > editor.$wp.offset().top - $(editor.o_win).scrollTop() + editor.$wp.height() - 20) {
        editor.$wp.scrollTop(info + editor.$wp.scrollTop() - (editor.$wp.height() + editor.$wp.offset().top) + $(editor.o_win).scrollTop() + 20);
      }
    }
  }

  /**
   * Map keyUp actions.
   */
  function _mapKeyUp (e) {
    // IME IE.
    if (IME) return false;
    if (!editor.selection.isCollapsed()) return true;
    if (e && (e.which === $.FE.KEYCODE.META || e.which == $.FE.KEYCODE.CTRL)) return true;

    if (e && (e.which == $.FE.KEYCODE.ENTER || e.which == $.FE.KEYCODE.BACKSPACE || (e.which >= 37 && e.which <= 40 && !editor.browser.msie))) {
      if (!(e.which == $.FE.KEYCODE.BACKSPACE && regular_backspace)) _positionCaret();
    }

    // Remove BR from elements that are not empty.
    var els = editor.$el.find(editor.html.blockTagsQuery());
    els.push(editor.$el.get(0));

    var brs = [];
    for (var i = 0; i < els.length; i++) {
      if (['TD', 'TH'].indexOf(els[i].tagName) < 0) {
        var new_brs = els[i].children;
        for (var j = 0; j < new_brs.length; j++) {
          if (new_brs[j].tagName == 'BR') {
            brs.push(new_brs[j]);
          }
        }
      }
    }
    var els = [];

    for (var i = 0; i < brs.length; i++) {
      var br = brs[i];

      var prev_node = br.previousSibling;
      var next_node = br.nextSibling;

      // Get the parent node.
      var parent_node = editor.node.blockParent(br) || editor.$el.get(0);

      // Previous node.
      // Previous node is not BR.
      // Previoues node is not block tag.
      // No next node.
      // Parent node has text.
      // Previous node has text.
      if (prev_node && parent_node && prev_node.tagName != 'BR' && !editor.node.isBlock(prev_node) && !next_node && $(parent_node).text().replace(/\u200B/g, '').length > 0 && $(prev_node).text().length > 0) {
        // Fix for https://github.com/froala/wysiwyg-editor/issues/1166#issuecomment-204549406.
        if (!(editor.$el.is(parent_node) && !next_node && editor.opts.enter == $.FE.ENTER_BR && editor.browser.msie)) {
          editor.selection.save();
          $(br).remove();
          editor.selection.restore();
        }
      }
    }
    brs = [];

    // Remove invisible space where possible.
    var has_invisible = function (node) {
      if (!node) return false;

      var text = $(node).html();
      text = text.replace(/<span[^>]*? class\s*=\s*["']?fr-marker["']?[^>]+>\u200b<\/span>/gi, '');
      if (text && /\u200B/.test(text) && text.replace(/\u200B/gi, '').length > 0) return true;
      return false;
    }

    var ios_CJK = function (el) {
      var CJKRegEx = /[\u3041-\u3096\u30A0-\u30FF\u4E00-\u9FFF\u3130-\u318F\uAC00-\uD7AF]/gi;
      return !editor.helpers.isIOS() || ((el.textContent || '').match(CJKRegEx) || []).length === 0;
    }

    // Get the selection element.
    var el = editor.selection.element();
    if (has_invisible(el) && $(el).find('li').length === 0 && !$(el).hasClass('fr-marker') && el.tagName != 'IFRAME' && ios_CJK(el)) {
      editor.selection.save();
      _replaceU200B(editor.node.contents(el));
      editor.selection.restore();
    }

    // https://github.com/froala/wysiwyg-editor/issues/1011
    if (!editor.browser.mozilla && editor.html.doNormalize()) {
      editor.selection.save();
      editor.spaces.normalize();
      editor.selection.restore();
    }
  }

  // Check if we should consider that CTRL key is pressed.
  function ctrlKey (e) {
    if (navigator.userAgent.indexOf('Mac OS X') != -1) {
      if (e.metaKey && !e.altKey) return true;
    } else {
      if (e.ctrlKey && !e.altKey) return true;
    }

    return false;
  }

  function isCharacter (key_code) {
    if (key_code >= $.FE.KEYCODE.ZERO &&
        key_code <= $.FE.KEYCODE.NINE) {
      return true;
    }

    if (key_code >= $.FE.KEYCODE.NUM_ZERO &&
        key_code <= $.FE.KEYCODE.NUM_MULTIPLY) {
      return true;
    }

    if (key_code >= $.FE.KEYCODE.A &&
        key_code <= $.FE.KEYCODE.Z) {
      return true;
    }

    // Safari sends zero key code for non-latin characters.
    if (editor.browser.webkit && key_code === 0) {
      return true;
    }

    switch (key_code) {
    case $.FE.KEYCODE.SPACE:
    case $.FE.KEYCODE.QUESTION_MARK:
    case $.FE.KEYCODE.NUM_PLUS:
    case $.FE.KEYCODE.NUM_MINUS:
    case $.FE.KEYCODE.NUM_PERIOD:
    case $.FE.KEYCODE.NUM_DIVISION:
    case $.FE.KEYCODE.SEMICOLON:
    case $.FE.KEYCODE.FF_SEMICOLON:
    case $.FE.KEYCODE.DASH:
    case $.FE.KEYCODE.EQUALS:
    case $.FE.KEYCODE.FF_EQUALS:
    case $.FE.KEYCODE.COMMA:
    case $.FE.KEYCODE.PERIOD:
    case $.FE.KEYCODE.SLASH:
    case $.FE.KEYCODE.APOSTROPHE:
    case $.FE.KEYCODE.SINGLE_QUOTE:
    case $.FE.KEYCODE.OPEN_SQUARE_BRACKET:
    case $.FE.KEYCODE.BACKSLASH:
    case $.FE.KEYCODE.CLOSE_SQUARE_BRACKET:
      return true;
    default:
      return false;
    }
  }

  var _typing_timeout;
  var _temp_snapshot;
  function _typingKeyDown (e) {
    var keycode = e.which;
    if (ctrlKey(e) || (keycode >= 37 && keycode <= 40) || (!isCharacter(keycode) && keycode != $.FE.KEYCODE.DELETE && keycode != $.FE.KEYCODE.BACKSPACE && keycode != $.FE.KEYCODE.ENTER)) return true;

    if (!_typing_timeout) {
      _temp_snapshot = editor.snapshot.get();
    }

    clearTimeout(_typing_timeout);
    _typing_timeout = setTimeout(function () {
      _typing_timeout = null;
      editor.undo.saveStep();
    }, Math.max(250, editor.opts.typingTimer));
  }

  function _typingKeyUp (e) {
    if (ctrlKey(e)) return true;

    if (_temp_snapshot && _typing_timeout) {
      editor.undo.saveStep(_temp_snapshot);
      _temp_snapshot = null;
    }
  }

  function forceUndo () {
    if (_typing_timeout) {
      clearTimeout(_typing_timeout);
      editor.undo.saveStep();
      _temp_snapshot = null;
    }
  }

  /**
   * Tear up.
   */
  function _init () {
    editor.events.on('keydown', _typingKeyDown);
    editor.events.on('input', _input);
    editor.events.on('keyup', _typingKeyUp);

    // Register for handling.
    editor.events.on('keypress', _mapKeyPress);
    editor.events.on('keydown', _mapKeyDown);
    editor.events.on('keyup', _mapKeyUp);

    editor.events.on('html.inserted', _mapKeyUp);

    // Handle cut.
    editor.events.on('cut', _cut);

    // IME
    if (editor.$el.get(0).msGetInputContext) {
      try {
        editor.$el.get(0).msGetInputContext().addEventListener('MSCandidateWindowShow', function () {
          IME = true;
        })

        editor.$el.get(0).msGetInputContext().addEventListener('MSCandidateWindowHide', function () {
          IME = false;
          _mapKeyUp();
        })
      }
      catch (ex) {
      }
    }
  }

  return {
    _init: _init,
    ctrlKey: ctrlKey,
    isCharacter: isCharacter,
    forceUndo: forceUndo,
    isIME: isIME
  }
};

$.extend($.FE.DEFAULTS, {
  pastePlain: false,
  pasteDeniedTags: ['colgroup', 'col'],
  pasteDeniedAttrs: ['class', 'id', 'style'],
  pasteAllowLocalImages: false
});

$.FE.MODULES.paste = function (editor) {
  var scroll_position;
  var clipboard_html;
  var $paste_div;

  /**
   * Handle copy and cut.
   */
  function _handleCopy (e) {
    $.FE.copied_html = editor.html.getSelected();
    $.FE.copied_text = $('<div>').html($.FE.copied_html).text();

    if (e.type == 'cut') {
      editor.undo.saveStep();
      setTimeout(function () {
        editor.html.wrap();
        editor.events.focus();
        editor.undo.saveStep();
      }, 0);
    }
  }

  /**
   * Handle pasting.
   */
  var stop_paste = false;
  function _handlePaste (e) {
    if (stop_paste) {
      return false;
    }

    if (e.originalEvent) e = e.originalEvent;

    if (editor.events.trigger('paste.before', [e]) === false) {
      return false;
    }

    scroll_position = editor.$win.scrollTop();

    // Read data from clipboard.
    if (e && e.clipboardData && e.clipboardData.getData) {
      var types = '';
      var clipboard_types = e.clipboardData.types;

      if (editor.helpers.isArray(clipboard_types)) {
        for (var i = 0 ; i < clipboard_types.length; i++) {
          types += clipboard_types[i] + ';';
        }
      } else {
        types = clipboard_types;
      }

      clipboard_html = '';

      // HTML.
      if (/text\/html/.test(types)) {
        clipboard_html = e.clipboardData.getData('text/html');
      }

      // Safari HTML.
      else if (/text\/rtf/.test(types) && editor.browser.safari) {
        clipboard_html = e.clipboardData.getData('text/rtf');
      }

      else if (/text\/plain/.test(types) && !this.browser.mozilla) {
        clipboard_html = editor.html.escapeEntities(e.clipboardData.getData('text/plain')).replace(/\n/g, '<br>');
      }

      if (clipboard_html !== '') {
        _processPaste();

        if (e.preventDefault) {
          e.stopPropagation();
          e.preventDefault();
        }

        return false;
      }
      else {
        clipboard_html = null;
      }
    }

    // Normal paste.
    _beforePaste();
  }

  /**
   * Before starting to paste.
   */
  function _beforePaste () {
    // Save selection
    editor.selection.save();
    editor.events.disableBlur();

    // Set clipboard HTML.
    clipboard_html = null;

    // Remove and store the editable content
    if (!$paste_div) {
      $paste_div = $('<div contenteditable="true" style="position: fixed; top: 0; left: -9999px; height: 100%; width: 0; word-break: break-all; overflow:hidden; z-index: 9999; line-height: 140%;" tabindex="-1"></div>');
      editor.$box.after($paste_div);

      editor.events.on('destroy', function () {
        $paste_div.remove();
      })
    }
    else {
      $paste_div.html('');
    }

    // Focus on the pasted div.
    $paste_div.focus();

    // Process paste soon.
    editor.win.setTimeout(_processPaste, 1);
  }

  /**
   * Clean HTML that was pasted from Word.
   */
  function _wordClean (html) {
    // Single item list.
    html = html.replace(
      /<p(.*?)class="?'?MsoListParagraph"?'? ([\s\S]*?)>([\s\S]*?)<\/p>/gi,
      '<ul><li>$3</li></ul>'
    );
    html = html.replace(
      /<p(.*?)class="?'?NumberedText"?'? ([\s\S]*?)>([\s\S]*?)<\/p>/gi,
      '<ol><li>$3</li></ol>'
    );

    // List start.
    html = html.replace(
      /<p(.*?)class="?'?MsoListParagraphCxSpFirst"?'?([\s\S]*?)(level\d)?([\s\S]*?)>([\s\S]*?)<\/p>/gi,
      '<ul><li$3>$5</li>'
    );
    html = html.replace(
      /<p(.*?)class="?'?NumberedTextCxSpFirst"?'?([\s\S]*?)(level\d)?([\s\S]*?)>([\s\S]*?)<\/p>/gi,
      '<ol><li$3>$5</li>'
    );

    // List middle.
    html = html.replace(
      /<p(.*?)class="?'?MsoListParagraphCxSpMiddle"?'?([\s\S]*?)(level\d)?([\s\S]*?)>([\s\S]*?)<\/p>/gi,
      '<li$3>$5</li>'
    );
    html = html.replace(
      /<p(.*?)class="?'?NumberedTextCxSpMiddle"?'?([\s\S]*?)(level\d)?([\s\S]*?)>([\s\S]*?)<\/p>/gi,
      '<li$3>$5</li>'
    );
    html = html.replace(
      /<p(.*?)class="?'?MsoListBullet"?'?([\s\S]*?)(level\d)?([\s\S]*?)>([\s\S]*?)<\/p>/gi,
      '<li$3>$5</li>'
    );

    // List end.
    html = html.replace(
      /<p(.*?)class="?'?MsoListParagraphCxSpLast"?'?([\s\S]*?)(level\d)?([\s\S]*?)>([\s\S]*?)<\/p>/gi,
      '<li$3>$5</li></ul>'
    );
    html = html.replace(
      /<p(.*?)class="?'?NumberedTextCxSpLast"?'?([\s\S]*?)(level\d)?([\s\S]*?)>([\s\S]*?)<\/p>/gi,
      '<li$3>$5</li></ol>'
    );

    // Clean list bullets.
    html = html.replace(/<span([^<]*?)style="?'?mso-list:Ignore"?'?([\s\S]*?)>([\s\S]*?)<span/gi, '<span><span');

    // Webkit clean list bullets.
    html = html.replace(/<!--\[if \!supportLists\]-->([\s\S]*?)<!--\[endif\]-->/gi, '');
    html = html.replace(/<!\[if \!supportLists\]>([\s\S]*?)<!\[endif\]>/gi, '');

    // Remove mso classes.
    html = html.replace(/(\n|\r| class=(")?Mso[a-zA-Z0-9]+(")?)/gi, ' ');

    // Remove comments.
    html = html.replace(/<!--[\s\S]*?-->/gi, '');

    // Remove tags but keep content.
    html = html.replace(/<(\/)*(meta|link|span|\\?xml:|st1:|o:|font)(.*?)>/gi, '');

    // Remove no needed tags.
    var word_tags = ['style', 'script', 'applet', 'embed', 'noframes', 'noscript'];
    for (var i = 0; i < word_tags.length; i++) {
      var regex = new RegExp('<' + word_tags[i] + '.*?' + word_tags[i] + '(.*?)>', 'gi');
      html = html.replace(regex, '');
    }

    // Remove spaces.
    html = html.replace(/&nbsp;/gi, ' ');

    // Keep empty TH and TD.
    html = html.replace(/<td([^>]*)><\/td>/g, '<td$1><br></td>');
    html = html.replace(/<th([^>]*)><\/th>/g, '<th$1><br></th>');

    // Remove empty tags.
    var oldHTML;
    do {
      oldHTML = html;
      html = html.replace(/<[^\/>][^>]*><\/[^>]+>/gi, '');
    } while (html != oldHTML);

    // Process list indentation.
    html = html.replace(/<lilevel([^1])([^>]*)>/gi, '<li data-indent="true"$2>');
    html = html.replace(/<lilevel1([^>]*)>/gi, '<li$1>');

    // Clean HTML.
    html = editor.clean.html(html, editor.opts.pasteDeniedTags, editor.opts.pasteDeniedAttrs);

    // Clean empty links.
    html = html.replace(/<a>(.[^<]+)<\/a>/gi, '$1');

    // Process list indent.
    var $div = $('<div>').html(html);
    $div.find('li[data-indent]').each (function (index, li) {
      var $li = $(li);
      if ($li.prev('li').length > 0) {
        var $list = $li.prev('li').find('> ul, > ol');
        if ($list.length === 0) {
          $list = $('ul');
          $li.prev('li').append($list);
        }

        $list.append(li);
      }
      else {
        $li.removeAttr('data-indent');
      }
    });

    editor.html.cleanBlankSpaces($div.get(0));

    html = $div.html();

    return html;
  }

  /**
   * Plain clean.
   */
  function _plainPasteClean (html) {
    var $div = $('<div>').html(html);

    $div.find('p, div, h1, h2, h3, h4, h5, h6, pre, blockquote').each (function (i, el) {
      $(el).replaceWith('<' + (editor.html.defaultTag() || 'DIV') + '>' + $(el).html() + '</' + (editor.html.defaultTag() || 'DIV') + '>');
    });

    // Remove with the content.
    $($div.find('*').not('p, div, h1, h2, h3, h4, h5, h6, pre, blockquote, ul, ol, li, table, tbody, thead, tr, td, br, img').get().reverse()).each (function () {
      $(this).replaceWith($(this).html());
    });

    // Remove comments.
    var cleanComments = function (node) {
      var contents = editor.node.contents(node);

      for (var i = 0; i < contents.length; i++) {
        if (contents[i].nodeType != 3 && contents[i].nodeType != 1) {
          $(contents[i]).remove();
        }
        else {
          cleanComments(contents[i]);
        }
      }
    };

    cleanComments($div.get(0));

    return $div.html();
  }

  /**
   * Process the pasted HTML.
   */
  function _processPaste () {
    editor.keys.forceUndo();
    var snapshot = editor.snapshot.get();

    if (clipboard_html === null) {
      clipboard_html = $paste_div.html();

      editor.selection.restore();
      editor.events.enableBlur();
    }

    var response = editor.events.chainTrigger('paste.beforeCleanup', clipboard_html);
    if (typeof(response) === 'string') {
      clipboard_html = response;
    }

    // Keep only body if there is.
    if (clipboard_html.indexOf('<body') >= 0) {
      clipboard_html = clipboard_html.replace(/[.\s\S\w\W<>]*<body[^>]*>([.\s\S\w\W<>]*)<\/body>[.\s\S\w\W<>]*/g, '$1');
    }

    // Google Docs paste.
    if (clipboard_html.indexOf('id="docs-internal-guid') >= 0) {
      clipboard_html = clipboard_html.replace(/^.* id="docs-internal-guid[^>]*>(.*)<\/b>.*$/, '$1');
    }

    // Word paste.
    if (clipboard_html.match(/(class=\"?Mso|style=\"[^\"]*\bmso\-|w:WordDocument)/gi)) {
      // Strip spaces at the beginning.
      clipboard_html = clipboard_html.replace(/^\n*/g, '').replace(/^ /g, '');

      // Firefox paste.
      if (clipboard_html.indexOf('<colgroup>') === 0) {
        clipboard_html = '<table>' + clipboard_html + '</table>';
      }

      clipboard_html = _wordClean(clipboard_html);
      clipboard_html = _removeEmptyTags(clipboard_html);
    }

    // Paste.
    else {
      editor.opts.htmlAllowComments = false;
      clipboard_html = editor.clean.html(clipboard_html, editor.opts.pasteDeniedTags, editor.opts.pasteDeniedAttrs);
      editor.opts.htmlAllowComments = true;
      clipboard_html = _removeEmptyTags(clipboard_html);

      clipboard_html = clipboard_html.replace(/\r|\n|\t/g, '');

      if ($.FE.copied_text && $('<div>').html(clipboard_html).text().replace(/(\u00A0)/gi, ' ').replace(/\r|\n/gi, '') == $.FE.copied_text.replace(/(\u00A0)/gi, ' ').replace(/\r|\n/gi, '')) {
        clipboard_html = $.FE.copied_html;
      }

      clipboard_html = clipboard_html.replace(/^ */g, '').replace(/ *$/g, '');
    }

    // Do plain paste cleanup.
    if (editor.opts.pastePlain) {
      clipboard_html = _plainPasteClean(clipboard_html);
    }

    // After paste cleanup event.
    response = editor.events.chainTrigger('paste.afterCleanup', clipboard_html);
    if (typeof(response) === 'string') {
      clipboard_html = response;
    }

    // Check if there is anything to clean.
    if (clipboard_html !== '') {
      // Normalize spaces.
      var $tmp = $('<div>').html(clipboard_html);
      editor.spaces.normalize($tmp.get(0));
      $tmp.find('span').each (function () {
        if (this.attributes.length == 0) {
          $(this).replaceWith(this.innerHTML);
        }
      })

      // Remove unecessary new_lines.
      $tmp.find('br').each (function () {
        if (this.previousSibling && editor.node.isBlock(this.previousSibling)) {
          $(this).remove();
        }
      })

      clipboard_html = $tmp.html();

      // Insert HTML.
      editor.html.insert(clipboard_html, true);
    }

    _afterPaste();

    editor.undo.saveStep(snapshot);
    editor.undo.saveStep();
  }

  /**
   * After pasting.
   */
  function _afterPaste () {
    editor.events.trigger('paste.after');
  }

  /**
   * Remove possible empty tags in pasted HTML.
   */
  function _removeEmptyTags (html) {
    var i;
    var $div = $('<div>').html(html);

    // Clean empty tags.
    var empty_tags = $div.find('*:empty:not(br, img, td, th)');
    while (empty_tags.length) {
      for (i = 0; i < empty_tags.length; i++) {
        $(empty_tags[i]).remove();
      }

      empty_tags = $div.find('*:empty:not(br, img, td, th)');
    }

    // Workaround for Nodepad paste.
    var divs = $div.find('> div:not([style]), td > div, th > div, li > div');
    while (divs.length && i++ < 100) {
      var $dv = $(divs[divs.length - 1]);

      if (editor.html.defaultTag() && editor.html.defaultTag() != 'div') {
        $dv.replaceWith('<' + editor.html.defaultTag() + '>' + $dv.html() + '</' + editor.html.defaultTag() + '>' );
      }
      else {
        if (!$dv.find('*:last').is('br')) {
          $dv.replaceWith($dv.html() + '<br>');
        }
        else {
          $dv.replaceWith($dv.html());
        }
      }

      divs = $div.find('> div:not([style]), td > div, th > div, li > div');
    }

    // Remove divs.
    divs = $div.find('div:not([style])');
    while (divs.length) {
      for (i = 0; i < divs.length; i++) {
        var $el = $(divs[i]);
        var text = $el.html().replace(/\u0009/gi, '').trim();

        $el.replaceWith(text);
      }

      divs = $div.find('div:not([style])');
    }

    return $div.html();
  }

  /**
   * Initialize.
   */
  function _init () {
    editor.events.on('copy', _handleCopy);
    editor.events.on('cut', _handleCopy);
    editor.events.on('paste', _handlePaste);

    if (editor.browser.msie && editor.browser.version < 11) {
      editor.events.on('mouseup', function (e) {
        if (e.button == 2) {
          setTimeout(function () {
            stop_paste = false;
          }, 50);
          stop_paste = true;
        }
      }, true)

      editor.events.on('beforepaste', _handlePaste);
    }
  }

  return {
    _init: _init
  }
};

$.extend($.FE.DEFAULTS, {
  shortcutsEnabled: ['show', 'bold', 'italic', 'underline', 'strikeThrough', 'indent', 'outdent', 'undo', 'redo'],
  shortcutsHint: true
});

$.FE.SHORTCUTS_MAP = {};

$.FE.RegisterShortcut = function (key, cmd, val, letter, shift, option) {
  $.FE.SHORTCUTS_MAP[(shift ? '^' : '') + (option ? '@' : '') + key] = {
    cmd: cmd,
    val: val,
    letter: letter,
    shift: shift,
    option: option
  }

  $.FE.DEFAULTS.shortcutsEnabled.push(cmd);
}

$.FE.RegisterShortcut($.FE.KEYCODE.E, 'show', null, 'E', false, false);
$.FE.RegisterShortcut($.FE.KEYCODE.B, 'bold', null, 'B', false, false);
$.FE.RegisterShortcut($.FE.KEYCODE.I, 'italic', null, 'I', false, false);
$.FE.RegisterShortcut($.FE.KEYCODE.U, 'underline', null, 'U', false, false);
$.FE.RegisterShortcut($.FE.KEYCODE.S, 'strikeThrough', null, 'S', false, false);
$.FE.RegisterShortcut($.FE.KEYCODE.CLOSE_SQUARE_BRACKET, 'indent', null, ']', false, false);
$.FE.RegisterShortcut($.FE.KEYCODE.OPEN_SQUARE_BRACKET, 'outdent', null, '[', false, false);
$.FE.RegisterShortcut($.FE.KEYCODE.Z, 'undo', null, 'Z', false, false);
$.FE.RegisterShortcut($.FE.KEYCODE.Z, 'redo', null, 'Z', true, false);

$.FE.MODULES.shortcuts = function (editor) {
  var inverse_map = null;

  function get (cmd) {
    if (!editor.opts.shortcutsHint) return null;

    if (!inverse_map) {
      inverse_map = {};
      for (var key in $.FE.SHORTCUTS_MAP) {
        if ($.FE.SHORTCUTS_MAP.hasOwnProperty(key) && editor.opts.shortcutsEnabled.indexOf($.FE.SHORTCUTS_MAP[key].cmd) >= 0) {
          inverse_map[$.FE.SHORTCUTS_MAP[key].cmd + '.' + ($.FE.SHORTCUTS_MAP[key].val || '')] = {
            shift: $.FE.SHORTCUTS_MAP[key].shift,
            option: $.FE.SHORTCUTS_MAP[key].option,
            letter: $.FE.SHORTCUTS_MAP[key].letter
          }
        }
      }
    }

    var srct = inverse_map[cmd];

    if (!srct) return null;
    return (editor.helpers.isMac() ? String.fromCharCode(8984) : 'Ctrl+') + (srct.shift ? (editor.helpers.isMac() ? String.fromCharCode(8679) : 'Shift+') : '') + (srct.option ? (editor.helpers.isMac() ? String.fromCharCode(8997) : 'Alt+') : '') + srct.letter;
  }

  /**
   * Execute shortcut.
   */
  function exec (e) {
    if (!editor.core.hasFocus()) return true;

    var keycode = e.which;

    var ctrlKey = navigator.userAgent.indexOf('Mac OS X') != -1 ? e.metaKey : e.ctrlKey;

    // Build shortcuts map.
    var map_key = (e.shiftKey ? '^' : '') + (e.altKey ? '@' : '') + keycode;
    if (ctrlKey && $.FE.SHORTCUTS_MAP[map_key]) {
      var cmd = $.FE.SHORTCUTS_MAP[map_key].cmd;

      // Check if shortcut is enabled.
      if (cmd && editor.opts.shortcutsEnabled.indexOf(cmd) >= 0) {
        var val = $.FE.SHORTCUTS_MAP[map_key].val;

        // Search for button.
        var $btn;
        if (cmd && !val) {
          $btn = editor.$tb.find('.fr-command[data-cmd="' + cmd + '"]');
        }
        else if (cmd && val) {
          $btn = editor.$tb.find('.fr-command[data-cmd="' + cmd + '"][data-param1="' + val + '"]');
        }

        // Button found.
        if ($btn.length) {
          e.preventDefault();
          e.stopPropagation();

          $btn.parents('.fr-toolbar').data('instance', editor);

          if (e.type == 'keydown') {
            editor.button.exec($btn);
          }

          return false;
        }

        // Search for command.
        else if (cmd && editor.commands[cmd]) {
          e.preventDefault();
          e.stopPropagation();

          if (e.type == 'keydown') {
            editor.commands[cmd]();
          }

          return false;
        }
      }
    }
  }

  /**
   * Initialize.
   */
  function _init () {
    editor.events.on('keydown', exec, true);
    editor.events.on('keyup', exec, true);
  }

  return {
    _init: _init,
    get: get
  }
}

$.FE.MODULES.snapshot = function (editor) {
  /**
   * Get the index of a node inside it's parent.
   */
  function _getNodeIndex (node) {
    var childNodes = node.parentNode.childNodes;
    var idx = 0;
    var prevNode = null;
    for (var i = 0; i < childNodes.length; i++) {
      if (prevNode) {
        // Current node is text and it is empty.
        var isEmptyText = (childNodes[i].nodeType === Node.TEXT_NODE && childNodes[i].textContent === '');

        // Previous node is text, current node is text.
        var twoTexts = (prevNode.nodeType === Node.TEXT_NODE && childNodes[i].nodeType === Node.TEXT_NODE);

        if (!isEmptyText && !twoTexts) idx++;
      }

      if (childNodes[i] == node) return idx;

      prevNode = childNodes[i];
    }
  }

  /**
   * Determine the location of the node inside the element.
   */
  function _getNodeLocation (node) {
    var loc = [];
    if (!node.parentNode) return [];
    while (!editor.node.isElement(node)) {
      loc.push(_getNodeIndex(node));
      node = node.parentNode;
    }

    return loc.reverse();
  }

  /**
   * Get the range offset inside the node.
   */
  function _getRealNodeOffset (node, offset) {
    while (node && node.nodeType === Node.TEXT_NODE) {
      var prevNode = node.previousSibling;
      if (prevNode && prevNode.nodeType == Node.TEXT_NODE) {
        offset += prevNode.textContent.length;
      }
      node = prevNode;
    }

    return offset;
  }

  /**
   * Codify each range.
   */
  function _getRange (range) {
    return {
      scLoc: _getNodeLocation(range.startContainer),
      scOffset: _getRealNodeOffset(range.startContainer, range.startOffset),
      ecLoc: _getNodeLocation(range.endContainer),
      ecOffset: _getRealNodeOffset(range.endContainer, range.endOffset)
    }
  }

  /**
   * Get the current snapshot.
   */
  function get () {
    var snapshot = {};

    editor.events.trigger('snapshot.before');

    snapshot.html = editor.$wp ? editor.$el.html() : editor.$oel.get(0).outerHTML;

    snapshot.ranges = [];
    if (editor.$wp && editor.selection.inEditor() && editor.core.hasFocus()) {
      var ranges = editor.selection.ranges();
      for (var i = 0; i < ranges.length; i++) {
        snapshot.ranges.push(_getRange(ranges[i]));
      }
    }

    editor.events.trigger('snapshot.after');

    return snapshot;
  }

  /**
   * Determine node by its location in the main element.
   */
  function _getNodeByLocation (loc) {
    var node = editor.$el.get(0);
    for (var i = 0; i < loc.length; i++) {
      node = node.childNodes[loc[i]];
    }

    return node;
  }

  /**
   * Restore range from snapshot.
   */
  function _restoreRange (sel, range_snapshot) {
    try {
      // Get range info.
      var startNode = _getNodeByLocation(range_snapshot.scLoc);
      var startOffset = range_snapshot.scOffset;
      var endNode = _getNodeByLocation(range_snapshot.ecLoc);
      var endOffset = range_snapshot.ecOffset;

      // Restore range.
      var range = editor.doc.createRange();
      range.setStart(startNode, startOffset);
      range.setEnd(endNode, endOffset);

      sel.addRange(range);
    }
    catch (ex) {
      console.warn (ex)
    }
  }

  /**
   * Restore snapshot.
   */
  function restore (snapshot) {
    // Restore HTML.
    if (editor.$el.html() != snapshot.html) editor.$el.html(snapshot.html);

    // Get selection.
    var sel = editor.selection.get();

    // Make sure to clear current selection.
    editor.selection.clear();

    // Focus.
    editor.events.focus(true);

    // Restore Ranges.
    for (var i = 0; i < snapshot.ranges.length; i++) {
      _restoreRange(sel, snapshot.ranges[i]);
    }
  }

  /**
   * Compare two snapshots.
   */
  function equal (s1, s2) {
    if (s1.html != s2.html) return false;
    if (editor.core.hasFocus() && JSON.stringify(s1.ranges) != JSON.stringify(s2.ranges)) return false;

    return true;
  }

  return {
    get: get,
    restore: restore,
    equal: equal
  }
};

$.FE.MODULES.undo = function (editor) {
  /**
   * Disable the default browser undo.
   */
  function _disableBrowserUndo (e) {
    var keyCode = e.which;
    var ctrlKey = editor.keys.ctrlKey(e);

    // Ctrl Key.
    if (ctrlKey) {
      if (keyCode == 90 && e.shiftKey) {
        e.preventDefault();
      }

      if (keyCode == 90) {
        e.preventDefault();
      }
    }
  }

  function canDo () {
    if (editor.undo_stack.length === 0 || editor.undo_index <= 1) {
      return false;
    }

    return true;
  }

  function canRedo () {
    if (editor.undo_index == editor.undo_stack.length) {
      return false;
    }

    return true;
  }

  var last_html = null;
  function saveStep (snapshot) {
    if (!editor.undo_stack || editor.undoing || editor.$el.get(0).querySelectorAll('.fr-marker').length) return false;

    if (typeof snapshot == 'undefined') {
      snapshot = editor.snapshot.get();

      if (!editor.undo_stack[editor.undo_index - 1] || !editor.snapshot.equal(editor.undo_stack[editor.undo_index - 1], snapshot)) {
        dropRedo();
        editor.undo_stack.push(snapshot);
        editor.undo_index++;

        if (snapshot.html != last_html) {
          editor.events.trigger('contentChanged');
          last_html = snapshot.html;
        }
      }
    }
    else {
      dropRedo();
      if (editor.undo_index > 0) {
        editor.undo_stack[editor.undo_index - 1] = snapshot;
      }
      else {
        editor.undo_stack.push(snapshot);
        editor.undo_index++;
      }
    }
  }

  function dropRedo () {
    if (!editor.undo_stack || editor.undoing) return false;

    while (editor.undo_stack.length > editor.undo_index) {
      editor.undo_stack.pop();
    }
  }

  function _do () {
    if (editor.undo_index > 1) {
      editor.undoing = true;

      // Get snapshot.
      var snapshot = editor.undo_stack[--editor.undo_index - 1];

      // Clear any existing content changed timers.
      clearTimeout(editor._content_changed_timer);

      // Restore snapshot.
      editor.snapshot.restore(snapshot);

      // Hide popups.
      editor.popups.hideAll();

      // Enable toolbar.
      editor.toolbar.enable();

      // Call content changed.
      editor.events.trigger('contentChanged');

      editor.events.trigger('commands.undo');

      editor.undoing = false;
    }
  }

  function _redo () {
    if (editor.undo_index < editor.undo_stack.length) {
      editor.undoing = true;

      // Get snapshot.
      var snapshot = editor.undo_stack[editor.undo_index++];

      // Clear any existing content changed timers.
      clearTimeout(editor._content_changed_timer)

      // Restore snapshot.
      editor.snapshot.restore(snapshot);

      // Hide popups.
      editor.popups.hideAll();

      // Enable toolbar.
      editor.toolbar.enable();

      // Call content changed.
      editor.events.trigger('contentChanged');

      editor.events.trigger('commands.redo');

      editor.undoing = false;
    }
  }

  function reset () {
    editor.undo_index = 0;
    editor.undo_stack = [];
  }

  function _destroy () {
    editor.undo_stack = [];
  }

  /**
   * Initialize
   */
  function _init () {
    reset();
    editor.events.on('initialized', function () {
      last_html = editor.html.get(false, true);
    });

    editor.events.on('blur', function () {
      editor.undo.saveStep();
    })

    editor.events.on('keydown', _disableBrowserUndo);

    editor.events.on('destroy', _destroy);
  }

  return {
    _init: _init,
    run: _do,
    redo: _redo,
    canDo: canDo,
    canRedo: canRedo,
    dropRedo: dropRedo,
    reset: reset,
    saveStep: saveStep
  }
};

$.FE.ICON_DEFAULT_TEMPLATE = 'font_awesome';

$.FE.ICON_TEMPLATES = {
  font_awesome: '<i class="fa fa-[NAME]"></i>',
  text: '<span style="text-align: center;">[NAME]</span>',
  image: '<img src=[SRC] alt=[ALT] />'
}

$.FE.ICONS = {
  bold: {NAME: 'bold'},
  italic: {NAME: 'italic'},
  underline: {NAME: 'underline'},
  strikeThrough: {NAME: 'strikethrough'},
  subscript: {NAME: 'subscript'},
  superscript: {NAME: 'superscript'},
  color: {NAME: 'tint'},
  outdent: {NAME: 'outdent'},
  indent: {NAME: 'indent'},
  undo: {NAME: 'rotate-left'},
  redo: {NAME: 'rotate-right'},
  insertHR: {NAME: 'minus'},
  clearFormatting: {NAME: 'eraser'},
  selectAll: {NAME: 'mouse-pointer'}
}

$.FE.DefineIconTemplate = function (name, options) {
  $.FE.ICON_TEMPLATES[name] = options;
}

$.FE.DefineIcon = function (name, options) {
  $.FE.ICONS[name] = options;
}

$.FE.MODULES.icon = function (editor) {
  function create (command) {
    var icon = null;
    var info = $.FE.ICONS[command];
    if (typeof info != 'undefined') {
      var template = info.template || $.FE.ICON_DEFAULT_TEMPLATE;
      if (template && (template = $.FE.ICON_TEMPLATES[template])) {
        icon = template.replace(/\[([a-zA-Z]*)\]/g, function (str, a1) {
          return (a1 == 'NAME' ? (info[a1] || command) : info[a1]);
        });
      }
    }

    return (icon || command);
  }

  return {
    create: create
  }
};

$.FE.MODULES.tooltip = function (editor) {
  function hide () {
    // Position fixed for: https://github.com/froala/wysiwyg-editor/issues/1247.
    if (editor.$tooltip) editor.$tooltip.removeClass('fr-visible').css('left', '-3000px').css('position', 'fixed');
  }

  function to ($el, above) {
    if (!$el.data('title')) {
      $el.data('title', $el.attr('title'));
    }

    if (!$el.data('title')) return false;

    if (!editor.$tooltip) _init();

    $el.removeAttr('title');
    editor.$tooltip.text($el.data('title'));
    editor.$tooltip.addClass('fr-visible');

    var left = $el.offset().left + ($el.outerWidth() - editor.$tooltip.outerWidth()) / 2;

    // Normalize screen position.
    if (left < 0) left = 0;
    if (left + editor.$tooltip.outerWidth() > $(editor.o_win).width()) {
      left = $(editor.o_win).width() - editor.$tooltip.outerWidth();
    }

    if (typeof above == 'undefined') above = editor.opts.toolbarBottom;
    var top = !above ? $el.offset().top + $el.outerHeight() : $el.offset().top - editor.$tooltip.height();

    editor.$tooltip.css('position', '');
    editor.$tooltip.css('left', left);
    editor.$tooltip.css('top', top);
  }

  function bind ($el, selector, above) {
    if (!editor.helpers.isMobile()) {
      editor.events.$on($el, 'mouseenter', selector, function (e) {
        if (!$(e.currentTarget).hasClass('fr-disabled') && !editor.edit.isDisabled()) {
          to($(e.currentTarget), above);
        }
      }, true);

      editor.events.$on($el, 'mouseleave ' + editor._mousedown + ' ' + editor._mouseup, selector, function (e) {
        hide();
      }, true);
    }
  }

  function _init () {
    if (!editor.helpers.isMobile()) {
      if (!editor.shared.$tooltip) {
        editor.shared.$tooltip = $('<div class="fr-tooltip"></div>');

        editor.$tooltip = editor.shared.$tooltip;

        if (editor.opts.theme) {
          editor.$tooltip.addClass(editor.opts.theme + '-theme');
        }

        $(editor.o_doc).find('body').append(editor.$tooltip);
      }
      else {
        editor.$tooltip = editor.shared.$tooltip;
      }

      editor.events.on('shared.destroy', function () {
        editor.$tooltip.html('').removeData().remove();
        editor.$tooltip = null;
      }, true);
    }
  }

  return {
    hide: hide,
    to: to,
    bind: bind
  }
};

$.FE.MODULES.button = function (editor) {
  var buttons = [];

  if (editor.opts.toolbarInline || editor.opts.toolbarContainer) {
    if (!editor.shared.buttons) editor.shared.buttons = [];
    buttons = editor.shared.buttons;
  }

  var popup_buttons = [];
  if (!editor.shared.popup_buttons) editor.shared.popup_buttons = [];
  popup_buttons = editor.shared.popup_buttons;

  /**
   * Click was made on a dropdown button.
   */
  function _dropdownButtonClick (e) {
    // Get current btn and dropdown.
    var $btn = $(e.currentTarget);
    var $dropdown = $btn.next();

    var active = $btn.hasClass('fr-active');
    var mobile = editor.helpers.isMobile();

    var $active_dropdowns = $('.fr-dropdown.fr-active').not($btn);

    var inst = $btn.parents('.fr-toolbar, .fr-popup').data('instance') || editor;

    // Hide keyboard. We need the entire space.
    if (inst.helpers.isIOS() && inst.$el.get(0).querySelectorAll('.fr-marker').length == 0) {
      inst.selection.save();
      inst.selection.clear();
      inst.selection.restore();
    }

    // Dropdown is not active.
    if (!active) {
      // Call refresh on show.
      var cmd = $btn.data('cmd');
      $dropdown.find('.fr-command').removeClass('fr-active');
      if ($.FE.COMMANDS[cmd] && $.FE.COMMANDS[cmd].refreshOnShow) {
        $.FE.COMMANDS[cmd].refreshOnShow.apply(inst, [$btn, $dropdown]);
      }

      $dropdown.css('left', $btn.offset().left - $btn.parent().offset().left - (editor.opts.direction == 'rtl' ? $dropdown.width() - $btn.outerWidth() : 0));

      if (!editor.opts.toolbarBottom) {
        $dropdown.css('top', $btn.position().top + $btn.outerHeight());
      }
      else {
        $dropdown.css('bottom', editor.$tb.height() - $btn.position().top);
      }
    }

    // Blink and activate.
    $btn.addClass('fr-blink').toggleClass('fr-active');
    setTimeout (function () {
      $btn.removeClass('fr-blink');
    }, 300);

    // Check if it exceeds window on the right.
    if ($dropdown.offset().left + $dropdown.outerWidth() > $(editor.opts.scrollableContainer).offset().left +  $(editor.opts.scrollableContainer).outerWidth()) {
      $dropdown.css('margin-left', -($dropdown.offset().left + $dropdown.outerWidth() - $(editor.opts.scrollableContainer).offset().left - $(editor.opts.scrollableContainer).outerWidth()))
    }

    // Hide dropdowns that might be active.
    $active_dropdowns.removeClass('fr-active');
    $active_dropdowns.parent('.fr-toolbar:not(.fr-inline)').css('zIndex', '');

    if ($btn.parents('.fr-popup').length == 0 && !editor.opts.toolbarInline) {
      if ($btn.hasClass('fr-active')) {
        editor.$tb.css('zIndex', (editor.opts.zIndex || 1) + 4);
      }
      else {
        editor.$tb.css('zIndex', '');
      }
    }
  }

  function exec ($btn) {
    // Blink.
    $btn.addClass('fr-blink');
    setTimeout (function () {
      $btn.removeClass('fr-blink');
    }, 500);

    // Get command, value and additional params.
    var cmd = $btn.data('cmd');
    var params = [];
    while (typeof $btn.data('param' + (params.length + 1)) != 'undefined') {
      params.push($btn.data('param' + (params.length + 1)));
    }

    // Hide dropdowns that might be active including the current one.
    var $active_dropdowns = $('.fr-dropdown.fr-active');
    if ($active_dropdowns.length) {
      $active_dropdowns.removeClass('fr-active');

      $active_dropdowns.parent('.fr-toolbar:not(.fr-inline)').css('zIndex', '');
    }

    // Call the command.
    $btn.parents('.fr-popup, .fr-toolbar').data('instance').commands.exec(cmd, params);
  }

  /**
   * Click was made on a command button.
   */
  function _commandButtonClick (e) {
    var $btn = $(e.currentTarget);
    exec($btn);
  }

  function _click (e) {
    var $btn = $(e.currentTarget);

    var inst = $btn.parents('.fr-popup, .fr-toolbar').data('instance');

    if ($btn.parents('.fr-popup').length == 0 && !$btn.data('popup')) {
      inst.popups.hideAll();
    }

    // Popups are visible, but not in the current instance.
    if (inst.popups.areVisible() && !inst.popups.areVisible(inst)) {
      // Hide markers in other instances.
      for (var i = 0; i < $.FE.INSTANCES.length; i++) {
        if ($.FE.INSTANCES[i] != inst && $.FE.INSTANCES[i].popups && $.FE.INSTANCES[i].popups.areVisible()) {
          $.FE.INSTANCES[i].$el.find('.fr-marker').remove();
        }
      }

      inst.popups.hideAll();
    }

    // Dropdown button.
    if ($btn.hasClass('fr-dropdown')) {
      _dropdownButtonClick(e);
    }

    // Regular button.
    else {
      _commandButtonClick(e);

      if ($.FE.COMMANDS[$btn.data('cmd')] && $.FE.COMMANDS[$btn.data('cmd')].refreshAfterCallback != false) {
        inst.button.bulkRefresh();
      }
    }
  }

  function _hideActiveDropdowns ($el) {
    var $active_dropdowns = $el.find('.fr-dropdown.fr-active');

    if ($active_dropdowns.length) {
      $active_dropdowns.removeClass('fr-active');

      $active_dropdowns.parent('.fr-toolbar:not(.fr-inline)').css('zIndex', '');
    }
  }

  /**
   * Click in the dropdown menu.
   */
  function _dropdownMenuClick (e) {
    e.preventDefault();
    e.stopPropagation();
  }

  /**
   * Click on the dropdown wrapper.
   */
  function _dropdownWrapperClick (e) {
    e.stopPropagation();

    // Prevent blurring.
    if (!editor.helpers.isMobile()) {
      return false;
    }
  }

  /**
   * Bind callbacks for commands.
   */
  function bindCommands ($el, tooltipAbove) {
    editor.events.bindClick($el, '.fr-command:not(.fr-disabled)', _click);

    // Click on the dropdown menu.
    editor.events.$on($el, editor._mousedown + ' ' + editor._mouseup + ' ' + editor._move, '.fr-dropdown-menu', _dropdownMenuClick, true);

    // Click on the dropdown wrapper.
    editor.events.$on($el, editor._mousedown + ' ' + editor._mouseup + ' ' + editor._move, '.fr-dropdown-menu .fr-dropdown-wrapper', _dropdownWrapperClick, true);

    // Hide dropdowns that might be active.
    var _document = $el.get(0).ownerDocument;
    var _window = 'defaultView' in _document ? _document.defaultView : _document.parentWindow;
    var hideDropdowns = function (e) {
      if (!e || (e.type == editor._mouseup && e.target != $('html').get(0)) || (e.type == 'keydown' && ((editor.keys.isCharacter(e.which) && !editor.keys.ctrlKey(e)) || e.which == $.FE.KEYCODE.ESC))) {
        _hideActiveDropdowns($el);
      }
    }
    editor.events.$on($(_window), editor._mouseup + ' resize keydown', hideDropdowns, true);

    if (editor.opts.iframe) {
      editor.events.$on(editor.$win, editor._mouseup, hideDropdowns, true);
    }

    // Add refresh.
    if ($el.hasClass('fr-popup')) {
      $.merge(popup_buttons, $el.find('.fr-btn').toArray());
    }
    else {
      $.merge(buttons, $el.find('.fr-btn').toArray());
    }

    // Assing tooltips to buttons.
    editor.tooltip.bind($el, '.fr-btn, .fr-title', tooltipAbove);
  }

  /**
   * Create the content for dropdown.
   */
  function _content (command, info) {
    var c = '';

    if (info.html) {
      if (typeof info.html == 'function') {
        c += info.html.call(editor);
      }
      else {
        c += info.html;
      }
    }
    else {
      var options = info.options;
      if (typeof options == 'function') options = options();

      c += '<ul class="fr-dropdown-list">';
      for (var val in options) {
        if (options.hasOwnProperty(val)) {
          var shortcut = editor.shortcuts.get(command + '.' + val);
          if (shortcut) {
            shortcut = '<span class="fr-shortcut">' + shortcut + '</span>';
          }
          else {
            shortcut = '';
          }

          c += '<li><a class="fr-command" data-cmd="' + command + '" data-param1="' + val + '" title="' + options[val] + '">' + editor.language.translate(options[val]) + '</a></li>';
        }
      }
      c += '</ul>';
    }

    return c;
  }

  /**
   * Create button.
   */
  function _build (command, info, visible) {
    var display_selection = info.displaySelection;
    if (typeof display_selection == 'function') {
      display_selection = display_selection(editor);
    }

    var icon;
    if (display_selection) {
      var default_selection = (typeof info.defaultSelection == 'function' ? info.defaultSelection(editor) : info.defaultSelection);
      icon = '<span style="width:' + (info.displaySelectionWidth || 100) + 'px">' + (default_selection || editor.language.translate(info.title)) + '</span>';
    }
    else {
      icon = editor.icon.create(info.icon || command)
    }

    var popup = info.popup ? ' data-popup="true"' : '';

    var shortcut = editor.shortcuts.get(command + '.');
    if (shortcut) {
      shortcut = ' (' + shortcut + ')';
    }
    else {
      shortcut = '';
    }

    var btn = '<button type="button" tabindex="-1" aria-label="' + (editor.language.translate(info.title) || '') + '" title="' + (editor.language.translate(info.title) || '') + shortcut + '" class="fr-command fr-btn' + (info.type == 'dropdown' ? ' fr-dropdown' : '') + (info.displaySelection ? ' fr-selection' : '') + (info.back ? ' fr-back' : '') + (info.disabled ? ' fr-disabled' : '') + (!visible ? ' fr-hidden' : '') + '" data-cmd="' + command + '"' + popup + '>' + icon + '</button>';

    if (info.type == 'dropdown') {
      // Build dropdown.
      var dropdown = '<div class="fr-dropdown-menu"><div class="fr-dropdown-wrapper"><div class="fr-dropdown-content">';

      dropdown += _content(command, info);

      dropdown += '</div></div></div>';

      btn += dropdown;
    }

    return btn;
  }

  function buildList (buttons, visible_buttons) {
    var str = '';
    for (var i = 0; i < buttons.length; i++) {
      var cmd_name = buttons[i];
      var cmd_info = $.FE.COMMANDS[cmd_name];

      if (cmd_info && typeof cmd_info.plugin !== 'undefined' && editor.opts.pluginsEnabled.indexOf(cmd_info.plugin) < 0) continue;

      if (cmd_info) {
        var visible = typeof visible_buttons != 'undefined' ? visible_buttons.indexOf(cmd_name) >= 0 : true;
        str += _build(cmd_name, cmd_info, visible);
      }
      else if (cmd_name == '|') {
        str += '<div class="fr-separator fr-vs"></div>';
      }
      else if (cmd_name == '-') {
        str += '<div class="fr-separator fr-hs"></div>';
      }
    }

    return str;
  }

  function refresh ($btn) {
    var inst = $btn.parents('.fr-popup, .fr-toolbar').data('instance') || editor;

    var cmd = $btn.data('cmd');

    var $dropdown;
    if (!$btn.hasClass('fr-dropdown')) $btn.removeClass('fr-active');
    else $dropdown = $btn.next();

    if ($.FE.COMMANDS[cmd] && $.FE.COMMANDS[cmd].refresh) {
     $.FE.COMMANDS[cmd].refresh.apply(inst, [$btn, $dropdown]);
    }
    else if (editor.refresh[cmd]) {
      inst.refresh[cmd]($btn, $dropdown);
    }
  }

  function _bulkRefresh (btns) {
    var inst = editor.$tb ? (editor.$tb.data('instance') || editor) : editor;

    // Check the refresh event.
    if (editor.events.trigger('buttons.refresh') == false) return true;

    setTimeout(function () {
      var focused = (inst.selection.inEditor() && inst.core.hasFocus());

      for (var i = 0; i < btns.length; i++) {
        var $btn = $(btns[i]);
        var cmd = $btn.data('cmd');
        if ($btn.parents('.fr-popup').length == 0) {
          if (focused || ($.FE.COMMANDS[cmd] && $.FE.COMMANDS[cmd].forcedRefresh)) {
            inst.button.refresh($btn);
          }
          else {
            if (!$btn.hasClass('fr-dropdown')) $btn.removeClass('fr-active');
          }
        }
        else if ($btn.parents('.fr-popup').is(':visible')) {
          inst.button.refresh($btn);
        }
      }
    }, 0);
  }

  /**
   * Do buttons refresh.
   */
  function bulkRefresh () {
    _bulkRefresh(buttons);
    _bulkRefresh(popup_buttons);
  }

  function _destroy () {
    buttons = [];
    popup_buttons = [];
  }

  /**
   * Initialize.
   */
  function _init () {
    // Assign refresh and do refresh.
    if (editor.opts.toolbarInline) {
      editor.events.on('toolbar.show', bulkRefresh);
    }
    else {
      editor.events.on('mouseup', bulkRefresh);
      editor.events.on('keyup', bulkRefresh);
      editor.events.on('blur', bulkRefresh);
      editor.events.on('focus', bulkRefresh);
      editor.events.on('contentChanged', bulkRefresh);
    }

    editor.events.on('shared.destroy', _destroy);
  }

  return {
    _init: _init,
    buildList: buildList,
    bindCommands: bindCommands,
    refresh: refresh,
    bulkRefresh: bulkRefresh,
    exec: exec
  }
};

$.FE.POPUP_TEMPLATES = {
  'text.edit': '[_EDIT_]'
};

$.FE.RegisterTemplate = function (name, template) {
  $.FE.POPUP_TEMPLATES[name] = template;
}

$.FE.MODULES.popups = function (editor) {
  if (!editor.shared.popups) editor.shared.popups = {};
  var popups = editor.shared.popups;

  function setContainer(id, $container) {
    if (!$container.is(':visible')) $container = $(editor.opts.scrollableContainer);

    if (!$container.is(popups[id].data('container'))) {
      popups[id].data('container', $container);
      $container.append(popups[id]);
    }
  }

  /**
   * Show popup at a specific position.
   */
  function show (id, left, top, obj_height) {
    // Restore selection on show if it is there.
    if (areVisible() && editor.$el.find('.fr-marker').length > 0) {
      editor.events.disableBlur();
      editor.selection.restore();
    }

    hideAll([id]);

    if (!popups[id]) return false;

    // Hide active dropdowns.
    $('.fr-dropdown.fr-active').removeClass('fr-active').parent('.fr-toolbar').css('zIndex', '');

    // Set the current instance for the popup.
    popups[id].data('instance', editor);
    if (editor.$tb) editor.$tb.data('instance', editor);

    var width = popups[id].outerWidth();
    var height = popups[id].outerHeight();
    var is_visible = isVisible(id);
    popups[id].addClass('fr-active').removeClass('fr-hidden').find('input, textarea').removeAttr('disabled');

    var $container = popups[id].data('container');

    // If container is toolbar then increase zindex.
    if ($container.is(editor.$tb)) editor.$tb.css('zIndex', (editor.opts.zIndex || 1) + 4);

    // Inline mode when container is toolbar.
    if (editor.opts.toolbarInline && $container && editor.$tb && $container.get(0) == editor.$tb.get(0)) {
      setContainer(id, $(editor.opts.scrollableContainer));
      top = editor.$tb.offset().top - editor.helpers.getPX(editor.$tb.css('margin-top'));
      left = editor.$tb.offset().left + editor.$tb.outerWidth() / 2 + (parseFloat(editor.$tb.find('.fr-arrow').css('margin-left')) || 0) + editor.$tb.find('.fr-arrow').outerWidth() / 2;

      if (editor.$tb.hasClass('fr-above') && top) {
        top += editor.$tb.outerHeight();
      }

      obj_height = 0;
    }

    // Apply iframe correction.
    $container = popups[id].data('container');
    if (editor.opts.iframe && !obj_height && !is_visible) {
      if (left) left -= editor.$iframe.offset().left;
      if (top) top -= editor.$iframe.offset().top;
    }

    // Apply left correction.
    if (left) left = left - width / 2;

    // Toolbar at the bottom and container is toolbar.
    if (editor.opts.toolbarBottom && $container && editor.$tb && $container.get(0) == editor.$tb.get(0)) {
      popups[id].addClass('fr-above');
      if (top) top = top - popups[id].outerHeight();
    }

    // Position editor.
    popups[id].removeClass('fr-active');
    editor.position.at(left, top, popups[id], obj_height || 0);
    popups[id].addClass('fr-active');

    // Focus in the first field.
    var active_popup = popups[id].find('input:visible, textarea:visible').get(0);
    if (active_popup) {
      // Save selection if necessary.

      if (editor.$el.find('.fr-marker').length == 0 && editor.core.hasFocus()) {
        editor.selection.save();
      }

      editor.events.disableBlur();
      $(active_popup).select().focus();
    }

    if (editor.opts.toolbarInline) editor.toolbar.hide();

    editor.events.trigger('popups.show.' + id);

    // https://github.com/froala/wysiwyg-editor/issues/1248
    _events(id)._repositionPopup();
  }

  function onShow (id, callback) {
    editor.events.on('popups.show.' + id, callback);
  }

  /**
   * Find visible popup.
   */
  function isVisible (id) {
    return (popups[id] && popups[id].hasClass('fr-active') && editor.core.sameInstance(popups[id])) || false;
  }

  /**
   * Check if there is any popup visible.
   */
  function areVisible (new_instance) {
    for (var id in popups) {
      if (popups.hasOwnProperty(id)) {
        if (isVisible(id) && (typeof new_instance == 'undefined' || popups[id].data('instance') == new_instance)) return true;
      }
    }

    return false;
  }

  /**
   * Hide popup.
   */
  function hide (id) {
    if (popups[id] && popups[id].hasClass('fr-active')) {
      popups[id].removeClass('fr-active fr-above');
      editor.events.trigger('popups.hide.' + id);

      // Reset toolbar zIndex.
      if (editor.$tb) {
        if (editor.opts.zIndex > 1) {
          editor.$tb.css('zIndex', editor.opts.zIndex + 1);
        }
        else {
          editor.$tb.css('zIndex', '');
        }
      }

      editor.events.disableBlur();
      popups[id].find('input, textarea, button').filter(':focus').blur();
      popups[id].find('input, textarea').attr('disabled', 'disabled');
    }
  }

  /**
   * Assign an event for hiding.
   */
  function onHide (id, callback) {
    editor.events.on('popups.hide.' + id, callback);
  }

  /**
   * Get the popup with the specific id.
   */
  function get (id) {
    var $popup = popups[id];

    if ($popup && !$popup.data('inst' + editor.id)) {
      var ev = _events(id);
      _bindInstanceEvents(ev, id);
    }

    return $popup;
  }

  function onRefresh (id, callback) {
    editor.events.on('popups.refresh.' + id, callback);
  }

  /**
   * Refresh content inside the popup.
   */
  function refresh (id) {
    editor.events.trigger('popups.refresh.' + id);

    var btns = popups[id].find('.fr-command');
    for (var i = 0; i < btns.length; i++) {
      var $btn = $(btns[i]);
      if ($btn.parents('.fr-dropdown-menu').length == 0) {
        editor.button.refresh($btn);
      }
    }
  }

  /**
   * Hide all popups.
   */
  function hideAll (except) {
    if (typeof except == 'undefined') except = [];

    for (var id in popups) {
      if (popups.hasOwnProperty(id)) {
        if (except.indexOf(id) < 0) {
          hide(id);
        }
      }
    }
  }

  editor.shared.exit_flag = false;
  function _markExit () {
    editor.shared.exit_flag = true;
  }

  function _unmarkExit () {
    editor.shared.exit_flag = false;
  }

  function _canExit () {
    return editor.shared.exit_flag;
  }

  function _buildTemplate (id, template) {
    // Load template.
    var html = $.FE.POPUP_TEMPLATES[id];
    if (typeof html == 'function') html = html.apply(editor);

    for (var nm in template) {
      if (template.hasOwnProperty(nm)) {
        html = html.replace('[_' + nm.toUpperCase() + '_]', template[nm]);
      }
    }

    return html;
  }

  function _build (id, template) {
    var html = _buildTemplate(id, template);

    var $popup = $('<div class="fr-popup' + (editor.helpers.isMobile() ? ' fr-mobile' : ' fr-desktop') +  (editor.opts.toolbarInline ? ' fr-inline' : '') + '"><span class="fr-arrow"></span>' + html + '</div>');

    if (editor.opts.theme) {
      $popup.addClass(editor.opts.theme + '-theme');
    }

    if (editor.opts.zIndex > 1) {
      editor.$tb.css('z-index', editor.opts.zIndex + 2);
    }

    if (editor.opts.direction != 'auto') {
      $popup.removeClass('fr-ltr fr-rtl').addClass('fr-' + editor.opts.direction);
    }

    $popup.find('input, textarea').attr('dir', editor.opts.direction).attr('disabled', 'disabled');

    var $container = $('body');
    $container.append($popup);
    $popup.data('container', $container);

    popups[id] = $popup;

    // Bind commands from the popup.
    editor.button.bindCommands($popup, false);

    return $popup;
  }

  function _events (id) {
    var $popup = popups[id];

    return {
      /**
       * Resize window.
       */
      _windowResize: function () {
        var inst = $popup.data('instance') || editor;

        if (!inst.helpers.isMobile() && $popup.is(':visible')) {
          inst.events.disableBlur();
          inst.popups.hide(id);
          inst.events.enableBlur();
        }
      },

      /**
       * Focus on an input.
       */
      _inputFocus: function (e) {
        var inst = $popup.data('instance') || editor;

        e.preventDefault();

        // IE workaround.
        setTimeout(function () {
          inst.events.enableBlur();
        }, 0);

        // Reposition scroll on mobile to the original one.
        if (inst.helpers.isMobile()) {
          var t = $(inst.o_win).scrollTop();
          setTimeout(function () {
            $(inst.o_win).scrollTop(t);
          }, 0);
        }
      },

      /**
       * Blur on an input.
       */
      _inputBlur: function (e) {
        var inst = $popup.data('instance') || editor;

        // Do not do blur on window change.
        if (document.activeElement != this && $(this).is(':visible')) {
          if (inst.events.blurActive()) {
            inst.events.trigger('blur');
          }

          inst.events.enableBlur();
        }
      },

      /**
       * Keydown on an input.
       */
      _inputKeydown: function (e) {
        var inst = $popup.data('instance') || editor;
        var key_code = e.which;

        // Tabbing.
        if ($.FE.KEYCODE.TAB == key_code) {
          e.preventDefault();

          var inputs = $popup.find('input, textarea, button, select').filter(':visible').not(':disabled').toArray();
          inputs.sort(function (a, b) {
            if (e.shiftKey) return $(a).attr('tabIndex') < $(b).attr('tabIndex');
            return $(a).attr('tabIndex') > $(b).attr('tabIndex');
          });

          inst.events.disableBlur();
          var idx = inputs.indexOf(this) + 1;
          if (idx == inputs.length) idx = 0;
          $(inputs[idx]).focus();
        }

        // ENTER.
        else if ($.FE.KEYCODE.ENTER == key_code) {
          if ($popup.find('.fr-submit:visible').length > 0) {
            e.preventDefault();
            e.stopPropagation();
            inst.events.disableBlur();
            inst.button.exec($popup.find('.fr-submit:visible:first'));
          }
        }

        // ESC.
        else if ($.FE.KEYCODE.ESC == key_code) {
          e.preventDefault();
          e.stopPropagation();

          if (inst.$el.find('.fr-marker')) {
            inst.events.disableBlur();
            $(this).data('skip', true);
            inst.selection.restore();
            inst.events.enableBlur();
          }

          if (isVisible(id) && $popup.find('.fr-back:visible').length) {
            inst.button.exec($popup.find('.fr-back:visible:first'))
          }
          else {
            inst.popups.hide(id);
          }

          if (inst.opts.toolbarInline) inst.toolbar.showInline(null, true);

          return false;
        }

        // Other KEY. Stop propagation to the window.
        else {
          e.stopPropagation();
        }
      },

      /**
       * Window keydown.
       */
      _windowKeydown: function (e) {
        if (!editor.core.sameInstance($popup)) return true;

        var inst = $popup.data('instance') || editor;

        var key_code = e.which;

        // ESC.
        if ($.FE.KEYCODE.ESC == key_code) {
          if (isVisible(id) && inst.opts.toolbarInline) {
            e.stopPropagation();

            if (isVisible(id) && $popup.find('.fr-back:visible').length) {
              inst.button.exec($popup.find('.fr-back:visible:first'))
            }
            else {
              inst.popups.hide(id);
              inst.toolbar.showInline(null, true);
            }
            return false;
          }
          else {
            if (isVisible(id) && $popup.find('.fr-back:visible').length) {
              inst.button.exec($popup.find('.fr-back:visible:first'))
            }
            else {
              inst.popups.hide(id);
            }
          }
        }
      },

      /**
       * Editor keydown.
       */
      _editorKeydown: function (e) {
        var inst = $popup.data('instance') || editor;

        // ESC.
        if (!inst.keys.ctrlKey(e) && e.which != $.FE.KEYCODE.ESC) {
          if (isVisible(id) && $popup.find('.fr-back:visible').length) {
            inst.button.exec($popup.find('.fr-back:visible:first'))
          }
          else {
            inst.popups.hide(id);
          }
        }
      },

      /**
       * Handling hitting the popup elements with the mouse.
       */
      _preventFocus: function (e) {
        var inst = $popup.data('instance') || editor;
        inst.events.disableBlur();

        // Get the original target.
        var originalTarget = e.originalEvent ? (e.originalEvent.target || e.originalEvent.originalTarget) : null;

        // Define the input selector.
        var input_selector = 'input, textarea, button, select, label, .fr-command';

        // Click was not made inside an input.
        if (originalTarget && !$(originalTarget).is(input_selector) && $(originalTarget).parents(input_selector).length === 0) {
          e.stopPropagation();
          return false;
        }

        // Click was made on another input inside popup. Prevent propagation of the event.
        else if (originalTarget && $(originalTarget).is(input_selector)) {
          e.stopPropagation();
        }
      },

      /**
       * Mouseup inside the editor.
       */
      _editorMouseup: function (e) {
        // Check if popup is visible and we can exit.
        if ($popup.is(':visible') && _canExit()) {
          // If we have an input focused, then disable blur.
          if ($popup.find('input:focus, textarea:focus, button:focus, select:focus').filter(':visible').length > 0) {
            editor.events.disableBlur();
          }
        }
      },

      /**
       * Mouseup on window.
       */
      _windowMouseup: function (e) {
        if (!editor.core.sameInstance($popup)) return true;

        var inst = $popup.data('instance') || editor;

        if ($popup.is(':visible') && _canExit()) {
          e.stopPropagation();
          inst.markers.remove();
          inst.popups.hide(id);

          _unmarkExit();
        }
      },

      /**
       * Placeholder effect.
       */
      _doPlaceholder: function (e) {
        var $label = $(this).next();
        if ($label.length == 0) {
          $(this).after('<label>' + $(this).attr('placeholder') + '</label>');
        }

        $(this).toggleClass('fr-not-empty', $(this).val() != '');
      },

      /**
       * Reposition popup.
       */
      _repositionPopup: function (e) {
        // No height set or toolbar inline.
        if (!(editor.opts.height || editor.opts.heightMax) || editor.opts.toolbarInline) return true;

        if (editor.$wp && isVisible(id) && $popup.parent().get(0) == $(editor.opts.scrollableContainer).get(0)) {
          // Popup top - wrapper top.
          var p_top = $popup.offset().top - editor.$wp.offset().top;

          // Wrapper height.
          var w_height = editor.$wp.outerHeight();

          if ($popup.hasClass('fr-above')) p_top += $popup.outerHeight();

          // 1. Popup top > w_height.
          // 2. Popup top + popup height < 0.
          if (p_top > w_height || p_top < 0) {
            $popup.addClass('fr-hidden');
          }
          else {
            $popup.removeClass('fr-hidden');
          }
        }
      }
    }
  }

  function _bindInstanceEvents (ev, id) {
    // Editor mouseup.
    editor.events.on('mouseup', ev._editorMouseup, true);
    if (editor.$wp) editor.events.on('keydown', ev._editorKeydown);

    // Hide all popups on blur.
    editor.events.on('blur', function (e) {
      if (areVisible()) editor.markers.remove();

      hideAll();
    });

    // Update the position of the popup.
    if (editor.$wp && !editor.helpers.isMobile()) {
      editor.events.$on(editor.$wp, 'scroll.popup' + id, ev._repositionPopup);
    }

    editor.events.on('window.keydown', ev._windowKeydown);
    editor.events.on('window.mouseup', ev._windowMouseup, true);

    popups[id].data('inst' + editor.id, true);

    editor.events.on('destroy', function () {
      if (editor.core.sameInstance(popups[id])) {
        popups[id].removeClass('fr-active').appendTo('body');
      }
    }, true)
  }

  /**
   * Create a popup.
   */
  function create (id, template) {
    var $popup = _build(id, template);

    // Build events.
    var ev = _events(id);

    _bindInstanceEvents(ev, id);

    // Input Focus / Blur / Keydown.
    editor.events.$on($popup, 'mousedown mouseup touchstart touchend touch', '*', ev._preventFocus, true);
    editor.events.$on($popup, 'focus', 'input, textarea, button, select', ev._inputFocus, true);
    editor.events.$on($popup, 'blur', 'input, textarea, button, select', ev._inputBlur, true);
    editor.events.$on($popup, 'keydown', 'input, textarea, button, select', ev._inputKeydown, true);

    // Placeholder.
    editor.events.$on($popup, 'keydown keyup change input', 'input, textarea', ev._doPlaceholder, true);

    // Toggle checkbox.
    if (editor.helpers.isIOS()) {
      editor.events.$on($popup, 'touchend', 'label', function () {
        $('#' + $(this).attr('for')).prop('checked', function (i, val) {
          return !val;
        })
      }, true);
    }

    // Window mouseup.
    editor.events.$on($(editor.o_win), 'resize', ev._windowResize, true);

    return $popup;
  }

  /**
   * Destroy.
   */
  function _destroy () {
    for (var id in popups) {
      if (popups.hasOwnProperty(id)) {
        var $popup = popups[id];
        $popup.html('').removeData().remove();
        popups[id] = null;
      }
    }

    popups = [];
  }

  /**
   * Initialization.
   */
  function _init () {
    editor.events.on('shared.destroy', _destroy, true);

    editor.events.on('window.mousedown', _markExit);
    editor.events.on('window.touchmove', _unmarkExit);

    editor.events.on('mousedown', function (e) {
      if (areVisible()) {
        e.stopPropagation();

        // Remove markers.
        editor.$el.find('.fr-marker').remove();

        // Prepare for exit.
        _markExit();

        // Disable blur.
        editor.events.disableBlur();
      }
    })
  }

  return {
    _init: _init,
    create: create,
    get: get,
    show: show,
    hide: hide,
    onHide: onHide,
    hideAll: hideAll,
    setContainer: setContainer,
    refresh: refresh,
    onRefresh: onRefresh,
    onShow: onShow,
    isVisible: isVisible,
    areVisible: areVisible
  }
};

$.FE.MODULES.position = function (editor) {
  /**
  * Get bounding rect around selection.
  *
  */
  function getBoundingRect () {
    var boundingRect;

    var range = editor.selection.ranges(0);
    if (range && range.collapsed && editor.selection.inEditor()) {
      var remove = false;
      if (editor.$el.find('.fr-marker').length == 0) {
        editor.selection.save();
        remove = true;
      }

      var $marker = editor.$el.find('.fr-marker:first');
      $marker.css('display', 'inline');
      $marker.css('line-height', '');
      var offset = $marker.offset();
      var height = $marker.outerHeight();
      $marker.css('display', 'none');
      $marker.css('line-height', 0);

      boundingRect = {}
      boundingRect.left = offset.left;
      boundingRect.width = 0;
      boundingRect.height = height;
      boundingRect.top = offset.top - (editor.helpers.isIOS() ? 0 : $(editor.o_win).scrollTop());
      boundingRect.right = 1;
      boundingRect.bottom = 1;
      boundingRect.ok = true;

      if (remove) editor.selection.restore();
    }
    else if (range) {
      boundingRect = range.getBoundingClientRect();
    }

    return boundingRect;
  }

  /**
   * Normalize top positioning.
   */
  function _topNormalized ($el, top, obj_height) {
    var height = $el.outerHeight();

    if (!editor.helpers.isMobile() && editor.$tb && $el.parent().get(0) != editor.$tb.get(0)) {
      // 1. Parent offset + toolbar top + toolbar height > scrollableContainer height.
      // 2. Selection doesn't go above the screen.
      var p_height = $el.parent().height() - 20 - (editor.opts.toolbarBottom ? editor.$tb.outerHeight() : 0);
      var p_offset = $el.parent().offset().top;
      var new_top = top - height - (obj_height || 0);

      if ($el.parent().get(0) == $(editor.opts.scrollableContainer).get(0)) p_offset = p_offset - $el.parent().position().top;

      var s_height = $(editor.opts.scrollableContainer).get(0).scrollHeight;
      if (p_offset + top + height > $(editor.opts.scrollableContainer).offset().top + s_height && $el.parent().offset().top + new_top > 0) {
        top = new_top;
        $el.addClass('fr-above');
      }
      else {
        $el.removeClass('fr-above');
      }
    }

    return top;
  }

  /**
   * Normalize left position.
   */
  function _leftNormalized ($el, left) {
    var width = $el.outerWidth();

    // Normalize right.
    if ($el.parent().offset().left + left + width > editor.helpers.getPX($(editor.opts.scrollableContainer).css('left')) + $(editor.opts.scrollableContainer).width() - 10) {
     left = $(editor.opts.scrollableContainer).width() - width - 10 - $el.parent().offset().left + $(editor.opts.scrollableContainer).offset().left;
    }

    // Normalize left.
    if ($el.parent().offset().left + left < $(editor.opts.scrollableContainer).offset().left) {
      left = 10 - $el.parent().offset().left + $(editor.opts.scrollableContainer).offset().left;
    }

    return left;
  }

  /**
   * Place editor below selection.
   */
  function forSelection ($el) {
    var selection_rect = getBoundingRect();

    $el.css('top', 0).css('left', 0);

    var top = selection_rect.top + selection_rect.height;
    var left = selection_rect.left + selection_rect.width / 2 - $el.outerWidth() / 2 + $(editor.o_win).scrollLeft();

    if (!editor.opts.iframe) {
      top += $(editor.o_win).scrollTop();
    }

    at(left, top, $el, selection_rect.height);
  }

  /**
   * Position element at the specified position.
   */
  function at (left, top, $el, obj_height) {
    var $container = $el.data('container');

    if ($container && $container.get(0).tagName != 'BODY') {
      if (left) left -= $container.offset().left;
      if (top) top -= $container.offset().top - $container.scrollTop();
    }

    // Apply iframe correction.
    if (editor.opts.iframe && $container && editor.$tb && $container.get(0) != editor.$tb.get(0)) {
      if (left) left += editor.$iframe.offset().left;
      if (top) top += editor.$iframe.offset().top;
    }

    var new_left = _leftNormalized($el, left);

    if (left) {
      // Set the new left.
      $el.css('left', new_left);

      // Normalize arrow.
      var $arrow = $el.find('.fr-arrow');
      if (!$arrow.data('margin-left')) $arrow.data('margin-left', editor.helpers.getPX($arrow.css('margin-left')));
      $arrow.css('margin-left', left - new_left + $arrow.data('margin-left'));
    }

    if (top) $el.css('top', _topNormalized($el, top, obj_height));
  }

  /**
   * Special case for update sticky on iOS.
   */
  function _updateIOSSticky (el) {
    var $el = $(el);
    var is_on = $el.is('.fr-sticky-on');
    var prev_top = $el.data('sticky-top');
    var scheduled_top = $el.data('sticky-scheduled');

    // Create a dummy div that we show then sticky is on.
                      if (typeof prev_top == 'undefined') {
      $el.data('sticky-top', 0);
      var $dummy = $('<div class="fr-sticky-dummy" style="height: ' + $el.outerHeight() + 'px;"></div>');
                        editor.$box.prepend($dummy);
                      }
    else {
      editor.$box.find('.fr-sticky-dummy').css('height', $el.outerHeight());
    }

    // Position sticky doesn't work when the keyboard is on the screen.
    if (editor.core.hasFocus() || editor.$tb.find('input:visible:focus').length > 0) {
      // Get the current scroll.
      var x_scroll = $(window).scrollTop();

      // Get the current top.
      // We make sure that we keep it within the editable box.
      var x_top = Math.min(Math.max(x_scroll - editor.$tb.parent().offset().top, 0), editor.$tb.parent().outerHeight() - $el.outerHeight());

      // Not the same top and different than the already scheduled.
      if (x_top != prev_top && x_top != scheduled_top) {
        // Clear any too soon change to avoid flickering.
        clearTimeout($el.data('sticky-timeout'));

        // Store the current scheduled top.
        $el.data('sticky-scheduled', x_top);

        // Hide the toolbar for a rich experience.
        if ($el.outerHeight() < x_scroll - editor.$tb.parent().offset().top) {
          $el.addClass('fr-opacity-0');
        }

        // Set the timeout for changing top.
        // Based on the test 100ms seems to be the best timeout.
        $el.data('sticky-timeout', setTimeout(function () {
          // Get the current top.
          var c_scroll = $(window).scrollTop();
          var c_top = Math.min(Math.max(c_scroll - editor.$tb.parent().offset().top, 0), editor.$tb.parent().outerHeight() - $el.outerHeight());

          if (c_top > 0 && editor.$tb.parent().get(0).tagName == 'BODY') c_top += editor.$tb.parent().position().top;

          // Don't update if it is not different than the prev top.
          if (c_top != prev_top) {
            $el.css('top', Math.max(c_top, 0));

            $el.data('sticky-top', c_top);
            $el.data('sticky-scheduled', c_top);
          }

          // Show toolbar.
          $el.removeClass('fr-opacity-0');
        }, 100));
      }

      // Turn on sticky mode.
      if (!is_on) {
        $el.css('top', '0');
        $el.width(editor.$tb.parent().width());
        $el.addClass('fr-sticky-on');
        editor.$box.addClass('fr-sticky-box');
      }
    }

    // Turn off sticky mode.
    else {
      clearTimeout($(el).css('sticky-timeout'));
      $el.css('top', '0');
      $el.css('position', '');
      $el.width('');
      $el.data('sticky-top', 0);
      $el.removeClass('fr-sticky-on');
      editor.$box.removeClass('fr-sticky-box');
    }
  }

  /**
   * Update sticky location for browsers that don't support sticky.
   * https://github.com/filamentgroup/fixed-sticky
   *
   * The MIT License (MIT)
   *
   * Copyright (c) 2013 Filament Group
   */
  function _updateSticky (el) {
    if( !el.offsetWidth ) { return; }

                      var el_top;
                      var el_bottom;
    var $el = $(el);
    var height = $el.outerHeight();

    var position = $el.data('sticky-position');

    // Viewport height.
    var viewport_height = $(editor.opts.scrollableContainer == 'body' ? editor.o_win : editor.opts.scrollableContainer).outerHeight();

    var scrollable_top = 0;
    var scrollable_bottom = 0;
    if (editor.opts.scrollableContainer !== 'body') {
      scrollable_top = $(editor.opts.scrollableContainer).offset().top;
      scrollable_bottom = $(editor.o_win).outerHeight() - scrollable_top - viewport_height;
    }

    var offset_top = editor.opts.scrollableContainer == 'body' ? $(editor.o_win).scrollTop() : scrollable_top;

                      var is_on = $el.is('.fr-sticky-on');

    // Decide parent.
    if (!$el.data('sticky-parent')) {
      $el.data('sticky-parent', $el.parent());
    }
    var $parent = $el.data('sticky-parent');
                var parent_top = $parent.offset().top;
                      var parent_height = $parent.outerHeight();

                      if (!$el.data('sticky-offset')) {
                              $el.data('sticky-offset', true);
                              $el.after('<div class="fr-sticky-dummy" style="height: ' + height + 'px;"></div>');
                      }

    // Detect position placement.
    if (!position) {
                              // Some browsers require fixed/absolute to report accurate top/left values.
                              var skip_setting_fixed = $el.css('top') !== 'auto' || $el.css('bottom') !== 'auto';

      // Set to position fixed for a split of second.
                              if(!skip_setting_fixed) {
                                      $el.css('position', 'fixed');
                              }

      // Find position.
                              position = {
                                      top: $el.hasClass('fr-top'),
                                      bottom: $el.hasClass('fr-bottom')
                              };

      // Remove position fixed.
                              if(!skip_setting_fixed) {
                                      $el.css('position', '');
                              }

      // Store position.
                              $el.data('sticky-position', position);

      $el.data('top', $el.hasClass('fr-top') ? $el.css('top') : 'auto');
      $el.data('bottom', $el.hasClass('fr-bottom') ? $el.css('bottom') : 'auto');
              }

    // Detect if is OK to fix at the top.
                      var isFixedToTop = function () {
                              // 1. Top condition.
      // 2. Bottom condition.
                              return parent_top <  offset_top + el_top &&
              parent_top + parent_height - height >= offset_top + el_top;
                      }

    // Detect if it is OK to fix at the bottom.
                      var isFixedToBottom = function () {
                              return parent_top + height < offset_top + viewport_height - el_bottom &&
                                      parent_top + parent_height > offset_top + viewport_height - el_bottom ;
                      }

                      el_top = editor.helpers.getPX($el.data('top'));
                      el_bottom = editor.helpers.getPX($el.data('bottom'));

    var at_top = (position.top && isFixedToTop());
    var at_bottom = (position.bottom && isFixedToBottom());

    // Should be fixed.
                      if (at_top || at_bottom) {
      $el.css('width', $parent.width() + 'px');

      if (!is_on) {
                                      $el.addClass('fr-sticky-on')
        $el.removeClass('fr-sticky-off');

        if ($el.css('top')) {
          if ($el.data('top') != 'auto') {
            $el.css('top', editor.helpers.getPX($el.data('top')) + scrollable_top);
          }
          else {
            $el.data('top', 'auto');
          }
        }

        if ($el.css('bottom')) {
          if ($el.data('bottom') != 'auto') {
            $el.css('bottom', editor.helpers.getPX($el.data('bottom')) + scrollable_bottom);
          }
          else {
            $el.css('bottom', 'auto');
          }
        }
                              }
                      }

    // Shouldn't be fixed.
    else {
                              if (!$el.hasClass('fr-sticky-off')) {
        // Reset.
        $el.width('');
        $el.removeClass('fr-sticky-on');
        $el.addClass('fr-sticky-off');

        if ($el.css('top') && $el.css('top') != 'auto') {
          $el.css('top', 0);
        }

        if ($el.css('bottom')) $el.css('bottom', 0);
                              }
    }
  }

 /**
  * Test if browser supports sticky.
  * https://github.com/filamentgroup/fixed-sticky
  *
  * The MIT License (MIT)
  *
  * Copyright (c) 2013 Filament Group
  */
  function _testSticky () {
    var el = document.createElement('test');
              var mStyle = el.style;

                      mStyle.cssText = 'position:' + [ '-webkit-', '-moz-', '-ms-', '-o-', '' ].join('sticky; position:') + ' sticky;';

              return mStyle['position'].indexOf('sticky') !== -1 && !editor.helpers.isIOS() && !editor.helpers.isAndroid();
  }

  /**
   * Initialize sticky position.
   */
  function _initSticky () {
    if (!_testSticky()) {
      editor._stickyElements = [];

      // iOS special case.
      if (editor.helpers.isIOS()) {
        // Use an animation frame to make sure we're always OK with the updates.
        var animate = function () {
          editor.helpers.requestAnimationFrame()(animate);

          for (var i = 0; i < editor._stickyElements.length; i++) {
            _updateIOSSticky(editor._stickyElements[i]);
          }
        };
        animate();

        // Hide toolbar on touchmove. This is very useful on iOS versions < 8.
        editor.events.$on($(editor.o_win), 'scroll', function () {
          if (editor.core.hasFocus()) {
            for (var i = 0; i < editor._stickyElements.length; i++) {
              var $el = $(editor._stickyElements[i]);
              var $parent = $el.parent();
              var c_scroll = $(window).scrollTop();

              if ($el.outerHeight() < c_scroll - $parent.offset().top) {
                $el.addClass('fr-opacity-0');
                $el.data('sticky-top', -1);
                $el.data('sticky-scheduled', -1);
              }
            }
          }
        }, true);
      }

      // Default case. Do the updates on scroll.
      else {
        editor.events.$on($(editor.opts.scrollableContainer == 'body' ? editor.o_win : editor.opts.scrollableContainer), 'scroll', refresh, true);
        editor.events.$on($(editor.o_win), 'resize', refresh, true);

        editor.events.on('initialized', refresh);
        editor.events.on('focus', refresh);

        editor.events.$on($(editor.o_win), 'resize', 'textarea', refresh, true);
      }
    }

    editor.events.on('destroy', function (e) {
      editor._stickyElements = [];
    });
  }

  function refresh () {
    for (var i = 0; i < editor._stickyElements.length; i++) {
      _updateSticky(editor._stickyElements[i]);
    }
  }

  /**
   * Mark element as sticky.
   */
  function addSticky ($el) {
    $el.addClass('fr-sticky');

    if (editor.helpers.isIOS()) $el.addClass('fr-sticky-ios');

    if (!_testSticky()) {
      editor._stickyElements.push($el.get(0));
    }
  }

  function _init () {
    _initSticky();
  }

  return {
    _init: _init,
    forSelection: forSelection,
    addSticky: addSticky,
    refresh: refresh,
    at: at,
    getBoundingRect: getBoundingRect
  }
};

$.FE.MODULES.refresh = function (editor) {
  function undo ($btn) {
    $btn.toggleClass('fr-disabled', !editor.undo.canDo());
  }

  function redo ($btn) {
    $btn.toggleClass('fr-disabled', !editor.undo.canRedo());
  }

  function indent ($btn) {
    if ($btn.hasClass('fr-no-refresh')) return false;

    var blocks = editor.selection.blocks();
    for (var i = 0; i < blocks.length; i++) {
      var p_node = blocks[i].previousSibling;
      while (p_node && p_node.nodeType == Node.TEXT_NODE && p_node.textContent.length === 0) {
        p_node = p_node.previousSibling;
      }
      if (blocks[i].tagName == 'LI' && !p_node) {
        $btn.addClass('fr-disabled');
      }
      else {
        $btn.removeClass('fr-disabled');
        return true;
      }
    }
  }

  function outdent ($btn) {
    if ($btn.hasClass('fr-no-refresh')) return false;

    var blocks = editor.selection.blocks();
    for (var i = 0; i < blocks.length; i++) {
      var prop = (editor.opts.direction == 'rtl' || $(blocks[i]).css('direction') == 'rtl') ? 'margin-right' : 'margin-left';

      if (blocks[i].tagName == 'LI' || blocks[i].parentNode.tagName == 'LI') {
        $btn.removeClass('fr-disabled');
        return true;
      }

      if (editor.helpers.getPX($(blocks[i]).css(prop)) > 0) {
        $btn.removeClass('fr-disabled');
        return true;
      }
    }

    $btn.addClass('fr-disabled');
  }

  return {
    undo: undo,
    redo: redo,
    outdent: outdent,
    indent: indent
  }
};

$.extend($.FE.DEFAULTS, {
  editInPopup: false
});

$.FE.MODULES.textEdit = function (editor) {
  function _initPopup () {
    // Image buttons.
    var txt = '<div id="fr-text-edit-' + editor.id + '" class="fr-layer fr-text-edit-layer"><div class="fr-input-line"><input type="text" placeholder="' + editor.language.translate('Text') + '" tabIndex="1"></div><div class="fr-action-buttons"><button type="button" class="fr-command fr-submit" data-cmd="updateText" tabIndex="2">' + editor.language.translate('Update') + '</button></div></div>'

    var template = {
      edit: txt
    };

    var $popup = editor.popups.create('text.edit', template);
  }

  function _showPopup () {
    var $popup = editor.popups.get('text.edit');

    var text;
    if (editor.$el.prop('tagName') === 'INPUT') {
      text = editor.$el.attr('placeholder');
    }
    else {
      text = editor.$el.text();
    }

    $popup.find('input').val(text).trigger('change');
    editor.popups.setContainer('text.edit', $('body'));
    editor.popups.show('text.edit', editor.$el.offset().left + editor.$el.outerWidth() / 2, editor.$el.offset().top + editor.$el.outerHeight(), editor.$el.outerHeight());
  }

  function _initEvents () {
    // Show edit popup.
    editor.events.$on(editor.$el, editor._mouseup, function (e) {
      setTimeout (function () {
        _showPopup();
      }, 10);
    })
  }

  function update () {
    var $popup = editor.popups.get('text.edit');

    var new_text = $popup.find('input').val();

    if (new_text.length == 0) new_text = editor.opts.placeholderText;

    if (editor.$el.prop('tagName') === 'INPUT') {
      editor.$el.attr('placeholder', new_text);
    }
    else {
      editor.$el.text(new_text);
    }

    editor.events.trigger('contentChanged');

    editor.popups.hide('text.edit');
  }

  /**
   * Initialize.
   */
  function _init () {
    if (editor.opts.editInPopup) {
      _initPopup();
      _initEvents();
    }
  }

  return {
    _init: _init,
    update: update
  }
};

$.FE.RegisterCommand('updateText', {
  focus: false,
  undo: false,
  callback: function () {
    this.textEdit.update();
  }
})

// Extend defaults.
$.extend($.FE.DEFAULTS, {
  toolbarBottom: false,
  toolbarButtons: ['fullscreen', 'bold', 'italic', 'underline', 'strikeThrough', 'subscript', 'superscript', 'fontFamily', 'fontSize', '|', 'color', 'emoticons', 'inlineStyle', 'paragraphStyle', '|', 'paragraphFormat', 'align', 'formatOL', 'formatUL', 'outdent', 'indent', 'quote', 'insertHR', '-', 'insertLink', 'insertImage', 'insertVideo', 'insertFile', 'insertTable', 'undo', 'redo', 'clearFormatting', 'selectAll', 'html', 'applyFormat', 'removeFormat'],
  toolbarButtonsXS: ['bold', 'italic', 'fontFamily', 'fontSize', '|', 'undo', 'redo'],
  toolbarButtonsSM: ['bold', 'italic', 'underline', '|', 'fontFamily', 'fontSize', 'insertLink', 'insertImage', 'table', '|', 'undo', 'redo'],
  toolbarButtonsMD: ['fullscreen', 'bold', 'italic', 'underline', 'fontFamily', 'fontSize', 'color', 'paragraphStyle', 'paragraphFormat', 'align', 'formatOL', 'formatUL', 'outdent', 'indent', 'quote', 'insertHR', '-', 'insertLink', 'insertImage', 'insertVideo', 'insertFile', 'insertTable', 'undo', 'redo', 'clearFormatting'],
  toolbarContainer: null,
  toolbarInline: false,
  toolbarSticky: true,
  toolbarStickyOffset: 0,
  toolbarVisibleWithoutSelection: false
});

$.FE.MODULES.toolbar = function (editor) {
  var _document, _window;

  // Create a button map for each screen size.
  var _buttons_map = [];
  _buttons_map[$.FE.XS] = editor.opts.toolbarButtonsXS || editor.opts.toolbarButtons;
  _buttons_map[$.FE.SM] = editor.opts.toolbarButtonsSM || editor.opts.toolbarButtons;
  _buttons_map[$.FE.MD] = editor.opts.toolbarButtonsMD || editor.opts.toolbarButtons;
  _buttons_map[$.FE.LG] = editor.opts.toolbarButtons;

  function _addOtherButtons (buttons, toolbarButtons) {
    for (var i = 0; i < toolbarButtons.length; i++) {
      if (toolbarButtons[i] != '-' && toolbarButtons[i] != '|' && buttons.indexOf(toolbarButtons[i]) < 0) {
        buttons.push(toolbarButtons[i]);
      }
    }
  }

  /**
   * Add buttons to the toolbar.
   */
  function _addButtons () {
    var _buttons = $.merge([], _screenButtons());
    _addOtherButtons(_buttons, editor.opts.toolbarButtonsXS || []);
    _addOtherButtons(_buttons, editor.opts.toolbarButtonsSM || []);
    _addOtherButtons(_buttons, editor.opts.toolbarButtonsMD || []);
    _addOtherButtons(_buttons, editor.opts.toolbarButtons);

    for (var i = _buttons.length - 1; i >= 0; i--) {
      if (_buttons[i] != '-' && _buttons[i] != '|' && _buttons.indexOf(_buttons[i]) < i) {
        _buttons.splice(i, 1);
      }
    }

    var buttons_list = editor.button.buildList(_buttons, _screenButtons());
    editor.$tb.append(buttons_list);
    editor.button.bindCommands(editor.$tb);
  }

  /**
   * The buttons that should be visible on the current screen size.
   */
  function _screenButtons () {
    var screen_size = editor.helpers.screenSize();
    return _buttons_map[screen_size];
  }

  function _showScreenButtons () {
    var c_buttons = _screenButtons();

    // Remove separator from toolbar.
    editor.$tb.find('.fr-separator').remove();

    // Hide all buttons.
    editor.$tb.find('> .fr-command').addClass('fr-hidden');

    // Reorder buttons.
    for (var i = 0; i < c_buttons.length; i++) {
      if (c_buttons[i] == '|' || c_buttons[i] == '-') {
        editor.$tb.append(editor.button.buildList([c_buttons[i]]));
      }
      else {
        var $btn = editor.$tb.find('> .fr-command[data-cmd="' + c_buttons[i] + '"]');
        var $dropdown = null;
        if ($btn.next().hasClass('fr-dropdown-menu')) $dropdown = $btn.next();

        $btn.removeClass('fr-hidden').appendTo(editor.$tb);
        if ($dropdown) $dropdown.appendTo(editor.$tb);
      }
    }
  }

  /**
   * Set the buttons visibility based on screen size.
   */
  function _setVisibility () {
    editor.events.$on($(editor.o_win), 'resize', _showScreenButtons, true);
    editor.events.$on($(editor.o_win), 'orientationchange', _showScreenButtons, true);
  }

  function showInline (e, force) {
    setTimeout(function () {
      if (e && e.which == $.FE.KEYCODE.ESC) {
        // Nothing.
      }
      else if (editor.selection.inEditor() && editor.core.hasFocus() && !editor.popups.areVisible()) {
        if ((editor.opts.toolbarVisibleWithoutSelection && e && e.type != 'keyup') || (!editor.selection.isCollapsed() && !editor.keys.isIME()) || force) {
          editor.$tb.data('instance', editor);

          // Check if we should actually show the toolbar.
          if (editor.events.trigger('toolbar.show', [e]) == false) return false;

          if (!editor.opts.toolbarContainer) {
            editor.position.forSelection(editor.$tb);
          }

          editor.$tb.show();
        }
      }
    }, 0);
  }

  function hide (e) {
    // Prevent hiding when dropdown is active and we scoll in it.
    // https://github.com/froala/wysiwyg-editor/issues/1290
    var $active_dropdowns = $('.fr-dropdown.fr-active');
    if ($active_dropdowns.next().find(editor.o_doc.activeElement).length) return true;

    // Check if we should actually hide the toolbar.
    if (editor.events.trigger('toolbar.hide') == false) return false;

    editor.$tb.hide();
  }

  function show () {
    // Check if we should actually hide the toolbar.
    if (editor.events.trigger('toolbar.show') == false) return false;

    editor.$tb.show();
  }

  /**
   * Set the events for show / hide toolbar.
   */
  function _initInlineBehavior () {
    // Window mousedown.
    editor.events.on('window.mousedown', hide);

    // Element keydown.
    editor.events.on('keydown', hide);

    // Element blur.
    editor.events.on('blur', hide);

    // Window mousedown.
    editor.events.on('window.mouseup', showInline);

    if (editor.helpers.isMobile()) {
      if (!editor.helpers.isIOS()) {
        editor.events.on('window.touchend', showInline);

        if (editor.browser.mozilla) {
          setInterval(showInline, 200);
        }
      }
    }
    else {
      editor.events.on('window.keyup', showInline);
    }

    // Hide editor on ESC.
    editor.events.on('keydown', function (e) {
      if (e && e.which == $.FE.KEYCODE.ESC) {
        hide();
      }
    });

    editor.events.$on(editor.$wp, 'scroll.toolbar', showInline);
    editor.events.on('commands.after', showInline);

    if (editor.helpers.isMobile()) {
      editor.events.$on(editor.$doc, 'selectionchange', showInline);
      editor.events.$on(editor.$doc, 'orientationchange', showInline);
    }
  }

  function _initPositioning () {
    // Toolbar is inline.
    if (editor.opts.toolbarInline) {
      // Mobile should handle this as regular.
      $(editor.opts.scrollableContainer).append(editor.$tb);

      // Add toolbar to body.
      editor.$tb.data('container', $(editor.opts.scrollableContainer));

      // Add inline class.
      editor.$tb.addClass('fr-inline');

      // Add arrow.
      editor.$tb.prepend('<span class="fr-arrow"></span>')

      // Init mouse behavior.
      _initInlineBehavior();

      editor.opts.toolbarBottom = false;
    }

    // Toolbar is normal.
    else {
      // Won't work on iOS.
      if (editor.opts.toolbarBottom && !editor.helpers.isIOS()) {
        editor.$box.append(editor.$tb);
        editor.$tb.addClass('fr-bottom');
        editor.$box.addClass('fr-bottom');
      }
      else {
        editor.opts.toolbarBottom = false;
        editor.$box.prepend(editor.$tb);
        editor.$tb.addClass('fr-top');
        editor.$box.addClass('fr-top');
      }

      editor.$tb.addClass('fr-basic');

      if (editor.opts.toolbarSticky) {
        if (editor.opts.toolbarStickyOffset) {
          if (editor.opts.toolbarBottom) {
            editor.$tb.css('bottom', editor.opts.toolbarStickyOffset);
          }
          else {
            editor.$tb.css('top', editor.opts.toolbarStickyOffset);
          }
        }

        editor.position.addSticky(editor.$tb);
      }
    }
  }

  /**
   * Destroy.
   */
  function _sharedDestroy () {
    editor.$tb.html('').removeData().remove();
    editor.$tb = null;
  }

  function _destroy () {
    editor.$box.removeClass('fr-top fr-bottom fr-inline fr-basic');
    editor.$box.find('.fr-sticky-dummy').remove();
  }

  function _setDefaults () {
    if (editor.opts.theme) {
      editor.$tb.addClass(editor.opts.theme + '-theme');
    }

    if (editor.opts.zIndex > 1) {
      editor.$tb.css('z-index', editor.opts.zIndex + 1);
    }

    // Set direction.
    if (editor.opts.direction != 'auto') {
      editor.$tb.removeClass('fr-ltr fr-rtl').addClass('fr-' + editor.opts.direction);
    }

    // Mark toolbar for desktop / mobile.
    if (!editor.helpers.isMobile()) {
      editor.$tb.addClass('fr-desktop');
    }
    else {
      editor.$tb.addClass('fr-mobile');
    }

    // Set the toolbar specific position inline / normal.
    if (!editor.opts.toolbarContainer) {
      _initPositioning();
    }
    else {
      if (editor.opts.toolbarInline) {
        _initInlineBehavior();
        hide();
      }

      if (editor.opts.toolbarBottom) editor.$tb.addClass('fr-bottom');
      else editor.$tb.addClass('fr-top');
    }

    // Set documetn and window for toolbar.
    _document = editor.$tb.get(0).ownerDocument;
    _window = 'defaultView' in _document ? _document.defaultView : _document.parentWindow;

    // Add buttons to the toolbar.
    // Set their visibility for different screens.
    // Asses commands to the butttons.
    _addButtons();
    _setVisibility();

    // Make sure we don't trigger blur.
    editor.events.$on(editor.$tb, editor._mousedown + ' ' + editor._mouseup, function (e) {
      var originalTarget = e.originalEvent ? (e.originalEvent.target || e.originalEvent.originalTarget) : null;
      if (originalTarget && originalTarget.tagName != 'INPUT' && !editor.edit.isDisabled()) {
        e.stopPropagation();
        e.preventDefault();
        return false;
      }
    }, true);
  }

  /**
   * Initialize
   */
  var tb_exists = false;
  function _init () {
    if (!editor.$wp) return false;

    // Container for toolbar.
    if (editor.opts.toolbarContainer) {
      // Shared toolbar.
      if (!editor.shared.$tb) {
        editor.shared.$tb = $('<div class="fr-toolbar"></div>');
        editor.$tb = editor.shared.$tb;
        $(editor.opts.toolbarContainer).append(editor.$tb);
        _setDefaults();
        editor.$tb.data('instance', editor);
      }
      else {
        editor.$tb = editor.shared.$tb;

        if (editor.opts.toolbarInline) _initInlineBehavior();
      }

      if (editor.opts.toolbarInline) {
        // Update box.
        editor.$box.addClass('fr-inline');
      }
      else {
        editor.$box.addClass('fr-basic');
      }

      // On focus set the current instance.
      editor.events.on('focus', function () {
        editor.$tb.data('instance', editor);
      }, true);

      editor.opts.toolbarInline = false;
    }
    else {
      // Inline toolbar.
      if (editor.opts.toolbarInline) {
        // Update box.
        editor.$box.addClass('fr-inline');

        // Check for shared toolbar.
        if (!editor.shared.$tb) {
          editor.shared.$tb = $('<div class="fr-toolbar"></div>');
          editor.$tb = editor.shared.$tb;
          _setDefaults();
        }
        else {
          editor.$tb = editor.shared.$tb;

          // Init mouse behavior.
          _initInlineBehavior();
        }
      }
      else {
        editor.$box.addClass('fr-basic');
        editor.$tb = $('<div class="fr-toolbar"></div>');
        _setDefaults();

        editor.$tb.data('instance', editor);
      }
    }

    // Destroy.
    editor.events.on('destroy', _destroy, true);
    editor.events.on(!editor.opts.toolbarInline ? 'destroy' : 'shared.destroy', _sharedDestroy, true);
  }

  var disabled = false;
  function disable () {
    if (!disabled && editor.$tb) {
      editor.$tb.find('> .fr-command').addClass('fr-disabled fr-no-refresh');
      disabled = true;
    }
  }

  function enable () {
    if (disabled && editor.$tb) {
      editor.$tb.find('> .fr-command').removeClass('fr-disabled fr-no-refresh');
      disabled = false;
    }

    editor.button.bulkRefresh();
  }

  return {
    _init: _init,
    hide: hide,
    show: show,
    showInline: showInline,
    disable: disable,
    enable: enable
  }
};

}));