amis 1.9.1-beta.28 → 1.9.1-beta.29

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. package/lib/components/AnchorNav.d.ts +8 -2
  2. package/lib/components/AnchorNav.js +24 -5
  3. package/lib/components/AnchorNav.js.map +2 -2
  4. package/lib/components/DatePicker.d.ts +41 -40
  5. package/lib/components/DatePicker.js +2 -2
  6. package/lib/components/DatePicker.js.map +2 -2
  7. package/lib/components/DateRangePicker.d.ts +44 -41
  8. package/lib/components/DateRangePicker.js +25 -8
  9. package/lib/components/DateRangePicker.js.map +2 -2
  10. package/lib/components/MonthRangePicker.d.ts +41 -40
  11. package/lib/components/MonthRangePicker.js +2 -2
  12. package/lib/components/MonthRangePicker.js.map +2 -2
  13. package/lib/components/SearchBox.d.ts +124 -42
  14. package/lib/components/SearchBox.js +181 -19
  15. package/lib/components/SearchBox.js.map +2 -2
  16. package/lib/components/Tag.d.ts +12 -10
  17. package/lib/components/Tag.js +18 -7
  18. package/lib/components/Tag.js.map +2 -2
  19. package/lib/locale/de-DE.js +1 -0
  20. package/lib/locale/de-DE.js.map +2 -2
  21. package/lib/locale/en-US.js +1 -0
  22. package/lib/locale/en-US.js.map +2 -2
  23. package/lib/locale/zh-CN.js +1 -0
  24. package/lib/locale/zh-CN.js.map +2 -2
  25. package/lib/renderers/Card.d.ts +7 -2
  26. package/lib/renderers/Card.js +11 -7
  27. package/lib/renderers/Card.js.map +2 -2
  28. package/lib/renderers/Form/InputExcel.d.ts +1 -0
  29. package/lib/renderers/Form/InputExcel.js +5 -0
  30. package/lib/renderers/Form/InputExcel.js.map +2 -2
  31. package/lib/renderers/Form/InputFile.d.ts +2 -2
  32. package/lib/renderers/Form/InputFile.js +6 -5
  33. package/lib/renderers/Form/InputFile.js.map +2 -2
  34. package/lib/renderers/Form/InputText.d.ts +8 -0
  35. package/lib/renderers/Form/InputText.js +8 -8
  36. package/lib/renderers/Form/InputText.js.map +2 -2
  37. package/lib/renderers/Table/index.js +1 -1
  38. package/lib/renderers/Table/index.js.map +2 -2
  39. package/lib/renderers/Tag.js +2 -2
  40. package/lib/renderers/Tag.js.map +2 -2
  41. package/lib/renderers/Wizard.d.ts +1 -1
  42. package/lib/renderers/Wizard.js +72 -75
  43. package/lib/renderers/Wizard.js.map +2 -2
  44. package/lib/store/formItem.js +2 -1
  45. package/lib/store/formItem.js.map +2 -2
  46. package/lib/themes/ang-ie11.css +120 -15
  47. package/lib/themes/ang.css +124 -15
  48. package/lib/themes/ang.css.map +1 -1
  49. package/lib/themes/antd-ie11.css +120 -15
  50. package/lib/themes/antd.css +124 -15
  51. package/lib/themes/antd.css.map +1 -1
  52. package/lib/themes/cxd-ie11.css +120 -15
  53. package/lib/themes/cxd.css +124 -15
  54. package/lib/themes/cxd.css.map +1 -1
  55. package/lib/themes/dark-ie11.css +120 -15
  56. package/lib/themes/dark.css +124 -15
  57. package/lib/themes/dark.css.map +1 -1
  58. package/lib/themes/default-ie11.css +120 -15
  59. package/lib/themes/default.css +124 -15
  60. package/lib/themes/default.css.map +1 -1
  61. package/package.json +3 -3
  62. package/schema.json +35 -14
  63. package/scss/_properties.scss +5 -0
  64. package/scss/components/_anchor-nav.scss +1 -0
  65. package/scss/components/_calendar.scss +32 -14
  66. package/scss/components/_search-box.scss +116 -10
  67. package/scss/components/_tag.scss +12 -3
  68. package/scss/components/form/_date-range.scss +1 -1
  69. package/sdk/ang-ie11.css +131 -14
  70. package/sdk/ang.css +135 -14
  71. package/sdk/antd-ie11.css +131 -14
  72. package/sdk/antd.css +135 -14
  73. package/sdk/cxd-ie11.css +131 -14
  74. package/sdk/cxd.css +135 -14
  75. package/sdk/dark-ie11.css +131 -14
  76. package/sdk/dark.css +135 -14
  77. package/sdk/locale/de-DE.js +1 -0
  78. package/sdk/sdk-ie11.css +131 -14
  79. package/sdk/sdk.css +135 -14
  80. package/sdk/sdk.js +17 -17
  81. package/src/components/AnchorNav.tsx +40 -7
  82. package/src/components/DatePicker.tsx +8 -4
  83. package/src/components/DateRangePicker.tsx +34 -8
  84. package/src/components/MonthRangePicker.tsx +4 -2
  85. package/src/components/SearchBox.tsx +262 -29
  86. package/src/components/Tag.tsx +14 -3
  87. package/src/locale/de-DE.ts +1 -0
  88. package/src/locale/en-US.ts +1 -0
  89. package/src/locale/zh-CN.ts +1 -0
  90. package/src/renderers/Card.tsx +24 -12
  91. package/src/renderers/Form/InputExcel.tsx +6 -0
  92. package/src/renderers/Form/InputFile.tsx +19 -18
  93. package/src/renderers/Form/InputText.tsx +20 -5
  94. package/src/renderers/Table/index.tsx +1 -1
  95. package/src/renderers/Tag.tsx +2 -0
  96. package/src/renderers/Wizard.tsx +2 -1
  97. package/src/store/formItem.ts +4 -1
