'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.Formats = undefined;

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

var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _escapeStringRegexp = require('escape-string-regexp');

var _escapeStringRegexp2 = _interopRequireDefault(_escapeStringRegexp);

var _formats = require('./formats');

var _formats2 = _interopRequireDefault(_formats);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

/**
 * TextareaEditor class.
 *
 * @param {HTMLElement} el - the textarea element to wrap around
 */

var TextareaEditor = function () {
  function TextareaEditor(el) {
    _classCallCheck(this, TextareaEditor);

    this.el = el;
  }

  /**
   * Set or get selection range.
   *
   * @param {Array} [range]
   * @return {Array|TextareaEditor}
   */

  _createClass(TextareaEditor, [{
    key: 'range',
    value: function range(_range) {
      var el = this.el;

      if (_range == null) {
        return [el.selectionStart || 0, el.selectionEnd || 0];
      }

      this.focus();

      var _range2 = _slicedToArray(_range, 2);

      el.selectionStart = _range2[0];
      el.selectionEnd = _range2[1];

      return this;
    }

    /**
     * Insert given text at the current cursor position.
     *
     * @param {String} text - text to insert
     * @return {TextareaEditor}
     */

  }, {
    key: 'insert',
    value: function insert(text) {
      var inserted = true;
      this.el.contentEditable = true;
      this.focus();

      try {
        document.execCommand('insertText', false, text);
      } catch (e) {
        inserted = false;
      }

      this.el.contentEditable = false;

      if (inserted) return this;

      try {
        document.execCommand('ms-beginUndoUnit');
      } catch (e) {}

      var _selection = this.selection(),
          before = _selection.before,
          after = _selection.after;

      this.el.value = before + text + after;

      try {
        document.execCommand('ms-endUndoUnit');
      } catch (e) {}

      var event = document.createEvent('Event');
      event.initEvent('input', true, true);
      this.el.dispatchEvent(event);
      return this;
    }

    /**
     * Set foucs on the TextareaEditor's element.
     *
     * @return {TextareaEditor}
     */

  }, {
    key: 'focus',
    value: function focus() {
      if (document.activeElement !== this.el) this.el.focus();
      return this;
    }

    /**
     * Get selected text.
     *
     * @return {Object}
     * @private
     */

  }, {
    key: 'selection',
    value: function selection() {
      var _range3 = this.range(),
          _range4 = _slicedToArray(_range3, 2),
          start = _range4[0],
          end = _range4[1];

      var value = normalizeNewlines(this.el.value);
      return {
        before: value.slice(0, start),
        content: value.slice(start, end),
        after: value.slice(end)
      };
    }

    /**
     * Get format by name.
     *
     * @param {String|Object} format
     * @return {Object}
     * @private
     */

  }, {
    key: 'getFormat',
    value: function getFormat(format) {
      if ((typeof format === 'undefined' ? 'undefined' : _typeof(format)) == 'object') {
        return normalizeFormat(format);
      }

      if (!_formats2.default.hasOwnProperty(format)) {
        throw new Error('Invalid format ' + format);
      }

      return normalizeFormat(_formats2.default[format]);
    }

    /**
     * Toggle given `format` on current selection.
     * Any additional arguments are passed on to `.format()`.
     *
     * @param {String|Object} format - name of format or an object
     * @return {TextareaEditor}
     */

  }, {
    key: 'toggle',
    value: function toggle(format) {
      if (this.hasFormat(format)) return this.unformat(format);

      for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
        args[_key - 1] = arguments[_key];
      }

      return this.format.apply(this, [format].concat(args));
    }

    /**
     * Format current selcetion with given `format`.
     *
     * @param {String|Object} name - name of format or an object
     * @return {TextareaEditor}
     */

  }, {
    key: 'format',
    value: function format(name) {
      for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
        args[_key2 - 1] = arguments[_key2];
      }

      var format = this.getFormat(name);
      var prefix = format.prefix,
          suffix = format.suffix,
          multiline = format.multiline;

      var _selection2 = this.selection(),
          before = _selection2.before,
          content = _selection2.content,
          after = _selection2.after;

      var lines = multiline ? content.split('\n') : [content];

      var _range5 = this.range(),
          _range6 = _slicedToArray(_range5, 2),
          start = _range6[0],
          end = _range6[1];

      // format lines


      lines = lines.map(function (line, i) {
        var pval = maybeCall.apply(undefined, [prefix.value, line, i + 1].concat(args));
        var sval = maybeCall.apply(undefined, [suffix.value, line, i + 1].concat(args));

        if (!multiline || !content.length) {
          start += pval.length;
          end += pval.length;
        } else {
          end += pval.length + sval.length;
        }

        return pval + line + sval;
      });

      var insert = lines.join('\n');

      // newlines before and after block
      if (format.block) {
        var nlb = matchLength(before, /\n+$/);
        var nla = matchLength(after, /^\n+/);

        if (before) {
          while (nlb < 2) {
            insert = '\n' + insert;
            start++;
            end++;
            nlb++;
          }
        }

        if (after) {
          while (nla < 2) {
            insert = insert + '\n';
            nla++;
          }
        }
      }

      this.insert(insert);
      this.range([start, end]);
      return this;
    }

    /**
     * Remove given `format` from current selection.
     *
     * @param {String|Object} name - name of format or an object
     * @return {TextareaEditor}
     */

  }, {
    key: 'unformat',
    value: function unformat(name) {
      if (!this.hasFormat(name)) return this;

      var format = this.getFormat(name);

      var prefix = format.prefix,
          suffix = format.suffix,
          multiline = format.multiline;

      var _selection3 = this.selection(),
          before = _selection3.before,
          content = _selection3.content,
          after = _selection3.after;

      var lines = multiline ? content.split('\n') : [content];

      var _range7 = this.range(),
          _range8 = _slicedToArray(_range7, 2),
          start = _range8[0],
          end = _range8[1];

      // If this is not a multiline format, include prefixes and suffixes just
      // outside the selection.


      if ((!multiline || lines.length == 1) && hasSuffix(before, prefix) && hasPrefix(after, suffix)) {
        start -= suffixLength(before, prefix);
        end += prefixLength(after, suffix);
        this.range([start, end]);
        lines = [this.selection().content];
      }

      // remove formatting from lines
      lines = lines.map(function (line) {
        var plen = prefixLength(line, prefix);
        var slen = suffixLength(line, suffix);
        return line.slice(plen, line.length - slen);
      });

      // insert and set selection
      var insert = lines.join('\n');
      this.insert(insert);
      this.range([start, start + insert.length]);

      return this;
    }

    /**
     * Check if current seletion has given format.
     *
     * @param {String|Object} name - name of format or an object
     * @return {Boolean}
     */

  }, {
    key: 'hasFormat',
    value: function hasFormat(name) {
      var format = this.getFormat(name);
      var prefix = format.prefix,
          suffix = format.suffix,
          multiline = format.multiline;

      var _selection4 = this.selection(),
          before = _selection4.before,
          content = _selection4.content,
          after = _selection4.after;

      var lines = content.split('\n');

      // prefix and suffix outside selection
      if (!multiline || lines.length == 1) {
        return hasSuffix(before, prefix) && hasPrefix(after, suffix) || hasPrefix(content, prefix) && hasSuffix(content, suffix);
      }

      // check which line(s) are formatted
      var formatted = lines.filter(function (line) {
        return hasPrefix(line, prefix) && hasSuffix(line, suffix);
      });

      return formatted.length === lines.length;
    }
  }]);

  return TextareaEditor;
}();

