expensify-common 1.0.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +15 -6
  2. package/dist/API.d.ts +11 -0
  3. package/{lib/API.jsx → dist/API.js} +230 -165
  4. package/dist/APIDeferred.d.ts +7 -0
  5. package/{lib/APIDeferred.jsx → dist/APIDeferred.js} +35 -53
  6. package/dist/BrowserDetect.d.ts +19 -0
  7. package/dist/BrowserDetect.js +107 -0
  8. package/dist/CONST.d.ts +813 -0
  9. package/{lib/CONST.jsx → dist/CONST.js} +245 -167
  10. package/dist/Cookie.d.ts +68 -0
  11. package/{lib/Cookie.jsx → dist/Cookie.js} +23 -36
  12. package/dist/CredentialsWrapper.d.ts +32 -0
  13. package/dist/CredentialsWrapper.js +52 -0
  14. package/dist/Device.d.ts +8 -0
  15. package/dist/Device.js +15 -0
  16. package/dist/ExpenseRule.d.ts +39 -0
  17. package/{lib/ExpenseRule.jsx → dist/ExpenseRule.js} +12 -14
  18. package/dist/ExpensiMark.d.ts +142 -0
  19. package/dist/ExpensiMark.js +1026 -0
  20. package/dist/Func.d.ts +40 -0
  21. package/{lib/Func.jsx → dist/Func.js} +19 -25
  22. package/dist/Log.d.ts +3 -0
  23. package/dist/Log.js +41 -0
  24. package/dist/Logger.d.ts +77 -0
  25. package/dist/Logger.js +126 -0
  26. package/dist/Network.d.ts +6 -0
  27. package/{lib/Network.jsx → dist/Network.js} +48 -45
  28. package/dist/Num.d.ts +95 -0
  29. package/{lib/Num.jsx → dist/Num.js} +20 -40
  30. package/dist/PageEvent.d.ts +25 -0
  31. package/dist/PageEvent.js +28 -0
  32. package/dist/PubSub.d.ts +2 -0
  33. package/{lib/PubSub.jsx → dist/PubSub.js} +27 -39
  34. package/dist/ReportHistoryStore.d.ts +64 -0
  35. package/dist/ReportHistoryStore.js +261 -0
  36. package/dist/Templates.d.ts +2 -0
  37. package/{lib/Templates.jsx → dist/Templates.js} +33 -48
  38. package/dist/Url.d.ts +22 -0
  39. package/dist/Url.js +30 -0
  40. package/dist/components/CopyText.d.ts +45 -0
  41. package/{lib/components/CopyText.jsx → dist/components/CopyText.js} +16 -23
  42. package/dist/components/StepProgressBar.d.ts +22 -0
  43. package/dist/components/StepProgressBar.js +68 -0
  44. package/dist/components/form/element/combobox.d.ts +237 -0
  45. package/{lib → dist}/components/form/element/combobox.js +361 -516
  46. package/dist/components/form/element/dropdown.d.ts +35 -0
  47. package/dist/components/form/element/dropdown.js +66 -0
  48. package/dist/components/form/element/dropdownItem.d.ts +55 -0
  49. package/dist/components/form/element/dropdownItem.js +118 -0
  50. package/dist/components/form/element/onOffSwitch.d.ts +94 -0
  51. package/dist/components/form/element/onOffSwitch.js +195 -0
  52. package/dist/components/form/element/switch.d.ts +58 -0
  53. package/{lib → dist}/components/form/element/switch.js +29 -66
  54. package/dist/fastMerge.d.ts +9 -0
  55. package/dist/fastMerge.js +77 -0
  56. package/dist/index.d.ts +19 -0
  57. package/dist/index.js +71 -0
  58. package/dist/jquery.expensifyIframify.d.ts +10 -0
  59. package/{lib → dist}/jquery.expensifyIframify.js +52 -93
  60. package/dist/mixins/PubSub.d.ts +20 -0
  61. package/{lib/mixins/PubSub.jsx → dist/mixins/PubSub.js} +12 -11
  62. package/dist/mixins/extraClasses.d.ts +8 -0
  63. package/{lib → dist}/mixins/extraClasses.js +8 -12
  64. package/dist/mixins/validationClasses.d.ts +12 -0
  65. package/dist/mixins/validationClasses.js +58 -0
  66. package/dist/str.d.ts +613 -0
  67. package/{lib → dist}/str.js +176 -160
  68. package/dist/tlds.d.ts +2 -0
  69. package/dist/tlds.js +4 -0
  70. package/dist/utils.d.ts +5 -0
  71. package/dist/utils.js +13 -0
  72. package/package.json +47 -18
  73. package/.editorconfig +0 -34
  74. package/.eslintrc.js +0 -11
  75. package/.github/CODEOWNERS +0 -2
  76. package/.github/CONTRIBUTING.md +0 -163
  77. package/.github/ISSUE_TEMPLATE.md +0 -3
  78. package/.github/PULL_REQUEST_TEMPLATE.md +0 -14
  79. package/.github/workflows/cla.yml +0 -39
  80. package/.github/workflows/lint.yml +0 -29
  81. package/.github/workflows/test.yml +0 -29
  82. package/CLA.md +0 -20
  83. package/Gruntfile.js +0 -13
  84. package/__tests__/ExpensiMark-test.js +0 -340
  85. package/__tests__/Logger-test.js +0 -55
  86. package/__tests__/Str-test.js +0 -53
  87. package/babel.config.js +0 -12
  88. package/grunt/configloader.js +0 -17
  89. package/grunt/configs/chokidar.js +0 -23
  90. package/grunt/configs/eslint.js +0 -15
  91. package/grunt/task/watch.js +0 -3
  92. package/grunt/taskloader.js +0 -25
  93. package/lib/BrowserDetect.jsx +0 -91
  94. package/lib/ExpensiMark.js +0 -253
  95. package/lib/Log.jsx +0 -36
  96. package/lib/Logger.jsx +0 -154
  97. package/lib/PageEvent.jsx +0 -23
  98. package/lib/ReportHistoryStore.jsx +0 -194
  99. package/lib/components/StepProgressBar.js +0 -49
  100. package/lib/components/form/element/dropdown.js +0 -90
  101. package/lib/components/form/element/dropdownItem.js +0 -178
  102. package/lib/components/form/element/onOffSwitch.jsx +0 -229
  103. package/lib/mixins/validationClasses.js +0 -23
  104. package/lib/tlds.jsx +0 -3
