UNPKG

zx-editor

Version:

ZxEditor is a HTML5 rich text editor

3,582 lines 93.1 kB
/*!
 * zx-editor v3.0.4-alpha
 * https://github.com/capricorncd/zx-editor
 * Copyright © 2018-present, capricorncd
 * Released under the MIT License
 * Released on: 2021-03-20 19:17:57
 */
(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  typeof define === 'function' && define.amd ? define(factory) :
  (global = global || self, global.ZxEditor = factory());
}(this, (function () { 'use strict';

  function _typeof(obj) {
    "@babel/helpers - typeof";

    if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
      _typeof = function (obj) {
        return typeof obj;
      };
    } else {
      _typeof = function (obj) {
        return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
      };
    }

    return _typeof(obj);
  }

  /**
   * SSR Window 1.0.1
   * Better handling for window object in SSR environment
   * https://github.com/nolimits4web/ssr-window
   *
   * Copyright 2018, Vladimir Kharlampidi
   *
   * Licensed under MIT
   *
   * Released on: July 18, 2018
   */
  var doc = typeof document === 'undefined' ? {
    body: {},
    addEventListener: function addEventListener() {},
    removeEventListener: function removeEventListener() {},
    activeElement: {
      blur: function blur() {},
      nodeName: ''
    },
    querySelector: function querySelector() {
      return null;
    },
    querySelectorAll: function querySelectorAll() {
      return [];
    },
    getElementById: function getElementById() {
      return null;
    },
    createEvent: function createEvent() {
      return {
        initEvent: function initEvent() {}
      };
    },
    createElement: function createElement() {
      return {
        children: [],
        childNodes: [],
        style: {},
        setAttribute: function setAttribute() {},
        getElementsByTagName: function getElementsByTagName() {
          return [];
        }
      };
    },
    location: {
      hash: ''
    }
  } : document; // eslint-disable-line

  var win = typeof window === 'undefined' ? {
    document: doc,
    navigator: {
      userAgent: ''
    },
    location: {},
    history: {},
    CustomEvent: function CustomEvent() {
      return this;
    },
    addEventListener: function addEventListener() {},
    removeEventListener: function removeEventListener() {},
    getComputedStyle: function getComputedStyle() {
      return {
        getPropertyValue: function getPropertyValue() {
          return '';
        }
      };
    },
    Image: function Image() {},
    Date: function Date() {},
    screen: {},
    setTimeout: function setTimeout() {},
    clearTimeout: function clearTimeout() {}
  } : window; // eslint-disable-line

  /**
   * unique
   * Remove duplicate elements in an array
   * @param arr
   * @return {Array}
   */

  function unique(arr) {
    var uniqueArray = [];

    for (var i = 0; i < arr.length; i += 1) {
      if (uniqueArray.indexOf(arr[i]) === -1) uniqueArray.push(arr[i]);
    }

    return uniqueArray;
  }
  /**
   * 改变元素的nodeName
   * @param oldNode
   * @param tagName target nodeName
   * @return {*}
   */

  function changeNodeName(oldNode, tagName) {
    var oldNodeName = oldNode.nodeName.toLowerCase(); // check tagName

    if (typeof tagName !== 'string' || util.strip(tagName).length === 0) {
      throw new TypeError("changeNodeName(oldNode, tagName) 'tagName' is ".concat(tagName, ", should be a 'string'"));
    }

    if (oldNodeName === tagName.toLowerCase()) return oldNode;
    var el = doc.createElement(tagName); // 非Element节点,当字符串节点处理

    if (oldNode.nodeType !== 1) {
      el.appendChild(oldNode);
    } else {
      // 获取属性class, style, id
      if (oldNode.className) el.className = oldNode.className;
      if (oldNode.id) el.id = oldNode.id;
      var style = oldNode.getAttribute('style');
      if (style) el.setAttribute('style', style);
      el.innerHTML = oldNode.innerHTML;
    } // replace Node on document


    if (oldNode.parentNode) {
      oldNode.parentNode.replaceChild(el, oldNode);
    }

    return el;
  }
  /**
   * addEventListener
   * @param el
   * @param eventType
   * @param fn
   * @param useCapture
   */

  function addEventListener(el, eventType, fn, useCapture) {
    if (el.addEventListener) {
      el.addEventListener(eventType, fn, useCapture);
    } else if (el.attachEvent) {
      el.attachEvent(eventType, fn);
    } else {
      el["on".concat(eventType)] = fn;
    }
  }
  /**
   * removeEventListener
   * @param el
   * @param eventType
   * @param fn
   * @param useCapture
   */

  function removeEventListener(el, eventType, fn, useCapture) {
    if (el.removeEventListener) {
      el.removeEventListener(eventType, fn, useCapture);
    } else if (el.detachEvent) {
      el.detachEvent(eventType, fn);
    } else {
      el["on".concat(eventType)] = null;
    }
  }
  /**
   * crate text node
   * @param str
   * @return {Text | ActiveX.IXMLDOMText}
   */

  function createTextNode(str) {
    return doc.createTextNode(str);
  }
  /**
   * create element
   * @param tag HTML tag name
   * @param attrs attributes
   * @return {*|{children, childNodes, style, setAttribute, getElementsByTagName}|ActiveX.IXMLDOMElement|HTMLElement}
   */


  function createElement(tag, attrs) {
    if (!tag && typeof tag !== 'string') {
      throw new TypeError('Parameter error');
    }

    var el = doc.createElement(tag);

    if (attrs && _typeof(attrs) === 'object') {
      for (var key in attrs) {
        if (attrs.hasOwnProperty(key)) {
          el.setAttribute(key, attrs[key]);
        }
      }
    }

    return el;
  }
  /**
   * create dom
   * @param vnode {tag: 'div', attrs: {class: 'class-name', 'data-id': 1000}, child: ['any text', vnode]}
   * @return {*}
   */

  function createVdom(vnode) {
    if (!vnode) return null;

    if (typeof vnode === 'string') {
      return createTextNode(vnode);
    }

    var tag = vnode.tag;
    var attrs = vnode.attrs;
    var child = vnode.child;
    if (!tag && !attrs && !child) return null; // 创建dom

    var el = createElement(tag || 'div', attrs);

    if (Array.isArray(child) && child.length) {
      var itemNode;
      child.forEach(function (item) {
        itemNode = createVdom(item);
        if (itemNode) el.appendChild(itemNode);
      });
    } else if (child && typeof child === 'string') {
      el.appendChild(createTextNode(child));
    }

    return el;
  }

  var arr = []; // Support: Android <=4.0 only
  // Make sure we trim BOM and NBSP

  var regTrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; // navigator

  var USER_AGENT = win.navigator.userAgent;
  var PLATFORM = win.navigator.platform;
  /**
   * 转换为整数
   * @param n
   * @returns {*}
   */

  function _int(n) {
    var num = parseInt(n);
    return isNaN(num) ? 0 : num;
  }
  /**
   * 去除字符串首尾空格
   * @param str
   * @returns {string}
   */


  function trim(str) {
    return str ? str.toString().replace(regTrim, '') : '';
  }
  /**
   * 将伪数组,转化为数组
   * @param ArrayLike
   * @param index
   * @return {T[]}
   */


  function slice(ArrayLike, index) {
    return arr.slice.call(ArrayLike, _int(index));
  }
  /**
   * 去除字符串多余空格
   * 除首尾外,中间连续空格被替换为一个空格
   * @param str
   * @return {string}
   */


  function strip(str) {
    return trim(str).replace(/\s+/g, ' ');
  }
  /**
   * 是否为Element对象
   * @param el
   * @return {*|boolean}
   */


  function isElement(el) {
    return isObject(el) && el.nodeType;
  }
  /**
   * 是否为对象
   * @param obj
   * @param isAbsolute 必须满足父级原型为Object的对象
   * @return {*|boolean}
   */


  function isObject(obj, isAbsolute) {
    var isObj = obj && _typeof(obj) === 'object' && !Array.isArray(obj);
    return isAbsolute ? isObj && obj.toString() === '[object Object]' : isObj;
  }

  function isIPhone() {
    return /iphone/i.test(USER_AGENT) && /iphone/.test(PLATFORM);
  }

  function isIPhoneX() {
    return win.screen.height === 812 && win.screen.width === 375;
  }

  function isWindow(obj) {
    return obj !== null && obj === obj.window;
  }
  /**
   * 移除html标签
   * @param str
   * @return {string}
   */


  function removeHtmlTags(str) {
    str = (str + '').replace(/<\/?.+?>/g, '');
    return strip(str);
  }

  function toHex(num) {
    var n = typeof num === 'number' ? num : _int(num);
    var hex = n.toString(16);
    return hex[1] ? hex : '0' + hex;
  }

  function rgbToHex(rgb) {
    var hex = '';

    if (/rgb.*?\((\d+)\D+?(\d+)\D+?(\d+)/.test(rgb)) {
      hex += toHex(RegExp.$1);
      hex += toHex(RegExp.$2);
      hex += toHex(RegExp.$3);
    }

    return hex ? '#' + hex : rgb;
  }
  /**
   * string to hump format
   * @param str line-height: lineHeight
   * @return {string}
   */


  function toHump(str) {
    return strip(str).replace(/([-_\s]+\w)/g, function (match, $1) {
      // console.log(match, $1)
      return $1[1].toUpperCase();
    });
  }

  function supportBoxModel() {
    var body = doc.getElementsByTagName('body')[0];
    var div = doc.createElement('div');
    body.appendChild(div); // Figure out if the W3C box model works as expected

    div.innerHTML = '';
    div.style.width = div.style.paddingLeft = '1px'; // 通过检测div块的offsetWidth值是否是2px来判断浏览器是否支持盒模型

    var result = div.offsetWidth === 2;
    body.removeChild(div);
    return result;
  }

  var util = {
    isIPhone: isIPhone(),
    isIPhoneX: isIPhoneX(),
    supportBoxModel: supportBoxModel(),
    changeNodeName: changeNodeName,
    "int": _int,
    isElement: isElement,
    isObject: isObject,
    isWindow: isWindow,
    removeHtmlTags: removeHtmlTags,
    rgbToHex: rgbToHex,
    slice: slice,
    strip: strip,
    trim: trim,
    toHump: toHump,
    unique: unique
  };

  /**
   * Created by Capricorncd.
   * User: https://github.com/capricorncd
   * Date: 2019/04/17 20:36
   */
  /**
   * DOM操作
   * @param selector
   * @param context
   * @constructor
   */

  function ZxQuery(selector, context) {
    var _this = this;

    // HANDLE: $(""), $(null), $(undefined), $(false)
    if (!selector) return this;
    if (selector instanceof ZxQuery) return selector;
    var doms; // Handle HTML strings

    if (typeof selector === 'string') {
      if (selector[0] === '<' && selector[selector.length - 1] === '>' && selector.length >= 3) {
        // Assume that strings that start and end with <> are HTML and skip the regex check
        var tempDiv = doc.createElement('div');
        tempDiv.innerHTML = selector;
        doms = util.slice(tempDiv.childNodes);
      } else if (/^[#\w\d_,\s-]+$/.test(selector)) {
        if (context instanceof ZxQuery) {
          context = context[0];
        }

        context = util.isElement(context) && context.nodeType === 1 ? context : doc;
        doms = util.slice(context.querySelectorAll(util.strip(selector)));
      } // HANDLE: $(DOMElement)

    } else if (selector.nodeType || selector === win || selector === doc) {
      doms = [selector]; // HANDLE: $(DOMElements)
    } else if (Array.isArray(selector) && selector.every(function (item) {
      return util.isElement(item);
    })) {
      doms = selector;
    }

    if (doms) {
      doms.forEach(function (el, i) {
        _this[i] = el;
      });
      this.length = doms.length;
    }

    return this;
  }
  /**
   * prototype
   */

  ZxQuery.prototype = {
    /**
     * constructor
     */
    constructor: ZxQuery,

    /**
     * length
     */
    length: 0,

    /**
     * ********************************************
     * find item
     * ********************************************
     */

    /**
     * get children
     * @param selector
     * @param anyNode Boolean
     * @return {*|HTMLElement}
     */
    children: function children(selector, anyNode) {
      // check arguments
      if (typeof selector === 'boolean') {
        anyNode = selector;
        selector = void 0;
      }

      var children = [];
      var el;

      for (var i = 0; i < this.length; i++) {
        var childNodes = this[i].childNodes;

        for (var j = 0; j < childNodes.length; j++) {
          el = childNodes[j];

          if (!selector) {
            if (anyNode) {
              children.push(el);
            } else if (el.nodeType === 1) {
              children.push(el);
            }
          } else if (el.nodeType === 1 && $(el).is(selector)) {
            children.push(el);
          }
        }
      }

      return $(unique(children));
    },

    /**
     * Find the nearest matching element in the parent
     * @param selector
     * @return {*}
     */
    closest: function closest(selector) {
      var closest = this;

      if (typeof selector === 'undefined') {
        return new $();
      }

      if (!closest.is(selector)) {
        closest = closest.parents(selector).eq(0);
      }

      return closest;
    },

    /**
     * equal
     * @param i
     * @return {*|jQuery|HTMLElement}
     */
    eq: function eq(i) {
      var len = this.length;
      var index = util["int"](i) + (i < 0 ? len : 0);
      return $(this[index]);
    },

    /**
     * find item
     * @param selector
     * @return {*|jQuery|HTMLElement}
     */
    find: function find(selector) {
      var found;
      var foundElements = [];

      for (var i = 0; i < this.length; i++) {
        if (this[i].nodeType !== 1) continue;
        found = this[i].querySelectorAll(selector);

        for (var j = 0; j < found.length; j++) {
          foundElements.push(found[j]);
        }
      }

      return $(foundElements);
    },

    /**
     *
     * @param parent
     * @return {*}
     */
    findParentFrom: function findParentFrom(parent) {
      if (parent instanceof ZxQuery) parent = parent[0];
      var current = this[0]; // if (!current || !current.nodeType || !parent || !parent.nodeType) return null

      if (current === parent) return null;
      var tmpParent;

      while (current.parentNode) {
        tmpParent = current.parentNode;
        if (tmpParent === parent) return $(current);
        current = tmpParent;
      }

      return null;
    },

    /**
     * get first child
     * @return {*|jQuery|HTMLElement}
     */
    firstChild: function firstChild() {
      return this.children().eq(0);
    },

    /**
     * get last child
     * @return {*|jQuery|HTMLElement}
     */
    lastChild: function lastChild() {
      return this.children().eq(-1);
    },

    /**
     * get next siblings
     * @param selector
     * @return {*|jQuery|HTMLElement}
     */
    next: function next(selector) {
      if (this.length > 0) {
        if (selector) {
          if (this[0].nextElementSibling && $(this[0].nextElementSibling).is(selector)) {
            return $([this[0].nextElementSibling]);
          }

          return $();
        }

        if (this[0].nextElementSibling) return $([this[0].nextElementSibling]);
      }

      return $();
    },

    /**
     * get nearest parents
     * @param selector
     * @return {*|jQuery|HTMLElement}
     */
    parent: function parent(selector) {
      var parents = [];

      for (var i = 0; i < this.length; i++) {
        if (this[i].parentNode !== null) {
          if (selector) {
            if ($(this[i].parentNode).is(selector)) parents.push(this[i].parentNode);
          } else {
            parents.push(this[i].parentNode);
          }
        }
      }

      return $(unique(parents));
    },

    /**
     * get all parents
     * @param selector
     * @return {*|jQuery|HTMLElement}
     */
    parents: function parents(selector) {
      var parents = [];

      for (var i = 0; i < this.length; i++) {
        var parent = this[i].parentNode;

        while (parent) {
          if (selector) {
            if ($(parent).is(selector)) parents.push(parent);
          } else {
            parents.push(parent);
          }

          parent = parent.parentNode;
        }
      }

      return $(unique(parents));
    },

    /**
     * ********************************************
     * class
     * ********************************************
     */

    /**
     * add className
     * @param className
     * @return {ZxQuery}
     */
    addClass: function addClass(className) {
      var _this2 = this;

      if (!className) return this;
      var classes = className.split(' ');
      classes.forEach(function (cls) {
        for (var j = 0; j < _this2.length; j++) {
          _this2[j] && _this2[j].classList && _this2[j].classList.add(cls);
        }
      });
      return this;
    },

    /**
     * remove className
     * @param className
     * @return {ZxQuery}
     */
    removeClass: function removeClass(className) {
      var _this3 = this;

      var classes = className.split(' ');
      classes.forEach(function (cls) {
        for (var j = 0; j < _this3.length; j++) {
          _this3[j] && _this3[j].classList && _this3[j].classList.remove(cls);
        }
      });
      return this;
    },

    /**
     * check className
     * @param className
     * @return {boolean}
     */
    hasClass: function hasClass(className) {
      if (!this[0]) return false;
      return this[0].classList.contains(className);
    },

    /**
     * ********************************************
     * attribute, data
     * ********************************************
     */

    /**
     * get/set attribute
     * @param attrs attribute or object
     * @param value
     * @return {*}
     */
    attr: function attr(attrs, value) {
      if (arguments.length === 1 && typeof attrs === 'string') {
        // Get attr
        if (this[0]) return this[0].getAttribute(attrs);
        return void 0;
      } // Set attrs


      for (var i = 0; i < this.length; i++) {
        if (arguments.length === 2) {
          // String
          this[i].setAttribute(attrs, value);
        } else {
          // Object
          for (var attr in attrs) {
            this[i][attr] = attrs[attr];
            this[i].setAttribute(attr, attrs[attr]);
          }
        }
      }

      return this;
    },

    /**
     * remove attribute
     * @param attr
     * @return {ZxQuery}
     */
    removeAttr: function removeAttr(attr) {
      for (var i = 0; i < this.length; i++) {
        this[i].removeAttribute(attr);
      }

      return this;
    },

    /**
     * get/set data-
     * @param key
     * @param value
     * @return {*}
     */
    data: function data(key, value) {
      var el;

      if (typeof value === 'undefined') {
        el = this[0];

        if (el) {
          if (el.dataStorage) return el.dataStorage[key];
          var val = el.getAttribute("data-".concat(key));
          return /^\d+\.?\d*$/.test(val) ? +val : val;
        }

        return undefined;
      } // Set value


      for (var i = 0; i < this.length; i++) {
        el = this[i];
        if (!el.dataStorage) el.dataStorage = {};
        el.dataStorage[key] = value;
      }

      return this;
    },

    /**
     * ********************************************
     * insert node
     * ********************************************
     */

    /**
     * appendChild
     * @param args
     * @return {ZxQuery}
     */
    append: function append() {
      var newChild;

      for (var k = 0; k < arguments.length; k++) {
        newChild = k < 0 || arguments.length <= k ? undefined : arguments[k];

        for (var i = 0; i < this.length; i++) {
          if (typeof newChild === 'string') {
            var tempDiv = doc.createElement('div');
            tempDiv.innerHTML = newChild;

            while (tempDiv.firstChild) {
              this[i].appendChild(tempDiv.firstChild);
            }
          } else if (newChild instanceof ZxQuery) {
            for (var j = 0; j < newChild.length; j++) {
              this[i].appendChild(newChild[j]);
            }
          } else {
            this[i].appendChild(newChild);
          }
        }
      }

      return this;
    },

    /**
     * append child to parent
     * @param parent
     * @return {ZxQuery}
     */
    appendTo: function appendTo(parent) {
      $(parent).append(this);
      return this;
    },

    /**
     * insert after selector
     * @param selector
     */
    insertAfter: function insertAfter(selector) {
      var after = $(selector);

      for (var i = 0; i < this.length; i++) {
        if (after.length === 1) {
          after[0].parentNode.insertBefore(this[i], after[0].nextSibling);
        } else if (after.length > 1) {
          for (var j = 0; j < after.length; j++) {
            after[j].parentNode.insertBefore(this[i].cloneNode(true), after[j].nextSibling);
          }
        }
      }
    },
    insertBefore: function insertBefore(selector) {
      var before = $(selector);

      for (var i = 0; i < this.length; i++) {
        if (before.length === 1) {
          before[0].parentNode.insertBefore(this[i], before[0]);
        } else if (before.length > 1) {
          for (var j = 0; j < before.length; j++) {
            before[j].parentNode.insertBefore(this[i].cloneNode(true), before[j]);
          }
        }
      }
    },
    prepend: function prepend(newChild) {
      var i;
      var j;

      for (i = 0; i < this.length; i++) {
        if (typeof newChild === 'string') {
          var tempDiv = doc.createElement('div');
          tempDiv.innerHTML = newChild;

          for (j = tempDiv.childNodes.length - 1; j >= 0; j--) {
            this[i].insertBefore(tempDiv.childNodes[j], this[i].childNodes[0]);
          }
        } else if (newChild instanceof ZxQuery) {
          for (j = 0; j < newChild.length; j++) {
            this[i].insertBefore(newChild[j], this[i].childNodes[0]);
          }
        } else {
          this[i].insertBefore(newChild, this[i].childNodes[0]);
        }
      }

      return this;
    },
    prependTo: function prependTo(parent) {
      $(parent).prepend(this);
      return this;
    },

    /**
     * replace child
     * @param selector
     * @return {ZxQuery}
     */
    replace: function replace(selector) {
      var newChild = $(selector);
      var parentNode;

      for (var i = 0; i < this.length; i++) {
        parentNode = this[i].parentNode;

        if (parentNode) {
          parentNode.replaceChild(newChild[0].cloneNode(true), this[i]);
        }
      }

      return this;
    },

    /**
     * remove item
     * @param index
     * @return {ZxQuery}
     */
    remove: function remove(index) {
      var el;

      if (typeof index !== 'number') {
        for (var i = 0; i < this.length; i++) {
          el = this[i];
          if (el.parentNode) el.parentNode.removeChild(el);
        }
      } else {
        el = this[index];
        if (el && el.parentNode) el.parentNode.removeChild(el);
      }

      return this;
    },

    /**
     * ********************************************
     * check
     * ********************************************
     */

    /**
     * is
     * @param selector
     * @return {*}
     */
    is: function is(selector) {
      var el = this[0];
      var compareWith;
      var i;
      if (!el || typeof selector === 'undefined') return false;

      if (typeof selector === 'string') {
        if (el.matches) return el.matches(selector);else if (el.webkitMatchesSelector) return el.webkitMatchesSelector(selector);else if (el.msMatchesSelector) return el.msMatchesSelector(selector);
        compareWith = $(selector);

        for (i = 0; i < compareWith.length; i++) {
          if (compareWith[i] === el) return true;
        }

        return false;
      } else if (selector === doc) return el === doc;else if (selector === win) return el === win;

      if (selector.nodeType || selector instanceof ZxQuery) {
        compareWith = selector.nodeType ? [selector] : selector;

        for (i = 0; i < compareWith.length; i++) {
          if (compareWith[i] === el) return true;
        }

        return false;
      }

      return false;
    },
    isTextNode: function isTextNode() {
      var el = this[0];
      if (!el || !el.nodeType) return false;
      return el.nodeType === 3;
    },

    /**
     * 子元素(不包含br)是否为空
     * @return {boolean}
     */
    isEmpty: function isEmpty() {
      return !util.strip(this.text()) && !this.find('img, video, audio')[0];
    },

    /**
     * target是否为this的第一个子元素
     * @param target
     * @return {boolean}
     */
    isFirstChildren: function isFirstChildren(target) {
      return this.children().eq(0).is(target);
    },

    /**
     * target是否为this的最后一个子元素
     * @param target
     * @return {boolean}
     */
    isLastChildren: function isLastChildren(target) {
      return this.children().eq(-1).is(target);
    },
    indexOf: function indexOf(el) {
      if (el instanceof ZxQuery) el = el[0];

      for (var i = 0; i < this.length; i++) {
        if (this[i] === el) return i;
      }

      return -1;
    },

    /**
     * this is in $parent children
     * @param $parent
     * @return {boolean}
     */
    // isInChild ($parent) {
    //   let el = this[0]
    //   let childNodes = $parent[0].childNodes
    //   let tmp
    //   for (let i = 0; i < childNodes.length; i++) {
    //     tmp = childNodes[i]
    //     if (tmp === el) return true
    //     if (tmp.nodeType === 1 && this.isInChild($(tmp))) return true
    //   }
    //   return false
    // },

    /**
     * every
     * @param fn
     * @return {boolean}
     */
    every: function every(fn) {
      if (typeof fn !== 'function') throw new TypeError("every(fn) fn is not a function");
      var len = this.length;
      var thisArg = arguments.length >= 2 ? arguments[1] : void 0;

      for (var i = 0; i < len; i++) {
        if (fn.call(thisArg, $(this[i]), i)) return false;
      }

      return true;
    },

    /**
     * ********************************************
     * content
     * ********************************************
     */
    text: function text(_text) {
      if (typeof _text === 'undefined') {
        if (this[0]) {
          return this[0].textContent.trim();
        }

        return '';
      }

      for (var i = 0; i < this.length; i++) {
        this[i].textContent = _text;
      }

      return this;
    },
    html: function html(_html) {
      if (typeof _html === 'undefined') {
        return this[0] ? this[0].innerHTML : '';
      }

      for (var i = 0; i < this.length; i++) {
        this[i].innerHTML = _html;
      }

      return this;
    },

    /**
     * ********************************************
     * node
     * ********************************************
     */
    nodeName: function nodeName() {
      return this[0] ? this[0].nodeName.toLowerCase() : null;
    },
    // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
    nodeType: function nodeType() {
      return this[0] ? this[0].nodeType : 0;
    },
    changeNodeName: function changeNodeName$1(nodeName) {
      for (var i = 0; i < this.length; i++) {
        // change nodeName
        this[i] = changeNodeName(this[i], nodeName);
      }

      return this;
    },

    /**
     * ********************************************
     * visible
     * ********************************************
     */
    hide: function hide() {
      for (var i = 0; i < this.length; i++) {
        this[i].style.display = 'none';
      }

      return this;
    },
    show: function show() {
      for (var i = 0; i < this.length; i++) {
        var el = this[i];

        if (el.style.display === 'none') {
          el.style.display = '';
        }

        if (win.getComputedStyle(el, null).getPropertyValue('display') === 'none') {
          // Still not visible
          el.style.display = 'block';
        }
      }

      return this;
    },

    /**
     * ********************************************
     * Events
     * ********************************************
     */
    on: function on() {
      for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
        args[_key] = arguments[_key];
      }

      var eventType = args[0],
          fn = args[1],
          useCapture = args[2];
      var el;

      for (var i = 0; i < this.length; i++) {
        el = this[i];

        if (el.nodeType === 1 || el === doc || el === win) {
          addEventListener(el, eventType, fn, useCapture);
        }
      }

      return this;
    },
    off: function off() {
      for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
        args[_key2] = arguments[_key2];
      }

      var eventType = args[0],
          fn = args[1],
          useCapture = args[2];
      var el;

      for (var i = 0; i < this.length; i++) {
        el = this[i];

        if (el.nodeType === 1 || el === doc || el === win) {
          removeEventListener(el, eventType, fn, useCapture);
        }
      }

      return this;
    },
    trigger: function trigger() {
      for (var _len3 = arguments.length, args = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
        args[_key3] = arguments[_key3];
      }

      var events = args[0].split(' ');
      var eventData = args[1];

      for (var i = 0; i < events.length; i++) {
        var event = events[i];

        for (var j = 0; j < this.length; j++) {
          var el = this[j];
          var evt = void 0;

          try {
            evt = new win.CustomEvent(event, {
              detail: eventData,
              bubbles: true,
              cancelable: true
            });
          } catch (e) {
            evt = doc.createEvent('Event');
            evt.initEvent(event, true, true);
            evt.detail = eventData;
          }

          el.dispatchEvent(evt);
        }
      }

      return this;
    },

    /**
     * ********************************************
     * css
     * ********************************************
     */
    css: function css(props, value) {
      var i;

      if (arguments.length === 1) {
        if (typeof props === 'string') {
          if (this[0]) return win.getComputedStyle(this[0], null).getPropertyValue(props);
          return void 0;
        } else {
          for (i = 0; i < this.length; i += 1) {
            for (var prop in props) {
              this[i].style[prop] = props[prop];
            }
          }
        }
      }

      if (arguments.length === 2 && typeof props === 'string') {
        for (i = 0; i < this.length; i += 1) {
          this[i].style[props] = value;
        }
      }

      return this;
    },
    styles: function styles() {
      if (this[0]) return win.getComputedStyle(this[0], null);
      return {};
    },

    /**
     * ********************************************
     * size
     * ********************************************
     */
    width: function width() {
      if (this[0] === win) return win.innerWidth;
      return this.length > 0 ? parseFloat(this.css('width')) : null;
    },
    outerWidth: function outerWidth(includeMargins) {
      if (this.length > 0) {
        var offsetWidth = this[0].offsetWidth;

        if (includeMargins) {
          var styles = this.styles();
          return offsetWidth + parseFloat(styles.getPropertyValue('margin-right')) + parseFloat(styles.getPropertyValue('margin-left'));
        }

        return offsetWidth;
      }

      return null;
    },
    height: function height() {
      if (this[0] === win) return win.innerHeight;
      return this.length > 0 ? parseFloat(this.css('height')) : null;
    },
    outerHeight: function outerHeight(includeMargins) {
      if (this.length > 0) {
        var offsetHeight = this[0].offsetHeight;

        if (includeMargins) {
          var styles = this.styles();
          return offsetHeight + parseFloat(styles.getPropertyValue('margin-top')) + parseFloat(styles.getPropertyValue('margin-bottom'));
        }

        return offsetHeight;
      }

      return null;
    },
    offset: function offset() {
      if (this.length > 0) {
        var el = this[0];
        var box = el.getBoundingClientRect();
        var body = doc.body;
        var clientTop = el.clientTop || body.clientTop || 0;
        var clientLeft = el.clientLeft || body.clientLeft || 0;
        var scrollTop = el === win ? win.scrollY : el.scrollTop;
        var scrollLeft = el === win ? win.scrollX : el.scrollLeft;
        return {
          top: box.top + scrollTop - clientTop,
          left: box.left + scrollLeft - clientLeft
        };
      }

      return null;
    },

    /**
     * ********************************************
     * for
     * ********************************************
     */
    each: function each(fn) {
      if (!fn) return this;

      for (var i = 0; i < this.length; i++) {
        if (fn.call(this[i], i, this[i]) === false) {
          return this;
        }
      }

      return this;
    },

    /**
     * ********************************************
     * scroll
     * ********************************************
     */
    scrollTop: function scrollTop(value) {
      if (!this.length) return;
      var hasScrollTop = ('scrollTop' in this[0]);
      if (value === undefined) return hasScrollTop ? this[0].scrollTop : this[0].pageYOffset;
      return this.each(hasScrollTop ? function () {
        this.scrollTop = value;
      } : function () {
        this.scrollTo(this.scrollX, value);
      });
    }
  };

  var $ = function $(selector, context) {
    return new ZxQuery(selector, context);
  };

  /**
   * Created by Capricorncd.
   * Date: 2019/04/15 13:14
   * Copyright © 2017-present, https://github.com/capricorncd
   */
  /**
   * Cursor Class
   * @param $parent
   * @constructor
   */

  function CursorClass($parent) {
    // Limit focus range
    this.$parent = $parent;
    this.selection = null;
    this.range = null;
    this.currentNode = null;
    this.timer = null;
    var $el = $parent.lastChild(); // init offset

    this.offset = $el.text().length || 0;
    if ($el[0]) this.setRange($el[0], this.offset);
  }
  /**
   * prototype
   * @type {{constructor: CursorClass, init(*=): void, setRange(*=, *=): void, getRange(): *}}
   */


  CursorClass.prototype = {
    /**
     * constructor
     */
    constructor: CursorClass,
    init: function init() {
      this.selection = window.getSelection();

      try {
        this.range = this.selection.getRangeAt(0);
      } catch (e) {
        this.range = new Range();
      }
    },

    /**
     * 设置或创建光标位置
     * @param el
     * @param offset
     */
    setRange: function setRange(el, offset) {
      var _this = this;

      if (el instanceof ZxQuery) {
        el = el[0];
      }

      if (this.selection === null) {
        this.init();
      } else {
        // remove all range object
        this.selection.removeAllRanges();
      } // 光标移动到到原来的位置加上新内容的长度


      el = el || this.currentNode;

      if (el) {
        // el: '<section>inner text.</section>'
        var targetNode = el.childNodes[el.childNodes.length - 1] || el; // check img/video/audio
        // console.log(targetNode.nodeName, this.offset)

        if (/IMG|VIDEO|AUDIO/.test(targetNode.nodeName)) {
          this.offset = 1; // get parentNode, can't set offset = 1 of IMG node.

          targetNode = targetNode.parentNode;
        } else if (typeof offset === 'undefined') {
          // get min offset, because offset cannot exceed the length of targetNode.textContent
          this.offset = Math.min($(targetNode).text().length, this.offset);
        } else {
          this.offset = offset;
        }

        this.range.setStart(targetNode, this.offset);
        this.currentNode = el;
      } // cursor start and end position is collapse


      this.range.collapse(true);

      if (this.timer) {
        clearTimeout(this.timer);
        this.timer = null;
      } // 延时执行,键盘自动收起后再触发focus


      this.timer = setTimeout(function () {
        // 插入新的光标对象
        _this.selection.addRange(_this.range);
      }, 100);
    },

    /**
     * get cursor node in $parent
     * @param needElement Want to get element node, not ZxQuery object
     * @return {*}
     */
    getCurrentNode: function getCurrentNode(needElement) {
      // 获取选定对象
      // this.selection = window.getSelection()
      // 设置最后光标对象
      try {
        this.range = this.selection.getRangeAt(0);
      } catch (e) {
        this.range = new Range();
      }

      this.currentNode = this.range.endContainer;
      this.offset = this.range.startOffset; // Check whether currentNode is in the this.$parent

      var $currentNode = $(this.currentNode).findParentFrom(this.$parent);
      var $current = $currentNode || this.$parent.lastChild();
      return needElement ? $current[0] : $current;
    }
  };

  /**
   * Created by Capricorncd.
   * User: https://github.com/capricorncd
   * Date: 2019/04/21 22:42
   */

  var fn = function fn() {};

  var DEF_OPTS = {
    head: null,
    // className
    className: '',
    // head setup
    headHeight: 44,
    headLeftBtnClassName: '',
    headLeftBtnText: '',
    headTitle: 'Expansion Panel',
    // height
    height: 260,
    // body内容
    body: null,
    // Used to distinguish ExpansionPanel instance
    name: 'expansion-panel',
    onHeadClick: fn,
    onBodyClick: fn,
    onBeforeShow: fn,
    onShow: fn,
    onBeforeHide: fn,
    onHide: fn
  };
  /**
   * ExpansionPanel
   * Extension panel fixed at the bottom of the editor
   * @param options
   * @param zxEditor ZxEditor instance
   * @constructor
   */

  function ExpansionPanel(options, zxEditor) {
    var _this = this;

    if (!zxEditor instanceof ZxEditor) {
      throw new TypeError("new ExpansionPanel() parameter error, arguments[1] is not a ZxEditor instance.");
    } // options


    var opts = this.options = Object.assign({}, DEF_OPTS, options); // editor instance

    this.editorInstance = zxEditor;
    zxEditor.expansionPanels.push(this); // visible

    this.visible = false; // Used to distinguish ExpansionPanel instance

    this.name = opts.name;
    this.$body = $("<div class=\"body-wrapper\" style=\"height:".concat(opts.height - opts.headHeight, "px;\"></div>")); // node

    this.$el = $("<div class=\"zx-editor-expansion-panel border-top\"></div>"); // click
    // stop propagation

    zxEditor.$eventHandlers[this.name] = {
      $target: this.$el,
      type: 'click',
      handler: function handler(e) {
        e.stopPropagation();
      }
    };
    this.$el.on('click', zxEditor.$eventHandlers[this.name].handler);

    if (opts.headHeight > 0) {
      // custom head
      if (opts.head) {
        this.$head = $(opts.head);
      } else {
        // default head
        this.$head = $("<div class=\"head-wrapper border-bottom ".concat(opts.headAlign, "\" style=\"height:").concat(opts.headHeight, "px;line-height:").concat(opts.headHeight, "px;\"><div class=\"l cur ").concat(opts.headLeftBtnClassName, "\">").concat(opts.headLeftBtnText, "</div>").concat(opts.headTitle || '', "</div>")); // left btn

        var $leftBtn = this.$head.find('.l');
        zxEditor.$eventHandlers[this.name + 'HeadLeftBtn'] = {
          $target: $leftBtn,
          type: 'click',
          handler: function handler(e) {
            opts.onHeadClick.call(_this, e, 'left-button');
          }
        };
        $leftBtn.on('click', zxEditor.$eventHandlers[this.name + 'HeadLeftBtn'].handler);
        var $switch = $("<i class=\"switch\" style=\"width:".concat(opts.headHeight, "px;height:").concat(opts.headHeight, "px;\"></i>"));
        this.$head.append($switch); // switch event

        zxEditor.$eventHandlers[this.name + 'HeadSwitch'] = {
          $target: $switch,
          type: 'click',
          handler: function handler(e) {
            opts.onHeadClick.call(_this, e, 'switch');

            _this.hide();
          }
        };
        $switch.on('click', zxEditor.$eventHandlers[this.name + 'HeadSwitch'].handler);
      }

      this.$el.append(this.$head);
      this.$head.on('click', function (e) {
        opts.onHeadClick.call(_this, e);
      });
    } // body


    try {
      this.$body.append($(opts.body));
    } catch (e) {
      throw e;
    }

    this.$body.on('click', function (e) {
      opts.onBodyClick.call(_this, e);
    });
    this.$el.append(this.$body); // append to $editor

    zxEditor.$editor.append(this.$el); // init

    this.init(this.options);
  }

  ExpansionPanel.prototype = {
    /**
     * constructor
     */
    constructor: ExpansionPanel,

    /**
     * init position
     * Used to window.onresize
     * @param options
     */
    init: function init(options) {
      options = options || this.options;
      var winHeight = window.innerHeight;
      this.$el.css({
        height: "".concat(options.height, "px"),
        top: winHeight + 'px'
      });
    },

    /**
     * show
     */
    show: function show() {
      if (!this.visible) {
        this.options.onBeforeShow.call(this);
        this.$el.removeClass('out').addClass('in');
        this.visible = true;
        this.editorInstance.emit('expansionPanelShow', this, this.editorInstance);
        this.options.onShow.call(this);
      }
    },

    /**
     * hide
     */
    hide: function hide() {
      if (this.visible) {
        this.options.onBeforeHide.call(this);
        this.$el.removeClass('in').addClass('out');
        this.visible = false;
        this.editorInstance.emit('expansionPanelHide', this, this.editorInstance);
        this.options.onHide.call(this);
      }
    }
  };

  /**
   * Created by Capricorncd.
   * User: https://github.com/capricorncd
   * Date: 2019/04/29 21:08
   */
  function changeTag(tag) {
    // same tag
    if (this.$cursorNode.nodeName() === tag) return;
    var cursorNode = this.$cursorNode[0];
    var childNodes = cursorNode.childNodes;
    var len = childNodes.length; // to ul

    if (tag === 'ul') {
      var ul = doc.createElement('ul');
      cloneAttrs(ul, cursorNode);

      for (var i = 0; i < len; i++) {
        ul.appendChild(createLi(childNodes[i].cloneNode(true)));
      }

      cursorNode.parentNode.replaceChild(ul, cursorNode); // save current node

      this.$cursorNode = $(ul);
      return;
    } // change ul


    if (cursorNode.nodeName === 'UL') {
      var li, el;

      for (var _i = 0; _i < len; _i++) {
        li = childNodes[_i].cloneNode(true);
        cloneAttrs(li, cursorNode);
        el = this.changeNodeName(li, tag);
        $(el).insertBefore(this.$cursorNode);
      } // remove old ul


      this.$cursorNode.remove();
      this.$cursorNode = $(el);
      return;
    }

    this.$cursorNode.changeNodeName(tag);
    this.cursor.setRange(this.$cursorNode);
  }

  function createLi(child) {
    var li = doc.createElement('li');
    li.appendChild(child);
    return li;
  }

  function cloneAttrs(target, source) {
    var style, id, className;
    style = source.getAttribute('style');
    id = source.id;
    className = source.className;
    if (style) target.setAttribute('style', style);
    if (id) target.id = id;
    if (className) target.className = className;
  }

  /**
   * Created by Capricorncd.
   * User: https://github.com/capricorncd
   * Date: 2019/04/21 10:33
   */
  var IPHONEX_BOTTOM_OFFSET_HEIGHT = 34;

  /**
   * Created by Capricorncd.
   * User: https://github.com/capricorncd
   * Date: 2019/04/27 21:11
   */

  var DEF_COLORS = ['#333', '#d0d0d0', '#ff583d', '#fdaa25', '#44c67b', '#14b2e0', '#b065e2'];
  /**
   * create color vnode
   * @param colors
   * @return Object
   */

  function createColorVNode(colors) {
    var arr = [];
    colors.forEach(function (color, i) {
      if (/^#\w{3,6}$/.test(color)) {
        arr.push({
          tag: 'dd',
          attrs: {
            "class": i === 0 ? 'active' : '',
            'data-color': formatColorHexadecimal(color.toLowerCase())
          },
          child: [{
            tag: 'i',
            attrs: {
              style: "background:".concat(color)
            }
          }]
        });
      }
    });
    return arr;
  }

  function formatColorHexadecimal(hex) {
    var len = hex.length;
    return len === 7 ? hex : "#".concat(hex[1]).concat(hex[1]).concat(hex[2]).concat(hex[2]).concat(hex[3]).concat(hex[3]);
  }
  /**
   * text style panel
   * @param options
   * @return {{name: string, className: string, events: {type: string, handler: events.handler}}}
   */


  function styleExpansionPanel(options) {
    var _this = this;

    var zxEditor = this; // dom structure

    var panelBodyChild = [];
    var styleNode = {
      tag: 'dl',
      attrs: {
        "class": '__style-wrapper border-bottom'
      },
      child: [{
        tag: 'dd',
        attrs: {
          style: 'font-weight: 800;',
          'data-style': 'fontWeight:800'
        },
        child: ['B']
      }, {
        tag: 'dd',
        attrs: {
          style: 'font-style: italic;',
          'data-style': 'fontStyle:italic'
        },
        child: ['I']
      }, {
        tag: 'dd',
        attrs: {
          style: 'text-decoration: line-through;',
          'data-style': 'textDecoration:line-through'
        },
        child: ['abc']
      }, {
        tag: 'dd',
        attrs: {
          style: '',
          'data-style': 'textAlign:left',
          "class": 'text-align--l'
        }
      }, {
        tag: 'dd',
        attrs: {
          style: '',
          'data-style': 'textAlign:center',
          "class": 'text-align--c'
        }
      }, {
        tag: 'dd',
        attrs: {
          style: '',
          'data-style': 'textAlign:right',
          "class": 'text-align--r'
        }
      }]
    };
    panelBodyChild.push(styleNode);
    var COLORS = Array.isArray(options.textStyleColors) ? options.textStyleColors : DEF_COLORS;

    if (COLORS.length) {
      var colorsNode = {
        tag: 'dl',
        attrs: {
          "class": '__color-wrapper border-bottom'
        },
        child: createColorVNode(COLORS)
      };
      panelBodyChild.push(colorsNode);
    }

    var tagNode = {
      tag: 'dl',
      attrs: {
        "class": '__tag-wrapper'
      },
      child: [{
        tag: 'dd',
        attrs: {
          "class": '__h2',
          'data-tag': 'h2'
        },
        child: ['大标题', {
          tag: 'i'
        }]
      }, {
        tag: 'dd',
        attrs: {
          "class": '__h4',
          'data-tag': 'h4'
        },
        child: ['小标题', {
          tag: 'i'
        }]
      }, {
        tag: 'dd',
        attrs: {
          "class": '__section active',
          'data-tag': 'section'
        },
        child: ['正文', {
          tag: 'i'
        }]
      }, {
        tag: 'dd',
        attrs: {
          "class": '__blockquote',
          'data-tag': 'blockquote'
        },
        child: ['引用', {
          tag: 'i'
        }]
      }, {
        tag: 'dd',
        attrs: {
          "class": '__ul',
          'data-tag': 'ul'
        },
        child: ['无序列表', {
          tag: 'i'
        }]
      }]
    };
    panelBodyChild.push(tagNode);
    var panelBody = createVdom({
      tag: 'div',
      attrs: {
        "class": 'text-style-outer-wrapper'
      },
      child: panelBodyChild
    });
    var $panelBody = $(panelBody); // instance text style

    this.textStylePanel = new this.ExpansionPanel({
      headLeftBtnText: options.textStyleHeadLeftBtnText,
      headTitle: options.textStyleTitle,
      headAlign: options.textStyleHeadAlign,
      body: $panelBody,
      onHeadClick: handleHeadClick
    }, this); // handle events
    // style

    var $styles = $panelBody.find('.__style-wrapper').children();
    $styles.on('click', function () {
      var style = $(this).data('style').split(':');
      var key = style[0];
      var cursorNode = zxEditor.$cursorNode[0];
      cursorNode.style[key] = cursorNode.style[key] === style[1] ? '' : style[1];
      zxEditor.cursor.setRange();
      zxEditor.$content.trigger('input');
    }); // color

    var $colorsParent = $panelBody.find('.__color-wrapper');
    var $colors = $colorsParent.children();
    $colors.on('click', function () {
      var $el;

      for (var i = 0; i < $colors.length; i++) {
        $el = $($colors[i]);

        if ($el[0] === this && !$el.hasClass('active')) {
          $el.addClass('active');
          zxEditor.$cursorNode.css('color', $el.data('color'));
        } else if ($el.hasClass('active')) {
          $el.removeClass('active');
        }
      }

      zxEditor.cursor.setRange();
      zxEditor.$content.trigger('input');
    }); // tag

    var $tagsParent = $panelBody.find('.__tag-wrapper');
    var $tags = $tagsParent.children();
    $tags.on('click', function () {
      if ($(this).hasClass('active')) return;
      var tag = $(this).data('tag');
      var $el;

      for (var i = 0; i < $tags.length; i++) {
        $el = $($tags[i]);

        if ($el[0] === this) {
          // add active class name
          $el.addClass('active'); // change tag

          changeTag.call(zxEditor, tag);
        } else if ($el.hasClass('active')) {
          $el.removeClass('active');
        }
      }

      zxEditor.$content.trigger('input');
    });

    if (this.isIPhoneX) {
      $tagsParent.css('margin-bottom', IPHONEX_BOTTOM_OFFSET_HEIGHT + 'px');
    } // extend method

    /**
     * reset active state
     * when content onClick and onKeyup code === 13
     */


    this.textStylePanel.resetActiveState = function () {
      var $cursorNode = _this.cursor.getCurrentNode(); // check tag


      var tag = $cursorNode.nodeName();
      setTagInPanel(tag); // check color

      var color = _this.rgbToHex($cursorNode.css('color'));

      setColorInPanel(color);
    };
    /**
     * set tag in panel
     * @param tag
     */


    function setTagInPanel(tag) {
      var $activeTag = $tagsParent.find('.active');

      if ($activeTag.data('tag') !== tag) {
        $activeTag.removeClass('active');
        $tagsParent.find(".__".concat(tag)).addClass('active');
      }
    }
    /**
     * set color in panel
     * @param color
     */


    function setColorInPanel(color) {
      var $activeColor = $colorsParent.find('.active');

      if ($activeColor.data('color') !== color) {
        $activeColor.removeClass('active');
        var $tmp;

        for (var i = 0; i < $colors.length; i++) {
          $tmp = $($colors[i]);

          if ($tmp.data('color') === color) {
            $tmp.addClass('active');
            break;
          }
        }
      }
    }
    /**
     * handle head click
     * @param type
     */


    function handleHeadClick(type) {
      // clear style
      if (type === 'left-button') {
        // clear style
        var currentNode = zxEditor.$cursorNode[0];
        currentNode.className = '';
        currentNode.setAttribute('style', '');

        if (currentNode.nodeName !== 'SECTION') {
          zxEditor.$cursorNode = $(zxEditor.changeNodeName(currentNode, 'section'));
        } // reset text style expansion


        setColorInPanel(COLORS[0]);
        setTagInPanel('section'); // set Range

        zxEditor.cursor.setRange(zxEditor.$cursorNode);
      }
    }

    return {
      name: 'text-style',
      className: 'text-style-btn',
      events: {
        type: 'click',
        handler: function handler() {
          _this.textStylePanel.show();
        }
      }
    };
  }

  /**
   * Created by Capricorncd.
   * User: https://github.com/capricorncd
   * Date: 2019/05/04 00:09
   */
  function textStyle() {
    this.toolbar.addButton(styleExpansionPanel.call(this, this.options));
  }

  /**
   * Created by Capricorncd.
   * User: https://github.com/capricorncd
   * Date: 2019/04/27 21:19
   */
  function selectPictureBtn() {
    var _this = this;

    var options = this.options;
    /**
     * *******************************************
     * select picture
     * image file handler
     * *******************************************
     */
    // select picture

    var $selectPictureLabel = $('<label class="toolbar-icon-pic" style="position:absolute;top:0;left:0;width:100%;height:100%;"></label>');
    var $selectPictrueInput = $('<input type="file" accept="image/*" style="display:none">');
    $selectPictureLabel.append($selectPictrueInput);
    var input = $selectPictrueInput[0]; // register selectPictureInputClick

    this.$eventHandlers['selectPictureInputClick'] = {
      $target: $selectPictrueInput,
      type: 'click',
      handler: function handler() {
        input.value = '';
      },
      capture: false
    }; // image section template

    var imageSectionTemplate = /^<\w+\b.*<\/\w+>$/.test(options.imageSectionTemp) ? options.imageSectionTemp : "<section><img src=\"{url}\"></section>"; // register selectPictureInputChange

    var imageOptions = Object.assign({}, _this.options, {
      width: options.imageMaxWidth
    });
    this.$eventHandlers['selectPictureInputChange'] = {
      $target: $selectPictrueInput,
      type: 'change',
      handler: function handler(e) {
        var file = input.files[0];

        _this.emit('selectPictureInputChange', file, e, _this); // customize Picture Handler


        if (options.customPictureHandler) return; // handler picture

        _this.fileToBase64(file, imageOptions).then(function (res) {
          // console.log(res)
          var $el = $(imageSectionTemplate.replace('{url}', res.base64)); // set attribute

          $el.find('img').attr({
            id: 'zxEditor_img_' + +new Date(),
            alt: file.name
          }); // insert to $content

          _this.insertElm($el);
        })["catch"](function (e) {
          _this.emit('error', e, 'fileToBase64');
        });
      },
      capture: false
    };
    this.toolbar.addButton({
      name: 'select-picture',
      el: $selectPictureLabel
    });
  }

  var DEF_BTN_OPTS = {
    name: '',
    // button className
    className: '',
    // ElementHTML or $()
    el: null,
    // innerHTML
    innerHtml: '',
    // style
    style: '',
    // obj{type: 'click', handler: fn, capture: false} or array[obj1, obj2]
    events: null
  };

  function Toolbar(options, zxEditor) {
    // zxEditor instance
    this.editorInstance = zxEditor; // options

    this.options = options; // visible

    this.visible = options.toolbarBeenFixed; // create element

    this.height = options.toolbarHeight;
    this.$el = $("<div class=\"zx-editor-toolbar-wrapper border-top ".concat(this.visible ? 'in' : 'out', "\" style=\"height:").concat(this.height + (util.isIPhoneX ? IPHONEX_BOTTOM_OFFSET_HEIGHT : 0), "px;\"><dl class=\"inner-wrapper\" style=\"height:").concat(this.height, "px;\"></dl></div>")); // append to $editor

    zxEditor.$editor.append(this.$el); // init

    this.init(options);
  }

  Toolbar.prototype = {
    /**
     * constructor
     */
    constructor: Toolbar,

    /**
     * Used to window.onresize
     * @param options
     */
    init: function init(options) {
      options = options || this.options;
      var winHeight = win.innerHeight;
      this.$el.css({
        top: winHeight + 'px'
      });
      if (this.visible) this.editorInstance.emit('toolbarShow', this, this.editorInstance);
    },

    /**
     * show
     */
    show: function show() {
      if (!this.visible) {
        // change className
        this.$el.removeClass('out').addClass('in');
        this.visible = true;
        this.editorInstance.emit('toolbarShow', this, this.editorInstance);
      }
    },

    /**
     * hide
     */
    hide: function hide() {
      if (this.visible) {
        // change className
        this.$el.removeClass('in').addClass('out');
        this.visible = false;
        this.editorInstance.emit('toolbarHide', this, this.editorInstance);
      }
    },

    /**
     * add button
     * @param opts
     * @param index Insert index
     */
    addButton: function addButton(opts, index) {
      var _this = this;

      // params
      var params = Object.assign({}, DEF_BTN_OPTS, opts); // name

      if (!params.name) params.name = 'toolbar-btn-' + +new Date(); // create $node

      var $btn = $("<dd class=\"icon-item\" data-name=\"".concat(params.name, "\" style=\"").concat(params.style, "\"></dd>")); // handle el

      if (params.el && _typeof(params.el) === 'object') {
        $btn.append($(params.el));
      } else {
        // handle innerHtml
        $btn.html(params.innerHtml);
      } // className


      if (params.className) {
        $btn.addClass(params.className);
      } // style


      var css = {};

      if (this.options.toolbarHeight) {
        css.width = css.height = this.options.toolbarHeight + 'px';
      }

      $btn.css(css); // insert to document

      var $btns = this.$el.find('.inner-wrapper').children();

      if (typeof index === 'number' && index < $btns.length) {
        $btn.insertBefore($btns[index]);
      } else {
        this.$el.find('.inner-wrapper').append($btn);
      } // events


      if (params.events) {
        var events;

        if (util.isObject(params.events)) {
          events = [];
          events.push(params.events);
        } else if (Array.isArray(params.events)) {
          events = params.events;
        } // register events


        var eventHandlerKey;
        var $eventHandlers = this.editorInstance.$eventHandlers;
        events.forEach(function (item) {
          if (item && typeof item.type === 'string' && typeof item.handler === 'function') {
            eventHandlerKey = "toolbarBtnEvent_".concat(item.name);
            $eventHandlers[eventHandlerKey] = {
              $target: $btn,
              type: item.type,
              handler: item.handler.bind(_this.editorInstance),
              capture: typeof item.capture === 'boolean' ? item.capture : false
            };
            $btn.on(item.type, $eventHandlers[eventHandlerKey].handler, $eventHandlers[eventHandlerKey].capture);
          } else {
            throw new TypeError("Function addButton(opts), opts.events's parameter error.");
          }
        });
      }
    }
  };

  /**
   * Created by Capricorncd.
   * User: https://github.com/capricorncd
   * Date: 2019/04/15 21:49
   */
  function initDom(options) {
    var _this = this;

    // content
    this.$content = $("<div class=\"zx-editor-content-wrapper is-empty\" contenteditable=\"".concat(options.editable, "\"><section><br></section></div>"));
    this.setContentHeight(options); // editor

    this.$editor = $("<div class=\"zx-editor\"></div>"); // append to $eidtor

    this.$editor.append(this.$content); // toolbar

    this.toolbar = new Toolbar(options, this); // append to $wrapper

    this.$wrapper.append(this.$editor); // get lineHeight

    this.lineHeight = util["int"](this.$content.styles().lineHeight); // plugin
    // sort btn

    options.toolbarBtns.forEach(function (name) {
      if (name === 'select-picture') {
        _this.plugin(selectPictureBtn);
      } else if (name === 'text-style') {
        _this.plugin(textStyle);
      }
    });
  }

  /**
   * Created by Capricorncd.
   * User: https://github.com/capricorncd
   * Date: 2019/04/15 21:46
   */
  function initStyle(options) {
    /**
     * *****************************************
     * append style
     * *****************************************
     */
    // main color
    var style = ".zx-editor .m-color{color:".concat(options.mainColor, " !important;}.zx-editor .text-style-outer-wrapper .__tag-wrapper dd i {border-color:").concat(options.mainColor, " !important;}"); // line-height, caret-color

    if (options.lineHeight || options.cursorColor || options.textColor) {
      var lineHeight = options.lineHeight ? "line-height:".concat(options.lineHeight, ";") : '';
      var caretColor = options.cursorColor ? "caret-color:".concat(options.cursorColor, ";") : '';
      var textColor = options.textColor ? "color:".concat(options.textColor, ";") : '';
      style += ".zx-editor .zx-editor-content-wrapper{".concat(lineHeight + caretColor + textColor, "}");
    }

    if (options.paragraphTailSpacing) {
      style += ".zx-editor .zx-editor-content-wrapper > *{margin-bottom:".concat(options.paragraphTailSpacing, ";}");
    } // placeholder


    if (options.placeholder || options.placeholderColor) {
      var content = options.placeholder ? "content:'".concat(options.placeholder, "';") : '';
      var color = options.placeholderColor ? "color:".concat(options.placeholderColor, ";") : '';
      style += ".zx-editor .zx-editor-content-wrapper.is-empty:before{".concat(content + color, "}");
    }

    if (options.borderColor) {
      style += ".zx-editor .border-top:before, .zx-editor .border-bottom:after {border-color: ".concat(options.borderColor);
    }
    /**
     * *****************************************
     * editor $content
     * *****************************************
     */


    $('head').append("<style type=\"text/css\">".concat(style, "</style>"));
  }

  /**
   * Created by Capricorncd.
   * User: https://github.com/capricorncd
   * Date: 2019/04/15 21:52
   */
  /**
   * 获取剪切文本内容
   * @param e event
   * @return {Promise<any>}
   */

  function getPasteText(e) {
    return new Promise(function (resolve) {
      var clipboardData = e.clipboardData;
      var pasteStr = '';

      if (!clipboardData) {
        resolve('');
        return;
      }

      var items = clipboardData.items; // console.log(e)
      // console.log(clipboardData)
      // console.log(items)

      if (items && items.length > 0) {
        (function () {
          var len = items.length;
          var count = 0;
          var i, item;

          for (i = 0; i < len; i++) {
            item = items[i]; // 获取文本内容

            if (/^text\/plain/.test(item.type)) {
              item.getAsString(function (str) {
                pasteStr += str;
                counter(count, len);
              });
            } else {
              counter(count, len);
            }
          }
        })();
      } else {
        resolve('');
      }
      /**
       * 计数
       * @param count
       * @param len
       */


      function counter(count, len) {
        count++;
        if (count === len) resolve(util.strip(pasteStr));
      }
    });
  }

  /**
   * Created by Capricorncd.
   * User: https://github.com/capricorncd
   * Date: 2019/04/15 21:53
   */
  /**
   * handle events
   */

  function handleEvents() {
    var $content = this.$content;
    var $window = $(win);
    var options = this.options;
    /**
     * ****************************************************
     * window on resize
     * ****************************************************
     */

    function windowResize(e) {
      this.emit('windowResize', e, this); // toolbar

      this.toolbar.init(); // expansion panels

      this.expansionPanels.forEach(function (item) {
        item.init();
      });
    }

    this.$eventHandlers.windowResize = {
      $target: $window,
      type: 'resize',
      handler: windowResize.bind(this)
    };
    /**
     * ****************************************************
     * content on paste
     * ****************************************************
     */

    function contentPaste(e) {
      var _this = this;

      this.emit('paste', e, this);

      if (!options.customPasteHandler) {
        e.preventDefault();
        getPasteText(e).then(function (str) {
          // 添加至焦点处
          _this.insertElm(util.removeHtmlTags(str));
        });
      }
    }

    this.$eventHandlers.contentPaste = {
      $target: $content,
      type: 'paste',
      handler: contentPaste.bind(this)
    };
    /**
     * ****************************************************
     * content on input
     * ****************************************************
     */

    function contentInput(e) {
      this.emit('input', e, this); // check empty in content

      this._checkEmpty(); // check cursor node position


      this.checkPosition(); // emit content on change

      this.emit('change', e, this);
    }

    this.$eventHandlers.contentInput = {
      $target: $content,
      type: 'input',
      handler: contentInput.bind(this)
    };
    /**
     * ****************************************************
     * content on focus
     * ****************************************************
     */

    function contentFocus(e) {
      this.emit('focus', e, this); // console.error('contentFocus')
      // hide all expansionPanels
      // this.expansionPanels.forEach(ep => {
      //   ep.hide()
      // })
      // toolbar

      if (!this.options.toolbarBeenFixed) {
        this.toolbar.show();
      }
    }

    this.$eventHandlers.contentFocus = {
      $target: $content,
      type: 'focus',
      handler: contentFocus.bind(this)
    };
    /**
     * ****************************************************
     * content on blur
     * ****************************************************
     */

    function contentBlur(e) {
      this.emit('blur', e, this); // save $cursorNode

      this.$cursorNode = this.cursor.getCurrentNode();

      this._checkChildSection(); // console.warn(this.$cursorNode[0])
      // toolbar


      if (!this.options.toolbarBeenFixed) {
        this.toolbar.hide();
      }
    }

    this.$eventHandlers.contentBlur = {
      $target: $content,
      type: 'blur',
      handler: contentBlur.bind(this)
    };
    /**
     * ****************************************************
     * content on click
     * ****************************************************
     */

    function contentClick(e) {
      this.emit('click', e, this); // save $cursorNode

      this.$cursorNode = this.cursor.getCurrentNode(); // check position

      this.checkPosition(); // textStylePanel is undefined, or is hide

      if (!this.textStylePanel || !this.textStylePanel.visible) return;
      this.textStylePanel.resetActiveState();
    }

    this.$eventHandlers.contentClick = {
      $target: $content,
      type: 'click',
      handler: contentClick.bind(this)
    };
    /**
     * ****************************************************
     * content on keydown
     * ****************************************************
     */

    function contentKeydown(e) {
      this.emit('keydown', e, this);
    }

    this.$eventHandlers.contentKeyup = {
      $target: $content,
      type: 'keydown',
      handler: contentKeydown.bind(this)
    };
    /**
     * ****************************************************
     * content on keyup
     * ****************************************************
     */

    function contentKeyup(e) {
      this.emit('keyup', e, this); // handle enter keyup

      if (e.key === 'Enter' || e.keyCode === 13) {
        // check section node
        this._checkChildSection(); // content on click


        contentClick.call(this);
      } // check position
      // this.checkPosition()

    }

    this.$eventHandlers.contentKeyup = {
      $target: $content,
      type: 'keyup',
      handler: contentKeyup.bind(this)
    };
    /**
     * ****************************************************
     * register events
     * ****************************************************
     */

    var evt;

    for (var key in this.$eventHandlers) {
      evt = this.$eventHandlers[key];
      evt.$target.on(evt.type, evt.handler, evt.capture);
    }
  }

  /**
   * Create by capricorncd
   * 2018/5/30 0030.
   */

  /**
   * on
   * @param notifyName
   * @param fn
   */
  function on(notifyName, fn) {
    if (!notifyName || typeof notifyName !== 'string' || !fn || typeof fn !== 'function') return;

    if (!this.customEvents[notifyName]) {
      this.customEvents[notifyName] = [];
    }

    this.customEvents[notifyName].push(fn);
  }
  /**
   * emit
   * @param notifyName
   */


  function emit(notifyName) {
    var notifyArr = this.customEvents[notifyName];
    if (!notifyArr) return;
    var args = Array.prototype.slice.call(arguments, 1);

    for (var i = 0; i < notifyArr.length; i++) {
      try {
        notifyArr[i].apply(null, args);
      } catch (e) {
        this.emit('error', e, 'emit');
      }
    }
  }
  /**
   * off
   * @param notifyName
   */


  function off(notifyName, fn) {
    if (this.customEvents[notifyName]) {
      if (typeof fn === 'function' && this.customEvents[notifyName]) {
        var index = this.customEvents[notifyName].findIndex(function (item) {
          return item === fn;
        });
        if (index >= 0) this.customEvents[notifyName].splice(index, 1);
      } else {
        this.customEvents[notifyName] = null;
        delete this.customEvents[notifyName];
      }
    }
  }

  /**
   * Created by Capricorncd.
   * User: https://github.com/capricorncd
   * Date: 2019/04/24 21:37
   */
  function getImageInfo(base64, file, opts, callback) {
    var img = doc.createElement('img');
    img.src = base64;
    img.setAttribute('alt', file.name); // 加载图片

    img.onload = function () {
      var defaultData = {
        element: img,
        base64: base64,
        width: img.naturalWidth || img.width,
        height: img.naturalHeight || img.height,
        type: file.type,
        size: file.size,
        name: file.name,
        file: file
      };
      callback(null, opts.orientation > 1 ? rotateAndToBase64(defaultData, opts.orientation) : defaultData);
    };

    img.onerror = callback;
  }
  /**
   * Decide whether the picture is rotated or not according to 'opts.orientation' value
   * @param raw
   * @param type
   * @returns {Object}
   */

  function rotateAndToBase64(raw, orientation) {
    var canvas = doc.createElement('canvas');
    var ctx = canvas.getContext('2d');
    var img = raw.element;
    var imgWidth = raw.width;
    var imgHeight = raw.height;
    canvas.width = imgWidth;
    canvas.height = imgHeight; // rotate image

    switch (orientation) {
      // 90deg
      case 6:
        canvas.width = imgHeight;
        canvas.height = imgWidth;
        ctx.rotate(Math.PI / 2); // (0, -imgHeight)

        ctx.drawImage(img, 0, -imgHeight, imgWidth, imgHeight);
        break;
      // 180deg

      case 3:
        ctx.rotate(Math.PI);
        ctx.drawImage(img, -imgWidth, -imgHeight, imgWidth, imgHeight);
        break;
      // -90(270)deg

      case 8:
        canvas.width = imgHeight;
        canvas.height = imgWidth;
        ctx.rotate(3 * Math.PI / 2);
        ctx.drawImage(img, -imgWidth, 0, imgWidth, imgHeight);
        break;

      default:
        ctx.drawImage(img, 0, 0, imgWidth, imgHeight);
    }

    var base64 = canvas.toDataURL(raw.type);
    img.src = base64;
    return Object.assign(raw, {
      element: canvas,
      base64: base64,
      width: canvas.width,
      height: canvas.height
    });
  }
  /**
   * 创建Canvas
   * @param el Image object or Canvas element
   * @param p clip options
   * @returns {Element}
   */


  function createCanvas(el, p) {
    var canvas = doc.createElement('canvas');
    canvas.width = p.cw;
    canvas.height = p.ch;
    var ctx = canvas.getContext('2d'); // 操作过于频繁,iPhone部分手机会获取不到ctx,is null
    // 下面代码会抛出异常

    ctx.drawImage(el, p.sx, p.sy, p.sw, p.sh, 0, 0, canvas.width, canvas.height);
    return canvas;
  }
  /**
   * 缩放比列
   * @param {Number} numerator
   * @param {Number} denominator
   */

  function ratio(numerator, denominator) {
    return parseInt(numerator / denominator * 10000) / 10000;
  }
  /**
   * Calculate the position and size of the generated image clipping
   * @param {Number} iw // image width
   * @param {Number} ih // image height
   * @param {Object} opts
   */


  function computeCropInfo(iw, ih, opts) {
    // target image width, height
    var targetWidth = util["int"](opts.width);
    var targetHeight = util["int"](opts.height); // image width or height, less than target width or height
    // don't resize

    if (!opts.forceImageResize && targetWidth > 0 && iw < targetWidth && targetHeight > 0 && ih < targetHeight) {
      return {
        sx: 0,
        sy: 0,
        sw: iw,
        sh: ih,
        scaling: 1,
        cw: iw,
        ch: ih
      };
    } // scaling


    var scaling = 1; // Pictures begin to crop in x, y coordinates

    var sx = 0;
    var sy = 0; // canvas

    var canvasWidth = iw;
    var canvasHieght = ih; // Picture Size after Equal Ratio Scaling

    var sw = 0;
    var sh = 0; // Scale to the right size, then cut in the middle

    if (targetWidth > 0 && targetHeight > 0) {
      // The size of canvas is to set the size for cutting
      canvasWidth = targetWidth;
      canvasHieght = targetHeight; // Adjust the size of the picture according to the target width: the width of the picture equals the width of the clipping frame

      sw = targetWidth;
      sh = Math.floor(targetWidth * ih / iw);
      scaling = ratio(iw, targetWidth); // Picture height beyond the clipping box, can be normal clipping

      if (sh >= targetHeight) {
        sx = 0;
        sy = util["int"]((sh - targetHeight) / 2 * scaling);
      } // To satisfy the clipping requirement, we need to resize: image height === clipping frame height
      else {
          scaling = ratio(ih, targetHeight);
          sw = Math.floor(targetHeight * iw / ih);
          sh = targetHeight;
          sx = util["int"]((sw - targetWidth) / 2 * scaling);
          sy = 0;
        }
    } // Zoom Picture Code **********************************
    // Only width is set
    else if (targetWidth > 0) {
        scaling = ratio(iw, targetWidth);
        canvasWidth = targetWidth;
        canvasHieght = Math.floor(targetWidth * ih / iw);
      } // Only height is set
      else if (targetHeight > 0) {
          scaling = ratio(ih, targetHeight);
          canvasWidth = Math.floor(targetHeight * iw / ih);
          canvasHieght = targetHeight;
        }

    return {
      sx: sx,
      sy: sy,
      sw: util["int"](canvasWidth * scaling),
      sh: util["int"](canvasHieght * scaling),
      scaling: scaling,
      cw: canvasWidth,
      ch: canvasHieght
    };
  }

  /**
   * Created by Capricorncd.
   * User: https://github.com/capricorncd
   * Date: 2019/04/24 21:13
   */
  var DEF_OPTIONS = {
    width: 0,
    heigth: 0,
    imageMaxSize: null,
    ignoreGif: true,
    forceResize: false
  };
  /**
   * base64 data to Blob
   * @param base64Data
   * @return {Blob}
   */

  function base64ToBlobData(base64Data) {
    // base64数据格式:
    // "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAkGB+wgHBgkIBwgKCgkLDRYPDQw//9k="
    var type, onlyData;

    if (/^data:(\w+\/\w+);base64,(.+)/.test(base64Data)) {
      type = RegExp.$1;
      onlyData = RegExp.$2;
    } else {
      throw new TypeError("base64ToBlobData(base64Data), base64Data non-base64 data!");
    }

    var data = win.atob(onlyData);
    var ia = new Uint8Array(data.length);

    for (var i = 0; i < data.length; i++) {
      ia[i] = data.charCodeAt(i);
    }

    return new Blob([ia], {
      type: type
    });
  }
  /**
   * file to base64 data
   * @param file
   * @param opts
   * @return {Promise<any>}
   */


  function fileToBase64(file, opts) {
    return new Promise(function (resolve, reject) {
      if (!file || !file instanceof File) {
        throw new TypeError("file is not a File object");
      }

      var options = Object.assign({}, DEF_OPTIONS, opts); // check file type

      if (!/image\/.*/i.test(file.type)) {
        reject(new TypeError("\"".concat(file.name, "\" is not Image File!")));
        return;
      } // rotate image


      if (typeof win.EXIF === 'undefined') {
        options.orientation = 0;
        handleFile(file, options).then(resolve)["catch"](reject);
      } else {
        win.EXIF.getData(file, function () {
          var info = win.EXIF.getAllTags(this) || {}; // Shooting direction

          options.orientation = info.Orientation;
          handleFile(file, options).then(resolve)["catch"](reject);
        });
      }
    });
  }
  /**
   * to blob url
   * @param blob Blob数据
   * @returns {*}
   */


  function blobToUrl(blob) {
    return win.URL.createObjectURL(blob);
  }

  function handleFile(file, opts) {
    return new Promise(function (resolve, reject) {
      var reader = new FileReader();
      reader.readAsDataURL(file);

      reader.onload = function () {
        // get image info
        getImageInfo(this.result, file, opts, function (e, raw) {
          if (e) {
            reject(e);
            return;
          } // check gif


          if (file.type === 'image/gif' && opts.ignoreGif) {
            raw.data = file;
            raw.url = blobToUrl(file);
            raw.raw = raw;
            resolve(raw);
            return;
          }

          var result = handleImageData(raw, opts); // check file size

          if (opts.imageMaxSize > 0 && result.size / 1024 > opts.imageMaxSize) {
            reject(new RangeError("File size \"".concat(result.size / 1024, "\" out of range, limit is ").concat(opts.imageMaxSize, "Kib")));
            return;
          }

          resolve(result);
        });
      };

      reader.onerror = reject;
    });
  }
  /**
   * Processing picture data, clipping and compression
   * @param raw
   * @param opts
   * @private
   */


  function handleImageData(raw, opts) {
    // file type
    var dataType = raw.type || raw.file.type; // Calculate the position and size of zooming or clipping pictures

    var res = computeCropInfo(raw.width, raw.height, opts); // image or canvas

    var el = raw.element;
    var scaling = 2;
    var sw = res.sw;
    var sh = res.sh;
    var sx = res.sx;
    var sy = res.sy;

    if (res.scaling > scaling) {
      scaling = res.scaling;

      while (scaling > 2) {
        el = createCanvas(el, {
          cw: res.cw * scaling,
          ch: res.ch * scaling,
          sx: sx,
          sy: sy,
          sw: sw,
          sh: sh
        });
        sw = el.width;
        sh = el.height;
        sx = sy = 0;
        scaling -= 1;
      }
    }

    el = createCanvas(el, {
      cw: res.cw,
      ch: res.ch,
      sx: sx,
      sy: sy,
      sw: sw,
      sh: sh
    });
    var base64 = el.toDataURL(dataType);
    var blob = base64ToBlobData(base64);
    return {
      element: el,
      type: dataType,
      width: res.cw,
      height: res.ch,
      data: blob,
      base64: base64,
      size: blob.size,
      url: blobToUrl(blob),
      // 原始图片数据
      raw: raw
    };
  }

  /**
   * Created by Capricorncd.
   * User: https://github.com/capricorncd
   * Date: 2019/04/16 20:28
   */
  function extendPrototypes(ZxEditor) {
    // custom events
    ZxEditor.prototype.on = on;
    ZxEditor.prototype.emit = emit;
    ZxEditor.prototype.off = off; // static method

    ZxEditor.prototype.getPasteText = getPasteText; // static utils

    for (var key in util) {
      if (util.hasOwnProperty(key)) {
        ZxEditor.prototype[key] = util[key];
      }
    }

    ZxEditor.prototype.blobToUrl = blobToUrl;
    ZxEditor.prototype.base64ToBlobData = base64ToBlobData;
    ZxEditor.prototype.fileToBase64 = fileToBase64; // dom

    ZxEditor.prototype.createElement = createElement;
    ZxEditor.prototype.createVdom = createVdom;
  }

  /**
   * Created by Capricorncd.
   * Date: 2019/04/12 11:12
   * Copyright © 2017-present, https://github.com/capricorncd
   */
  var DEF_OPTIONS$1 = {
    // 内容是否可以被编辑
    editable: true,
    // 编辑器输入内容绝对定位
    fixed: false,
    // editor min height
    // minHeight: window.innerHeight,
    // style
    placeholder: 'Enter...',
    placeholderColor: '',
    lineHeight: 1.5,
    // paragraph tail spacing, default 10px
    paragraphTailSpacing: '',
    cursorColor: '',
    textColor: '',
    // iphone会自动移动,难控制
    cursorOffsetTop: 30,
    // 自定义粘贴处理
    customPasteHandler: false,

    /**
     * ******************************
     * toolbar options
     * ******************************
     */
    // Has the toolbar been fixed?
    toolbarBeenFixed: true,
    toolbarHeight: 50,
    // buttons name, and order
    toolbarBtns: ['select-picture', 'text-style'],

    /**
     * ******************************
     * image options
     * ******************************
     */
    // customize Picture Handler
    customPictureHandler: false,
    // image max width
    imageMaxWidth: 720,
    // image max size, unit Kib, default 20M
    imageMaxSize: 20480,
    // template
    imageSectionTemp: "<section class=\"child-is-picture\"><img src=\"{url}\"></section>",
    // GIF pictures are not processed
    ignoreGif: true,
    // Force the width/height of the picture, even if the width/height of the picture
    // is smaller than the target width/height
    forceImageResize: false,

    /**
     * ******************************
     * text style options
     * ******************************
     */
    // text style, value ['#333', '#f00', ...]
    textStyleColors: null,
    textStyleTitle: 'Set Style',
    textStyleHeadLeftBtnText: 'Clear style',
    textStyleHeadAlign: 'center',

    /**
     * ******************************
     * color options
     * ******************************
     */
    mainColor: '',
    // border color
    borderColor: ''
  };

  function ZxEditor(selector, _options) {
    if (!this instanceof ZxEditor) {
      throw new Error('ZxEditor is a constructor and should be called with the `new` keyword');
    }
    /**
     * ***************************************************
     * check selector
     * ***************************************************
     */


    this.$wrapper = $(selector);

    if (!this.$wrapper[0]) {
      throw new Error("Can't found '".concat(selector, "' Node in document!"));
    } // version


    this.version = '3.0.4-alpha'; // ZxQuery instance

    this.$ = $;
    this.ExpansionPanel = ExpansionPanel; // options

    this.options = Object.assign(DEF_OPTIONS$1, _options);
    this.init(this.options);
  }

  ZxEditor.prototype = {
    constructor: ZxEditor,
    init: function init(options) {
      options = options || this.options;
      /**
       * ***************************************************
       * event listeners
       * or expansionPanel instance
       * ***************************************************
       */
      // $().on, $().off, $().trigger

      this.$eventHandlers = {}; // this.on, this.off, this.emit

      this.customEvents = {}; // extend prototype

      extendPrototypes(ZxEditor); // expansionPanel instances

      this.expansionPanels = [];
      /**
       * ***************************************************
       * create dom
       * ***************************************************
       */

      initDom.call(this, options);
      /**
       * ***************************************************
       * style and placeholder
       * ***************************************************
       */

      initStyle.call(this, options);
      /**
       * ***************************************************
       * cursor
       * ***************************************************
       */

      this.cursor = new CursorClass(this.$content);
      this.$cursorNode = this.cursor.getCurrentNode();
      /**
       * ***************************************************
       * event: last
       * ***************************************************
       */

      handleEvents.call(this);
    },

    /**
     * 插入元素或字符串
     * @param el
     */
    insertElm: function insertElm(el) {
      // string
      if (!el) return; // 光标元素及偏移量

      var $cursorNode = this.$cursorNode;
      var newRangeEl, newRangeOffset;
      /**
       * string
       */

      if (typeof el === 'string') {
        // 光标所在元素内容为空
        if ($cursorNode.isEmpty()) {
          $cursorNode.text(el);
          newRangeEl = $cursorNode;
          newRangeOffset = el.length;
        } else if ($cursorNode.children().every(function ($item) {
          return $item.isTextNode();
        })) {
          var rangeOffset = this.cursor.offset;
          var rangeNodeStr = $cursorNode.text();
          var tmpStr = rangeNodeStr.substr(0, rangeOffset) + el + rangeNodeStr.substr(rangeOffset); // $section = $cursorNode.closest('section')

          $cursorNode.text(tmpStr);
          newRangeEl = $cursorNode;
          newRangeOffset = el.length + rangeOffset;
        } else {
          // 创建一个section
          var $newEl = $("<section>".concat(el, "</section>")); // 插入到childIndex后

          $newEl.insertAfter($cursorNode);
          newRangeEl = $newEl;
          newRangeOffset = el.length;
        }
      }
      /**
       * 插入元素为:非文本
       */
      else {
          var $el = $(el);
          var $elm;

          for (var i = 0; i < $el.length; i++) {
            $elm = $($el[i]);
            var nodeName = $elm.nodeName(); // SECTION

            if (nodeName !== 'section') {
              if ($elm.nodeType() === 1 && !/video|img|audio/.test(nodeName)) {
                $elm.changeNodeName('section');
              } else {
                var $tmp = $("<section></section>");
                $elm = $tmp.append($elm);
              }
            }

            if ($cursorNode.isEmpty()) {
              // siblings is empty
              if ($cursorNode.next()[0] && $cursorNode.next().isEmpty()) {
                $cursorNode.replace($elm);
              } else {
                $elm.insertBefore($cursorNode);
              }
            } else {
              $elm.insertAfter($cursorNode);
            } // 判断$el是否有下一个节点,有:光标指向el结束,无:则插入空行,并移动光标


            var next = $elm.next()[0];

            if (next) {
              newRangeEl = $elm;
              newRangeOffset = $elm.isTextNode() ? $elm.text().length : 0;
            } else {
              var $section = $("<section><br></section>");
              this.$content.append($section);
              newRangeEl = $section;
              newRangeOffset = 0;
            }
          }
        }

      this._checkChildSection();

      this.$content.trigger('input'); // console.log(newRangeEl, newRangeOffset)

      this.cursor.setRange(newRangeEl, newRangeOffset);
    },

    /**
     * 插入空行
     */
    insertBlankLine: function insertBlankLine() {
      var $el = $("<section><br></section>");
      this.insertElm($el);
      this.cursor.setRange($el, 0);
    },

    /**
     * 检查内容是否为空
     * @private
     */
    _checkEmpty: function _checkEmpty() {
      var $el = this.$content;

      if ($el.isEmpty()) {
        $el.addClass('is-empty');
      } else if ($el.hasClass('is-empty')) {
        $el.removeClass('is-empty');
      }
    },

    /**
     * 检查一级子元素,nodeName是否为(SECTION|H1|H2|H3|H4|BLOCKQUOTE|UL)
     * 否:则替换为section标签,或者放入section标签内
     * @private
     */
    _checkChildSection: function _checkChildSection() {
      if (!this.$cursorNode) this.$cursorNode = this.cursor.getCurrentNode();
      var cursorNode = this.$cursorNode[0];
      var isCursorNode = false;
      var parent = this.$content[0];
      var childNodes = parent.childNodes;
      var el;

      for (var i = 0; i < childNodes.length; i++) {
        el = childNodes[i];

        if (el.nodeType === 1) {
          if (!/SECTION|H1|H2|H3|H4|BLOCKQUOTE|UL/.test(el.nodeName)) {
            isCursorNode = el === cursorNode;
            el = util.changeNodeName(el, 'section');

            if (isCursorNode) {
              this.$cursorNode = $(el);
              this.cursor.setRange(el);
            }
          }
        } else {
          var $tmp = $("<section></section>");
          $tmp.append(el.cloneNode());
          parent.replaceChild($tmp[0], el);
          this.$cursorNode = $tmp;
          this.cursor.setRange($tmp);
        }
      }
    },

    /**
     * 清空内容
     */
    remove: function remove() {
      this.setHtml();
    },

    /**
     * 设置编辑器内容
     * @param html
     */
    setHtml: function setHtml(html) {
      this.$content.html(html || '<section><br></section>');

      this._checkChildSection();

      this.cursor.setRange(this.$content.firstChild(), 0);
      this.$content.trigger('input');
    },

    /**
     * 获取编辑器html内容
     * 返回html内容
     * @return {*}
     */
    getHtml: function getHtml() {
      return this.$content.html();
    },

    /**
     * 获取编辑器文本内容,
     * 以纯文本形式返回数据
     * @return {*|string}
     */
    getText: function getText() {
      return this.$content.text();
    },

    /**
     * destroy event and Node
     */
    destroy: function destroy() {
      var evt; // remove $events

      for (var key in this.$eventHandlers) {
        evt = this.$eventHandlers[key];
        evt.$target.off(evt.type, evt.handler, evt.capture);
        delete this.$eventHandlers[key];
      } // remove customEvents


      for (var _key in this.customEvents) {
        evt = this.customEvents[_key];
        this.off(_key);
      } // other object


      this.cursor = null;
      this.toolbar = null;
      this.textStylePanel = null; // other ExpansionPanel

      for (var _key2 in this) {
        if (this[_key2] instanceof ExpansionPanel) {
          this[_key2] = null;
        }
      } // Node


      this.$editor.remove();
    },

    /**
     * set content height
     * default minHeight is window innerHeight, marginBottom
     * @param data
     */
    setContentHeight: function setContentHeight(data) {
      var winHeight = window.innerHeight;
      var styles = {
        // 防止正文内容被键盘挡住,无法查看
        marginBottom: winHeight + 'px'
      }; // check height

      if (data.height) {
        styles.height = typeof data.height === 'number' ? data.height + 'px' : data.height;
      } else {
        styles.minHeight = (util["int"](data.minHeight) || winHeight) + 'px';
      }

      this.$content.css(styles);
    },

    /**
     * get base64 images from this.$content
     * @returns {Array}
     */
    getBase64Images: function getBase64Images() {
      var arr = [];
      var $imgs = this.$content.find('img');
      var img, base64;

      for (var i = 0; i < $imgs.length; i++) {
        img = $imgs[i];
        base64 = img.src;

        if (/^data:.+?;base64,/.test(base64)) {
          arr.push({
            id: img.id,
            base64: base64,
            blob: base64ToBlobData(base64)
          });
        }
      }

      return arr;
    },

    /**
     * 设置指定id图片src
     * @param id
     * @param src
     * @returns {boolean}
     */
    setImageSrc: function setImageSrc(id, src) {
      var img = this.$content.find('#' + id)[0];

      if (img) {
        img.src = src;
        img.removeAttribute('id');
        return true;
      }

      return false;
    },

    /**
     * plugin
     * @param fn
     */
    plugin: function plugin(fn) {
      if (typeof fn === 'function') {
        fn.call(this);
      }
    },

    /**
     * check cursor position
     */
    checkPosition: function checkPosition() {
      var $el = this.$cursorNode = this.cursor.getCurrentNode(); // 当前光标位置

      var cursorOffset = this.cursor.offset; // 文本内容长度

      var len = $el.text().length; // 当前元素高度

      var height = $el.height(); // 当前元素top

      var top = $el.offset().top;
      var scrollTop; // 当前光标位置,距当前元素顶部距离

      var cursorHeight = 0; // 每行大概有几个字

      var textNumOfPerLine = len / (height / this.lineHeight); // 当前光标所在行

      var line = height > this.lineHeight ? Math.floor(cursorOffset / textNumOfPerLine) : 1; // 当前光标位置,至当前元素顶部高度 - 1行高,防止移动后,光标位置太贴近顶部

      var cursorHeightInCurrentNode = (line - 1) * this.lineHeight; // editor postion: fixed;

      if (this.options.fixed) ; else {
        // 当前光标位置超过了屏幕的4分之1
        if (cursorHeightInCurrentNode > window.innerHeight / 4) {
          cursorHeight = cursorHeightInCurrentNode;
        }

        scrollTop = $(window).scrollTop();
        $(window).scrollTop(scrollTop + top + cursorHeight - this.options.cursorOffsetTop);
      }
    }
  };

  return ZxEditor;

})));