refactor(back-top): ts

This commit is contained in:
07akioni 2021-01-17 11:03:58 +08:00
parent 74db9fbf71
commit e6568da5e4
13 changed files with 363 additions and 312 deletions

View File

@ -1,2 +0,0 @@
/* istanbul ignore file */
export { default as NBackTop } from './src/BackTop.vue'

2
src/back-top/index.ts Normal file
View File

@ -0,0 +1,2 @@
/* istanbul ignore file */
export { default as NBackTop } from './src/BackTop'

View File

@ -0,0 +1,293 @@
import {
h,
ref,
computed,
toRef,
watch,
nextTick,
defineComponent,
renderSlot,
mergeProps,
Transition,
VNode,
PropType,
onMounted,
onBeforeUnmount
} from 'vue'
import { VLazyTeleport } from 'vueuc'
import { useIsMounted, useMergedState } from 'vooks'
import { getScrollParent, unwrapElement } from 'seemly'
import { useTheme } from '../../_mixins'
import type { ThemeProps } from '../../_mixins'
import { NBaseIcon } from '../../_base'
import { formatLength, warn } from '../../_utils'
import { backTopLight } from '../styles'
import type { BackTopTheme } from '../styles'
import BackTopIcon from './BackTopIcon'
import style from './styles/index.cssr'
export default defineComponent({
name: 'BackTop',
inheritAttrs: false,
props: {
...(useTheme.props as ThemeProps<BackTopTheme>),
show: {
type: Boolean,
default: undefined
},
right: {
type: [Number, String],
default: 40
},
bottom: {
type: [Number, String],
default: 40
},
to: {
type: [String, Object],
default: 'body'
},
visibilityHeight: {
type: Number,
default: 180
},
listenTo: {
type: [String, Object, Function],
default: undefined
},
// eslint-disable-next-line vue/prop-name-casing
'onUpdate:show': {
type: Function,
default: () => {}
},
// deprecated
target: {
type: Function as PropType<() => HTMLElement | undefined>,
validator: () => {
warn(
'back-top',
'`target` is deprecated, please use `listen-to` instead.'
)
return true
},
default: undefined
},
onShow: {
type: Function as PropType<(() => void) | undefined>,
default: undefined
},
onHide: {
type: Function as PropType<(() => void) | undefined>,
default: undefined
}
},
setup (props) {
const scrollTopRef = ref<number | null>(null)
const uncontrolledShowRef = computed(() => {
if (scrollTopRef.value === null) return false
return scrollTopRef.value >= props.visibilityHeight
})
const DomInfoReadyRef = ref(false)
watch(uncontrolledShowRef, (value) => {
if (DomInfoReadyRef.value) {
props['onUpdate:show'](value)
}
})
const controlledShowRef = toRef(props, 'show')
const mergedShowRef = useMergedState(controlledShowRef, uncontrolledShowRef)
const transitionDisabledRef = ref(true)
const placeholderRef = ref<HTMLElement | null>(null)
const styleRef = computed((): {
right: string
bottom: string
} => {
return {
right: formatLength(props.right),
bottom: formatLength(props.bottom)
}
})
let scrollElement: HTMLElement
let scrollListenerRegistered: boolean
// deprecated
watch(mergedShowRef, (value) => {
if (DomInfoReadyRef.value) {
if (value) {
props.onShow?.()
}
props.onHide?.()
}
})
const themeRef = useTheme('BackTop', 'BackTop', style, backTopLight, props)
function init (): void {
if (scrollListenerRegistered) return
scrollListenerRegistered = true
const scrollEl =
props.target?.() ||
unwrapElement(props.listenTo) ||
getScrollParent(placeholderRef.value)
if (!scrollEl) {
if (__DEV__) {
warn(
'back-top',
'Container of back-top element is not found. This could be a bug of naive-ui.'
)
}
return
}
scrollElement = scrollEl
const { to } = props
const target = typeof to === 'string' ? document.querySelector(to) : to
if (__DEV__ && !target) {
warn('back-top', 'Target is not found.')
}
if (scrollEl) {
scrollEl.addEventListener('scroll', handleScroll)
handleScroll()
}
}
function handleClick (e: MouseEvent): void {
if (scrollElement.nodeName === '#document') {
;((scrollElement as unknown) as Document).documentElement.scrollTo({
top: 0,
behavior: 'smooth'
})
} else {
scrollElement.scrollTo({
top: 0,
behavior: 'smooth'
})
}
}
function handleScroll (): void {
if (scrollElement.nodeName === '#document') {
scrollTopRef.value = ((scrollElement as unknown) as Document).documentElement.scrollTop
} else {
scrollTopRef.value = scrollElement.scrollTop
}
if (!DomInfoReadyRef.value) {
void nextTick(() => {
DomInfoReadyRef.value = true
})
}
}
function handleAfterEnter (): void {
transitionDisabledRef.value = false
}
onMounted(() => {
init()
transitionDisabledRef.value = mergedShowRef.value
})
onBeforeUnmount(() => {
if (scrollElement) {
scrollElement.removeEventListener('scroll', handleScroll)
}
})
return {
placeholderRef,
style: styleRef,
mergedShow: mergedShowRef,
isMounted: useIsMounted(),
scrollElement: ref(null),
scrollTop: scrollTopRef,
DomInfoReady: DomInfoReadyRef,
transitionDisabled: transitionDisabledRef,
handleAfterEnter,
handleScroll,
handleClick,
cssVars: computed(() => {
const {
self: {
color,
boxShadow,
boxShadowHover,
boxShadowPressed,
iconColor,
iconColorHover,
iconColorPressed,
width,
height,
iconSize,
borderRadius,
textColor
},
common: { cubicBezierEaseInOut }
} = themeRef.value
return {
'--bezier': cubicBezierEaseInOut,
'--border-radius': borderRadius,
'--height': height,
'--width': width,
'--box-shadow': boxShadow,
'--box-shadow-hover': boxShadowHover,
'--box-shadow-pressed': boxShadowPressed,
'--color': color,
'--icon-size': iconSize,
'--icon-color': iconColor,
'--icon-color-hover': iconColorHover,
'--icon-color-pressed': iconColorPressed,
'--text-color': textColor
}
})
}
},
render () {
return (
<div
ref="placeholderRef"
class="n-back-top-placeholder"
style="display: none"
aria-hidden
>
<VLazyTeleport to={this.to} show={this.mergedShow}>
{{
default: () => (
<Transition
name="n-fade-in-scale-up-transition"
appear={this.isMounted}
onAfterEnter={this.handleAfterEnter}
>
{{
default: () =>
this.mergedShow
? h(
'div',
mergeProps(this.$attrs, {
class: [
'n-back-top',
{
'n-back-top--transition-disabled': this
.transitionDisabled
}
],
style: {
...this.style,
...this.cssVars
},
onClick: this.handleClick
}),
[
renderSlot(
this.$slots,
'default',
undefined,
() => [
(
<NBaseIcon>
{{ default: () => BackTopIcon }}
</NBaseIcon>
) as VNode
]
)
]
)
: null
}}
</Transition>
)
}}
</VLazyTeleport>
</div>
)
}
})

