Skip to content

Commit b56cf29

Browse files
committed
!26 merge xingyan/fix-category-search into dev
feat(CategorySearch): 支持配置是否自动聚焦搜索框 & 选中项较多时支持折叠 & 输入框筛选点确定触发校验 & 解决Tag组件titleContent参数未响应式变更的问题 Created-by: xingyan Commit-by: xingyan Merged-by: GreatZP Description: feat(CategorySearch): 支持配置是否自动聚焦搜索框 See merge request: DevCloudFE/vue-devui!26
2 parents 4615813 + b9c4bfd commit b56cf29

12 files changed

Lines changed: 261 additions & 82 deletions

File tree

packages/devui-vue/devui/category-search/src/category-search-types.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ExtractPropTypes, PropType, InjectionKey, SetupContext, Ref } from 'vue';
2+
import type { PopoverProps } from '../../popover';
23

34
export type CategorySearchTagType = 'radio' | 'checkbox' | 'label' | 'textInput' | 'numberRange' | 'keyword';
45
export type StyleType = 'default' | 'gray';
@@ -131,6 +132,9 @@ export interface ExtendConfig {
131132
export interface ITagContext {
132133
toggle: (status?: boolean) => void;
133134
}
135+
export interface SelectedCategoryEvent {
136+
tag: ICategorySearchTagItem;
137+
}
134138

135139
export const categorySearchProps = {
136140
category: {
@@ -190,7 +194,7 @@ export const categorySearchProps = {
190194
filterTitle: '',
191195
labelConnector: '|',
192196
noCategoriesAvailable: '',
193-
tagMenuEmpty: ''
197+
tagMenuEmpty: '',
194198
}),
195199
},
196200
extendConfig: {
@@ -202,8 +206,19 @@ export const categorySearchProps = {
202206
},
203207
appendToBody: {
204208
type: Boolean,
205-
default: true
206-
}
209+
default: true,
210+
},
211+
inputAutofocus: {
212+
type: Boolean,
213+
default: true,
214+
},
215+
collapseTags: {
216+
type: [Boolean, Number],
217+
default: false,
218+
},
219+
collapseTagsTooltip: {
220+
type: Object as PropType<PopoverProps>,
221+
},
207222
};
208223
export type CategorySearchProps = ExtractPropTypes<typeof categorySearchProps>;
209224

packages/devui-vue/devui/category-search/src/category-search.tsx

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ import CategorySearchMore from './components/category-search-more';
99
import { categorySearchProps } from './category-search-types';
1010
import type { CategorySearchProps } from './category-search-types';
1111
import { useCategorySearch } from './composables/use-category-search';
12+
import { useCategorySelectedTags } from './composables/use-category-selected-tags';
13+
import { Popover } from '../../popover';
14+
import { Tag } from '../../tag';
1215
import './category-search.scss';
1316

