shengnian-editor
Version:
Shengnian React Rich Text Editor
514 lines (465 loc) • 17.3 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _stringify = require('babel-runtime/core-js/json/stringify');
var _stringify2 = _interopRequireDefault(_stringify);
var _extends2 = require('babel-runtime/helpers/extends');
var _extends3 = _interopRequireDefault(_extends2);
var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of');
var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf);
var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
var _createClass2 = require('babel-runtime/helpers/createClass');
var _createClass3 = _interopRequireDefault(_createClass2);
var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn');
var _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2);
var _inherits2 = require('babel-runtime/helpers/inherits');
var _inherits3 = _interopRequireDefault(_inherits2);
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _reactDom = require('react-dom');
var _scrollView = require('./scrollView');
var _scrollView2 = _interopRequireDefault(_scrollView);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// import PropTypes from 'prop-types'
var IMPERATIVE_API = ['blur', 'checkValidity', 'click', 'focus', 'select', 'setCustomValidity', 'setSelectionRange', 'setRangeText'];
function getScrollOffset() {
return {
x: window.pageXOffset !== undefined ? window.pageXOffset : (document.documentElement || document.body.parentNode || document.body).scrollLeft,
y: window.pageYOffset !== undefined ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop
};
}
var Autocomplete = function (_React$Component) {
(0, _inherits3.default)(Autocomplete, _React$Component);
function Autocomplete(props) {
(0, _classCallCheck3.default)(this, Autocomplete);
var _this = (0, _possibleConstructorReturn3.default)(this, (Autocomplete.__proto__ || (0, _getPrototypeOf2.default)(Autocomplete)).call(this, props));
_this.state = {
isOpen: false,
highlightedIndex: null
};
_this._debugStates = [];
_this.ensureHighlightedIndex = _this.ensureHighlightedIndex.bind(_this);
_this.exposeAPI = _this.exposeAPI.bind(_this);
_this.handleInputFocus = _this.handleInputFocus.bind(_this);
_this.handleInputBlur = _this.handleInputBlur.bind(_this);
_this.handleChange = _this.handleChange.bind(_this);
_this.handleKeyDown = _this.handleKeyDown.bind(_this);
_this.handleInputClick = _this.handleInputClick.bind(_this);
_this.maybeAutoCompleteText = _this.maybeAutoCompleteText.bind(_this);
return _this;
}
(0, _createClass3.default)(Autocomplete, [{
key: 'componentWillMount',
value: function componentWillMount() {
// this.refs is frozen, so we need to assign a new object to it
this.refs = {};
this._ignoreBlur = false;
this._ignoreFocus = false;
this._scrollOffset = null;
this._scrollTimer = null;
}
}, {
key: 'componentWillUnmount',
value: function componentWillUnmount() {
clearTimeout(this._scrollTimer);
this._scrollTimer = null;
}
}, {
key: 'componentWillReceiveProps',
value: function componentWillReceiveProps(nextProps) {
if (this.state.highlightedIndex !== null) {
this.setState(this.ensureHighlightedIndex);
}
if (nextProps.autoHighlight && (this.props.value !== nextProps.value || this.state.highlightedIndex === null)) {
this.setState(this.maybeAutoCompleteText);
}
}
}, {
key: 'componentDidMount',
value: function componentDidMount() {
if (this.isOpen()) {
this.setMenuPositions();
}
}
}, {
key: 'componentDidUpdate',
value: function componentDidUpdate(prevProps, prevState) {
if (this.state.isOpen && !prevState.isOpen || 'open' in this.props && this.props.open && !prevProps.open) this.setMenuPositions();
this.maybeScrollItemIntoView();
if (prevState.isOpen !== this.state.isOpen) {
this.props.onMenuVisibilityChange(this.state.isOpen);
}
}
}, {
key: 'exposeAPI',
value: function exposeAPI(el) {
var _this2 = this;
this.refs.input = el;
IMPERATIVE_API.forEach(function (ev) {
return _this2[ev] = el && el[ev] && el[ev].bind(el);
});
}
}, {
key: 'maybeScrollItemIntoView',
value: function maybeScrollItemIntoView() {
if (this.isOpen() && this.state.highlightedIndex !== null) {
var itemNode = this.refs['item-' + this.state.highlightedIndex];
var menuNode = this.refs.menu;
(0, _scrollView2.default)((0, _reactDom.findDOMNode)(itemNode), (0, _reactDom.findDOMNode)(menuNode), { onlyScrollIfNeeded: true });
}
}
}, {
key: 'handleKeyDown',
value: function handleKeyDown(event) {
if (Autocomplete.keyDownHandlers[event.key]) Autocomplete.keyDownHandlers[event.key].call(this, event);else if (!this.isOpen()) {
this.setState({
isOpen: true
});
}
}
}, {
key: 'handleChange',
value: function handleChange(event) {
this.props.onChange(event, event.target.value);
}
}, {
key: 'getFilteredItems',
value: function getFilteredItems(props) {
var items = props.items;
if (props.shouldItemRender) {
items = items.filter(function (item) {
return props.shouldItemRender(item, props.value);
});
}
if (props.sortItems) {
items.sort(function (a, b) {
return props.sortItems(a, b, props.value);
});
}
return items;
}
}, {
key: 'maybeAutoCompleteText',
value: function maybeAutoCompleteText(state, props) {
var highlightedIndex = state.highlightedIndex;
var value = props.value,
getItemValue = props.getItemValue;
var index = highlightedIndex === null ? 0 : highlightedIndex;
var matchedItem = this.getFilteredItems(props)[index];
if (value !== '' && matchedItem) {
var itemValue = getItemValue(matchedItem);
var itemValueDoesMatch = itemValue.toLowerCase().indexOf(value.toLowerCase()) === 0;
if (itemValueDoesMatch) {
return { highlightedIndex: index };
}
}
return { highlightedIndex: null };
}
}, {
key: 'ensureHighlightedIndex',
value: function ensureHighlightedIndex(state, props) {
if (state.highlightedIndex >= this.getFilteredItems(props).length) {
return { highlightedIndex: null };
}
}
}, {
key: 'setMenuPositions',
value: function setMenuPositions() {
var node = this.refs.input;
var rect = node.getBoundingClientRect();
var computedStyle = global.window.getComputedStyle(node);
var marginBottom = parseInt(computedStyle.marginBottom, 10) || 0;
var marginLeft = parseInt(computedStyle.marginLeft, 10) || 0;
var marginRight = parseInt(computedStyle.marginRight, 10) || 0;
this.setState({
menuTop: rect.bottom + marginBottom,
menuLeft: rect.left + marginLeft,
menuWidth: rect.width + marginLeft + marginRight
});
}
}, {
key: 'highlightItemFromMouse',
value: function highlightItemFromMouse(index) {
this.setState({ highlightedIndex: index });
}
}, {
key: 'selectItemFromMouse',
value: function selectItemFromMouse(item) {
var _this3 = this;
var value = this.props.getItemValue(item);
// The menu will de-render before a mouseLeave event
// happens. Clear the flag to release control over focus
this.setIgnoreBlur(false);
this.setState({
isOpen: false,
highlightedIndex: null
}, function () {
_this3.props.onSelect(value, item);
});
}
}, {
key: 'setIgnoreBlur',
value: function setIgnoreBlur(ignore) {
this._ignoreBlur = ignore;
}
}, {
key: 'renderMenu',
value: function renderMenu() {
var _this4 = this;
var items = this.getFilteredItems(this.props).map(function (item, index) {
var element = _this4.props.renderItem(item, _this4.state.highlightedIndex === index, { cursor: 'default' });
return _react2.default.cloneElement(element, {
onMouseEnter: function onMouseEnter() {
return _this4.highlightItemFromMouse(index);
},
onClick: function onClick() {
return _this4.selectItemFromMouse(item);
},
ref: function ref(e) {
return _this4.refs['item-' + index] = e;
}
});
});
var style = {
left: this.state.menuLeft,
top: this.state.menuTop,
minWidth: this.state.menuWidth
};
var menu = this.props.renderMenu(items, this.props.value, style);
return _react2.default.cloneElement(menu, {
ref: function ref(e) {
return _this4.refs.menu = e;
},
// Ignore blur to prevent menu from de-rendering before we can process click
onMouseEnter: function onMouseEnter() {
return _this4.setIgnoreBlur(true);
},
onMouseLeave: function onMouseLeave() {
return _this4.setIgnoreBlur(false);
}
});
}
}, {
key: 'handleInputBlur',
value: function handleInputBlur(event) {
var _this5 = this;
if (this._ignoreBlur) {
this._ignoreFocus = true;
this._scrollOffset = getScrollOffset();
this.refs.input.focus();
return;
}
var setStateCallback = void 0;
var highlightedIndex = this.state.highlightedIndex;
if (this.props.selectOnBlur && highlightedIndex !== null) {
var _items = this.getFilteredItems(this.props);
var item = _items[highlightedIndex];
var _value = this.props.getItemValue(item);
setStateCallback = function setStateCallback() {
return _this5.props.onSelect(_value, item);
};
}
this.setState({
isOpen: false,
highlightedIndex: null
}, setStateCallback);
var onBlur = this.props.inputProps.onBlur;
if (onBlur) {
onBlur(event);
}
}
}, {
key: 'handleInputFocus',
value: function handleInputFocus(event) {
var _this6 = this;
if (this._ignoreFocus) {
this._ignoreFocus = false;
var _scrollOffset = this._scrollOffset,
x = _scrollOffset.x,
y = _scrollOffset.y;
this._scrollOffset = null;
// Focus will cause the browser to scroll the <input> into view.
// This can cause the mouse coords to change, which in turn
// could cause a new highlight to happen, cancelling the click
// event (when selecting with the mouse)
window.scrollTo(x, y);
// Some browsers wait until all focus event handlers have been
// processed before scrolling the <input> into view, so let's
// scroll again on the next tick to ensure we're back to where
// the user was before focus was lost. We could do the deferred
// scroll only, but that causes a jarring split second jump in
// some browsers that scroll before the focus event handlers
// are triggered.
clearTimeout(this._scrollTimer);
this._scrollTimer = setTimeout(function () {
_this6._scrollTimer = null;
window.scrollTo(x, y);
}, 0);
return;
}
this.setState({ isOpen: true });
var onFocus = this.props.inputProps.onFocus;
if (onFocus) {
onFocus(event);
}
}
}, {
key: 'isInputFocused',
value: function isInputFocused() {
var el = this.refs.input;
return el.ownerDocument && el === el.ownerDocument.activeElement;
}
}, {
key: 'handleInputClick',
value: function handleInputClick() {
// Input will not be focused if it's disabled
if (this.isInputFocused() && !this.isOpen()) this.setState({ isOpen: true });
}
}, {
key: 'composeEventHandlers',
value: function composeEventHandlers(internal, external) {
return external ? function (e) {
internal(e);external(e);
} : internal;
}
}, {
key: 'isOpen',
value: function isOpen() {
return 'open' in this.props ? this.props.open : this.state.isOpen;
}
}, {
key: 'render',
value: function render() {
if (this.props.debug) {
// you don't like it, you love it
this._debugStates.push({
id: this._debugStates.length,
state: this.state
});
}
var inputProps = this.props.inputProps;
var open = this.isOpen();
return _react2.default.createElement(
'div',
(0, _extends3.default)({ style: (0, _extends3.default)({}, this.props.wrapperStyle) }, this.props.wrapperProps),
this.props.renderInput((0, _extends3.default)({}, inputProps, {
role: 'combobox',
'aria-autocomplete': 'list',
'aria-expanded': open,
autoComplete: 'off',
ref: this.exposeAPI,
onFocus: this.handleInputFocus,
onBlur: this.handleInputBlur,
onChange: this.handleChange,
onKeyDown: this.composeEventHandlers(this.handleKeyDown, inputProps.onKeyDown),
onClick: this.composeEventHandlers(this.handleInputClick, inputProps.onClick),
value: this.props.value
})),
open && this.renderMenu(),
this.props.debug && _react2.default.createElement(
'pre',
{ style: { marginLeft: 300 } },
(0, _stringify2.default)(this._debugStates.slice(Math.max(0, this._debugStates.length - 5), this._debugStates.length), null, 2)
)
);
}
}]);
return Autocomplete;
}(_react2.default.Component);
Autocomplete.defaultProps = {
value: '',
wrapperProps: {},
wrapperStyle: {
display: 'inline-block'
},
inputProps: {},
renderInput: function renderInput(props) {
return _react2.default.createElement('input', props);
},
onChange: function onChange() {},
onSelect: function onSelect() {},
renderMenu: function renderMenu(items, value, style) {
return _react2.default.createElement('div', { style: (0, _extends3.default)({}, style, this.menuStyle), children: items });
},
menuStyle: {
borderRadius: '3px',
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.1)',
background: 'rgba(255, 255, 255, 0.9)',
padding: '2px 0',
fontSize: '90%',
position: 'fixed',
overflow: 'auto',
maxHeight: '50%' // TODO: don't cheat, let it flow to the bottom
},
autoHighlight: true,
selectOnBlur: false,
onMenuVisibilityChange: function onMenuVisibilityChange() {}
};
Autocomplete.keyDownHandlers = {
ArrowDown: function ArrowDown(event) {
event.preventDefault();
var itemsLength = this.getFilteredItems(this.props).length;
if (!itemsLength) return;
var highlightedIndex = this.state.highlightedIndex;
var index = highlightedIndex === null || highlightedIndex === itemsLength - 1 ? 0 : highlightedIndex + 1;
this.setState({
highlightedIndex: index,
isOpen: true
});
},
ArrowUp: function ArrowUp(event) {
event.preventDefault();
var itemsLength = this.getFilteredItems(this.props).length;
if (!itemsLength) return;
var highlightedIndex = this.state.highlightedIndex;
var index = highlightedIndex === 0 || highlightedIndex === null ? itemsLength - 1 : highlightedIndex - 1;
this.setState({
highlightedIndex: index,
isOpen: true
});
},
Enter: function Enter(event) {
var _this7 = this;
// Key code 229 is used for selecting items from character selectors (Pinyin, Kana, etc)
if (event.keyCode !== 13) return;
if (!this.isOpen()) {
// menu is closed so there is no selection to accept -> do nothing
return;
} else if (this.state.highlightedIndex == null) {
// input has focus but no menu item is selected + enter is hit -> close the menu, highlight whatever's in input
this.setState({
isOpen: false
}, function () {
_this7.refs.input.select();
});
} else {
// text entered + menu item has been highlighted + enter is hit -> update value to that of selected menu item, close the menu
event.preventDefault();
var item = this.getFilteredItems(this.props)[this.state.highlightedIndex];
var _value2 = this.props.getItemValue(item);
this.setState({
isOpen: false,
highlightedIndex: null
}, function () {
//this.refs.input.focus() // TODO: file issue
_this7.refs.input.setSelectionRange(_value2.length, _value2.length);
_this7.props.onSelect(_value2, item);
});
}
},
Escape: function Escape() {
// In case the user is currently hovering over the menu
this.setIgnoreBlur(false);
this.setState({
highlightedIndex: null,
isOpen: false
});
},
Tab: function Tab() {
// In case the user is currently hovering over the menu
this.setIgnoreBlur(false);
}
};
exports.default = Autocomplete;