UNPKG

atom-nuclide

Version:

A unified developer experience for web and mobile development, built as a suite of features on top of Atom to provide hackability and the support of an active community.

389 lines (352 loc) 14.9 kB
Object.defineProperty(exports, '__esModule', { value: true }); 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 _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } } 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; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } /* * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the license found in the LICENSE file in * the root directory of this source tree. */ var _rxjsBundlesRxUmdMinJs2; function _rxjsBundlesRxUmdMinJs() { return _rxjsBundlesRxUmdMinJs2 = _interopRequireDefault(require('rxjs/bundles/Rx.umd.min.js')); } var _atom2; function _atom() { return _atom2 = require('atom'); } var _AtomInput2; function _AtomInput() { return _AtomInput2 = require('./AtomInput'); } var _reactForAtom2; function _reactForAtom() { return _reactForAtom2 = require('react-for-atom'); } /** * A Combo Box. * TODO allow making text input non-editable via props * TODO open/close options dropdown upon focus/blur * TODO add public getter/setter for textInput * TODO use generic search provider * TODO move combobox to separate package. */ var Combobox = (function (_React$Component) { _inherits(Combobox, _React$Component); _createClass(Combobox, null, [{ key: 'defaultProps', value: { className: '', maxOptionCount: 10, onChange: function onChange(newValue) {}, onSelect: function onSelect(newValue) {}, width: 200 }, enumerable: true }]); function Combobox(props) { _classCallCheck(this, Combobox); _get(Object.getPrototypeOf(Combobox.prototype), 'constructor', this).call(this, props); this.state = { error: null, filteredOptions: [], loadingOptions: false, options: [], optionsVisible: false, selectedIndex: -1, textInput: props.initialTextInput }; this.receiveUpdate = this.receiveUpdate.bind(this); this._handleTextInputChange = this._handleTextInputChange.bind(this); this._handleInputBlur = this._handleInputBlur.bind(this); this._handleInputFocus = this._handleInputFocus.bind(this); this._handleMoveDown = this._handleMoveDown.bind(this); this._handleMoveUp = this._handleMoveUp.bind(this); this._handleCancel = this._handleCancel.bind(this); this._handleConfirm = this._handleConfirm.bind(this); this._scrollSelectedOptionIntoViewIfNeeded = this._scrollSelectedOptionIntoViewIfNeeded.bind(this); } _createClass(Combobox, [{ key: 'componentDidMount', value: function componentDidMount() { var node = (_reactForAtom2 || _reactForAtom()).ReactDOM.findDOMNode(this); var _subscriptions = this._subscriptions = new (_atom2 || _atom()).CompositeDisposable(); _subscriptions.add(atom.commands.add(node, 'core:move-up', this._handleMoveUp), atom.commands.add(node, 'core:move-down', this._handleMoveDown), atom.commands.add(node, 'core:cancel', this._handleCancel), atom.commands.add(node, 'core:confirm', this._handleConfirm), this.refs.freeformInput.onDidChange(this._handleTextInputChange)); this.requestUpdate(this.state.textInput); } }, { key: 'componentWillUnmount', value: function componentWillUnmount() { if (this._subscriptions) { this._subscriptions.dispose(); } if (this._updateSubscription != null) { this._updateSubscription.unsubscribe(); } } }, { key: 'requestUpdate', value: function requestUpdate(textInput) { var _this = this; // Cancel pending update. if (this._updateSubscription != null) { this._updateSubscription.unsubscribe(); } this.setState({ error: null, loadingOptions: true }); this._updateSubscription = (_rxjsBundlesRxUmdMinJs2 || _rxjsBundlesRxUmdMinJs()).default.Observable.fromPromise(this.props.requestOptions(textInput)).subscribe(function (options) { return _this.receiveUpdate(options); }, function (err) { _this.setState({ error: err, loadingOptions: false, options: [], filteredOptions: [] }); if (_this.props.onRequestOptionsError != null) { _this.props.onRequestOptionsError(err); } }); } }, { key: 'receiveUpdate', value: function receiveUpdate(newOptions) { var filteredOptions = this._getFilteredOptions(newOptions, this.state.textInput); this.setState({ error: null, loadingOptions: false, options: newOptions, filteredOptions: filteredOptions }); } }, { key: 'selectValue', value: function selectValue(newValue, didRenderCallback) { this.refs.freeformInput.setText(newValue); this.setState({ textInput: newValue, selectedIndex: -1, optionsVisible: false }, didRenderCallback); this.props.onSelect(newValue); // Selecting a value in the dropdown changes the text as well. Call the callback accordingly. this.props.onChange(newValue); } }, { key: 'getText', value: function getText() { return this.refs.freeformInput.getText(); } // TODO use native (fuzzy/strict - configurable?) filter provider }, { key: '_getFilteredOptions', value: function _getFilteredOptions(options, filterValue) { var lowerCaseState = filterValue.toLowerCase(); return options.map(function (option) { var valueLowercase = option.toLowerCase(); return { value: option, valueLowercase: valueLowercase, matchIndex: valueLowercase.indexOf(lowerCaseState) }; }).filter(function (option) { return option.matchIndex !== -1; }).slice(0, this.props.maxOptionCount); } }, { key: '_handleTextInputChange', value: function _handleTextInputChange() { var newText = this.refs.freeformInput.getText(); if (newText === this.state.textInput) { return; } this.requestUpdate(newText); var filteredOptions = this._getFilteredOptions(this.state.options, newText); var selectedIndex = undefined; if (filteredOptions.length === 0) { // If there aren't any options, don't select anything. selectedIndex = -1; } else if (this.state.selectedIndex === -1 || this.state.selectedIndex >= filteredOptions.length) { // If there are options and the selected index is out of bounds, // default to the first item. selectedIndex = 0; } else { selectedIndex = this.state.selectedIndex; } this.setState({ textInput: newText, optionsVisible: true, filteredOptions: filteredOptions, selectedIndex: selectedIndex }); this.props.onChange(newText); } }, { key: '_handleInputFocus', value: function _handleInputFocus() { this.requestUpdate(this.state.textInput); this.setState({ optionsVisible: true }); } }, { key: '_handleInputBlur', value: function _handleInputBlur() { // Delay hiding the combobox long enough for a click inside the combobox to trigger on it in // case the blur was caused by a click inside the combobox. 150ms is empirically long enough to // let the stack clear from this blur event and for the click event to trigger. setTimeout(this._handleCancel, 150); var onBlur = this.props.onBlur; if (onBlur != null) { onBlur(this.getText()); } } }, { key: '_handleItemClick', value: function _handleItemClick(selectedValue, event) { var _this2 = this; this.selectValue(selectedValue, function () { // Focus the input again because the click will cause the input to blur. This mimics native // <select> behavior by keeping focus in the form being edited. var input = (_reactForAtom2 || _reactForAtom()).ReactDOM.findDOMNode(_this2.refs.freeformInput); if (input) { input.focus(); } }); } }, { key: '_handleMoveDown', value: function _handleMoveDown() { this.setState({ selectedIndex: Math.min(this.props.maxOptionCount - 1, this.state.selectedIndex + 1, this.state.filteredOptions.length - 1) }, this._scrollSelectedOptionIntoViewIfNeeded); } }, { key: '_handleMoveUp', value: function _handleMoveUp() { this.setState({ selectedIndex: Math.max(0, this.state.selectedIndex - 1) }, this._scrollSelectedOptionIntoViewIfNeeded); } }, { key: '_handleCancel', value: function _handleCancel() { this.setState({ optionsVisible: false }); } }, { key: '_handleConfirm', value: function _handleConfirm() { var option = this.state.filteredOptions[this.state.selectedIndex]; if (option !== undefined) { this.selectValue(option.value); } } }, { key: '_setSelectedIndex', value: function _setSelectedIndex(selectedIndex) { this.setState({ selectedIndex: selectedIndex }); } }, { key: '_scrollSelectedOptionIntoViewIfNeeded', value: function _scrollSelectedOptionIntoViewIfNeeded() { var selectedOption = (_reactForAtom2 || _reactForAtom()).ReactDOM.findDOMNode(this.refs.selectedOption); if (selectedOption) { selectedOption.scrollIntoViewIfNeeded(); } } }, { key: 'render', value: function render() { var _this3 = this; var optionsContainer = undefined; var options = []; if (this.props.loadingMessage && this.state.loadingOptions) { options.push((_reactForAtom2 || _reactForAtom()).React.createElement( 'li', { key: 'loading-text', className: 'loading' }, (_reactForAtom2 || _reactForAtom()).React.createElement( 'span', { className: 'loading-message' }, this.props.loadingMessage ) )); } if (this.state.error != null && this.props.formatRequestOptionsErrorMessage != null) { var message = this.props.formatRequestOptionsErrorMessage(this.state.error); options.push((_reactForAtom2 || _reactForAtom()).React.createElement( 'li', { className: 'text-error' }, message )); } if (this.state.optionsVisible) { options.push.apply(options, _toConsumableArray(this.state.filteredOptions.map(function (option, i) { var beforeMatch = option.value.substring(0, option.matchIndex); var endOfMatchIndex = option.matchIndex + _this3.state.textInput.length; var highlightedMatch = option.value.substring(option.matchIndex, endOfMatchIndex); var afterMatch = option.value.substring(endOfMatchIndex, option.value.length); var isSelected = i === _this3.state.selectedIndex; return (_reactForAtom2 || _reactForAtom()).React.createElement( 'li', { className: isSelected ? 'selected' : null, key: option.value, onClick: _this3._handleItemClick.bind(_this3, option.value), onMouseOver: _this3._setSelectedIndex.bind(_this3, i), ref: isSelected ? 'selectedOption' : null }, beforeMatch, (_reactForAtom2 || _reactForAtom()).React.createElement( 'strong', { className: 'text-highlight' }, highlightedMatch ), afterMatch ); }))); if (!options.length) { options.push((_reactForAtom2 || _reactForAtom()).React.createElement( 'li', { className: 'text-subtle', key: 'no-results-found' }, 'No results found' )); } optionsContainer = (_reactForAtom2 || _reactForAtom()).React.createElement( 'ol', { className: 'list-group' }, options ); } var _props = this.props; var initialTextInput = _props.initialTextInput; var placeholderText = _props.placeholderText; var size = _props.size; var width = _props.width; var wrapperStyle = { width: width == null ? undefined : width + 'px' }; return (_reactForAtom2 || _reactForAtom()).React.createElement( 'div', { className: 'select-list popover-list popover-list-subtle ' + this.props.className, style: wrapperStyle }, (_reactForAtom2 || _reactForAtom()).React.createElement((_AtomInput2 || _AtomInput()).AtomInput, { initialValue: initialTextInput, onBlur: this._handleInputBlur, onFocus: this._handleInputFocus, placeholderText: placeholderText, ref: 'freeformInput', size: size, width: width }), optionsContainer ); } }]); return Combobox; })((_reactForAtom2 || _reactForAtom()).React.Component); exports.Combobox = Combobox;