1417
export default defineComponent({
1518
name: 'DCategorySearch',
1619
props: categorySearchProps,
17-
emits: ['search', 'selectedTagsChange', 'createFilter', 'clearAll', 'searchKeyChange'],
20+
emits: ['search', 'selectedTagsChange', 'selectedCategory', 'createFilter', 'clearAll', 'searchKeyChange'],
1821
setup(props: CategorySearchProps, ctx: SetupContext) {
1922
const {
2023
rootRef,
@@ -28,6 +31,7 @@ export default defineComponent({
2831
operationConfig,
2932
onSearch,
3033
} = useCategorySearch(props, ctx);
34+
const { isCollapseTags, collapseTagsCount, popoverConfig } = useCategorySelectedTags(props);
3135

3236
return () => (
3337
<div
@@ -42,9 +46,31 @@ export default defineComponent({
4246
)}
4347
<div ref={scrollBarRef} class="dp-category-search-line-container">
4448
<ul class="dp-category-search-line">
45-
{innerSelectedTags.value.map((item) => (
46-
<CategorySearchTagDropdown item={item} isJoinLabelType={joinLabelTypes.includes(item.type || '')} />
47-
))}
49+
{innerSelectedTags.value.map((item, index) => {
50+
if (isCollapseTags.value) {
51+
return (
52+
index < collapseTagsCount.value && (
53+
<CategorySearchTagDropdown item={item} isJoinLabelType={joinLabelTypes.includes(item.type || '')} />
54+
)
55+
);
56+
} else {
57+
return <CategorySearchTagDropdown item={item} isJoinLabelType={joinLabelTypes.includes(item.type || '')} />;
58+
}
59+
})}
60+
{isCollapseTags.value && innerSelectedTags.value.length > collapseTagsCount.value && (
61+
<Popover auto-update-position {...popoverConfig.value}>
62+
{{
63+
default: () => <Tag deletable={false}>{`+${innerSelectedTags.value.length - collapseTagsCount.value}`}</Tag>,
64+
content: () =>
65+
innerSelectedTags.value.map(
66+
(item, index) =>
67+
index >= collapseTagsCount.value && (
68+
<CategorySearchTagDropdown item={item} isJoinLabelType={joinLabelTypes.includes(item.type || '')} />
69+
)
70+
),
71+
}}
72+
</Popover>
73+
)}
4874
<CategorySearchInput ref={inputRef} />
4975
</ul>
5076
</div>
Lines changed: 43 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,50 @@
1-
import { defineComponent, toRefs, inject, reactive } from 'vue';
1+
import { defineComponent, inject, reactive, ref } from 'vue';
22
import type { SetupContext } from 'vue';
33
import { Button } from '../../../button';
44
import { typeMenuProps, categorySearchInjectionKey } from '../category-search-types';
55
import type { TypeMenuProps, CategorySearchInjection } from '../category-search-types';
66

77
export default defineComponent({
8-
name: 'DCategorySearchTextInput',
9-
props: typeMenuProps,
10-
emits: ['close'],
11-
setup(props: TypeMenuProps, ctx: SetupContext) {
12-
const { tag } = toRefs(props);
13-
const { getTextInputValue } = inject(categorySearchInjectionKey) as CategorySearchInjection;
14-
const formData = reactive({
15-
text: tag.value.value!.value,
16-
})
17-
const onConfirmClick = () => {
18-
getTextInputValue(tag.value, formData.text as string);
19-
ctx.emit('close');
20-
};
21-
const onCancelClick = () => {
22-
ctx.emit('close');
23-
};
8+
name: 'DCategorySearchTextInput',
9+
props: typeMenuProps,
10+
emits: ['close'],
11+
setup(props: TypeMenuProps, ctx: SetupContext) {
12+
const formEl = ref();
13+
const { getTextInputValue } = inject(categorySearchInjectionKey) as CategorySearchInjection;
14+
const formData = reactive({
15+
text: props.tag.value!.value,
16+
});
17+
const onConfirmClick = () => {
18+
formEl.value.validate((isValid: boolean) => {
19+
if (isValid) {
20+
getTextInputValue(props.tag, formData.text as string);
21+
ctx.emit('close');
22+
}
23+
});
24+
};
25+
const onCancelClick = () => {
26+
ctx.emit('close');
27+
};
2428

25-
return () => (
26-
<d-form data={formData} pop-position={['right']}>
27-
<d-form-item field='text' rules={tag.value.validateRules}>
28-
<d-input
29-
v-model={formData.text}
30-
autocomplete='off'
31-
autofocus
32-
maxlength={tag.value.maxLength}
33-
placeholder={tag.value.placeholder || ''}></d-input>
34-
</d-form-item>
35-
<div class='dp-dropdown-operation-area'>
36-
<Button variant='solid' onClick={onConfirmClick}>
37-
确定
38-
</Button>
39-
<Button variant='solid' color='secondary' onClick={onCancelClick}>
40-
取消
41-
</Button>
42-
</div>
43-
</d-form>
44-
);
45-
},
46-
});
29+
return () => (
30+
<d-form ref={formEl} data={formData} pop-position={['right']}>
31+
<d-form-item field="text" rules={props.tag.validateRules}>
32+
<d-input
33+
v-model={formData.text}
34+
autocomplete="off"
35+
autofocus
36+
maxlength={props.tag.maxLength}
37+
placeholder={props.tag.placeholder || ''}></d-input>
38+
</d-form-item>
39+
<div class="dp-dropdown-operation-area">
40+
<Button variant="solid" onClick={onConfirmClick}>
41+
确定
42+
</Button>
43+
<Button variant="solid" color="secondary" onClick={onCancelClick}>
44+
取消
45+
</Button>
46+
</div>
47+
</d-form>
48+
);
49+
},
50+
});

packages/devui-vue/devui/category-search/src/composables/use-category-search.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function useCategorySearch(props: CategorySearchProps, ctx: SetupContext)
3838
showSearchCategory,
3939
filterNameRules,
4040
extendConfig,
41-
appendToBody
41+
appendToBody,
4242
} = toRefs(props);
4343
const innerCategory: Ref<ICategorySearchTagItem[]> = ref([]);
4444
const innerSelectedTags: Ref<ICategorySearchTagItem[]> = ref([]);
@@ -173,7 +173,7 @@ export function useCategorySearch(props: CategorySearchProps, ctx: SetupContext)
173173

174174
const onSearchKeyTagClick = () => {
175175
innerSearchKey.value = searchKeyCache;
176-
inputRef.value.focus();
176+
props.inputAutofocus && inputRef.value.focus();
177177
ctx.emit('searchKeyChange', innerSearchKey.value);
178178
};
179179

@@ -209,7 +209,8 @@ export function useCategorySearch(props: CategorySearchProps, ctx: SetupContext)
209209
}
210210
currentSelectTag.value.title = setTitle(currentSelectTag.value, currentSelectTag.value.type || '', '');
211211
inputRef.value.openMenu();
212-
inputRef.value.focus();
212+
props.inputAutofocus && inputRef.value.focus();
213+
ctx.emit('selectedCategory', { tag: item });
213214
}, DROPDOWN_ANIMATION_TIMEOUT);
214215
};
215216

