/**
 * Copyright (c) 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule DraftEditor.react
 * @typechecks
 *
 */

'use strict';

var isNode = require('itsa-utils').isNode;

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 _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 _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } };

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

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

var DefaultDraftBlockRenderMap = require('./DefaultDraftBlockRenderMap');
var DefaultDraftInlineStyle = require('./DefaultDraftInlineStyle');
var DraftEditorCompositionHandler = require('./DraftEditorCompositionHandler');
var DraftEditorContents = require('./DraftEditorContents.react');
var DraftEditorDragHandler = require('./DraftEditorDragHandler');
var DraftEditorEditHandler = require('./DraftEditorEditHandler');
var DraftEditorPlaceholder = require('./DraftEditorPlaceholder.react');
var EditorState = require('./EditorState');
var React = require('react');
var ReactDOM = require('react-dom');
var Scroll = require('fbjs/lib/Scroll');
var Style = require('fbjs/lib/Style');
var UserAgent = isNode || require('fbjs/lib/UserAgent');

var cx = require('fbjs/lib/cx');
var emptyFunction = require('fbjs/lib/emptyFunction');
var generateRandomKey = require('./generateRandomKey');
var getDefaultKeyBinding = require('./getDefaultKeyBinding');
var nullthrows = require('fbjs/lib/nullthrows');
var getScrollPosition = require('fbjs/lib/getScrollPosition');

var isIE = !isNode && UserAgent.isBrowser('IE');

// IE does not support the `input` event on contentEditable, so we can't
// observe spellcheck behavior.
var allowSpellCheck = !isIE;

// Define a set of handler objects to correspond to each possible `mode`
// of editor behavior.
var handlerMap = {
  'edit': DraftEditorEditHandler,
  'composite': DraftEditorCompositionHandler,
  'drag': DraftEditorDragHandler,
  'cut': null,
  'render': null
};