@@ -1,149 +1,100 @@
1
- import React from 'react';
2
- import ReactDOM from 'react-dom';
3
- import PropTypes from 'prop-types';
4
- import cn from 'classnames';
5
- import _ from 'underscore';
6
- import get from 'lodash/get';
7
- import has from 'lodash/has';
8
- import Str from '../../../str';
9
- import DropDown from './dropdown';
10
-
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const react_1 = __importDefault(require("react"));
7
+ const react_dom_1 = __importDefault(require("react-dom"));
8
+ const prop_types_1 = __importDefault(require("prop-types"));
9
+ const classnames_1 = __importDefault(require("classnames"));
10
+ const underscore_1 = __importDefault(require("underscore"));
11
+ const get_1 = __importDefault(require("lodash/get"));
12
+ const has_1 = __importDefault(require("lodash/has"));
13
+ const uniqBy_1 = __importDefault(require("lodash/uniqBy"));
14
+ const str_1 = __importDefault(require("../../../str"));
15
+ const dropdown_1 = __importDefault(require("./dropdown"));
11
16
  const propTypes = {
12
17
  // These are the elements to show in the dropdown
13
- options: PropTypes.arrayOf(
14
- PropTypes.shape({
15
- // The value of the option, should be unique
16
- value: PropTypes.oneOfType([
17
- PropTypes.string,
18
- PropTypes.number
19
- ]),
20
-
21
- // The human readable text of the option
22
- text: PropTypes.string,
23
-
24
- // If we need more than text to style more
25
- children: PropTypes.element,
26
-
27
- // Whether or not this tag has an error (like out of policy)
28
- hasError: PropTypes.bool,
29
-
30
- // If this option is disabled, then it can't be selected and needs styled differently
31
- disabled: PropTypes.bool,
32
-
33
- // An id to use when checking if the options changed. It's used to avoid triggering changes when passing
34
- // children to it, if the id didn't change we assume no other properties changed either.
35
- id: PropTypes.any,
36
-
37
- // Whether or not the object should be displayed as a divider
38
- // Used to identify the divider in some UI elements that need a separation between listed options
39
- divider: PropTypes.bool,
40
-
41
- // Whether or not the option is represented more than once in the list of options and should be selected
42
- // Used in some UI elements that may display the same user details multiple times to identify which ones
43
- // are duplicates
44
- selected: PropTypes.bool
45
- })
46
- ).isRequired,
47
-
18
+ options: prop_types_1.default.arrayOf(prop_types_1.default.shape({
19
+ // The value of the option, should be unique
20
+ value: prop_types_1.default.oneOfType([prop_types_1.default.string, prop_types_1.default.number]),
21
+ // The human readable text of the option
22
+ text: prop_types_1.default.string,
23
+ // If we need more than text to style more
24
+ children: prop_types_1.default.element,
25
+ // Whether or not this tag has an error (like out of policy)
26
+ hasError: prop_types_1.default.bool,
27
+ // If this option is disabled, then it can't be selected and needs styled differently
28
+ disabled: prop_types_1.default.bool,
29
+ // An id to use when checking if the options changed. It's used to avoid triggering changes when passing
30
+ // children to it, if the id didn't change we assume no other properties changed either.
31
+ id: prop_types_1.default.oneOfType([prop_types_1.default.string, prop_types_1.default.number]),
32
+ // Whether or not the object should be displayed as a divider
33
+ // Used to identify the divider in some UI elements that need a separation between listed options
34
+ divider: prop_types_1.default.bool,
35
+ // Whether or not the option is represented more than once in the list of options and should be selected
36
+ // Used in some UI elements that may display the same user details multiple times to identify which ones
37
+ // are duplicates
38
+ selected: prop_types_1.default.bool,
39
+ })).isRequired,
48
40
  // A default value to have selected
49
- defaultValue: PropTypes.oneOfType([
50
- PropTypes.string,
51
- PropTypes.number
52
- ]),
53
-
41
+ defaultValue: prop_types_1.default.oneOfType([prop_types_1.default.string, prop_types_1.default.number]),
54
42
  // A default value to have selected - Use this one rather than defaultValue when the parent view might change the value passed.
55
- value: PropTypes.oneOfType([
56
- PropTypes.string,
57
- PropTypes.number
58
- ]),
59
-
43
+ value: prop_types_1.default.oneOfType([prop_types_1.default.string, prop_types_1.default.number]),
60
44
  // A list of options to show as selected too. This is used when we are using this component to implement a multi
61
45
  // select, where another component holds all the selected options and this component only has to show the ones
62
46
  // that are already selected with the check mark.
63
- alreadySelectedOptions: PropTypes.arrayOf(
64
- PropTypes.shape({
65
- value: PropTypes.oneOfType([
66
- PropTypes.string,
67
- PropTypes.number
68
- ]),
69
- text: PropTypes.string,
70
- })
71
- ),
72
-
47
+ alreadySelectedOptions: prop_types_1.default.arrayOf(prop_types_1.default.shape({
48
+ value: prop_types_1.default.oneOfType([prop_types_1.default.string, prop_types_1.default.number]),
49
+ text: prop_types_1.default.string,
50
+ })),
73
51
  // A callback that is fired when the value of the selector has changed
74
- onChange: PropTypes.func,
75
-
52
+ onChange: prop_types_1.default.func,
76
53
  // A callback that is fired when the value is cleared out by deleting text in the input
77
- onClear: PropTypes.func,
78
-
54
+ onClear: prop_types_1.default.func,
79
55
  // Callback fired when the dropdown is open/closed. It will pass a boolean param with true if it's open
80
- onDropdownStateChange: PropTypes.func,
81
-
56
+ onDropdownStateChange: prop_types_1.default.func,
82
57
  // Maximum amount of items to show
83
- maxItemsToShow: PropTypes.number,
84
-
58
+ maxItemsToShow: prop_types_1.default.number,
85
59
  // Maximum amount of search results
86
- maxSearchResults: PropTypes.number,
87
-
60
+ maxSearchResults: prop_types_1.default.number,
88
61
  // By default we show the selected option's value
89
62
  // You can overwrite it to display a different property (like 'text')
90
- propertyToDisplay: PropTypes.oneOf(['text', 'value']),
91
-
63
+ propertyToDisplay: prop_types_1.default.oneOf(['text', 'value']),
92
64
  // Whether or not the combobox should be open when we initialize it
93
- openOnInit: PropTypes.bool,
94
-
65
+ openOnInit: prop_types_1.default.bool,
95
66
  // A property to allow the combobox set to any manual entry the user chooses
96
- allowAnyValue: PropTypes.bool,
97
-
67
+ allowAnyValue: prop_types_1.default.bool,
98
68
  // What text should we show when the text entered doesn't match any of the results. It's an underscore template
99
69
  // that will be passed `value` as the current entered text.
100
- noResultsText: PropTypes.string,
101
-
70
+ noResultsText: prop_types_1.default.string,
102
71
  // A property to determine if the selector is user editable
103
- isReadOnly: PropTypes.bool,
104
-
72
+ isReadOnly: prop_types_1.default.bool,
105
73
  // Text to use for the placeholder, defaults to "Type to search..."
106
- placeholder: PropTypes.string,
107
-
74
+ placeholder: prop_types_1.default.string,
108
75
  // An array of extra classes to put on the combobox
109
- extraClasses: PropTypes.oneOfType([
110
- PropTypes.string,
111
- PropTypes.array,
112
- PropTypes.object,
113
- ]),
114
-
76
+ extraClasses: prop_types_1.default.oneOfType([prop_types_1.default.string, prop_types_1.default.array, prop_types_1.default.object]),
115
77
  // This makes this component hidden till someone manually calls openDropdown
116
- hideUntilManuallyOpened: PropTypes.bool,
117
-
78
+ hideUntilManuallyOpened: prop_types_1.default.bool,
118
79
  // Do we want to always show the selected options on top?
119
- alwaysShowSelectedOnTop: PropTypes.bool,
120
-
80
+ alwaysShowSelectedOnTop: prop_types_1.default.bool,
121
81
  // Should this combobox have an error state on init?
122
- hasInitialError: PropTypes.bool,
123
-
82
+ hasInitialError: prop_types_1.default.bool,
124
83
  // Bootstrap 4 compatibility flag
125
- bs4: PropTypes.bool,
126
-
84
+ bs4: prop_types_1.default.bool,
127
85
  // Always include the disabled options in search results
128
- showDisabledOptionsInResults: PropTypes.bool,
129
-
86
+ showDisabledOptionsInResults: prop_types_1.default.bool,
130
87
  // Do we want to autoscroll to the top of the selector on item selection
131
- autoScrollToTop: PropTypes.bool,
132
-
88
+ autoScrollToTop: prop_types_1.default.bool,
133
89
  // Classes to apply to the dropdown in the combobox
134
- dropDownClasses: PropTypes.oneOfType([
135
- PropTypes.string,
136
- PropTypes.array,
137
- PropTypes.object,
138
- ])
90
+ dropDownClasses: prop_types_1.default.oneOfType([prop_types_1.default.string, prop_types_1.default.array, prop_types_1.default.object]),
139
91
  };
