@veltra/desktop 1.3.6 → 1.3.8
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.
- package/dist/components/action/action-group.vue.d.ts +1 -1
- package/dist/components/action/action.vue.d.ts +1 -1
- package/dist/components/auto-complete/auto-complete.vue.d.ts +1 -1
- package/dist/components/cascade/cascade.vue.d.ts +1 -1
- package/dist/components/empty/empty.vue.d.ts +1 -1
- package/dist/components/multi-select/multi-select.vue.d.ts +1 -1
- package/dist/components/multi-tree-select/multi-tree-select.vue.d.ts +2 -2
- package/dist/components/number-range-input/number-range-input.vue.d.ts +1 -1
- package/dist/components/select/select.js +42 -51
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/select/select.vue.d.ts +4 -12
- package/dist/components/textarea/textarea.vue.d.ts +1 -1
- package/dist/components/tree-select/tree-select.js +40 -33
- package/dist/components/tree-select/tree-select.js.map +1 -1
- package/dist/components/tree-select/tree-select.vue.d.ts +2 -2
- package/dist/types/select.d.ts +5 -4
- package/dist/types/tree-select.d.ts +4 -2
- package/package.json +6 -6
- package/src/components/select/__test__/select.test.ts +77 -2
- package/src/components/select/select.vue +39 -47
- package/src/components/tree-select/__test__/tree-select.test.ts +133 -0
- package/src/components/tree-select/tree-select.vue +44 -38
- package/src/types/select.ts +5 -4
- package/src/types/tree-select.ts +4 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tree-select.js","names":["$attrs","$slots"],"sources":["../../../src/components/tree-select/tree-select.vue"],"sourcesContent":["<template>\n <u-dropdown\n v-if=\"!readonly\"\n v-bind=\"$attrs\"\n :class=\"[cls.b, bem.is('disabled', disabled)]\"\n trigger=\"click\"\n :content-class=\"[cls.e('panel'), cls.em('panel', size), contentClass]\"\n :content-style=\"contentStyle\"\n ref=\"dropdownRef\"\n v-model:visible=\"dropdownVisible\"\n :min-width=\"minWidth\"\n :width=\"width\"\n :disabled=\"disabled\"\n @update:visible=\"handleDropdownVisible\"\n @mouseenter=\"hovered = true\"\n @mouseleave=\"hovered = false\"\n >\n <template #trigger>\n <u-input\n ref=\"inputRef\"\n :size=\"size\"\n :disabled=\"disabled\"\n :placeholder=\"inputPlaceholder\"\n :clearable=\"false\"\n :model-value=\"inputValue\"\n @update:model-value=\"handleQueryInput\"\n :native-readonly=\"!filterable || !querying\"\n @click.capture=\"handleTriggerClickCapture\"\n >\n <template #prefix v-if=\"$slots.prefix\">\n <slot name=\"prefix\" />\n </template>\n <template #suffix>\n <transition name=\"zoom-in\" mode=\"out-in\">\n <u-icon\n v-if=\"showClear\"\n :class=\"cls.e('clear')\"\n title=\"清除\"\n key=\"clear\"\n @click.stop=\"handleClear\"\n >\n <Close />\n </u-icon>\n <u-icon v-else :class=\"cls.e('arrow')\" key=\"arrow\"><ArrowDown /></u-icon>\n </transition>\n </template>\n </u-input>\n </template>\n <template #content>\n <!-- 菜单列表 -->\n\n <u-tree\n v-bind=\"treeProps\"\n :selected=\"model\"\n @update:selected=\"handleSelect\"\n ref=\"treeRef\"\n selectable\n :class=\"cls.e('content-tree')\"\n :slots=\"slots\"\n scroll-to-view\n />\n </template>\n </u-dropdown>\n\n <template v-else>\n {{ text || label || FORM_EMPTY_CONTENT }}\n </template>\n</template>\n\n<script lang=\"ts\" setup>\nimport { dfs, o } from '@cat-kit/core'\nimport { useFormFallbackProps } from '@veltra/compositions'\nimport { ArrowDown, Close } from '@veltra/icons/normal'\nimport { bem, fieldKey, FORM_EMPTY_CONTENT } from '@veltra/utils'\nimport { injectFormContext } from '@veltra/utils'\nimport { computed, nextTick, shallowRef, useTemplateRef, watch } from 'vue'\n\nimport type { TreeSelectProps, TreeSelectEmits, TreeExposed, InputExposed } from '../../types'\nimport { UDropdown } from '../dropdown'\nimport { UIcon } from '../icon'\nimport { UInput } from '../input'\nimport { UTree } from '../tree'\nimport type { TreeSlotsScope } from '../tree/di'\n\ndefineOptions({ name: 'UTreeSelect', inheritAttrs: false })\n\nconst props = withDefaults(defineProps<TreeSelectProps>(), {\n labelKey: 'label',\n valueKey: 'value',\n placeholder: '请选择',\n expandAll: false,\n clearable: true,\n disabled: undefined,\n readonly: undefined,\n filterable: false,\n minWidth: '280px'\n})\n\nconst emit = defineEmits<TreeSelectEmits>()\n\nconst slots = defineSlots<{\n /** 默认插槽 */\n default?: (props: TreeSlotsScope) => any\n /** 前缀插槽 */\n prefix?: () => any\n}>()\n\nconst treeProps = computed(() => {\n return o(props as Record<string, any>).omit([\n 'tips',\n 'field',\n 'placeholder',\n 'disabled',\n 'label',\n 'readonly',\n 'contentClass',\n 'contentStyle',\n 'minWidth',\n 'width'\n ])\n})\n\nconst cls = bem('tree-select')\n\nconst labelKey = computed(() => fieldKey(props.labelKey, 'label'))\nconst valueKey = computed(() => fieldKey(props.valueKey, 'value'))\n\n/**过滤 */\nconst qs = shallowRef('')\nwatch(qs, (qs) => {\n treeRef.value?.filter(qs)\n})\n\nconst model = defineModel<string | number>()\n\nconst label = shallowRef<string>()\n\nconst dropdownVisible = shallowRef(false)\n\n/**\n * 是否处于查询态:决定触发输入框显示查询串还是选中标签。\n * 与面板可见状态解耦 —— 选择后立即退出查询态,\n * 不等面板关闭动画结束(否则显示值会延迟一个动画时长才恢复)。\n */\nconst querying = shallowRef(false)\n\nconst inputRef = useTemplateRef<InputExposed>('inputRef')\n\n/** 触发输入框的值:查询态下承载查询串,否则展示选中标签 */\nconst inputValue = computed(() => {\n if (props.filterable && querying.value) return qs.value\n return props.text ?? (model.value ? label.value : undefined)\n})\n\n/** 查询态下已选标签降级为占位提示 */\nconst inputPlaceholder = computed(() => {\n const display = props.text ?? (model.value ? label.value : undefined)\n if (props.filterable && querying.value && display) {\n return display\n }\n return props.placeholder\n})\n\nfunction handleQueryInput(value: string) {\n if (!props.filterable) return\n qs.value = value\n}\n\nconst hovered = shallowRef(false)\n\n/** 悬停且存在选中值时展示清除按钮(替代下拉箭头) */\nconst showClear = computed(() => {\n if (!props.clearable || disabled.value || !hovered.value) return false\n return !!(props.text ?? (model.value ? label.value : undefined))\n})\n\n/**\n * 过滤模式下拦截输入区域的点击:\n * 阻止 dropdown 的 trigger 开合切换,保持面板展开以不中断输入。\n * 非输入区域(箭头、留白)放行,维持原开合行为。\n */\nfunction handleTriggerClickCapture(e: MouseEvent) {\n if (!props.filterable || disabled.value) return\n if (!(e.target instanceof HTMLInputElement)) return\n\n e.stopPropagation()\n if (!dropdownVisible.value) dropdownRef.value?.open()\n}\n\nwatch(dropdownVisible, (visible) => {\n if (visible && props.filterable) {\n // 面板展开后进入查询态并聚焦输入框,可以立即输入查询\n querying.value = true\n nextTick(() => inputRef.value?.el?.focus())\n } else if (!visible) {\n querying.value = false\n }\n})\n\nconst { formProps } = injectFormContext()\n\nconst { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props])\n\nconst treeRef = shallowRef<TreeExposed>()\n\nconst dropdownRef = shallowRef<InstanceType<typeof UDropdown>>()\n\n/**清空 */\nconst handleClear = () => {\n model.value = ''\n label.value = undefined\n emit('clear')\n emit('change', undefined)\n emit('update:text', '')\n}\n\nlet changedByEvent = false\nwatch(\n [() => props.data, model],\n ([data, model]) => {\n if (changedByEvent) return\n if (!data?.length || model === undefined) {\n label.value = undefined\n model = undefined\n return\n }\n\n let founded = false\n const childrenKey = props.childrenKey ?? 'children'\n data.some((item) => {\n dfs(\n item,\n (v) => {\n if (v[valueKey.value] === model) {\n label.value = v[labelKey.value]\n founded = true\n return true\n }\n },\n childrenKey\n )\n\n return founded\n })\n },\n { immediate: true }\n)\n\nconst handleSelect = (selected?: string | number, selectedData?: Record<string, any>) => {\n changedByEvent = true\n nextTick(() => {\n changedByEvent = false\n })\n\n model.value = selected ?? ''\n\n if (selectedData) {\n label.value = o(selectedData).get(labelKey.value)\n } else {\n label.value = ''\n }\n\n emit('change', selectedData)\n emit('update:text', label.value)\n // 立即退出查询态,输入框同步恢复显示选中标签\n querying.value = false\n qs.value = ''\n dropdownRef.value?.close()\n}\n\nconst handleDropdownVisible = (visible: boolean) => {\n if (!visible) {\n qs.value = ''\n }\n}\n</script>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tree-select.js","names":["$attrs","$slots"],"sources":["../../../src/components/tree-select/tree-select.vue"],"sourcesContent":["<template>\n <u-dropdown\n v-if=\"!readonly\"\n v-bind=\"$attrs\"\n :class=\"[cls.b, bem.is('disabled', disabled)]\"\n trigger=\"click\"\n :content-class=\"[cls.e('panel'), cls.em('panel', size), contentClass]\"\n :content-style=\"contentStyle\"\n ref=\"dropdownRef\"\n v-model:visible=\"dropdownVisible\"\n :min-width=\"minWidth\"\n :width=\"width\"\n :disabled=\"disabled\"\n @update:visible=\"handleDropdownVisible\"\n @mouseenter=\"hovered = true\"\n @mouseleave=\"hovered = false\"\n >\n <template #trigger>\n <u-input\n ref=\"inputRef\"\n :size=\"size\"\n :disabled=\"disabled\"\n :placeholder=\"inputPlaceholder\"\n :clearable=\"false\"\n :model-value=\"inputValue\"\n @update:model-value=\"handleQueryInput\"\n :native-readonly=\"!filterable || !querying\"\n @click.capture=\"handleTriggerClickCapture\"\n >\n <template #prefix v-if=\"$slots.prefix\">\n <slot name=\"prefix\" />\n </template>\n <template #suffix>\n <transition name=\"zoom-in\" mode=\"out-in\">\n <u-icon\n v-if=\"showClear\"\n :class=\"cls.e('clear')\"\n title=\"清除\"\n key=\"clear\"\n @click.stop=\"handleClear\"\n >\n <Close />\n </u-icon>\n <u-icon v-else :class=\"cls.e('arrow')\" key=\"arrow\"><ArrowDown /></u-icon>\n </transition>\n </template>\n </u-input>\n </template>\n <template #content>\n <!-- 菜单列表 -->\n\n <u-tree\n v-bind=\"treeProps\"\n :selected=\"model\"\n @update:selected=\"handleSelect\"\n ref=\"treeRef\"\n selectable\n :class=\"cls.e('content-tree')\"\n :slots=\"slots\"\n scroll-to-view\n />\n </template>\n </u-dropdown>\n\n <template v-else>\n {{ label || FORM_EMPTY_CONTENT }}\n </template>\n</template>\n\n<script lang=\"ts\" setup>\nimport { dfs, o } from '@cat-kit/core'\nimport { useFormFallbackProps, useUserAction } from '@veltra/compositions'\nimport { ArrowDown, Close } from '@veltra/icons/normal'\nimport { bem, fieldKey, FORM_EMPTY_CONTENT } from '@veltra/utils'\nimport { injectFormContext } from '@veltra/utils'\nimport { computed, nextTick, shallowRef, useTemplateRef, watch } from 'vue'\n\nimport type { TreeSelectProps, TreeSelectEmits, TreeExposed, InputExposed } from '../../types'\nimport { UDropdown } from '../dropdown'\nimport { UIcon } from '../icon'\nimport { UInput } from '../input'\nimport { UTree } from '../tree'\nimport type { TreeSlotsScope } from '../tree/di'\n\ndefineOptions({ name: 'UTreeSelect', inheritAttrs: false })\n\nconst props = withDefaults(defineProps<TreeSelectProps>(), {\n labelKey: 'label',\n valueKey: 'value',\n placeholder: '请选择',\n expandAll: false,\n clearable: true,\n disabled: undefined,\n readonly: undefined,\n filterable: false,\n minWidth: '280px'\n})\n\nconst emit = defineEmits<TreeSelectEmits>()\n\nconst slots = defineSlots<{\n /** 默认插槽 */\n default?: (props: TreeSlotsScope) => any\n /** 前缀插槽 */\n prefix?: () => any\n}>()\n\nconst treeProps = computed(() => {\n return o(props as Record<string, any>).omit([\n 'tips',\n 'field',\n 'placeholder',\n 'disabled',\n 'label',\n 'readonly',\n 'contentClass',\n 'contentStyle',\n 'minWidth',\n 'width'\n ])\n})\n\nconst cls = bem('tree-select')\n\nconst labelKey = computed(() => fieldKey(props.labelKey, 'label'))\nconst valueKey = computed(() => fieldKey(props.valueKey, 'value'))\n\n/**过滤 */\nconst qs = shallowRef('')\nwatch(qs, (qs) => {\n treeRef.value?.filter(qs)\n})\n\nconst model = defineModel<string | number>()\n\n/** 内部展示文案,仅由 data 推导;通过 update:text 单向通知父级 */\nconst label = shallowRef<string>()\n\nconst dropdownVisible = shallowRef(false)\n\n/**\n * 是否处于查询态:决定触发输入框显示查询串还是选中标签。\n * 与面板可见状态解耦 —— 选择后立即退出查询态,\n * 不等面板关闭动画结束(否则显示值会延迟一个动画时长才恢复)。\n */\nconst querying = shallowRef(false)\n\nconst inputRef = useTemplateRef<InputExposed>('inputRef')\n\n/** 更新内部文案;值变化时单向 emit update:text */\nfunction setLabel(next?: string) {\n if (label.value === next) return\n label.value = next\n emit('update:text', next)\n}\n\n/** 触发输入框的值:查询态下承载查询串,否则展示选中标签 */\nconst inputValue = computed(() => {\n if (props.filterable && querying.value) return qs.value\n return model.value || model.value === 0 ? label.value : undefined\n})\n\n/** 查询态下已选标签降级为占位提示 */\nconst inputPlaceholder = computed(() => {\n const display = model.value || model.value === 0 ? label.value : undefined\n if (props.filterable && querying.value && display) {\n return display\n }\n return props.placeholder\n})\n\nfunction handleQueryInput(value: string) {\n if (!props.filterable) return\n qs.value = value\n}\n\nconst hovered = shallowRef(false)\n\n/** 悬停且存在选中值时展示清除按钮(替代下拉箭头) */\nconst showClear = computed(() => {\n if (!props.clearable || disabled.value || !hovered.value) return false\n return !!(model.value || model.value === 0 ? label.value : undefined)\n})\n\n/**\n * 过滤模式下拦截输入区域的点击:\n * 阻止 dropdown 的 trigger 开合切换,保持面板展开以不中断输入。\n * 非输入区域(箭头、留白)放行,维持原开合行为。\n */\nfunction handleTriggerClickCapture(e: MouseEvent) {\n if (!props.filterable || disabled.value) return\n if (!(e.target instanceof HTMLInputElement)) return\n\n e.stopPropagation()\n if (!dropdownVisible.value) dropdownRef.value?.open()\n}\n\nwatch(dropdownVisible, (visible) => {\n if (visible && props.filterable) {\n // 面板展开后进入查询态并聚焦输入框,可以立即输入查询\n querying.value = true\n nextTick(() => inputRef.value?.el?.focus())\n } else if (!visible) {\n querying.value = false\n }\n})\n\nconst { formProps } = injectFormContext()\n\nconst { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props])\n\nconst treeRef = shallowRef<TreeExposed>()\n\nconst dropdownRef = shallowRef<InstanceType<typeof UDropdown>>()\n\nconst { userAction, isUserActive } = useUserAction()\n\n/** 清空 */\nconst handleClear = userAction(() => {\n model.value = ''\n setLabel(undefined)\n emit('clear')\n emit('change', undefined)\n})\n\n/** 外部回显:按 data 查找 label(O(n));用户动作期内跳过 */\nwatch(\n [() => props.data, model],\n ([data, modelVal]) => {\n if (isUserActive()) return\n if (!data?.length || modelVal === undefined || modelVal === null || modelVal === '') {\n setLabel(undefined)\n return\n }\n\n let nextLabel: string | undefined\n const childrenKey = props.childrenKey ?? 'children'\n data.some((item) => {\n let founded = false\n dfs(\n item,\n (v) => {\n if (v[valueKey.value] === modelVal) {\n nextLabel = v[labelKey.value]\n founded = true\n return true\n }\n },\n childrenKey\n )\n\n return founded\n })\n setLabel(nextLabel)\n },\n { immediate: true }\n)\n\n/** 用户选择:O(1) 写入值与文案,跳过 data 回显查找 */\nconst handleSelect = userAction(\n (selected?: string | number, selectedData?: Record<string, any>) => {\n model.value = selected ?? ''\n\n if (selectedData) {\n setLabel(o(selectedData).get(labelKey.value))\n } else {\n setLabel(undefined)\n }\n\n emit('change', selectedData)\n // 立即退出查询态,输入框同步恢复显示选中标签\n querying.value = false\n qs.value = ''\n dropdownRef.value?.close()\n }\n)\nconst handleDropdownVisible = (visible: boolean) => {\n if (!visible) {\n qs.value = ''\n }\n}\n</script>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsFA,MAAM,QAAQ;EAYd,MAAM,OAAO;EAEb,MAAM,QAAQ,SAAA;EAOd,MAAM,YAAY,eAAe;GAC/B,OAAO,EAAE,KAA4B,CAAC,CAAC,KAAK;IAC1C;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;GACF,CAAC;EACH,CAAC;EAED,MAAM,MAAM,IAAI,aAAa;EAE7B,MAAM,WAAW,eAAe,SAAS,MAAM,UAAU,OAAO,CAAC;EACjE,MAAM,WAAW,eAAe,SAAS,MAAM,UAAU,OAAO,CAAC;;EAGjE,MAAM,KAAK,WAAW,EAAE;EACxB,MAAM,KAAK,OAAO;GAChB,QAAQ,OAAO,OAAO,EAAE;EAC1B,CAAC;EAED,MAAM,QAAQ,SAA4B,SAAA,YAAC;;EAG3C,MAAM,QAAQ,WAAmB;EAEjC,MAAM,kBAAkB,WAAW,KAAK;;;;;;EAOxC,MAAM,WAAW,WAAW,KAAK;EAEjC,MAAM,WAAW,eAA6B,UAAU;;EAGxD,SAAS,SAAS,MAAe;GAC/B,IAAI,MAAM,UAAU,MAAM;GAC1B,MAAM,QAAQ;GACd,KAAK,eAAe,IAAI;EAC1B;;EAGA,MAAM,aAAa,eAAe;GAChC,IAAI,MAAM,cAAc,SAAS,OAAO,OAAO,GAAG;GAClD,OAAO,MAAM,SAAS,MAAM,UAAU,IAAI,MAAM,QAAQ,KAAA;EAC1D,CAAC;;EAGD,MAAM,mBAAmB,eAAe;GACtC,MAAM,UAAU,MAAM,SAAS,MAAM,UAAU,IAAI,MAAM,QAAQ,KAAA;GACjE,IAAI,MAAM,cAAc,SAAS,SAAS,SACxC,OAAO;GAET,OAAO,MAAM;EACf,CAAC;EAED,SAAS,iBAAiB,OAAe;GACvC,IAAI,CAAC,MAAM,YAAY;GACvB,GAAG,QAAQ;EACb;EAEA,MAAM,UAAU,WAAW,KAAK;;EAGhC,MAAM,YAAY,eAAe;GAC/B,IAAI,CAAC,MAAM,aAAa,SAAS,SAAS,CAAC,QAAQ,OAAO,OAAO;GACjE,OAAO,CAAC,EAAE,MAAM,SAAS,MAAM,UAAU,IAAI,MAAM,QAAQ,KAAA;EAC7D,CAAC;;;;;;EAOD,SAAS,0BAA0B,GAAe;GAChD,IAAI,CAAC,MAAM,cAAc,SAAS,OAAO;GACzC,IAAI,EAAE,EAAE,kBAAkB,mBAAmB;GAE7C,EAAE,gBAAgB;GAClB,IAAI,CAAC,gBAAgB,OAAO,YAAY,OAAO,KAAK;EACtD;EAEA,MAAM,kBAAkB,YAAY;GAClC,IAAI,WAAW,MAAM,YAAY;IAE/B,SAAS,QAAQ;IACjB,eAAe,SAAS,OAAO,IAAI,MAAM,CAAC;GAC5C,OAAO,IAAI,CAAC,SACV,SAAS,QAAQ;EAErB,CAAC;EAED,MAAM,EAAE,cAAc,kBAAkB;EAExC,MAAM,EAAE,MAAM,UAAU,aAAa,qBAAqB,CAAC,aAAa,CAAC,GAAG,KAAK,CAAC;EAElF,MAAM,UAAU,WAAwB;EAExC,MAAM,cAAc,WAA2C;EAE/D,MAAM,EAAE,YAAY,iBAAiB,cAAc;;EAGnD,MAAM,cAAc,iBAAiB;GACnC,MAAM,QAAQ;GACd,SAAS,KAAA,CAAS;GAClB,KAAK,OAAO;GACZ,KAAK,UAAU,KAAA,CAAS;EAC1B,CAAC;;EAGD,MACE,OAAO,MAAM,MAAM,KAAK,IACvB,CAAC,MAAM,cAAc;GACpB,IAAI,aAAa,GAAG;GACpB,IAAI,CAAC,MAAM,UAAU,aAAa,KAAA,KAAa,aAAa,QAAQ,aAAa,IAAI;IACnF,SAAS,KAAA,CAAS;IAClB;GACF;GAEA,IAAI;GACJ,MAAM,cAAc,MAAM,eAAe;GACzC,KAAK,MAAM,SAAS;IAClB,IAAI,UAAU;IACd,IACE,OACC,MAAM;KACL,IAAI,EAAE,SAAS,WAAW,UAAU;MAClC,YAAY,EAAE,SAAS;MACvB,UAAU;MACV,OAAO;KACT;IACF,GACA,WACF;IAEA,OAAO;GACT,CAAC;GACD,SAAS,SAAS;EACpB,GACA,EAAE,WAAW,KAAK,CACpB;;EAGA,MAAM,eAAe,YAClB,UAA4B,iBAAuC;GAClE,MAAM,QAAQ,YAAY;GAE1B,IAAI,cACF,SAAS,EAAE,YAAY,CAAC,CAAC,IAAI,SAAS,KAAK,CAAC;QAE5C,SAAS,KAAA,CAAS;GAGpB,KAAK,UAAU,YAAY;GAE3B,SAAS,QAAQ;GACjB,GAAG,QAAQ;GACX,YAAY,OAAO,MAAM;EAC3B,CACF;EACA,MAAM,yBAAyB,YAAqB;GAClD,IAAI,CAAC,SACH,GAAG,QAAQ;EAEf;;WAtRW,MAAA,QAAA,KAAA,UAAA,GADT,YA6Da,MAAA,WAAA,GA7Db,WA6Da,EAAA,KAAA,EAAA,GA3DHA,KAAAA,QAAM;IACb,OAAK,CAAG,MAAA,GAAA,CAAG,CAAC,GAAG,MAAA,GAAA,CAAG,CAAC,GAAE,YAAa,MAAA,QAAA,CAAQ,CAAA;IAC3C,SAAQ;IACP,iBAAa;KAAG,MAAA,GAAA,CAAG,CAAC,EAAC,OAAA;KAAW,MAAA,GAAA,CAAG,CAAC,GAAE,SAAU,MAAA,IAAA,CAAI;KAAG,QAAA;IAAY;IACnE,iBAAe,QAAA;aACZ;IAAJ,KAAI;IACI,SAAS,gBAAA;+DAAA,gBAAe,QAAA,SAIf,qBAAA;IAHhB,aAAW,QAAA;IACX,OAAO,QAAA;IACP,UAAU,MAAA,QAAA;IAEV,cAAU,OAAA,OAAA,OAAA,MAAA,WAAE,QAAA,QAAO;IACnB,cAAU,OAAA,OAAA,OAAA,MAAA,WAAE,QAAA,QAAO;;IAET,SAAO,cA6BN,CA5BV,YA4BU,MAAA,aAAA,GAAA;cA3BJ;KAAJ,KAAI;KACH,MAAM,MAAA,IAAA;KACN,UAAU,MAAA,QAAA;KACV,aAAa,iBAAA;KACb,WAAW;KACX,eAAa,WAAA;KACb,uBAAoB;KACpB,mBAAe,CAAG,QAAA,cAAU,CAAK,SAAA;qBAClB;;KAKL,QAAM,cAYF,CAXb,YAWa,YAAA;MAXD,MAAK;MAAU,MAAK;;6BASrB,CAPD,UAAA,SAAA,UAAA,GADR,YAQS,MAAA,WAAA,GAAA;OANN,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,OAAA,CAAA;OACb,OAAM;OACN,KAAI;OACH,SAAK,cAAO,MAAA,WAAA,GAAW,CAAA,MAAA,CAAA;;8BAEf,CAAT,YAAS,MAAA,KAAA,CAAA,CAAA,CAAA;;mDAEX,YAAyE,MAAA,WAAA,GAAA;OAAzD,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,OAAA,CAAA;OAAW,KAAI;;8BAAqB,CAAb,YAAa,MAAA,SAAA,CAAA,CAAA,CAAA;;;;;;QAd5CC,KAAAA,OAAO,SAAA;WAApB;uBACa,CAAtB,WAAsB,KAAA,QAAA,QAAA,CAAA,CAAA;;;;;;;;;IAkBjB,SAAO,cACH,CAAb,mBAAA,QAAA,GAEA,YASE,MAAA,WAAA,GATF,WACU,UAQR,OARiB;KAChB,UAAU,MAAA;KACV,qBAAiB,MAAA,YAAA;cACd;KAAJ,KAAI;KACJ,YAAA;KACC,OAAO,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA;KACL;KACR,kBAAA;;;;;;;;;;;;;;;uBAKN,mBAEW,UAAA,EAAA,KAAA,EAAA,GAAA,CAAA,gBAAA,gBADN,MAAA,SAAS,MAAA,kBAAA,CAAkB,GAAA,CAAA,CAAA,GAAA,EAAA"}
|
|
@@ -31,10 +31,10 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
|
|
|
31
31
|
valueKey: string;
|
|
32
32
|
placeholder: string;
|
|
33
33
|
clearable: boolean;
|
|
34
|
-
filterable: boolean;
|
|
35
|
-
minWidth: string;
|
|
36
34
|
disabled: boolean;
|
|
37
35
|
readonly: boolean;
|
|
36
|
+
filterable: boolean;
|
|
37
|
+
minWidth: string;
|
|
38
38
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
39
39
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
40
40
|
declare const _default: typeof __VLS_export;
|
package/dist/types/select.d.ts
CHANGED
|
@@ -5,8 +5,6 @@ import { DeconstructValue, FormComponentProps } from "@veltra/utils";
|
|
|
5
5
|
interface SelectProps extends FormComponentProps {
|
|
6
6
|
/** 绑定值 */
|
|
7
7
|
modelValue?: any;
|
|
8
|
-
/** 文本内容 */
|
|
9
|
-
text?: string;
|
|
10
8
|
/**
|
|
11
9
|
* 列表选项
|
|
12
10
|
* @description 如果传入一个函数,那么filterable会被强制启用
|
|
@@ -54,8 +52,11 @@ interface SelectProps extends FormComponentProps {
|
|
|
54
52
|
};
|
|
55
53
|
}
|
|
56
54
|
interface SelectEmits {
|
|
57
|
-
/**
|
|
58
|
-
|
|
55
|
+
/**
|
|
56
|
+
* 选中项文案变化(单向通知,用于同步父级冗余字段)
|
|
57
|
+
* @description 展示始终由 options 推导,请用 `@update:text` 而非 `v-model:text`
|
|
58
|
+
*/
|
|
59
|
+
(e: 'update:text', text?: string): void;
|
|
59
60
|
(e: 'update:modelValue', modelValue?: any): void;
|
|
60
61
|
(e: 'change', option?: Record<string, any>): void;
|
|
61
62
|
}
|
|
@@ -25,8 +25,6 @@ interface TreeSelectProps extends FormComponentProps, Omit<TreeProps, 'selected'
|
|
|
25
25
|
* @default 跟随触发元素的宽度
|
|
26
26
|
*/
|
|
27
27
|
width?: string;
|
|
28
|
-
/** 显示文本 */
|
|
29
|
-
text?: string;
|
|
30
28
|
/** 内容容器样式 */
|
|
31
29
|
contentStyle?: CSSProperties | string;
|
|
32
30
|
/** 内容容器类名 */
|
|
@@ -37,6 +35,10 @@ interface TreeSelectEmits {
|
|
|
37
35
|
(e: 'clear'): void;
|
|
38
36
|
(e: 'update:modelValue', value?: string | number): void;
|
|
39
37
|
(e: 'change', selectedData?: Record<string, any>): void;
|
|
38
|
+
/**
|
|
39
|
+
* 选中项文案变化(单向通知,用于同步父级冗余字段)
|
|
40
|
+
* @description 展示始终由 data 推导,请用 `@update:text` 而非 `v-model:text`
|
|
41
|
+
*/
|
|
40
42
|
(e: 'update:text', text?: string): void;
|
|
41
43
|
}
|
|
42
44
|
/** 树形选择器组件暴露的属性和方法(组件内部使用) */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veltra/desktop",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.8",
|
|
4
4
|
"files": [
|
|
5
5
|
"dist",
|
|
6
6
|
"src"
|
|
@@ -57,11 +57,11 @@
|
|
|
57
57
|
"@cat-kit/core": ">=1.1.8",
|
|
58
58
|
"@cat-kit/excel": ">=1.0.0",
|
|
59
59
|
"@cat-kit/fe": "^1.1.8",
|
|
60
|
-
"@veltra/compositions": "1.3.
|
|
61
|
-
"@veltra/directives": "1.3.
|
|
62
|
-
"@veltra/icons": "1.3.
|
|
63
|
-
"@veltra/styles": "1.3.
|
|
64
|
-
"@veltra/utils": "1.3.
|
|
60
|
+
"@veltra/compositions": "1.3.8",
|
|
61
|
+
"@veltra/directives": "1.3.8",
|
|
62
|
+
"@veltra/icons": "1.3.8",
|
|
63
|
+
"@veltra/styles": "1.3.8",
|
|
64
|
+
"@veltra/utils": "1.3.8",
|
|
65
65
|
"vue": ">=3.5.40"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { createApp, h, nextTick, ref } from 'vue'
|
|
2
|
+
import { createApp, h, nextTick, ref, type Ref } from 'vue'
|
|
3
3
|
|
|
4
4
|
import USelect from '../select.vue'
|
|
5
5
|
|
|
6
6
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
7
7
|
|
|
8
|
-
function mountSelect(props: Record<string, unknown>) {
|
|
8
|
+
function mountSelect(props: Record<string, unknown> = {}) {
|
|
9
9
|
const host = document.createElement('div')
|
|
10
10
|
document.body.appendChild(host)
|
|
11
11
|
|
|
12
12
|
const model = ref(props.modelValue)
|
|
13
|
+
const options = ref(props.options) as Ref<unknown>
|
|
14
|
+
const texts: (string | undefined)[] = []
|
|
15
|
+
|
|
13
16
|
const app = createApp({
|
|
14
17
|
render() {
|
|
15
18
|
return h(USelect, {
|
|
16
19
|
...props,
|
|
17
20
|
modelValue: model.value,
|
|
21
|
+
options: options.value as any,
|
|
18
22
|
'onUpdate:modelValue': (value: unknown) => {
|
|
19
23
|
model.value = value
|
|
24
|
+
},
|
|
25
|
+
'onUpdate:text': (text?: string) => {
|
|
26
|
+
texts.push(text)
|
|
20
27
|
}
|
|
21
28
|
})
|
|
22
29
|
}
|
|
@@ -27,6 +34,8 @@ function mountSelect(props: Record<string, unknown>) {
|
|
|
27
34
|
return {
|
|
28
35
|
host,
|
|
29
36
|
model,
|
|
37
|
+
options,
|
|
38
|
+
texts,
|
|
30
39
|
unmount() {
|
|
31
40
|
app.unmount()
|
|
32
41
|
host.remove()
|
|
@@ -107,4 +116,70 @@ describe('USelect', () => {
|
|
|
107
116
|
unmount()
|
|
108
117
|
}
|
|
109
118
|
})
|
|
119
|
+
|
|
120
|
+
it('emits update:text with option label when modelValue echoes against options', async () => {
|
|
121
|
+
const { host, texts, unmount } = mountSelect({
|
|
122
|
+
modelValue: 'beijing',
|
|
123
|
+
options: [
|
|
124
|
+
{ label: '北京(最新)', value: 'beijing' },
|
|
125
|
+
{ label: '上海(最新)', value: 'shanghai' }
|
|
126
|
+
]
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
await nextTick()
|
|
131
|
+
expect(texts.at(-1)).toBe('北京(最新)')
|
|
132
|
+
expect(host.querySelector('input')!.value).toBe('北京(最新)')
|
|
133
|
+
} finally {
|
|
134
|
+
unmount()
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('emits update:text on user select and clear', async () => {
|
|
139
|
+
const { host, model, texts, unmount } = mountSelect({
|
|
140
|
+
filterable: true,
|
|
141
|
+
options: [
|
|
142
|
+
{ label: '北京', value: 'beijing' },
|
|
143
|
+
{ label: '上海', value: 'shanghai' }
|
|
144
|
+
]
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
await openDropdown(host)
|
|
149
|
+
await clickOption(queryOptions().find((el) => el.textContent === '上海')!)
|
|
150
|
+
expect(model.value).toBe('shanghai')
|
|
151
|
+
expect(texts.at(-1)).toBe('上海')
|
|
152
|
+
|
|
153
|
+
// 清除:父级清空值时回显同步发出 update:text
|
|
154
|
+
model.value = undefined
|
|
155
|
+
await nextTick()
|
|
156
|
+
await nextTick()
|
|
157
|
+
expect(texts.at(-1)).toBeUndefined()
|
|
158
|
+
expect(host.querySelector('input')!.value).toBe('')
|
|
159
|
+
} finally {
|
|
160
|
+
unmount()
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('emits update:text after async options arrive for an existing modelValue', async () => {
|
|
165
|
+
const { host, options, texts, unmount } = mountSelect({ modelValue: 'shanghai', options: [] })
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
await nextTick()
|
|
169
|
+
expect(texts).toHaveLength(0)
|
|
170
|
+
expect(host.querySelector('input')!.value).toBe('shanghai')
|
|
171
|
+
|
|
172
|
+
options.value = [
|
|
173
|
+
{ label: '北京(最新)', value: 'beijing' },
|
|
174
|
+
{ label: '上海(最新)', value: 'shanghai' }
|
|
175
|
+
]
|
|
176
|
+
await nextTick()
|
|
177
|
+
await nextTick()
|
|
178
|
+
|
|
179
|
+
expect(texts.at(-1)).toBe('上海(最新)')
|
|
180
|
+
expect(host.querySelector('input')!.value).toBe('上海(最新)')
|
|
181
|
+
} finally {
|
|
182
|
+
unmount()
|
|
183
|
+
}
|
|
184
|
+
})
|
|
110
185
|
})
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
|
|
121
121
|
<script lang="ts" setup>
|
|
122
122
|
import { o } from '@cat-kit/core'
|
|
123
|
-
import { useFormFallbackProps, useVirtualizer } from '@veltra/compositions'
|
|
123
|
+
import { useFormFallbackProps, useUserAction, useVirtualizer } from '@veltra/compositions'
|
|
124
124
|
import { ArrowDown, Close } from '@veltra/icons/normal'
|
|
125
125
|
import { bem, fieldKey, FORM_EMPTY_CONTENT, scrollIntoContainerView, withUnit } from '@veltra/utils'
|
|
126
126
|
import { injectFormContext } from '@veltra/utils'
|
|
@@ -177,7 +177,8 @@ const { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, prop
|
|
|
177
177
|
})
|
|
178
178
|
|
|
179
179
|
const currentIndex = shallowRef(-1)
|
|
180
|
-
|
|
180
|
+
/** 内部展示文案,仅由选项推导;通过 update:text 单向通知父级 */
|
|
181
|
+
const label = shallowRef<string>()
|
|
181
182
|
const selected = shallowRef<Record<string, any>>()
|
|
182
183
|
|
|
183
184
|
const displayedValue = computed(() => {
|
|
@@ -194,9 +195,8 @@ const filterable = computed(() => {
|
|
|
194
195
|
return props.filterable || typeof props.options === 'function'
|
|
195
196
|
})
|
|
196
197
|
|
|
197
|
-
const { queryString, options, temOptionsToCreatedOptions, clearCreatedOptions } =
|
|
198
|
-
props
|
|
199
|
-
})
|
|
198
|
+
const { queryString, options, allOptions, temOptionsToCreatedOptions, clearCreatedOptions } =
|
|
199
|
+
useOptions({ props })
|
|
200
200
|
|
|
201
201
|
const dropdownVisible = shallowRef(false)
|
|
202
202
|
|
|
@@ -246,57 +246,54 @@ function handleTriggerClickCapture(e: MouseEvent) {
|
|
|
246
246
|
if (!dropdownVisible.value) dropdownRef.value?.open()
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
|
|
250
|
-
let userSelecting = false
|
|
251
|
-
function lock() {
|
|
252
|
-
userSelecting = true
|
|
253
|
-
}
|
|
249
|
+
const { userAction, isUserActive } = useUserAction()
|
|
254
250
|
|
|
255
|
-
/**
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
userSelecting = false
|
|
261
|
-
// 选择/清除会同步重建选项列表(临时项转正、查询串清空),
|
|
262
|
-
// 重建触发的回显在锁定期间被跳过,解锁后按最新列表重同步一次,
|
|
263
|
-
// 避免高亮停留在选择前的旧索引上
|
|
264
|
-
const value = preferredValue !== undefined ? preferredValue : props.modelValue
|
|
265
|
-
syncSelected(value, options.value)
|
|
266
|
-
})
|
|
251
|
+
/** 更新内部文案;值变化时单向 emit update:text */
|
|
252
|
+
function setLabel(next?: string) {
|
|
253
|
+
if (label.value === next) return
|
|
254
|
+
label.value = next
|
|
255
|
+
emit('update:text', next)
|
|
267
256
|
}
|
|
268
257
|
|
|
269
|
-
/** 按 modelValue
|
|
270
|
-
function syncSelected(modelValue: any,
|
|
258
|
+
/** 按 modelValue 与完整选项列表同步高亮索引、选中项与显示标签(外部回显用,O(n)) */
|
|
259
|
+
function syncSelected(modelValue: any, sourceOptions: Record<string, any>[] | undefined) {
|
|
271
260
|
// 查询输入期间高亮第一项(创建模式下通常是临时项,便于回车创建)
|
|
272
261
|
if (queryString.value) {
|
|
273
|
-
currentIndex.value = options
|
|
262
|
+
currentIndex.value = options.value.length ? 0 : -1
|
|
274
263
|
return
|
|
275
264
|
}
|
|
276
265
|
|
|
277
|
-
if (!
|
|
266
|
+
if (!sourceOptions?.length) return
|
|
278
267
|
|
|
279
268
|
if (modelValue !== undefined && modelValue !== null && modelValue !== '') {
|
|
280
|
-
currentIndex.value =
|
|
281
|
-
|
|
282
|
-
|
|
269
|
+
currentIndex.value = sourceOptions.findIndex(
|
|
270
|
+
(option) => o(option).get(valueKey.value) === modelValue
|
|
271
|
+
)
|
|
272
|
+
selected.value = currentIndex.value >= 0 ? sourceOptions[currentIndex.value] : undefined
|
|
273
|
+
setLabel(selected.value ? o(selected.value).get(labelKey.value) : undefined)
|
|
283
274
|
} else {
|
|
284
275
|
currentIndex.value = -1
|
|
285
276
|
selected.value = undefined
|
|
286
|
-
|
|
277
|
+
setLabel(undefined)
|
|
287
278
|
}
|
|
288
279
|
}
|
|
289
280
|
|
|
290
|
-
//
|
|
281
|
+
// 回显:以 allOptions 匹配,避免过滤列表缺项时清掉已选文案;用户动作期内跳过
|
|
291
282
|
watch(
|
|
292
|
-
[() => props.modelValue,
|
|
293
|
-
([modelValue
|
|
294
|
-
if (
|
|
295
|
-
syncSelected(modelValue,
|
|
283
|
+
[() => props.modelValue, allOptions],
|
|
284
|
+
([modelValue]) => {
|
|
285
|
+
if (isUserActive()) return
|
|
286
|
+
syncSelected(modelValue, allOptions.value)
|
|
296
287
|
},
|
|
297
288
|
{ immediate: true }
|
|
298
289
|
)
|
|
299
290
|
|
|
291
|
+
// 过滤时单独同步高亮(allOptions 不变,不会触发上方回显 watch)
|
|
292
|
+
watch([queryString, options], () => {
|
|
293
|
+
if (!queryString.value) return
|
|
294
|
+
currentIndex.value = options.value.length ? 0 : -1
|
|
295
|
+
})
|
|
296
|
+
|
|
300
297
|
const baseVirtualEnabled = computed(() => options.value.length > 80)
|
|
301
298
|
const virtualEnabled = computed(() => baseVirtualEnabled.value && !props.grid)
|
|
302
299
|
|
|
@@ -364,14 +361,13 @@ const handleDropdownVisible = (visible: boolean) => {
|
|
|
364
361
|
}
|
|
365
362
|
}
|
|
366
363
|
|
|
367
|
-
/**
|
|
368
|
-
const handleSelect = (option: Record<string, any>, _index: number) => {
|
|
369
|
-
lock()
|
|
364
|
+
/** 单选:用户动作内 O(1) 写入值与文案,跳过 modelValue 回显的 O(n) 查找 */
|
|
365
|
+
const handleSelect = userAction((option: Record<string, any>, _index: number) => {
|
|
370
366
|
const value = option?.[valueKey.value]
|
|
371
367
|
selected.value = option
|
|
372
368
|
|
|
373
369
|
emit('update:modelValue', value)
|
|
374
|
-
|
|
370
|
+
setLabel(option?.[labelKey.value])
|
|
375
371
|
emit('change', option)
|
|
376
372
|
if (option.__isTemp) {
|
|
377
373
|
// 转正后列表同步重建为完整源(临时项置顶索引失效),必须按 value 重定位
|
|
@@ -386,21 +382,17 @@ const handleSelect = (option: Record<string, any>, _index: number) => {
|
|
|
386
382
|
selected.value = options.value[currentIndex.value]
|
|
387
383
|
}
|
|
388
384
|
dropdownRef.value?.close()
|
|
389
|
-
|
|
390
|
-
unlock(value)
|
|
391
|
-
}
|
|
385
|
+
})
|
|
392
386
|
|
|
393
387
|
/** 清除选项 */
|
|
394
|
-
const handleClear = () => {
|
|
395
|
-
lock()
|
|
388
|
+
const handleClear = userAction(() => {
|
|
396
389
|
selected.value = undefined
|
|
397
390
|
currentIndex.value = -1
|
|
398
|
-
|
|
391
|
+
setLabel(undefined)
|
|
399
392
|
clearCreatedOptions()
|
|
400
393
|
emit('update:modelValue', undefined)
|
|
401
394
|
emit('change', undefined)
|
|
402
|
-
|
|
403
|
-
}
|
|
395
|
+
})
|
|
404
396
|
|
|
405
397
|
function getCurrentEl() {
|
|
406
398
|
return scrollRef.value?.contentRef?.querySelector('li.is-selected') as HTMLElement | undefined
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { createApp, h, nextTick, ref, type Ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
import UTreeSelect from '../tree-select.vue'
|
|
5
|
+
|
|
6
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
7
|
+
|
|
8
|
+
const treeData = [
|
|
9
|
+
{
|
|
10
|
+
label: '北京',
|
|
11
|
+
value: 'beijing',
|
|
12
|
+
children: [
|
|
13
|
+
{ label: '朝阳区(最新)', value: 'chaoyang' },
|
|
14
|
+
{ label: '海淀区(最新)', value: 'haidian' }
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{ label: '上海(最新)', value: 'shanghai' }
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
function mountTreeSelect(props: Record<string, unknown> = {}) {
|
|
21
|
+
const host = document.createElement('div')
|
|
22
|
+
document.body.appendChild(host)
|
|
23
|
+
|
|
24
|
+
const model = ref(props.modelValue) as Ref<string | number | undefined>
|
|
25
|
+
const data = ref(props.data ?? treeData)
|
|
26
|
+
const texts: (string | undefined)[] = []
|
|
27
|
+
|
|
28
|
+
const app = createApp({
|
|
29
|
+
render() {
|
|
30
|
+
return h(UTreeSelect, {
|
|
31
|
+
...props,
|
|
32
|
+
modelValue: model.value,
|
|
33
|
+
data: data.value,
|
|
34
|
+
expandAll: true,
|
|
35
|
+
'onUpdate:modelValue': (value?: string | number) => {
|
|
36
|
+
model.value = value
|
|
37
|
+
},
|
|
38
|
+
'onUpdate:text': (text?: string) => {
|
|
39
|
+
texts.push(text)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
app.mount(host)
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
host,
|
|
49
|
+
model,
|
|
50
|
+
data,
|
|
51
|
+
texts,
|
|
52
|
+
unmount() {
|
|
53
|
+
app.unmount()
|
|
54
|
+
host.remove()
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function openDropdown(host: HTMLElement) {
|
|
60
|
+
const input = host.querySelector('input')!
|
|
61
|
+
input.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }))
|
|
62
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
63
|
+
await nextTick()
|
|
64
|
+
await sleep(25)
|
|
65
|
+
if (queryNodeContents().length > 0) return
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function queryNodeContents() {
|
|
69
|
+
return [...document.body.querySelectorAll<HTMLElement>('.u-tree__node-content')]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function clickNodeByLabel(label: string) {
|
|
73
|
+
const node = queryNodeContents().find((el) => el.textContent?.includes(label))
|
|
74
|
+
expect(node, `tree node "${label}" not found`).toBeTruthy()
|
|
75
|
+
node!.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
76
|
+
await nextTick()
|
|
77
|
+
await sleep(50)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe('UTreeSelect', () => {
|
|
81
|
+
it('emits update:text when modelValue echoes against data', async () => {
|
|
82
|
+
const { host, texts, unmount } = mountTreeSelect({ modelValue: 'chaoyang' })
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
await nextTick()
|
|
86
|
+
expect(texts.at(-1)).toBe('朝阳区(最新)')
|
|
87
|
+
expect(host.querySelector('input')!.value).toBe('朝阳区(最新)')
|
|
88
|
+
} finally {
|
|
89
|
+
unmount()
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('emits update:text after data arrives for an existing modelValue', async () => {
|
|
94
|
+
const { host, data, texts, unmount } = mountTreeSelect({ modelValue: 'shanghai', data: [] })
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
await nextTick()
|
|
98
|
+
expect(texts).toHaveLength(0)
|
|
99
|
+
|
|
100
|
+
data.value = treeData
|
|
101
|
+
await nextTick()
|
|
102
|
+
await nextTick()
|
|
103
|
+
|
|
104
|
+
expect(texts.at(-1)).toBe('上海(最新)')
|
|
105
|
+
expect(host.querySelector('input')!.value).toBe('上海(最新)')
|
|
106
|
+
} finally {
|
|
107
|
+
unmount()
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('emits update:text on user select and clear', async () => {
|
|
112
|
+
const { host, model, texts, unmount } = mountTreeSelect({
|
|
113
|
+
// filterable 下点击 input 会显式 open,避免非过滤态依赖冒泡开合的不稳定
|
|
114
|
+
filterable: true
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await openDropdown(host)
|
|
119
|
+
await clickNodeByLabel('上海(最新)')
|
|
120
|
+
expect(model.value).toBe('shanghai')
|
|
121
|
+
expect(texts.at(-1)).toBe('上海(最新)')
|
|
122
|
+
|
|
123
|
+
// 清除:父级清空值时回显同步发出 update:text
|
|
124
|
+
model.value = undefined
|
|
125
|
+
await nextTick()
|
|
126
|
+
await nextTick()
|
|
127
|
+
expect(texts.at(-1)).toBeUndefined()
|
|
128
|
+
expect(host.querySelector('input')!.value).toBe('')
|
|
129
|
+
} finally {
|
|
130
|
+
unmount()
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
})
|