@@ -49,8 +49,13 @@ export interface AnchorNavProps extends ThemeProps {
49
49
  direction?: 'vertical' | 'horizontal'; // 导航方向
50
50
  }
51
51
 
52
+ interface SectionOffset {
53
+ key: string | number;
54
+ offsetTop: number;
55
+ }
56
+
52
57
  export interface AnchorNavState {
53
- offsetArr: PlainObject[]; // 记录每个段落的offsetTop
58
+ offsetArr: SectionOffset[]; // 记录每个段落的offsetTop
54
59
  fromSelect: boolean; // 标识滚动触发来源
55
60
  }
56
61
 
@@ -67,17 +72,45 @@ export class AnchorNav extends React.Component<AnchorNavProps, AnchorNavState> {
67
72
  // 滚动区域DOM
68
73
  contentDom: React.RefObject<HTMLDivElement> = React.createRef();
69
74
 
75
+ // 后代节点观察器
76
+ observer: MutationObserver;
77
+
70
78
  componentDidMount() {
71
- // 初始化滚动标识
79
+ // 初始化滚动标识
72
80
  this.setState({fromSelect: false});
73
81
 
74
- // add scroll event
75
82
  const sectionRootDom =
76
83
  this.contentDom && (this.contentDom.current as HTMLElement);
77
- sectionRootDom.addEventListener('scroll', this.scrollToNav);
78
- let offsetArr: Array<object> = [];
84
+
85
+ this.updateSectionOffset(sectionRootDom, false);
86
+ this.observer = new MutationObserver((mutations: MutationRecord[]) => {
87
+ const ModDetected = mutations.some(record =>
88
+ record.target.parentNode?.isSameNode(sectionRootDom)
89
+ );
90
+
91
+ if (ModDetected) {
92
+ this.updateSectionOffset(sectionRootDom, true);
93
+ }
94
+ });
95
+ this.observer.observe(sectionRootDom, {childList: true, subtree: true});
96
+ }
97
+
98
+ componentWillUnmount() {
99
+ if (this.contentDom && this.contentDom.current) {
100
+ this.contentDom.current.removeEventListener('scroll', this.scrollToNav);
101
+ }
102
+ this.observer && this.observer.disconnect();
103
+ }
104
+
105
+ updateSectionOffset(parentNode: HTMLElement, inited: boolean) {
106
+ const offsetArr: Array<SectionOffset> = [];
79
107
  const {children, active} = this.props;
80
108
 
109
+ if (!inited) {
110
+ // add scroll event
111
+ parentNode.addEventListener('scroll', this.scrollToNav);
112
+ }
113
+
81
114
  // 收集段落区域offsetTop
82
115
  children &&
83
116
  React.Children.forEach(
@@ -85,7 +118,7 @@ export class AnchorNav extends React.Component<AnchorNavProps, AnchorNavState> {
85
118
  (section: AnchorNavSectionComponent, index: number) => {
86
119
  offsetArr.push({
87
120
  key: section.props.name,
88
- offsetTop: (sectionRootDom.children[index] as HTMLElement).offsetTop
121
+ offsetTop: (parentNode.children[index] as HTMLElement).offsetTop
89
122
  });
90
123
  }
91
124
  );
@@ -94,7 +127,7 @@ export class AnchorNav extends React.Component<AnchorNavProps, AnchorNavState> {
94
127
  {
95
128
  offsetArr
96
129
  },
97
- () => active && this.scrollToSection(active)
130
+ !inited ? () => active && this.scrollToSection(active) : undefined
98
131
  );
99
132
  }
100
133
 
@@ -276,7 +276,7 @@ export interface DateProps extends LocaleProps, ThemeProps {
276
276
  };
277
277
  };
278
278
  popOverContainer?: any;
279
-
279
+ label?: string | false;
280
280
  borderMode?: 'full' | 'half' | 'none';
281
281
  // 是否为内嵌模式,如果开启就不是 picker 了,直接页面点选。
282
282
  embed?: boolean;
@@ -657,7 +657,8 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
657
657
  largeMode,
658
658
  scheduleClassNames,
659
659
  onScheduleClick,
660
- mobileCalendarMode
660
+ mobileCalendarMode,
661
+ label
661
662
  } = this.props;
