'use strict';

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

var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };

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 _key = require('../utils/key');

var _key2 = _interopRequireDefault(_key);

var _offsetKey = require('../utils/offset-key');

var _offsetKey2 = _interopRequireDefault(_offsetKey);

var _raw = require('../serializers/raw');

var _raw2 = _interopRequireDefault(_raw);

var _react = require('react');

var _react2 = _interopRequireDefault(_react);

var _text = require('./text');

var _text2 = _interopRequireDefault(_text);

var _void = require('./void');

var _void2 = _interopRequireDefault(_void);

var _keycode = require('keycode');

var _keycode2 = _interopRequireDefault(_keycode);

var _environment = require('../utils/environment');

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

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

/**
 * Noop.
 */

function noop() {}

/**
 * Content.
 */

var Content = function (_React$Component) {
  _inherits(Content, _React$Component);

  /**
   * Constructor.
   *
   * @param {Object} props
   */

  /**
   * Property types.
   */

  function Content(props) {
    _classCallCheck(this, Content);

    var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(Content).call(this, props));

    _initialiseProps.call(_this);

    _this.tmp = {};
    return _this;
  }

  /**
   * Should the component update?
   *
   * @param {Object} props
   * @param {Object} state
   * @return {Boolean} shouldUpdate
   */

  /**
   * Default properties.
   */

  /**
   * While rendering set a flag, so that we can ignore any `select` events that
   * fire because of the leaf nodes updating their selection.
   *
   * @param {Object} props
   */

  /**
   * On before input, bubble up.
   *
   * @param {Event} e
   */

  /**
   * On blur, update the selection to be not focused.
   *
   * @param {Event} e
   */

  /**
   * On change, bubble up.
   *
   * @param {State} state
   */

  /**
   * On copy, defer to `onCutCopy`, then bubble up.
   *
   * @param {Event} e
   */

  /**
   * On cut, defer to `onCutCopy`, then bubble up.
   *
   * @param {Event} e
   */

  /**
   * On cut and copy, add the currently selected fragment to the currently
   * selected DOM, so that it will show up when pasted.
   *
   * @param {Event} e
   */

  /**
   * On key down, prevent the default behavior of certain commands that will
   * leave the editor in an out-of-sync state, then bubble up.
   *
   * @param {Event} e
   */

  /**
   * On paste, determine the type and bubble up.
   *
   * @param {Event} e
   */

  /**
   * On select, update the current state's selection.
   *
   * @param {Event} e
   */

  /**
   * Render the editor content.
   *
   * @return {Element} element
   */

  /**
   * Render a `node`.
   *
   * @param {Node} node
   * @return {Element} element
   */

  /**
   * Render an element `node`.
   *
   * @param {Node} node
   * @return {Element} element
   */

  /**
   * Render a void wrapper around an `element` for `node`.
   *
   * @param {Node} node
   * @param {Element} element
   * @return {Element} element
   */

  /**
   * Render a text `node`.
   *
   * @param {Node} node
   * @return {Element} element
   */

  return Content;
}(_react2.default.Component);

/**
 * Export.
 */

Content.propTypes = {
  className: _react2.default.PropTypes.string,
  editor: _react2.default.PropTypes.object.isRequired,
  onBeforeInput: _react2.default.PropTypes.func,
  onChange: _react2.default.PropTypes.func,
  onKeyDown: _react2.default.PropTypes.func,
  onPaste: _react2.default.PropTypes.func,
  onSelect: _react2.default.PropTypes.func,
  renderMark: _react2.default.PropTypes.func.isRequired,
  renderNode: _react2.default.PropTypes.func.isRequired,
  state: _react2.default.PropTypes.object.isRequired,
  style: _react2.default.PropTypes.object
};
Content.defaultProps = {
  style: {}
};

