refactor(components): [scrollbar] switch to script-setup syntax (#7986)

This commit is contained in:
zz
2022-05-31 07:55:03 +08:00
committed by GitHub
parent d3f50525f8
commit 54efcfbdfd
8 changed files with 290 additions and 336 deletions

View File

@@ -1,19 +1,14 @@
import { buildProps } from '@element-plus/utils'
import type { ExtractPropTypes } from 'vue'
import type Bar from './bar.vue'
export const barProps = buildProps({
always: {
type: Boolean,
default: true,
},
width: {
type: String,
default: '',
},
height: {
type: String,
default: '',
},
width: String,
height: String,
ratioX: {
type: Number,
default: 1,
@@ -24,3 +19,5 @@ export const barProps = buildProps({
},
} as const)
export type BarProps = ExtractPropTypes<typeof barProps>
export type BarInstance = InstanceType<typeof Bar>

View File

@@ -8,36 +8,28 @@
:always="always"
/>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
<script lang="ts" setup>
import { ref } from 'vue'
import { GAP } from './util'
import Thumb from './thumb.vue'
import { barProps } from './bar'
export default defineComponent({
components: {
Thumb,
},
props: barProps,
setup(props) {
const moveX = ref(0)
const moveY = ref(0)
const GAP = 4 // top 2 + bottom 2 of bar instance
const props = defineProps(barProps)
const handleScroll = (wrap: HTMLDivElement) => {
if (wrap) {
const offsetHeight = wrap.offsetHeight - GAP
const offsetWidth = wrap.offsetWidth - GAP
const moveX = ref(0)
const moveY = ref(0)
moveY.value = ((wrap.scrollTop * 100) / offsetHeight) * props.ratioY
moveX.value = ((wrap.scrollLeft * 100) / offsetWidth) * props.ratioX
}
}
const handleScroll = (wrap: HTMLDivElement) => {
if (wrap) {
const offsetHeight = wrap.offsetHeight - GAP
const offsetWidth = wrap.offsetWidth - GAP
return {
handleScroll,
moveX,
moveY,
}
},
moveY.value = ((wrap.scrollTop * 100) / offsetHeight) * props.ratioY
moveX.value = ((wrap.scrollLeft * 100) / offsetWidth) * props.ratioX
}
}
defineExpose({
handleScroll,
})
</script>

View File

@@ -1,5 +1,6 @@
import { buildProps, definePropType, isNumber } from '@element-plus/utils'
import type { ExtractPropTypes, StyleValue } from 'vue'
import type Scrollbar from './scrollbar.vue'
export const scrollbarProps = buildProps({
height: {
@@ -10,10 +11,7 @@ export const scrollbarProps = buildProps({
type: [String, Number],
default: '',
},
native: {
type: Boolean,
default: false,
},
native: Boolean,
wrapStyle: {
type: definePropType<StyleValue>([String, Object, Array]),
default: '',
@@ -35,16 +33,12 @@ export const scrollbarProps = buildProps({
type: String,
default: 'div',
},
always: {
type: Boolean,
default: false,
},
always: Boolean,
minSize: {
type: Number,
default: 20,
},
} as const)
export type ScrollbarProps = ExtractPropTypes<typeof scrollbarProps>
export const scrollbarEmits = {
@@ -57,3 +51,5 @@ export const scrollbarEmits = {
}) => isNumber(scrollTop) && isNumber(scrollLeft),
}
export type ScrollbarEmits = typeof scrollbarEmits
export type ScrollbarInstance = InstanceType<typeof Scrollbar>

View File

@@ -31,10 +31,9 @@
</template>
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import {
computed,
defineComponent,
nextTick,
onMounted,
onUpdated,
@@ -47,165 +46,152 @@ import { useEventListener, useResizeObserver } from '@vueuse/core'
import { addUnit, debugWarn, isNumber, isObject } from '@element-plus/utils'
import { scrollbarContextKey } from '@element-plus/tokens'
import { useNamespace } from '@element-plus/hooks'
import { GAP } from './util'
import Bar from './bar.vue'
import { scrollbarEmits, scrollbarProps } from './scrollbar'
import type { BarInstance } from './bar'
import type { CSSProperties, StyleValue } from 'vue'
export default defineComponent({
defineOptions({
name: 'ElScrollbar',
components: {
Bar,
},
props: scrollbarProps,
emits: scrollbarEmits,
})
setup(props, { emit }) {
const ns = useNamespace('scrollbar')
const props = defineProps(scrollbarProps)
const emit = defineEmits(scrollbarEmits)
let stopResizeObserver: (() => void) | undefined = undefined
let stopResizeListener: (() => void) | undefined = undefined
const ns = useNamespace('scrollbar')
const scrollbar$ = ref<HTMLDivElement>()
const wrap$ = ref<HTMLDivElement>()
const resize$ = ref<HTMLElement>()
let stopResizeObserver: (() => void) | undefined = undefined
let stopResizeListener: (() => void) | undefined = undefined
const sizeWidth = ref('0')
const sizeHeight = ref('0')
const barRef = ref()
const moveX = ref(0)
const moveY = ref(0)
const ratioY = ref(1)
const ratioX = ref(1)
const SCOPE = 'ElScrollbar'
const GAP = 4 // top 2 + bottom 2 of bar instance
const scrollbar$ = ref<HTMLDivElement>()
const wrap$ = ref<HTMLDivElement>()
const resize$ = ref<HTMLElement>()
const style = computed<StyleValue>(() => {
const style: CSSProperties = {}
if (props.height) style.height = addUnit(props.height)
if (props.maxHeight) style.maxHeight = addUnit(props.maxHeight)
return [props.wrapStyle, style]
const sizeWidth = ref('0')
const sizeHeight = ref('0')
const barRef = ref<BarInstance>()
const ratioY = ref(1)
const ratioX = ref(1)
const SCOPE = 'ElScrollbar'
const style = computed<StyleValue>(() => {
const style: CSSProperties = {}
if (props.height) style.height = addUnit(props.height)
if (props.maxHeight) style.maxHeight = addUnit(props.maxHeight)
return [props.wrapStyle, style]
})
const handleScroll = () => {
if (wrap$.value) {
barRef.value?.handleScroll(wrap$.value)
emit('scroll', {
scrollTop: wrap$.value.scrollTop,
scrollLeft: wrap$.value.scrollLeft,
})
}
}
const handleScroll = () => {
if (wrap$.value) {
barRef.value?.handleScroll(wrap$.value)
function scrollTo(xCord: number, yCord?: number): void
function scrollTo(options: ScrollToOptions): void
function scrollTo(arg1: unknown, arg2?: number) {
if (isObject(arg1)) {
wrap$.value!.scrollTo(arg1)
} else if (isNumber(arg1) && isNumber(arg2)) {
wrap$.value!.scrollTo(arg1, arg2)
}
}
emit('scroll', {
scrollTop: wrap$.value.scrollTop,
scrollLeft: wrap$.value.scrollLeft,
})
}
const setScrollTop = (value: number) => {
if (!isNumber(value)) {
debugWarn(SCOPE, 'value must be a number')
return
}
wrap$.value!.scrollTop = value
}
const setScrollLeft = (value: number) => {
if (!isNumber(value)) {
debugWarn(SCOPE, 'value must be a number')
return
}
wrap$.value!.scrollLeft = value
}
const update = () => {
if (!wrap$.value) return
const offsetHeight = wrap$.value.offsetHeight - GAP
const offsetWidth = wrap$.value.offsetWidth - GAP
const originalHeight = offsetHeight ** 2 / wrap$.value.scrollHeight
const originalWidth = offsetWidth ** 2 / wrap$.value.scrollWidth
const height = Math.max(originalHeight, props.minSize)
const width = Math.max(originalWidth, props.minSize)
ratioY.value =
originalHeight /
(offsetHeight - originalHeight) /
(height / (offsetHeight - height))
ratioX.value =
originalWidth /
(offsetWidth - originalWidth) /
(width / (offsetWidth - width))
sizeHeight.value = height + GAP < offsetHeight ? `${height}px` : ''
sizeWidth.value = width + GAP < offsetWidth ? `${width}px` : ''
}
watch(
() => props.noresize,
(noresize) => {
if (noresize) {
stopResizeObserver?.()
stopResizeListener?.()
} else {
;({ stop: stopResizeObserver } = useResizeObserver(resize$, update))
stopResizeListener = useEventListener('resize', update)
}
},
{ immediate: true }
)
function scrollTo(xCord: number, yCord?: number): void
function scrollTo(options: ScrollToOptions): void
function scrollTo(arg1: unknown, arg2?: number) {
if (isObject(arg1)) {
wrap$.value!.scrollTo(arg1)
} else if (isNumber(arg1) && isNumber(arg2)) {
wrap$.value!.scrollTo(arg1, arg2)
}
}
const setScrollTop = (value: number) => {
if (!isNumber(value)) {
debugWarn(SCOPE, 'value must be a number')
return
}
wrap$.value!.scrollTop = value
}
const setScrollLeft = (value: number) => {
if (!isNumber(value)) {
debugWarn(SCOPE, 'value must be a number')
return
}
wrap$.value!.scrollLeft = value
}
const update = () => {
if (!wrap$.value) return
const offsetHeight = wrap$.value.offsetHeight - GAP
const offsetWidth = wrap$.value.offsetWidth - GAP
const originalHeight = offsetHeight ** 2 / wrap$.value.scrollHeight
const originalWidth = offsetWidth ** 2 / wrap$.value.scrollWidth
const height = Math.max(originalHeight, props.minSize)
const width = Math.max(originalWidth, props.minSize)
ratioY.value =
originalHeight /
(offsetHeight - originalHeight) /
(height / (offsetHeight - height))
ratioX.value =
originalWidth /
(offsetWidth - originalWidth) /
(width / (offsetWidth - width))
sizeHeight.value = height + GAP < offsetHeight ? `${height}px` : ''
sizeWidth.value = width + GAP < offsetWidth ? `${width}px` : ''
}
watch(
() => props.noresize,
(noresize) => {
if (noresize) {
stopResizeObserver?.()
stopResizeListener?.()
} else {
;({ stop: stopResizeObserver } = useResizeObserver(resize$, update))
stopResizeListener = useEventListener('resize', update)
watch(
() => [props.maxHeight, props.height],
() => {
if (!props.native)
nextTick(() => {
update()
if (wrap$.value) {
barRef.value?.handleScroll(wrap$.value)
}
},
{ immediate: true }
)
watch(
() => [props.maxHeight, props.height],
() => {
if (!props.native)
nextTick(() => {
update()
if (wrap$.value) {
barRef.value?.handleScroll(wrap$.value)
}
})
}
)
provide(
scrollbarContextKey,
reactive({
scrollbarElement: scrollbar$,
wrapElement: wrap$,
})
)
}
)
onMounted(() => {
if (!props.native) nextTick(() => update())
})
onUpdated(() => update())
provide(
scrollbarContextKey,
reactive({
scrollbarElement: scrollbar$,
wrapElement: wrap$,
})
)
return {
ns,
scrollbar$,
wrap$,
resize$,
barRef,
moveX,
moveY,
ratioX,
ratioY,
sizeWidth,
sizeHeight,
style,
update,
handleScroll,
scrollTo,
setScrollTop,
setScrollLeft,
}
},
onMounted(() => {
if (!props.native) nextTick(() => update())
})
onUpdated(() => update())
defineExpose({
/** @description update scrollbar state manually */
update,
/** @description scrolls to a particular set of coordinates */
scrollTo,
/** @description set distance to scroll top */
setScrollTop,
/** @description set distance to scroll left */
setScrollLeft,
/** @description handle scroll event */
handleScroll,
})
</script>

View File

@@ -1,5 +1,6 @@
import { buildProps } from '@element-plus/utils'
import type { ExtractPropTypes } from 'vue'
import type Thumb from './thumb.vue'
export const thumbProps = buildProps({
vertical: Boolean,
@@ -12,3 +13,5 @@ export const thumbProps = buildProps({
always: Boolean,
} as const)
export type ThumbProps = ExtractPropTypes<typeof thumbProps>
export type ThumbInstance = InstanceType<typeof Thumb>

View File

@@ -16,183 +16,154 @@
</transition>
</template>
<script lang="ts">
import {
computed,
defineComponent,
inject,
onBeforeUnmount,
ref,
toRef,
} from 'vue'
<script lang="ts" setup>
import { computed, inject, onBeforeUnmount, ref, toRef } from 'vue'
import { isClient, useEventListener } from '@vueuse/core'
import { scrollbarContextKey } from '@element-plus/tokens'
import { throwError } from '@element-plus/utils'
import { useNamespace } from '@element-plus/hooks'
import { BAR_MAP, renderThumbStyle } from './util'
import { thumbProps } from './thumb'
const COMPONENT_NAME = 'Thumb'
export default defineComponent({
name: COMPONENT_NAME,
props: thumbProps,
const props = defineProps(thumbProps)
setup(props) {
const scrollbar = inject(scrollbarContextKey)
const ns = useNamespace('scrollbar')
const scrollbar = inject(scrollbarContextKey)
const ns = useNamespace('scrollbar')
if (!scrollbar)
throwError(COMPONENT_NAME, 'can not inject scrollbar context')
if (!scrollbar) throwError(COMPONENT_NAME, 'can not inject scrollbar context')
const instance = ref<HTMLDivElement>()
const thumb = ref<HTMLDivElement>()
const instance = ref<HTMLDivElement>()
const thumb = ref<HTMLDivElement>()
const thumbState = ref({})
const visible = ref(false)
const thumbState = ref<Partial<Record<'X' | 'Y', number>>>({})
const visible = ref(false)
let cursorDown = false
let cursorLeave = false
let originalOnSelectStart:
| ((this: GlobalEventHandlers, ev: Event) => any)
| null = isClient ? document.onselectstart : null
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 bar = computed(() => BAR_MAP[props.vertical ? 'vertical' : 'horizontal'])
const thumbStyle = computed(() =>
renderThumbStyle({
size: props.size,
move: props.move,
bar: bar.value,
})
)
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 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
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)
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 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 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]
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
}
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 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 mouseMoveDocumentHandler = (e: MouseEvent) => {
if (!instance.value || !thumb.value) return
if (cursorDown === false) return
const prevPage = thumbState.value[bar.value.axis]
if (!prevPage) 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 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 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 mouseMoveScrollbarHandler = () => {
cursorLeave = false
visible.value = !!props.size
}
const mouseLeaveScrollbarHandler = () => {
cursorLeave = true
visible.value = cursorDown
}
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 {
ns,
instance,
thumb,
bar,
thumbStyle,
visible,
clickTrackHandler,
clickThumbHandler,
}
},
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
)
</script>

View File

@@ -1,4 +1,7 @@
import type { CSSProperties } from 'vue'
import type { ThumbProps } from './thumb'
export const GAP = 4 // top 2 + bottom 2 of bar instance
export const BAR_MAP = {
vertical: {
@@ -23,7 +26,13 @@ export const BAR_MAP = {
},
} as const
export const renderThumbStyle = ({ move, size, bar }): CSSProperties => ({
export const renderThumbStyle = ({
move,
size,
bar,
}: Pick<ThumbProps, 'move' | 'size'> & {
bar: typeof BAR_MAP[keyof typeof BAR_MAP]
}): CSSProperties => ({
[bar.size]: size,
transform: `translate${bar.axis}(${move}%)`,
})

View File

@@ -1,10 +1,10 @@
import type { InjectionKey } from 'vue'
export interface scrollbarContext {
export interface ScrollbarContext {
scrollbarElement: HTMLDivElement
wrapElement: HTMLDivElement
}
export const scrollbarContextKey: InjectionKey<scrollbarContext> = Symbol(
export const scrollbarContextKey: InjectionKey<ScrollbarContext> = Symbol(
'scrollbarContextKey'
)