diff --git a/packages/devui-vue/devui/auto-focus/index.ts b/packages/devui-vue/devui/auto-focus/index.ts new file mode 100644 index 0000000000..a8277cbbad --- /dev/null +++ b/packages/devui-vue/devui/auto-focus/index.ts @@ -0,0 +1,12 @@ +import type { App } from 'vue'; +import AutoFocus from './src/auto-focus-directive'; + +export { AutoFocus }; + +export default { + title: 'AutoFocus 自动聚焦', + category: '公共', + install(app: App): void { + app.directive('dAutoFocus', AutoFocus); + }, +}; diff --git a/packages/devui-vue/devui/auto-focus/src/auto-focus-directive.ts b/packages/devui-vue/devui/auto-focus/src/auto-focus-directive.ts new file mode 100644 index 0000000000..a24663e54f --- /dev/null +++ b/packages/devui-vue/devui/auto-focus/src/auto-focus-directive.ts @@ -0,0 +1,7 @@ +export default { + mounted: (el: HTMLElement, binding: Record) => { + if (binding.value) { + el.focus(); + } + }, +}; diff --git a/packages/devui-vue/devui/code-editor/src/code-editor-types.ts b/packages/devui-vue/devui/code-editor/src/code-editor-types.ts index 500bb246e2..0067d7dff6 100644 --- a/packages/devui-vue/devui/code-editor/src/code-editor-types.ts +++ b/packages/devui-vue/devui/code-editor/src/code-editor-types.ts @@ -70,7 +70,7 @@ export const codeEditorProps = { type: Array as PropType, default: () => [] } -} +}; export type CodeEditorProps = ExtractPropTypes; @@ -86,4 +86,4 @@ export interface PositionInfo { export interface LayoutInfo extends PositionInfo { minimapWidth?: number; offsetLeft?: number; -} \ No newline at end of file +} diff --git a/packages/devui-vue/devui/code-editor/src/code-editor.tsx b/packages/devui-vue/devui/code-editor/src/code-editor.tsx index 6069ccbe3d..1bce5641d9 100644 --- a/packages/devui-vue/devui/code-editor/src/code-editor.tsx +++ b/packages/devui-vue/devui/code-editor/src/code-editor.tsx @@ -2,7 +2,7 @@ import { defineComponent } from 'vue'; import type { SetupContext } from 'vue'; import { codeEditorProps, CodeEditorProps } from './code-editor-types'; import { useCodeEditor } from './composables/use-code-editor'; -import './code-editor.scss' +import './code-editor.scss'; export default defineComponent({ name: 'DCodeEditor', @@ -11,6 +11,6 @@ export default defineComponent({ setup(props: CodeEditorProps, ctx: SetupContext) { const { editorEl } = useCodeEditor(props, ctx); - return () =>
+ return () =>
; } -}) \ No newline at end of file +}); diff --git a/packages/devui-vue/devui/code-editor/src/composables/use-code-editor.ts b/packages/devui-vue/devui/code-editor/src/composables/use-code-editor.ts index e041caaf18..977cb57ae2 100644 --- a/packages/devui-vue/devui/code-editor/src/composables/use-code-editor.ts +++ b/packages/devui-vue/devui/code-editor/src/composables/use-code-editor.ts @@ -180,8 +180,11 @@ export function useCodeEditor(props: CodeEditorProps, ctx: SetupContext) { model?.onDidChangeContent( throttle(() => { - modifyValueFromInner = true; - ctx.emit('update:modelValue', model.getValue()); + const editorValue = model.getValue(); + if (modelValue.value !== editorValue) { + modifyValueFromInner = true; + ctx.emit('update:modelValue', model.getValue()); + } }, 100) ); } diff --git a/packages/devui-vue/devui/code-review/src/code-review-types.ts b/packages/devui-vue/devui/code-review/src/code-review-types.ts index 3d112e9db3..951f24c19a 100644 --- a/packages/devui-vue/devui/code-review/src/code-review-types.ts +++ b/packages/devui-vue/devui/code-review/src/code-review-types.ts @@ -1,6 +1,7 @@ import type { ExtractPropTypes, InjectionKey, PropType, SetupContext, Ref } from 'vue'; import type { DiffFile } from 'diff2html/lib/types'; +export type DiffType = 'modify' | 'add' | 'delete' | 'rename'; export type OutputFormat = 'line-by-line' | 'side-by-side'; export type ExpandDirection = 'up' | 'down' | 'updown' | 'all'; export type LineSide = 'left' | 'right'; @@ -37,6 +38,10 @@ export const codeReviewProps = { type: Boolean, default: false, }, + diffType: { + type: String as PropType, + default: 'modify', + }, outputFormat: { type: String as PropType, default: 'line-by-line', @@ -53,6 +58,7 @@ export const codeReviewProps = { export type CodeReviewProps = ExtractPropTypes; export interface CodeReviewContext { + diffType: Ref; reviewContentRef: Ref; diffInfo: DiffFile; isFold: Ref; diff --git a/packages/devui-vue/devui/code-review/src/code-review.scss b/packages/devui-vue/devui/code-review/src/code-review.scss index 38fb97d37f..88077ea8d0 100644 --- a/packages/devui-vue/devui/code-review/src/code-review.scss +++ b/packages/devui-vue/devui/code-review/src/code-review.scss @@ -8,6 +8,7 @@ border-radius: $devui-border-radius-card; &__header { + position: relative; display: flex; justify-content: space-between; align-items: center; @@ -25,6 +26,40 @@ box-shadow: inset 0 -1px 0 0 $devui-brand-foil; } + .diff-type { + position: absolute; + left: 0; + top: 0; + width: 16px; + height: 16px; + font-size: $devui-font-size-sm; + letter-spacing: 0; + text-align: center; + line-height: 16px; + border-radius: 8px 0 8px 0; + user-select: none; + + &.modify { + color: #fa9841; + background-color: rgba(250, 152, 65, 0.2); + } + + &.add { + color: #3ac295; + background-color: rgba(58, 194, 149, 0.2); + } + + &.delete { + color: #f66f6a; + background-color: rgba(246, 111, 106, 0.2); + } + + &.rename { + color: #71757f; + background-color: rgba(113, 117, 127, 0.2); + } + } + .file-info { display: flex; align-items: center; @@ -36,6 +71,10 @@ line-height: 20px; } + & > svg { + margin-right: 8px; + } + .invert { transform: scale(-1); } @@ -45,7 +84,6 @@ font-size: $devui-font-size-sm; color: $devui-text; font-weight: bold; - padding-left: 8px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; @@ -72,7 +110,9 @@ &__content { line-height: 20px; - table { + table.d2h-diff-table { + table-layout: fixed; + font-size: 12px; margin: 0; } @@ -175,6 +215,7 @@ .d2h-file-side-diff { width: 100%; + overflow: hidden; } .d2h-code-side-linenumber { diff --git a/packages/devui-vue/devui/code-review/src/code-review.tsx b/packages/devui-vue/devui/code-review/src/code-review.tsx index fd8d1afdfe..ac2e59574a 100644 --- a/packages/devui-vue/devui/code-review/src/code-review.tsx +++ b/packages/devui-vue/devui/code-review/src/code-review.tsx @@ -1,4 +1,4 @@ -import { defineComponent, onMounted, provide } from 'vue'; +import { defineComponent, onMounted, provide, toRefs } from 'vue'; import type { SetupContext } from 'vue'; import CodeReviewHeader from './components/code-review-header'; import { CommentIcon } from './components/code-review-icons'; @@ -16,6 +16,7 @@ export default defineComponent({ emits: ['foldChange', 'addComment', 'afterViewInit', 'contentRefresh'], setup(props: CodeReviewProps, ctx: SetupContext) { const ns = useNamespace('code-review'); + const { diffType } = toRefs(props); const { renderHtml, reviewContentRef, diffFile, onContentClick } = useCodeReview(props, ctx); const { isFold, toggleFold } = useCodeReviewFold(props, ctx); const { commentLeft, commentTop, mouseEvent, onCommentMouseLeave, onCommentIconClick, insertComment, removeComment } = @@ -25,7 +26,7 @@ export default defineComponent({ ctx.emit('afterViewInit', { toggleFold, insertComment, removeComment }); }); - provide(CodeReviewInjectionKey, { reviewContentRef, diffInfo: diffFile.value[0], isFold, rootCtx: ctx }); + provide(CodeReviewInjectionKey, { diffType, reviewContentRef, diffInfo: diffFile.value[0], isFold, rootCtx: ctx }); return () => (
diff --git a/packages/devui-vue/devui/code-review/src/components/code-review-header.tsx b/packages/devui-vue/devui/code-review/src/components/code-review-header.tsx index cffa8e3aa3..aff973f89f 100644 --- a/packages/devui-vue/devui/code-review/src/components/code-review-header.tsx +++ b/packages/devui-vue/devui/code-review/src/components/code-review-header.tsx @@ -12,7 +12,7 @@ export default defineComponent({ emits: ['click'], setup(_, ctx: SetupContext) { const ns = useNamespace('code-review'); - const { diffInfo, isFold, rootCtx } = inject(CodeReviewInjectionKey) as CodeReviewContext; + const { diffType, diffInfo, isFold, rootCtx } = inject(CodeReviewInjectionKey) as CodeReviewContext; const { copyTipsText, tipsPopType, onCopy } = useCodeReviewCopy(diffInfo); const onClick = (e: Event) => { const composedPath = e.composedPath(); @@ -26,6 +26,7 @@ export default defineComponent({ return () => (
+ {diffType.value[0].toUpperCase()}
{diffInfo.newName} diff --git a/packages/devui-vue/devui/editor-md/src/composables/helper.ts b/packages/devui-vue/devui/editor-md/src/composables/helper.ts index 6993ee810b..c360b61c5d 100644 --- a/packages/devui-vue/devui/editor-md/src/composables/helper.ts +++ b/packages/devui-vue/devui/editor-md/src/composables/helper.ts @@ -26,4 +26,4 @@ export function refreshEditorCursor() { event.initEvent('resize', true, true); } window.dispatchEvent(event); -} \ No newline at end of file +} diff --git a/packages/devui-vue/devui/editor-md/src/composables/md-render-service.ts b/packages/devui-vue/devui/editor-md/src/composables/md-render-service.ts index d669bb1a0a..2e0b94ead1 100644 --- a/packages/devui-vue/devui/editor-md/src/composables/md-render-service.ts +++ b/packages/devui-vue/devui/editor-md/src/composables/md-render-service.ts @@ -20,7 +20,7 @@ export class MDRenderService { return ''; } }) as any; - private baseUrl: string = ''; + private baseUrl = ''; private breaks = true; private renderParse: Function | undefined; @@ -93,7 +93,7 @@ export class MDRenderService { plugins.forEach(item => { const { plugin, opts } = item; this.mdt.use(plugin, opts); - }) + }); } private onIgnoreTagAttr(tag: string, name: string, value: string, isWhiteAttr: boolean) { @@ -115,7 +115,7 @@ export class MDRenderService { return `<${p1} id="${p2}-${headerRecord.get(p2)}">`; } else { headerRecord.set(p2, 0); - return `<${p1} id="${p2}">` + return `<${p1} id="${p2}">`; } }); } @@ -142,7 +142,7 @@ export class MDRenderService { right: true }), }, - }) + }); setTimeout(() => { refreshMermaid(); @@ -155,7 +155,7 @@ export class MDRenderService { if (mdRules) { Object.keys(mdRules).forEach(rule => { this.mdt[rule].set(mdRules[rule]); - }) + }); } } -} \ No newline at end of file +} diff --git a/packages/devui-vue/devui/editor-md/src/composables/use-editor-md-theme.ts b/packages/devui-vue/devui/editor-md/src/composables/use-editor-md-theme.ts index 28aa930faf..9635e19d9e 100644 --- a/packages/devui-vue/devui/editor-md/src/composables/use-editor-md-theme.ts +++ b/packages/devui-vue/devui/editor-md/src/composables/use-editor-md-theme.ts @@ -10,7 +10,7 @@ export function useEditorMdTheme(callback: () => void) { isDarkMode.value = themeService.currentTheme.id.indexOf('dark') !== -1; callback(); } - } + }; onBeforeMount(() => { themeService = window['devuiThemeService']; @@ -25,9 +25,9 @@ export function useEditorMdTheme(callback: () => void) { onBeforeUnmount(() => { if (themeService && themeService.eventBus) { - themeService.eventBus.remove('themeChanged', themeChange) + themeService.eventBus.remove('themeChanged', themeChange); } - }) + }); return { isDarkMode }; -} \ No newline at end of file +} diff --git a/packages/devui-vue/devui/editor-md/src/composables/use-editor-md-toolbar.ts b/packages/devui-vue/devui/editor-md/src/composables/use-editor-md-toolbar.ts index 18c6693bc2..7e823c44f7 100644 --- a/packages/devui-vue/devui/editor-md/src/composables/use-editor-md-toolbar.ts +++ b/packages/devui-vue/devui/editor-md/src/composables/use-editor-md-toolbar.ts @@ -4,5 +4,5 @@ import { EditorMdInjectionKey, IEditorMdInjection } from '../editor-md-types'; export function useToolbar() { const { toolbars, toolbarConfig } = inject(EditorMdInjectionKey) as IEditorMdInjection; - return { toolbars, toolbarConfig } -} \ No newline at end of file + return { toolbars, toolbarConfig }; +} diff --git a/packages/devui-vue/devui/editor-md/src/icons-config.ts b/packages/devui-vue/devui/editor-md/src/icons-config.ts index 73b053ee87..752fdad005 100644 --- a/packages/devui-vue/devui/editor-md/src/icons-config.ts +++ b/packages/devui-vue/devui/editor-md/src/icons-config.ts @@ -90,13 +90,13 @@ export const LIST_ORDERED_ICON = ` -` +`; export const LIST_CHECK_ICON = ` -` +`; export const CODE_BLOCK_ICON = ` @@ -130,13 +130,13 @@ export const LINK_ICON = ` export const CODE_ICON = ` -` +`; export const H1_ICON = ` -` +`; export const H2_ICON = ` @@ -160,4 +160,4 @@ export const FULLSCREEN_EXPAND_ICON = ` -`; \ No newline at end of file +`; diff --git a/packages/devui-vue/devui/editor-md/src/plugins/mermaid.ts b/packages/devui-vue/devui/editor-md/src/plugins/mermaid.ts index 631016ba03..ee80aa9eef 100644 --- a/packages/devui-vue/devui/editor-md/src/plugins/mermaid.ts +++ b/packages/devui-vue/devui/editor-md/src/plugins/mermaid.ts @@ -12,7 +12,7 @@ const DEFAULT_CONFIG = { function render(code: string, options: Record) { try { - return `
${code}
` + return `
${code}
`; } catch (err: any) { return `
${err.name}: ${err.message}
`; } @@ -27,14 +27,14 @@ export function mermaidRender(md: any, options = {}) { const token = tokens[idx]; const code = token.content.trim(); if (token.info.startsWith('mermaid')) { - return render(code, options) + return render(code, options); } - return defaultRenderer(tokens, idx, opts, env, self) - } + return defaultRenderer(tokens, idx, opts, env, self); + }; } export function refreshMermaid(delay = 0) { setTimeout(() => { Mermaid.init(); }, delay); -} \ No newline at end of file +} diff --git a/packages/devui-vue/devui/editor-md/src/utils.ts b/packages/devui-vue/devui/editor-md/src/utils.ts index f0bc9bf250..05acbc88fc 100644 --- a/packages/devui-vue/devui/editor-md/src/utils.ts +++ b/packages/devui-vue/devui/editor-md/src/utils.ts @@ -29,7 +29,7 @@ export function locale(key: string): string { underline: '下划线', strike: '删除线', color: '字体颜色', - backgound: '背景色', + background: '背景色', orderedlist: '有序列表', unorderedlist: '无序列表', checklist: '任务列表', @@ -82,7 +82,7 @@ export function locale(key: string): string { 'counter-limit-tips': '{{countUnit}}数超出最大允许值', 'ie-msg': '为了更好体验,请使用chrome浏览器', loading: '正在加载中...', - pasting: '您粘贴内容较多, 正在努力加载中,请耐心等待...' - } + pasting: '您粘贴内容较多, 正在努力加载中,请耐心等待...', + }; return localeMap[key]; -} \ No newline at end of file +} diff --git a/packages/devui-vue/devui/form/__tests__/form-item-input.spec.tsx b/packages/devui-vue/devui/form/__tests__/form-item-input.spec.tsx index 6e611d9031..6dcb0120df 100644 --- a/packages/devui-vue/devui/form/__tests__/form-item-input.spec.tsx +++ b/packages/devui-vue/devui/form/__tests__/form-item-input.spec.tsx @@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'; import { reactive, ref, nextTick } from 'vue'; import { Form, FormItem } from '../index'; import { Input } from '../../input'; -import { useNamespace } from '../../shared/hooks/use-namespace'; +import { useNamespace } from '@devui/shared/utils'; import { wait } from '../../shared/utils'; const formNs = useNamespace('form', true); diff --git a/packages/devui-vue/devui/form/__tests__/form.spec.ts b/packages/devui-vue/devui/form/__tests__/form.spec.ts index 7a80ce49ba..bbb87cf42c 100644 --- a/packages/devui-vue/devui/form/__tests__/form.spec.ts +++ b/packages/devui-vue/devui/form/__tests__/form.spec.ts @@ -9,7 +9,7 @@ import { Switch } from '../../switch'; import { Checkbox, CheckboxGroup } from '../../checkbox'; import { DatePickerPro, DRangeDatePickerPro } from '../../date-picker-pro'; import { Textarea } from '../../textarea'; -import { useNamespace } from '../../shared/hooks/use-namespace'; +import { useNamespace } from '@devui/shared/utils'; jest.mock('../../locale/create', () => ({ createI18nTranslate: () => jest.fn(), diff --git a/packages/devui-vue/devui/form/index.ts b/packages/devui-vue/devui/form/index.ts index 281174e8c7..cf930e6954 100644 --- a/packages/devui-vue/devui/form/index.ts +++ b/packages/devui-vue/devui/form/index.ts @@ -7,6 +7,7 @@ export { Form, FormItem, FormOperation }; export * from './src/form-types'; export * from './src/components/form-item/form-item-types'; +export * from './src/components/form-control/form-control-types'; export default { title: 'Form 表单', diff --git a/packages/devui-vue/devui/form/src/components/form-control/form-control.scss b/packages/devui-vue/devui/form/src/components/form-control/form-control.scss index 65b6a5d25f..4eab634f66 100644 --- a/packages/devui-vue/devui/form/src/components/form-control/form-control.scss +++ b/packages/devui-vue/devui/form/src/components/form-control/form-control.scss @@ -1,4 +1,4 @@ -@import '../../../../styles-var/devui-var.scss'; +@import '@devui/theme/styles-var/devui-var.scss'; .#{$devui-prefix}-form__control { flex: 1 1 auto; diff --git a/packages/devui-vue/devui/form/src/components/form-control/form-control.tsx b/packages/devui-vue/devui/form/src/components/form-control/form-control.tsx index f5d9c492c5..6bed3c571a 100644 --- a/packages/devui-vue/devui/form/src/components/form-control/form-control.tsx +++ b/packages/devui-vue/devui/form/src/components/form-control/form-control.tsx @@ -1,9 +1,10 @@ -import { defineComponent, ref } from 'vue'; +import { defineComponent, ref, watch, computed, inject, onUnmounted } from 'vue'; import type { SetupContext } from 'vue'; +import { FormContext, FORM_TOKEN } from '../../form-types'; import { formControlProps, FormControlProps } from './form-control-types'; import { Popover } from '../../../../popover'; import { SuccessIcon, ErrorIcon, PendingIcon } from '../form-icons'; -import { useNamespace } from '../../../../shared/hooks/use-namespace'; +import { useNamespace } from '@devui/shared/utils'; import { useFormControl, useFormControlValidate } from './use-form-control'; import './form-control.scss'; @@ -11,20 +12,61 @@ export default defineComponent({ name: 'DFormControl', props: formControlProps, setup(props: FormControlProps, ctx: SetupContext) { + const formContext = inject(FORM_TOKEN) as FormContext; const formControl = ref(); + const popoverRef = ref(); const ns = useNamespace('form'); - const { controlClasses, controlContainerClasses } = useFormControl(props); + const showPopoverClick = ref(true); + const { controlClasses, controlContainerClasses, labelData } = useFormControl(props); const { feedbackStatus, showFeedback, showPopover, showMessage, errorMessage, popPosition } = useFormControlValidate(); + const align = computed(() => { + if (popPosition.value?.some((item: string) => item.includes('start'))) { + return 'start'; + } + if (popPosition.value?.some((item: string) => item.includes('end'))) { + return 'end'; + } + return undefined; + }); + + const onDocumentClick = (e: Event) => { + const composedPath = e.composedPath(); + if (composedPath.includes(popoverRef.value.triggerEl)) { + showPopoverClick.value = true; + } else { + showPopoverClick.value = false; + } + }; + + watch(showPopover, (val) => { + if (val) { + setTimeout(() => { + document.addEventListener('click', onDocumentClick); + }); + } else { + showPopoverClick.value = true; + document.removeEventListener('click', onDocumentClick); + } + }); + + onUnmounted(() => { + document.removeEventListener('click', onDocumentClick); + }); + return () => (
+ position={popPosition.value} + align={align.value} + scroll-element="auto" + append-to-body-scroll-strategy={formContext.appendToBodyScrollStrategy}> {ctx.slots.default?.()} {showFeedback.value && ( @@ -37,7 +79,9 @@ export default defineComponent({
{showMessage.value &&
{errorMessage.value}
} - {props.extraInfo &&
{props.extraInfo}
} + {labelData.value.formItemCtx.slots.extraInfo + ? labelData.value.formItemCtx.slots.extraInfo() + : props.extraInfo &&
{props.extraInfo}
}
); diff --git a/packages/devui-vue/devui/form/src/components/form-control/use-form-control.ts b/packages/devui-vue/devui/form/src/components/form-control/use-form-control.ts index ef7fec383e..db9d4a90ff 100644 --- a/packages/devui-vue/devui/form/src/components/form-control/use-form-control.ts +++ b/packages/devui-vue/devui/form/src/components/form-control/use-form-control.ts @@ -1,9 +1,9 @@ import { computed, inject, toRefs } from 'vue'; -import { FormControlProps, UseFormControl, UseFormControlValidate } from './form-control-types'; +import { FormControlProps, UseFormControlValidate } from './form-control-types'; import { FormItemContext, FORM_ITEM_TOKEN, LabelData, LABEL_DATA } from '../form-item/form-item-types'; -import { useNamespace } from '../../../../shared/hooks/use-namespace'; +import { useNamespace } from '@devui/shared/utils'; -export function useFormControl(props: FormControlProps): UseFormControl { +export function useFormControl(props: FormControlProps) { const labelData = inject(LABEL_DATA) as LabelData; const ns = useNamespace('form'); const { feedbackStatus } = toRefs(props); @@ -20,7 +20,7 @@ export function useFormControl(props: FormControlProps): UseFormControl { [ns.em('control-container', 'feedback-error')]: Boolean(feedbackStatus?.value === 'error'), })); - return { controlClasses, controlContainerClasses }; + return { controlClasses, controlContainerClasses, labelData }; } export function useFormControlValidate(): UseFormControlValidate { diff --git a/packages/devui-vue/devui/form/src/components/form-item/form-item-types.ts b/packages/devui-vue/devui/form/src/components/form-item/form-item-types.ts index 6bcde27d68..4e340cccac 100644 --- a/packages/devui-vue/devui/form/src/components/form-item/form-item-types.ts +++ b/packages/devui-vue/devui/form/src/components/form-item/form-item-types.ts @@ -1,5 +1,5 @@ import type { RuleItem, ValidateFieldsError } from 'async-validator'; -import type { ComputedRef, ExtractPropTypes, PropType, InjectionKey, Ref } from 'vue'; +import type { ComputedRef, ExtractPropTypes, PropType, InjectionKey, Ref, SetupContext } from 'vue'; import { LabelAlign, LabelSize, Layout } from '../../form-types'; import { FeedbackStatus } from '../form-control/form-control-types'; @@ -19,6 +19,13 @@ export type PopPosition = | 'left-start' | 'left-end'; +export interface HelpTips { + content: string; + position?: PopPosition[]; + trigger?: 'hover' | 'click'; + popType?: string; +} + export interface FormRuleItem extends RuleItem { trigger?: Array; } @@ -49,7 +56,7 @@ export const formItemProps = { default: undefined, }, helpTips: { - type: String, + type: [String, Object] as PropType, default: '', }, feedbackStatus: { @@ -70,6 +77,8 @@ export type LabelData = ComputedRef<{ layout: Layout; labelSize: LabelSize; labelAlign: LabelAlign; + helpTips: string | HelpTips; + formItemCtx: SetupContext; }>; export interface FormItemContext extends FormItemProps { diff --git a/packages/devui-vue/devui/form/src/components/form-item/form-item.scss b/packages/devui-vue/devui/form/src/components/form-item/form-item.scss index d7ff9396c8..4183681ef2 100644 --- a/packages/devui-vue/devui/form/src/components/form-item/form-item.scss +++ b/packages/devui-vue/devui/form/src/components/form-item/form-item.scss @@ -1,4 +1,4 @@ -@import '../../../../styles-var/devui-var.scss'; +@import '@devui/theme/styles-var/devui-var.scss'; .#{$devui-prefix}-form__item { &--horizontal { diff --git a/packages/devui-vue/devui/form/src/components/form-item/form-item.tsx b/packages/devui-vue/devui/form/src/components/form-item/form-item.tsx index cf1547da5d..d31610c23d 100644 --- a/packages/devui-vue/devui/form/src/components/form-item/form-item.tsx +++ b/packages/devui-vue/devui/form/src/components/form-item/form-item.tsx @@ -24,6 +24,8 @@ export default defineComponent({ layout: formContext.layout, labelSize: formContext.labelSize, labelAlign: formContext.labelAlign, + helpTips: helpTips.value, + formItemCtx: ctx, })); provide(LABEL_DATA, labelData); const context: FormItemContext = reactive({ @@ -42,6 +44,7 @@ export default defineComponent({ provide(FORM_ITEM_TOKEN, context); ctx.expose({ + validate, resetField, clearValidate, }); @@ -58,7 +61,7 @@ export default defineComponent({ return () => (
- {ctx.slots.label ? ctx.slots.label() : label?.value} + {ctx.slots.label ? ctx.slots.label() : label?.value} {ctx.slots.default?.()} diff --git a/packages/devui-vue/devui/form/src/components/form-item/use-form-item.ts b/packages/devui-vue/devui/form/src/components/form-item/use-form-item.ts index 3c72d12bb0..75218258a2 100644 --- a/packages/devui-vue/devui/form/src/components/form-item/use-form-item.ts +++ b/packages/devui-vue/devui/form/src/components/form-item/use-form-item.ts @@ -14,7 +14,7 @@ import { MessageType, UseFormItemRule } from './form-item-types'; -import { useNamespace } from '../../../../shared/hooks/use-namespace'; +import { useNamespace } from '@devui/shared/utils'; function getFieldValue(obj: Record, path: string) { return { diff --git a/packages/devui-vue/devui/form/src/components/form-label/form-label.scss b/packages/devui-vue/devui/form/src/components/form-label/form-label.scss index ee6541e4b0..413c97ef3c 100644 --- a/packages/devui-vue/devui/form/src/components/form-label/form-label.scss +++ b/packages/devui-vue/devui/form/src/components/form-label/form-label.scss @@ -1,4 +1,4 @@ -@import '../../../../styles-var/devui-var.scss'; +@import '@devui/theme/styles-var/devui-var.scss'; .#{$devui-prefix}-form__label { align-self: flex-start; @@ -35,7 +35,8 @@ .#{$devui-prefix}-form__label-span { display: inline-block; vertical-align: middle; - color: $devui-text; + font-size: $devui-font-size; + color: $devui-aide-text; } .#{$devui-prefix}-form__label--required { @@ -46,6 +47,7 @@ margin-right: 8px; margin-left: -12px; } + &-hide { &::before { display: none; @@ -61,3 +63,9 @@ margin-left: 4px; cursor: pointer; } + +.#{$devui-prefix}-form__label-tips-popover { + .dv-popover__icon-wrap + span { + flex: 1; + } +} diff --git a/packages/devui-vue/devui/form/src/components/form-label/form-label.tsx b/packages/devui-vue/devui/form/src/components/form-label/form-label.tsx index ef58829662..802715645b 100644 --- a/packages/devui-vue/devui/form/src/components/form-label/form-label.tsx +++ b/packages/devui-vue/devui/form/src/components/form-label/form-label.tsx @@ -1,24 +1,22 @@ import { defineComponent } from 'vue'; import type { SetupContext } from 'vue'; -import { formLabelProps, FormLabelProps } from './form-label-types'; import Popover from '../../../../popover/src/popover'; import { HelpTipsIcon } from '../form-icons'; -import { useNamespace } from '../../../../shared/hooks/use-namespace'; +import { useNamespace } from '@devui/shared/utils'; import { useFormLabel } from './use-form-label'; import './form-label.scss'; export default defineComponent({ name: 'DFormLabel', - props: formLabelProps, - setup(props: FormLabelProps, ctx: SetupContext) { + setup(_, ctx: SetupContext) { const ns = useNamespace('form'); - const { labelClasses, labelInnerClasses } = useFormLabel(); + const { labelClasses, labelInnerClasses, tipsPopover } = useFormLabel(); return () => ( {ctx.slots.default?.()} - {props.helpTips && ( - + {tipsPopover.value.content && ( + , )} diff --git a/packages/devui-vue/devui/form/src/components/form-label/use-form-label.ts b/packages/devui-vue/devui/form/src/components/form-label/use-form-label.ts index d8896f9e4a..fa9a45f08f 100644 --- a/packages/devui-vue/devui/form/src/components/form-label/use-form-label.ts +++ b/packages/devui-vue/devui/form/src/components/form-label/use-form-label.ts @@ -1,15 +1,21 @@ import { computed, inject } from 'vue'; -import { FormItemContext, FORM_ITEM_TOKEN, LabelData, LABEL_DATA } from '../form-item/form-item-types'; -import { UseFormLabel } from './form-label-types'; -import { useNamespace } from '../../../../shared/hooks/use-namespace'; +import { FormItemContext, FORM_ITEM_TOKEN, LabelData, LABEL_DATA, HelpTips } from '../form-item/form-item-types'; +import { useNamespace } from '@devui/shared/utils'; import { FORM_TOKEN, FormContext } from '../../form-types'; -export function useFormLabel(): UseFormLabel { +export function useFormLabel() { const formContext = inject(FORM_TOKEN) as FormContext; const formItemContext = inject(FORM_ITEM_TOKEN) as FormItemContext; const labelData = inject(LABEL_DATA) as LabelData; const ns = useNamespace('form'); + const defaultTipsPopover: HelpTips = { + content: '', + position: ['top'], + trigger: 'hover', + popType: 'info', + }; + const labelClasses = computed(() => ({ [`${ns.e('label')}`]: true, [`${ns.em('label', 'vertical')}`]: labelData.value.layout === 'vertical', @@ -23,5 +29,13 @@ export function useFormLabel(): UseFormLabel { [`${ns.em('label', 'required-hide')}`]: formItemContext.isRequired && formContext.hideRequiredMark, })); - return { labelClasses, labelInnerClasses }; + const tipsPopover = computed(() => { + if (typeof labelData.value.helpTips === 'string') { + return { ...defaultTipsPopover, content: labelData.value.helpTips }; + } else { + return { ...defaultTipsPopover, ...labelData.value.helpTips }; + } + }); + + return { labelClasses, labelInnerClasses, tipsPopover }; } diff --git a/packages/devui-vue/devui/form/src/components/form-operation/form-operation.scss b/packages/devui-vue/devui/form/src/components/form-operation/form-operation.scss index b0b51e2c98..e2360199e5 100644 --- a/packages/devui-vue/devui/form/src/components/form-operation/form-operation.scss +++ b/packages/devui-vue/devui/form/src/components/form-operation/form-operation.scss @@ -1,4 +1,4 @@ -@import '../../../../styles-var/devui-var.scss'; +@import '@devui/theme/styles-var/devui-var.scss'; .#{$devui-prefix}-form-operation { .star { diff --git a/packages/devui-vue/devui/form/src/form-types.ts b/packages/devui-vue/devui/form/src/form-types.ts index 0d42d4950c..ddd8a9d105 100644 --- a/packages/devui-vue/devui/form/src/form-types.ts +++ b/packages/devui-vue/devui/form/src/form-types.ts @@ -1,5 +1,5 @@ import type { ValidateError, ValidateFieldsError, Rules, Values } from 'async-validator'; -import type { PropType, ExtractPropTypes, InjectionKey, SetupContext } from 'vue'; +import type { PropType, ExtractPropTypes, InjectionKey, SetupContext, Prop } from 'vue'; import { FormItemContext, FormRuleItem, @@ -14,6 +14,8 @@ export type LabelSize = 'sm' | 'md' | 'lg'; export type FormSize = 'sm' | 'md' | 'lg'; export type LabelAlign = 'start' | 'center' | 'end'; export type FormData = Record; +export type StyleType = 'default' | 'gray'; +export type AppendToBodyScrollStrategy = 'close' | 'reposition'; export type FormRules = Partial>>; export interface ValidateFailure { @@ -67,7 +69,15 @@ export const formProps = { hideRequiredMark: { type: Boolean, default: false, - } + }, + styleType: { + type: String as PropType, + default: 'default', + }, + appendToBodyScrollStrategy: { + type: String as PropType, + default: 'reposition', + }, } as const; export interface UseFieldCollection { @@ -93,6 +103,8 @@ export interface FormContext extends FormProps { export const FORM_TOKEN: InjectionKey = Symbol('dForm'); +export const STYLE_TOKEN: InjectionKey = Symbol('dForm'); + export interface DValidateResult { errors: E; fields: F; diff --git a/packages/devui-vue/devui/form/src/form.tsx b/packages/devui-vue/devui/form/src/form.tsx index 7532a1bf8d..3d7c57bd79 100644 --- a/packages/devui-vue/devui/form/src/form.tsx +++ b/packages/devui-vue/devui/form/src/form.tsx @@ -1,6 +1,6 @@ import { defineComponent, provide, reactive, SetupContext, toRefs, watch } from 'vue'; -import { formProps, FormProps, FORM_TOKEN } from './form-types'; -import { useNamespace } from '../../shared/hooks/use-namespace'; +import { formProps, FormProps, FORM_TOKEN, STYLE_TOKEN } from './form-types'; +import { useNamespace } from '@devui/shared/utils'; import useFieldCollection from './composables/use-field-collection'; import useFormValidation from './composables/use-form-validation'; @@ -37,6 +37,11 @@ export default defineComponent({ }) ); + provide( + STYLE_TOKEN, + props.styleType + ); + ctx.expose({ validate, validateFields, diff --git a/packages/devui-vue/devui/gantt/src/gantt-milestone/milestone-icon.tsx b/packages/devui-vue/devui/gantt/src/gantt-milestone/milestone-icon.tsx index b0c2b58617..88ae76e99e 100644 --- a/packages/devui-vue/devui/gantt/src/gantt-milestone/milestone-icon.tsx +++ b/packages/devui-vue/devui/gantt/src/gantt-milestone/milestone-icon.tsx @@ -209,4 +209,4 @@ export const MilestoneIcon = () => ( -) +); diff --git a/packages/devui-vue/devui/image-preview/src/image-preview-directive.ts b/packages/devui-vue/devui/image-preview/src/image-preview-directive.ts index e321958dbd..f8007aa171 100644 --- a/packages/devui-vue/devui/image-preview/src/image-preview-directive.ts +++ b/packages/devui-vue/devui/image-preview/src/image-preview-directive.ts @@ -19,15 +19,11 @@ function unmountedPreviewImages() { } function getImgByEl(el: HTMLElement): Array { - const imgs = [...el.querySelectorAll('img')]; - const urlList = imgs.map((item: HTMLImageElement) => { - return (item.getAttribute('preview-src') || item.getAttribute('src')) ?? ''; - }); + const urlList = [...el.querySelectorAll('img')].map((ele: HTMLImageElement) => ele.getAttribute('src') as string); return urlList; } function handleImg(e: MouseEvent) { - e.stopPropagation(); const el = e.currentTarget as PreviewHTMLElement; const target = e.target as PreviewHTMLElement; if (target?.nodeName?.toLowerCase() === 'img') { diff --git a/packages/devui-vue/devui/image-preview/src/image-preview.scss b/packages/devui-vue/devui/image-preview/src/image-preview.scss index a72d604f63..b54576045f 100644 --- a/packages/devui-vue/devui/image-preview/src/image-preview.scss +++ b/packages/devui-vue/devui/image-preview/src/image-preview.scss @@ -122,4 +122,6 @@ bottom: 0; z-index: calc($devui-z-index-full-page-overlay - 1); background: $devui-shadow; + border-radius: $devui-border-radius; + box-shadow: $devui-shadow-length-fullscreen-overlay $devui-shadow; } diff --git a/packages/devui-vue/devui/image-preview/src/image-preview.tsx b/packages/devui-vue/devui/image-preview/src/image-preview.tsx index 78c393014d..e03c066b62 100644 --- a/packages/devui-vue/devui/image-preview/src/image-preview.tsx +++ b/packages/devui-vue/devui/image-preview/src/image-preview.tsx @@ -2,7 +2,7 @@ import { defineComponent, Fragment, ref, computed, onMounted, onUnmounted } from import { imagePreviewProps, ImagePreviewProps } from './image-preview-types'; import ImagePreviewService from './image-preview-service'; import Transform from './transform'; -import { useNamespace } from '../../shared/hooks/use-namespace'; +import { useNamespace } from '@devui/shared/utils'; import './image-preview.scss'; export default defineComponent({ diff --git a/packages/devui-vue/devui/input/__tests__/input.spec.ts b/packages/devui-vue/devui/input/__tests__/input.spec.ts index e077fa2da8..58621aba84 100644 --- a/packages/devui-vue/devui/input/__tests__/input.spec.ts +++ b/packages/devui-vue/devui/input/__tests__/input.spec.ts @@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'; import { ref, nextTick, reactive } from 'vue'; import DInput from '../src/input'; import { Form, FormItem } from '../../form'; -import { useNamespace } from '../../shared/hooks/use-namespace'; +import { useNamespace } from '@devui/shared/utils'; const ns = useNamespace('input'); const dotNs = useNamespace('input', true); diff --git a/packages/devui-vue/devui/input/src/composables/use-input-event.ts b/packages/devui-vue/devui/input/src/composables/use-input-event.ts index 9eed1d03e2..330f032874 100644 --- a/packages/devui-vue/devui/input/src/composables/use-input-event.ts +++ b/packages/devui-vue/devui/input/src/composables/use-input-event.ts @@ -1,10 +1,11 @@ -import { inject } from 'vue'; +import { inject, ref } from 'vue'; import type { Ref, SetupContext } from 'vue'; import { FORM_ITEM_TOKEN, FormItemContext } from '../../../form/src/components/form-item/form-item-types'; -import { InputProps, UseInputEvent } from '../input-types'; +import { InputProps } from '../input-types'; -export function useInputEvent(isFocus: Ref, props: InputProps, ctx: SetupContext, focus: () => void): UseInputEvent { +export function useInputEvent(isFocus: Ref, props: InputProps, ctx: SetupContext, focus: () => void) { const formItemContext = inject(FORM_ITEM_TOKEN, undefined) as FormItemContext; + const isComposition = ref(false); const onFocus = (e: FocusEvent) => { isFocus.value = true; ctx.emit('focus', e); @@ -20,6 +21,9 @@ export function useInputEvent(isFocus: Ref, props: InputProps, ctx: Set const onInput = (e: Event) => { ctx.emit('input', (e.target as HTMLInputElement).value); + if (isComposition.value) { + return; + } ctx.emit('update:modelValue', (e.target as HTMLInputElement).value); }; @@ -37,5 +41,22 @@ export function useInputEvent(isFocus: Ref, props: InputProps, ctx: Set focus(); }; - return { onFocus, onBlur, onInput, onChange, onKeydown, onClear }; + const onCompositionStart = () => { + isComposition.value = true; + }; + + const onCompositionUpdate = (e: CompositionEvent) => { + const text = (e.target as HTMLInputElement)?.value; + const lastCharacter = text[text.length - 1] || ''; + isComposition.value = !/([(\uAC00-\uD7AF)|(\u3130-\u318F)])+/gi.test(lastCharacter); + }; + + const onCompositionEnd = (e: CompositionEvent) => { + if (isComposition.value) { + isComposition.value = false; + onInput(e); + } + }; + + return { onFocus, onBlur, onInput, onChange, onKeydown, onClear, onCompositionStart, onCompositionUpdate, onCompositionEnd }; } diff --git a/packages/devui-vue/devui/input/src/composables/use-input-render.ts b/packages/devui-vue/devui/input/src/composables/use-input-render.ts index 7165eac7dd..1a257318cb 100644 --- a/packages/devui-vue/devui/input/src/composables/use-input-render.ts +++ b/packages/devui-vue/devui/input/src/composables/use-input-render.ts @@ -1,8 +1,8 @@ import { computed, inject, toRefs, ref } from 'vue'; import type { SetupContext } from 'vue'; -import { FORM_TOKEN, FormContext, FORM_ITEM_TOKEN, FormItemContext } from '../../../form'; +import { FORM_TOKEN, FormContext, FORM_ITEM_TOKEN, FormItemContext, STYLE_TOKEN } from '../../../form'; import { InputProps, UseInputRender } from '../input-types'; -import { useNamespace } from '../../../shared/hooks/use-namespace'; +import { useNamespace } from '@devui/shared/utils'; export function useInputRender(props: InputProps, ctx: SetupContext): UseInputRender { const formContext = inject(FORM_TOKEN, undefined) as FormContext; @@ -16,6 +16,8 @@ export function useInputRender(props: InputProps, ctx: SetupContext): UseInputRe const inputDisabled = computed(() => disabled.value || formContext?.disabled); const inputSize = computed(() => size?.value || formContext?.size || ''); + const styleType = inject(STYLE_TOKEN, undefined); + const { style, class: customClass, ...otherAttrs } = ctx.attrs; const customStyle = { style }; @@ -34,6 +36,7 @@ export function useInputRender(props: InputProps, ctx: SetupContext): UseInputRe [slotNs.b()]: slots.prepend || slots.append, [ns.m('append')]: slots.append, [ns.m('prepend')]: slots.prepend, + [ns.m('gray-style')]: styleType === 'gray', }, customClass, ]); diff --git a/packages/devui-vue/devui/input/src/input-types.tsx b/packages/devui-vue/devui/input/src/input-types.tsx index 6880649287..9bfdb4f1c7 100644 --- a/packages/devui-vue/devui/input/src/input-types.tsx +++ b/packages/devui-vue/devui/input/src/input-types.tsx @@ -42,6 +42,14 @@ export const inputProps = { type: String, default: '', }, + title: { + type: String, + default: '', + }, + autofocus: { + type: Boolean, + default: false, + }, } as const; export type InputProps = ExtractPropTypes; diff --git a/packages/devui-vue/devui/input/src/input.scss b/packages/devui-vue/devui/input/src/input.scss index 25cd7e85ae..eb6499e63e 100644 --- a/packages/devui-vue/devui/input/src/input.scss +++ b/packages/devui-vue/devui/input/src/input.scss @@ -72,6 +72,10 @@ background: none; outline: none; box-sizing: border-box; + + &::placeholder { + color: $devui-placeholder; + } } &--prepend { @@ -158,4 +162,33 @@ &__password--icon { cursor: pointer; } + + &--gray-style:not(.#{$devui-prefix}-input--disabled) { + .#{$devui-prefix}-input__wrapper:not(.#{$devui-prefix}-input--error) { + background: $devui-gray-5; + border-color: $devui-gray-5; + + &:hover { + background: $devui-gray-10; + border-color: $devui-gray-10; + } + } + } +} + +body[ui-theme='galaxy-theme'] { + .#{$devui-prefix}-input__inner:-webkit-autofill, + .#{$devui-prefix}-input__inner:-webkit-autofill:hover, + .#{$devui-prefix}-input__inner:-webkit-autofill:focus, + .#{$devui-prefix}-input__inner:-webkit-autofill:active { + -webkit-box-shadow: 0 0 0 1000px transparent inset !important; + box-shadow: 0 0 0 1000px transparent inset !important; + caret-color: white; + } + + .#{$devui-prefix}-input__inner:-internal-autofill-previewed, + .#{$devui-prefix}-input__inner:-internal-autofill-selected { + -webkit-text-fill-color: $devui-text; + transition: background-color 99999s ease-out 0.5s; + } } diff --git a/packages/devui-vue/devui/input/src/input.tsx b/packages/devui-vue/devui/input/src/input.tsx index 36932f00a2..8dd904f96b 100644 --- a/packages/devui-vue/devui/input/src/input.tsx +++ b/packages/devui-vue/devui/input/src/input.tsx @@ -1,9 +1,10 @@ import { defineComponent, watch, inject, toRefs, shallowRef, ref, computed, getCurrentInstance } from 'vue'; import type { SetupContext } from 'vue'; import Icon from '../../icon/src/icon'; +import { AutoFocus } from '../../auto-focus'; import { inputProps, InputProps } from './input-types'; import { FORM_ITEM_TOKEN, FormItemContext } from '../../form/src/components/form-item/form-item-types'; -import { useNamespace } from '../../shared/hooks/use-namespace'; +import { useNamespace } from '@devui/shared/utils'; import { useInputRender } from './composables/use-input-render'; import { useInputEvent } from './composables/use-input-event'; import { useInputFunction } from './composables/use-input-function'; @@ -12,6 +13,9 @@ import { createI18nTranslate } from '../../locale/create'; export default defineComponent({ name: 'DInput', + directives: { + dAutoFocus: AutoFocus, + }, inheritAttrs: false, props: inputProps, emits: ['update:modelValue', 'focus', 'blur', 'input', 'change', 'keydown', 'clear'], @@ -20,7 +24,7 @@ export default defineComponent({ const t = createI18nTranslate('DInput', app); const formItemContext = inject(FORM_ITEM_TOKEN, undefined) as FormItemContext; - const { modelValue } = toRefs(props); + const { modelValue, placeholder, title, autofocus } = toRefs(props); const ns = useNamespace('input'); const slotNs = useNamespace('input-slot'); const { inputDisabled, inputSize, isFocus, wrapClasses, inputClasses, customStyle, otherAttrs } = useInputRender(props, ctx); @@ -28,7 +32,8 @@ export default defineComponent({ const input = shallowRef(); const { select, focus, blur } = useInputFunction(input); - const { onFocus, onBlur, onInput, onChange, onKeydown, onClear } = useInputEvent(isFocus, props, ctx, focus); + const { onFocus, onBlur, onInput, onChange, onKeydown, onClear, onCompositionStart, onCompositionUpdate, onCompositionEnd } = + useInputEvent(isFocus, props, ctx, focus); const passwordVisible = ref(false); const clickPasswordIcon = () => { @@ -67,17 +72,22 @@ export default defineComponent({ )} {suffixVisible && ( @@ -92,7 +102,7 @@ export default defineComponent({ /> )} {showClearable.value && ( - + )} )} diff --git a/packages/devui-vue/devui/list/index.ts b/packages/devui-vue/devui/list/index.ts index 31b46e6222..0348019b32 100644 --- a/packages/devui-vue/devui/list/index.ts +++ b/packages/devui-vue/devui/list/index.ts @@ -7,7 +7,6 @@ export { List, ListItem }; export default { title: 'List 列表', category: '数据展示', - status: '10%', install(app: App): void { app.component(List.name, List); app.component(ListItem.name, ListItem); diff --git a/packages/devui-vue/devui/list/src/list-item.scss b/packages/devui-vue/devui/list/src/list-item.scss index 5cd77d240e..daf89b440d 100644 --- a/packages/devui-vue/devui/list/src/list-item.scss +++ b/packages/devui-vue/devui/list/src/list-item.scss @@ -8,6 +8,7 @@ line-height: 36px; padding: 0 8px; color: $devui-text; + border-radius: $devui-border-radius; box-sizing: border-box; cursor: pointer; transition: diff --git a/packages/devui-vue/devui/list/src/list-item.tsx b/packages/devui-vue/devui/list/src/list-item.tsx index 4778bb7454..ecd31287d2 100644 --- a/packages/devui-vue/devui/list/src/list-item.tsx +++ b/packages/devui-vue/devui/list/src/list-item.tsx @@ -1,5 +1,5 @@ import { defineComponent } from 'vue'; -import { useNamespace } from '../../shared/hooks/use-namespace'; +import { useNamespace } from '@devui/shared/utils'; import './list-item.scss'; export default defineComponent({ diff --git a/packages/devui-vue/devui/list/src/list.tsx b/packages/devui-vue/devui/list/src/list.tsx index 9285b18589..8e82b927c2 100644 --- a/packages/devui-vue/devui/list/src/list.tsx +++ b/packages/devui-vue/devui/list/src/list.tsx @@ -1,5 +1,5 @@ import { defineComponent } from 'vue'; -import { useNamespace } from '../../shared/hooks/use-namespace'; +import { useNamespace } from '@devui/shared/utils'; import './list.scss'; export default defineComponent({ diff --git a/packages/devui-vue/devui/progress/src/progress.tsx b/packages/devui-vue/devui/progress/src/progress.tsx index 7961a5f326..f9ac7c0ee1 100644 --- a/packages/devui-vue/devui/progress/src/progress.tsx +++ b/packages/devui-vue/devui/progress/src/progress.tsx @@ -1,7 +1,6 @@ import { CSSProperties, defineComponent, effect, reactive, ref, toRefs, watch } from 'vue'; -import { middleNum } from '../../shared/utils'; import { ProgressProps, progressProps, ISvgData } from './progress-types'; -import { useNamespace } from '../../shared/hooks/use-namespace'; +import { useNamespace, middleNum } from '@devui/shared/utils'; import './progress.scss'; export default defineComponent({ diff --git a/packages/devui-vue/devui/select/__tests__/option-group.spec.tsx b/packages/devui-vue/devui/select/__tests__/option-group.spec.tsx index e9ddb3eefe..aab710e526 100644 --- a/packages/devui-vue/devui/select/__tests__/option-group.spec.tsx +++ b/packages/devui-vue/devui/select/__tests__/option-group.spec.tsx @@ -3,7 +3,7 @@ import { ref, nextTick } from 'vue'; import DSelect from '../src/select'; import DOption from '../src/components/option'; import DOptionGroup from '../src/components/option-group'; -import { useNamespace } from '../../shared/hooks/use-namespace'; +import { useNamespace } from '@devui/shared/utils'; const ns = useNamespace('select', true); diff --git a/packages/devui-vue/devui/select/__tests__/option.spec.tsx b/packages/devui-vue/devui/select/__tests__/option.spec.tsx index ea97bda63a..6ae19f8fe8 100644 --- a/packages/devui-vue/devui/select/__tests__/option.spec.tsx +++ b/packages/devui-vue/devui/select/__tests__/option.spec.tsx @@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'; import { ref, nextTick, reactive } from 'vue'; import DSelect from '../src/select'; import DOption from '../src/components/option'; -import { useNamespace } from '../../shared/hooks/use-namespace'; +import { useNamespace } from '@devui/shared/utils'; const ns = useNamespace('select', true); const notDotNs = useNamespace('select'); diff --git a/packages/devui-vue/devui/select/__tests__/select.spec.tsx b/packages/devui-vue/devui/select/__tests__/select.spec.tsx index 8c9a30969b..b10ecca24b 100644 --- a/packages/devui-vue/devui/select/__tests__/select.spec.tsx +++ b/packages/devui-vue/devui/select/__tests__/select.spec.tsx @@ -3,7 +3,7 @@ import { ref, reactive, nextTick } from 'vue'; import DSelect from '../src/select'; import { Form as DForm, FormItem as DFormItem } from '../../form'; import { Button } from '../../button'; -import { useNamespace } from '../../shared/hooks/use-namespace'; +import { useNamespace } from '@devui/shared/utils'; const ns = useNamespace('select', true); const notDotNs = useNamespace('select'); diff --git a/packages/devui-vue/devui/select/src/components/option-group.tsx b/packages/devui-vue/devui/select/src/components/option-group.tsx index bbc5ed373e..0f17ab676c 100644 --- a/packages/devui-vue/devui/select/src/components/option-group.tsx +++ b/packages/devui-vue/devui/select/src/components/option-group.tsx @@ -1,7 +1,7 @@ import { defineComponent, provide, reactive, toRefs } from 'vue'; import type { SetupContext } from 'vue'; import { optionGroupProps, OptionGroupProps, OptionGroupContext } from '../select-types'; -import { useNamespace } from '../../../shared/hooks/use-namespace'; +import { useNamespace } from '@devui/shared/utils'; import { OPTION_GROUP_TOKEN } from '../const'; export default defineComponent({ diff --git a/packages/devui-vue/devui/select/src/components/select-content.tsx b/packages/devui-vue/devui/select/src/components/select-content.tsx index d3c90b5b40..d87704d9bc 100644 --- a/packages/devui-vue/devui/select/src/components/select-content.tsx +++ b/packages/devui-vue/devui/select/src/components/select-content.tsx @@ -1,16 +1,17 @@ import { defineComponent, inject, computed, withModifiers } from 'vue'; +import type { SetupContext } from 'vue'; import AlertCloseIcon from '../../../alert/src/components/alert-close-icon'; import SelectArrowIcon from './select-arrow-icon'; import { Tag, SizeType } from '../../../tag'; import { Popover } from '../../../popover'; -import { useNamespace } from '../../../shared/hooks/use-namespace'; +import { useNamespace } from '@devui/shared/utils'; import useSelectContent from '../composables/use-select-content'; import { OptionObjectItem } from '../select-types'; import { FORM_ITEM_TOKEN } from '../../../form'; export default defineComponent({ name: 'SelectContent', - setup() { + setup(_, ctx: SetupContext) { const formItemContext = inject(FORM_ITEM_TOKEN, undefined); const ns = useNamespace('select'); const clearCls = computed(() => ({ @@ -24,18 +25,27 @@ export default defineComponent({ const multipleCls = ns.e('multiple'); const multipleInputCls = ns.em('multiple', 'input'); const { + singleInputRef, searchQuery, + singleSearchKey, selectedData, isSelectDisable, isSupportCollapseTags, isDisabledTooltip, isReadOnly, + isSupportFilter, selectionCls, inputCls, tagSize, placeholder, + singlePlaceholder, + singlePlaceholderWidth, isMultiple, + isPlaceholderDark, displayInputValue, + onSingleInputWrapClick, + onMultipleClick, + onArrowClick, handleClear, tagDelete, onFocus, @@ -43,18 +53,33 @@ export default defineComponent({ queryFilter, } = useSelectContent(); + const clearSingleSearchKey = () => { + singleSearchKey.value = ''; + }; + + const clearMultipleSearchKey = () => { + searchQuery.value = ''; + }; + + ctx.expose({ + clearSingleSearchKey, + clearMultipleSearchKey, + }); + return () => { return (
{isMultiple.value ? ( -
+
{!isSupportCollapseTags.value && selectedData.value.length >= 1 && selectedData.value.map((item: OptionObjectItem) => ( tagDelete(item), ['prevent', 'stop'])} key={item.value} + maxWidth={'78%'} + class={['multiple-tag', { disabled: isSelectDisable.value || item.disabled }]} size={tagSize.value as SizeType}> {item.name} @@ -62,6 +87,8 @@ export default defineComponent({ {isSupportCollapseTags.value && selectedData.value.length >= 1 && ( tagDelete(selectedData.value[0]), ['prevent', 'stop'])} size={tagSize.value as SizeType}> {selectedData.value[0].name} @@ -70,6 +97,7 @@ export default defineComponent({ {isSupportCollapseTags.value && selectedData.value.length > 1 && ( {`+${selectedData.value.length - 1}`}, @@ -82,6 +110,7 @@ export default defineComponent({ deletable onTagDelete={withModifiers(() => tagDelete(item), ['prevent', 'stop'])} key={item.value} + class="popover-tag" size={tagSize.value as SizeType}> {item.name} @@ -93,37 +122,45 @@ export default defineComponent({ )}
) : ( - +
+ {!singleSearchKey.value && ( + + {displayInputValue.value || singlePlaceholder.value} + + )} + +
)} - +
diff --git a/packages/devui-vue/devui/select/src/composables/use-option.ts b/packages/devui-vue/devui/select/src/composables/use-option.ts index 5b9b16330f..2a8b6df8c9 100644 --- a/packages/devui-vue/devui/select/src/composables/use-option.ts +++ b/packages/devui-vue/devui/select/src/composables/use-option.ts @@ -2,7 +2,7 @@ import { computed, inject, onBeforeMount, onBeforeUnmount, ref } from 'vue'; import { OptionProps, UseOptionReturnType } from '../select-types'; import { SELECT_TOKEN, OPTION_GROUP_TOKEN } from '../const'; import { className } from '../utils'; -import { useNamespace } from '../../../shared/hooks/use-namespace'; +import { useNamespace } from '@devui/shared/utils'; export default function useOption(props: OptionProps): UseOptionReturnType { const ns = useNamespace('select'); const select = inject(SELECT_TOKEN, null); @@ -22,23 +22,26 @@ export default function useOption(props: OptionProps): UseOptionReturnType { } }); + const isDisabled = computed(() => props.disabled || (optionGroup?.disabled ? true : false)); + const optionItem = computed(() => { return { name: props.name || props.value + '' || '', value: props.value, create: props.create, _checked: false, + disabled: isDisabled.value, }; }); - const isDisabled = computed(() => props.disabled || (optionGroup?.disabled ? true : false)); - const isObjectOption = ref(!!props.name); const selectOptionCls = computed(() => { return className(ns.e('item'), { active: isOptionSelected.value, disabled: isDisabled.value, + [ns.em('item', 'sm')]: select?.selectSize === 'sm', + [ns.em('item', 'lg')]: select?.selectSize === 'lg', }); }); diff --git a/packages/devui-vue/devui/select/src/composables/use-select-content.ts b/packages/devui-vue/devui/select/src/composables/use-select-content.ts index 1c4150602b..5fd5110443 100644 --- a/packages/devui-vue/devui/select/src/composables/use-select-content.ts +++ b/packages/devui-vue/devui/select/src/composables/use-select-content.ts @@ -1,8 +1,8 @@ import { computed, inject, ref, getCurrentInstance } from 'vue'; import { SELECT_TOKEN } from '../const'; -import { FORM_ITEM_TOKEN } from '../../../form'; +import { FORM_ITEM_TOKEN, STYLE_TOKEN } from '../../../form'; import { OptionObjectItem, UseSelectContentReturnType } from '../select-types'; -import { useNamespace } from '../../../shared/hooks/use-namespace'; +import { useNamespace } from '@devui/shared/utils'; import { className } from '../utils'; import { isFunction } from 'lodash'; import { createI18nTranslate } from '../../../locale/create'; @@ -11,11 +11,17 @@ export default function useSelectContent(): UseSelectContentReturnType { const ns = useNamespace('select'); const select = inject(SELECT_TOKEN); const formItemContext = inject(FORM_ITEM_TOKEN, undefined); + const styleType = inject(STYLE_TOKEN, undefined); const app = getCurrentInstance(); const t = createI18nTranslate('DSelect', app); const searchQuery = ref(''); + const singleSearchKey = ref(''); + const singleInputRef = ref(); + + const singlePlaceholderWidth = computed(() => (select?.dropdownWidth ? `${select?.dropdownWidth - 40}px` : 'auto')); + const selectedData = computed(() => { return select?.selectedOptions || []; }); @@ -36,12 +42,27 @@ export default function useSelectContent(): UseSelectContentReturnType { if (select?.selectedOptions) { return select.selectedOptions.length > 1 ? select.selectedOptions.map((item) => item?.name || item?.value || '').join(',') - : select.selectedOptions[0]?.name || ''; + : select.selectedOptions[0]?.name || (select.showEmptyWhenUnmatched ? '' : select.modelValue); } else { return ''; } }); + const isPlaceholderDark = computed(() => { + if (!singleSearchKey.value) { + if (isSelectDisable.value) { + return false; + } + if (!displayInputValue.value) { + return true; + } else { + return select?.isSelectFocus; + } + } else { + return false; + } + }); + // 是否可清空 const mergeClearable = computed(() => { return !isSelectDisable.value && !!select?.allowClear && (displayInputValue.value ? true : false); @@ -52,10 +73,13 @@ export default function useSelectContent(): UseSelectContentReturnType { return !isSupportTagsTooltip.value || !!select?.isOpen; }); + const isSupportFilter = computed(() => isFunction(select?.filter) || (typeof select?.filter === 'boolean' && select?.filter)); + const selectionCls = computed(() => { return className(ns.e('selection'), { [ns.e('clearable')]: mergeClearable.value, [ns.em('selection', 'error')]: isValidateError.value, + [ns.em('selection', 'gray-style')]: styleType === 'gray', }); }); @@ -70,14 +94,24 @@ export default function useSelectContent(): UseSelectContentReturnType { const placeholder = computed(() => (displayInputValue.value ? '' : select?.placeholder || t('placeholder'))); + const singlePlaceholder = computed(() => select?.placeholder || t('placeholder')); + const isMultiple = computed(() => !!select?.multiple); const handleClear = (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); + searchQuery.value = ''; + singleSearchKey.value = ''; select?.handleClear(); }; + const onSingleInputWrapClick = () => { + if (!select?.selectDisabled) { + singleInputRef.value.focus(); + } + }; + const tagDelete = (data: OptionObjectItem) => { if (data && (data.value || data.value === 0)) { select?.tagDelete(data); @@ -89,35 +123,65 @@ export default function useSelectContent(): UseSelectContentReturnType { }; const onBlur = (e: FocusEvent) => { + singleSearchKey.value = ''; select?.onBlur(e); }; + const onMultipleClick = () => { + if (select?.selectDisabled) { + return; + } + if (select?.isOpen) { + searchQuery.value = ''; + select?.onBlur(); + } else { + select?.onFocus(); + } + }; + + const onArrowClick = () => { + if (isMultiple.value) { + onMultipleClick(); + } + }; + const queryFilter = (e: Event) => { e.preventDefault(); e.stopPropagation(); const query = (e.target as HTMLInputElement).value; + singleSearchKey.value = query; + searchQuery.value = query; if (!isReadOnly.value && select?.debounceQueryFilter) { select?.debounceQueryFilter(query); } }; return { + singleInputRef, searchQuery, + singleSearchKey, selectedData, isSelectDisable, isSupportCollapseTags, isDisabledTooltip, + isSupportFilter, isReadOnly, selectionCls, inputCls, tagSize, placeholder, + singlePlaceholder, + singlePlaceholderWidth, isMultiple, displayInputValue, + isPlaceholderDark, + onSingleInputWrapClick, handleClear, tagDelete, onFocus, onBlur, + onMultipleClick, + onArrowClick, queryFilter, }; } diff --git a/packages/devui-vue/devui/select/src/composables/use-select-menu-size.ts b/packages/devui-vue/devui/select/src/composables/use-select-menu-size.ts new file mode 100644 index 0000000000..e56bf01271 --- /dev/null +++ b/packages/devui-vue/devui/select/src/composables/use-select-menu-size.ts @@ -0,0 +1,34 @@ +import { ref, onMounted, onBeforeUnmount } from 'vue'; +import type { Ref } from 'vue'; + +export function useSelectMenuSize(selectRef: Ref, dropdownRef: Ref, isOpen: Ref) { + const originRef = ref(); + const dropdownWidth = ref(0); + let observer: ResizeObserver; + + const updateDropdownWidth = () => { + dropdownWidth.value = originRef.value?.getBoundingClientRect().width || 0; + if (isOpen.value) { + dropdownRef.value.updatePosition(); + } + }; + + const watchInputSize = () => { + if (window) { + observer = new window.ResizeObserver(updateDropdownWidth); + observer.observe(originRef.value); + } + }; + + onMounted(() => { + originRef.value = selectRef.value.$el; + watchInputSize(); + updateDropdownWidth(); + }); + + onBeforeUnmount(() => { + observer?.unobserve(originRef.value); + }); + + return { originRef, dropdownWidth }; +} diff --git a/packages/devui-vue/devui/select/src/select-types.ts b/packages/devui-vue/devui/select/src/select-types.ts index c48cebe806..b242c44b55 100644 --- a/packages/devui-vue/devui/select/src/select-types.ts +++ b/packages/devui-vue/devui/select/src/select-types.ts @@ -10,12 +10,13 @@ export interface OptionObjectItem { export type OptionItem = number | string | ({ value: string | number } & Partial); export type Options = Array; -export type ModelValue = number | string | Array; +export type ModelValue = number | string | Array | boolean; export type filterValue = boolean | ((query: string) => void); export type SelectSize = 'sm' | 'md' | 'lg'; +export type Position = 'top' | 'right' | 'bottom' | 'left'; export const selectProps = { modelValue: { - type: [String, Number, Array] as PropType, + type: [String, Number, Array, Boolean] as PropType, default: '', }, 'onUpdate:modelValue': { @@ -30,6 +31,10 @@ export const selectProps = { type: String as PropType, default: '', }, + position: { + type: Array as PropType, + default: () => ['bottom', 'top'], + }, // TODO: 这个api命名不合理 overview: { type: String as PropType<'border' | 'underlined'>, @@ -103,6 +108,10 @@ export const selectProps = { type: Number, default: 0, }, + showEmptyWhenUnmatched: { + type: Boolean, + default: true, + }, } as const; export type SelectProps = ExtractPropTypes; @@ -112,7 +121,6 @@ export type OptionModelValue = number | string; export interface UseSelectReturnType { selectDisabled: ComputedRef; selectSize: ComputedRef; - originRef: Ref; dropdownRef: Ref; isOpen: Ref; selectCls: ComputedRef; @@ -139,14 +147,16 @@ export interface SelectContext extends SelectProps { selectDisabled: boolean; selectSize: string; isOpen: boolean; + isSelectFocus: boolean; selectedOptions: OptionObjectItem[]; filterQuery: string; + dropdownWidth: number; valueChange: (item: OptionObjectItem) => void; handleClear: () => void; updateInjectOptions: (item: Record, operation: string, isObject: boolean) => void; tagDelete: (data: OptionObjectItem) => void; - onFocus: (e: FocusEvent) => void; - onBlur: (e: FocusEvent) => void; + onFocus: () => void; + onBlur: () => void; debounceQueryFilter: (query: string) => void; } @@ -179,23 +189,32 @@ export interface UseOptionReturnType { } export interface UseSelectContentReturnType { + singleInputRef: Ref; searchQuery: Ref; + singleSearchKey: Ref; selectedData: ComputedRef; isSelectDisable: ComputedRef; isSupportCollapseTags: ComputedRef; isDisabledTooltip: ComputedRef; isReadOnly: ComputedRef; + isSupportFilter: ComputedRef; selectionCls: ComputedRef; inputCls: ComputedRef; tagSize: ComputedRef; placeholder: ComputedRef; + singlePlaceholder: ComputedRef; + singlePlaceholderWidth: ComputedRef; isMultiple: ComputedRef; displayInputValue: ComputedRef; + isPlaceholderDark: ComputedRef; handleClear: (e: MouseEvent) => void; + onSingleInputWrapClick: () => void; + onMultipleClick: (e: any) => void; tagDelete: (data: OptionObjectItem) => void; onFocus: (e: FocusEvent) => void; onBlur: (e: FocusEvent) => void; queryFilter: (e: Event) => void; + onArrowClick: () => void; } export interface UseSelectFunctionReturn { diff --git a/packages/devui-vue/devui/select/src/select.scss b/packages/devui-vue/devui/select/src/select.scss index e849c8a675..15fec2dc42 100644 --- a/packages/devui-vue/devui/select/src/select.scss +++ b/packages/devui-vue/devui/select/src/select.scss @@ -2,9 +2,9 @@ $border-change-time: 300ms; $border-change-function: cubic-bezier(0.645, 0.045, 0.355, 1); -$select-input-height-sm: $devui-size-sm; -$select-input-height-md: $devui-size-md; -$select-input-height-lg: $devui-size-lg; +$select-input-height-sm: 28px; +$select-input-height-md: 32px; +$select-input-height-lg: 40px; $select-arrow-width: 28px; $transition-base-time: 0.25s; $select-dropdown-max-height: 300px; @@ -40,8 +40,9 @@ $select-item-min-height: 36px; .#{$devui-prefix}-tag { margin: 4px 0 4px 4px; .#{$devui-prefix}-tag__item.#{$devui-prefix}-tag--md { - height: 18px; - line-height: 16px; + height: 20px; + line-height: 20px; + font-size: 12px; } .#{$devui-prefix}-tag__item.#{$devui-prefix}-tag--default { @@ -59,10 +60,26 @@ $select-item-min-height: 36px; line-height: 16px; } } + + .#{$devui-prefix}-select__selection { + .single-inner-input { + .input-placeholder { + top: 3px; + } + } + } } &--lg { font-size: $devui-font-size-lg; + + .#{$devui-prefix}-select__selection { + .single-inner-input { + .input-placeholder { + top: 9px; + } + } + } } } @@ -98,6 +115,10 @@ $select-item-min-height: 36px; background-color: $devui-disabled-bg; border-color: $devui-disabled-line; color: $devui-disabled-text; + + .single-inner-input .input-placeholder { + cursor: not-allowed; + } } .#{$devui-prefix}-select__arrow, @@ -112,7 +133,9 @@ $select-item-min-height: 36px; .#{$devui-prefix}-select--open { .#{$devui-prefix}-select__arrow { - transform: rotate3d(0, 0, 1, 180deg); + svg { + transform: rotate3d(0, 0, 1, 180deg); + } } } @@ -143,9 +166,33 @@ $select-item-min-height: 36px; border-color: $devui-danger-line; } } + + .single-inner-input { + position: relative; + width: 100%; + + .input-placeholder { + position: absolute; + top: 5px; + left: 8px; + line-height: 20px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + cursor: pointer; + font-family: inherit; + user-select: none; + + &.placeholder-dark { + color: $devui-placeholder; + } + } + } } .#{$devui-prefix}-select__multiple { + width: 100%; + min-height: 30px; display: flex; align-items: center; flex-wrap: wrap; @@ -155,14 +202,33 @@ $select-item-min-height: 36px; display: flex; max-width: 100%; min-width: 15px; + flex: 1; } + + .multiple-tag { + max-width: 100%; + + &.disabled > .#{$devui-prefix}-tag__item { + background-color: $devui-disabled-bg; + border: 1px solid $devui-disabled-line; + color: $devui-disabled-text; + line-height: 18px; + cursor: not-allowed; + } + } +} + +.popover-tag:not(:last-of-type) { + max-width: 100%; + margin-right: 4px; } .#{$devui-prefix}-select__input { cursor: pointer; width: 100%; height: calc($select-input-height-md - 2px); - padding: 4px $select-arrow-width 4px 10px; + padding: 4px $select-arrow-width 4px 8px; + font-size: $devui-font-size; color: $devui-text; vertical-align: middle; outline: none; @@ -171,6 +237,10 @@ $select-item-min-height: 36px; border: none; @include border-transition(); + &::placeholder { + color: $devui-placeholder; + } + &:not([disabled]):not(.disabled) { &:hover { border-color: $devui-form-control-line-hover; @@ -192,10 +262,12 @@ $select-item-min-height: 36px; } &.#{$devui-prefix}-select__input--lg { + font-size: $devui-font-size-lg; height: calc($select-input-height-lg - 2px); } &.#{$devui-prefix}-select__input--sm { + font-size: $devui-font-size-sm; height: calc($select-input-height-sm - 2px); } } @@ -234,11 +306,13 @@ $select-item-min-height: 36px; } .#{$devui-prefix}-select__arrow { - transform: rotate3d(0, 0, 1, 0deg); - transition: transform $transition-base-time ease-out; + svg { + transform: rotate3d(0, 0, 1, 0deg); + transition: transform $transition-base-time ease-out; - svg path { - fill: $devui-icon-text; + path { + fill: $devui-icon-text; + } } } @@ -254,8 +328,8 @@ $select-item-min-height: 36px; max-height: $select-dropdown-max-height; width: 100%; overflow-y: auto; - padding: 0; margin: 0; + box-sizing: border-box; } .#{$devui-prefix}-select__item { @@ -270,6 +344,8 @@ $select-item-min-height: 36px; white-space: nowrap; text-overflow: ellipsis; border: 0; + margin-bottom: 4px; + border-radius: 2px; color: $devui-text; box-sizing: border-box; cursor: pointer; @@ -289,6 +365,28 @@ $select-item-min-height: 36px; background-color: $devui-disabled-bg; color: $devui-disabled-text; } + + &--sm { + font-size: $devui-font-size-sm; + } + + &--lg { + font-size: $devui-font-size-lg; + } +} + +.#{$devui-prefix}-select__dropdown--multiple { + .#{$devui-prefix}-select__item { + &:hover:not(.disabled) { + color: $devui-list-item-hover-text; + background-color: $devui-list-item-hover-bg; + } + + &.active { + color: $devui-text; + background-color: transparent; + } + } } .#{$devui-prefix}-select--sm .#{$devui-prefix}-select__item { @@ -377,3 +475,17 @@ $select-item-min-height: 36px; color: $devui-aide-text; } } + +.select-checkbox { + overflow: hidden; +} + +.#{$devui-prefix}-select__selection--gray-style:not(.#{$devui-prefix}-select__selection--error) { + background-color: $devui-gray-5; + border-color: $devui-gray-5; + + &:hover { + background-color: $devui-gray-10; + border-color: $devui-gray-10; + } +} diff --git a/packages/devui-vue/devui/select/src/select.tsx b/packages/devui-vue/devui/select/src/select.tsx index c6b7990258..1af13bb189 100644 --- a/packages/devui-vue/devui/select/src/select.tsx +++ b/packages/devui-vue/devui/select/src/select.tsx @@ -6,11 +6,11 @@ import { Transition, toRefs, getCurrentInstance, - onMounted, + onBeforeMount, Teleport, - watch, withModifiers, - onUnmounted, + onMounted, + nextTick, } from 'vue'; import type { SetupContext } from 'vue'; import useSelect from './use-select'; @@ -18,17 +18,18 @@ import { selectProps, SelectProps, SelectContext } from './select-types'; import { SELECT_TOKEN } from './const'; import { Checkbox } from '../../checkbox'; import Option from './components/option'; -import { useNamespace } from '../../shared/hooks/use-namespace'; +import { useNamespace } from '@devui/shared/utils'; import SelectContent from './components/select-content'; import useSelectFunction from './composables/use-select-function'; +import { useSelectMenuSize } from './composables/use-select-menu-size'; import './select.scss'; import { createI18nTranslate } from '../../locale/create'; -import { FlexibleOverlay, Placement } from '../../overlay'; +import { FlexibleOverlay } from '../../overlay'; export default defineComponent({ name: 'DSelect', props: selectProps, - emits: ['toggle-change', 'value-change', 'update:modelValue', 'focus', 'blur', 'remove-tag', 'clear'], + emits: ['toggle-change', 'value-change', 'update:modelValue', 'focus', 'blur', 'remove-tag', 'clear', 'load-more'], setup(props: SelectProps, ctx: SetupContext) { const app = getCurrentInstance(); const t = createI18nTranslate('DSelect', app); @@ -38,7 +39,6 @@ export default defineComponent({ const { selectDisabled, selectSize, - originRef, dropdownRef, isOpen, selectCls, @@ -59,10 +59,15 @@ export default defineComponent({ toggleChange, isShowCreateOption, } = useSelect(props, selectRef, ctx, focus, blur, isSelectFocus, t); + const dropdownContainer = ref(); + const { originRef, dropdownWidth } = useSelectMenuSize(selectRef, dropdownRef, isOpen); const scrollbarNs = useNamespace('scrollbar'); const ns = useNamespace('select'); - const dropdownCls = ns.e('dropdown'); + const dropdownCls = { + [ns.e('dropdown')]: true, + [ns.em('dropdown', 'multiple')]: props.multiple, + }; const listCls = { [ns.e('dropdown-list')]: true, [scrollbarNs.b()]: true, @@ -70,28 +75,23 @@ export default defineComponent({ const dropdownEmptyCls = ns.em('dropdown', 'empty'); ctx.expose({ focus, blur, toggleChange }); const isRender = ref(false); - const position = ref(['bottom-start', 'top-start']); - const dropdownWidth = ref('0'); - const updateDropdownWidth = () => { - dropdownWidth.value = originRef?.value?.clientWidth ? originRef.value.clientWidth + 'px' : '100%'; - }; + onBeforeMount(() => { + isRender.value = true; + }); - watch(selectRef, (val) => { - if (val) { - originRef.value = val.$el; - updateDropdownWidth(); + const scrollToBottom = () => { + const compareHeight = dropdownContainer.value.scrollHeight - dropdownContainer.value.clientHeight; + const scrollTop = dropdownContainer.value.scrollTop; + if (scrollTop === compareHeight) { + ctx.emit('load-more'); } - }); + }; onMounted(() => { - isRender.value = true; - updateDropdownWidth(); - window.addEventListener('resize', updateDropdownWidth); - }); - - onUnmounted(() => { - window.removeEventListener('resize', updateDropdownWidth); + nextTick(() => { + dropdownContainer.value.addEventListener('scroll', scrollToBottom); + }); }); provide( @@ -101,8 +101,10 @@ export default defineComponent({ selectDisabled, selectSize, isOpen, + isSelectFocus, selectedOptions, filterQuery, + dropdownWidth, valueChange, handleClear, updateInjectOptions, @@ -118,7 +120,7 @@ export default defineComponent({ class={selectCls.value} onClick={withModifiers(() => { toggleChange(!isOpen.value); - }, ['stop'])}> + }, [])}> @@ -126,15 +128,19 @@ export default defineComponent({ v-model={isRender.value} ref={dropdownRef} origin={originRef.value} - align="start" offset={4} - position={position.value} + place-strategy="no-space" + position={props.position} style={{ visibility: isOpen.value ? 'visible' : 'hidden', 'z-index': isOpen.value ? 'var(--devui-z-index-dropdown, 1052)' : -1, }}> -
-
    +
    +
      {isShowCreateOption.value && (