@@ -250,6 +251,7 @@ export function useCategorySearch(props: CategorySearchProps, ctx: SetupContext)
250251
innerSearchKey.value = '';
251252
inputRef.value.closeMenu();
252253
chooseCategory(tag);
254+
ctx.emit('selectedCategory', { tag });
253255
setTimeout(() => {
254256
isFocus.value = true;
255257
enterSearch.value = false;
@@ -482,7 +484,7 @@ export function useCategorySearch(props: CategorySearchProps, ctx: SetupContext)
482484

483485
function finishChoose() {
484486
currentSelectTag.value = undefined;
485-
inputRef.value.focus();
487+
props.inputAutofocus && inputRef.value.focus();
486488
}
487489

488490
function afterDropdownClosed() {
@@ -665,7 +667,7 @@ export function useCategorySearch(props: CategorySearchProps, ctx: SetupContext)
665667
}
666668
} else if (!isInit) {
667669
// 初始化不聚焦,避免展开下拉
668-
inputRef.value.focus();
670+
props.inputAutofocus && inputRef.value.focus();
669671
}
670672
}
671673

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { ref, watch } from 'vue';
2+
import type { CategorySearchProps } from '../category-search-types';
3+
4+
export function useCategorySelectedTags(props: CategorySearchProps) {
5+
const isCollapseTags = ref(false);
6+
const collapseTagsCount = ref(0);
7+
const defaultPopoverConfig = {
8+
trigger: 'hover',
9+
position: ['top', 'bottom'],
10+
};
11+
const popoverConfig = ref(defaultPopoverConfig);
12+
13+
watch(
14+
() => props.collapseTags,
15+
(val) => {
16+
if (typeof val === 'boolean') {
17+
isCollapseTags.value = val;
18+
collapseTagsCount.value = val ? 1 : 0;
19+
}
20+
if (typeof val === 'number') {
21+
isCollapseTags.value = true;
22+
collapseTagsCount.value = val;
23+
}
24+
},
25+
{ immediate: true }
26+
);
27+
28+
watch(
29+
() => props.collapseTagsTooltip,
30+
(val) => {
31+
if (val) {
32+
popoverConfig.value = { ...defaultPopoverConfig, ...val };
33+
}
34+
},
35+
{ immediate: true }
36+
);
37+
38+
return { isCollapseTags, collapseTagsCount, popoverConfig };
39+
}