View File

@ -1,251 +0,0 @@
<template>
<div
ref="placeholder"
class="n-back-top-placeholder"
style="display: none"
aria-hidden
>
<!-- placeholder exists to find scroll parent, later we may use vue's placeholder -->
<v-lazy-teleport :to="to" :show="mergedShow">
<transition
name="n-fade-in-scale-up-transition"
:appear="isMounted"
@after-enter="handleAfterEnter"
>
<div
v-if="mergedShow"
v-bind="$attrs"
class="n-back-top"
:class="{
'n-back-top--transition-disabled': transitionDisabled
}"
:style="{
right: styleRight,
bottom: styleBottom,
...cssVars
}"
@click="handleClick"
>
<slot>
<n-base-icon>
<back-top-icon />
</n-base-icon>
</slot>
</div>
</transition>
</v-lazy-teleport>
</div>
</template>
<script>
import { ref, computed, toRef, watch, nextTick, defineComponent } from 'vue'
import { VLazyTeleport } from 'vueuc'
import { useIsMounted, useMergedState } from 'vooks'
import { getScrollParent, unwrapElement } from 'seemly'
import { useTheme } from '../../_mixins'
import { NBaseIcon } from '../../_base'
import { formatLength, warn } from '../../_utils'
import { backTopLight } from '../styles'
import BackTopIcon from './BackTopIcon.vue'
import style from './styles/index.cssr.js'
export default defineComponent({
name: 'BackTop',
components: {
VLazyTeleport,
BackTopIcon,
NBaseIcon
},
inheritAttrs: false,
props: {
...useTheme.props,
show: {
type: Boolean,
default: undefined
},
right: {
type: [Number, String],
default: 40
},
bottom: {
type: [Number, String],
default: 40
},
to: {
type: [String, Object],
default: 'body'
},
visibilityHeight: {
type: Number,
default: 180
},
listenTo: {
type: [String, Object, Function],
default: undefined
},
// eslint-disable-next-line vue/prop-name-casing
'onUpdate:show': {
type: Function,
default: () => {}
},
// deprecated
target: {
validator () {
warn(
'back-top',
'`target` is deprecated, please use `listen-to` instead.'
)
return true
},
default: undefined
},
onShow: {
type: Function,
default: () => {}
},
onHide: {
type: Function,
default: () => {}
}
},
setup (props) {
const scrollTopRef = ref(null)
const uncontrolledShowRef = computed(() => {
if (scrollTopRef.value === null) return false
return scrollTopRef.value >= props.visibilityHeight
})
const DomInfoReadyRef = ref(false)
watch(uncontrolledShowRef, (value) => {
if (DomInfoReadyRef.value) {
props['onUpdate:show'](value)
}
})
const controlledShowRef = toRef(props, 'show')
const mergedShowRef = useMergedState(controlledShowRef, uncontrolledShowRef)
// deprecated
watch(mergedShowRef, (value) => {
if (DomInfoReadyRef.value) {
if (value) props.onShow()
props.onHide()
}
})
const themeRef = useTheme('BackTop', 'BackTop', style, backTopLight, props)
return {
mergedShow: mergedShowRef,
isMounted: useIsMounted(),
scrollElement: ref(null),
scrollTop: scrollTopRef,
transitionDisabled: ref(true),
scrollListenerRegistered: ref(false),
DomInfoReady: DomInfoReadyRef,
cssVars: computed(() => {
const {
self: {
color,
boxShadow,
boxShadowHover,
boxShadowPressed,
iconColor,
iconColorHover,
iconColorPressed,
width,
height,
iconSize,
borderRadius,
textColor
},
common: { cubicBezierEaseInOut }
} = themeRef.value
return {
'--bezier': cubicBezierEaseInOut,
'--border-radius': borderRadius,
'--height': height,
'--width': width,
'--box-shadow': boxShadow,
'--box-shadow-hover': boxShadowHover,
'--box-shadow-pressed': boxShadowPressed,
'--color': color,
'--icon-size': iconSize,
'--icon-color': iconColor,
'--icon-color-hover': iconColorHover,
'--icon-color-pressed': iconColorPressed,
'--text-color': textColor
}
})
}
},
computed: {
styleRight () {
return formatLength(this.right)
},
styleBottom () {
return formatLength(this.bottom)
}
},
mounted () {
this.init()
this.transitionDisabled = this.mergedShow
},
beforeUnmount () {
if (this.scrollElement) {
this.scrollElement.removeEventListener('scroll', this.handleScroll)
}
},
methods: {
init () {
if (this.scrollListenerRegistered) return
this.scrollListenerRegistered = true
const scrollElement =
(this.target && this.target()) ||
unwrapElement(this.listenTo) ||
getScrollParent(this.$refs.placeholder)
if (__DEV__ && !scrollElement) {
warn(
'back-top',
'Container of back-top element is not found. This could be a bug of naive-ui.'
)
}
this.scrollElement = scrollElement
const { to } = this
const target = typeof to === 'string' ? document.querySelector(to) : to
if (__DEV__ && !target) {
warn('back-top', 'Target is not found.')
}
if (scrollElement) {
scrollElement.addEventListener('scroll', this.handleScroll)
this.handleScroll()
}
},
handleClick (e) {
const { scrollElement } = this
if (scrollElement.nodeName === '#document') {
scrollElement.documentElement.scrollTo({
top: 0,
behavior: 'smooth'
})
} else {
scrollElement.scrollTo({
top: 0,
behavior: 'smooth'
})
}
},
handleScroll () {
const { scrollElement } = this
if (scrollElement.nodeName === '#document') {
this.scrollTop = scrollElement.documentElement.scrollTop
} else {
this.scrollTop = scrollElement.scrollTop
}
if (!this.DomInfoReady) {
nextTick(() => {
this.DomInfoReady = true
})
}
},
handleAfterEnter () {
this.transitionDisabled = false
}
}
})
</script>