140
-
141
92
  const defaultProps = {
142
93
  defaultValue: '',
143
94
  value: '',
144
- onChange: () => {},
145
- onClear: () => {},
146
- onDropdownStateChange: () => {},
95
+ onChange: () => { },
96
+ onClear: () => { },
97
+ onDropdownStateChange: () => { },
147
98
  maxItemsToShow: 500,
148
99
  maxSearchResults: 200,
149
100
  propertyToDisplay: 'value',
@@ -162,15 +113,13 @@ const defaultProps = {
162
113
  autoScrollToTop: true,
163
114
  dropDownClasses: [],
164
115
  };
165
-
166
116
  /**
167
117
  * A combobox useful for searching for values in a large list
168
118
  */
169
119
  // eslint-disable-next-line react/no-unsafe
170
- class Combobox extends React.Component {
120
+ class Combobox extends react_1.default.Component {
171
121
  constructor(props) {
172
122
  super(props);
173
-
174
123
  // Bind to our private methods
175
124
  this.getStartState = this.getStartState.bind(this);
176
125
  this.getTruncatedOptions = this.getTruncatedOptions.bind(this);
@@ -194,64 +143,214 @@ class Combobox extends React.Component {
194
143
  this.handleKeyDown = this.handleKeyDown.bind(this);
195
144
  this.performSearch = this.performSearch.bind(this);
196
145
  this.handleClick = this.handleClick.bind(this);
197
-
198
146
  this.state = this.getStartState();
199
147
  this.scrollPosition = 0;
200
148
  }
201
-
202
149
  componentDidMount() {
203
- if (this.props.openOnInit) {
204
- // Our dropdown will be open, so we need to listen for our click away events
205
- // and put focus on the input
206
- _.defer(this.resetClickAwayHandler);
207
- $(this.value).focus().select();
150
+ if (!this.props.openOnInit) {
151
+ return;
208
152
  }
153
+ // Our dropdown will be open, so we need to listen for our click away events
154
+ // and put focus on the input
155
+ underscore_1.default.defer(this.resetClickAwayHandler);
156
+ $(this.value).focus().select();
209
157
  }
210
-
158
+ // eslint-disable-next-line react/no-unsafe
211
159
  UNSAFE_componentWillReceiveProps(nextProps) {
212
- if (!_.isUndefined(nextProps.value) && !_.isEmpty(nextProps.value) && !_.isEqual(nextProps.value, this.state.currentValue)) {
213
- this.setValue(nextProps.value);
160
+ if (underscore_1.default.isUndefined(nextProps.value) || underscore_1.default.isEmpty(nextProps.value) || underscore_1.default.isEqual(nextProps.value, this.state.currentValue)) {
161
+ return;
214
162
  }
215
-
216
- if (!_.isUndefined(nextProps.options)) {
163
+ this.setValue(nextProps.value);
164
+ if (!underscore_1.default.isUndefined(nextProps.options)) {
217
165
  // If the options have an id property, we use that to compare them and determine if they changed, if not
218
166
  // we'll use the whole options array.
219
- if (has(nextProps.options, '0.id')) {
220
- if (!_.isEqual(_.pluck(nextProps.options, 'id'), _.pluck(this.props.options, 'id')) || !_.isEqual(_.pluck(nextProps.alreadySelectedOptions, 'id'), _.pluck(this.props.alreadySelectedOptions, 'id'))) {
167
+ if ((0, has_1.default)(nextProps.options, '0.id')) {
168
+ if (!underscore_1.default.isEqual(underscore_1.default.pluck(nextProps.options, 'id'), underscore_1.default.pluck(this.props.options, 'id')) ||
169
+ !underscore_1.default.isEqual(underscore_1.default.pluck(nextProps.alreadySelectedOptions, 'id'), underscore_1.default.pluck(this.props.alreadySelectedOptions, 'id'))) {
221
170
  this.reset(false, nextProps.options, nextProps.alreadySelectedOptions);
222
171
  }
223
- } else if (!_.isEqual(nextProps.options, this.props.options) || !_.isEqual(nextProps.alreadySelectedOptions, this.props.alreadySelectedOptions)) {
172
+ }
173
+ else if (!underscore_1.default.isEqual(nextProps.options, this.props.options) || !underscore_1.default.isEqual(nextProps.alreadySelectedOptions, this.props.alreadySelectedOptions)) {
224
174
  this.reset(false, nextProps.options, nextProps.alreadySelectedOptions);
225
175
  }
226
176
  }
227
-
228
- if (!_.isUndefined(nextProps.openOnInit) && !_.isEqual(nextProps.openOnInit, this.props.openOnInit)) {
177
+ if (!underscore_1.default.isUndefined(nextProps.openOnInit) && !underscore_1.default.isEqual(nextProps.openOnInit, this.props.openOnInit)) {
229
178
  this.setState({
230
- isDropdownOpen: nextProps.openOnInit
179
+ isDropdownOpen: nextProps.openOnInit,
231
180
  });
232
181
  }
233
-
234
- if (!_.isUndefined(nextProps.isReadOnly) && !_.isEqual(nextProps.isReadOnly, this.props.isReadOnly)) {
182
+ if (!underscore_1.default.isUndefined(nextProps.isReadOnly) && !underscore_1.default.isEqual(nextProps.isReadOnly, this.props.isReadOnly)) {
235
183
  this.setState({
236
- isDisabled: nextProps.isReadOnly
184
+ isDisabled: nextProps.isReadOnly,
237
185
  });
238
186
  }
239
187
  }
240
-
241
188
  componentDidUpdate() {
242
- const dropDown = ReactDOM.findDOMNode(this.dropDown);
243
-
244
189
  // Set scroll position to the last known location when applicable
245
- if (dropDown && !this.props.autoScrollToTop) {
246
- dropDown.scrollTop = this.scrollPosition;
190
+ if (!this.dropDown || this.props.autoScrollToTop) {
191
+ return;
247
192
  }
193
+ this.dropDown.scrollTop = this.scrollPosition;
248
194
  }
249
-
250
195
  componentWillUnmount() {
251
196
  // Don't ever let this handler linger after unmounting
252
197
  $('body').off('click.clickaway', this.handleClickAway);
253
198
  }
254
-
199
+ /**
200
+ * Handle state changes for when a user clicks on an item in the dropdown
201
+ *
202
+ * @param {String|Number} selectedValue
203
+ */
204
+ handleClick(selectedValue) {
205
+ this.deselectCurrentOption();
206
+ // Select the new item, set our new indexes, close the dropdown
207
+ // Unselect all other options
208
+ let newSelectedIndex = (0, underscore_1.default)(this.options).findIndex({ value: selectedValue });
209
+ let currentlySelectedOption = (0, underscore_1.default)(this.options).findWhere({ value: selectedValue });
210
+ // If allowAnyValue is true and currentValue is absent then set it manually to what the user has entered.
211
+ if (newSelectedIndex === -1 && this.props.allowAnyValue && selectedValue) {
212
+ newSelectedIndex = 0;
213
+ currentlySelectedOption = {
214
+ text: selectedValue,
215
+ value: selectedValue,
216
+ };
217
+ }
218
+ // Get the scroll position of the currently selected value
219
+ this.scrollPosition = this.dropDown.scrollTop;
220
+ const stateUpdateCallback = () => {
221
+ this.resetClickAwayHandler();
222
+ // Fire our onChange callback
223
+ this.initialValue = selectedValue;
224
+ this.props.onChange(selectedValue);
225
+ };
226
+ this.setState({
227
+ options: this.getTruncatedOptions(selectedValue),
228
+ selectedIndex: newSelectedIndex,
229
+ focusedIndex: newSelectedIndex,
230
+ currentValue: selectedValue,
231
+ currentText: (0, get_1.default)(currentlySelectedOption, 'text', ''),
232
+ isDropdownOpen: false,
233
+ hasError: (0, get_1.default)(currentlySelectedOption, 'hasError', false),
234
+ }, stateUpdateCallback);
235
+ }
236
+ /**
237
+ * Fired when we "click away" with the dropdown open
238
+ *
239
+ * @param {SyntheticEvent} e
240
+ */
241
+ handleClickAway(e) {
242
+ // Ignore the click away event if something within our combobox was clicked
243
+ // BIG HACK! I had to add the check for the support password click because it was causing a near
244
+ // unreproducible bug: https://github.com/Expensify/Expensify/issues/76745 that only I could reproduce.
245
+ // It is 100% unexplainable, but works for me
246
+ if ($(e.target).attr('id') === 'supportPassword' ||
247
+ ($(e.target).parents('.expensify-input-group').length && $(e.target).parents('.expensify-input-group')[0] === react_dom_1.default.findDOMNode(this)) // eslint-disable-line react/no-find-dom-node
248
+ ) {
249
+ return;
250
+ }
251
+ // If the dropdown is open then we don't really care and can do an early return
252
+ if (!this.state.isDropdownOpen) {
253
+ return;
254
+ }
255
+ this.reset(true);
256
+ // Trigger a change so that inline editor knows to cancel itself
257
+ this.props.onChange(this.initialValue || this.props.value || this.props.defaultValue);
258
+ }
259
+ /**
260
+ * This is triggered whenever there is a key up event in the text input
261
+ *
262
+ * @param {SyntheticEvent} e
263
+ */
264
+ handleKeyDown(e) {
265
+ let oldFocusedIndex;
266
+ let newFocusedIndex;
267
+ let currentValue;
268
+ let currentText;
269
+ const updateStateDownKey = (state) => ({
270
+ focusedIndex: newFocusedIndex,
271
+ options: state.options,
272
+ isDropdownOpen: true,
273
+ });
274
+ const updateStateUpKey = (state) => ({
275
+ focusedIndex: newFocusedIndex,
276
+ options: state.options,
277
+ isDropdownOpen: true,
278
+ });
279
+ const updateStateEnterKey = (state) => ({
280
+ options: this.getTruncatedOptions(currentValue),
281
+ selectedIndex: state.focusedIndex,
282
+ currentValue,
283
+ currentText,
284
+ isDropdownOpen: false,
285
+ });
286
+ const resetStateEnterKey = () => {
287
+ this.resetClickAwayHandler();
288
+ // Fire our onChange callback
289
+ this.props.onChange(currentValue);
290
+ this.initialValue = currentValue;
291
+ };
292
+ // Handle the arrow keys
293
+ switch (e.which) {
294
+ case 40:
295
+ // Down - move focused selection down
296
+ oldFocusedIndex = this.state.focusedIndex;
297
+ newFocusedIndex = oldFocusedIndex + 1;
298
+ // Wrap around to the top of the list
299
+ if (newFocusedIndex > this.state.options.length - 1) {
300
+ newFocusedIndex = 0;
301
+ }
302
+ this.switchFocusedIndex(oldFocusedIndex, newFocusedIndex);
303
+ this.setState(updateStateDownKey, this.resetClickAwayHandler);
304
+ this.stopEvent(e);
305
+ break;
306
+ case 38:
307
+ // Up - move focused selection up
308
+ oldFocusedIndex = this.state.focusedIndex;
309
+ newFocusedIndex = oldFocusedIndex - 1;
310
+ // Wrap around to the bottom of the list
311
+ if (newFocusedIndex < 0) {
312
+ newFocusedIndex = this.state.options.length - 1;
313
+ }
314
+ this.switchFocusedIndex(oldFocusedIndex, newFocusedIndex);
315
+ this.setState(updateStateUpKey, this.resetClickAwayHandler);
316
+ this.stopEvent(e);
317
+ break;
318
+ case 13: {
319
+ // Enter - set selection
320
+ if (!this.state.isDropdownOpen) {
321
+ return;
322
+ }
323
+ // Don't select disabled things
324
+ if (this.state.options[this.state.focusedIndex].disabled) {
325
+ break;
326
+ }
327
+ this.deselectCurrentOption();
328
+ currentValue = this.state.options[this.state.focusedIndex].value;
329
+ currentText = this.state.options[this.state.focusedIndex].text;
330
+ // if allowAnyValue is true and currentValue is absent then set it manually to what the user has entered
331
+ if (!currentValue && this.props.allowAnyValue) {
332
+ currentValue = e.target.value;
333
+ currentText = currentValue;
334
+ }
335
+ this.setState(updateStateEnterKey, resetStateEnterKey);
336
+ this.stopEvent(e);
337
+ break;
338
+ }
339
+ case 9:
340
+ case 27:
341
+ // Tab & Escape - clear selection
342
+ this.reset(true);
343
+ // Fire our onChange callback
344
+ this.props.onChange(this.initialValue || this.props.value || this.props.defaultValue);
345
+ // Stop the event from propagating if the escape key is pressed
346
+ if (e.which === 27) {
347
+ this.stopEvent(e);
348
+ }
349
+ break;
350
+ default:
351
+ break;
352
+ }
353
+ }
255
354
  /**
256
355
  * @param {Boolean} noDefaultValue Overrides the default value to have '' as the currentValue
257
356
  * @param {Object[]} [newOptions] when resetting this component, we can specify new options to use
@@ -261,20 +360,16 @@ class Combobox extends React.Component {
261
360
  */
262
361
  getStartState(noDefaultValue, newOptions, newAlreadySelectedOptions) {
263
362
  this.options = newOptions || this.props.options;
264
-
265
363
  const value = this.props.value || this.props.defaultValue || '';
266
364
  const currentValue = this.initialValue || value;
267
-
365
+ const matchingOptionWithoutSMSDomain = (o) => (str_1.default.isString(o) ? str_1.default.removeSMSDomain(o.value) : o.value) === currentValue && !o.isFake;
268
366
  // We use removeSMSDomain here in case currentValue is a phone number
269
- let defaultSelectedOption = _(this.options).find(o => (Str.isString(o) ? Str.removeSMSDomain(o.value) : o.value) === currentValue
270
- && !o.isFake);
271
-
367
+ let defaultSelectedOption = (0, underscore_1.default)(this.options).find(matchingOptionWithoutSMSDomain);
272
368
  // If no default was found and initialText was present then we can use initialText values
273
369
  if (!defaultSelectedOption && this.initialText) {
274
370
  defaultSelectedOption = {};
275
371
  defaultSelectedOption.text = this.initialText;
276
372
  }
277
-
278
373
  return {
279
374
  isDropdownOpen: this.props.openOnInit,
280
375
  currentValue: noDefaultValue ? this.initialValue || '' : value,
@@ -283,10 +378,9 @@ class Combobox extends React.Component {
283
378
  focusedIndex: 0,
284
379
  selectedIndex: null,
285
380
  isDisabled: this.props.isReadOnly,
286
- hasError: (defaultSelectedOption && defaultSelectedOption.hasError) || this.props.hasInitialError
381
+ hasError: (defaultSelectedOption && defaultSelectedOption.hasError) || this.props.hasInitialError,
287
382
  };
288
383
  }
289
-
290
384
  /**
291
385
  * Returns a set of our full options according to the maxItemsToShow
292
386
  *
@@ -296,34 +390,22 @@ class Combobox extends React.Component {
296
390
  */
297
391
  getTruncatedOptions(currentValue, newAlreadySelectedOptions) {
298
392
  const alreadySelected = newAlreadySelectedOptions || this.props.alreadySelectedOptions;
299
-
300
393
  // Get the divider index if we have one
301
- const dividerIndex = _.findIndex(this.options, option => option.divider);
302
-
394
+ const findDivider = (option) => option.divider;
395
+ const dividerIndex = underscore_1.default.findIndex(this.options, findDivider);
303
396
  // Split into two arrays everything before and after the divider (if the divider does not exist then we'll return a single array)
304
397
  const splitOptions = dividerIndex ? [this.options.slice(0, dividerIndex + 1), this.options.slice(dividerIndex + 1)] : [this.options];
305
-
398
+ const formatOption = (option) => (Object.assign({ focused: false, isSelected: option.selected && (underscore_1.default.isEqual(option.value, currentValue) || Boolean(underscore_1.default.findWhere(alreadySelected, { value: option.value }))) }, option));
399
+ const sortByOption = (o) => {
400
+ // Unselectable text-only entries (isFake: true) go to the bottom and selected entries go to the top only if alwaysShowSelectedOnTop was passed
401
+ if (o.showLast) {
402
+ return 2;
403
+ }
404
+ return o.isSelected && this.props.alwaysShowSelectedOnTop ? 0 : 1;
405
+ };
306
406
  // Take each array and format it, sort it, and move selected items to top (if applicable)
307
- const truncatedOptions = _.chain(splitOptions)
308
- .map(array => (
309
- _.chain(array)
310
- .map(option => ({
311
- focused: false,
312
- isSelected: option.selected && (_.isEqual(option.value, currentValue) || Boolean(_.findWhere(alreadySelected, {value: option.value}))),
313
- ...option
314
- }))
315
- .sortBy((o) => {
316
- // Unselectable text-only entries (isFake: true) go to the bottom and selected entries go to the top only if alwaysShowSelectedOnTop was passed
317
- if (o.showLast) {
318
- return 2;
319
- }
320
- return o.isSelected && this.props.alwaysShowSelectedOnTop ? 0 : 1;
321
- })
322
- .first(this.props.maxItemsToShow)
323
- .value()))
324
- .flatten()
325
- .value();
326
-
407
+ const formatOptions = (array) => underscore_1.default.chain(array).map(formatOption).sortBy(sortByOption).first(this.props.maxItemsToShow).value();
408
+ const truncatedOptions = underscore_1.default.chain(splitOptions).map(formatOptions).flatten().value();
327
409
  if (!truncatedOptions.length) {
328
410
  truncatedOptions.push({
329
411
  text: 'No items',
@@ -332,7 +414,8 @@ class Combobox extends React.Component {
332
414
  isFake: true,
333
415
  showLast: true,
334
416
  });
335
- } else if (this.options.length > this.props.maxItemsToShow) {
417
+ }
418
+ else if (this.options.length > this.props.maxItemsToShow) {
336
419
  truncatedOptions.push({
337
420
  text: `Viewing first ${this.props.maxItemsToShow} items. Search to see more.`,
338
421
  value: '',
@@ -341,10 +424,8 @@ class Combobox extends React.Component {
341
424
  showLast: true,
342
425
  });
343
426
  }
344
-
345
427
  return truncatedOptions;
346
428
  }
347
-
348
429
  /**
349
430
  * Returns the selected value(s)
350
431
  *
@@ -353,7 +434,6 @@ class Combobox extends React.Component {
353
434
  getValue() {
354
435
  return this.state.currentValue;
355
436
  }
356
-
357
437
  /**
358
438
  * Sets the current value
359
439
  *
@@ -362,71 +442,71 @@ class Combobox extends React.Component {
362
442
  setValue(val) {
363
443
  // We need to look in `this.options` for the matching option because `this.state.options` is a truncated list
364
444
  // and might not have every option
365
- const optionMatchingVal = _.findWhere(this.options, {value: val});
366
- const currentText = get(optionMatchingVal, 'text', '');
367
-
368
- this.initialValue = val;
369
- this.setState(state => ({
445
+ const optionMatchingVal = underscore_1.default.findWhere(this.options, { value: val });
446
+ const currentText = (0, get_1.default)(optionMatchingVal, 'text', '');
447
+ const deselectOption = (initialOption) => {
448
+ const option = initialOption;
449
+ const isSelected = underscore_1.default.isEqual(option.value, val);
450
+ option.isSelected = isSelected || Boolean(underscore_1.default.findWhere(this.props.alreadySelectedOptions, { value: option.value }));
451
+ return option;
452
+ };
453
+ const deselectOptions = (options) => (0, underscore_1.default)(options).map(deselectOption);
454
+ const setValueState = (state) => ({
370
455
  currentValue: val,
371
456
  currentText,
372
-
373
457
  // Deselect all other options but the one matching our value
374
- options: _(state.options).map((o) => {
375
- const option = o;
376
- const isSelected = _.isEqual(option.value, val);
377
- option.isSelected = isSelected
378
- || Boolean(_.findWhere(this.props.alreadySelectedOptions, {value: option.value}));
379
-
380
- return option;
381
- })
382
- }));
458
+ options: deselectOptions(state.options),
459
+ });
460
+ this.initialValue = val;
461
+ this.setState(setValueState);
383
462
  }
384
-
385
463
  /**
386
464
  * Hard sets the current text to what we want
387
465
  *
388
- * @param {String} currentText
466
+ * @param {String} val
389
467
  */
390
- setText(currentText) {
468
+ setText(val) {
469
+ // See if there is a value in the options that matches the text we want to set. If the option
470
+ // does exist, then use the text property of that option for the text to display. If the option
471
+ // does not exist, then just display whatever value was passed
472
+ const optionMatchingVal = underscore_1.default.findWhere(this.options, { value: val });
473
+ const currentText = (0, get_1.default)(optionMatchingVal, 'text', val);
391
474
  this.initialValue = currentText;
392
475
  this.initialText = currentText;
393
- this.setState({currentText});
476
+ this.setState({ currentText });
394
477
  }
395
-
396
478
  /**
397
479
  * Sets the disabled state of this component
398
480
  *
399
481
  * @param {Boolean} isDisabled
400
482
  */
401
483
  setDisabled(isDisabled) {
402
- this.setState({isDisabled});
484
+ this.setState({ isDisabled });
403
485
  }
404
-
405
486
  /**
406
487
  * Set an error for a specific tag
407
488
  */
408
489
  setError() {
409
490
  this.setState({
410
- hasError: true
491
+ hasError: true,
411
492
  });
412
493
  }
413
-
414
494
  /**
415
495
  * Deselects any current option if it exists
416
496
  */
417
497
  deselectCurrentOption() {
418
- if (this.state.selectedIndex !== null && this.state.options[this.state.selectedIndex]) {
419
- this.setState((state) => {
420
- const newOptions = [...state.options];
421
- newOptions[state.selectedIndex].isSelected = false;
422
-
423
- return {
424
- options: newOptions
425
- };
426
- });
498
+ if (this.state.selectedIndex === null || !this.state.options[this.state.selectedIndex]) {
499
+ return;
427
500
  }
501
+ const setValueState = (state) => {
502
+ const newOptions = [...state.options];
503
+ newOptions[state.selectedIndex].isSelected = false;
504
+ return {
505
+ options: newOptions,
506
+ };
507
+ };
508
+ this.setState(setValueState);
428
509
  }
429
-
430
510
  /**
431
511
  * Swaps the focused index from {oldFocusedIndex} to {newFocusedIndex}
432
512
  *
@@ -434,17 +514,16 @@ class Combobox extends React.Component {
434
514
  * @param {number} newFocusedIndex
435
515
  */
436
516
  switchFocusedIndex(oldFocusedIndex, newFocusedIndex) {
437
- this.setState((state) => {
517
+ const setFocusedState = (state) => {
438
518
  const newOptions = [...state.options];
439
519
  newOptions[oldFocusedIndex].focused = false;
440
520
  newOptions[newFocusedIndex].focused = true;
441
-
442
521
  return {
443
- options: newOptions
522
+ options: newOptions,
444
523
  };
445
- });
524
+ };
525
+ this.setState(setFocusedState);
446
526
  }
447
-
448
527
  /**
449
528
  * Returns the selector to it's original state
450
529
  *
@@ -457,52 +536,25 @@ class Combobox extends React.Component {
457
536
  this.options = newOptions;
458
537
  }
459
538
  const state = this.getStartState(noDefaultValue, this.options, newAlreadySelectedOptions);
460
- this.setState(state, () => this.props.onDropdownStateChange(Boolean(state.isDropdownOpen)));
539
+ const handleDropdownStateChange = () => {
540
+ this.props.onDropdownStateChange(Boolean(state.isDropdownOpen));
541
+ };
542
+ this.setState(state, handleDropdownStateChange);
461
543
  }
462
-
463
544
  /**
464
545
  * When the dropdown is closed, we reset the focused property of all of our options
465
546
  */
466
547
  resetFocusedElements() {
467
- this.setState(state => ({
468
- options: _(state.options).map((o) => {
469
- const option = o;
470
- option.focused = false;
471
- return option;
472
- })
473
- }));
474
- }
475
-
476
- /**
477
- * Fired when we "click away" with the dropdown open
478
- *
479
- * @param {SyntheticEvent} e
480
- */
481
- handleClickAway(e) {
482
- // Ignore the click away event if something within our combobox was clicked
483
- // BIG HACK! I had to add the check for the support password click because it was causing a near
484
- // unreproducible bug: https://github.com/Expensify/Expensify/issues/76745 that only I could reproduce.
485
- // It is 100% unexplainable, but works for me
486
- if ($(e.target).attr('id') === 'supportPassword'
487
- || (
488
- $(e.target).parents('.expensify-input-group').length
489
- && $(e.target).parents('.expensify-input-group')[0] === ReactDOM.findDOMNode(this)
490
- )
491
- ) {
492
- return;
493
- }
494
-
495
- // If the dropdown is open then we don't really care and can do an early return
496
- if (!this.state.isDropdownOpen) {
497
- return;
498
- }
499
-
500
- this.reset(true);
501
-
502
- // Trigger a change so that inline editor knows to cancel itself
503
- this.props.onChange(this.initialValue || this.props.value || this.props.defaultValue);
548
+ const resetFocusedProperty = (o) => {
549
+ const option = o;
550
+ option.focused = false;
551
+ return option;
552
+ };
553
+ const setValueState = (state) => ({
554
+ options: (0, underscore_1.default)(state.options).map(resetFocusedProperty),
555
+ });
556
+ this.setState(setValueState);
504
557
  }
505
-
506
558
  /**
507
559
  * Sets a clicklistener to close the dropdown if it is currently open
508
560
  */
@@ -510,31 +562,30 @@ class Combobox extends React.Component {
510
562
  $('body').off('click.clickaway', this.handleClickAway);
511
563
  if (this.state.isDropdownOpen) {
512
564
  $('body').on('click.clickaway', this.handleClickAway);
513
- } else {
565
+ }
566
+ else {
514
567
  this.resetFocusedElements();
515
568
  }
516
569
  }
517
-
518
570
  /**
519
571
  * Clear an error for a specific tag
520
572
  */
521
573
  clearError() {
522
574
  this.setState({
523
- hasError: false
575
+ hasError: false,
524
576
  });
525
577
  }
526
-
527
578
  /**
528
579
  * Opens or closes the dropdown
529
580
  */
530
581
  toggleDropdown() {
531
582
  if (this.state.isDropdownOpen) {
532
583
  this.closeDropdown();
533
- } else {
584
+ }
585
+ else {
534
586
  this.openDropdown();
535
587
  }
536
588
  }
537
-
538
589
  /**
539
590
  * Just open the dropdown (when the input gets focus)
540
591
  */
@@ -542,15 +593,16 @@ class Combobox extends React.Component {
542
593
  if (this.state.isDropdownOpen) {
543
594
  return;
544
595
  }
545
- this.setState({
546
- isDropdownOpen: true
547
- }, () => {
596
+ const setValueState = () => ({
597
+ isDropdownOpen: true,
598
+ });
599
+ const resetState = () => {
548
600
  this.props.onDropdownStateChange(true);
549
601
  this.resetClickAwayHandler();
550
602
  $(this.value).focus().select();
551
- });
603
+ };
604
+ this.setState(setValueState, resetState);
552
605
  }
553
-
554
606
  /**
555
607
  * Closes the dropdown and removes the click handler that calls this function after the dropdown is closed
556
608
  *
@@ -560,28 +612,26 @@ class Combobox extends React.Component {
560
612
  if (!this.state.isDropdownOpen) {
561
613
  return;
562
614
  }
563
-
564
615
  // If the dropdown is being closed from clicking on our input, then we actually don't close the dropdown. This
565
616
  // occurs because when first opening the dropdown when the input receives focus, it fires off the clickaway
566
617
  // event and I couldn't stop it. So this is the work around.
567
618
  if (e && ($(e.target)[0] === $(this.value)[0] || $(e.target).parent().hasClass('disabled'))) {
568
619
  return;
569
620
  }
570
-
571
- this.setState({
572
- isDropdownOpen: false
573
- }, () => {
621
+ const setValueState = () => ({
622
+ isDropdownOpen: false,
623
+ });
624
+ const resetState = () => {
574
625
  this.props.onDropdownStateChange(false);
575
626
  this.resetClickAwayHandler();
576
- });
577
-
627
+ };
628
+ this.setState(setValueState, resetState);
578
629
  // The value a user selects is set in state prior to this function running so we want to always treat this as if
579
630
  // it were just a blur event and reset the input to an empty value and then let onChange handle showing the proper value
580
631
  // This is because users who click the arrow used to be able to save incorrect values in the combobox: https://github.com/Expensify/Expensify/issues/75793#issuecomment-380260662
581
632
  this.reset(true);
582
633
  this.props.onChange(this.initialValue || this.props.value || this.props.defaultValue);
583
634
  }
584
-
585
635
  /**
586
636
  * If the user presses tab when the dropdown button is focused, then we close the dropdown
587
637
  * because they are trying to get to the next form components
@@ -589,11 +639,11 @@ class Combobox extends React.Component {
589
639
  * @param {SyntheticEvent} e
590
640
  */
591
641
  closeDropdownOnTabOut(e) {
592
- if (e.which === 9) {
593
- this.closeDropdown();
642
+ if (e.which !== 9) {
643
+ return;
594
644
  }
645
+ this.handleClick(this.state.currentValue);
595
646
  }
596
-
597
647
  /**
598
648
  * Stops events from doing anything outside of the current function (in theory).
599
649
  * This doesn't work between jquery and React events because React's event propagation is a separate system.
@@ -604,121 +654,12 @@ class Combobox extends React.Component {
604
654
  e.preventDefault();
605
655
  if (e.nativeEvent) {
606
656
  e.nativeEvent.stopImmediatePropagation();
607
- } else {
657
+ }
658
+ else {
608
659
  e.stopImmediatePropagation();
609
660
  }
610
661
  e.stopPropagation();
611
662
  }
612
-
613
- /**
614
- * This is triggered whenever there is a key up event in the text input
615
- *
616
- * @param {SyntheticEvent} e
617
- */
618
- handleKeyDown(e) {
619
- let oldFocusedIndex;
620
- let newFocusedIndex;
621
- let currentValue;
622
- let currentText;
623
-
624
- // Handle the arrow keys
625
- switch (e.which) {
626
- case 40:
627
- // Down - move focused selection down
628
- oldFocusedIndex = this.state.focusedIndex;
629
- newFocusedIndex = oldFocusedIndex + 1;
630
-
631
- // Wrap around to the top of the list
632
- if (newFocusedIndex > this.state.options.length - 1) {
633
- newFocusedIndex = 0;
634
- }
635
-
636
- this.switchFocusedIndex(oldFocusedIndex, newFocusedIndex);
637
- this.setState(state => ({
638
- focusedIndex: newFocusedIndex,
639
- options: state.options,
640
- isDropdownOpen: true
641
- }), this.resetClickAwayHandler);
642
- this.stopEvent(e);
643
- break;
644
-
645
- case 38:
646
- // Up - move focused selection up
647
- oldFocusedIndex = this.state.focusedIndex;
648
- newFocusedIndex = oldFocusedIndex - 1;
649
-
650
- // Wrap around to the bottom of the list
651
- if (newFocusedIndex < 0) {
652
- newFocusedIndex = this.state.options.length - 1;
653
- }
654
-
655
- this.switchFocusedIndex(oldFocusedIndex, newFocusedIndex);
656
- this.setState(state => ({
657
- focusedIndex: newFocusedIndex,
658
- options: state.options,
659
- isDropdownOpen: true
660
- }), this.resetClickAwayHandler);
661
- this.stopEvent(e);
662
- break;
663
-
664
- case 13: {
665
- // Enter - set selection
666
- if (!this.state.isDropdownOpen) {
667
- return;
668
- }
669
-
670
- // Don't select disabled things
671
- if (this.state.options[this.state.focusedIndex].disabled) {
672
- break;
673
- }
674
- this.deselectCurrentOption();
675
-
676
- currentValue = this.state.options[this.state.focusedIndex].value;
677
- currentText = this.state.options[this.state.focusedIndex].text;
678
-
679
- // if allowAnyValue is true and currentValue is absent then set it manually to what the user has entered
680
- if (!currentValue && this.props.allowAnyValue) {
681
- currentValue = e.target.value;
682
- currentText = currentValue;
683
- }
684
-
685
- this.setState(state => ({
686
- options: this.getTruncatedOptions(currentValue),
687
- selectedIndex: state.focusedIndex,
688
- currentValue,
689
- currentText,
690
- isDropdownOpen: false
691
- }), () => {
692
- this.resetClickAwayHandler();
693
-
694
- // Fire our onChange callback
695
- this.props.onChange(currentValue);
696
- this.initialValue = currentValue;
697
- });
698
-
699
- this.stopEvent(e);
700
- break;
701
- }
702
-
703
- case 9:
704
- case 27:
705
- // Tab & Escape - clear selection
706
- this.reset(true);
707
-
708
- // Fire our onChange callback
709
- this.props.onChange(this.initialValue || this.props.value || this.props.defaultValue);
710
-
711
- // Stop the event from propagating if the escape key is pressed
712
- if (e.which === 27) {
713
- this.stopEvent(e);
714
- }
715
- break;
716
-
717
- default:
718
- break;
719
- }
720
- }
721
-
722
663
  /**
723
664
  * This is triggered whenever there is a change event in the text input
724
665
  * (from any valid input that's not being handled from the keyUp event)
@@ -728,73 +669,56 @@ class Combobox extends React.Component {
728
669
  performSearch(e) {
729
670
  // If empty value, hide the dropdown, update the initial fields and show the original options.
730
671
  const value = e.target.value;
731
-
732
672
  if (value === '') {
733
673
  this.initialValue = '';
734
674
  this.initialText = '';
735
-
736
675
  // This will show all the original items while the input field stays empty. Also the dropdown will remain open.
737
676
  this.setState({
738
677
  currentValue: '',
739
678
  currentText: '',
740
679
  isDropdownOpen: true,
741
- options: this.getTruncatedOptions('')
680
+ options: this.getTruncatedOptions(''),
742
681
  }, this.resetClickAwayHandler);
743
-
744
682
  // Trigger the callback to clear out the value
745
683
  this.props.onClear();
746
684
  return;
747
685
  }
748
-
749
686
  // Search our original options. We want:
750
687
  // Exact matches first
751
688
  // beginning-of-string matches second
752
689
  // middle-of-string matches last
753
- const matchRegexes = [
754
- new RegExp(`^${Str.escapeForRegExp(value)}$`, 'i'),
755
- new RegExp(`^${Str.escapeForRegExp(value)}`, 'i'),
756
- new RegExp(Str.escapeForRegExp(value), 'i'),
757
- ];
690
+ const matchRegexes = [new RegExp(`^${str_1.default.escapeForRegExp(value)}$`, 'i'), new RegExp(`^${str_1.default.escapeForRegExp(value)}`, 'i'), new RegExp(str_1.default.escapeForRegExp(value), 'i')];
758
691
  let hasMoreResults = false;
759
692
  let matches = new Set();
760
- const searchOptions = _.uniq(this.options, 'value');
761
-
693
+ const searchOptions = (0, uniqBy_1.default)(this.options, 'value');
762
694
  for (let i = 0; i < matchRegexes.length; i++) {
763
695
  if (matches.size < this.props.maxSearchResults) {
764
696
  for (let j = 0; j < searchOptions.length; j++) {
765
697
  const option = searchOptions[j];
766
698
  const isDisabled = option.disabled;
767
- const isMatch = matchRegexes[i].test(option.text.toString().replace(new RegExp(/&nbsp;/g), ''));
768
-
699
+ const isMatch = matchRegexes[i].test(option.text.toString().replace(/&nbsp;/g, ''));
769
700
  // Don't include the disabled options that match the regex unless we specified we want them to show.
770
701
  // If we want them to show then add them whether they match the regex or not.
771
- if ((!isDisabled && isMatch)
772
- || (isDisabled && this.props.showDisabledOptionsInResults)) {
702
+ if ((!isDisabled && isMatch) || (isDisabled && this.props.showDisabledOptionsInResults && isMatch)) {
773
703
  matches.add(option);
774
704
  }
775
-
776
705
  if (matches.size === this.props.maxSearchResults) {
777
706
  hasMoreResults = true;
778
707
  break;
779
708
  }
780
709
  }
781
- } else {
710
+ }
711
+ else {
782
712
  hasMoreResults = true;
783
713
  break;
784
714
  }
785
715
  }
786
716
  matches = Array.from(matches);
787
-
788
- const options = _(matches).map(option => ({
789
- focused: false,
790
- isSelected: _.isEqual(option.value, value) || Boolean(_.findWhere(this.props.alreadySelectedOptions, {value: option.value})),
791
- ...option
792
- }));
793
-
717
+ const formatOption = (option) => (Object.assign({ focused: false, isSelected: underscore_1.default.isEqual(option.value ? option.value.toUpperCase : '', value.toUpperCase()) || Boolean(underscore_1.default.findWhere(this.props.alreadySelectedOptions, { value: option.value })) }, option));
718
+ const options = (0, underscore_1.default)(matches).map(formatOption);
794
719
  // Focus the first option if there is one and show a message dependent on what options are present
795
720
  if (options.length) {
796
721
  options[0].focused = true;
797
-
798
722
  if (hasMoreResults) {
799
723
  options.push({
800
724
  text: `Viewing first ${options.length} results`,
@@ -804,67 +728,32 @@ class Combobox extends React.Component {
804
728
  showLast: true,
805
729
  });
806
730
  }
807
- } else {
731
+ }
732
+ else {
808
733
  options.push({
809
- text: this.props.allowAnyValue ? value : _.template(this.props.noResultsText)({value}),
734
+ text: this.props.allowAnyValue ? value : underscore_1.default.template(this.props.noResultsText)({ value }),
810
735
  value: this.props.allowAnyValue ? value : '',
811
736
  isSelectable: this.props.allowAnyValue,
812
737
  isFake: true,
813
738
  showLast: true,
814
739
  });
815
740
  }
816
-
817
- this.setState((state) => {
741
+ const setValueState = (state) => {
818
742
  let shouldShowDropdown = state.isDropdownOpen;
819
-
820
743
  // If we don't have an empty value, show the dropdown.
821
744
  if (value.trim() !== '' && !shouldShowDropdown) {
822
745
  shouldShowDropdown = true;
823
746
  }
824
-
825
747
  return {
826
748
  currentValue: value,
827
749
  currentText: value,
828
750
  isDropdownOpen: shouldShowDropdown,
829
751
  focusedIndex: 0,
830
- options
752
+ options,
831
753
  };
832
- }, this.resetClickAwayHandler);
833
- }
834
-
835
- /**
836
- * Handle state changes for when a user clicks on an item in the dropdown
837
- *
838
- * @param {String|Number} selectedValue
839
- */
840
- handleClick(selectedValue) {
841
- this.deselectCurrentOption();
842
-
843
- // Select the new item, set our new indexes, close the dropdown
844
- // Unselect all other options
845
- const newSelectedIndex = _(this.options).findIndex({value: selectedValue});
846
- const currentlySelectedOption = _(this.options).findWhere({value: selectedValue});
847
-
848
- // Get the scroll position of the currently selected value
849
- this.scrollPosition = ReactDOM.findDOMNode(this.dropDown).scrollTop;
850
-
851
- this.setState({
852
- options: this.getTruncatedOptions(selectedValue),
853
- selectedIndex: newSelectedIndex,
854
- focusedIndex: newSelectedIndex,
855
- currentValue: selectedValue,
856
- currentText: get(currentlySelectedOption, 'text', ''),
857
- isDropdownOpen: false,
858
- hasError: get(currentlySelectedOption, 'hasError', false)
859
- }, () => {
860
- this.resetClickAwayHandler();
861
-
862
- // Fire our onChange callback
863
- this.initialValue = selectedValue;
864
- this.props.onChange(selectedValue);
865
- });
754
+ };
755
+ this.setState(setValueState, this.resetClickAwayHandler);
866
756
  }
867
-
868
757
  render() {
869
758
  if (this.props.hideUntilManuallyOpened && !this.state.isDropdownOpen) {
870
759
  return null;
@@ -872,67 +761,23 @@ class Combobox extends React.Component {
872
761
  const inputGroupClasses = {
873
762
  'input-group': true,
874
763
  'expensify-input-group': true,
875
- open: this.state.isDropdownOpen
764
+ open: this.state.isDropdownOpen,
876
765
  };
877
766
  const inputGroupBtnClasses = {
878
767
  'input-group-btn': !this.props.bs4,
879
768
  'input-group-append': this.props.bs4,
880
- open: this.state.isDropdownOpen
769
+ open: this.state.isDropdownOpen,
881
770
  };
882
- const toggleBtnClasses = this.props.bs4
883
- ? 'dropdown-toggle dropdown-toggle-split btn btn-outline-primary'
884
- : 'dropdown-toggle btn btn-default';
885
-
886
- return (
887
- <div
888
- className={cn(inputGroupClasses, this.props.extraClasses)}
889
- onKeyDown={this.handleKeyDown}
890
- role="presentation"
891
- >
892
- <input
893
- ref={ref => this.value = ref}
894
- type="text"
895
- className={cn(['form-control', {error: this.state.hasError}])}
896
- disabled={this.state.isDisabled}
897
- aria-label="..."
898
- onChange={this.performSearch}
899
- onKeyDown={this.closeDropdownOnTabOut}
900
- value={this.props.propertyToDisplay === 'value'
901
- ? _.unescape(this.state.currentValue)
902
- : _.unescape(this.state.currentText.replace(new RegExp(/&nbsp;/g), ''))}
903
- onFocus={this.openDropdown}
904
- autoComplete="off"
905
- placeholder={this.props.placeholder}
906
- tabIndex="0"
907
- />
908
- <div className={cn(inputGroupBtnClasses)}>
909
- <button
910
- type="button"
911
- className={cn(toggleBtnClasses, {error: this.state.hasError})}
912
- disabled={this.state.isDisabled}
913
- onClick={this.toggleDropdown}
914
- onKeyDown={this.closeDropdownOnTabOut}
915
- tabIndex="-1"
916
- >
917
- {!this.props.bs4 && <span className="caret" />}
918
- <span className="sr-only">Toggle Dropdown</span>
919
- </button>
920
- {this.state.isDropdownOpen && (
921
- <DropDown
922
- ref={ref => this.dropDown = ref}
923
- options={this.state.options}
924
- bs4={this.props.bs4}
925
- extraClasses={cn({'expensify-dropdown': true, show: this.props.bs4 && this.state.isDropdownOpen}, this.props.dropDownClasses)}
926
- onChange={this.handleClick}
927
- />
928
- )}
929
- </div>
930
- </div>
931
- );
771
+ const toggleBtnClasses = this.props.bs4 ? 'dropdown-toggle dropdown-toggle-split btn btn-outline-primary' : 'dropdown-toggle btn btn-default';
772
+ return (react_1.default.createElement("div", { className: (0, classnames_1.default)(inputGroupClasses, this.props.extraClasses), onKeyDown: this.handleKeyDown, role: "presentation" },
773
+ react_1.default.createElement("input", { ref: (ref) => (this.value = ref), type: "text", className: (0, classnames_1.default)(['form-control', { error: this.state.hasError }]), disabled: this.state.isDisabled, "aria-label": "...", onChange: this.performSearch, onKeyDown: this.closeDropdownOnTabOut, value: this.props.propertyToDisplay === 'value' ? underscore_1.default.unescape(this.state.currentValue) : underscore_1.default.unescape(this.state.currentText.replace(/&nbsp;/g, '')), onFocus: this.openDropdown, autoComplete: "off", placeholder: this.props.placeholder, tabIndex: "0" }),
774
+ react_1.default.createElement("div", { className: (0, classnames_1.default)(inputGroupBtnClasses) },
775
+ react_1.default.createElement("button", { type: "button", className: (0, classnames_1.default)(toggleBtnClasses, { error: this.state.hasError }), disabled: this.state.isDisabled, onClick: this.toggleDropdown, onKeyDown: this.closeDropdownOnTabOut, tabIndex: "-1" },
776
+ !this.props.bs4 && react_1.default.createElement("span", { className: "caret" }),
777
+ react_1.default.createElement("span", { className: "sr-only" }, "Toggle Dropdown")),
778
+ this.state.isDropdownOpen && (react_1.default.createElement(dropdown_1.default, { ref: (ref) => (this.dropDown = ref), options: this.state.options, bs4: this.props.bs4, extraClasses: (0, classnames_1.default)({ 'expensify-dropdown': true, show: this.props.bs4 && this.state.isDropdownOpen }, this.props.dropDownClasses), onChange: this.handleClick })))));
932
779
  }
933
780
  }
934
-
935
781
  Combobox.propTypes = propTypes;
936
782
  Combobox.defaultProps = defaultProps;
937
-
938
- export default Combobox;
783
+ exports.default = Combobox;