forked from Gitlink/forgeplus-react
472 lines
17 KiB
JavaScript
472 lines
17 KiB
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
|
||
|
|
* @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 <font> 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;
|