UNPKG

d2-ui

Version:
463 lines (415 loc) 17.8 kB
/** * 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 _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 = 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 = 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; 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' }; 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.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. */