d2-ui
Version:
463 lines (415 loc) • 17.8 kB
JavaScript
/**
* 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.
*/