foundation.ts 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084
  1. /* argus-disable unPkgSensitiveInfo */
  2. /* eslint-disable max-len */
  3. import BaseFoundation, { DefaultAdapter } from '../base/foundation';
  4. import { isNumber, isString, isEqual, omit } from 'lodash';
  5. import KeyCode, { ENTER_KEY } from '../utils/keyCode';
  6. import warning from '../utils/warning';
  7. import isNullOrUndefined from '../utils/isNullOrUndefined';
  8. import { BasicOptionProps } from './optionFoundation';
  9. import isEnterPress from '../utils/isEnterPress';
  10. import { handlePrevent } from '../utils/a11y';
  11. export interface SelectAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
  12. getTriggerWidth(): number;
  13. setOptionsWidth?(): any;
  14. updateFocusState(focus: boolean): void;
  15. focusTrigger(): void;
  16. unregisterClickOutsideHandler(): void;
  17. setOptionWrapperWidth(width: string | number): void;
  18. getOptionsFromChildren(): BasicOptionProps[];
  19. updateOptions(options: BasicOptionProps[]): void;
  20. rePositionDropdown(): void;
  21. updateFocusIndex(index: number): void;
  22. updateSelection(selection: Map<any, any>): void;
  23. openMenu(): void;
  24. notifyDropdownVisibleChange(visible: boolean): void;
  25. registerClickOutsideHandler(event: any): void;
  26. toggleInputShow(show: boolean, cb: () => void): void;
  27. closeMenu(): void;
  28. notifyCreate(option: BasicOptionProps): void;
  29. getMaxLimit(): number;
  30. getSelections(): Map<any, any>;
  31. notifyMaxLimit(arg: BasicOptionProps): void;
  32. notifyClear(): void;
  33. updateInputValue(inputValue: string): void;
  34. focusInput(): void;
  35. notifySearch(inputValue: string): void;
  36. registerKeyDown(handler: () => void): void;
  37. unregisterKeyDown(): void;
  38. notifyChange(value: string | BasicOptionProps | (string | BasicOptionProps)[]): void;
  39. notifySelect(value: BasicOptionProps['value'], option: BasicOptionProps): void;
  40. notifyDeselect(value: BasicOptionProps['value'], option: BasicOptionProps): void;
  41. notifyBlur(event: any): void;
  42. notifyFocus(event: any): void;
  43. notifyListScroll(event: any): void;
  44. notifyMouseLeave(event: any): void;
  45. notifyMouseEnter(event: any): void;
  46. updateHovering(isHover: boolean): void;
  47. updateScrollTop(index?: number): void;
  48. getContainer(): any;
  49. getFocusableElements(node: any): any[];
  50. getActiveElement(): any;
  51. setIsFocusInContainer(isFocusInContainer: boolean): void;
  52. getIsFocusInContainer(): boolean;
  53. }
  54. type LabelValue = string | number;
  55. type PropValue = LabelValue | Record<string, any>;
  56. export default class SelectFoundation extends BaseFoundation<SelectAdapter> {
  57. constructor(adapter: SelectAdapter) {
  58. super({ ...adapter });
  59. }
  60. // keyboard event listner
  61. // eslint-disable-next-line @typescript-eslint/member-ordering
  62. _keydownHandler: (...arg: any[]) => void | null = null;
  63. init() {
  64. this._setDropdownWidth();
  65. const isDefaultOpen = this.getProp('defaultOpen');
  66. const isOpen = this.getProp('open');
  67. const originalOptions = this._collectOptions();
  68. this._setDefaultSelection(originalOptions);
  69. if (isDefaultOpen || isOpen) {
  70. this.open(undefined, originalOptions);
  71. }
  72. const autoFocus = this.getProp('autoFocus');
  73. if (autoFocus) {
  74. this.focus();
  75. }
  76. }
  77. focus() {
  78. const isFilterable = this._isFilterable();
  79. const isMultiple = this._isMultiple();
  80. this._adapter.updateFocusState(true);
  81. this._adapter.setIsFocusInContainer(false);
  82. if (isFilterable && isMultiple) {
  83. // when filter and multiple, only focus input
  84. this.focusInput();
  85. } else if (isFilterable && !isMultiple){
  86. // when filter and not multiple, only show input and focus input
  87. this.toggle2SearchInput(true);
  88. } else {
  89. this._focusTrigger();
  90. }
  91. }
  92. _focusTrigger() {
  93. this._adapter.focusTrigger();
  94. // this.bindKeyBoardEvent();
  95. }
  96. destroy() {
  97. this._adapter.unregisterClickOutsideHandler();
  98. // this.unBindKeyBoardEvent();
  99. }
  100. _setDropdownWidth() {
  101. const { style, dropdownMatchSelectWidth } = this.getProps();
  102. let width;
  103. if (dropdownMatchSelectWidth) {
  104. if (style && isNumber(style.width)) {
  105. width = style.width;
  106. } else if (style && isString(style.width) && !style.width.includes('%')) {
  107. width = style.width;
  108. } else {
  109. width = this._adapter.getTriggerWidth();
  110. }
  111. this._adapter.setOptionWrapperWidth(width);
  112. }
  113. }
  114. _collectOptions() {
  115. const originalOptions = this._adapter.getOptionsFromChildren();
  116. this._adapter.updateOptions(originalOptions);
  117. // Reposition the drop-down layer
  118. this._adapter.rePositionDropdown();
  119. return originalOptions;
  120. }
  121. _setDefaultSelection(originalOptions: BasicOptionProps[]) {
  122. let { value } = this.getProps();
  123. const { defaultValue } = this.getProps();
  124. if (this._isControlledComponent()) {
  125. // do nothing
  126. } else {
  127. value = defaultValue;
  128. }
  129. this._update(value, originalOptions);
  130. }
  131. // call when props.optionList change
  132. handleOptionListChange() {
  133. const newOptionList = this._collectOptions();
  134. const { selections } = this.getStates();
  135. this.updateOptionsActiveStatus(selections, newOptionList);
  136. // reset focusIndex
  137. const { defaultActiveFirstOption } = this.getProps();
  138. if (defaultActiveFirstOption) {
  139. this._adapter.updateFocusIndex(0);
  140. }
  141. }
  142. // In uncontrolled mode, when props.optionList change,
  143. // but already had defaultValue or choose some option
  144. handleOptionListChangeHadDefaultValue() {
  145. const selections = this.getState('selections');
  146. let value;
  147. const { onChangeWithObject } = this.getProps();
  148. const isMultiple = this._isMultiple();
  149. switch (true) {
  150. case isMultiple && Boolean(selections.size):
  151. try {
  152. value = [...selections].map(item =>
  153. // At this point item1 is directly the object
  154. (onChangeWithObject ? item[1] : item[1].value)
  155. );
  156. } catch (error) {
  157. value = [];
  158. }
  159. break;
  160. case isMultiple && !selections.size:
  161. value = [];
  162. break;
  163. case !isMultiple && Boolean(selections.size):
  164. try {
  165. value = onChangeWithObject ? [...selections][0][1] : [...selections][0][1].value;
  166. } catch (error) {}
  167. break;
  168. case !isMultiple && !selections.size:
  169. break;
  170. default:
  171. break;
  172. }
  173. const originalOptions = this._adapter.getOptionsFromChildren();
  174. this._update(value, originalOptions);
  175. }
  176. // call when props.value change
  177. handleValueChange(value: PropValue) {
  178. const { allowCreate } = this.getProps();
  179. let originalOptions;
  180. // AllowCreate and controlled mode, no need to re-collect optionList
  181. if (allowCreate && this._isControlledComponent()) {
  182. originalOptions = this.getState('options') as BasicOptionProps[];
  183. originalOptions.forEach(item => (item._show = true));
  184. } else {
  185. // originalOptions = this.getState('options');
  186. // The options in state cannot be used directly here, because it is possible to update the optionList and props.value at the same time, and the options in state are still old at this time
  187. originalOptions = this._adapter.getOptionsFromChildren();
  188. }
  189. // Multi-selection, controlled mode, you need to reposition the drop-down menu after updating
  190. this._adapter.rePositionDropdown();
  191. this._update(value, originalOptions);
  192. }
  193. // Update the selected item in the selection box
  194. _update(propValue: PropValue, originalOptions: BasicOptionProps[]) {
  195. let selections;
  196. if (!this._isMultiple()) {
  197. // Radio
  198. selections = this._updateSingle(propValue, originalOptions);
  199. } else {
  200. selections = this._updateMultiple(propValue as (PropValue)[], originalOptions);
  201. }
  202. // Update the text in the selection box
  203. this._adapter.updateSelection(selections);
  204. // Update the selected item in the drop-down box
  205. this.updateOptionsActiveStatus(selections, originalOptions);
  206. }
  207. // Optionally selected updates (when components are mounted, or after value changes)
  208. _updateSingle(propValue: PropValue, originalOptions: BasicOptionProps[]) {
  209. const selections = new Map();
  210. const { onChangeWithObject } = this.getProps();
  211. // When onChangeWithObject is true, the defaultValue or Value passed by the props should be the object, which corresponds to the result returned by onChange, so the value of the object needs to be taken as a judgment comparison
  212. const selectedValue = onChangeWithObject && typeof propValue !== 'undefined' ? (propValue as BasicOptionProps).value : propValue;
  213. const selectedOptions = originalOptions.filter(option => option.value === selectedValue);
  214. const noMatchOptionInList = !selectedOptions.length && typeof selectedValue !== 'undefined';
  215. // If the current value, there is a matching option in the optionList
  216. if (selectedOptions.length) {
  217. const selectedOption = selectedOptions[0];
  218. const optionExist = { ...selectedOption };
  219. // if (onChangeWithObject) {
  220. // OptionExist = {... propValue }; // value is the object with the'value 'Key
  221. // }
  222. selections.set(optionExist.label, optionExist);
  223. } else if (noMatchOptionInList) {
  224. // If the current value does not have a corresponding item in the optionList, construct an option and update it to the selection. However, it does not need to be inserted into the list
  225. let optionNotExist = { value: propValue, label: propValue, _notExist: true, _scrollIndex: -1 } as BasicOptionProps;
  226. if (onChangeWithObject) {
  227. optionNotExist = { ...propValue as BasicOptionProps, _notExist: true, _scrollIndex: -1 };
  228. }
  229. selections.set(optionNotExist.label, optionNotExist);
  230. }
  231. return selections;
  232. }
  233. // Multi-selected option update (when the component is mounted, or after the value changes)
  234. _updateMultiple(propValue: PropValue[], originalOptions: BasicOptionProps[]) {
  235. const nowSelections = this.getState('selections');
  236. let selectedOptionList: any[] = [];
  237. // Multiple selection is to determine whether it is an array to avoid the problem of defaultValue/value incoming string error
  238. const propValueIsArray = Array.isArray(propValue);
  239. this.checkMultipleProps();
  240. // If N values are currently selected, the corresponding option data is retrieved from the current selections for retrieval. Because these selected options may not exist in the new optionList
  241. if (nowSelections.size) {
  242. selectedOptionList = [...nowSelections].map(item => item[1]);
  243. }
  244. const selections = new Map();
  245. let selectedValues = propValue;
  246. const { onChangeWithObject } = this.getProps();
  247. // When onChangeWithObject is true
  248. if (onChangeWithObject && propValueIsArray) {
  249. selectedValues = (propValue as BasicOptionProps[]).map(item => item.value);
  250. }
  251. if (propValueIsArray && selectedValues.length) {
  252. (selectedValues as LabelValue[]).forEach((selectedValue, i: number) => {
  253. // The current value exists in the current optionList
  254. const index = originalOptions.findIndex(option => option.value === selectedValue);
  255. if (index !== -1) {
  256. selections.set(originalOptions[index].label, originalOptions[index]);
  257. } else {
  258. // The current value exists in the optionList that has been selected before the change, and does not exist in the current optionList, then directly take the corresponding value from the selections, no need to construct a new option
  259. const indexInSelectedList = selectedOptionList.findIndex(option => option.value === selectedValue);
  260. if (indexInSelectedList !== -1) {
  261. const option = selectedOptionList[indexInSelectedList];
  262. selections.set(option.label, option);
  263. } else {
  264. // The current value does not exist in the current optionList or the list before the change. Construct an option and update it to the selection
  265. let optionNotExist = { value: selectedValue, label: selectedValue, _notExist: true };
  266. onChangeWithObject ? (optionNotExist = { ...propValue[i] as any, _notExist: true }) : null;
  267. selections.set(optionNotExist.label, { ...optionNotExist, _scrollIndex: -1 });
  268. }
  269. }
  270. });
  271. }
  272. return selections;
  273. }
  274. _isMultiple() {
  275. return this.getProp('multiple');
  276. }
  277. _isDisabled() {
  278. return this.getProp('disabled');
  279. }
  280. _isFilterable() {
  281. return Boolean(this.getProp('filter')); // filter can be boolean or function
  282. }
  283. handleClick(e: any) {
  284. const { clickToHide } = this.getProps();
  285. const { isOpen } = this.getStates();
  286. const isDisabled = this._isDisabled();
  287. if (isDisabled) {
  288. return;
  289. } else if (!isOpen) {
  290. this.open();
  291. this._notifyFocus(e);
  292. } else if (isOpen && clickToHide) {
  293. this.close(e);
  294. } else if (isOpen && !clickToHide) {
  295. this.focusInput();
  296. }
  297. }
  298. open(acInput?: string, originalOptions?: BasicOptionProps[]) {
  299. const isFilterable = this._isFilterable();
  300. const options = originalOptions || this.getState('options');
  301. // When searchable, when the drop-down box expands
  302. if (isFilterable) {
  303. // Also clears the options filter to show all candidates
  304. // Options created dynamically but not selected are also filtered out
  305. const sugInput = '';
  306. const newOptions = this._filterOption(options, sugInput).filter(item => !item._inputCreateOnly);
  307. this._adapter.updateOptions(newOptions);
  308. this.toggle2SearchInput(true);
  309. }
  310. this._adapter.openMenu();
  311. this._setDropdownWidth();
  312. this._adapter.notifyDropdownVisibleChange(true);
  313. this.bindKeyBoardEvent();
  314. this._adapter.registerClickOutsideHandler((e: MouseEvent) => {
  315. this.close(e);
  316. this._notifyBlur(e);
  317. this._adapter.updateFocusState(false);
  318. });
  319. }
  320. toggle2SearchInput(isShow: boolean) {
  321. if (isShow) {
  322. this._adapter.toggleInputShow(isShow, () => this.focusInput());
  323. } else {
  324. // only when choose the option and close the panel, the input can be hide
  325. this._adapter.toggleInputShow(isShow, () => undefined);
  326. }
  327. }
  328. close(e?: any) {
  329. // to support A11y, closing the panel trigger does not necessarily lose focus
  330. const isFilterable = this._isFilterable();
  331. if (isFilterable) {
  332. // this.unBindKeyBoardEvent();
  333. this.clearInput();
  334. this.toggle2SearchInput(false);
  335. }
  336. this._adapter.closeMenu();
  337. this._adapter.notifyDropdownVisibleChange(false);
  338. this._adapter.setIsFocusInContainer(false);
  339. // this.unBindKeyBoardEvent();
  340. // this._notifyBlur(e);
  341. this._adapter.unregisterClickOutsideHandler();
  342. // this._adapter.updateFocusState(false);
  343. }
  344. onSelect(option: BasicOptionProps, optionIndex: number, event: MouseEvent | KeyboardEvent) {
  345. const isDisabled = this._isDisabled();
  346. if (isDisabled) {
  347. return;
  348. }
  349. // If the allowCreate dynamically created option is selected, onCreate needs to be triggered
  350. if (option._inputCreateOnly) {
  351. this._adapter.notifyCreate(option);
  352. }
  353. const isMultiple = this._isMultiple();
  354. if (!isMultiple) {
  355. this._handleSingleSelect(option, event);
  356. this._focusTrigger();
  357. } else {
  358. this._handleMultipleSelect(option, event);
  359. }
  360. this._adapter.updateFocusIndex(optionIndex);
  361. }
  362. _handleSingleSelect({ value, label, ...rest }: BasicOptionProps, event: any) {
  363. const selections = new Map().set(label, { value, label, ...rest });
  364. // First trigger onSelect, then trigger onChange
  365. this._notifySelect(value, { value, label, ...rest });
  366. // If it is a controlled component, directly notify
  367. if (this._isControlledComponent()) {
  368. this._notifyChange(selections);
  369. this.close(event);
  370. } else {
  371. this._adapter.updateSelection(selections);
  372. // notify user
  373. this._notifyChange(selections);
  374. // Update the selected item in the drop-down box
  375. this.close(event);
  376. this.updateOptionsActiveStatus(selections);
  377. }
  378. }
  379. _handleMultipleSelect({ value, label, ...rest }: BasicOptionProps, event: MouseEvent | KeyboardEvent) {
  380. const maxLimit = this._adapter.getMaxLimit();
  381. const selections = this._adapter.getSelections();
  382. const { autoClearSearchValue } = this.getProps();
  383. if (selections.has(label)) {
  384. this._notifyDeselect(value, { value, label, ...rest });
  385. selections.delete(label);
  386. } else if (maxLimit && selections.size === maxLimit) {
  387. this._adapter.notifyMaxLimit({ value, label, ...omit(rest, '_scrollIndex') });
  388. return;
  389. } else {
  390. this._notifySelect(value, { value, label, ...rest });
  391. selections.set(label, { value, label, ...rest });
  392. }
  393. if (this._isControlledComponent()) {
  394. // Controlled components, directly notified
  395. this._notifyChange(selections);
  396. if (this._isFilterable()) {
  397. if (autoClearSearchValue) {
  398. this.clearInput();
  399. }
  400. this.focusInput();
  401. }
  402. } else {
  403. // Uncontrolled components, update ui
  404. this._adapter.updateSelection(selections);
  405. // In multi-select mode, the drop-down pop-up layer is repositioned every time the value is changed, because the height selection of the selection box may have changed
  406. this._adapter.rePositionDropdown();
  407. let { options } = this.getStates();
  408. // Searchable filtering, when selected, resets Input
  409. if (this._isFilterable()) {
  410. // When filter active,if autoClearSearchValue is true,reset input after select
  411. if (autoClearSearchValue) {
  412. this.clearInput();
  413. // At the same time, the filtering of options is also cleared, in order to show all candidates
  414. const sugInput = '';
  415. options = this._filterOption(options, sugInput);
  416. }
  417. this.focusInput();
  418. }
  419. this.updateOptionsActiveStatus(selections, options);
  420. this._notifyChange(selections);
  421. }
  422. }
  423. clearSelected() {
  424. const selections = new Map();
  425. if (this._isControlledComponent()) {
  426. this._notifyChange(selections);
  427. this._adapter.notifyClear();
  428. } else {
  429. this._adapter.updateSelection(selections);
  430. this.updateOptionsActiveStatus(selections);
  431. this._notifyChange(selections);
  432. this._adapter.notifyClear();
  433. }
  434. // when call manually by ref method
  435. const { isOpen } = this.getStates();
  436. if (isOpen) {
  437. this._adapter.rePositionDropdown();
  438. }
  439. }
  440. // Update the selected item in the drop-down box
  441. updateOptionsActiveStatus(selections: Map<any, any>, options: BasicOptionProps[] = this.getState('options')) {
  442. const { allowCreate } = this.getProps();
  443. const newOptions = options.map(option => {
  444. if (selections.has(option.label)) {
  445. option._selected = true;
  446. if (allowCreate) {
  447. delete option._inputCreateOnly;
  448. }
  449. } else {
  450. if (option._inputCreateOnly) {
  451. option._show = false;
  452. }
  453. option._selected = false;
  454. }
  455. return option;
  456. });
  457. this._adapter.updateOptions(newOptions);
  458. }
  459. removeTag(item: BasicOptionProps) {
  460. const selections = this._adapter.getSelections();
  461. selections.delete(item.label);
  462. if (this._isControlledComponent()) {
  463. this._notifyDeselect(item.value, item);
  464. this._notifyChange(selections);
  465. } else {
  466. this._notifyDeselect(item.value, item);
  467. this._adapter.updateSelection(selections);
  468. this.updateOptionsActiveStatus(selections);
  469. // Repostion drop-down layer, because the selection may have changed the number of rows, resulting in a height change
  470. this._adapter.rePositionDropdown();
  471. this._notifyChange(selections);
  472. }
  473. }
  474. clearInput() {
  475. this._adapter.updateInputValue('');
  476. this._adapter.notifySearch('');
  477. // reset options filter
  478. const { options } = this.getStates();
  479. const { remote } = this.getProps();
  480. let optionsAfterFilter = options;
  481. if (!remote) {
  482. optionsAfterFilter = this._filterOption(options, '');
  483. }
  484. this._adapter.updateOptions(optionsAfterFilter);
  485. }
  486. focusInput() {
  487. this._adapter.focusInput();
  488. this._adapter.updateFocusState(true);
  489. this._adapter.setIsFocusInContainer(false);
  490. }
  491. handleInputChange(sugInput: string) {
  492. // Input is a controlled component, so the value needs to be updated
  493. this._adapter.updateInputValue(sugInput);
  494. const { options, isOpen } = this.getStates();
  495. const { allowCreate, remote } = this.getProps();
  496. let optionsAfterFilter = options;
  497. if (!remote) {
  498. // Filter options based on input
  499. optionsAfterFilter = this._filterOption(options, sugInput);
  500. }
  501. // When allowClear is true, an entry can be created. You need to include the current input as a new Option input
  502. optionsAfterFilter = this._createOptionByInput(allowCreate, optionsAfterFilter, sugInput);
  503. this._adapter.updateOptions(optionsAfterFilter);
  504. this._adapter.notifySearch(sugInput);
  505. // In multi-select mode, the drop-down box is repositioned each time you enter, because it may cause a line break as the input changes
  506. if (this._isMultiple()) {
  507. this._adapter.rePositionDropdown();
  508. }
  509. }
  510. _filterOption(originalOptions: BasicOptionProps[], sugInput: string) {
  511. const filter = this.getProp('filter');
  512. if (!filter) {
  513. // 1. No filtering
  514. return originalOptions;
  515. } else if (typeof filter === 'boolean' && filter) {
  516. // 2. When true, the default filter is used
  517. const input = sugInput.toLowerCase();
  518. return originalOptions.map(option => {
  519. const label = option.label.toString().toLowerCase();
  520. const groupLabel = option._parentGroup && option._parentGroup.label;
  521. const matchOption = label.includes(input);
  522. const matchGroup = isString(groupLabel) && groupLabel.toLowerCase().includes(input);
  523. if (matchOption || matchGroup) {
  524. option._show = true;
  525. } else {
  526. option._show = false;
  527. }
  528. return option;
  529. });
  530. } else if (typeof filter === 'function') {
  531. // 3. When passing in a custom function, use a custom function for filtering
  532. return originalOptions.map(option => {
  533. filter(sugInput, option) ? (option._show = true) : (option._show = false);
  534. return option;
  535. });
  536. }
  537. return undefined;
  538. }
  539. _createOptionByInput(allowCreate: boolean, optionsAfterFilter: BasicOptionProps[], sugInput: string) {
  540. if (allowCreate) {
  541. if (sugInput) {
  542. // optionsAfterFilter clone ??? needClone ?
  543. const newOptionByInput = {
  544. _show: true,
  545. _selected: false,
  546. value: sugInput,
  547. label: sugInput,
  548. // True indicates that the option was dynamically created during user filtering
  549. _inputCreateOnly: true,
  550. };
  551. let createOptionIndex = -1;
  552. let matchOptionIndex = -1;
  553. optionsAfterFilter.forEach((option, index) => {
  554. if (!option._show && !option._inputCreateOnly) {
  555. return;
  556. }
  557. // The matching algorithm is not necessarily through labels?
  558. if (option.label === sugInput) {
  559. matchOptionIndex = index;
  560. }
  561. if (option._inputCreateOnly) {
  562. createOptionIndex = index;
  563. option.value = sugInput;
  564. option.label = sugInput;
  565. option._show = true;
  566. }
  567. });
  568. if (createOptionIndex === -1 && matchOptionIndex === -1) {
  569. optionsAfterFilter.push(newOptionByInput);
  570. }
  571. if (matchOptionIndex !== -1) {
  572. optionsAfterFilter = optionsAfterFilter.filter(item => !item._inputCreateOnly);
  573. }
  574. } else {
  575. // Delete input unselected items
  576. optionsAfterFilter = optionsAfterFilter.filter(item => !item._inputCreateOnly);
  577. }
  578. }
  579. // TODO Promise supports asynchronous creation
  580. return optionsAfterFilter;
  581. }
  582. bindKeyBoardEvent() {
  583. this._keydownHandler = event => {
  584. this._handleKeyDown(event);
  585. };
  586. this._adapter.registerKeyDown(this._keydownHandler);
  587. }
  588. unBindKeyBoardEvent() {
  589. if (this._keydownHandler) {
  590. this._adapter.unregisterKeyDown();
  591. }
  592. }
  593. _handleKeyDown(event: KeyboardEvent) {
  594. const key = event.keyCode;
  595. const { loading, filter, multiple, disabled } = this.getProps();
  596. const { isOpen } = this.getStates();
  597. if (loading || disabled) {
  598. return;
  599. }
  600. switch (key) {
  601. case KeyCode.UP:
  602. // Prevent Input's cursor from following
  603. // Prevent Input cursor from following
  604. event.preventDefault();
  605. this._handleArrowKeyDown(-1);
  606. break;
  607. case KeyCode.DOWN:
  608. // Prevent Input's cursor from following
  609. // Prevent Input cursor from following
  610. event.preventDefault();
  611. this._handleArrowKeyDown(1);
  612. break;
  613. case KeyCode.BACKSPACE:
  614. this._handleBackspaceKeyDown();
  615. break;
  616. case KeyCode.ENTER:
  617. // internal-issues:302
  618. // prevent trigger form’s submit when use in form
  619. handlePrevent(event);
  620. this._handleEnterKeyDown(event);
  621. break;
  622. case KeyCode.ESC:
  623. isOpen && this.close(event);
  624. filter && !multiple && this._focusTrigger();
  625. break;
  626. case KeyCode.TAB:
  627. // check if slot have focusable element
  628. this._handleTabKeyDown(event);
  629. break;
  630. default:
  631. break;
  632. }
  633. }
  634. handleContainerKeyDown(event: any) {
  635. // when focus in contanier, handle the key down
  636. const key = event.keyCode;
  637. const { isOpen } = this.getStates();
  638. switch (key) {
  639. case KeyCode.TAB:
  640. isOpen && this._handleTabKeyDown(event);
  641. break;
  642. default:
  643. break;
  644. }
  645. }
  646. _getEnableFocusIndex(offset: number) {
  647. const { focusIndex, options } = this.getStates();
  648. const visibleOptions = options.filter((item: BasicOptionProps) => item._show);
  649. // let visibleOptions = options;
  650. const optionsLength = visibleOptions.length;
  651. let index = focusIndex + offset;
  652. if (index < 0) {
  653. index = optionsLength - 1;
  654. }
  655. if (index >= optionsLength) {
  656. index = 0;
  657. }
  658. // avoid newIndex option is disabled
  659. if (offset > 0) {
  660. let nearestActiveOption = -1;
  661. for (let i = 0; i < visibleOptions.length; i++) {
  662. const optionIsActive = !visibleOptions[i].disabled;
  663. if (optionIsActive) {
  664. nearestActiveOption = i;
  665. }
  666. if (nearestActiveOption >= index) {
  667. break;
  668. }
  669. }
  670. index = nearestActiveOption;
  671. } else {
  672. let nearestActiveOption = visibleOptions.length;
  673. for (let i = optionsLength - 1; i >= 0; i--) {
  674. const optionIsActive = !visibleOptions[i].disabled;
  675. if (optionIsActive) {
  676. nearestActiveOption = i;
  677. }
  678. if (nearestActiveOption <= index) {
  679. break;
  680. }
  681. }
  682. index = nearestActiveOption;
  683. }
  684. // console.log('new:' + index);
  685. this._adapter.updateFocusIndex(index);
  686. this._adapter.updateScrollTop(index);
  687. }
  688. _handleArrowKeyDown(offset: number) {
  689. const { isOpen } = this.getStates();
  690. isOpen ? this._getEnableFocusIndex(offset) : this.open();
  691. }
  692. _handleTabKeyDown(event: any){
  693. const { isOpen } = this.getStates();
  694. this._adapter.updateFocusState(false);
  695. if (isOpen){
  696. const container = this._adapter.getContainer();
  697. const focusableElements = this._adapter.getFocusableElements(container);
  698. const focusableNum = focusableElements.length;
  699. if (focusableNum > 0){
  700. // Shift + Tab will move focus backward
  701. if (event.shiftKey) {
  702. this._handlePanelOpenShiftTabKeyDown(focusableElements, event);
  703. } else {
  704. this._handlePanelOpenTabKeyDown(focusableElements, event);
  705. }
  706. } else {
  707. // there are no focusable elements inside the container, tab to next element and trigger blur
  708. this.close();
  709. this._notifyBlur(event);
  710. }
  711. } else {
  712. // tab or shift tab to next element and trigger blur
  713. this._notifyBlur(event);
  714. }
  715. }
  716. _handlePanelOpenTabKeyDown(focusableElements: any[], event: any) {
  717. const activeElement = this._adapter.getActiveElement();
  718. const isFocusInContainer = this._adapter.getIsFocusInContainer();
  719. if (!isFocusInContainer){
  720. // focus in trigger, set next focus to the first element in container
  721. focusableElements[0].focus();
  722. this._adapter.setIsFocusInContainer(true);
  723. handlePrevent(event);
  724. } else if (activeElement === focusableElements[focusableElements.length - 1]) {
  725. // focus in the last element in container, focus back to trigger and close panel
  726. this._focusTrigger();
  727. this.close();
  728. handlePrevent(event);
  729. }
  730. }
  731. _handlePanelOpenShiftTabKeyDown(focusableElements: any[], event: any) {
  732. const activeElement = this._adapter.getActiveElement();
  733. const isFocusInContainer = this._adapter.getIsFocusInContainer();
  734. if (!isFocusInContainer) {
  735. // focus in trigger, close the panel, shift tab to previe element and trigger blur
  736. this.close();
  737. this._notifyBlur(event);
  738. } else if (activeElement === focusableElements[0]) {
  739. // focus in the first element in container, focus back to trigger
  740. this._focusTrigger();
  741. this._adapter.setIsFocusInContainer(false);
  742. handlePrevent(event);
  743. }
  744. }
  745. _handleEnterKeyDown(event: KeyboardEvent) {
  746. const { isOpen, options, focusIndex } = this.getStates();
  747. if (!isOpen){
  748. this.open();
  749. } else {
  750. if (focusIndex !== -1) {
  751. const visibleOptions = options.filter((item: BasicOptionProps) => item._show);
  752. const { length } = visibleOptions;
  753. // fix issue 1201
  754. if (length <= focusIndex) {
  755. return;
  756. }
  757. if (visibleOptions && length) {
  758. const selectedOption = visibleOptions[focusIndex];
  759. if (selectedOption.disabled) {
  760. return;
  761. }
  762. this.onSelect(selectedOption, focusIndex, event);
  763. }
  764. } else {
  765. this.close();
  766. }
  767. }
  768. }
  769. _handleBackspaceKeyDown() {
  770. if (this._isMultiple()) {
  771. const selections = this._adapter.getSelections();
  772. const { inputValue } = this.getStates();
  773. const length = selections.size;
  774. if (length && !inputValue) {
  775. const keys = [...selections.keys()];
  776. let index = length - 1;
  777. let targetLabel = keys[index];
  778. let targetItem = selections.get(targetLabel);
  779. let isAllDisabled = false;
  780. // can skip disabled item when remove trigger by backspace
  781. if (targetItem.disabled && index === 0) {
  782. return;
  783. }
  784. while (targetItem.disabled && index !== 0) {
  785. index = index - 1;
  786. targetLabel = keys[index];
  787. targetItem = selections.get(targetLabel);
  788. // eslint-disable-next-line
  789. if (index == 0 && targetItem.disabled) {
  790. isAllDisabled = true;
  791. }
  792. }
  793. if (!isAllDisabled) {
  794. this.removeTag(targetItem);
  795. }
  796. }
  797. }
  798. }
  799. _notifyChange(selections: Map<any, any>) {
  800. const { onChangeWithObject } = this.getProps();
  801. const stateSelections = this.getState('selections');
  802. let notifyVal;
  803. const selectionsProps = [...selections.values()];
  804. const isMultiple = this._isMultiple();
  805. const hasChange = this._diffSelections(selections, stateSelections, isMultiple);
  806. if (!hasChange) {
  807. return;
  808. }
  809. switch (true) {
  810. case onChangeWithObject:
  811. this._notifyChangeWithObject(selections);
  812. break;
  813. case !onChangeWithObject && !isMultiple:
  814. notifyVal = selectionsProps.length ? selectionsProps[0].value : undefined;
  815. this._adapter.notifyChange(notifyVal);
  816. break;
  817. case !onChangeWithObject && isMultiple:
  818. notifyVal = selectionsProps.length ? selectionsProps.map(props => props.value) : [];
  819. this._adapter.notifyChange(notifyVal);
  820. break;
  821. default:
  822. break;
  823. }
  824. }
  825. _removeInternalKey(option: BasicOptionProps) {
  826. // eslint-disable-next-line
  827. let newOption = { ...option };
  828. delete newOption._parentGroup;
  829. delete newOption._show;
  830. delete newOption._selected;
  831. delete newOption._scrollIndex;
  832. if ('_keyInOptionList' in newOption) {
  833. newOption.key = newOption._keyInOptionList;
  834. delete newOption._keyInOptionList;
  835. }
  836. return newOption;
  837. }
  838. _notifySelect(value: BasicOptionProps['value'], option: BasicOptionProps) {
  839. const newOption = this._removeInternalKey(option);
  840. this._adapter.notifySelect(value, newOption);
  841. }
  842. _notifyDeselect(value: BasicOptionProps['value'], option: BasicOptionProps) {
  843. const newOption = this._removeInternalKey(option);
  844. this._adapter.notifyDeselect(value, newOption);
  845. }
  846. _diffSelections(selections: Map<any, any>, oldSelections: Map<any, any>, isMultiple: boolean) {
  847. let diff = true;
  848. if (!isMultiple) {
  849. const selectionProps = [...selections.values()];
  850. const oldSelectionProps = [...oldSelections.values()];
  851. const optionLabel = selectionProps[0] ? selectionProps[0].label : selectionProps[0];
  852. const oldOptionLabel = oldSelectionProps[0] ? oldSelectionProps[0].label : oldSelectionProps[0];
  853. diff = !isEqual(optionLabel, oldOptionLabel);
  854. } else {
  855. // When multiple selection, there is no scene where the value is different between the two operations
  856. }
  857. return diff;
  858. }
  859. // When onChangeWithObject is true, the onChange input parameter is not only value, but also label and other parameters
  860. _notifyChangeWithObject(selections: Map<any, any>) {
  861. const stateSelections = this.getState('selections');
  862. const values = [];
  863. for (const item of selections.entries()) {
  864. let val = { label: item[0], ...item[1] };
  865. val = this._removeInternalKey(val);
  866. values.push(val);
  867. }
  868. if (!this._isMultiple()) {
  869. this._adapter.notifyChange(values[0]);
  870. } else {
  871. this._adapter.notifyChange(values);
  872. }
  873. }
  874. // Scenes that may trigger blur:
  875. // 1、clickOutSide
  876. // 2、 tab to next element/ shift tab to previous element
  877. // 3、[remove when add a11y] click option / press enter, and then select complete(when multiple is false
  878. // 4、[remove when add a11y] press esc when dropdown list open
  879. _notifyBlur(e: FocusEvent) {
  880. this._adapter.notifyBlur(e);
  881. }
  882. // Scenes that may trigger focus:
  883. // 1、click selection
  884. _notifyFocus(e: FocusEvent) {
  885. this._adapter.notifyFocus(e);
  886. }
  887. handleMouseEnter(e: MouseEvent) {
  888. this._adapter.updateHovering(true);
  889. this._adapter.notifyMouseEnter(e);
  890. }
  891. handleMouseLeave(e: MouseEvent) {
  892. this._adapter.updateHovering(false);
  893. this._adapter.notifyMouseLeave(e);
  894. }
  895. handleClearClick(e: MouseEvent) {
  896. const { filter } = this.getProps();
  897. if (filter) {
  898. this.clearInput();
  899. }
  900. this.clearSelected();
  901. // prevent this click open dropdown
  902. e.stopPropagation();
  903. }
  904. handleKeyPress(e: KeyboardEvent) {
  905. if (e && e.key === ENTER_KEY) {
  906. this.handleClick(e);
  907. }
  908. }
  909. /* istanbul ignore next */
  910. handleClearBtnEnterPress(e: KeyboardEvent) {
  911. if (isEnterPress(e)) {
  912. this.handleClearClick(e as any);
  913. }
  914. }
  915. handleOptionMouseEnter(optionIndex: number) {
  916. this._adapter.updateFocusIndex(optionIndex);
  917. }
  918. handleListScroll(e: any) {
  919. this._adapter.notifyListScroll(e);
  920. }
  921. handleTriggerFocus(e) {
  922. this.bindKeyBoardEvent();
  923. this._adapter.updateFocusState(true);
  924. this._adapter.setIsFocusInContainer(false);
  925. }
  926. handleTriggerBlur(e: FocusEvent) {
  927. this._adapter.updateFocusState(false);
  928. const { filter, autoFocus } = this.getProps();
  929. const { isOpen, isFocus } = this.getStates();
  930. // Under normal circumstances, blur will be accompanied by clickOutsideHandler, so the notify of blur can be called uniformly in clickOutsideHandler
  931. // But when autoFocus, because clickOutsideHandler is not register, you need to listen for the trigger's blur and trigger the notify callback
  932. if (autoFocus && isFocus && !isOpen) {
  933. this._notifyBlur(e);
  934. }
  935. }
  936. handleInputBlur(e: any) {
  937. const { filter, autoFocus } = this.getProps();
  938. const isMultiple = this._isMultiple();
  939. if (autoFocus && filter && !isMultiple ) {
  940. // under this condition, when input blur, hide the input
  941. this.toggle2SearchInput(false);
  942. }
  943. }
  944. selectAll() {
  945. const { options } = this.getStates();
  946. const { onChangeWithObject } = this.getProps();
  947. let selectedValues = [];
  948. const isMultiple = this._isMultiple();
  949. if (!isMultiple) {
  950. console.warn(`[Semi Select]: It seems that you have called the selectAll method in the single-selection Select.
  951. Please note that this is not a legal way to use it`
  952. );
  953. return;
  954. }
  955. if (onChangeWithObject) {
  956. selectedValues = options;
  957. } else {
  958. selectedValues = options.map((option: BasicOptionProps) => option.value);
  959. }
  960. this.handleValueChange(selectedValues);
  961. this._adapter.notifyChange(selectedValues);
  962. }
  963. /**
  964. * Check whether the props
  965. * -defaultValue/value in multiple selection mode is array
  966. * @param {Object} props
  967. */
  968. checkMultipleProps(props?: Record<string, any>) {
  969. if (this._isMultiple()) {
  970. const currentProps = props ? props : this.getProps();
  971. const { defaultValue, value } = currentProps;
  972. const selectedValues = value || defaultValue;
  973. if (!isNullOrUndefined(selectedValues) && !Array.isArray(selectedValues)) {
  974. /* istanbul ignore next */
  975. warning(true, '[Semi Select] defaultValue/value should be array type in multiple mode');
  976. }
  977. }
  978. }
  979. updateScrollTop() {
  980. this._adapter.updateScrollTop();
  981. }
  982. }