UNPKG

shengnian-editor

Version:

Shengnian React Rich Text Editor

514 lines (465 loc) 17.3 kB
'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;