mirror of
https://github.com/element-plus/element-plus.git
synced 2025-02-23 11:59:34 +08:00
perf(components): [el-scrollbar] prevent re-render when scrolling (#5670)
* perf(components): [el-scrollbar] prevent re-render when scrolling * chore: improve code
This commit is contained in:
parent
8322bcf843
commit
0d8f08ab9b
@ -7,4 +7,4 @@ export default ElScrollbar
|
||||
|
||||
export * from './src/util'
|
||||
export * from './src/scrollbar'
|
||||
export * from './src/bar'
|
||||
export * from './src/thumb'
|
||||
|
@ -2,13 +2,25 @@ import { buildProps } from '@element-plus/utils/props'
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
|
||||
export const barProps = buildProps({
|
||||
vertical: Boolean,
|
||||
size: String,
|
||||
move: Number,
|
||||
ratio: {
|
||||
type: Number,
|
||||
required: true,
|
||||
always: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
ratioX: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
ratioY: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
always: Boolean,
|
||||
} as const)
|
||||
export type BarProps = ExtractPropTypes<typeof barProps>
|
||||
|
@ -1,193 +1,42 @@
|
||||
<template>
|
||||
<transition name="el-scrollbar-fade">
|
||||
<div
|
||||
v-show="always || visible"
|
||||
ref="instance"
|
||||
:class="['el-scrollbar__bar', 'is-' + bar.key]"
|
||||
@mousedown="clickTrackHandler"
|
||||
>
|
||||
<div
|
||||
ref="thumb"
|
||||
class="el-scrollbar__thumb"
|
||||
:style="thumbStyle"
|
||||
@mousedown="clickThumbHandler"
|
||||
></div>
|
||||
</div>
|
||||
</transition>
|
||||
<thumb :move="moveX" :ratio="ratioX" :size="width" :always="always" />
|
||||
<thumb
|
||||
:move="moveY"
|
||||
:ratio="ratioY"
|
||||
:size="height"
|
||||
vertical
|
||||
:always="always"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
inject,
|
||||
onBeforeUnmount,
|
||||
ref,
|
||||
toRef,
|
||||
} from 'vue'
|
||||
import { useEventListener, isClient } from '@vueuse/core'
|
||||
import { scrollbarContextKey } from '@element-plus/tokens'
|
||||
import { throwError } from '@element-plus/utils/error'
|
||||
import { BAR_MAP, renderThumbStyle } from './util'
|
||||
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import Thumb from './thumb.vue'
|
||||
import { barProps } from './bar'
|
||||
|
||||
const COMPONENT_NAME = 'Bar'
|
||||
export default defineComponent({
|
||||
name: COMPONENT_NAME,
|
||||
components: {
|
||||
Thumb,
|
||||
},
|
||||
props: barProps,
|
||||
|
||||
setup(props) {
|
||||
const scrollbar = inject(scrollbarContextKey)
|
||||
if (!scrollbar)
|
||||
throwError(COMPONENT_NAME, 'can not inject scrollbar context')
|
||||
const moveX = ref(0)
|
||||
const moveY = ref(0)
|
||||
const GAP = 4 // top 2 + bottom 2 of bar instance
|
||||
|
||||
const instance = ref<HTMLDivElement>()
|
||||
const thumb = ref<HTMLDivElement>()
|
||||
const handleScroll = (wrap: HTMLDivElement) => {
|
||||
if (wrap) {
|
||||
const offsetHeight = wrap.offsetHeight - GAP
|
||||
const offsetWidth = wrap.offsetWidth - GAP
|
||||
|
||||
const barStore = ref({})
|
||||
const visible = ref(false)
|
||||
|
||||
let cursorDown = false
|
||||
let cursorLeave = false
|
||||
let onselectstartStore:
|
||||
| ((this: GlobalEventHandlers, ev: Event) => any)
|
||||
| null = isClient ? document.onselectstart : null
|
||||
|
||||
const bar = computed(
|
||||
() => BAR_MAP[props.vertical ? 'vertical' : 'horizontal']
|
||||
)
|
||||
|
||||
const thumbStyle = computed(() =>
|
||||
renderThumbStyle({
|
||||
size: props.size,
|
||||
move: props.move,
|
||||
bar: bar.value,
|
||||
})
|
||||
)
|
||||
|
||||
const offsetRatio = computed(
|
||||
() =>
|
||||
// offsetRatioX = original width of thumb / current width of thumb / ratioX
|
||||
// offsetRatioY = original height of thumb / current height of thumb / ratioY
|
||||
// instance height = wrap height - GAP
|
||||
instance.value![bar.value.offset] ** 2 /
|
||||
scrollbar.wrapElement![bar.value.scrollSize] /
|
||||
props.ratio /
|
||||
thumb.value![bar.value.offset]
|
||||
)
|
||||
|
||||
const clickThumbHandler = (e: MouseEvent) => {
|
||||
// prevent click event of middle and right button
|
||||
e.stopPropagation()
|
||||
if (e.ctrlKey || [1, 2].includes(e.button)) return
|
||||
|
||||
window.getSelection()?.removeAllRanges()
|
||||
startDrag(e)
|
||||
|
||||
const el = e.currentTarget as HTMLDivElement
|
||||
if (!el) return
|
||||
barStore.value[bar.value.axis] =
|
||||
el[bar.value.offset] -
|
||||
(e[bar.value.client] - el.getBoundingClientRect()[bar.value.direction])
|
||||
moveY.value = ((wrap.scrollTop * 100) / offsetHeight) * props.ratioY
|
||||
moveX.value = ((wrap.scrollLeft * 100) / offsetWidth) * props.ratioX
|
||||
}
|
||||
}
|
||||
|
||||
const clickTrackHandler = (e: MouseEvent) => {
|
||||
if (!thumb.value || !instance.value || !scrollbar.wrapElement) return
|
||||
|
||||
const offset = Math.abs(
|
||||
(e.target as HTMLElement).getBoundingClientRect()[bar.value.direction] -
|
||||
e[bar.value.client]
|
||||
)
|
||||
const thumbHalf = thumb.value[bar.value.offset] / 2
|
||||
const thumbPositionPercentage =
|
||||
((offset - thumbHalf) * 100 * offsetRatio.value) /
|
||||
instance.value[bar.value.offset]
|
||||
|
||||
scrollbar.wrapElement[bar.value.scroll] =
|
||||
(thumbPositionPercentage *
|
||||
scrollbar.wrapElement[bar.value.scrollSize]) /
|
||||
100
|
||||
}
|
||||
|
||||
const startDrag = (e: MouseEvent) => {
|
||||
e.stopImmediatePropagation()
|
||||
cursorDown = true
|
||||
document.addEventListener('mousemove', mouseMoveDocumentHandler)
|
||||
document.addEventListener('mouseup', mouseUpDocumentHandler)
|
||||
onselectstartStore = document.onselectstart
|
||||
document.onselectstart = () => false
|
||||
}
|
||||
|
||||
const mouseMoveDocumentHandler = (e: MouseEvent) => {
|
||||
if (!instance.value || !thumb.value) return
|
||||
if (cursorDown === false) return
|
||||
|
||||
const prevPage = barStore.value[bar.value.axis]
|
||||
if (!prevPage) return
|
||||
|
||||
const offset =
|
||||
(instance.value.getBoundingClientRect()[bar.value.direction] -
|
||||
e[bar.value.client]) *
|
||||
-1
|
||||
const thumbClickPosition = thumb.value[bar.value.offset] - prevPage
|
||||
const thumbPositionPercentage =
|
||||
((offset - thumbClickPosition) * 100 * offsetRatio.value) /
|
||||
instance.value[bar.value.offset]
|
||||
scrollbar.wrapElement[bar.value.scroll] =
|
||||
(thumbPositionPercentage *
|
||||
scrollbar.wrapElement[bar.value.scrollSize]) /
|
||||
100
|
||||
}
|
||||
|
||||
const mouseUpDocumentHandler = () => {
|
||||
cursorDown = false
|
||||
barStore.value[bar.value.axis] = 0
|
||||
document.removeEventListener('mousemove', mouseMoveDocumentHandler)
|
||||
document.removeEventListener('mouseup', mouseUpDocumentHandler)
|
||||
restoreOnselectstart()
|
||||
if (cursorLeave) visible.value = false
|
||||
}
|
||||
|
||||
const mouseMoveScrollbarHandler = () => {
|
||||
cursorLeave = false
|
||||
visible.value = !!props.size
|
||||
}
|
||||
|
||||
const mouseLeaveScrollbarHandler = () => {
|
||||
cursorLeave = true
|
||||
visible.value = cursorDown
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
restoreOnselectstart()
|
||||
document.removeEventListener('mouseup', mouseUpDocumentHandler)
|
||||
})
|
||||
|
||||
const restoreOnselectstart = () => {
|
||||
if (document.onselectstart !== onselectstartStore)
|
||||
document.onselectstart = onselectstartStore
|
||||
}
|
||||
|
||||
useEventListener(
|
||||
toRef(scrollbar, 'scrollbarElement'),
|
||||
'mousemove',
|
||||
mouseMoveScrollbarHandler
|
||||
)
|
||||
useEventListener(
|
||||
toRef(scrollbar, 'scrollbarElement'),
|
||||
'mouseleave',
|
||||
mouseLeaveScrollbarHandler
|
||||
)
|
||||
|
||||
return {
|
||||
instance,
|
||||
thumb,
|
||||
bar,
|
||||
thumbStyle,
|
||||
visible,
|
||||
clickTrackHandler,
|
||||
clickThumbHandler,
|
||||
handleScroll,
|
||||
moveX,
|
||||
moveY,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
@ -20,14 +20,14 @@
|
||||
</component>
|
||||
</div>
|
||||
<template v-if="!native">
|
||||
<bar :move="moveX" :ratio="ratioX" :size="sizeWidth" :always="always" />
|
||||
<bar
|
||||
:move="moveY"
|
||||
:ratio="ratioY"
|
||||
:size="sizeHeight"
|
||||
vertical
|
||||
ref="barRef"
|
||||
:height="sizeHeight"
|
||||
:width="sizeWidth"
|
||||
:always="always"
|
||||
/>
|
||||
:ratio-x="ratioX"
|
||||
:ratio-y="ratioY"
|
||||
></bar>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@ -69,6 +69,7 @@ export default defineComponent({
|
||||
|
||||
const sizeWidth = ref('0')
|
||||
const sizeHeight = ref('0')
|
||||
const barRef = ref()
|
||||
const moveX = ref(0)
|
||||
const moveY = ref(0)
|
||||
const ratioY = ref(1)
|
||||
@ -85,13 +86,7 @@ export default defineComponent({
|
||||
|
||||
const handleScroll = () => {
|
||||
if (wrap$.value) {
|
||||
const offsetHeight = wrap$.value.offsetHeight - GAP
|
||||
const offsetWidth = wrap$.value.offsetWidth - GAP
|
||||
|
||||
moveY.value =
|
||||
((wrap$.value.scrollTop * 100) / offsetHeight) * ratioY.value
|
||||
moveX.value =
|
||||
((wrap$.value.scrollLeft * 100) / offsetWidth) * ratioX.value
|
||||
barRef.value?.handleScroll(wrap$.value)
|
||||
|
||||
emit('scroll', {
|
||||
scrollTop: wrap$.value.scrollTop,
|
||||
@ -170,7 +165,7 @@ export default defineComponent({
|
||||
scrollbar$,
|
||||
wrap$,
|
||||
resize$,
|
||||
|
||||
barRef,
|
||||
moveX,
|
||||
moveY,
|
||||
ratioX,
|
||||
|
14
packages/components/scrollbar/src/thumb.ts
Normal file
14
packages/components/scrollbar/src/thumb.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { buildProps } from '@element-plus/utils/props'
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
|
||||
export const thumbProps = buildProps({
|
||||
vertical: Boolean,
|
||||
size: String,
|
||||
move: Number,
|
||||
ratio: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
always: Boolean,
|
||||
} as const)
|
||||
export type ThumbProps = ExtractPropTypes<typeof thumbProps>
|
194
packages/components/scrollbar/src/thumb.vue
Normal file
194
packages/components/scrollbar/src/thumb.vue
Normal file
@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<transition name="el-scrollbar-fade">
|
||||
<div
|
||||
v-show="always || visible"
|
||||
ref="instance"
|
||||
:class="['el-scrollbar__bar', 'is-' + bar.key]"
|
||||
@mousedown="clickTrackHandler"
|
||||
>
|
||||
<div
|
||||
ref="thumb"
|
||||
class="el-scrollbar__thumb"
|
||||
:style="thumbStyle"
|
||||
@mousedown="clickThumbHandler"
|
||||
></div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
computed,
|
||||
defineComponent,
|
||||
inject,
|
||||
onBeforeUnmount,
|
||||
ref,
|
||||
toRef,
|
||||
} from 'vue'
|
||||
import { useEventListener, isClient } from '@vueuse/core'
|
||||
import { scrollbarContextKey } from '@element-plus/tokens'
|
||||
import { throwError } from '@element-plus/utils/error'
|
||||
import { BAR_MAP, renderThumbStyle } from './util'
|
||||
|
||||
import { thumbProps } from './thumb'
|
||||
|
||||
const COMPONENT_NAME = 'Thumb'
|
||||
export default defineComponent({
|
||||
name: COMPONENT_NAME,
|
||||
props: thumbProps,
|
||||
|
||||
setup(props) {
|
||||
const scrollbar = inject(scrollbarContextKey)
|
||||
if (!scrollbar)
|
||||
throwError(COMPONENT_NAME, 'can not inject scrollbar context')
|
||||
|
||||
const instance = ref<HTMLDivElement>()
|
||||
const thumb = ref<HTMLDivElement>()
|
||||
|
||||
const thumbState = ref({})
|
||||
const visible = ref(false)
|
||||
|
||||
let cursorDown = false
|
||||
let cursorLeave = false
|
||||
let originalOnSelectStart:
|
||||
| ((this: GlobalEventHandlers, ev: Event) => any)
|
||||
| null = isClient ? document.onselectstart : null
|
||||
|
||||
const bar = computed(
|
||||
() => BAR_MAP[props.vertical ? 'vertical' : 'horizontal']
|
||||
)
|
||||
|
||||
const thumbStyle = computed(() =>
|
||||
renderThumbStyle({
|
||||
size: props.size,
|
||||
move: props.move,
|
||||
bar: bar.value,
|
||||
})
|
||||
)
|
||||
|
||||
const offsetRatio = computed(
|
||||
() =>
|
||||
// offsetRatioX = original width of thumb / current width of thumb / ratioX
|
||||
// offsetRatioY = original height of thumb / current height of thumb / ratioY
|
||||
// instance height = wrap height - GAP
|
||||
instance.value![bar.value.offset] ** 2 /
|
||||
scrollbar.wrapElement![bar.value.scrollSize] /
|
||||
props.ratio /
|
||||
thumb.value![bar.value.offset]
|
||||
)
|
||||
|
||||
const clickThumbHandler = (e: MouseEvent) => {
|
||||
// prevent click event of middle and right button
|
||||
e.stopPropagation()
|
||||
if (e.ctrlKey || [1, 2].includes(e.button)) return
|
||||
|
||||
window.getSelection()?.removeAllRanges()
|
||||
startDrag(e)
|
||||
|
||||
const el = e.currentTarget as HTMLDivElement
|
||||
if (!el) return
|
||||
thumbState.value[bar.value.axis] =
|
||||
el[bar.value.offset] -
|
||||
(e[bar.value.client] - el.getBoundingClientRect()[bar.value.direction])
|
||||
}
|
||||
|
||||
const clickTrackHandler = (e: MouseEvent) => {
|
||||
if (!thumb.value || !instance.value || !scrollbar.wrapElement) return
|
||||
|
||||
const offset = Math.abs(
|
||||
(e.target as HTMLElement).getBoundingClientRect()[bar.value.direction] -
|
||||
e[bar.value.client]
|
||||
)
|
||||
const thumbHalf = thumb.value[bar.value.offset] / 2
|
||||
const thumbPositionPercentage =
|
||||
((offset - thumbHalf) * 100 * offsetRatio.value) /
|
||||
instance.value[bar.value.offset]
|
||||
|
||||
scrollbar.wrapElement[bar.value.scroll] =
|
||||
(thumbPositionPercentage *
|
||||
scrollbar.wrapElement[bar.value.scrollSize]) /
|
||||
100
|
||||
}
|
||||
|
||||
const startDrag = (e: MouseEvent) => {
|
||||
e.stopImmediatePropagation()
|
||||
cursorDown = true
|
||||
document.addEventListener('mousemove', mouseMoveDocumentHandler)
|
||||
document.addEventListener('mouseup', mouseUpDocumentHandler)
|
||||
originalOnSelectStart = document.onselectstart
|
||||
document.onselectstart = () => false
|
||||
}
|
||||
|
||||
const mouseMoveDocumentHandler = (e: MouseEvent) => {
|
||||
if (!instance.value || !thumb.value) return
|
||||
if (cursorDown === false) return
|
||||
|
||||
const prevPage = thumbState.value[bar.value.axis]
|
||||
if (!prevPage) return
|
||||
|
||||
const offset =
|
||||
(instance.value.getBoundingClientRect()[bar.value.direction] -
|
||||
e[bar.value.client]) *
|
||||
-1
|
||||
const thumbClickPosition = thumb.value[bar.value.offset] - prevPage
|
||||
const thumbPositionPercentage =
|
||||
((offset - thumbClickPosition) * 100 * offsetRatio.value) /
|
||||
instance.value[bar.value.offset]
|
||||
scrollbar.wrapElement[bar.value.scroll] =
|
||||
(thumbPositionPercentage *
|
||||
scrollbar.wrapElement[bar.value.scrollSize]) /
|
||||
100
|
||||
}
|
||||
|
||||
const mouseUpDocumentHandler = () => {
|
||||
cursorDown = false
|
||||
thumbState.value[bar.value.axis] = 0
|
||||
document.removeEventListener('mousemove', mouseMoveDocumentHandler)
|
||||
document.removeEventListener('mouseup', mouseUpDocumentHandler)
|
||||
restoreOnselectstart()
|
||||
if (cursorLeave) visible.value = false
|
||||
}
|
||||
|
||||
const mouseMoveScrollbarHandler = () => {
|
||||
cursorLeave = false
|
||||
visible.value = !!props.size
|
||||
}
|
||||
|
||||
const mouseLeaveScrollbarHandler = () => {
|
||||
cursorLeave = true
|
||||
visible.value = cursorDown
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
restoreOnselectstart()
|
||||
document.removeEventListener('mouseup', mouseUpDocumentHandler)
|
||||
})
|
||||
|
||||
const restoreOnselectstart = () => {
|
||||
if (document.onselectstart !== originalOnSelectStart)
|
||||
document.onselectstart = originalOnSelectStart
|
||||
}
|
||||
|
||||
useEventListener(
|
||||
toRef(scrollbar, 'scrollbarElement'),
|
||||
'mousemove',
|
||||
mouseMoveScrollbarHandler
|
||||
)
|
||||
useEventListener(
|
||||
toRef(scrollbar, 'scrollbarElement'),
|
||||
'mouseleave',
|
||||
mouseLeaveScrollbarHandler
|
||||
)
|
||||
|
||||
return {
|
||||
instance,
|
||||
thumb,
|
||||
bar,
|
||||
thumbStyle,
|
||||
visible,
|
||||
clickTrackHandler,
|
||||
clickThumbHandler,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
Loading…
Reference in New Issue
Block a user