662
663
 
663
664
  const __ = this.props.translate;
@@ -687,7 +688,7 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
687
688
  );
688
689
  const CalendarMobileTitle = (
689
690
  <div className={`${ns}CalendarMobile-title`}>
690
- {__('Calendar.datepicker')}
691
+ {label && typeof label === 'string' ? label : __('Calendar.datepicker')}
691
692
  </div>
692
693
  );
693
694
  const useCalendarMobile =
@@ -788,7 +789,10 @@ export class DatePicker extends React.Component<DateProps, DatePickerState> {
788
789
  ) : null}
789
790
 
790
791
  <a className={cx(`DatePicker-toggler`)}>
791
- <Icon icon={viewMode === 'time' ? 'clock' : 'date'} className="icon" />
792
+ <Icon
793
+ icon={viewMode === 'time' ? 'clock' : 'date'}
794
+ className="icon"
795
+ />
792
796
  </a>
793
797
 
794
798
  {!(useMobileUI && isMobile()) && isOpened ? (
@@ -55,6 +55,7 @@ export interface DateRangePickerProps extends ThemeProps, LocaleProps {
55
55
  onBlur?: Function;
56
56
  type?: string;
57
57
  onRef?: any;
58
+ label?: string | false;
58
59
  }
59
60
 
60
61
  export interface DateRangePickerState {
@@ -62,6 +63,8 @@ export interface DateRangePickerState {
62
63
  isFocused: boolean;
63
64
  startDate?: moment.Moment;
64
65
  endDate?: moment.Moment;
66
+ oldStartDate?: moment.Moment;
67
+ oldEndDate?: moment.Moment;
65
68
  editState?: 'start' | 'end'; // 编辑开始时间还是结束时间
66
69
  startInputValue?: string;
67
70
  endInputValue?: string;
@@ -534,6 +537,8 @@ export class DateRangePicker extends React.Component<
534
537
  editState: 'start',
535
538
  startDate,
536
539
  endDate,
540
+ oldStartDate: startDate,
541
+ oldEndDate: endDate,
537
542
  startInputValue: startDate?.format(inputFormat),
538
543
  endInputValue: endDate?.format(inputFormat)
539
544
  };
@@ -645,7 +650,17 @@ export class DateRangePicker extends React.Component<
645
650
  });
646
651
  }
