UNPKG

21.9 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6
7var _extends2 = require('babel-runtime/helpers/extends');
8
9var _extends3 = _interopRequireDefault(_extends2);
10
11var _typeof2 = require('babel-runtime/helpers/typeof');
12
13var _typeof3 = _interopRequireDefault(_typeof2);
14
15var _objectWithoutProperties2 = require('babel-runtime/helpers/objectWithoutProperties');
16
17var _objectWithoutProperties3 = _interopRequireDefault(_objectWithoutProperties2);
18
19var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of');
20
21var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf);
22
23var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
24
25var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
26
27var _createClass2 = require('babel-runtime/helpers/createClass');
28
29var _createClass3 = _interopRequireDefault(_createClass2);
30
31var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn');
32
33var _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2);
34
35var _inherits2 = require('babel-runtime/helpers/inherits');
36
37var _inherits3 = _interopRequireDefault(_inherits2);
38
39var _simpleAssign = require('simple-assign');
40
41var _simpleAssign2 = _interopRequireDefault(_simpleAssign);
42
43var _react = require('react');
44
45var _react2 = _interopRequireDefault(_react);
46
47var _propTypes = require('prop-types');
48
49var _propTypes2 = _interopRequireDefault(_propTypes);
50
51var _reactDom = require('react-dom');
52
53var _reactDom2 = _interopRequireDefault(_reactDom);
54
55var _keycode = require('keycode');
56
57var _keycode2 = _interopRequireDefault(_keycode);
58
59var _TextField = require('../TextField');
60
61var _TextField2 = _interopRequireDefault(_TextField);
62
63var _Menu = require('../Menu');
64
65var _Menu2 = _interopRequireDefault(_Menu);
66
67var _MenuItem = require('../MenuItem');
68
69var _MenuItem2 = _interopRequireDefault(_MenuItem);
70
71var _Divider = require('../Divider');
72
73var _Divider2 = _interopRequireDefault(_Divider);
74
75var _Popover = require('../Popover/Popover');
76
77var _Popover2 = _interopRequireDefault(_Popover);
78
79var _propTypes3 = require('../utils/propTypes');
80
81var _propTypes4 = _interopRequireDefault(_propTypes3);
82
83function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
84
85function getStyles(props, context, state) {
86 var anchorEl = state.anchorEl;
87 var fullWidth = props.fullWidth;
88
89
90 var styles = {
91 root: {
92 display: 'inline-block',
93 position: 'relative',
94 width: fullWidth ? '100%' : 256
95 },
96 menu: {
97 width: '100%'
98 },
99 list: {
100 display: 'block',
101 width: fullWidth ? '100%' : 256
102 },
103 innerDiv: {
104 overflow: 'hidden'
105 }
106 };
107
108 if (anchorEl && fullWidth) {
109 styles.popover = {
110 width: anchorEl.clientWidth
111 };
112 }
113
114 return styles;
115}
116
117var AutoComplete = function (_Component) {
118 (0, _inherits3.default)(AutoComplete, _Component);
119
120 function AutoComplete() {
121 var _ref;
122
123 var _temp, _this, _ret;
124
125 (0, _classCallCheck3.default)(this, AutoComplete);
126
127 for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
128 args[_key] = arguments[_key];
129 }
130
131 return _ret = (_temp = (_this = (0, _possibleConstructorReturn3.default)(this, (_ref = AutoComplete.__proto__ || (0, _getPrototypeOf2.default)(AutoComplete)).call.apply(_ref, [this].concat(args))), _this), _this.state = {
132 anchorEl: null,
133 focusTextField: true,
134 open: false,
135 searchText: undefined
136 }, _this.handleRequestClose = function () {
137 // Only take into account the Popover clickAway when we are
138 // not focusing the TextField.
139 if (!_this.state.focusTextField) {
140 _this.close();
141 }
142 }, _this.handleMouseDown = function (event) {
143 // Keep the TextField focused
144 event.preventDefault();
145 }, _this.handleItemClick = function (event, child) {
146 var dataSource = _this.props.dataSource;
147 var index = parseInt(child.key, 10);
148 var chosenRequest = dataSource[index];
149 var searchText = _this.chosenRequestText(chosenRequest);
150
151 var updateInput = function updateInput() {
152 return _this.props.onUpdateInput(searchText, _this.props.dataSource, {
153 source: 'click'
154 });
155 };
156 _this.timerClickCloseId = function () {
157 return setTimeout(function () {
158 _this.timerClickCloseId = null;
159 _this.close();
160 _this.props.onNewRequest(chosenRequest, index);
161 }, _this.props.menuCloseDelay);
162 };
163
164 if (typeof _this.props.searchText !== 'undefined') {
165 updateInput();
166 _this.timerClickCloseId();
167 } else {
168 _this.setState({
169 searchText: searchText
170 }, function () {
171 updateInput();
172 _this.timerClickCloseId();
173 });
174 }
175 }, _this.chosenRequestText = function (chosenRequest) {
176 if (typeof chosenRequest === 'string') {
177 return chosenRequest;
178 } else {
179 return chosenRequest[_this.props.dataSourceConfig.text];
180 }
181 }, _this.handleEscKeyDown = function () {
182 _this.close();
183 }, _this.handleKeyDown = function (event) {
184 if (_this.props.onKeyDown) _this.props.onKeyDown(event);
185
186 switch ((0, _keycode2.default)(event)) {
187 case 'enter':
188 _this.close();
189 var searchText = _this.state.searchText;
190 if (searchText !== '') {
191 _this.props.onNewRequest(searchText, -1);
192 }
193 break;
194
195 case 'esc':
196 _this.close();
197 break;
198
199 case 'down':
200 event.preventDefault();
201 _this.setState({
202 open: true,
203 focusTextField: false,
204 anchorEl: _reactDom2.default.findDOMNode(_this.refs.searchTextField)
205 });
206 break;
207
208 default:
209 break;
210 }
211 }, _this.handleChange = function (event) {
212 var searchText = event.target.value;
213
214 // Make sure that we have a new searchText.
215 // Fix an issue with a Cordova Webview
216 if (searchText === _this.state.searchText) {
217 return;
218 }
219
220 var state = {
221 open: true,
222 anchorEl: _reactDom2.default.findDOMNode(_this.refs.searchTextField)
223 };
224
225 if (_this.props.searchText === undefined) {
226 state.searchText = searchText;
227 }
228
229 _this.setState(state);
230
231 _this.props.onUpdateInput(searchText, _this.props.dataSource, {
232 source: 'change'
233 });
234 }, _this.handleBlur = function (event) {
235 if (_this.state.focusTextField && _this.timerClickCloseId === null) {
236 _this.timerBlurClose = setTimeout(function () {
237 _this.close();
238 }, 0);
239 }
240
241 if (_this.props.onBlur) {
242 _this.props.onBlur(event);
243 }
244 }, _this.handleFocus = function (event) {
245 if (!_this.state.open && _this.props.openOnFocus) {
246 _this.setState({
247 open: true,
248 anchorEl: _reactDom2.default.findDOMNode(_this.refs.searchTextField)
249 });
250 }
251
252 _this.setState({
253 focusTextField: true
254 });
255
256 if (_this.props.onFocus) {
257 _this.props.onFocus(event);
258 }
259 }, _temp), (0, _possibleConstructorReturn3.default)(_this, _ret);
260 }
261
262 (0, _createClass3.default)(AutoComplete, [{
263 key: 'componentWillMount',
264 value: function componentWillMount() {
265 this.requestsList = [];
266 this.setState({
267 open: this.props.open,
268 searchText: this.props.searchText || ''
269 });
270 this.timerClickCloseId = null;
271 }
272 }, {
273 key: 'componentWillReceiveProps',
274 value: function componentWillReceiveProps(nextProps) {
275 if (this.props.searchText !== nextProps.searchText) {
276 this.setState({
277 searchText: nextProps.searchText
278 });
279 }
280 if (this.props.open !== nextProps.open) {
281 this.setState({
282 open: nextProps.open,
283 anchorEl: _reactDom2.default.findDOMNode(this.refs.searchTextField)
284 });
285 }
286 }
287 }, {
288 key: 'componentWillUnmount',
289 value: function componentWillUnmount() {
290 clearTimeout(this.timerClickCloseId);
291 clearTimeout(this.timerBlurClose);
292 }
293 }, {
294 key: 'close',
295 value: function close() {
296 this.setState({
297 open: false,
298 anchorEl: null
299 });
300
301 if (this.props.onClose) {
302 this.props.onClose();
303 }
304 }
305 }, {
306 key: 'blur',
307 value: function blur() {
308 this.refs.searchTextField.blur();
309 }
310 }, {
311 key: 'focus',
312 value: function focus() {
313 this.refs.searchTextField.focus();
314 }
315 }, {
316 key: 'render',
317 value: function render() {
318 var _this2 = this;
319
320 var _props = this.props,
321 anchorOrigin = _props.anchorOrigin,
322 animated = _props.animated,
323 animation = _props.animation,
324 dataSource = _props.dataSource,
325 dataSourceConfig = _props.dataSourceConfig,
326 disableFocusRipple = _props.disableFocusRipple,
327 errorStyle = _props.errorStyle,
328 floatingLabelText = _props.floatingLabelText,
329 filter = _props.filter,
330 fullWidth = _props.fullWidth,
331 style = _props.style,
332 hintText = _props.hintText,
333 maxSearchResults = _props.maxSearchResults,
334 menuCloseDelay = _props.menuCloseDelay,
335 textFieldStyle = _props.textFieldStyle,
336 menuStyle = _props.menuStyle,
337 menuProps = _props.menuProps,
338 listStyle = _props.listStyle,
339 targetOrigin = _props.targetOrigin,
340 onBlur = _props.onBlur,
341 onClose = _props.onClose,
342 onFocus = _props.onFocus,
343 onKeyDown = _props.onKeyDown,
344 onNewRequest = _props.onNewRequest,
345 onUpdateInput = _props.onUpdateInput,
346 openOnFocus = _props.openOnFocus,
347 popoverProps = _props.popoverProps,
348 searchTextProp = _props.searchText,
349 other = (0, _objectWithoutProperties3.default)(_props, ['anchorOrigin', 'animated', 'animation', 'dataSource', 'dataSourceConfig', 'disableFocusRipple', 'errorStyle', 'floatingLabelText', 'filter', 'fullWidth', 'style', 'hintText', 'maxSearchResults', 'menuCloseDelay', 'textFieldStyle', 'menuStyle', 'menuProps', 'listStyle', 'targetOrigin', 'onBlur', 'onClose', 'onFocus', 'onKeyDown', 'onNewRequest', 'onUpdateInput', 'openOnFocus', 'popoverProps', 'searchText']);
350
351 var _ref2 = popoverProps || {},
352 popoverStyle = _ref2.style,
353 popoverOther = (0, _objectWithoutProperties3.default)(_ref2, ['style']);
354
355 var _state = this.state,
356 open = _state.open,
357 anchorEl = _state.anchorEl,
358 searchText = _state.searchText,
359 focusTextField = _state.focusTextField;
360 var prepareStyles = this.context.muiTheme.prepareStyles;
361
362 var styles = getStyles(this.props, this.context, this.state);
363
364 var requestsList = [];
365
366 dataSource.every(function (item, index) {
367 switch (typeof item === 'undefined' ? 'undefined' : (0, _typeof3.default)(item)) {
368 case 'string':
369 if (filter(searchText, item, item)) {
370 requestsList.push({
371 text: item,
372 value: _react2.default.createElement(_MenuItem2.default, {
373 innerDivStyle: styles.innerDiv,
374 value: item,
375 primaryText: item,
376 disableFocusRipple: disableFocusRipple,
377 key: index
378 })
379 });
380 }
381 break;
382
383 case 'object':
384 if (item && typeof item[_this2.props.dataSourceConfig.text] === 'string') {
385 var itemText = item[_this2.props.dataSourceConfig.text];
386 if (!_this2.props.filter(searchText, itemText, item)) break;
387
388 var itemValue = item[_this2.props.dataSourceConfig.value];
389 if (itemValue && itemValue.type && (itemValue.type.muiName === _MenuItem2.default.muiName || itemValue.type.muiName === _Divider2.default.muiName)) {
390 requestsList.push({
391 text: itemText,
392 value: _react2.default.cloneElement(itemValue, {
393 key: index,
394 disableFocusRipple: disableFocusRipple
395 })
396 });
397 } else {
398 requestsList.push({
399 text: itemText,
400 value: _react2.default.createElement(_MenuItem2.default, {
401 innerDivStyle: styles.innerDiv,
402 primaryText: itemText,
403 disableFocusRipple: disableFocusRipple,
404 key: index
405 })
406 });
407 }
408 }
409 break;
410
411 default:
412 // Do nothing
413 }
414
415 return !(maxSearchResults && maxSearchResults > 0 && requestsList.length === maxSearchResults);
416 });
417
418 this.requestsList = requestsList;
419
420 var menu = open && requestsList.length > 0 && _react2.default.createElement(
421 _Menu2.default,
422 (0, _extends3.default)({
423 ref: 'menu',
424 autoWidth: false,
425 disableAutoFocus: focusTextField,
426 onEscKeyDown: this.handleEscKeyDown,
427 initiallyKeyboardFocused: true,
428 onItemClick: this.handleItemClick,
429 onMouseDown: this.handleMouseDown,
430 style: (0, _simpleAssign2.default)(styles.menu, menuStyle),
431 listStyle: (0, _simpleAssign2.default)(styles.list, listStyle)
432 }, menuProps),
433 requestsList.map(function (i) {
434 return i.value;
435 })
436 );
437
438 return _react2.default.createElement(
439 'div',
440 { style: prepareStyles((0, _simpleAssign2.default)(styles.root, style)) },
441 _react2.default.createElement(_TextField2.default, (0, _extends3.default)({
442 ref: 'searchTextField',
443 autoComplete: 'off',
444 onBlur: this.handleBlur,
445 onFocus: this.handleFocus,
446 onKeyDown: this.handleKeyDown,
447 floatingLabelText: floatingLabelText,
448 hintText: hintText,
449 fullWidth: fullWidth,
450 multiLine: false,
451 errorStyle: errorStyle,
452 style: textFieldStyle
453 }, other, {
454 // value and onChange are idiomatic properties often leaked.
455 // We prevent their overrides in order to reduce potential bugs.
456 value: searchText,
457 onChange: this.handleChange
458 })),
459 _react2.default.createElement(
460 _Popover2.default,
461 (0, _extends3.default)({
462 style: (0, _simpleAssign2.default)({}, styles.popover, popoverStyle),
463 canAutoPosition: false,
464 anchorOrigin: anchorOrigin,
465 targetOrigin: targetOrigin,
466 open: open,
467 anchorEl: anchorEl,
468 useLayerForClickAway: false,
469 onRequestClose: this.handleRequestClose,
470 animated: animated,
471 animation: animation
472 }, popoverOther),
473 menu
474 )
475 );
476 }
477 }]);
478 return AutoComplete;
479}(_react.Component);
480
481AutoComplete.defaultProps = {
482 anchorOrigin: {
483 vertical: 'bottom',
484 horizontal: 'left'
485 },
486 animated: true,
487 dataSourceConfig: {
488 text: 'text',
489 value: 'value'
490 },
491 disableFocusRipple: true,
492 filter: function filter(searchText, key) {
493 return searchText !== '' && key.indexOf(searchText) !== -1;
494 },
495 fullWidth: false,
496 open: false,
497 openOnFocus: false,
498 onUpdateInput: function onUpdateInput() {},
499 onNewRequest: function onNewRequest() {},
500 menuCloseDelay: 300,
501 targetOrigin: {
502 vertical: 'top',
503 horizontal: 'left'
504 }
505};
506AutoComplete.contextTypes = {
507 muiTheme: _propTypes2.default.object.isRequired
508};
509AutoComplete.propTypes = process.env.NODE_ENV !== "production" ? {
510 /**
511 * Location of the anchor for the auto complete.
512 */
513 anchorOrigin: _propTypes4.default.origin,
514 /**
515 * If true, the auto complete is animated as it is toggled.
516 */
517 animated: _propTypes2.default.bool,
518 /**
519 * Override the default animation component used.
520 */
521 animation: _propTypes2.default.func,
522 /**
523 * Array of strings or nodes used to populate the list.
524 */
525 dataSource: _propTypes2.default.array.isRequired,
526 /**
527 * Config for objects list dataSource.
528 *
529 * @typedef {Object} dataSourceConfig
530 *
531 * @property {string} text `dataSource` element key used to find a string to be matched for search
532 * and shown as a `TextField` input value after choosing the result.
533 * @property {string} value `dataSource` element key used to find a string to be shown in search results.
534 */
535 dataSourceConfig: _propTypes2.default.object,
536 /**
537 * Disables focus ripple when true.
538 */
539 disableFocusRipple: _propTypes2.default.bool,
540 /**
541 * Override style prop for error.
542 */
543 errorStyle: _propTypes2.default.object,
544 /**
545 * The error content to display.
546 */
547 errorText: _propTypes2.default.node,
548 /**
549 * Callback function used to filter the auto complete.
550 *
551 * @param {string} searchText The text to search for within `dataSource`.
552 * @param {string} key `dataSource` element, or `text` property on that element if it's not a string.
553 * @returns {boolean} `true` indicates the auto complete list will include `key` when the input is `searchText`.
554 */
555 filter: _propTypes2.default.func,
556 /**
557 * The content to use for adding floating label element.
558 */
559 floatingLabelText: _propTypes2.default.node,
560 /**
561 * If true, the field receives the property `width: 100%`.
562 */
563 fullWidth: _propTypes2.default.bool,
564 /**
565 * The hint content to display.
566 */
567 hintText: _propTypes2.default.node,
568 /**
569 * Override style for list.
570 */
571 listStyle: _propTypes2.default.object,
572 /**
573 * The max number of search results to be shown.
574 * By default it shows all the items which matches filter.
575 */
576 maxSearchResults: _propTypes2.default.number,
577 /**
578 * Delay for closing time of the menu.
579 */
580 menuCloseDelay: _propTypes2.default.number,
581 /**
582 * Props to be passed to menu.
583 */
584 menuProps: _propTypes2.default.object,
585 /**
586 * Override style for menu.
587 */
588 menuStyle: _propTypes2.default.object,
589 /** @ignore */
590 onBlur: _propTypes2.default.func,
591 /**
592 * Callback function fired when the menu is closed.
593 */
594 onClose: _propTypes2.default.func,
595 /** @ignore */
596 onFocus: _propTypes2.default.func,
597 /** @ignore */
598 onKeyDown: _propTypes2.default.func,
599 /**
600 * Callback function that is fired when a list item is selected, or enter is pressed in the `TextField`.
601 *
602 * @param {string} chosenRequest Either the `TextField` input value, if enter is pressed in the `TextField`,
603 * or the dataSource object corresponding to the list item that was selected.
604 * @param {number} index The index in `dataSource` of the list item selected, or `-1` if enter is pressed in the
605 * `TextField`.
606 */
607 onNewRequest: _propTypes2.default.func,
608 /**
609 * Callback function that is fired when the user updates the `TextField`.
610 *
611 * @param {string} searchText The auto-complete's `searchText` value.
612 * @param {array} dataSource The auto-complete's `dataSource` array.
613 * @param {object} params Additional information linked the update.
614 */
615 onUpdateInput: _propTypes2.default.func,
616 /**
617 * Auto complete menu is open if true.
618 */
619 open: _propTypes2.default.bool,
620 /**
621 * If true, the list item is showed when a focus event triggers.
622 */
623 openOnFocus: _propTypes2.default.bool,
624 /**
625 * Props to be passed to popover.
626 */
627 popoverProps: _propTypes2.default.object,
628 /**
629 * Text being input to auto complete.
630 */
631 searchText: _propTypes2.default.string,
632 /**
633 * Override the inline-styles of the root element.
634 */
635 style: _propTypes2.default.object,
636 /**
637 * Origin for location of target.
638 */
639 targetOrigin: _propTypes4.default.origin,
640 /**
641 * Override the inline-styles of AutoComplete's TextField element.
642 */
643 textFieldStyle: _propTypes2.default.object
644} : {};
645
646
647AutoComplete.levenshteinDistance = function (searchText, key) {
648 var current = [];
649 var prev = void 0;
650 var value = void 0;
651
652 for (var i = 0; i <= key.length; i++) {
653 for (var j = 0; j <= searchText.length; j++) {
654 if (i && j) {
655 if (searchText.charAt(j - 1) === key.charAt(i - 1)) value = prev;else value = Math.min(current[j], current[j - 1], prev) + 1;
656 } else {
657 value = i + j;
658 }
659 prev = current[j];
660 current[j] = value;
661 }
662 }
663 return current.pop();
664};
665
666AutoComplete.noFilter = function () {
667 return true;
668};
669
670AutoComplete.defaultFilter = AutoComplete.caseSensitiveFilter = function (searchText, key) {
671 return searchText !== '' && key.indexOf(searchText) !== -1;
672};
673
674AutoComplete.caseInsensitiveFilter = function (searchText, key) {
675 return key.toLowerCase().indexOf(searchText.toLowerCase()) !== -1;
676};
677
678AutoComplete.levenshteinDistanceFilter = function (distanceLessThan) {
679 if (distanceLessThan === undefined) {
680 return AutoComplete.levenshteinDistance;
681 } else if (typeof distanceLessThan !== 'number') {
682 throw 'Error: AutoComplete.levenshteinDistanceFilter is a filter generator, not a filter!';
683 }
684
685 return function (s, k) {
686 return AutoComplete.levenshteinDistance(s, k) < distanceLessThan;
687 };
688};
689
690AutoComplete.fuzzyFilter = function (searchText, key) {
691 var compareString = key.toLowerCase();
692 searchText = searchText.toLowerCase();
693
694 var searchTextIndex = 0;
695 for (var index = 0; index < key.length; index++) {
696 if (compareString[index] === searchText[searchTextIndex]) {
697 searchTextIndex += 1;
698 }
699 }
700
701 return searchTextIndex === searchText.length;
702};
703
704AutoComplete.Item = _MenuItem2.default;
705AutoComplete.Divider = _Divider2.default;
706
707exports.default = AutoComplete;
\No newline at end of file