View File

@ -0,0 +1,25 @@
import { h } from 'vue'
export default (
<svg
viewBox="0 0 24 24"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xlinkHref="http://www.w3.org/1999/xlink"
>
<g stroke="none" stroke-width="1" fill-rule="evenodd">
<g transform="translate(-139.000000, -4423.000000)" fill-rule="nonzero">
<g transform="translate(120.000000, 4285.000000)">
<g transform="translate(7.000000, 126.000000)">
<g transform="translate(24.000000, 24.000000) scale(1, -1) translate(-24.000000, -24.000000) translate(12.000000, 12.000000)">
<g transform="translate(4.000000, 2.000000)">
<path d="M8,0 C8.51283584,0 8.93550716,0.38604019 8.99327227,0.883378875 L9,1 L9,10.584 L12.2928932,7.29289322 C12.6834175,6.90236893 13.3165825,6.90236893 13.7071068,7.29289322 C14.0675907,7.65337718 14.0953203,8.22060824 13.7902954,8.61289944 L13.7071068,8.70710678 L8.70710678,13.7071068 L8.62544899,13.7803112 L8.618,13.784 L8.59530661,13.8036654 L8.4840621,13.8753288 L8.37133602,13.9287745 L8.22929083,13.9735893 L8.14346259,13.9897165 L8.03324678,13.9994506 L7.9137692,13.9962979 L7.77070917,13.9735893 L7.6583843,13.9401293 L7.57677845,13.9063266 L7.47929125,13.8540045 L7.4048407,13.8036865 L7.38131006,13.7856883 C7.35030318,13.7612383 7.32077858,13.7349921 7.29289322,13.7071068 L2.29289322,8.70710678 L2.20970461,8.61289944 C1.90467972,8.22060824 1.93240926,7.65337718 2.29289322,7.29289322 C2.65337718,6.93240926 3.22060824,6.90467972 3.61289944,7.20970461 L3.70710678,7.29289322 L7,10.585 L7,1 L7.00672773,0.883378875 C7.06449284,0.38604019 7.48716416,0 8,0 Z" />
<path d="M14.9333333,15.9994506 C15.5224371,15.9994506 16,16.4471659 16,16.9994506 C16,17.5122865 15.5882238,17.9349578 15.0577292,17.9927229 L14.9333333,17.9994506 L1.06666667,17.9994506 C0.477562934,17.9994506 0,17.5517354 0,16.9994506 C0,16.4866148 0.411776203,16.0639435 0.9422708,16.0061783 L1.06666667,15.9994506 L14.9333333,15.9994506 Z" />
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
)