packages/devui-vue/devui/overlay/src/flexible-overlay/flexible-overlay-types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ export const flexibleOverlayProps = {
6969
type: Boolean,
7070
default: false,
7171
},
72+
autoUpdatePosition: {
73+
type: Boolean,
74+
default: false,
75+
},
7276
};
7377

7478
export type FlexibleOverlayProps = ExtractPropTypes<typeof flexibleOverlayProps>;

packages/devui-vue/devui/overlay/src/flexible-overlay/use-flexible-overlay.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export function useOverlay(props: FlexibleOverlayProps, emit: EmitEventFn) {
2929
const arrowRef = ref<HTMLElement | undefined>();
3030
const overlayWidth = ref(0);
3131
let originObserver: ResizeObserver;
32+
let overlayObserver: ResizeObserver;
3233

3334
const styles = computed(() => {
3435
if (fitOriginWidth.value) {
@@ -104,6 +105,17 @@ export function useOverlay(props: FlexibleOverlayProps, emit: EmitEventFn) {
104105
originEl && originObserver?.unobserve(originEl);
105106
};
106107

108+
const observeOverlay = () => {
109+
if (props.autoUpdatePosition && typeof window !== 'undefined' && overlayRef.value) {
110+
overlayObserver = new window.ResizeObserver(updatePosition);
111+
overlayObserver.observe(overlayRef.value);
112+
}
113+
};
114+
115+
const unobserveOverlay = () => {
116+
overlayRef.value && overlayObserver?.unobserve(overlayRef.value);
117+
};
118+
107119
watch(
108120
() => props.modelValue,
109121
() => {
@@ -112,17 +124,20 @@ export function useOverlay(props: FlexibleOverlayProps, emit: EmitEventFn) {
112124
window.addEventListener('scroll', scrollCallback, true);
113125
window.addEventListener('resize', updatePosition);
114126
observeOrigin();
127+
nextTick(observeOverlay);
115128
} else {
116129
window.removeEventListener('scroll', scrollCallback, true);
117130
window.removeEventListener('resize', updatePosition);
118131
unobserveOrigin();
132+
unobserveOverlay();
119133
}
120134
}
121135
);
122136
onUnmounted(() => {
123137
window.removeEventListener('scroll', scrollCallback, true);
124138
window.removeEventListener('resize', updatePosition);
125139
unobserveOrigin();
140+
unobserveOverlay();
126141
});
127142

128143
return { arrowRef, overlayRef, styles, updatePosition };

packages/devui-vue/devui/tag/src/tag.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export default defineComponent({
1313
const { type, color, checked, titleContent, deletable } = toRefs(props);
1414
const tagClass = useClass(props);
1515
const themeColor = useColor(props);
16-
const tagTitle = titleContent.value || '';
1716
const isDefaultTag = () => !type.value && !color.value;
1817

1918
const handleClick = (e: MouseEvent) => {
@@ -54,7 +53,7 @@ export default defineComponent({
5453
color: contentColor.value,
5554
backgroundColor: checked.value ? themeColor.value : !color.value ? '' : 'var(--devui-base-bg, #ffffff)',
5655
}}
57-
title={tagTitle}>
56+
title={titleContent.value || ''}>
5857
{slots.default?.()}
5958
{closeIconEl()}
6059
</span>

0 commit comments

Comments
 (0)