/** * 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 * @format * * @preventMunge */ 'use strict'; var _assign = require('object-assign'); var _extends = _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; }; 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; } 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 = 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 getScrollPosition = require('fbjs/lib/getScrollPosition'); var invariant = require('fbjs/lib/invariant'); var nullthrows = require('fbjs/lib/nullthrows'); var isIE = 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); function DraftEditor(props) { _classCallCheck(this, DraftEditor); var _this = _possibleConstructorReturn(this, _React$Component.call(this, props)); _this.focus = function (scrollPosition) { var editorState = _this.props.editorState; var alreadyHasFocus = editorState.getSelection().getHasFocus(); var editorNode = ReactDOM.findDOMNode(_this.editor); if (!editorNode) { // once in a while people call 'focus' in a setTimeout, and the node has // been deleted, so it can be null in that case. return; } var scrollParent = Style.getScrollParent(editorNode); var _ref = scrollPosition || getScrollPosition(scrollParent), x = _ref.x, y = _ref.y; !(editorNode instanceof HTMLElement) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'editorNode is not an HTMLElement') : invariant(false) : void 0; editorNode.focus(); // Restore scroll position if (scrollParent === window) { 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())); } }; _this.blur = function () { var editorNode = ReactDOM.findDOMNode(_this.editor); !(editorNode instanceof HTMLElement) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'editorNode is not an HTMLElement') : invariant(false) : void 0; editorNode.blur(); }; _this.setMode = function (mode) { _this._handler = handlerMap[mode]; }; _this.exitCurrentMode = function () { _this.setMode('edit'); }; _this.restoreEditorDOM = function (scrollPosition) { _this.setState({ contentsKey: _this.state.contentsKey + 1 }, function () { _this.focus(scrollPosition); }); }; _this.setClipboard = function (clipboard) { _this._clipboard = clipboard; }; _this.getClipboard = function () { return _this._clipboard; }; _this.update = function (editorState) { _this._latestEditorState = editorState; _this.props.onChange(editorState); }; _this.onDragEnter = function () { _this._dragCount++; }; _this.onDragLeave = function () { _this._dragCount--; if (_this._dragCount === 0) { _this.exitCurrentMode(); } }; _this._blockSelectEvents = false; _this._clipboard = null; _this._handler = null; _this._dragCount = 0; _this._editorKey = props.editorKey || generateRandomKey(); _this._placeholderAccessibilityID = 'placeholder-' + _this._editorKey; _this._latestEditorState = props.editorState; _this._latestCommittedEditorState = props.editorState; _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'); _this.getEditorKey = function () { return _this._editorKey; }; // See `restoreEditorDOM()`. _this.state = { contentsKey: 0 }; return _this; } /** * 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. */ /** * Define proxies that can route events to the current handler. */ DraftEditor.prototype._buildHandler = function _buildHandler(eventName) { var _this2 = this; return function (e) { if (!_this2.props.readOnly) { var method = _this2._handler && _this2._handler[eventName]; method && method(_this2, e); } }; }; DraftEditor.prototype._showPlaceholder = function _showPlaceholder() { return !!this.props.placeholder && !this.props.editorState.isInCompositionMode() && !this.props.editorState.getCurrentContent().hasText(); }; DraftEditor.prototype._renderPlaceholder = function _renderPlaceholder() { if (this._showPlaceholder()) { var placeHolderProps = { text: nullthrows(this.props.placeholder), editorState: this.props.editorState, textAlignment: this.props.textAlignment, accessibilityID: this._placeholderAccessibilityID }; return React.createElement(DraftEditorPlaceholder, placeHolderProps); } return null; }; DraftEditor.prototype.render = function render() { var _this3 = this; var _props = this.props, blockRenderMap = _props.blockRenderMap, blockRendererFn = _props.blockRendererFn, blockStyleFn = _props.blockStyleFn, customStyleFn = _props.customStyleFn, customStyleMap = _props.customStyleMap, editorState = _props.editorState, readOnly = _props.readOnly, textAlignment = _props.textAlignment, textDirectionality = _props.textDirectionality; var rootClass = cx({ 'DraftEditor/root': true, 'DraftEditor/alignLeft': textAlignment === 'left', 'DraftEditor/alignRight': textAlignment === 'right', 'DraftEditor/alignCenter': textAlignment === 'center' }); var contentStyle = { outline: 'none', // fix parent-draggable Safari bug. #1326 userSelect: 'text', WebkitUserSelect: 'text', whiteSpace: 'pre-wrap', wordWrap: 'break-word' }; // The aria-expanded and aria-haspopup properties should only be rendered // for a combobox. var ariaRole = this.props.role || 'textbox'; var ariaExpanded = ariaRole === 'combobox' ? !!this.props.ariaExpanded : null; var editorContentsProps = { blockRenderMap: blockRenderMap, blockRendererFn: blockRendererFn, blockStyleFn: blockStyleFn, customStyleMap: _extends({}, DefaultDraftInlineStyle, customStyleMap), customStyleFn: customStyleFn, editorKey: this._editorKey, editorState: editorState, key: 'contents' + this.state.contentsKey, textDirectionality: textDirectionality }; return React.createElement( 'div', { className: rootClass }, this._renderPlaceholder(), React.createElement( 'div', { className: cx('DraftEditor/editorContainer'), ref: function ref(_ref3) { return _this3.editorContainer = _ref3; } }, React.createElement( 'div', { 'aria-activedescendant': readOnly ? null : this.props.ariaActiveDescendantID, 'aria-autocomplete': readOnly ? null : this.props.ariaAutoComplete, 'aria-controls': readOnly ? null : this.props.ariaControls, 'aria-describedby': this.props.ariaDescribedBy || this._placeholderAccessibilityID, 'aria-expanded': readOnly ? null : ariaExpanded, 'aria-label': this.props.ariaLabel, 'aria-labelledby': this.props.ariaLabelledBy, 'aria-multiline': this.props.ariaMultiline, autoCapitalize: this.props.autoCapitalize, autoComplete: this.props.autoComplete, autoCorrect: this.props.autoCorrect, className: cx({ // Chrome's built-in translation feature mutates the DOM in ways // that Draft doesn't expect (ex: adding tags inside // DraftEditorLeaf spans) and causes problems. We add notranslate // here which makes its autotranslation skip over this subtree. notranslate: !readOnly, 'public/DraftEditor/content': true }), 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: function ref(_ref2) { return _this3.editor = _ref2; }, role: readOnly ? null : ariaRole, spellCheck: allowSpellCheck && this.props.spellCheck, style: contentStyle, suppressContentEditableWarning: true, tabIndex: this.props.tabIndex }, React.createElement(DraftEditorContents, editorContentsProps) ) ) ); }; DraftEditor.prototype.componentDidMount = 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. */ DraftEditor.prototype.componentWillUpdate = function componentWillUpdate(nextProps) { this._blockSelectEvents = true; this._latestEditorState = nextProps.editorState; }; DraftEditor.prototype.componentDidUpdate = function componentDidUpdate() { this._blockSelectEvents = false; this._latestCommittedEditorState = this.props.editorState; }; /** * Used via `this.focus()`. * * Force focus back onto the editor node. * * We attempt to preserve scroll position when focusing. You can also pass * a specified scroll position (for cases like `cut` behavior where it should * be restored to a known position). */ /** * 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. */ /** * Used via `this.restoreEditorDOM()`. * * Force a complete re-render of the DraftEditorContents 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. */ /** * Used via `this.setClipboard(...)`. * * Set the clipboard state for a cut/copy event. */ /** * Used via `this.getClipboard()`. * * Retrieve the clipboard state for a cut/copy event. */ /** * 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. */ /** * 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. */ /** * See `onDragEnter()`. */ return DraftEditor; }(React.Component); DraftEditor.defaultProps = { blockRenderMap: DefaultDraftBlockRenderMap, blockRendererFn: emptyFunction.thatReturnsNull, blockStyleFn: emptyFunction.thatReturns(''), keyBindingFn: getDefaultKeyBinding, readOnly: false, spellCheck: false, stripPastedStyles: false }; module.exports = DraftEditor;