View File

@ -1,29 +0,0 @@
<template>
<svg
viewBox="0 0 24 24"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<g stroke="none" stroke-width="1" fill-rule="evenodd">
<g transform="translate(-139.000000, -4423.000000)" fill-rule="nonzero">
<g transform="translate(120.000000, 4285.000000)">
<g transform="translate(7.000000, 126.000000)">
<g
transform="translate(24.000000, 24.000000) scale(1, -1) translate(-24.000000, -24.000000) translate(12.000000, 12.000000)"
>
<g transform="translate(4.000000, 2.000000)">
<path
d="M8,0 C8.51283584,0 8.93550716,0.38604019 8.99327227,0.883378875 L9,1 L9,10.584 L12.2928932,7.29289322 C12.6834175,6.90236893 13.3165825,6.90236893 13.7071068,7.29289322 C14.0675907,7.65337718 14.0953203,8.22060824 13.7902954,8.61289944 L13.7071068,8.70710678 L8.70710678,13.7071068 L8.62544899,13.7803112 L8.618,13.784 L8.59530661,13.8036654 L8.4840621,13.8753288 L8.37133602,13.9287745 L8.22929083,13.9735893 L8.14346259,13.9897165 L8.03324678,13.9994506 L7.9137692,13.9962979 L7.77070917,13.9735893 L7.6583843,13.9401293 L7.57677845,13.9063266 L7.47929125,13.8540045 L7.4048407,13.8036865 L7.38131006,13.7856883 C7.35030318,13.7612383 7.32077858,13.7349921 7.29289322,13.7071068 L2.29289322,8.70710678 L2.20970461,8.61289944 C1.90467972,8.22060824 1.93240926,7.65337718 2.29289322,7.29289322 C2.65337718,6.93240926 3.22060824,6.90467972 3.61289944,7.20970461 L3.70710678,7.29289322 L7,10.585 L7,1 L7.00672773,0.883378875 C7.06449284,0.38604019 7.48716416,0 8,0 Z"
/>
<path
d="M14.9333333,15.9994506 C15.5224371,15.9994506 16,16.4471659 16,16.9994506 C16,17.5122865 15.5882238,17.9349578 15.0577292,17.9927229 L14.9333333,17.9994506 L1.06666667,17.9994506 C0.477562934,17.9994506 0,17.5517354 0,16.9994506 C0,16.4866148 0.411776203,16.0639435 0.9422708,16.0061783 L1.06666667,15.9994506 L14.9333333,15.9994506 Z"
/>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</template>