var _initialiseProps = function _initialiseProps() {
  var _this2 = this;

  this.shouldComponentUpdate = function (props, state) {
    if (props.state.isNative) return false;
    return props.state.selection != _this2.props.state.selection || props.state.document != _this2.props.state.document;
  };

  this.componentWillUpdate = function (props, state) {
    _this2.tmp.isRendering = true;
  };

  this.componentDidUpdate = function (props, state) {
    setTimeout(function () {
      _this2.tmp.isRendering = false;
    });
  };

  this.onBeforeInput = function (e) {
    _this2.props.onBeforeInput(e);
  };

  this.onBlur = function (e) {
    if (_this2.tmp.isCopying) return;
    var state = _this2.props.state;


    state = state.transform().blur().apply({ isNative: true });

    _this2.onChange(state);
  };

  this.onChange = function (state) {
    _this2.props.onChange(state);
  };

  this.onCopy = function (e) {
    _this2.onCutCopy(e);
  };

  this.onCut = function (e) {
    _this2.onCutCopy(e);

    // Once the cut has successfully executed, delete the current selection.
    window.requestAnimationFrame(function () {
      var state = _this2.props.state.transform().delete().apply();
      _this2.onChange(state);
    });
  };

  this.onCutCopy = function (e) {
    var native = window.getSelection();
    if (!native.rangeCount) return;

    var state = _this2.props.state;
    var fragment = state.fragment;

    var raw = _raw2.default.serializeNode(fragment);
    var string = JSON.stringify(raw);
    var encoded = window.btoa(string);

    // Wrap the first character of the selection in a span that has the encoded
    // fragment attached as an attribute, so it will show up in the copied HTML.
    var range = native.getRangeAt(0);
    var contents = range.cloneContents();
    var wrapper = window.document.createElement('span');
    var text = contents.childNodes[0];
    var char = text.textContent.slice(0, 1);
    var first = window.document.createTextNode(char);
    var rest = text.textContent.slice(1);
    text.textContent = rest;
    wrapper.appendChild(first);
    wrapper.setAttribute('data-fragment', encoded);
    contents.insertBefore(wrapper, text);

    // Add the phony content to the DOM, and select it, so it will be copied.
    var body = window.document.querySelector('body');
    var div = window.document.createElement('div');
    div.setAttribute('contenteditable', true);
    div.style.position = 'absolute';
    div.style.left = '-9999px';
    div.appendChild(contents);
    body.appendChild(div);

    // Set the `isCopying` flag, so our `onSelect` logic doesn't fire.
    _this2.tmp.isCopying = true;
    native.selectAllChildren(div);

    // Revert to the previous selection right after copying.
    window.requestAnimationFrame(function () {
      body.removeChild(div);
      native.removeAllRanges();
      native.addRange(range);
      _this2.tmp.isCopying = false;
    });
  };

  this.onKeyDown = function (e) {
    var key = (0, _keycode2.default)(e.which);

    if (key == 'enter' || key == 'backspace' || key == 'delete' || key == 'b' && _key2.default.isCommand(e) || key == 'i' && _key2.default.isCommand(e) || key == 'y' && _key2.default.isWindowsCommand(e) || key == 'z' && _key2.default.isCommand(e)) {
      e.preventDefault();
    }

    _this2.props.onKeyDown(e);
  };

  this.onPaste = function (e) {
    e.preventDefault();
    var data = e.clipboardData;
    var paste = {};

    // COMPAT: In Firefox, `types` is array-like.
    var types = Array.from(data.types);

    // Handle files.
    if (data.files.length != 0) {
      paste.type = 'files';
      paste.files = data.files;
    }

    // Treat it as rich text if there is HTML content.
    else if (types.includes('text/html')) {
        paste.type = 'html';
        paste.text = data.getData('text/plain');
        paste.html = data.getData('text/html');
      }

      // Treat everything else as plain text.
      else {
          paste.type = 'text';
          paste.text = data.getData('text/plain');
        }

    // If html, and the html includes a `data-fragment` attribute, it's actually
    // a raw-serialized JSON fragment from a previous cut/copy, so deserialize
    // it and insert it normally.
    if (paste.type == 'html' && ~paste.html.indexOf('<span data-fragment="')) {
      var regexp = /data-fragment="([^\s]+)"/;
      var matches = regexp.exec(paste.html);

      var _matches = _slicedToArray(matches, 2);

      var full = _matches[0];
      var encoded = _matches[1];

      var string = window.atob(encoded);
      var json = JSON.parse(string);
      var fragment = _raw2.default.deserialize(json);
      var state = _this2.props.state;


      state = state.transform().insertFragment(fragment.document).apply();

      _this2.onChange(state);
      return;
    }

    paste.data = data;
    _this2.props.onPaste(e, paste);
  };

  this.onSelect = function (e) {
    if (_this2.tmp.isRendering) return;
    if (_this2.tmp.isCopying) return;

    var state = _this2.props.state;
    var _state = state;
    var document = _state.document;
    var selection = _state.selection;

    var native = window.getSelection();

    if (!native.rangeCount) {
      selection = selection.merge({ isFocused: false });
      state = state.merge({ selection: selection });
      _this2.onChange(state);
      return;
    }

    var anchorNode = native.anchorNode;
    var anchorOffset = native.anchorOffset;
    var focusNode = native.focusNode;
    var focusOffset = native.focusOffset;

    var anchor = _offsetKey2.default.findPoint(anchorNode, anchorOffset);
    var focus = _offsetKey2.default.findPoint(focusNode, focusOffset);

    state = state.transform().focus().moveTo({
      anchorKey: anchor.key,
      anchorOffset: anchor.offset,
      focusKey: focus.key,
      focusOffset: focus.offset
    }).apply({ isNative: true });

    _this2.onChange(state);
  };

  this.render = function () {
    var _props = _this2.props;
    var className = _props.className;
    var state = _props.state;
    var document = state.document;

    var children = document.nodes.map(function (node) {
      return _this2.renderNode(node);
    }).toArray();

    var style = _extends({}, _this2.props.style, {
      outline: 'none', // prevent the default outline styles
      whiteSpace: 'pre-wrap', // preserve adjacent whitespace and new lines
      wordWrap: 'break-word' // allow words to break if they are too long
    });

    return _react2.default.createElement(
      'div',
      {
        className: className,
        contentEditable: true, suppressContentEditableWarning: true,
        style: style,
        onBeforeInput: _this2.onBeforeInput,
        onBlur: _this2.onBlur,
        onCopy: _this2.onCopy,
        onCut: _this2.onCut,
        onKeyDown: _this2.onKeyDown,
        onPaste: _this2.onPaste,
        onSelect: _this2.onSelect,
        onKeyUp: noop
      },
      children
    );
  };

  this.renderNode = function (node) {
    switch (node.kind) {
      case 'block':
      case 'inline':
        return _this2.renderElement(node);
      case 'text':
        return _this2.renderText(node);
    }
  };

  this.renderElement = function (node) {
    var _props2 = _this2.props;
    var editor = _props2.editor;
    var renderNode = _props2.renderNode;
    var state = _props2.state;

    var Component = renderNode(node);
    var children = node.nodes.map(function (child) {
      return _this2.renderNode(child);
    }).toArray();

    var attributes = {
      'data-key': node.key
    };

    var element = _react2.default.createElement(
      Component,
      {
        attributes: attributes,
        key: node.key,
        editor: editor,
        node: node,
        state: state
      },
      children
    );

    return node.isVoid ? _this2.renderVoid(element, node) : element;
  };

  this.renderVoid = function (element, node) {
    var _props3 = _this2.props;
    var editor = _props3.editor;
    var state = _props3.state;

    return _react2.default.createElement(
      _void2.default,
      {
        key: node.key,
        editor: editor,
        node: node,
        state: state
      },
      element
    );
  };

  this.renderText = function (node) {
    var _props4 = _this2.props;
    var editor = _props4.editor;
    var renderMark = _props4.renderMark;
    var state = _props4.state;

    return _react2.default.createElement(_text2.default, {
      key: node.key,
      editor: editor,
      node: node,
      renderMark: renderMark,
      state: state
    });
  };
};

exports.default = Content;