Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(pro:search): each segment should have ellipsis separately #1614

Merged
merged 1 commit into from
Jul 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/pro/search/demo/Custom.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
:searchFields="searchFields"
:onChange="onChange"
:onSearch="onSearch"
overlayContainer="demo-pro-search-custom"
overlayContainer=".demo-pro-search-custom"
>
<template #userForm="{ value, setValue, ok }">
<IxSpace class="demo-pro-search-custom-user-form" vertical>
Expand Down
161 changes: 14 additions & 147 deletions packages/pro/search/src/components/SearchItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,8 @@
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/

import {
type Ref,
computed,
defineComponent,
inject,
normalizeClass,
onMounted,
onUnmounted,
provide,
ref,
watch,
} from 'vue'
import { computed, defineComponent, inject, normalizeClass, provide, ref, watch } from 'vue'

import { useResizeObserver } from '@idux/cdk/resize'
import { getScroll } from '@idux/cdk/scroll'
import { useEventListener } from '@idux/cdk/utils'
import { IxTooltip } from '@idux/components/tooltip'

import Segment from './segment/Segment'
Expand All @@ -29,7 +15,6 @@ import { useSegmentStates } from '../composables/useSegmentStates'
import { proSearchContext, searchItemContext } from '../token'
import { searchItemProps } from '../types'
import { renderIcon } from '../utils/RenderIcon'
import { getBoxSizingData } from '../utils/getBoxsizingData'

