Skip to content

Commit

Permalink
fix(pro:textarea): optimize performance (#1968)
Browse files Browse the repository at this point in the history
  • Loading branch information
sallerli1 authored Jul 22, 2024
1 parent 9eccfcb commit 0fade99
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 23 deletions.
35 changes: 28 additions & 7 deletions packages/pro/textarea/src/IndexColumn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,48 @@ import { proTextareaContext } from './token'

export default defineComponent({
setup() {
const { props, mergedPrefixCls, rowCounts, lineHeight, boxSizingData, setvisibleErrIndex } =
inject(proTextareaContext)!
const {
mergedPrefixCls,
errSet,
rowCounts,
renderedLinesIndex,
renderedTopOffset,
lineHeight,
boxSizingData,
setvisibleErrIndex,
} = inject(proTextareaContext)!

const mergedRowCnts = computed(() => (rowCounts.value.length > 0 ? rowCounts.value : [0]))
const mergedRowCnts = computed(() => {
if (rowCounts.value.length > 0) {
const { start, end } = renderedLinesIndex.value
return rowCounts.value.slice(start, end + 1)
}

return [0]
})
const columnStyle = computed(() =>
normalizeStyle({
paddingTop: `${boxSizingData.value?.paddingTop ?? 0}px`,
paddingTop: `${(boxSizingData.value?.paddingTop ?? 0) + renderedTopOffset.value}px`,
paddingBottom: `${boxSizingData.value?.paddingBottom ?? 0}px`,
}),
)

return () => {
const prefixCls = `${mergedPrefixCls.value}-index-column`

const _errSet = errSet.value
const _lineHeight = lineHeight.value
const start = renderedLinesIndex.value.start

return (
<div class={prefixCls} style={columnStyle.value}>
{mergedRowCnts.value.map((cnt, index) => {
const cellStyle = normalizeStyle({
height: `${cnt * lineHeight.value}px`,
height: `${cnt * _lineHeight}px`,
})

const errorIdx = props.errors?.find(error => error.index === index)?.index ?? -1
const lineIndex = index + start
const errorIdx = _errSet.has(lineIndex) ? lineIndex : -1
const cellClass = normalizeClass({
[`${prefixCls}-cell`]: true,
[`${prefixCls}-cell-error`]: errorIdx > -1,
Expand All @@ -47,12 +67,13 @@ export default defineComponent({

return (
<div
key={lineIndex}
class={cellClass}
style={cellStyle}
onMouseenter={handleCellMouseEnter}
onMouseleave={handleCellMouseLeave}
>
{index + 1}
{lineIndex + 1}
</div>
)
})}
Expand Down
10 changes: 8 additions & 2 deletions packages/pro/textarea/src/ProTextarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { useThemeToken } from '@idux/pro/theme'

import IndexColumn from './IndexColumn'
import { useErrorLines } from './composables/useErrorLines'
import { useErrorLineRender } from './composables/useLineRender'
import { useRowCounts } from './composables/useRowsCounts'
import Content from './content/Content'
import { proTextareaContext } from './token'
Expand Down Expand Up @@ -63,9 +64,12 @@ export default defineComponent({
} = ɵUseInput<HTMLTextAreaElement>(props, config)
const valueRef = toRef(accessor, 'value')

const scrollHolderRef = ref<HTMLElement>()

const { lineHeight, boxSizingData, resizeToFitContent } = ɵUseAutoRows(elementRef, ref(true))
const rowCounts = useRowCounts(props, elementRef, valueRef, lineHeight, boxSizingData)
const { rowCounts, rowHeights } = useRowCounts(props, elementRef, valueRef, lineHeight, boxSizingData)
const errorLinesContext = useErrorLines(props, lineHeight, rowCounts, boxSizingData)
const errorLineRenderContext = useErrorLineRender(props, rowHeights, scrollHolderRef)
const dataCount = useDataCount(props, config, valueRef)

const _handleInput = (evt: Event) => {
Expand All @@ -90,8 +94,10 @@ export default defineComponent({
boxSizingData,
lineHeight,
rowCounts,
rowHeights,
textareaRef: elementRef,
...errorLinesContext,
...errorLineRenderContext,
handleInput: _handleInput,
handleCompositionStart,
handleCompositionEnd,
Expand Down Expand Up @@ -145,7 +151,7 @@ export default defineComponent({
return (
<span class={classes.value} style={style.value}>
<div class={`${prefixCls}-index-column-bg`}></div>
<div class={`${prefixCls}-scroll-holder`} onMousedown={handleScrollHolderMouseDown}>
<div ref={scrollHolderRef} class={`${prefixCls}-scroll-holder`} onMousedown={handleScrollHolderMouseDown}>
<div class={`${prefixCls}-inner`}>
<IndexColumn />
<Content />
Expand Down
7 changes: 6 additions & 1 deletion packages/pro/textarea/src/composables/useErrorLines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@

import type { ProTextareaProps } from '../types'
import type { ɵBoxSizingData } from '@idux/components/textarea'
import type { ComputedRef, Ref } from 'vue'

import { type ComputedRef, type Ref, computed } from 'vue'

import { useState } from '@idux/cdk/utils'

export interface ErrorLinesContext {
visibleErrIndex: ComputedRef<number>
errSet: ComputedRef<Set<number>>
setvisibleErrIndex: (errIndex: number) => void
handleTextareaMouseMove: (evt: MouseEvent) => void
handleTextareaMouseLeave: (evt: MouseEvent) => void
Expand All @@ -26,6 +28,8 @@ export function useErrorLines(
): ErrorLinesContext {
const [visibleErrIndex, setvisibleErrIndex] = useState<number>(-1)

const errSet = computed(() => new Set(props.errors?.map(err => err.index)))

let currentRowTopLineIdx = -1
let currentRowBottomLineIdx = -1

Expand Down Expand Up @@ -69,6 +73,7 @@ export function useErrorLines(

return {
visibleErrIndex,
errSet,
setvisibleErrIndex,
handleTextareaMouseMove,
handleTextareaMouseLeave,
Expand Down
112 changes: 112 additions & 0 deletions packages/pro/textarea/src/composables/useLineRender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* @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 { ProTextareaProps, TextareaError } from '../types'

import { type ComputedRef, type Ref, onMounted, watch } from 'vue'

import { cancelRAF, rAF, useEventListener, useState } from '@idux/cdk/utils'

export interface LineRenderContext {
renderedErrors: ComputedRef<TextareaError[]>
renderedLinesIndex: ComputedRef<{ start: number; end: number }>
renderedTopOffset: ComputedRef<number>
}

export function useErrorLineRender(
props: ProTextareaProps,
rowHeights: ComputedRef<number[]>,
scrollHolderRef: Ref<HTMLElement | undefined>,
): LineRenderContext {
const [renderedErrors, setRenderedErrors] = useState<TextareaError[]>([])
const [renderedLinesIndex, setRenderedLinesIndex] = useState<{ start: number; end: number }>({ start: 0, end: 0 })
const [renderedTopOffset, setRenderedTopOffset] = useState<number>(0)

const calcErrorRenderState = () => {
if (!scrollHolderRef.value) {
setRenderedErrors([])
setRenderedLinesIndex({ start: 0, end: 0 })
setRenderedTopOffset(0)
return
}

const errors = [...(props.errors ?? [])].sort((err1, err2) => err2.index - err1.index)
const heights = rowHeights.value

const { scrollTop, clientHeight } = scrollHolderRef.value

const top = scrollTop
const bottom = top + clientHeight

let currentBottom = 0
let currentTop = 0
let currentErr = errors.pop()

const newErrors: TextareaError[] = []
let lineTopIndex: number = -1
let lineBottomIndex: number = 0
let offsetTop: number = 0

for (let index = 0; index < heights.length; index++) {
const height = heights[index]
const isCurrentErrIdx = currentErr && currentErr.index === index

currentBottom += height

if (currentBottom < top) {
if (isCurrentErrIdx) {
currentErr = errors.pop()
}

currentTop += height
continue
}

if (lineTopIndex === -1) {
lineTopIndex = index
offsetTop = currentTop
}

if (isCurrentErrIdx) {
newErrors.push(currentErr!)
currentErr = errors.pop()
}

lineBottomIndex = index

if (currentTop > bottom) {
break
}

currentTop += height
}

setRenderedErrors(newErrors)
setRenderedLinesIndex({ start: lineTopIndex, end: lineBottomIndex })
setRenderedTopOffset(offsetTop)
}

onMounted(() => {
watch([() => props.errors, rowHeights, scrollHolderRef], calcErrorRenderState, { immediate: true })
})

let rafId: number | null
useEventListener(scrollHolderRef, 'scroll', () => {
rafId && cancelRAF(rafId)

rafId = rAF(() => {
calcErrorRenderState()
rafId = null
})
})

return {
renderedErrors,
renderedLinesIndex,
renderedTopOffset,
}
}
56 changes: 45 additions & 11 deletions packages/pro/textarea/src/composables/useRowsCounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,43 +13,77 @@ import { useResizeObserver } from '@idux/cdk/resize'
import { useState } from '@idux/cdk/utils'
import { type ɵBoxSizingData, ɵMeasureTextarea } from '@idux/components/textarea'

export interface RowCountsContext {
rowCounts: ComputedRef<number[]>
rowHeights: ComputedRef<number[]>
}

export function useRowCounts(
props: ProTextareaProps,
textareaRef: Ref<HTMLTextAreaElement | undefined>,
valueRef: Ref<string | undefined>,
lineHeight: Ref<number>,
sizingData: ComputedRef<ɵBoxSizingData>,
): ComputedRef<number[]> {
): RowCountsContext {
const [rowCounts, setRowCounts] = useState<number[]>([])
const [rowHeights, setRowHeights] = useState<number[]>([])

let cachedRowCharLength: number[] = []

const calcRowCounts = () => {
const textarea = textareaRef.value!
const lines = valueRef.value?.split('\n') ?? []
const { paddingSize } = sizingData.value
const { rows } = props
const currentRowCounts = rowCounts.value
const currentRowHeights = rowHeights.value

const counts: number[] = []
const heights: number[] = []

const res = lines.map(line =>
ɵMeasureTextarea(
lines.forEach((line, index) => {
const charLength = line.length
if (cachedRowCharLength.length && cachedRowCharLength[index] === charLength) {
counts[index] = currentRowCounts[index]
heights[index] = currentRowHeights[index]
return
}

cachedRowCharLength[index] = charLength

const height = ɵMeasureTextarea(
textarea,
el => {
el.value = line || 'x'

// trigger reflow to ensure scrollHeight is calculated when referenced
void el.scrollHeight
return Math.round((el.scrollHeight - paddingSize) / lineHeight.value)
return el.scrollHeight - paddingSize
},
true,
),
)
)

if (rows && res.length < rows) {
res.push(...new Array(rows - res.length).fill(1))
counts[index] = Math.round(height / lineHeight.value)
heights[index] = height
})

if (rows && lines.length < rows) {
counts.push(...new Array(rows - lines.length).fill(1))
heights.push(...new Array(rows - lines.length).fill(lineHeight.value))
}

setRowCounts(res)
setRowCounts(counts)
setRowHeights(heights)
}

watch([valueRef, () => props.rows], calcRowCounts)
useResizeObserver(textareaRef, calcRowCounts)
useResizeObserver(textareaRef, () => {
cachedRowCharLength = []
calcRowCounts()
})

return rowCounts
return {
rowCounts,
rowHeights,
}
}
4 changes: 3 additions & 1 deletion packages/pro/textarea/src/content/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default defineComponent({
accessor,
boxSizingData,
rowCounts,
renderedErrors,
lineHeight,
textareaRef,
visibleErrIndex,
Expand All @@ -34,11 +35,12 @@ export default defineComponent({
} = inject(proTextareaContext)!

const renderErrorLines = () =>
props.errors
renderedErrors.value
?.map(
error =>
rowCounts.value.length > error.index && (
<ErrorLine
key={error.index}
style={getErrorLineStyle(error, rowCounts.value, lineHeight.value, boxSizingData.value?.paddingTop)}
message={error.message}
visible={error.index === visibleErrIndex.value}
Expand Down
4 changes: 3 additions & 1 deletion packages/pro/textarea/src/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@
*/

import type { ErrorLinesContext } from './composables/useErrorLines'
import type { LineRenderContext } from './composables/useLineRender'
import type { ProTextareaProps } from './types'
import type { FormAccessor } from '@idux/cdk/forms'
import type { ɵBoxSizingData } from '@idux/components/textarea'
import type { ComputedRef, InjectionKey, Ref } from 'vue'

export interface ProTextareaContext extends ErrorLinesContext {
export interface ProTextareaContext extends ErrorLinesContext, LineRenderContext {
props: ProTextareaProps
accessor: FormAccessor
boxSizingData: ComputedRef<ɵBoxSizingData>
lineHeight: Ref<number>
mergedPrefixCls: ComputedRef<string>
rowCounts: ComputedRef<number[]>
rowHeights: ComputedRef<number[]>
textareaRef: Ref<HTMLTextAreaElement | undefined>
handleInput: (evt: Event) => void
handleCompositionStart: (evt: CompositionEvent) => void
Expand Down

0 comments on commit 0fade99

Please sign in to comment.