diff --git a/examples/sites/demos/pc/app/base-select/all-text.spec.ts b/examples/sites/demos/pc/app/base-select/all-text.spec.ts index 39905a355b..b2766fb915 100644 --- a/examples/sites/demos/pc/app/base-select/all-text.spec.ts +++ b/examples/sites/demos/pc/app/base-select/all-text.spec.ts @@ -4,10 +4,10 @@ test('多选时自定义全部的文本', async ({ page }) => { page.on('pageerror', (exception) => expect(exception).toBeNull()) await page.goto('base-select#all-text') const wrap = page.locator('#all-text') - const select = wrap.locator('.tiny-base-select').nth(0) + const select = wrap.locator('.tiny-base-select') const dropdown = page.locator('body > .tiny-select-dropdown') const option = dropdown.locator('.tiny-option') - await select.locator('.tiny-input__suffix').click() + await select.click() await expect(option.filter({ hasText: '全部小吃' })).toHaveCount(1) }) diff --git a/examples/sites/demos/pc/app/base-select/basic-usage.spec.ts b/examples/sites/demos/pc/app/base-select/basic-usage.spec.ts index ff1e532181..128d9ea000 100644 --- a/examples/sites/demos/pc/app/base-select/basic-usage.spec.ts +++ b/examples/sites/demos/pc/app/base-select/basic-usage.spec.ts @@ -12,7 +12,7 @@ test('基础用法标签式', async ({ page }) => { await input.click() await option.filter({ hasText: '天津' }).click() await expect(input).toHaveValue('天津') - await select.locator('.tiny-input__suffix svg').click() + await input.click() await expect(page.getByRole('listitem').filter({ hasText: '天津' })).toHaveClass(/selected/) await option.filter({ hasText: '深圳' }).click() await expect(input).toHaveValue('深圳') @@ -35,7 +35,7 @@ test('基础用法配置式', async ({ page }) => { await input.click() await option.filter({ hasText: '天津' }).click() await expect(input).toHaveValue('天津') - await select.locator('.tiny-input__suffix svg').click() + await input.click() await expect(page.getByRole('listitem').filter({ hasText: '天津' })).toHaveClass(/selected/) await option.filter({ hasText: '深圳' }).click() await expect(input).toHaveValue('深圳') diff --git a/examples/sites/demos/pc/app/base-select/clearable.spec.ts b/examples/sites/demos/pc/app/base-select/clearable.spec.ts index 7906b0ed2a..dbadebf245 100644 --- a/examples/sites/demos/pc/app/base-select/clearable.spec.ts +++ b/examples/sites/demos/pc/app/base-select/clearable.spec.ts @@ -15,7 +15,7 @@ test('clearable', async ({ page }) => { await icon.click() await expect(input).toHaveValue('') // 验证选中 - await icon.click() + await input.click() await dropdown.getByRole('listitem').filter({ hasText: '上海' }).click() await expect(input).toHaveValue('上海') }) diff --git a/examples/sites/demos/pc/app/base-select/input-box-type.spec.ts b/examples/sites/demos/pc/app/base-select/input-box-type.spec.ts index 877deee36f..3948a0523b 100644 --- a/examples/sites/demos/pc/app/base-select/input-box-type.spec.ts +++ b/examples/sites/demos/pc/app/base-select/input-box-type.spec.ts @@ -14,7 +14,6 @@ test('下划线默认', async ({ page }) => { await expect(input).toHaveCSS('border-left-width', '0px') await expect(input).toHaveCSS('border-right-width', '0px') await expect(input).toHaveCSS('border-bottom-color', 'rgb(194, 194, 194)') - await expect(select.locator('svg')).toHaveCSS('fill', 'rgb(128, 128, 128)') await select.click() await option.first().click() @@ -36,7 +35,6 @@ test('下划线禁用', async ({ page }) => { await expect(input).toHaveCSS('border-right-width', '0px') await expect(input).toHaveCSS('border-bottom-color', 'rgb(219, 219, 219)') await expect(input).toHaveCSS('cursor', 'not-allowed') - await expect(select.locator('svg')).toHaveCSS('fill', 'rgb(194, 194, 194)') const hasDisabled = await input.evaluate((input) => input.hasAttribute('disabled')) await expect(hasDisabled).toBe(true) diff --git a/examples/sites/demos/pc/app/base-select/multiple.spec.ts b/examples/sites/demos/pc/app/base-select/multiple.spec.ts index 81a1c43b90..58fea268a6 100644 --- a/examples/sites/demos/pc/app/base-select/multiple.spec.ts +++ b/examples/sites/demos/pc/app/base-select/multiple.spec.ts @@ -10,7 +10,7 @@ test('多选时取远端数据与当前已选数据的并集', async ({ page }) const tag = select.locator('.tiny-tag') await expect(tag).toHaveCount(2) - await select.locator('.tiny-input__suffix').click() + await select.click() await option.filter({ hasText: '全部' }).click() await expect(tag).toHaveCount(7) await option.filter({ hasText: '全部' }).click() diff --git a/examples/sites/demos/pc/app/base-select/searchable.spec.ts b/examples/sites/demos/pc/app/base-select/searchable.spec.ts index 6c8a7a615c..321390e1b1 100644 --- a/examples/sites/demos/pc/app/base-select/searchable.spec.ts +++ b/examples/sites/demos/pc/app/base-select/searchable.spec.ts @@ -29,7 +29,7 @@ test('searchable-single', async ({ page }) => { }) await option.filter({ hasText: '上海' }).click() await page.waitForTimeout(500) - await expect(input).toHaveValue('') + await expect(input).toHaveValue('上海') }) test('searchable-multiple', async ({ page }) => { diff --git a/packages/renderless/src/base-select/index.ts b/packages/renderless/src/base-select/index.ts index debc4c3aac..9b5d001544 100644 --- a/packages/renderless/src/base-select/index.ts +++ b/packages/renderless/src/base-select/index.ts @@ -78,15 +78,15 @@ export const defaultOnQueryChange = } setFilteredSelectCls(nextTick, state, props) api.getOptionIndexArr() - - state.magicKey = state.magicKey > 0 ? -1 : 1 } export const queryChange = ({ props, state, constants }) => (value, isInput) => { if (props.optimization && isInput) { - const filterDatas = state.initDatas.filter((item) => new RegExp(escapeRegexpString(value), 'i').test(item.label)) + const filterDatas = state.initDatas.filter((item) => + new RegExp(escapeRegexpString(value), 'i').test(item[props.textField]) + ) state.datas = filterDatas } else { state.selectEmitter.emit(constants.EVENT_NAME.queryChange, value) @@ -95,7 +95,13 @@ export const queryChange = const setFilteredSelectCls = (nextTick, state, props) => { nextTick(() => { - if (props.multiple && props.showAlloption && props.filterable && state.query && !props.remote) { + if ( + props.multiple && + props.showAlloption && + (props.filterable || props.searchable) && + state.query && + !props.remote + ) { const filterSelectedVal = state.options .filter((item) => item.state.visible && item.state.itemSelected) .map((opt) => opt.value) @@ -141,7 +147,7 @@ export const handleQueryChange = state.hoverIndex = -1 - if (props.multiple && props.filterable && !props.shape) { + if (props.multiple && (props.filterable || props.searchable) && !props.shape && !state.selectDisabled) { nextTick(() => { const length = vm.$refs.input.value.length * 15 + 20 state.inputLength = state.collapseTags ? Math.min(50, length) : length @@ -177,9 +183,9 @@ export const handleMenuEnter = } export const emitChange = - ({ emit, props, state, constants }) => + ({ emit, props, state, constants, isMobileFirstMode }) => (value, changed) => { - if (state.device === 'mb' && props.multiple && !changed) return + if (isMobileFirstMode && state.device === 'mb' && props.multiple && !changed) return if (!isEqual(props.modelValue, state.compareValue)) { emit('change', value) @@ -225,17 +231,17 @@ export const getOption = if (props.optimization) { option = api.getSelectedOption(value) if (option) { - return { value: option.value, currentLabel: option.label || option.currentLabel } + return { value: option.value, currentLabel: option[props.textField] || option.currentLabel } } - option = state.datas.find((v) => getObj(v, props.valueKey) === value) + option = state.datas.find((v) => getObj(v, props.valueField) === value) if (option) { - return { value: option.value, currentLabel: option.label || option.currentLabel } + return { value: option[props.valueField], currentLabel: option[props.textField] || option.currentLabel } } } // tiny 新增 clearNoMatchValue的条件 const label = !isObject && !isNull && !isUndefined && !props.clearNoMatchValue ? value : '' - let newOption = { value, currentLabel: label } + let newOption = { value, currentLabel: label, isFakeLabel: true } if (props.multiple) { newOption.hitState = false @@ -249,9 +255,13 @@ export const getSelectedOption = (value) => { let option if (props.multiple) { - option = state.selected.find((v) => getObj(v, props.valueKey) === value) + option = state.selected.find((v) => getObj(v, props.valueField) === value && !v.isFakeLabel) } else { - if (!isEmptyObject(state.selected) && getObj(state.selected, props.valueKey) === value) { + if ( + !isEmptyObject(state.selected) && + getObj(state.selected, props.valueField) === value && + !state.selected.isFakeLabel + ) { option = state.selected } } @@ -310,7 +320,7 @@ export const setSelected = const option = getOptionOfSetSelected({ api, props }) state.selected = option state.selectedLabel = option.state.currentLabel || option.currentLabel - props.filterable && !props.shape && (state.query = state.selectedLabel) + ;(props.filterable || props.searchable) && !props.shape && (state.query = state.selectedLabel) } else { const result = getResultOfSetSelected({ state, props, api }) state.selectCls = result.length @@ -366,6 +376,8 @@ export const toggleCheckAll = value = [...new Set([...state.modelValue, ...enabledValues])] } else { value = state.modelValue.filter((val) => !enabledValues.includes(val)) + // 避免编译报错 + value = Array.from(new Set([...state.modelValue, ...enabledValues])) } } else { if (state.selectCls === 'check') { @@ -398,20 +410,33 @@ export const toggleCheckAll = export const handleFocus = ({ emit, props, state }) => (event) => { - if (!state.softFocus) { - if (props.automaticDropdown || props.filterable) { - state.visible = true - state.softFocus = true - } + state.willFocusRun = true + state.willFocusTimer && clearTimeout(state.willFocusTimer) + + state.willFocusTimer = setTimeout(() => { + state.willFocusTimer = 0 + if (!state.willFocusRun) return // 立即触发了blur,则不执行focus了 + + if (!state.softFocus) { + // tiny 新增 shape条件: 防止过滤器模式,且filterable时, 面板无法关闭的bug + if (props.shape === 'filter') { + return + } + + if (props.automaticDropdown || props.filterable || props.searchable) { + state.visible = true + state.softFocus = true + } - emit('focus', event) - } else { - if (state.searchSingleCopy && state.selectedLabel) { emit('focus', event) - } + } else { + if (state.searchSingleCopy && state.selectedLabel) { + emit('focus', event) + } - state.softFocus = false - } + state.softFocus = false + } + }, 10) } export const focus = @@ -432,6 +457,7 @@ export const blur = export const handleBlur = ({ constants, dispatch, emit, state, designConfig }) => (event) => { + state.willFocusRun = false clearTimeout(state.timer) state.timer = setTimeout(() => { if (state.isSilentBlur) { @@ -534,7 +560,7 @@ export const resetInputState = export const resetInputHeight = ({ constants, nextTick, props, vm, state, api, designConfig }) => () => { - if (state.collapseTags && !props.filterable) { + if (state.collapseTags && !(props.filterable || props.searchable)) { return } @@ -554,8 +580,6 @@ export const resetInputHeight = if (!state.isDisplayOnly && (props.hoverExpand || props.clickExpand) && !props.disabled) { api.calcCollapseTags() } - - const sizeInMap = designConfig?.state.initialInputHeight || state.initialInputHeight || 32 const noSelected = state.selected.length === 0 // tiny 新增的spacing (design中配置:aui为4,smb为0,tiny 默认为0) const spacingHeight = designConfig?.state?.spacingHeight ?? constants.SPACING_HEIGHT @@ -566,11 +590,11 @@ export const resetInputHeight = const tagsClientHeight = tags.clientHeight fastdom.mutate(() => { - input.style.height = Math.max(tagsClientHeight + spacingHeight, sizeInMap) + 'px' + input.style.height = Math.max(tagsClientHeight + spacingHeight, state.currentSizeMap) + 'px' }) }) } else { - input.style.height = noSelected ? sizeInMap + 'px' : Math.max(0, sizeInMap) + 'px' + input.style.height = noSelected ? state.currentSizeMap + 'px' : Math.max(0, state.currentSizeMap) + 'px' } } else { input.style.height = 'auto' @@ -648,8 +672,8 @@ export const handleOptionSelect = state.inputLength = 20 } - if (props.filterable) { - vm.$refs.input.focus() + if (props.filterable || props.searchable) { + vm.$refs.input?.focus() } if (props.autoClose) { @@ -673,7 +697,7 @@ export const handleOptionSelect = state.isSilentBlur = byClick - api.setSoftFocus() + if (!props.automaticDropdown) api.setSoftFocus() if (state.visible) { return @@ -692,18 +716,23 @@ export const initValue = } export const setSoftFocus = - ({ vm, state }) => + ({ vm, state, props }) => () => { - state.softFocus = true + // tiny 新增: 解决 reference 插槽时,选择数据后,需要点2次才能打开下拉面板 + // 如果有reference时, 它就没有Input这套机制了,没机会让softFocus为假了。 + if (vm.$slots.reference) { + return + } + state.softFocus = true const input = vm.$refs.input || vm.$refs.reference - if (input) { - input.focus() + // tiny 新增: 解决获焦即弹出时,关闭不了下拉面板,所以增加了!props.automaticDropdown条件 + if (!props.automaticDropdown) { + if (input) { + input.focus() + } } - - // tiny 新增: 解决 reference 插槽时,选择数据后,需要点2次才能打开下拉面板 - state.softFocus = false } export const getValueIndex = @@ -730,18 +759,24 @@ export const getValueIndex = } export const toggleMenu = - ({ vm, state, props, api }) => + ({ vm, state, props, api, designConfig }) => (e) => { - if (props.keepFocus && state.visible && props.filterable) { + if (props.keepFocus && state.visible && (props.filterable || props.searchable)) { return } + if (state.isIOS) { + state.selectHover = true + state.inputHovering = true + } + const event = e || window.event const enterCode = 13 const nodeName = event.target && event.target.nodeName const toggleVisible = props.ignoreEnter ? event.keyCode !== enterCode && nodeName === 'INPUT' : true - if (!props.displayOnly) { + const isStop = props.stopPropagation ?? designConfig?.props?.stopPropagation ?? false + if (!props.displayOnly && isStop) { event.stopPropagation() } @@ -750,7 +785,7 @@ export const toggleMenu = state.softFocus = false if (state.visible) { - if (!(props.filterable && props.shape)) { + if (!((props.filterable || props.searchable) && props.shape)) { const dom = vm.$refs.input || vm.$refs.reference dom?.focus && dom.focus() api.setOptionHighlight() @@ -763,6 +798,7 @@ export const selectOption = ({ api, state, props }) => (e) => { if (!state.visible || props.hideDrop) { + state.softFocus = false api.toggleMenu(e) } else { let option = '' @@ -828,7 +864,7 @@ export const onInputChange = ({ api, props, state, constants, nextTick }) => () => { if (!props.delay) { - if (props.filterable && state.query !== state.selectedLabel) { + if ((props.filterable || props.searchable) && state.query !== state.selectedLabel) { const isChange = false const isInput = true @@ -1057,7 +1093,7 @@ export const emptyText = } if ( - props.filterable && + (props.filterable || props.searchable) && state.query && ((props.remote && state.emptyFlag) || !state.options.some((option) => option.visible && option.state.visible)) ) { @@ -1091,10 +1127,12 @@ export const watchValue = state.currentPlaceholder = state.cachedPlaceHolder } - if (props.filterable && !props.reserveKeyword) { - // tiny 优化: 多选且props.reserveKeyword为false时, aui此处会多请求一次 - // searchable时,不清空query, 这样才能保持搜索结果 - !props.searchable && (state.query = '') + if ((props.filterable || props.searchable) && !props.reserveKeyword) { + // 还原AUI的做法 + const isChange = false + const isInput = true + state.query = '' + api.handleQueryChange(state.query, isChange, isInput) } } @@ -1103,7 +1141,7 @@ export const watchValue = !state.isClickChoose && api.initQuery({ init: true }).then(() => api.setSelected()) state.isClickChoose = false - if (props.filterable && !props.multiple) { + if ((props.filterable || props.searchable) && !props.multiple) { state.inputLength = 20 } @@ -1160,23 +1198,34 @@ export const calcOverFlow = const postOperOfToVisible = ({ props, state, constants }) => { if (props.multiple) { + if (props.modelValue && props.modelValue.length && props.initLabel && !state.selected.length) { + state.selectedLabel = props.initLabel + } return } if (state.selected) { - if (props.filterable && props.allowCreate && state.createdSelected && state.createdLabel) { - state.selectedLabel = state.createdLabel + if (props.renderType === constants.TYPE.Grid || props.renderType === constants.TYPE.Tree) { + state.selectedLabel = state.selected.currentLabel } else { - state.selectedLabel = state.selected.state.currentLabel || state.selected.currentLabel - } + if ((props.filterable || props.searchable) && props.allowCreate && state.createdSelected && state.createdLabel) { + state.selectedLabel = state.createdLabel + } else { + state.selectedLabel = state.selected.state.currentLabel || state.selected.currentLabel + } - if (props.filterable) { - state.query = state.selectedLabel + if (props.filterable || props.searchable) { + state.query = state.selectedLabel + } } - if (props.filterable) { + if (props.filterable || props.searchable) { state.currentPlaceholder = state.cachedPlaceHolder } + + if (props.modelValue && props.initLabel && !state.selectedLabel) { + state.selectedLabel = props.initLabel + } } } @@ -1212,18 +1261,18 @@ export const toVisible = export const toHide = ({ constants, state, props, vm, api }) => () => { - const { filterable, remote, remoteConfig, shape, multiple, valueField } = props + const { remote, remoteConfig, shape, renderType, multiple, valueField } = props state.selectEmitter.emit(constants.COMPONENT_NAME.SelectDropdown, constants.EVENT_NAME.updatePopper) - if (filterable) { + if (props.filterable || props.searchable) { state.query = remote || shape ? '' : state.selectedLabel const isChange = remote && remoteConfig.autoSearch && (state.firstAutoSearch || remoteConfig.clearData) state.firstAutoSearch = false api.handleQueryChange(state.query, isChange) if (multiple) { - vm.$refs.input.focus() + vm.$refs.input?.focus() } else { if (!remote) { state.selectEmitter.emit(constants.EVENT_NAME.queryChange, '') @@ -1240,17 +1289,17 @@ export const toHide = } export const watchVisible = - ({ api, constants, emit, state, vm, props }) => + ({ api, constants, emit, state, vm, props, isMobileFirstMode }) => (value) => { - if ((props.filterable || props.remote) && !value) { - vm.$refs.reference.blur() + if ((props.filterable || props.searchable || props.remote) && !value) { + vm.$refs.reference?.blur() } if (api.onCopying()) { return } - if (value && props.multiple && state.device === 'mb') { + if (value && props.multiple && isMobileFirstMode && state.device === 'mb') { state.selectedCopy = state.selected.slice() } @@ -1299,7 +1348,13 @@ export const watchOptions = } nextTick(() => { - if (parent.$el.querySelector('input') !== document.activeElement) { + if ( + parent.$el.querySelector('input') !== document.activeElement && // filterable时, 从 input 框离开了 + !( + document.activeElement?.classList.contains('tiny-input__inner') && // 并且当前不在下拉面板的searchable 的input中时, 才需要更新一下setSelect + document.activeElement.closest('.tiny-select-dropdown__search') + ) + ) { api.setSelected() } }) @@ -1307,12 +1362,31 @@ export const watchOptions = api.getOptionIndexArr() } +export const watchOptionsWhenAutoSelect = + ({ nextTick, props, state, api }) => + () => { + if (props.autoSelect && props.remote) { + nextTick(() => { + if (props.options?.length === 1 || state.options.length === 1) { + const { valueField } = props + const option = props.options?.length === 1 ? props.options[0] : state.options[0] + api.updateModelValue(props.multiple ? [option[props.valueField]] : option[props.valueField]) + state.visible = false + } + }) + } + } + export const getOptionIndexArr = ({ props, state, api }) => () => { setTimeout(() => { state.optionIndexArr = api.queryVisibleOptions().map((item) => Number(item.getAttribute('data-index'))) - if (props.defaultFirstOption && (props.filterable || props.remote) && state.filteredOptionsCount) { + if ( + props.defaultFirstOption && + (props.filterable || props.searchable || props.remote) && + state.filteredOptionsCount + ) { if (props.optimization) { optmzApis.checkDefaultFirstOption({ state }) } else { @@ -1356,7 +1430,7 @@ export const handleCopyClick = export const debouncRquest = ({ api, state, props }) => debounce(props.delay, () => { - if (props.filterable && state.query !== state.selectedLabel) { + if ((props.filterable || props.searchable) && state.query !== state.selectedLabel) { const isChange = false const isInput = true @@ -1425,6 +1499,15 @@ export const onMouseenterNative = } } +export const onMouseenterSelf = + ({ state }) => + () => { + if (!state.isIOS) { + state.selectHover = true + state.inputHovering = true + } + } + export const onMouseleaveNative = ({ state }) => (e) => { @@ -1568,7 +1651,7 @@ export const initQuery = ({ props, state, constants, vm }) => ({ init } = {}) => { const isRemote = - props.filterable && + (props.filterable || props.searchable) && props.remote && (typeof props.remoteMethod === 'function' || typeof props.initQuery === 'function') @@ -1590,6 +1673,15 @@ export const initQuery = return Promise.resolve(selected) } +export const computedCurrentSizeMap = + ({ state, designConfig }) => + () => { + const defaultSizeMap = { default: 32, mini: 24, small: 28, medium: 40 } + const sizeMap = designConfig?.state?.sizeMap || defaultSizeMap + + return sizeMap[state.selectSize || 'default'] + } + export const mounted = ({ api, parent, state, props, vm, designConfig }) => () => { @@ -1604,18 +1696,10 @@ export const mounted = state.completed = true - // tiny 新增: sizeMap适配不同主题 - const defaultSizeMap = { medium: 40, default: 32, small: 28, mini: 24 } - const sizeMap = designConfig?.state?.sizeMap || defaultSizeMap - if (props.multiple && Array.isArray(props.modelValue) && props.modelValue.length > 0) { state.currentPlaceholder = '' } - state.initialInputHeight = state.isDisplayOnly - ? sizeMap[state.selectSize || 'default'] // tiny 新增 : default, aui只处理了另3种情况,不传入时,要固定为default - : inputClientRect.height || sizeMap[state.selectSize] - addResizeListener(parentEl, api.handleResize) if (vm.$refs.tags) { @@ -1628,7 +1712,13 @@ export const mounted = state.inputWidth = inputClientRect.width - api.initQuery({ init: true }).then(() => api.setSelected()) + api.initQuery({ init: true }).then(() => { + api.setSelected(true) + + if (props.modelValue && props.initLabel) { + state.selectedLabel = props.initLabel + } + }) if (props.dataset) { api.watchPropsOption() @@ -1735,11 +1825,11 @@ export const computeMultipleLimit = } export const updateModelValue = - ({ props, emit, state }) => + ({ props, emit, state, isMobileFirstMode }) => (value, needUpdate) => { state.isClickChoose = true - if (state.device === 'mb' && props.multiple && !needUpdate) { + if (isMobileFirstMode && state.device === 'mb' && props.multiple && !needUpdate) { state.modelValue = value } else { emit('update:modelValue', value) @@ -1792,13 +1882,20 @@ export const computedTagsStyle = } export const computedReadonly = - ({ props, state }) => - () => - state.device === 'mb' || - props.readonly || - !props.filterable || - props.multiple || - (browserInfo.name !== BROWSER_NAME.IE && browserInfo.name !== BROWSER_NAME.Edge && !state.visible) + ({ props, state, isMobileFirstMode }) => + () => { + if (state.isIOS && props.filterable) { + return false + } else { + return ( + (isMobileFirstMode && state.device === 'mb') || + props.readonly || + !(props.filterable || props.searchable) || + props.multiple || + (browserInfo.name !== BROWSER_NAME.IE && browserInfo.name !== BROWSER_NAME.Edge && !state.visible) + ) + } + } export const computedShowClose = ({ props, state }) => @@ -1814,11 +1911,11 @@ export const computedShowClose = export const computedCollapseTagSize = (state) => () => state.selectSize export const computedShowNewOption = - ({ props, state }) => + ({ props, state, isMobileFirstMode }) => () => { - const query = state.device === 'mb' ? state.queryValue : state.query + const query = isMobileFirstMode && state.device === 'mb' ? state.queryValue : state.query return ( - props.filterable && + (props.filterable || props.searchable) && props.allowCreate && query && !state.options.filter((option) => !option.created).some((option) => option.state.currentLabel === state.query) @@ -1833,13 +1930,18 @@ export const computedShowCopy = export const computedOptionsAllDisabled = (state) => () => state.options.filter((option) => option.visible).every((option) => option.disabled) -export const computedDisabledTooltipContent = (state) => () => - state.selected.map((item) => (item.state ? item.state.currentLabel : item.currentLabel)).join(';') +export const computedDisabledTooltipContent = + ({ state }) => + () => { + // tiny 新增: 仅displayOnly且传入options属性时, 不需要渲染option + // 禁用的tooltip内容 和 仅展示的显示内容,都应该是当前label值,共用即可! + return state.displayOnlyContent + } export const computedSelectDisabled = - ({ props, parent }) => + ({ state }) => () => - props.disabled || (parent.form || {}).disabled || props.displayOnly || (parent.form || {}).displayOnly + state.isDisabled || state.isDisplayOnly export const computedIsExpand = ({ props, state }) => @@ -1943,7 +2045,7 @@ export const clearNoMatchValue = // 解决无界时,event.target 会变为 wujie_iframe的元素的bug export const handleDebouncedQueryChange = ({ state, api }) => debounce(state.debounce, (value) => { - api.handleQueryChange(value) + api.handleQueryChange(value, false, true) }) export const onClickCollapseTag = diff --git a/packages/renderless/src/base-select/vue.ts b/packages/renderless/src/base-select/vue.ts index 938ee20e7b..372e5a360a 100644 --- a/packages/renderless/src/base-select/vue.ts +++ b/packages/renderless/src/base-select/vue.ts @@ -53,6 +53,7 @@ import { watchPropsOption, onMouseenterNative, onMouseleaveNative, + onMouseenterSelf, onCopying, defaultOnQueryChange, queryChange, @@ -97,10 +98,14 @@ import { updateSelectedData, hidePanel, computedShowTagText, - isTagClosable + isTagClosable, + computedCurrentSizeMap, + watchOptionsWhenAutoSelect } from './index' import { debounce } from '@opentiny/utils' import { isNumber } from '@opentiny/utils' +import { useUserAgent } from '@opentiny/vue-hooks' +import { isServer } from '@opentiny/utils' export const api = [ 'state', @@ -146,6 +151,7 @@ export const api = [ 'navigateOptions', 'onMouseenterNative', 'onMouseleaveNative', + 'onMouseenterSelf', 'onCopying', 'handleDropdownClick', 'handleEnterTag', @@ -158,8 +164,19 @@ export const api = [ 'computedShowTagText', 'isTagClosable' ] - -const initState = ({ reactive, computed, props, api, emitter, parent, constants, useBreakpoint, vm, designConfig }) => { +const initState = ({ + reactive, + computed, + props, + api, + emitter, + parent, + constants, + isMobileFirstMode, + useBreakpoint, + vm, + designConfig +}) => { const stateAdd = initStateAdd({ computed, props, api, parent }) const state = reactive({ ...stateAdd, @@ -167,7 +184,6 @@ const initState = ({ reactive, computed, props, api, emitter, parent, constants, datas: [], initDatas: [], query: '', - magicKey: 0, options: [], visible: false, showCopy: computed(() => api.computedShowCopy()), @@ -184,7 +200,7 @@ const initState = ({ reactive, computed, props, api, emitter, parent, constants, optionsAllDisabled: computed(() => api.computedOptionsAllDisabled()), collapseTagSize: computed(() => api.computedCollapseTagSize()), showNewOption: computed(() => api.computedShowNewOption()), - selectSize: computed(() => props.size || state.formItemSize), + selectSize: computed(() => (!isServer ? props.size || state.formItemSize : 0)), optimizeOpts: computed(() => api.computeOptimizeOpts()), optimizeStore: { valueIndex: 0, recycleScrollerHeight: computed(() => api.recycleScrollerHeight()) }, @@ -202,13 +218,45 @@ const initState = ({ reactive, computed, props, api, emitter, parent, constants, selectedCopy: [], compareValue: null, selectedVal: computed(() => - state.device === 'mb' && props.multiple && state.visible ? state.selectedCopy : state.selected - ), - displayOnlyContent: computed(() => - props.multiple && Array.isArray(state.selected) - ? state.selected.map((item) => (item.state ? item.state.currentLabel : item.currentLabel)).join('; ') - : '' + isMobileFirstMode && state.device === 'mb' && props.multiple && state.visible + ? state.selectedCopy + : state.selected ), + displayOnlyContent: computed(() => { + if (vm.$slots.reference) { + return '' + } + if (props.multiple) { + if (Array.isArray(state.selected)) { + // 如果已经displayOnly 且传入了options,从这里找label, 否则从state.selected (displayOnly时不渲染options) + if (state.isDisplayOnly && props.options && props.options.length > 0) { + return state.selected + .map((item) => { + const find = props.options.find((opt) => opt[props.valueField] === item.value) + return find ? find[props.textField] : '' + }) + .join('; ') + } else { + return state.selected.map((item) => (item.state ? item.state.currentLabel : item.currentLabel)).join('; ') + } + } else { + return '' + } + } else { + // 单选 + if (state.selected) { + // 如果已经displayOnly 且传入了options,从这里找label, 否则从state.selected (displayOnly时不渲染options) + if (state.isDisplayOnly && props.options && props.options.length > 0) { + const find = props.options.find((opt) => opt[props.valueField] === state.selected.value) + return find ? find[props.textField] : '' + } else { + return state.selected.state?.currentLabel || state.selected.currentLabel || state.selected.label || '' + } + } else { + return '' + } + } + }), breakpoint: useBreakpoint ? useBreakpoint().current : '', isSaaSTheme: vm.theme === 'saas', disabledOptionHover: false, @@ -222,13 +270,21 @@ const initState = ({ reactive, computed, props, api, emitter, parent, constants, return designConfig.state.autoHideDownIcon } return true // tiny 默认为true - })() + })(), + designConfig, + currentSizeMap: computed(() => api.computedCurrentSizeMap()), + rootAutoTipConfig: computed(() => ({ + content: state.displayOnlyContent, + always: !!state.displayOnlyContent, + ...props.tooltipConfig + })) }) return state } const initStateAdd = ({ computed, props, api, parent }) => { + const { isIOS } = useUserAgent() return { selectedTags: [], tips: '', @@ -273,14 +329,17 @@ const initStateAdd = ({ computed, props, api, parent }) => { isDisplayOnly: computed(() => props.displayOnly || (parent.form || {}).displayOnly), isDisabled: computed(() => props.disabled || (parent.form || {}).disabled), isShowTagText: computed(() => api.computedShowTagText()), - searchSingleCopy: computed(() => props.allowCopy && !props.multiple && props.filterable), + searchSingleCopy: computed(() => props.allowCopy && !props.multiple && (props.filterable || props.searchable)), childrenName: computed(() => 'children'), tooltipContent: {}, isHidden: false, optionIndexArr: [], + isIOS, showCollapseTag: false, exceedMaxVisibleRow: false, // 是否超出默认最大显示行数 - toHideIndex: Infinity // 第一个超出被隐藏的索引 + toHideIndex: Infinity, // 第一个超出被隐藏的索引 + willFocusRun: false, // 进入focus时,延时等一下看是否触发blur,触发则不进入focus + willFocusTimer: 0 } } @@ -311,12 +370,12 @@ const initApi = ({ getChildValue: getChildValue(), getOption: getOption({ props, state, api }), getSelectedOption: getSelectedOption({ props, state }), - emitChange: emitChange({ emit, props, state, constants }), - directEmitChange: directEmitChange({ emit, props, state, constants }), + emitChange: emitChange({ emit, props, state, constants, isMobileFirstMode }), + directEmitChange: directEmitChange({ emit, props, state, constants, isMobileFirstMode }), toggleMenu: toggleMenu({ vm, state, props, api, isMobileFirstMode }), showTip: showTip({ props, state, vm }), onOptionDestroy: onOptionDestroy(state), - setSoftFocus: setSoftFocus({ vm, state }), + setSoftFocus: setSoftFocus({ vm, state, props }), resetInputWidth: resetInputWidth({ vm, state }), resetHoverIndex: resetHoverIndex({ props, state }), resetDatas: resetDatas({ props, state }), @@ -336,6 +395,7 @@ const initApi = ({ watchPropsOption: watchPropsOption({ constants, parent, props, state }), onMouseenterNative: onMouseenterNative({ state }), onMouseleaveNative: onMouseleaveNative({ state }), + onMouseenterSelf: onMouseenterSelf({ state }), onCopying: onCopying({ state, vm }), watchHoverIndex: watchHoverIndex({ state }), computeOptimizeOpts: computeOptimizeOpts({ props, designConfig }), @@ -343,17 +403,17 @@ const initApi = ({ computeMultipleLimit: computeMultipleLimit({ props, state }), watchInputHover: watchInputHover({ vm }), initQuery: initQuery({ props, state, constants, vm }), - updateModelValue: updateModelValue({ props, emit, state }), + updateModelValue: updateModelValue({ props, emit, state, isMobileFirstMode }), computedTagsStyle: computedTagsStyle({ props, parent, state, vm }), - computedReadonly: computedReadonly({ props, state }), + computedReadonly: computedReadonly({ props, state, isMobileFirstMode }), computedShowClose: computedShowClose({ props, state }), computedCollapseTagSize: computedCollapseTagSize(state), - computedShowNewOption: computedShowNewOption({ props, state }), + computedShowNewOption: computedShowNewOption({ props, state, isMobileFirstMode }), computedShowCopy: computedShowCopy({ props, state }), computedOptionsAllDisabled: computedOptionsAllDisabled(state), - computedDisabledTooltipContent: computedDisabledTooltipContent(state), + computedDisabledTooltipContent: computedDisabledTooltipContent({ props, state }), - computedSelectDisabled: computedSelectDisabled({ props, parent }), + computedSelectDisabled: computedSelectDisabled({ state }), computedIsExpand: computedIsExpand({ props, state }), watchInitValue: watchInitValue({ props, emit }), watchShowClose: watchShowClose({ nextTick, state, parent }), @@ -365,7 +425,9 @@ const initApi = ({ computedShowTagText: computedShowTagText({ state }), isTagClosable: isTagClosable(), updateSelectedData: updateSelectedData({ state }), - hidePanel: hidePanel({ state }) + hidePanel: hidePanel({ state }), + computedCurrentSizeMap: computedCurrentSizeMap({ state, designConfig }), + watchOptionsWhenAutoSelect: watchOptionsWhenAutoSelect({ state, props, nextTick, api }) }) addApi({ api, props, state, emit, constants, parent, nextTick, dispatch, vm, isMobileFirstMode, designConfig }) @@ -506,6 +568,10 @@ const initWatch = ({ watch, props, api, state, nextTick }) => { const addWatch = ({ watch, props, api, state, nextTick }) => { watch(() => [...state.options], api.watchOptions) + // tiny 新增: 支持autoSelect + watch(() => state.options, api.watchOptionsWhenAutoSelect) + props.options && watch(() => props.options, api.watchOptionsWhenAutoSelect) + watch(() => state.hoverIndex, api.watchHoverIndex) props.options && watch(() => props.options, api.watchPropsOption, { immediate: true, deep: true }) @@ -536,6 +602,7 @@ export const renderless = ( emitter, parent, constants, + isMobileFirstMode, useBreakpoint, vm, designConfig diff --git a/packages/vue/src/base-select/src/index.ts b/packages/vue/src/base-select/src/index.ts index 40e27e7e1f..819a3ce67f 100644 --- a/packages/vue/src/base-select/src/index.ts +++ b/packages/vue/src/base-select/src/index.ts @@ -13,7 +13,6 @@ import { $props, $prefix, $setup, defineComponent } from '@opentiny/vue-common' import { t } from '@opentiny/vue-locale' import template from 'virtual-template?pc|mobile-first' -import { IconChevronDown } from '@opentiny/vue-icon' const $constants = { CLASS: { @@ -246,11 +245,7 @@ export default defineComponent({ }, dropdownIcon: { type: [Object, String], - default: () => { - const defaultDropdownIcon = IconChevronDown() - defaultDropdownIcon.isDefault = true - return defaultDropdownIcon - } + default: '' }, disabledTooltipContent: String, hoverExpand: { @@ -299,10 +294,29 @@ export default defineComponent({ type: String, default: () => t('ui.select.add') }, + initLabel: { + type: String, + default: '' + }, blank: { type: Boolean, default: false }, + tooltipConfig: { + type: Object, + default() { + return {} + } + }, + showEmptyValue: Boolean, + dropdownHeight: { + type: String, + default: 'initial' + }, + stopPropagation: { + type: Boolean, + default: undefined + }, // 以下为 tiny 新增 searchable: { type: Boolean, @@ -348,6 +362,15 @@ export default defineComponent({ showAllTextTag: { type: Boolean, default: false + }, + // 配置多选时,Tag的最大宽度 + maxTagWidth: { + type: [String, Number], + default: null + }, + autoSelect: { + type: Boolean, + default: false } }, setup(props, context) { diff --git a/packages/vue/src/base-select/src/pc.vue b/packages/vue/src/base-select/src/pc.vue index c4bc63450a..ed3b1fe917 100644 --- a/packages/vue/src/base-select/src/pc.vue +++ b/packages/vue/src/base-select/src/pc.vue @@ -25,7 +25,7 @@ hoverExpand ? 'is-hover-expand' : '', clickExpand ? 'is-click-expand' : '', state.showCollapseTag ? 'collapse-tag-clicked' : '', - state.selectDisabled ? 'is-disabled' : '', + state.isDisabled ? 'is-disabled' : '', $parent.$attrs.class ]" @mouseleave.self=" @@ -34,20 +34,17 @@ state.inputHovering = false } " - @mouseenter.self=" - () => { - state.selectHover = true - state.inputHovering = true - } - " + @mouseenter.self="onMouseenterSelf" @click="toggleMenu" v-clickoutside="handleClose" v-bind="a($attrs, ['class', 'style'], true)" > +
+ @@ -117,8 +119,10 @@ :closable="false" :size="state.collapseTagSize" :type="state.getTagType" + :disabled="state.isDisabled" disable-transitions class="tiny-base-select__tags-number" + :maxWidth="maxTagWidth" > + {{ state.selected.length - 1 }} @@ -135,6 +139,7 @@ :closable="true" :size="state.collapseTagSize" @close="toggleCheckAll(false)" + :maxWidth="maxTagWidth" > {{ allText || t('ui.base.all') }} @@ -146,10 +151,11 @@ :type="state.getTagType" key="tags-collapse" data-tag="tags-collapse" - only-icon + :only-icon="!hoverExpand" :closable="false" :size="state.collapseTagSize" @click="onClickCollapseTag($event)" + :maxWidth="maxTagWidth" > @@ -168,10 +174,12 @@ :type="state.getTagType" @close="deleteTag($event, item)" disable-transitions + :maxWidth="maxTagWidth" > @@ -208,26 +216,13 @@ - - - - - {{ item.state ? item.state.currentLabel : item.currentLabel }}; - + + + + {{ item.state ? item.state.currentLabel : item.currentLabel }}; - - - + - + {{ state.selected.length }}/{{ multipleLimit }} @@ -363,7 +365,6 @@ > - - {{ allText || t('ui.base.all') }} + + +
+ {{ allText || t('ui.base.all') }} +
  • - - {{ allText || t('ui.base.all') }} + + +
    + {{ allText || t('ui.base.all') }} +
  • @@ -524,14 +532,18 @@ {{ state.emptyText }}

    -
    +
    - - - +
    @@ -554,17 +566,17 @@ import TinyOption from '@opentiny/vue-option' import TinyScrollbar from '@opentiny/vue-scrollbar' import TinySelectDropdown from '@opentiny/vue-select-dropdown' import TinyButton from '@opentiny/vue-button' -import { Clickoutside } from '@opentiny/vue-directive' +import { Clickoutside, AutoTip } from '@opentiny/vue-directive' import { - IconClose, - IconHalfselect, - IconCheck, - IconCheckedSur, - IconCopy, - IconDeltaDown, - IconSearch, - IconEllipsis, - IconChevronUp, + iconClose, + iconHalfselect, + iconCheck, + iconCheckedSur, + iconCopy, + iconDownWard, + iconSearch, + iconEllipsis, + iconChevronUp, iconAddCircle, iconLoadingShadow } from '@opentiny/vue-icon' @@ -572,9 +584,6 @@ import TinyTooltip from '@opentiny/vue-tooltip' import FilterBox from '@opentiny/vue-filter-box' import RecycleScroller from '@opentiny/vue-recycle-scroller' -// tiny 新增 -import TinyCheckbox from '@opentiny/vue-checkbox' - import '@opentiny/vue-theme/select/index.less' import '@opentiny/vue-theme/base-select/index.less' @@ -607,6 +616,7 @@ export default defineComponent({ ], directives: directive({ Clickoutside, + AutoTip, popover: { bind(el, binding, vnode) { getReference(el, binding, vnode) @@ -621,24 +631,23 @@ export default defineComponent({ TinyInput, TinyOption, TinyButton, - IconClose: IconClose(), + IconClose: iconClose(), TinyScrollbar, - IconCopy: IconCopy(), + IconCopy: iconCopy(), IconAddCircle: iconAddCircle(), IconLoadingShadow: iconLoadingShadow(), TinySelectDropdown, - IconHalfselect: IconHalfselect(), - IconCheck: IconCheck(), - IconCheckedSur: IconCheckedSur(), + IconHalfselect: iconHalfselect(), + IconCheck: iconCheck(), + IconCheckedSur: iconCheckedSur(), TinyFilterBox: FilterBox, TinyTooltip, TinyRecycleScroller: RecycleScroller, // tiny 新增, - IconSearch: IconSearch(), - IconDeltaDown: IconDeltaDown(), // 默认下拉图标 - TinyCheckbox, - IconEllipsis: IconEllipsis(), - IconChevronUp: IconChevronUp() + IconSearch: iconSearch(), + IconDownWard: iconDownWard(), // 默认下拉图标 + IconEllipsis: iconEllipsis(), + IconChevronUp: iconChevronUp() }, props: [ ...props, @@ -712,7 +721,11 @@ export default defineComponent({ 'topCreate', 'topCreateText', 'keepFocus', + 'initLabel', 'blank', + 'tooltipConfig', + 'showEmptyValue', + 'stopPropagation', // 以下为 tiny 新增 'searchable', 'showEmptyImage', @@ -724,7 +737,8 @@ export default defineComponent({ 'clickExpand', 'maxVisibleRows', 'showAllTextTag', - 'allText' + 'allText', + 'maxTagWidth' ], setup(props, context) { return setup({ props, context, renderless, api }) diff --git a/packages/vue/src/select/src/pc.vue b/packages/vue/src/select/src/pc.vue index 5419ff952a..36c1ec8943 100644 --- a/packages/vue/src/select/src/pc.vue +++ b/packages/vue/src/select/src/pc.vue @@ -419,7 +419,7 @@ v-model="state.query" :placeholder="t('ui.search.placeholder')" class="tiny-select-dropdown__search" - @update:modelValue="handleQueryChange(state.query)" + @update:modelValue="handleQueryChange(state.query, false, true)" >