/**
 * `DraftEditor` is the root editor component. It composes a `contentEditable`
 * div, and provides a wide variety of useful function props for managing the
 * state of the editor. See `DraftEditorProps` for details.
 */

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

  _createClass(DraftEditor, null, [{
    key: 'defaultProps',
    value: {
      blockRenderMap: DefaultDraftBlockRenderMap,
      blockRendererFn: emptyFunction.thatReturnsNull,
      blockStyleFn: emptyFunction.thatReturns(''),
      keyBindingFn: getDefaultKeyBinding,
      readOnly: false,
      spellCheck: false,
      stripPastedStyles: false
    },
    enumerable: true
  }]);

  function DraftEditor(props) {
    var _this = this;
    _classCallCheck(this, DraftEditor);

    _get(Object.getPrototypeOf(DraftEditor.prototype), 'constructor', this).call(this, props);

    this._blockSelectEvents = false;
    this._clipboard = null;
    this._guardAgainstRender = false;
    this._handler = null;
    this._dragCount = 0;
    this._editorKey = generateRandomKey();
    this._placeholderAccessibilityID = 'placeholder-' + this._editorKey;

    this._onBeforeInput = this._buildHandler('onBeforeInput');
    this._onBlur = this._buildHandler('onBlur');
    this._onCharacterData = this._buildHandler('onCharacterData');
    this._onCompositionEnd = this._buildHandler('onCompositionEnd');
    this._onCompositionStart = this._buildHandler('onCompositionStart');
    this._onCopy = this._buildHandler('onCopy');
    this._onCut = this._buildHandler('onCut');
    this._onDragEnd = this._buildHandler('onDragEnd');
    this._onDragOver = this._buildHandler('onDragOver');
    this._onDragStart = this._buildHandler('onDragStart');
    this._onDrop = this._buildHandler('onDrop');
    this._onInput = this._buildHandler('onInput');
    this._onFocus = this._buildHandler('onFocus');
    this._onKeyDown = this._buildHandler('onKeyDown');
    this._onKeyPress = this._buildHandler('onKeyPress');
    this._onKeyUp = this._buildHandler('onKeyUp');
    this._onMouseDown = this._buildHandler('onMouseDown');
    this._onMouseUp = this._buildHandler('onMouseUp');
    this._onPaste = this._buildHandler('onPaste');
    this._onSelect = this._buildHandler('onSelect');

    // Manual binding for public and internal methods.
    this.focus = this._focus.bind(this);
    this.blur = this._blur.bind(this);
    this.setMode = this._setMode.bind(this);
    this.exitCurrentMode = this._exitCurrentMode.bind(this);
    this.restoreEditorDOM = this._restoreEditorDOM.bind(this);
    this.setRenderGuard = this._setRenderGuard.bind(this);
    this.removeRenderGuard = this._removeRenderGuard.bind(this);
    this.setClipboard = this._setClipboard.bind(this);
    this.getClipboard = this._getClipboard.bind(this);
    this.getEditorKey = function () {
      return _this._editorKey;
    };
    this.update = this._update.bind(this);
    this.onDragEnter = this._onDragEnter.bind(this);
    this.onDragLeave = this._onDragLeave.bind(this);

    // See `_restoreEditorDOM()`.
    this.state = { containerKey: 0 };
  }

  /**
   * Build a method that will pass the event to the specified handler method.
   * This allows us to look up the correct handler function for the current
   * editor mode, if any has been specified.
   */

  _createClass(DraftEditor, [{
    key: '_buildHandler',
    value: function _buildHandler(eventName) {
      var _this2 = this;

      return function (e) {
        if (!_this2.props.readOnly) {
          var method = _this2._handler && _this2._handler[eventName];
          method && method.call(_this2, e);
        }
      };
    }
  }, {
    key: '_showPlaceholder',
    value: function _showPlaceholder() {
      return !!this.props.placeholder && !this.props.editorState.isInCompositionMode() && !this.props.editorState.getCurrentContent().hasText();
    }
  }, {
    key: '_renderPlaceholder',
    value: function _renderPlaceholder() {
      if (this._showPlaceholder()) {
        return React.createElement(DraftEditorPlaceholder, {
          text: nullthrows(this.props.placeholder),
          editorState: this.props.editorState,
          textAlignment: this.props.textAlignment,
          accessibilityID: this._placeholderAccessibilityID
        });
      }
      return null;
    }
  }, {
    key: 'render',
    value: function render() {
      var _props = this.props,
          _minHeight = _props.minHeight;
      var readOnly = _props.readOnly;
      var textAlignment = _props.textAlignment;

      var rootClass = cx({
        'DraftEditor/root': true,
        'DraftEditor/alignLeft': textAlignment === 'left',
        'DraftEditor/alignRight': textAlignment === 'right',
        'DraftEditor/alignCenter': textAlignment === 'center'
      });
      var hasContent = this.props.editorState.getCurrentContent().hasText();

      var contentStyle = {
        outline: 'none',
        whiteSpace: 'pre-wrap',
        wordWrap: 'break-word'
      };
      _minHeight && (contentStyle.minHeight=_minHeight);

      return React.createElement(
        'div',
        { className: rootClass },
        this._renderPlaceholder(),
        React.createElement(
          'div',
          {
            className: cx('DraftEditor/editorContainer'),
            key: 'editor' + this.state.containerKey,
            ref: 'editorContainer'
          },
          React.createElement(
            'div',
            {
              'aria-activedescendant': readOnly ? null : this.props.ariaActiveDescendantID,
              'aria-autocomplete': readOnly ? null : this.props.ariaAutoComplete,
              'aria-describedby': this._showPlaceholder() ? this._placeholderAccessibilityID : null,
              'aria-expanded': readOnly ? null : this.props.ariaExpanded,
              'aria-haspopup': readOnly ? null : this.props.ariaHasPopup,
              'aria-label': this.props.ariaLabel,
              'aria-owns': readOnly ? null : this.props.ariaOwneeID,
              className: cx('public/DraftEditor/content'),
              contentEditable: !readOnly,
              'data-testid': this.props.webDriverTestID,
              onBeforeInput: this._onBeforeInput,
              onBlur: this._onBlur,
              onCompositionEnd: this._onCompositionEnd,
              onCompositionStart: this._onCompositionStart,
              onCopy: this._onCopy,
              onCut: this._onCut,
              onDragEnd: this._onDragEnd,
              onDragEnter: this.onDragEnter,
              onDragLeave: this.onDragLeave,
              onDragOver: this._onDragOver,
              onDragStart: this._onDragStart,
              onDrop: this._onDrop,
              onFocus: this._onFocus,
              onInput: this._onInput,
              onKeyDown: this._onKeyDown,
              onKeyPress: this._onKeyPress,
              onKeyUp: this._onKeyUp,
              onMouseUp: this._onMouseUp,
              onPaste: this._onPaste,
              onSelect: this._onSelect,
              ref: 'editor',
              role: readOnly ? null : this.props.role || 'textbox',
              spellCheck: allowSpellCheck && this.props.spellCheck,
              style: contentStyle,
              suppressContentEditableWarning: true,
              tabIndex: this.props.tabIndex },
            React.createElement(DraftEditorContents, {
              blockRenderMap: this.props.blockRenderMap,
              blockRendererFn: this.props.blockRendererFn,
              blockStyleFn: this.props.blockStyleFn,
              customStyleMap: _extends({}, DefaultDraftInlineStyle, this.props.customStyleMap),
              editorKey: this._editorKey,
              editorState: this.props.editorState
            })
          )
        )
      );
    }
  }, {
    key: 'componentDidMount',
    value: function componentDidMount() {
      this.setMode('edit');

      /**
       * IE has a hardcoded "feature" that attempts to convert link text into
       * anchors in contentEditable DOM. This breaks the editor's expectations of
       * the DOM, and control is lost. Disable it to make IE behave.
       * See: http://blogs.msdn.com/b/ieinternals/archive/2010/09/15/
       * ie9-beta-minor-change-list.aspx
       */
      if (isIE) {
        document.execCommand('AutoUrlDetect', false, false);
      }
    }

    /**
     * Prevent selection events from affecting the current editor state. This
     * is mostly intended to defend against IE, which fires off `selectionchange`
     * events regardless of whether the selection is set via the browser or
     * programmatically. We only care about selection events that occur because
     * of browser interaction, not re-renders and forced selections.
     */
  }, {
    key: 'componentWillUpdate',
    value: function componentWillUpdate() {
      this._blockSelectEvents = true;
    }
  }, {
    key: 'componentDidUpdate',
    value: function componentDidUpdate() {
      this._blockSelectEvents = false;
    }

    /**
     * Used via `this.focus()`.
     *
     * Force focus back onto the editor node.
     *
     * Forcing focus causes the browser to scroll to the top of the editor, which
     * may be undesirable when the editor is taller than the viewport. To solve
     * this, either use a specified scroll position (in cases like `cut` behavior
     * where it should be restored to a known position) or store the current
     * scroll state and put it back in place after focus has been forced.
     */
  }, {
    key: '_focus',
    value: function _focus(scrollPosition) {
      var editorState = this.props.editorState;

      var alreadyHasFocus = editorState.getSelection().getHasFocus();
      var editorNode = ReactDOM.findDOMNode(this.refs.editor);

      var scrollParent = Style.getScrollParent(editorNode);

      var _ref = scrollPosition || getScrollPosition(scrollParent);

      var x = _ref.x;
      var y = _ref.y;

      editorNode.focus();
      if (scrollParent === window) {
        window && window.scrollTo && window.scrollTo(x, y);
      } else {
        Scroll.setTop(scrollParent, y);
      }

      // On Chrome and Safari, calling focus on contenteditable focuses the
      // cursor at the first character. This is something you don't expect when
      // you're clicking on an input element but not directly on a character.
      // Put the cursor back where it was before the blur.
      if (!alreadyHasFocus) {
        this.update(EditorState.forceSelection(editorState, editorState.getSelection()));
      }
    }
  }, {
    key: '_blur',
    value: function _blur() {
      ReactDOM.findDOMNode(this.refs.editor).blur();
    }

    /**
     * Used via `this.setMode(...)`.
     *
     * Set the behavior mode for the editor component. This switches the current
     * handler module to ensure that DOM events are managed appropriately for
     * the active mode.
     */
  }, {
    key: '_setMode',
    value: function _setMode(mode) {
      this._handler = handlerMap[mode];
    }
  }, {
    key: '_exitCurrentMode',
    value: function _exitCurrentMode() {
      this.setMode('edit');
    }

    /**
     * Used via `this.restoreEditorDOM()`.
     *
     * Force a complete re-render of the editor based on the current EditorState.
     * This is useful when we know we are going to lose control of the DOM
     * state (cut command, IME) and we want to make sure that reconciliation
     * occurs on a version of the DOM that is synchronized with our EditorState.
     */
  }, {
    key: '_restoreEditorDOM',
    value: function _restoreEditorDOM(scrollPosition) {
      var _this3 = this;

      this.setState({ containerKey: this.state.containerKey + 1 }, function () {
        _this3._focus(scrollPosition);
      });
    }

    /**
     * Guard against rendering. Intended for use when we need to manually
     * reset editor contents, to ensure that no outside influences lead to
     * React reconciliation when we are in an uncertain state.
     */
  }, {
    key: '_setRenderGuard',
    value: function _setRenderGuard() {
      this._guardAgainstRender = true;
    }
  }, {
    key: '_removeRenderGuard',
    value: function _removeRenderGuard() {
      this._guardAgainstRender = false;
    }

    /**
     * Used via `this.setClipboard(...)`.
     *
     * Set the clipboard state for a cut/copy event.
     */
  }, {
    key: '_setClipboard',
    value: function _setClipboard(clipboard) {
      this._clipboard = clipboard;
    }

    /**
     * Used via `this.getClipboard()`.
     *
     * Retrieve the clipboard state for a cut/copy event.
     */
  }, {
    key: '_getClipboard',
    value: function _getClipboard() {
      return this._clipboard;
    }

    /**
     * Used via `this.update(...)`.
     *
     * Propagate a new `EditorState` object to higher-level components. This is
     * the method by which event handlers inform the `DraftEditor` component of
     * state changes. A component that composes a `DraftEditor` **must** provide
     * an `onChange` prop to receive state updates passed along from this
     * function.
     */
  }, {
    key: '_update',
    value: function _update(editorState) {
      this.props.onChange(editorState);
    }

    /**
     * Used in conjunction with `_onDragLeave()`, by counting the number of times
     * a dragged element enters and leaves the editor (or any of its children),
     * to determine when the dragged element absolutely leaves the editor.
     */
  }, {
    key: '_onDragEnter',
    value: function _onDragEnter() {
      this._dragCount++;
    }

    /**
     * See `_onDragEnter()`.
     */
  }, {
    key: '_onDragLeave',
    value: function _onDragLeave() {
      this._dragCount--;
      if (this._dragCount === 0) {
        this.exitCurrentMode();
      }
    }
  }]);

  return DraftEditor;
})(React.Component);

module.exports = DraftEditor;

/**
 * Define proxies that can route events to the current handler.
 */