View File

@ -1,7 +1,8 @@
import commonVariables from './_common.js'
import commonVariables from './_common'
import { commonDark } from '../../_styles/new-common'
import type { BackTopTheme } from './light'
export default {
const backTopDark: BackTopTheme = {
name: 'BackTop',
common: commonDark,
self (vars) {
@ -24,3 +25,5 @@ export default {
}
}
}
export default backTopDark

View File

@ -1,2 +0,0 @@
export { default as backTopDark } from './dark.js'
export { default as backTopLight } from './light.js'

View File

@ -0,0 +1,3 @@
export { default as backTopDark } from './dark'
export { default as backTopLight } from './light'
export type { BackTopThemeVars, BackTopTheme } from './light'

View File

@ -1,26 +0,0 @@
import commonVariables from './_common.js'
import { commonLight } from '../../_styles/new-common'
export default {
name: 'BackTop',
common: commonLight,
self (vars) {
const {
popoverColor,
textColor2,
primaryColorHover,
primaryColorPressed
} = vars
return {
...commonVariables,
color: popoverColor,
textColor: textColor2,
iconColor: textColor2,
iconColorHover: primaryColorHover,
iconColorPressed: primaryColorPressed,
boxShadow: '0 2px 8px 0px rgba(0, 0, 0, .12)',
boxShadowHover: '0 2px 12px 0px rgba(0, 0, 0, .18)',
boxShadowPressed: '0 2px 12px 0px rgba(0, 0, 0, .18)'
}
}
}

View File

@ -0,0 +1,35 @@
import commonVariables from './_common'
import { commonLight } from '../../_styles/new-common'
import type { ThemeCommonVars } from '../../_styles/new-common'
import type { Theme } from '../../_mixins'
const self = (vars: ThemeCommonVars) => {
const {
popoverColor,
textColor2,
primaryColorHover,
primaryColorPressed
} = vars
return {
...commonVariables,
color: popoverColor,
textColor: textColor2,
iconColor: textColor2,
iconColorHover: primaryColorHover,
iconColorPressed: primaryColorPressed,
boxShadow: '0 2px 8px 0px rgba(0, 0, 0, .12)',
boxShadowHover: '0 2px 12px 0px rgba(0, 0, 0, .18)',
boxShadowPressed: '0 2px 12px 0px rgba(0, 0, 0, .18)'
}
}
export type BackTopThemeVars = ReturnType<typeof self>
const backTopLight: Theme<BackTopThemeVars> = {
name: 'BackTop',
common: commonLight,
self
}
export default backTopLight
export type BackTopTheme = typeof backTopLight