export default defineComponent({
props: searchItemProps,
Expand Down Expand Up @@ -85,31 +70,6 @@ export default defineComponent({
() => searchItemName.value + ' ' + segmentRenderDatas.value.map(data => data.input).join(' '),
)

const { wrapperScroll, segmentScrolls } = useSegmentsScroll(segmentsRef, segmentRenderDatas)

// when we move cursor within input elements,
// current cursor position will be scrolled into view,
// so if ther start segment input and the segments wrapper is scrolled to start,
// there is no ellipsis
//
// however the wrapper may not be scrolled to start because inputs may have padding and border,
// and cursor wont be moved outside content
// so padding and border may always be ouside the wrapper view area
const leftSideEllipsis = computed(() => {
const startEl = segmentScrolls.value[0]?.el
const startElBoxSizingData = startEl && getBoxSizingData(startEl)
const offset = startElBoxSizingData ? startElBoxSizingData.paddingLeft + startElBoxSizingData.borderLeft : 0

return wrapperScroll.value.left > offset + 1 || (segmentScrolls.value[0]?.left ?? 0) > 1
})
const rightSideEllipsis = computed(() => {
const endEl = segmentScrolls.value[1]?.el
const endElBoxSizingData = endEl && getBoxSizingData(endEl)
const offset = endElBoxSizingData ? endElBoxSizingData.paddingRight + endElBoxSizingData.borderRight : 0

return wrapperScroll.value.right > offset + 1 || (segmentScrolls.value[1]?.right ?? 0) > 1
})

const handleNameMouseDown = (evt: MouseEvent) => {
evt.stopPropagation()
evt.preventDefault()
Expand Down Expand Up @@ -141,27 +101,19 @@ export default defineComponent({
<span class={`${prefixCls}-name`} onMousedown={handleNameMouseDown}>
{searchItemName.value}
</span>
<span v-show={leftSideEllipsis.value} class={`${prefixCls}-ellipsis-left`} key="__ellisps_left__">
...
</span>
<span ref={segmentsRef} class={`${prefixCls}-segments`} key="__segments__">
{segmentRenderDatas.value.map(segment => (
<Segment
key={segment.name}
v-slots={slots}
v-show={segment.segmentVisible}
itemKey={props.searchItem!.key}
input={segment.input}
value={segment.value}
selectionStart={segment.selectionStart}
disabled={proSearchProps.disabled}
segment={segment}
/>
))}
</span>
<span v-show={rightSideEllipsis.value} class={`${prefixCls}-ellipsis-right`} key="__ellisps_right__">
...
</span>
{segmentRenderDatas.value.map(segment => (
<Segment
key={segment.name}
v-slots={slots}
v-show={segment.segmentVisible}
itemKey={props.searchItem!.key}
input={segment.input}
value={segment.value}
selectionStart={segment.selectionStart}
disabled={proSearchProps.disabled}
segment={segment}
/>
))}
{!proSearchProps.disabled && (
<span
class={`${prefixCls}-close-icon`}
Expand All @@ -177,88 +129,3 @@ export default defineComponent({
}
},
})

interface SegmentScroll {
el: HTMLElement | undefined
left: number
right: number
}
function useSegmentsScroll(
wrapperRef: Ref<HTMLElement | undefined>,
resetTrigger: Ref<unknown>,
): {
wrapperScroll: Ref<SegmentScroll>
segmentScrolls: Ref<SegmentScroll[]>
} {
const wrapperScroll = ref<SegmentScroll>({ left: 0, right: 0, el: wrapperRef.value })
const segmentScrolls = ref<SegmentScroll[]>([])

const getScrolls = (el: HTMLElement) => {
const { scrollLeft } = getScroll(el)
return {
el,
left: scrollLeft,
right: Math.max(el.scrollWidth - scrollLeft - el.offsetWidth, 0),
}
}

let clearListeners: (() => void) | null = null
let calcSize: (() => void) | null = null
const initScrollListeners = () => {
if (!wrapperRef.value) {
return
}

clearListeners?.()
segmentScrolls.value = []

const calcWrapperScroll = () => {
wrapperScroll.value = getScrolls(wrapperRef.value!)
}
const sizeCalculations = [calcWrapperScroll]

const listenerStopHandlers = [useEventListener(wrapperRef.value, 'scroll', calcWrapperScroll)]

const inputs = wrapperRef.value.querySelectorAll('input')
if (!inputs.length) {
return
}

// listen to the start and end input elements' scroll event
// and add its scroll calculation to the overall scroll calculation
;[inputs.item(0), inputs.item(inputs.length - 1)].forEach((inputEl, index) => {
const scrollCalculation = () => {
segmentScrolls.value[index] = getScrolls(inputEl)
}
sizeCalculations.push(scrollCalculation)
listenerStopHandlers.push(useEventListener(inputEl, 'scroll', scrollCalculation))
})

clearListeners = () => listenerStopHandlers.forEach(stop => stop())
calcSize = () => sizeCalculations.forEach(calc => calc())

calcSize()
}

let resizeStop: (() => void) | null = null
onMounted(() => {
// when reset is triggered by comsumer, we re-init the scroll calculations
watch(resetTrigger, initScrollListeners, { immediate: true })

// when wrapper is resized, calculate scrolls
resizeStop = useResizeObserver(wrapperRef, () => {
calcSize?.()
})
})
onUnmounted(() => {
clearListeners?.()
resizeStop?.()
clearListeners = null
calcSize = null
})

return {
wrapperScroll,
segmentScrolls,
}
}
1 change: 1 addition & 0 deletions packages/pro/search/src/components/segment/Segment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@
class={inputClasses.value}
value={props.input ?? ''}
disabled={props.disabled}
ellipsis={true}
placeholder={props.segment.placeholder}
onInput={handleInput}
onFocus={handleFocus}
Expand Down Expand Up @@ -285,61 +286,61 @@
setOverlayOpened(true)
setSelectionStart()
}
const handleKeyDown = (evt: KeyboardEvent) => {
evt.stopPropagation()
if (overlayOpened.value && panelOnKeyDown.value && !panelOnKeyDown.value(evt)) {
return
}

switch (evt.key) {
case 'Enter':
evt.preventDefault()
if (props.input || overlayOpened.value || !props.segment.panelRenderer) {
confirm()
} else {
setOverlayOpened(true)
}

break
case 'Backspace':
if (props.selectionStart === 0) {
evt.preventDefault()

changeActiveAndSelect(-1, 'end')
}
break
case 'Escape':
setOverlayOpened(false)
break
case 'ArrowLeft':
if ((getInputElement()?.selectionStart ?? 0) <= 0) {
evt.preventDefault()
changeActiveAndSelect(-1, 'end')
} else {
setSelectionStart()
setOverlayOpened(true)
}
break
case 'ArrowRight':
if ((getInputElement()?.selectionStart ?? 0) >= (props.input?.length ?? 0)) {
evt.preventDefault()
changeActiveAndSelect(1, 'start')
} else {
setSelectionStart()
setOverlayOpened(true)
}
break
default:
setSelectionStart()
setOverlayOpened(true)
break
}
}

return {
handleInput,
handleFocus,
handleKeyDown,
setPanelOnKeyDown,
}

Check notice on line 345 in packages/pro/search/src/components/segment/Segment.tsx

View check run for this annotation

codefactor.io / CodeFactor

packages/pro/search/src/components/segment/Segment.tsx#L289-L345

Complex Method
}
98 changes: 94 additions & 4 deletions packages/pro/search/src/components/segment/SegmentInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,22 @@
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/

import { type CSSProperties, computed, defineComponent, inject, normalizeClass, onMounted, ref, watch } from 'vue'

import { callEmit, convertCssPixel, useState } from '@idux/cdk/utils'
import {
type CSSProperties,
type Ref,
computed,
defineComponent,
inject,
normalizeClass,
onMounted,
onUnmounted,
ref,
watch,
} from 'vue'

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'
Expand All @@ -19,6 +32,7 @@ export default defineComponent({
setup(props: SegmentInputProps, { attrs, expose }) {
const { mergedPrefixCls } = inject(proSearchContext)!

const segmentWrapperRef = ref<HTMLElement>()
const segmentInputRef = ref<HTMLInputElement>()

const [inputWidth, setInputWidth] = useState(0)
Expand Down Expand Up @@ -60,13 +74,24 @@ export default defineComponent({
})

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

return (
<span class={normalizeClass([classes.value, className])}>
<span ref={segmentWrapperRef} class={normalizeClass([classes.value, className])}>
<span
v-show={props.ellipsis && leftSideEllipsis.value}
class={`${prefixCls}-ellipsis-left`}
key="__ellisps_left__"
>
...
</span>
<input
ref={segmentInputRef}
class={`${prefixCls}-inner`}
Expand All @@ -79,6 +104,13 @@ export default defineComponent({
onCompositionend={handleCompositionEnd}
{...rest}
></input>
<span
v-show={props.ellipsis && rightSideEllipsis.value}
class={`${prefixCls}-ellipsis-right`}
key="__ellisps_right__"
>
...
</span>
<MeasureElement onWidthChange={setInputWidth}>{props.value || props.placeholder || ''}</MeasureElement>
</span>
)
Expand Down Expand Up @@ -116,3 +148,61 @@ function useInputEvents(props: SegmentInputProps): InputEventHandlers {
handleCompositionEnd,
}
}

interface SegmentScroll {
left: number
right: number
}

function useSegmentScroll(props: SegmentInputProps, inputRef: Ref<HTMLInputElement | undefined>) {
const [segmentScroll, setSegmentScroll] = useState<SegmentScroll>({ left: 0, right: 0 }, true)
let inputWidth = 0
let oldSegmentScroll: SegmentScroll = { left: 0, right: 0 }

let stopScrollListen: (() => void) | undefined
let stopResizeListen: (() => void) | undefined

const getScrolls = (): SegmentScroll => {
const el = inputRef.value
if (!el) {
return { left: 0, right: 0 }
}

const { scrollLeft } = getScroll(el)
return {
left: scrollLeft,
right: Math.max(el.scrollWidth - scrollLeft - el.offsetWidth, 0),
}
}

const updateScroll = () => {
oldSegmentScroll = segmentScroll.value
setSegmentScroll(getScrolls())
}

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 {
updateScroll()
}
})
}
})

onUnmounted(() => {
stopScrollListen?.()
stopResizeListen?.()
})

return segmentScroll
}
1 change: 1 addition & 0 deletions packages/pro/search/src/components/segment/TempSegment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export default defineComponent({
class={`${mergedPrefixCls.value}-temp-segment-input`}
style={nameInputStyle.value}
value={input.value}
ellipsis={false}
onMousedown={handleMouseDown}
onInput={handleInput}
onKeydown={handleKeyDown}
Expand Down
1 change: 1 addition & 0 deletions packages/pro/search/src/types/segment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export type SegmentProps = ExtractInnerPropTypes<typeof segmentProps>
export const segmentIputProps = {
value: String,
disabled: Boolean,
ellipsis: Boolean,
placeholder: String,
onInput: [Function, Array] as PropType<MaybeArray<(input: string) => void>>,
onWidthChange: [Function, Array] as PropType<MaybeArray<(width: number) => void>>,
Expand Down
Loading
Loading