// Expose formats


exports.default = TextareaEditor;
exports.Formats = _formats2.default;

/**
 * Check if given prefix is present.
 * @private
 */

function hasPrefix(text, prefix) {
  var exp = new RegExp('^' + prefix.pattern);
  var result = exp.test(text);

  if (prefix.antipattern) {
    var _exp = new RegExp('^' + prefix.antipattern);
    result = result && !_exp.test(text);
  }

  return result;
}

/**
 * Check if given suffix is present.
 * @private
 */

function hasSuffix(text, suffix) {
  var exp = new RegExp(suffix.pattern + '$');
  var result = exp.test(text);

  if (suffix.antipattern) {
    var _exp2 = new RegExp(suffix.antipattern + '$');
    result = result && !_exp2.test(text);
  }

  return result;
}

/**
 * Get length of match.
 * @private
 */

function matchLength(text, exp) {
  var match = text.match(exp);
  return match ? match[0].length : 0;
}

/**
 * Get prefix length.
 * @private
 */

function prefixLength(text, prefix) {
  var exp = new RegExp('^' + prefix.pattern);
  return matchLength(text, exp);
}

/**
 * Get suffix length.
 * @private
 */

function suffixLength(text, suffix) {
  var exp = new RegExp(suffix.pattern + '$');
  return matchLength(text, exp);
}

/**
 * Normalize newlines.
 * @private
 */

function normalizeNewlines(str) {
  return str.replace('\r\n', '\n');
}

/**
 * Normalize format.
 * @private
 */

function normalizeFormat(format) {
  var clone = Object.assign({}, format);
  clone.prefix = normalizePrefixSuffix(format.prefix);
  clone.suffix = normalizePrefixSuffix(format.suffix);
  return clone;
}

/**
 * Normalize prefixes and suffixes.
 * @private
 */

function normalizePrefixSuffix() {
  var value = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';

  if ((typeof value === 'undefined' ? 'undefined' : _typeof(value)) == 'object') return value;
  return {
    value: value,
    pattern: (0, _escapeStringRegexp2.default)(value)
  };
}

/**
 * Call if function.
 * @private
 */

function maybeCall(value) {
  for (var _len3 = arguments.length, args = Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
    args[_key3 - 1] = arguments[_key3];
  }

  return typeof value == 'function' ? value.apply(undefined, args) : value;
}