mirror of
https://github.com/element-plus/element-plus.git
synced 2026-03-13 07:51:17 +08:00
refactor(components): refactor backtop (#3486)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { nextTick } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Backtop from '../src/index.vue'
|
||||
import Backtop from '../src/backtop.vue'
|
||||
|
||||
const _mount = (template: string) =>
|
||||
mount(
|
||||
@@ -21,6 +22,8 @@ describe('Backtop.vue', () => {
|
||||
</div>
|
||||
</div>
|
||||
`)
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.el-backtop').exists()).toBe(false)
|
||||
wrapper.element.scrollTop = 2000
|
||||
await wrapper.trigger('scroll')
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import Backtop from './src/index.vue'
|
||||
import type { App } from 'vue'
|
||||
import { withInstall } from '@element-plus/utils/with-install'
|
||||
|
||||
import type { SFCWithInstall } from '@element-plus/utils/types'
|
||||
import Backtop from './src/backtop.vue'
|
||||
|
||||
Backtop.install = (app: App): void => {
|
||||
app.component(Backtop.name, Backtop)
|
||||
}
|
||||
export const ElBacktop = withInstall(Backtop)
|
||||
export default ElBacktop
|
||||
|
||||
const _Backtop = Backtop as SFCWithInstall<typeof Backtop>
|
||||
|
||||
export default _Backtop
|
||||
export const ElBacktop = _Backtop
|
||||
export * from './src/backtop'
|
||||
|
||||
26
packages/components/backtop/src/backtop.ts
Normal file
26
packages/components/backtop/src/backtop.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { ExtractPropTypes } from 'vue'
|
||||
|
||||
export const backtopProps = {
|
||||
visibilityHeight: {
|
||||
type: Number,
|
||||
default: 200,
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
right: {
|
||||
type: Number,
|
||||
default: 40,
|
||||
},
|
||||
bottom: {
|
||||
type: Number,
|
||||
default: 40,
|
||||
},
|
||||
} as const
|
||||
export type BacktopProps = ExtractPropTypes<typeof backtopProps>
|
||||
|
||||
export const backtopEmits = {
|
||||
click: (evt: MouseEvent) => evt instanceof MouseEvent,
|
||||
}
|
||||
export type BacktopEmits = typeof backtopEmits
|
||||
89
packages/components/backtop/src/backtop.vue
Normal file
89
packages/components/backtop/src/backtop.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<transition name="el-fade-in">
|
||||
<div
|
||||
v-if="visible"
|
||||
:style="{
|
||||
right: styleRight,
|
||||
bottom: styleBottom,
|
||||
}"
|
||||
class="el-backtop"
|
||||
@click.stop="handleClick"
|
||||
>
|
||||
<slot>
|
||||
<i class="el-icon-caret-top"></i>
|
||||
</slot>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, onMounted, shallowRef } from 'vue'
|
||||
import { useEventListener, useThrottleFn } from '@vueuse/core'
|
||||
import { easeInOutCubic } from '@element-plus/utils/animation'
|
||||
import { throwError } from '@element-plus/utils/error'
|
||||
|
||||
import { backtopEmits, backtopProps } from './backtop'
|
||||
|
||||
const COMPONENT_NAME = 'ElBacktop'
|
||||
|
||||
export default defineComponent({
|
||||
name: COMPONENT_NAME,
|
||||
|
||||
props: backtopProps,
|
||||
emits: backtopEmits,
|
||||
|
||||
setup(props, { emit }) {
|
||||
const el = shallowRef<HTMLElement | undefined>(document.documentElement)
|
||||
const container = shallowRef<Document | HTMLElement>(document)
|
||||
const visible = ref(false)
|
||||
const styleBottom = computed(() => `${props.bottom}px`)
|
||||
const styleRight = computed(() => `${props.right}px`)
|
||||
|
||||
const scrollToTop = () => {
|
||||
if (!el.value) return
|
||||
const beginTime = Date.now()
|
||||
const beginValue = el.value.scrollTop
|
||||
const frameFunc = () => {
|
||||
if (!el.value) return
|
||||
const progress = (Date.now() - beginTime) / 500
|
||||
if (progress < 1) {
|
||||
el.value.scrollTop = beginValue * (1 - easeInOutCubic(progress))
|
||||
requestAnimationFrame(frameFunc)
|
||||
} else {
|
||||
el.value.scrollTop = 0
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(frameFunc)
|
||||
}
|
||||
const handleScroll = () => {
|
||||
if (el.value) visible.value = el.value.scrollTop >= props.visibilityHeight
|
||||
}
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
scrollToTop()
|
||||
emit('click', event)
|
||||
}
|
||||
|
||||
const handleScrollThrottled = useThrottleFn(handleScroll, 300)
|
||||
|
||||
onMounted(() => {
|
||||
if (props.target) {
|
||||
el.value =
|
||||
document.querySelector<HTMLElement>(props.target) ?? undefined
|
||||
if (!el.value) {
|
||||
throwError(COMPONENT_NAME, `target is not existed: ${props.target}`)
|
||||
}
|
||||
container.value = el.value
|
||||
}
|
||||
|
||||
useEventListener(container, 'scroll', handleScrollThrottled)
|
||||
})
|
||||
|
||||
return {
|
||||
visible,
|
||||
styleBottom,
|
||||
styleRight,
|
||||
handleClick,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
@@ -1,114 +0,0 @@
|
||||
<template>
|
||||
<transition name="el-fade-in">
|
||||
<div
|
||||
v-if="visible"
|
||||
:style="{
|
||||
right: styleRight,
|
||||
bottom: styleBottom,
|
||||
}"
|
||||
class="el-backtop"
|
||||
@click.stop="handleClick"
|
||||
>
|
||||
<slot>
|
||||
<i class="el-icon-caret-top"></i>
|
||||
</slot>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import throttle from 'lodash/throttle'
|
||||
import { on, off } from '@element-plus/utils/dom'
|
||||
import { easeInOutCubic } from '@element-plus/utils/animation'
|
||||
import { throwError } from '@element-plus/utils/error'
|
||||
|
||||
interface IElBacktopProps {
|
||||
visibilityHeight: number
|
||||
target: string
|
||||
right: number
|
||||
bottom: number
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ElBacktop',
|
||||
props: {
|
||||
visibilityHeight: {
|
||||
type: Number,
|
||||
default: 200,
|
||||
},
|
||||
target: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
right: {
|
||||
type: Number,
|
||||
default: 40,
|
||||
},
|
||||
bottom: {
|
||||
type: Number,
|
||||
default: 40,
|
||||
},
|
||||
},
|
||||
emits: ['click'],
|
||||
setup(props: IElBacktopProps, ctx) {
|
||||
const el = ref(null)
|
||||
const container = ref(null)
|
||||
const visible = ref(false)
|
||||
const styleBottom = computed(() => `${props.bottom}px`)
|
||||
const styleRight = computed(() => `${props.right}px`)
|
||||
const scope = 'ElBackTop'
|
||||
|
||||
const scrollToTop = () => {
|
||||
const beginTime = Date.now()
|
||||
const beginValue = el.value.scrollTop
|
||||
const rAF =
|
||||
window.requestAnimationFrame || ((func) => setTimeout(func, 16))
|
||||
const frameFunc = () => {
|
||||
const progress = (Date.now() - beginTime) / 500
|
||||
if (progress < 1) {
|
||||
el.value.scrollTop = beginValue * (1 - easeInOutCubic(progress))
|
||||
rAF(frameFunc)
|
||||
} else {
|
||||
el.value.scrollTop = 0
|
||||
}
|
||||
}
|
||||
rAF(frameFunc)
|
||||
}
|
||||
const onScroll = () => {
|
||||
visible.value = el.value.scrollTop >= props.visibilityHeight
|
||||
}
|
||||
const handleClick = (event) => {
|
||||
scrollToTop()
|
||||
ctx.emit('click', event)
|
||||
}
|
||||
|
||||
const throttledScrollHandler = throttle(onScroll, 300)
|
||||
|
||||
onMounted(() => {
|
||||
container.value = document
|
||||
el.value = document.documentElement
|
||||
if (props.target) {
|
||||
el.value = document.querySelector(props.target)
|
||||
if (!el.value) {
|
||||
throwError(scope, `target is not existed: ${props.target}`)
|
||||
}
|
||||
container.value = el.value
|
||||
}
|
||||
on(container.value, 'scroll', throttledScrollHandler)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
off(container.value, 'scroll', throttledScrollHandler)
|
||||
})
|
||||
|
||||
return {
|
||||
el,
|
||||
container,
|
||||
visible,
|
||||
styleBottom,
|
||||
styleRight,
|
||||
handleClick,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user