From bd0876debb1b0161b33e67ce1587b8e08786a97c Mon Sep 17 00:00:00 2001 From: sallerli1 Date: Tue, 1 Aug 2023 21:38:01 +0800 Subject: [PATCH] fix(pro:search): chinese couldn't be input --- .../search/src/components/MeasureElement.tsx | 35 ----- .../src/components/segment/SegmentInput.tsx | 129 ++++++++++++------ packages/pro/search/src/types/index.ts | 1 - .../pro/search/src/types/measureElement.ts | 13 -- packages/pro/search/src/utils/index.ts | 1 + .../pro/search/src/utils/measureTextWidth.ts | 47 +++++++ 6 files changed, 135 insertions(+), 91 deletions(-) delete mode 100644 packages/pro/search/src/components/MeasureElement.tsx delete mode 100644 packages/pro/search/src/types/measureElement.ts create mode 100644 packages/pro/search/src/utils/measureTextWidth.ts diff --git a/packages/pro/search/src/components/MeasureElement.tsx b/packages/pro/search/src/components/MeasureElement.tsx deleted file mode 100644 index f9df0f916..000000000 --- a/packages/pro/search/src/components/MeasureElement.tsx +++ /dev/null @@ -1,35 +0,0 @@ -/** - * @license - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE - */ - -import { defineComponent, inject, onMounted, ref, watch } from 'vue' - -import { callEmit } from '@idux/cdk/utils' - -import { useElementWidthMeasure } from '../composables/useElementWidthMeasure' -import { proSearchContext } from '../token' -import { measureElementProps } from '../types' - -export default defineComponent({ - props: measureElementProps, - setup(props, { slots }) { - const { mergedPrefixCls } = inject(proSearchContext)! - const measureElRef = ref() - const measureElWidth = useElementWidthMeasure(measureElRef) - - onMounted(() => { - watch(measureElWidth, width => { - callEmit(props.onWidthChange, width) - }) - }) - - return () => ( - - {slots.default?.()} - - ) - }, -}) diff --git a/packages/pro/search/src/components/segment/SegmentInput.tsx b/packages/pro/search/src/components/segment/SegmentInput.tsx index e42abb5dc..0462d7e99 100644 --- a/packages/pro/search/src/components/segment/SegmentInput.tsx +++ b/packages/pro/search/src/components/segment/SegmentInput.tsx @@ -7,6 +7,7 @@ import { type CSSProperties, + type ComputedRef, type Ref, computed, defineComponent, @@ -18,13 +19,15 @@ import { watch, } from 'vue' +import { debounce } from 'lodash-es' + import { useResizeObserver } from '@idux/cdk/resize' import { getScroll, setScroll } from '@idux/cdk/scroll' import { callEmit, convertCssPixel, useEventListener, useState } from '@idux/cdk/utils' import { proSearchContext } from '../../token' import { type SegmentInputProps, segmentIputProps } from '../../types' -import MeasureElement from '../MeasureElement' +import { measureTextWidth } from '../../utils' export default defineComponent({ inheritAttrs: false, @@ -35,14 +38,21 @@ export default defineComponent({ const segmentWrapperRef = ref() const segmentInputRef = ref() - const [inputWidth, setInputWidth] = useState(0) + const { input, handleInput, handleCompositionStart, handleCompositionEnd } = useInputEvents(props) + const segmentScroll = useSegmentScroll(props, segmentInputRef, segmentWrapperRef) + + const displayedText = computed(() => input.value || props.placeholder || '') + const [textWidth, setTextWidth] = useState(0) + + const leftSideEllipsis = computed(() => segmentScroll.value.left > 1) + const rightSideEllipsis = computed(() => segmentScroll.value.right > 1) const inputStyle = computed(() => ({ minWidth: props.disabled ? '0' : undefined, - width: convertCssPixel(inputWidth.value), + width: convertCssPixel(textWidth.value), })) - watch(inputWidth, width => { + watch(textWidth, width => { callEmit(props.onWidthChange, width) }) @@ -56,6 +66,13 @@ export default defineComponent({ }) onMounted(() => { + watch( + displayedText, + text => { + setTextWidth(measureTextWidth(text, segmentWrapperRef.value)) + }, + { immediate: true }, + ) watch( inputStyle, style => { @@ -73,12 +90,6 @@ export default defineComponent({ getInputElement, }) - const { handleInput, handleCompositionStart, handleCompositionEnd } = useInputEvents(props) - const segmentScroll = useSegmentScroll(props, segmentInputRef) - - const leftSideEllipsis = computed(() => segmentScroll.value.left > 1) - const rightSideEllipsis = computed(() => segmentScroll.value.right > 1) - return () => { const prefixCls = `${mergedPrefixCls.value}-segment-input` const { class: className, style, ...rest } = attrs @@ -96,7 +107,7 @@ export default defineComponent({ ref={segmentInputRef} class={`${prefixCls}-inner`} style={style as CSSProperties} - value={props.value ?? ''} + value={input.value ?? ''} disabled={props.disabled} placeholder={props.placeholder} onInput={handleInput} @@ -111,38 +122,50 @@ export default defineComponent({ > ... - {props.value || props.placeholder || ''} ) } }, }) -interface InputEventHandlers { +interface InputEventHandlerContext { + input: ComputedRef handleInput: (evt: Event) => void handleCompositionStart: () => void handleCompositionEnd: (evt: CompositionEvent) => void } -function useInputEvents(props: SegmentInputProps): InputEventHandlers { - const isComposing = ref(false) +function useInputEvents(props: SegmentInputProps): InputEventHandlerContext { + let isComposing = false + const [input, setInput] = useState(props.value) + + watch( + () => props.value, + value => { + setInput(value) + }, + ) const handleInput = (evt: Event) => { - if (!isComposing.value) { - callEmit(props.onInput, (evt.target as HTMLInputElement).value) + const inputValue = (evt.target as HTMLInputElement).value + setInput(inputValue) + + if (!isComposing) { + callEmit(props.onInput, inputValue) } } const handleCompositionStart = () => { - isComposing.value = true + isComposing = true } const handleCompositionEnd = (evt: CompositionEvent) => { - if (isComposing.value) { - isComposing.value = false + if (isComposing) { + isComposing = false handleInput(evt) } } return { + input, handleInput, handleCompositionStart, handleCompositionEnd, @@ -154,13 +177,17 @@ interface SegmentScroll { right: number } -function useSegmentScroll(props: SegmentInputProps, inputRef: Ref) { +function useSegmentScroll( + props: SegmentInputProps, + inputRef: Ref, + wrapperRef: Ref, +): ComputedRef { const [segmentScroll, setSegmentScroll] = useState({ left: 0, right: 0 }, true) - let inputWidth = 0 - let oldSegmentScroll: SegmentScroll = { left: 0, right: 0 } - let stopScrollListen: (() => void) | undefined - let stopResizeListen: (() => void) | undefined + let listenerStops: (() => void)[] + const stopListen = () => { + listenerStops.forEach(stop => stop()) + } const getScrolls = (): SegmentScroll => { const el = inputRef.value @@ -175,33 +202,51 @@ function useSegmentScroll(props: SegmentInputProps, inputRef: Ref { - oldSegmentScroll = segmentScroll.value + const updateScroll = debounce(() => { setSegmentScroll(getScrolls()) + }, 10) + + const scrollIntoView = () => { + if (!inputRef.value) { + return + } + + const width = inputRef.value.offsetWidth ?? 0 + const selectionStart = inputRef.value.selectionStart + + if (!width || selectionStart === null) { + return + } + + const textBefore = (inputRef.value.value ?? '').slice(0, selectionStart) + const textBeforeWidth = measureTextWidth(textBefore, wrapperRef.value) + const { scrollLeft } = getScroll(inputRef.value) + + if (textBeforeWidth < scrollLeft - 1) { + setScroll({ scrollLeft: Math.max(textBeforeWidth - 2, 0) }, inputRef.value) + } else if (textBeforeWidth > scrollLeft + width + 1) { + setScroll({ scrollLeft: textBeforeWidth - width + 2 }, inputRef.value) + } } onMounted(() => { - inputWidth = inputRef.value?.offsetWidth ?? 0 - if (props.ellipsis) { - stopScrollListen = useEventListener(inputRef, 'scroll', updateScroll) - stopResizeListen = useResizeObserver(inputRef, () => { - const width = inputRef.value?.offsetWidth ?? 0 - const widthOffset = inputWidth - width - inputWidth = width - - if (widthOffset > 0 && segmentScroll.value.left > 0 && oldSegmentScroll.left <= 0) { - setScroll({ scrollLeft: segmentScroll.value.left + widthOffset }, inputRef.value) - } else { + listenerStops = [ + useEventListener(inputRef, 'scroll', updateScroll), + useResizeObserver(inputRef, () => { + if (document.activeElement === inputRef.value) { + scrollIntoView() + } updateScroll() - } - }) + }), + useEventListener(inputRef, 'input', updateScroll), + useEventListener(inputRef, 'compositionend', scrollIntoView), + ] } }) onUnmounted(() => { - stopScrollListen?.() - stopResizeListen?.() + stopListen() }) return segmentScroll diff --git a/packages/pro/search/src/types/index.ts b/packages/pro/search/src/types/index.ts index f5543bf23..6180b4c0c 100644 --- a/packages/pro/search/src/types/index.ts +++ b/packages/pro/search/src/types/index.ts @@ -11,6 +11,5 @@ export * from './searchValue' export * from './searchFields' export * from './panels' export * from './proSearch' -export * from './measureElement' export * from './quickSelectPanel' export * from './overlay' diff --git a/packages/pro/search/src/types/measureElement.ts b/packages/pro/search/src/types/measureElement.ts deleted file mode 100644 index 3cf50ff12..000000000 --- a/packages/pro/search/src/types/measureElement.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * @license - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE - */ - -import type { MaybeArray } from '@idux/cdk/utils' -import type { PropType } from 'vue' - -export const measureElementProps = { - onWidthChange: [Function, Array] as PropType void>>, -} as const diff --git a/packages/pro/search/src/utils/index.ts b/packages/pro/search/src/utils/index.ts index 9ee91870c..be026cd43 100644 --- a/packages/pro/search/src/utils/index.ts +++ b/packages/pro/search/src/utils/index.ts @@ -8,3 +8,4 @@ export * from './getSelectableCommonParams' export * from './RenderIcon' export * from './selectData' +export * from './measureTextWidth' diff --git a/packages/pro/search/src/utils/measureTextWidth.ts b/packages/pro/search/src/utils/measureTextWidth.ts new file mode 100644 index 000000000..3cecc71ae --- /dev/null +++ b/packages/pro/search/src/utils/measureTextWidth.ts @@ -0,0 +1,47 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +const measureElementStyle = { + position: 'fixed', + visibility: 'hidden', + whiteSpace: 'pre-wrap', + top: '-100px', + left: '-100px', +} as const +const textWidthStyleProps = ['fontSize', 'letterSpacing'] as const + +export function measureTextWidth(text: string, parentEl?: HTMLElement): number { + const _parentEl = parentEl as (HTMLElement & { __pro_search_measure_el: HTMLSpanElement }) | undefined + const el = _parentEl?.__pro_search_measure_el ?? document.createElement('span') + + el.textContent = text + Object.keys(measureElementStyle).forEach(key => { + el.style.setProperty(key, measureElementStyle[key as keyof typeof measureElementStyle]) + }) + + if (parentEl) { + const parentStyle = getComputedStyle(parentEl) + textWidthStyleProps.forEach(key => { + el.style.setProperty(key, parentStyle[key]) + }) + } + + document.body.appendChild(el) + + void el.offsetWidth + const textWidth = el.getBoundingClientRect().width + + if (el.parentElement === document.body) { + document.body.removeChild(el) + } + + if (_parentEl && !_parentEl.__pro_search_measure_el) { + _parentEl.__pro_search_measure_el = el + } + + return textWidth +}