647
652
 
648
- close() {
653
+ close(isConfirm: boolean = false) {
654
+ if (!isConfirm) {
655
+ const {oldEndDate, oldStartDate} = this.state;
656
+ const {inputFormat} = this.props;
657
+ this.setState({
658
+ endDate: oldEndDate,
659
+ endInputValue: oldEndDate ? oldEndDate.format(inputFormat) : '',
660
+ startDate: oldStartDate,
661
+ startInputValue: oldStartDate ? oldStartDate.format(inputFormat) : ''
662
+ });
663
+ }
649
664
  this.setState(
650
665
  {
651
666
  isOpened: false,
@@ -693,7 +708,7 @@ export class DateRangePicker extends React.Component<
693
708
  if (this.state.startDate && !this.state.endDate) {
694
709
  this.setState({editState: 'end'});
695
710
  } else {
696
- this.close();
711
+ this.close(true);
697
712
  }
698
713
  }
699
714
 
@@ -746,6 +761,7 @@ export class DateRangePicker extends React.Component<
746
761
  );
747
762
  const newState = {
748
763
  startDate: date,
764
+ oldStartDate: startDate,
749
765
  startInputValue: date.format(inputFormat)
750
766
  } as any;
751
767
  // 这些没有时间的选择点第一次后第二次就是选结束时间
@@ -769,6 +785,7 @@ export class DateRangePicker extends React.Component<
769
785
  if (newValue.isBefore(startDate)) {
770
786
  this.setState({
771
787
  startDate: undefined,
788
+ oldStartDate: startDate,
772
789
  startInputValue: ''
773
790
  });
774
791
  }
@@ -777,6 +794,7 @@ export class DateRangePicker extends React.Component<
777
794
  this.setState(
778
795
  {
779
796
  endDate: date,
797
+ oldEndDate: endDate,
780
798
  endInputValue: date.format(inputFormat)
781
799
  },
782
800
  () => {
@@ -1226,7 +1244,8 @@ export class DateRangePicker extends React.Component<
1226
1244
  locale,
1227
1245
  embed,
1228
1246
  type,
1229
- viewMode = 'days'
1247
+ viewMode = 'days',
1248
+ useMobileUI
1230
1249
  } = this.props;
1231
1250
  const __ = this.props.translate;
1232
1251
 
@@ -1236,7 +1255,7 @@ export class DateRangePicker extends React.Component<
1236
1255
  const isTimeRange = type === 'input-datetime-range' || viewMode === 'time';
1237
1256
 
1238
1257
  return (
1239
- <div className={`${ns}DateRangePicker-wrap`} ref={this.calendarRef}>
1258
+ <div className={cx(`${ns}DateRangePicker-wrap`)} ref={this.calendarRef}>
1240
1259
  {this.renderRanges(ranges)}
1241
1260
  {(!isTimeRange || (editState === 'start' && !embed)) && (
1242
1261
  <Calendar
@@ -1299,7 +1318,10 @@ export class DateRangePicker extends React.Component<
1299
1318
 
1300
1319
  {embed ? null : (
1301
1320
  <div key="button" className={`${ns}DateRangePicker-actions`}>
1302
- <a className={cx('Button', 'Button--default')} onClick={this.close}>
1321
+ <a
1322
+ className={cx('Button', 'Button--default')}
1323
+ onClick={() => this.close}
1324
+ >
1303
1325
  {__('cancel')}
1304
1326
  </a>
1305
1327
  <a
@@ -1348,7 +1370,8 @@ export class DateRangePicker extends React.Component<
1348
1370
  maxDuration,
1349
1371
  dateFormat,
1350
1372
  viewMode = 'days',
1351
- ranges
1373
+ ranges,
1374
+ label
1352
1375
  } = this.props;
1353
1376
  const useCalendarMobile =
1354
1377
  useMobileUI &&
@@ -1400,7 +1423,7 @@ export class DateRangePicker extends React.Component<
1400
1423
 
1401
1424
  const CalendarMobileTitle = (
1402
1425
  <div className={`${ns}CalendarMobile-title`}>
1403
- {__('Calendar.datepicker')}
1426
+ {label && typeof label === 'string' ? label : __('Calendar.datepicker')}
1404
1427
  </div>
1405
1428
  );
1406
1429
 
@@ -1466,7 +1489,10 @@ export class DateRangePicker extends React.Component<
1466
1489
  <PopUp
1467
1490
  isShow={isOpened}
1468
1491
  container={popOverContainer}
1469
- className={cx(`${ns}CalendarMobile-pop`)}
1492
+ className={cx(
1493
+ `${ns}CalendarMobile-pop`,
1494
+ `${ns}CalendarMobile-pop--${viewMode}`
1495
+ )}
1470
1496
  onHide={this.close}
1471
1497
  header={CalendarMobileTitle}
1472
1498
  >
@@ -52,6 +52,7 @@ export interface MonthRangePickerProps extends ThemeProps, LocaleProps {
52
52
  useMobileUI?: boolean;
53
53
  onFocus?: Function;
54
54
  onBlur?: Function;
55
+ label?: string | false;
55
56
  }
56
57
 
57
58
  export interface MonthRangePickerState {
@@ -555,7 +556,8 @@ export class MonthRangePicker extends React.Component<
555
556
  maxDate,
556
557
  minDuration,
557
558
  maxDuration,
558
- ranges
559
+ ranges,
560
+ label
559
561
  } = this.props;
560
562
  const mobileUI = isMobile() && useMobileUI;
561
563
 
@@ -616,7 +618,7 @@ export class MonthRangePicker extends React.Component<
616
618
 
617
619
  const CalendarMobileTitle = (
618
620
  <div className={`${ns}CalendarMobile-title`}>
619
- {__('Calendar.datepicker')}
621
+ {label && typeof label === 'string' ? label : __('Calendar.datepicker')}
620
622
  </div>
621
623
  );
622
624
 
@@ -1,10 +1,31 @@
1
1
  import React from 'react';
2
+ import moment from 'moment';
3
+ import _ from 'lodash';
4
+ import debounce from 'lodash/debounce';
5
+ import isInteger from 'lodash/isInteger';
2
6
  import {ThemeProps, themeable} from '../theme';
3
7
  import {Icon} from './icons';
4
8
  import {uncontrollable} from 'uncontrollable';
5
9
  import {autobind} from '../utils/helper';
6
10
  import {LocaleProps, localeable} from '../locale';
7
- import debounce from 'lodash/debounce';
11
+
12
+ export interface HistoryRecord {
13
+ /** 历史记录值 */
14
+ value: string;
15
+ /** 历史记录生成的unix时间戳 */
16
+ timestamp?: number;
17
+ }
18
+
19
+ export interface SearchHistoryOptions {
20
+ /** 是否开启历史记录 */
21
+ enable: boolean;
22
+ /** 本地存储历史记录的key */
23
+ key?: string;
24
+ /** 历史记录数量上限 */
25
+ limit?: number;
26
+ /** 历史记录下拉面板CSS类名 */
27
+ dropdownClassName?: string;
28
+ }
8
29
 
9
30
  export interface SearchBoxProps extends ThemeProps, LocaleProps {
10
31
  name?: string;
@@ -22,29 +43,46 @@ export interface SearchBoxProps extends ThemeProps, LocaleProps {
22
43
  onActiveChange?: (active: boolean) => void;
23
44
  onSearch?: (value: string) => void;
24
45
  onCancel?: () => void;
46
+ /** 历史记录配置 */
47
+ history?: SearchHistoryOptions;
25
48
  }
26
49
 
27
50
  export interface SearchBoxState {
28
51
  isFocused: boolean;
52
+ isHistoryOpened: boolean;
53
+ inputValue: string;
54
+ historyRecords: HistoryRecord[];
29
55
  }
30
56
 
57
+ const historyDefaultOptions: Required<SearchHistoryOptions> = {
58
+ enable: false,
59
+ key: 'amis:search_history',
60
+ limit: 5,
61
+ dropdownClassName: ''
62
+ };
63
+
31
64
  export class SearchBox extends React.Component<SearchBoxProps, SearchBoxState> {
32
65
  inputRef: React.RefObject<HTMLInputElement> = React.createRef();
66
+
33
67
  static defaultProps = {
34
68
  mini: true,
35
69
  enhance: false,
36
70
  clearable: false,
37
- searchImediately: true
71
+ searchImediately: true,
72
+ history: historyDefaultOptions
38
73
  };
39
74
 
40
75
  state = {
41
- isFocused: false
76
+ isHistoryOpened: false,
77
+ isFocused: false,
78
+ inputValue: this.props.value ?? '',
79
+ historyRecords: this.getHistoryRecords()
42
80
  };
43
81
 
44
82
  lazyEmitSearch = debounce(
45
83
  () => {
46
84
  const onSearch = this.props.onSearch;
47
- onSearch?.(this.props.value || '');
85
+ onSearch?.(this.state.inputValue ?? '');
48
86
  },
49
87
  250,
50
88
  {
@@ -53,6 +91,12 @@ export class SearchBox extends React.Component<SearchBoxProps, SearchBoxState> {
53
91
  }
54
92
  );
55
93
 
94
+ componentDidUpdate(prevProps: SearchBoxProps) {
95
+ if (prevProps.value !== this.props.value) {
96
+ this.setState({inputValue: this.props.value ?? ''});
97
+ }
98
+ }
99
+
56
100
  componentWillUnmount() {
57
101
  this.lazyEmitSearch.cancel();
58
102
  }
@@ -67,22 +111,35 @@ export class SearchBox extends React.Component<SearchBoxProps, SearchBoxState> {
67
111
  @autobind
68
112
  handleCancel() {
69
113
  const {onActiveChange, onCancel, onChange} = this.props;
114
+
70
115
  onActiveChange?.(false);
71
116
  onCancel?.();
72
- onChange?.('');
117
+ this.setState({inputValue: ''}, () => onChange?.(''));
73
118
  }
74
119
 
75
120
  @autobind
76
121
  handleChange(e: React.ChangeEvent<HTMLInputElement>) {
77
122
  const {searchImediately, onChange} = this.props;
78
- onChange?.(e.currentTarget.value);
79
- searchImediately && this.lazyEmitSearch();
123
+ const inputValue = e.currentTarget.value;
124
+
125
+ this.setState({inputValue}, () => {
126
+ onChange?.(inputValue);
127
+ searchImediately && this.lazyEmitSearch();
128
+ });
80
129
  }
81
130
 
82
131
  @autobind
83
132
  handleSearch() {
84
- const {onSearch, value} = this.props;
85
- onSearch?.(value || '');
133
+ const {onSearch} = this.props;
134
+ const {inputValue} = this.state;
135
+ const {enable} = this.getHistoryOptions();
136
+
137
+ if (enable) {
138
+ this.insertHistoryRecord(inputValue);
139
+ this.setState({isFocused: false, isHistoryOpened: false});
140
+ }
141
+
142
+ onSearch?.(inputValue || '');
86
143
  }
87
144
 
88
145
  @autobind
@@ -96,11 +153,117 @@ export class SearchBox extends React.Component<SearchBoxProps, SearchBoxState> {
96
153
  @autobind
97
154
  handleClear() {
98
155
  const {searchImediately, onChange} = this.props;
99
- onChange?.('');
100
- searchImediately && this.lazyEmitSearch();
156
+
157
+ this.setState({inputValue: ''}, () => {
158
+ onChange?.('');
159
+ searchImediately && this.lazyEmitSearch();
160
+ });
101
161
  }
102
162
 
103
- render() {
163
+ @autobind
164
+ handleFocus() {
165
+ const {enable} = this.getHistoryOptions();
166
+ this.setState({isFocused: true, isHistoryOpened: enable});
167
+ }
168
+
169
+ @autobind
170
+ handleBlur(e: React.FocusEvent<HTMLInputElement>) {
171
+ this.setState({isFocused: false, isHistoryOpened: false});
172
+ }
173
+
174
+ handleHistoryRecordSelect(record: HistoryRecord) {
175
+ this.setState(
176
+ {inputValue: record.value, isHistoryOpened: false, isFocused: false},
177
+ () => this.handleSearch()
178
+ );
179
+ }
180
+
181
+ /** 获取历史搜索配置 */
182
+ getHistoryOptions(): Required<SearchHistoryOptions> {
183
+ const {history} = this.props;
184
+ const options = {
185
+ enable: !!history?.enable,
186
+ key: history?.key ?? historyDefaultOptions.key,
187
+ limit:
188
+ history?.limit && isInteger(history?.limit) && history?.limit > 0
189
+ ? history?.limit
190
+ : historyDefaultOptions.limit,
191
+ dropdownClassName: history?.dropdownClassName ?? ''
192
+ };
193
+
194
+ return options;
195
+ }
196
+
197
+ /** 获取历史记录 */
198
+ getHistoryRecords() {
199
+ const {key, limit} = this.getHistoryOptions();
200
+
201
+ try {
202
+ const storageValues = localStorage.getItem(key);
203
+
204
+ return _.chain(storageValues ? JSON.parse(storageValues) : [])
205
+ .uniqBy('value')
206
+ .orderBy(['timestamp'], ['desc'])
207
+ .slice(0, limit)
208
+ .value();
209
+ } catch {}
210
+
211
+ return [];
212
+ }
213
+
214
+ /** 清空历史记录 */
215
+ clearHistoryRecords(): HistoryRecord[] {
216
+ const {key} = this.getHistoryOptions();
217
+ localStorage.removeItem(key);
218
+ this.setState({historyRecords: []});
219
+
220
+ return [];
221
+ }
222
+
223
+ /** 删除一条历史记录 */
224
+ removeHistoryRecord(record: HistoryRecord): HistoryRecord[] {
225
+ const {key} = this.getHistoryOptions();
226
+ const datasource = this.getHistoryRecords();
227
+ const recordIndex = datasource.findIndex(
228
+ item => item.value === record.value
229
+ );
230
+
231
+ if (~recordIndex) {
232
+ datasource.splice(recordIndex, 1);
233
+ localStorage.setItem(key, JSON.stringify(datasource));
234
+ this.setState({historyRecords: datasource});
235
+ }
236
+
237
+ return datasource;
238
+ }
239
+
240
+ /** 新增一条历史记录 */
241
+ insertHistoryRecord(value: string): HistoryRecord[] {
242
+ const datasource = this.getHistoryRecords();
243
+
244
+ if (!value || datasource.find(item => item.value === value)) {
245
+ return datasource;
246
+ }
247
+
248
+ try {
249
+ const {key, limit} = this.getHistoryOptions();
250
+ const newDatasource = _.chain([
251
+ ...datasource,
252
+ {value, timestamp: moment().unix()}
253
+ ])
254
+ .orderBy(['timestamp'], ['desc'])
255
+ .slice(0, limit)
256
+ .value();
257
+
258
+ localStorage.setItem(key, JSON.stringify(newDatasource));
259
+ this.setState({historyRecords: newDatasource});
260
+ return newDatasource;
261
+ } catch {}
262
+
263
+ return datasource;
264
+ }
265
+
266
+ renderInput(isHistoryMode?: boolean) {
104
267
  const {
105
268
  classnames: cx,
106
269
  active,
@@ -111,43 +274,41 @@ export class SearchBox extends React.Component<SearchBoxProps, SearchBoxState> {
111
274
  mini,
112
275
  enhance,
113
276
  clearable,
114
- value,
115
277
  translate: __
116
278
  } = this.props;
117
-
118
- const isFocused = this.state.isFocused;
279
+ const {isFocused, inputValue} = this.state;
280
+ const {enable} = this.getHistoryOptions();
119
281
 
120
282
  return (
121
283
  <div
122
284
  className={cx(
123
285
  'SearchBox',
124
286
  enhance && 'SearchBox--enhance',
125
- className,
287
+ !!isHistoryMode ? '' : className,
126
288
  disabled ? 'is-disabled' : '',
127
289
  isFocused ? 'is-focused' : '',
128
290
  !mini || active ? 'is-active' : '',
291
+ {'is-history': enable}
129
292
  )}
130
293
  >
131
294
  <input
132
295
  name={name}
133
- disabled={disabled}
296
+ ref={this.inputRef}
297
+ onFocus={this.handleFocus}
298
+ onBlur={this.handleBlur}
134
299
  onChange={this.handleChange}
135
- value={value || ''}
300
+ onKeyDown={this.handleKeyDown}
301
+ value={inputValue ?? ''}
302
+ disabled={disabled}
136
303
  placeholder={__(placeholder || 'placeholder.enter')}
137
- ref={this.inputRef}
138
304
  autoComplete="off"
139
- onFocus={() => this.setState({ isFocused: true })}
140
- onBlur={() => this.setState({ isFocused: false })}
141
- onKeyDown={this.handleKeyDown}
142
305
  />
143
306
 
144
- {
145
- !mini && clearable && value && !disabled ? (
146
- <div className={cx('SearchBox-clearable')} onClick={this.handleClear}>
147
- <Icon icon="input-clear" className="icon"/>
148
- </div>
149
- ) : null
150
- }
307
+ {!mini && clearable && inputValue && !disabled ? (
308
+ <div className={cx('SearchBox-clearable')} onClick={this.handleClear}>
309
+ <Icon icon="input-clear" className="icon" />
310
+ </div>
311
+ ) : null}
151
312
 
152
313
  {!mini ? (
153
314
  <a className={cx('SearchBox-searchBtn')} onClick={this.handleSearch}>
@@ -165,6 +326,78 @@ export class SearchBox extends React.Component<SearchBoxProps, SearchBoxState> {
165
326
  </div>
166
327
  );
167
328
  }
329
+
330
+ renderTag(item: HistoryRecord, index: number) {
331
+ const {classnames: cx} = this.props;
332
+
333
+ return (
334
+ <span className={cx('Tag', 'SearchBox-history-tag')} key={index}>
335
+ <span
336
+ className={cx('SearchBox-history-tag-text')}
337
+ onMouseDown={(e: React.MouseEvent<any>) => {
338
+ e.preventDefault();
339
+ this.handleHistoryRecordSelect(item);
340
+ }}
341
+ >
342
+ {item.value}
343
+ </span>
344
+ <span
345
+ className={cx(`SearchBox-history-tag-close`)}
346
+ onMouseDown={(e: React.MouseEvent<any>) => {
347
+ e.preventDefault();
348
+ this.removeHistoryRecord(item);
349
+ }}
350
+ >
351
+ <Icon icon="close" className="icon" />
352
+ </span>
353
+ </span>
354
+ );
355
+ }
356
+
357
+ renderHitoryMode() {
358
+ const {classnames: cx, translate: __, className} = this.props;
359
+ const {isHistoryOpened, inputValue, historyRecords} = this.state;
360
+ const {dropdownClassName} = this.getHistoryOptions();
361
+ const showDropdown =
362
+ isHistoryOpened && !inputValue && historyRecords.length > 0;
363
+
364
+ return (
365
+ <div
366
+ id="searchbox-history"
367
+ className={cx('SearchBox-history', className)}
368
+ >
369
+ {this.renderInput(true)}
370
+
371
+ <div
372
+ className={cx('SearchBox-history-dropdown', dropdownClassName, {
373
+ 'is-active': showDropdown
374
+ })}
375
+ >
376
+ <header>
377
+ <h4>{__('searchHistory')}</h4>
378
+ <a
379
+ onMouseDown={(e: React.MouseEvent<any>) => {
380
+ e.preventDefault();
381
+ this.clearHistoryRecords();
382
+ }}
383
+ >
384
+ {__('clear')}
385
+ </a>
386
+ </header>
387
+
388
+ <div className={cx('SearchBox-history-content')}>
389
+ {historyRecords.map((item, index) => this.renderTag(item, index))}
390
+ </div>
391
+ </div>
392
+ </div>
393
+ );
394
+ }
395
+
396
+ render() {
397
+ const {enable} = this.getHistoryOptions();
398
+
399
+ return enable ? this.renderHitoryMode() : this.renderInput();
400
+ }
168
401
  }
169
402
 
170
403
  export default themeable(