@ticatec/uniface-element 0.3.21 → 0.3.23

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.
@@ -49,28 +49,32 @@
49
49
  }
50
50
 
51
51
  const lazyLoad = async (searchText: string, page: number, isLoadMore: boolean = false) => {
52
+ console.log('lazyLoad called: searchText=', searchText, 'page=', page);
53
+
52
54
  // 取消之前的加载请求
53
55
  if (loadingController) {
54
56
  loadingController.abort();
55
57
  }
56
-
58
+
57
59
  // 创建新的AbortController
58
60
  loadingController = new AbortController();
59
61
  const currentController = loadingController;
60
-
62
+
61
63
  loading = true;
62
64
  if (!isLoadMore) {
63
65
  isBusy = true;
64
66
  }
65
-
67
+
66
68
  try {
67
69
  let res = await lazyLoader(searchText, page);
68
-
70
+
71
+ console.log('lazyLoad result:', res);
72
+
69
73
  // 检查请求是否被取消
70
74
  if (currentController.signal.aborted) {
71
75
  return;
72
76
  }
73
-
77
+
74
78
  if (page == 1) {
75
79
  options = res.list;
76
80
  } else {
@@ -99,11 +103,14 @@
99
103
  }
100
104
 
101
105
  const delayReload = () => {
106
+ const searchText = inputText; // 直接使用 inputText,避免响应式更新延迟问题
107
+ console.log('delayReload: searchText=', searchText);
108
+
102
109
  // 取消之前的延迟加载
103
110
  if (interval) {
104
111
  clearInterval(interval);
105
112
  }
106
-
113
+
107
114
  // 取消当前正在进行的加载
108
115
  if (loadingController) {
109
116
  loadingController.abort();
@@ -111,18 +118,21 @@
111
118
  loading = false;
112
119
  isBusy = false;
113
120
  }
114
-
121
+
115
122
  // 重置状态
116
123
  options = [];
117
124
  hasMore = false;
118
-
125
+
119
126
  // 设置新的延迟加载
120
127
  idleTime = 0;
121
128
  interval = setInterval(() => {
122
129
  idleTime += 100;
123
130
  if (idleTime >= 200) {
124
131
  clearInterval(interval);
125
- lazyLoad(text, 1, false);
132
+ console.log('delayReload firing: searchText=', searchText, 'length=', searchText?.length);
133
+ if (searchText && searchText.length > 0) {
134
+ lazyLoad(searchText, 1, false);
135
+ }
126
136
  }
127
137
  }, 100);
128
138
  }
@@ -165,6 +175,7 @@
165
175
  let isUserTyping: boolean = false;
166
176
 
167
177
  const handleInput = () => {
178
+ console.log('handleInput: inputText=', inputText, 'text=', text);
168
179
  isUserTyping = true;
169
180
  if (!composing) {
170
181
  text = inputText;
@@ -244,9 +255,11 @@
244
255
  }
245
256
 
246
257
 
247
- $: if (!showPopup && value == null && !isUserTyping) {
248
- text = '';
249
- }
258
+ // 注释掉自动清空text的逻辑,因为它会与用户输入冲突
259
+ // $: if (!showPopup && value == null && !isUserTyping) {
260
+ // console.log('clearing text: showPopup=', showPopup, 'value=', value, 'isUserTyping=', isUserTyping);
261
+ // text = '';
262
+ // }
250
263
 
251
264
  // 监听popup关闭,如果是点击外部关闭且之前有焦点,触发blur
252
265
  $: if (!showPopup && hasFocus) {
@@ -3,8 +3,8 @@
3
3
  import {DisplayMode} from "../common/DisplayMode";
4
4
  import type {OnChangeHandler} from "../common/OnChangeHandler";
5
5
  import CommonPicker from "../common/CommonPicker.svelte";
6
- import {tick} from "svelte";
7
6
  import type {OnSelectedHandler} from "../common/OnSelectedHandler";
7
+ import SearchBox from "../search-box/SearchBox.svelte";
8
8
 
9
9
  export let variant: '' | 'plain' | 'outlined' | 'filled' = '';
10
10
  export let disabled: boolean = false;
@@ -28,9 +28,35 @@
28
28
  export let onblur: (() => void) | null = null;
29
29
  export let onSelected: OnSelectedHandler = null as unknown as OnSelectedHandler;
30
30
 
31
+ // Quick filter properties
32
+ export let quickFilter: boolean = false;
33
+ export let quickFilterPlaceholder: string = "Search...";
34
+ export let filterFunction: ((option: any, keyword: string) => boolean) | null = null;
35
+ export let noResultsText: string = "No results found";
36
+
37
+ let searchKeyword: string = '';
38
+ let searchBox: any;
39
+
40
+ const handleSearchInput = (value: string | null) => {
41
+ searchKeyword = value ?? '';
42
+ filteredOptions = getFilteredOptions();
43
+ };
44
+
45
+
46
+ const generateText = (v: string): string => {
47
+ let item = (options || []).find(el => el[keyField] == value);
48
+ return item == null ? (emptyText ?? '') : item[textField];
49
+ }
50
+
51
+ const defaultFilterFunction = (option: any, keyword: string): boolean => {
52
+ if (!keyword || keyword.length === 0) return true;
53
+ const text = String(option[textField] || '').toLowerCase();
54
+ return text.includes(keyword.toLowerCase());
55
+ }
56
+
31
57
 
32
58
  export const setFocus = () => {
33
- setTimeout(()=> {
59
+ setTimeout(() => {
34
60
  editor && editor.focus();
35
61
  }, 50);
36
62
  }
@@ -40,12 +66,21 @@
40
66
  let oldValue = value;
41
67
  let showPopup: boolean = false;
42
68
  let mh;
43
- let popupContainer: HTMLElement;
44
69
  let hasFocus: boolean = false;
45
70
 
46
- const generateText = (v: string): string => {
47
- let item = (options || []).find(el => el[keyField] == value);
48
- return item == null ? (emptyText ?? '') : item[textField];
71
+ const getFilteredOptions = () => {
72
+ if (searchKeyword.trim() == '') {
73
+ return [...options]
74
+ } else {
75
+ const filterFn = filterFunction || defaultFilterFunction;
76
+ return (options || []).filter(op => filterFn(op, searchKeyword));
77
+ }
78
+ }
79
+
80
+ const focusSearchBox = () => {
81
+ setTimeout(() => {
82
+ searchBox?.focus();
83
+ }, 50);
49
84
  }
50
85
 
51
86
  const openPopup = () => {
@@ -67,6 +102,8 @@
67
102
 
68
103
  $: textValue = generateText(value);
69
104
 
105
+ $: filteredOptions = quickFilter ? getFilteredOptions() : (options || []);
106
+
70
107
  $: mh = `height: ${menu$height > 0 ? menu$height + 'px' : 'auto'}`;
71
108
 
72
109
  $: if (value != oldValue) {
@@ -75,7 +112,11 @@
75
112
  }
76
113
 
77
114
  $: if (showPopup) {
78
- editor.focus();
115
+ if (quickFilter) {
116
+ focusSearchBox();
117
+ } else {
118
+ editor.focus();
119
+ }
79
120
  }
80
121
 
81
122
  // 监听popup关闭,如果是点击外部关闭且之前有焦点,触发blur
@@ -90,7 +131,7 @@
90
131
  }
91
132
 
92
133
  const handleKeyDown = (event: KeyboardEvent) => {
93
- if (event.key=='Tab') {
134
+ if (event.key == 'Tab') {
94
135
  showPopup = false;
95
136
  }
96
137
  }
@@ -111,6 +152,7 @@
111
152
  // 如果popup打开,不触发blur,让popup关闭时处理
112
153
  }
113
154
 
155
+ let filterText: string | null = null;
114
156
  </script>
115
157
 
116
158
  <CommonPicker {displayMode} {variant} {style} {compact} dropDownIcon="icon_google_arrow_drop_down"
@@ -119,19 +161,36 @@
119
161
  <input style="width: 100%" bind:this={editor} on:click={openPopup} {placeholder} readonly
120
162
  class="text-editor" bind:value={textValue} on:blur={handleBlur} on:focus={handleFocus} on:keydown={handleKeyDown}/>
121
163
 
122
- <div class="options-popover" style={mh} slot="popup-panel" bind:this={popupContainer}>
123
- {#each options as op}
124
- {#if hideOptions.indexOf(op[keyField]) < 0}
125
- {#if itemRender != null}
126
- <svelte:component this={itemRender.component} {...itemRender.props} item={op} disabled={disableOptions.indexOf(op[keyField])>-1}
127
- on:click={handleItemClick(op)}/>
128
- {:else}
129
- <div class="option-item" class:disabled={disableOptions.indexOf(op[keyField])>-1} aria-hidden="true"
130
- on:click={handleItemClick(op)}>
131
- <span>{op[textField]}</span>
132
- </div>
164
+ <div class="options-popover" style={mh} slot="popup-panel">
165
+ {#if quickFilter}
166
+ <div class="quick-filter-box">
167
+ <SearchBox
168
+ bind:value={filterText}
169
+ bind:this={searchBox}
170
+ oninput={handleSearchInput}
171
+ placeholder={quickFilterPlaceholder}
172
+ />
173
+ </div>
174
+ {/if}
175
+ {#if filteredOptions.length === 0}
176
+ <div class="no-results">
177
+ <span>{noResultsText}</span>
178
+ </div>
179
+ {:else}
180
+ {#each filteredOptions as op}
181
+ {#if hideOptions.indexOf(op[keyField]) < 0}
182
+ {#if itemRender != null}
183
+ <svelte:component this={itemRender.component} {...itemRender.props} item={op}
184
+ disabled={disableOptions.indexOf(op[keyField])>-1}
185
+ on:click={handleItemClick(op)}/>
186
+ {:else}
187
+ <div class="option-item" class:disabled={disableOptions.indexOf(op[keyField])>-1} aria-hidden="true"
188
+ on:click={handleItemClick(op)}>
189
+ <span>{op[textField]}</span>
190
+ </div>
191
+ {/if}
133
192
  {/if}
134
- {/if}
135
- {/each}
193
+ {/each}
194
+ {/if}
136
195
  </div>
137
196
  </CommonPicker>
@@ -36,6 +36,10 @@ declare const OptionsSelect: $$__sveltets_2_IsomorphicComponent<{
36
36
  onfocus?: (() => void) | null;
37
37
  onblur?: (() => void) | null;
38
38
  onSelected?: OnSelectedHandler;
39
+ quickFilter?: boolean;
40
+ quickFilterPlaceholder?: string;
41
+ filterFunction?: ((option: any, keyword: string) => boolean) | null;
42
+ noResultsText?: string;
39
43
  setFocus?: () => void;
40
44
  }, {
41
45
  [evt: string]: CustomEvent<any>;
@@ -9,7 +9,7 @@ A single-selection dropdown component that allows users to select one option fro
9
9
  - **Option Control**: Hide or disable specific options
10
10
  - **Custom Rendering**: Use custom components to render dropdown items
11
11
  - **Mandatory Mode**: Prevent clearing of selected values
12
- - **Search Integration**: Works with CommonPicker for consistent behavior
12
+ - **Quick Filter**: Built-in search/filter functionality for large option lists
13
13
  - **Focus Management**: Proper focus handling and keyboard navigation
14
14
  - **Display Modes**: Support for edit and view modes
15
15
  - **Empty State Handling**: Customizable placeholder and empty text
@@ -64,6 +64,10 @@ npm install @ticatec/uniface-element
64
64
  | `displayMode` | `DisplayMode` | `DisplayMode.Edit` | Edit or view mode |
65
65
  | `menu$height` | `number` | `0` | Maximum height of dropdown menu |
66
66
  | `itemRender` | `any` | `null` | Custom component for rendering items |
67
+ | `quickFilter` | `boolean` | `false` | Enable quick filter/search in dropdown |
68
+ | `quickFilterPlaceholder` | `string` | `'Search...'` | Placeholder text for search input |
69
+ | `filterFunction` | `(option: any, keyword: string) => boolean` | `null` | Custom filter function |
70
+ | `noResultsText` | `string` | `'No results found'` | Text when no options match filter |
67
71
  | `onchange` | `OnChangeHandler<any>` | `null` | Change event handler |
68
72
  | `onfocus` | `(() => void) \| null` | `null` | Focus event handler |
69
73
  | `onblur` | `(() => void) \| null` | `null` | Blur event handler |
@@ -183,11 +187,11 @@ npm install @ticatec/uniface-element
183
187
  let preferredLanguage = null;
184
188
  let languages = [
185
189
  { locale: 'en', display: 'English' },
186
- { locale: 'es', display: 'Espa�ol' },
187
- { locale: 'fr', display: 'Fran�ais' },
190
+ { locale: 'es', display: 'Espa�ol' },
191
+ { locale: 'fr', display: 'Fran�ais' },
188
192
  { locale: 'de', display: 'Deutsch' },
189
- { locale: 'zh', display: '-�' },
190
- { locale: 'ja', display: '�,�' }
193
+ { locale: 'zh', display: '-�' },
194
+ { locale: 'ja', display: '�,�' }
191
195
  ];
192
196
 
193
197
  function handleLanguageChange(locale) {
@@ -437,11 +441,78 @@ npm install @ticatec/uniface-element
437
441
  </table>
438
442
  ```
439
443
 
440
- ### Dynamic Options Loading
444
+ ### Quick Filter for Large Lists
445
+
446
+ When dealing with many options, enable the built-in quick filter:
441
447
 
442
448
  ```svelte
443
449
  <script>
444
450
  import OptionsSelect from '@ticatec/uniface-element/OptionsSelect';
451
+
452
+ let selectedCountry = null;
453
+ let countries = [
454
+ { code: 'US', name: 'United States' },
455
+ { code: 'CA', name: 'Canada' },
456
+ { code: 'UK', name: 'United Kingdom' },
457
+ { code: 'DE', name: 'Germany' },
458
+ { code: 'FR', name: 'France' },
459
+ { code: 'JP', name: 'Japan' },
460
+ { code: 'CN', name: 'China' },
461
+ { code: 'AU', name: 'Australia' },
462
+ // ... many more countries
463
+ ];
464
+ </script>
465
+
466
+ <OptionsSelect
467
+ options={countries}
468
+ keyField="code"
469
+ textField="name"
470
+ bind:value={selectedCountry}
471
+ variant="outlined"
472
+ placeholder="Select country"
473
+ quickFilter={true}
474
+ quickFilterPlaceholder="Type to search countries..."
475
+ />
476
+ ```
477
+
478
+ ### Custom Filter Function
479
+
480
+ Provide a custom filter function for advanced filtering:
481
+
482
+ ```svelte
483
+ <script>
484
+ import OptionsSelect from '@ticatec/uniface-element/OptionsSelect';
485
+
486
+ let selectedProduct = null;
487
+ let products = [
488
+ { sku: 'LAPTOP-001', name: 'Laptop', category: 'Electronics' },
489
+ { sku: 'PHONE-002', name: 'Smartphone', category: 'Electronics' },
490
+ { sku: 'SHIRT-003', name: 'T-Shirt', category: 'Clothing' },
491
+ { sku: 'PANTS-004', name: 'Jeans', category: 'Clothing' }
492
+ ];
493
+
494
+ const productFilter = (option, keyword) => {
495
+ if (!keyword) return true;
496
+ const search = keyword.toLowerCase();
497
+ return option.name.toLowerCase().includes(search) ||
498
+ option.sku.toLowerCase().includes(search) ||
499
+ option.category.toLowerCase().includes(search);
500
+ };
501
+ </script>
502
+
503
+ <OptionsSelect
504
+ options={products}
505
+ keyField="sku"
506
+ textField="name"
507
+ bind:value={selectedProduct}
508
+ quickFilter={true}
509
+ filterFunction={productFilter}
510
+ quickFilterPlaceholder="Search products..."
511
+ noResultsText="No matching products found"
512
+ />
513
+ ```
514
+
515
+ ### Dynamic Options Loading
445
516
 
446
517
  let selectedCategory = null;
447
518
  let selectedProduct = null;
@@ -531,7 +602,7 @@ You can provide a custom component to render dropdown items:
531
602
  </script>
532
603
 
533
604
  <div class="custom-item" class:disabled on:click={handleClick}>
534
- <div class="item-icon"><�</div>
605
+ <div class="item-icon"><�</div>
535
606
  <div class="item-content">
536
607
  <div class="item-title">{item.title}</div>
537
608
  <div class="item-description">{item.description}</div>
@@ -9,7 +9,7 @@
9
9
  - **选项控制**:隐藏或禁用特定选项
10
10
  - **自定义渲染**:使用自定义组件渲染下拉项
11
11
  - **必填模式**:防止清除已选值
12
- - **搜索集成**:与 CommonPicker 配合使用,提供一致的行为
12
+ - **快速过滤**:内置搜索/过滤功能,适用于大量选项
13
13
  - **焦点管理**:适当的焦点处理和键盘导航
14
14
  - **显示模式**:支持编辑和查看模式
15
15
  - **空状态处理**:可自定义的占位符和空文本
@@ -64,6 +64,10 @@ npm install @ticatec/uniface-element
64
64
  | `displayMode` | `DisplayMode` | `DisplayMode.Edit` | 编辑或查看模式 |
65
65
  | `menu$height` | `number` | `0` | 下拉菜单的最大高度 |
66
66
  | `itemRender` | `any` | `null` | 用于渲染选项的自定义组件 |
67
+ | `quickFilter` | `boolean` | `false` | 启用快速过滤/搜索 |
68
+ | `quickFilterPlaceholder` | `string` | `'Search...'` | 搜索输入框的占位符文本 |
69
+ | `filterFunction` | `(option: any, keyword: string) => boolean` | `null` | 自定义过滤函数 |
70
+ | `noResultsText` | `string` | `'No results found'` | 无匹配结果时显示的文本 |
67
71
  | `onchange` | `OnChangeHandler<any>` | `null` | 更改事件处理程序 |
68
72
  | `onfocus` | `(() => void) \| null` | `null` | 聚焦事件处理程序 |
69
73
  | `onblur` | `(() => void) \| null` | `null` | 失焦事件处理程序 |
@@ -437,6 +441,77 @@ npm install @ticatec/uniface-element
437
441
  </table>
438
442
  ```
439
443
 
444
+ ### 快速过滤(大量选项)
445
+
446
+ 当选项很多时,可以启用内置的快速过滤功能:
447
+
448
+ ```svelte
449
+ <script>
450
+ import OptionsSelect from '@ticatec/uniface-element/OptionsSelect';
451
+
452
+ let selectedCountry = null;
453
+ let countries = [
454
+ { code: 'US', name: '美国' },
455
+ { code: 'CA', name: '加拿大' },
456
+ { code: 'UK', name: '英国' },
457
+ { code: 'DE', name: '德国' },
458
+ { code: 'FR', name: '法国' },
459
+ { code: 'JP', name: '日本' },
460
+ { code: 'CN', name: '中国' },
461
+ { code: 'AU', name: '澳大利亚' },
462
+ // ... 更多国家
463
+ ];
464
+ </script>
465
+
466
+ <OptionsSelect
467
+ options={countries}
468
+ keyField="code"
469
+ textField="name"
470
+ bind:value={selectedCountry}
471
+ variant="outlined"
472
+ placeholder="选择国家"
473
+ quickFilter={true}
474
+ quickFilterPlaceholder="输入搜索国家..."
475
+ />
476
+ ```
477
+
478
+ ### 自定义过滤函数
479
+
480
+ 提供自定义过滤函数以实现高级过滤:
481
+
482
+ ```svelte
483
+ <script>
484
+ import OptionsSelect from '@ticatec/uniface-element/OptionsSelect';
485
+
486
+ let selectedProduct = null;
487
+ let products = [
488
+ { sku: 'LAPTOP-001', name: '笔记本电脑', category: '电子产品' },
489
+ { sku: 'PHONE-002', name: '智能手机', category: '电子产品' },
490
+ { sku: 'SHIRT-003', name: 'T恤', category: '服装' },
491
+ { sku: 'PANTS-004', name: '牛仔裤', category: '服装' }
492
+ ];
493
+
494
+ const productFilter = (option, keyword) => {
495
+ if (!keyword) return true;
496
+ const search = keyword.toLowerCase();
497
+ return option.name.toLowerCase().includes(search) ||
498
+ option.sku.toLowerCase().includes(search) ||
499
+ option.category.toLowerCase().includes(search);
500
+ };
501
+ </script>
502
+
503
+ <OptionsSelect
504
+ options={products}
505
+ keyField="sku"
506
+ textField="name"
507
+ bind:value={selectedProduct}
508
+ quickFilter={true}
509
+ filterFunction={productFilter}
510
+ quickFilterPlaceholder="搜索产品..."
511
+ noResultsText="未找到匹配的产品"
512
+ />
513
+ ```
514
+
440
515
  ### 动态选项加载
441
516
 
442
517
  ```svelte
@@ -1125,6 +1125,19 @@
1125
1125
  width: 20px;
1126
1126
  }
1127
1127
 
1128
+ .options-popover .quick-filter-box {
1129
+ position: sticky;
1130
+ top: 0;
1131
+ background: white;
1132
+ z-index: 1;
1133
+ }
1134
+ .options-popover .no-results {
1135
+ padding: 12px 16px;
1136
+ text-align: center;
1137
+ color: #9e9e9e;
1138
+ font-size: 14px;
1139
+ }
1140
+
1128
1141
  .uniface-radio-button {
1129
1142
  display: inline-block;
1130
1143
  height: 26px;
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@ticatec/uniface-element",
3
- "version": "0.3.21",
3
+ "version": "0.3.23",
4
4
  "description": "A comprehensive UI component library for Svelte applications with rich form controls, data tables, layouts and interactive elements",
5
5
  "keywords": [
6
6
  "svelte",
7
- "svelte5",
7
+ "svelte5",
8
8
  "ui",
9
9
  "components",
10
10
  "form",
@@ -381,6 +381,9 @@
381
381
  "@sveltejs/kit": "^2.60.1",
382
382
  "@sveltejs/package": "^2.5.7",
383
383
  "@sveltejs/vite-plugin-svelte": "^7.1.2",
384
+ "@ticatec/i18n": "^0.2.4",
385
+ "@ticatec/uniface-google-material-icons": "^0.1.8",
386
+ "dayjs": "^1.11.21",
384
387
  "eslint": "^10.4.0",
385
388
  "eslint-config-prettier": "^10.1.8",
386
389
  "eslint-plugin-svelte": "^3.17.1",
@@ -391,6 +394,7 @@
391
394
  "sass": "^1.99.0",
392
395
  "svelte": "^5.55.9",
393
396
  "svelte-check": "^4.4.8",
397
+ "svelte-portal": "^2.2.1",
394
398
  "svelte-preprocess": "^6.0.3",
395
399
  "tslib": "^2.8.1",
396
400
  "typescript": "^5.7.3",