mirror of
https://github.com/tusen-ai/naive-ui.git
synced 2025-01-12 12:25:16 +08:00
refactor(scrollbar): ts
This commit is contained in:
parent
9e28bb7fd8
commit
9e095c1a6a
@ -1,2 +0,0 @@
|
||||
/* istanbul ignore file */
|
||||
export { default as NScrollbar } from './src/ScrollBar.vue'
|
3
src/scrollbar/index.ts
Normal file
3
src/scrollbar/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
/* istanbul ignore file */
|
||||
export { default as NScrollbar } from './src/ScrollBar'
|
||||
export type { ScrollbarRef } from './src/ScrollBar'
|
685
src/scrollbar/src/ScrollBar.tsx
Normal file
685
src/scrollbar/src/ScrollBar.tsx
Normal file
@ -0,0 +1,685 @@
|
||||
import {
|
||||
h,
|
||||
ref,
|
||||
defineComponent,
|
||||
computed,
|
||||
PropType,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
mergeProps,
|
||||
renderSlot,
|
||||
Transition,
|
||||
VNode
|
||||
} from 'vue'
|
||||
import { on, off } from 'evtd'
|
||||
import { VResizeObserver } from 'vueuc'
|
||||
import { useIsIos } from 'vooks'
|
||||
import { useTheme } from '../../_mixins'
|
||||
import type { ThemeProps } from '../../_mixins'
|
||||
import { scrollbarLight } from '../styles'
|
||||
import type { ScrollbarTheme } from '../styles'
|
||||
import style from './styles/index.cssr'
|
||||
|
||||
interface ScrollTo {
|
||||
(x: number, y: number): void
|
||||
(
|
||||
x: {
|
||||
left?: number
|
||||
top?: number
|
||||
behavior?: ScrollBehavior
|
||||
debounce?: boolean
|
||||
},
|
||||
y: number
|
||||
): void
|
||||
(
|
||||
x: {
|
||||
el: HTMLElement
|
||||
behavior?: ScrollBehavior
|
||||
debounce?: boolean
|
||||
},
|
||||
y: number
|
||||
): void
|
||||
(
|
||||
x: {
|
||||
index: number
|
||||
elSize: number
|
||||
behavior?: ScrollBehavior
|
||||
debounce?: boolean
|
||||
},
|
||||
y: number
|
||||
): void
|
||||
(
|
||||
x: {
|
||||
position: 'top' | 'bottom'
|
||||
behavior?: ScrollBehavior
|
||||
debounce?: boolean
|
||||
},
|
||||
y: number
|
||||
): void
|
||||
}
|
||||
|
||||
export interface ScrollbarRef {
|
||||
scrollTo: ScrollTo
|
||||
sync: () => void
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Scrollbar',
|
||||
props: {
|
||||
...(useTheme.props as ThemeProps<ScrollbarTheme>),
|
||||
size: {
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
scrollable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
xScrollable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
container: {
|
||||
type: Function as PropType<undefined | (() => HTMLElement)>,
|
||||
default: undefined
|
||||
},
|
||||
content: {
|
||||
type: Function as PropType<undefined | (() => HTMLElement)>,
|
||||
default: undefined
|
||||
},
|
||||
containerStyle: {
|
||||
type: Object,
|
||||
default: undefined
|
||||
},
|
||||
contentClass: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
contentStyle: {
|
||||
type: Object,
|
||||
default: undefined
|
||||
},
|
||||
horizontalRailStyle: {
|
||||
type: Object,
|
||||
default: undefined
|
||||
},
|
||||
verticalRailStyle: {
|
||||
type: Object,
|
||||
default: undefined
|
||||
},
|
||||
onScroll: {
|
||||
type: Function as PropType<((e: Event) => void) | undefined>,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
// dom ref
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const contentRef = ref<HTMLElement | null>(null)
|
||||
const yRailRef = ref<HTMLElement | null>(null)
|
||||
const xRailRef = ref<HTMLElement | null>(null)
|
||||
|
||||
// data ref
|
||||
const contentHeightRef = ref<number | null>(null)
|
||||
const contentWidthRef = ref<number | null>(null)
|
||||
const containerHeightRef = ref<number | null>(null)
|
||||
const containerWidthRef = ref<number | null>(null)
|
||||
const yRailSizeRef = ref<number | null>(null)
|
||||
const xRailSizeRef = ref<number | null>(null)
|
||||
const containerScrollTopRef = ref<number | null>(null)
|
||||
const containerScrollLeftRef = ref<number | null>(null)
|
||||
const isShowXBarRef = ref(false)
|
||||
const isShowYBarRef = ref(false)
|
||||
|
||||
let yBarPressed = false
|
||||
let xBarPressed = false
|
||||
let xBarVanishTimerId: number | undefined
|
||||
let yBarVanishTimerId: number | undefined
|
||||
let memoYTop: number = 0
|
||||
let memoXLeft: number = 0
|
||||
let memoMouseX: number = 0
|
||||
let memoMouseY: number = 0
|
||||
const isIos = useIsIos()
|
||||
|
||||
const yBarSizeRef = computed(() => {
|
||||
const { value: containerHeight } = containerHeightRef
|
||||
const { value: contentHeight } = contentHeightRef
|
||||
const { value: yRailSize } = yRailSizeRef
|
||||
if (
|
||||
containerHeight === null ||
|
||||
contentHeight === null ||
|
||||
yRailSize === null
|
||||
) {
|
||||
return 0
|
||||
} else {
|
||||
return Math.min(
|
||||
containerHeight,
|
||||
(yRailSize * containerHeight) / contentHeight + props.size * 1.5
|
||||
)
|
||||
}
|
||||
})
|
||||
const yBarSizePxRef = computed(() => {
|
||||
return `${yBarSizeRef.value}px`
|
||||
})
|
||||
const xBarSizeRef = computed(() => {
|
||||
const { value: containerWidth } = containerWidthRef
|
||||
const { value: contentWidth } = contentWidthRef
|
||||
const { value: xRailSize } = xRailSizeRef
|
||||
if (
|
||||
containerWidth === null ||
|
||||
contentWidth === null ||
|
||||
xRailSize === null
|
||||
) {
|
||||
return 0
|
||||
} else {
|
||||
return (xRailSize * containerWidth) / contentWidth + props.size * 1.5
|
||||
}
|
||||
})
|
||||
const xBarSizePxRef = computed(() => {
|
||||
return `${xBarSizeRef.value}px`
|
||||
})
|
||||
const yBarTopRef = computed(() => {
|
||||
const { value: containerHeight } = containerHeightRef
|
||||
const { value: containerScrollTop } = containerScrollTopRef
|
||||
const { value: contentHeight } = contentHeightRef
|
||||
const { value: yRailSize } = yRailSizeRef
|
||||
if (
|
||||
containerHeight === null ||
|
||||
containerScrollTop === null ||
|
||||
contentHeight === null ||
|
||||
yRailSize === null
|
||||
) {
|
||||
return 0
|
||||
} else {
|
||||
return (
|
||||
(containerScrollTop / (contentHeight - containerHeight)) *
|
||||
(yRailSize - yBarSizeRef.value)
|
||||
)
|
||||
}
|
||||
})
|
||||
const yBarTopPxRef = computed(() => {
|
||||
return `${yBarTopRef.value}px`
|
||||
})
|
||||
const xBarLeftRef = computed(() => {
|
||||
const { value: containerWidth } = containerWidthRef
|
||||
const { value: containerScrollLeft } = containerScrollLeftRef
|
||||
const { value: contentWidth } = contentWidthRef
|
||||
const { value: xRailSize } = xRailSizeRef
|
||||
if (
|
||||
containerWidth === null ||
|
||||
containerScrollLeft === null ||
|
||||
contentWidth === null ||
|
||||
xRailSize === null
|
||||
) {
|
||||
return 0
|
||||
} else {
|
||||
return (
|
||||
(containerScrollLeft / (contentWidth - containerWidth)) *
|
||||
(xRailSize - xBarSizeRef.value)
|
||||
)
|
||||
}
|
||||
})
|
||||
const xBarLeftPxRef = computed(() => {
|
||||
return `${xBarLeftRef.value}px`
|
||||
})
|
||||
const sizePxRef = computed(() => {
|
||||
return `${props.size}px`
|
||||
})
|
||||
const needYBarRef = computed(() => {
|
||||
const { value: containerHeight } = containerHeightRef
|
||||
const { value: contentHeight } = contentHeightRef
|
||||
return (
|
||||
containerHeight !== null &&
|
||||
contentHeight !== null &&
|
||||
contentHeight > containerHeight
|
||||
)
|
||||
})
|
||||
const needXBarRef = computed(() => {
|
||||
const { value: containerWidth } = containerWidthRef
|
||||
const { value: contentWidth } = contentWidthRef
|
||||
return (
|
||||
containerWidth !== null &&
|
||||
contentWidth !== null &&
|
||||
contentWidth > containerWidth
|
||||
)
|
||||
})
|
||||
const mergedContainerRef = computed(() => {
|
||||
const { container } = props
|
||||
if (container) return container()
|
||||
return containerRef.value
|
||||
})
|
||||
const mergedContentRef = computed(() => {
|
||||
const { content } = props
|
||||
if (content) return content()
|
||||
return contentRef.value
|
||||
})
|
||||
|
||||
// methods
|
||||
function handleContentResize (): void {
|
||||
sync()
|
||||
}
|
||||
interface MergedScrollOptions {
|
||||
left?: number
|
||||
top?: number
|
||||
el?: HTMLElement
|
||||
position?: 'top' | 'bottom'
|
||||
behavior?: ScrollBehavior
|
||||
debounce?: boolean
|
||||
index?: number
|
||||
elSize?: number
|
||||
}
|
||||
const scrollTo: ScrollTo = (
|
||||
options: MergedScrollOptions | number,
|
||||
y?: number
|
||||
): void => {
|
||||
if (!props.scrollable) return
|
||||
if (typeof options === 'number') {
|
||||
scrollToPosition(options, y ?? 0, 0, false, 'auto')
|
||||
return
|
||||
}
|
||||
const {
|
||||
left,
|
||||
top,
|
||||
index,
|
||||
elSize,
|
||||
position,
|
||||
behavior,
|
||||
el,
|
||||
debounce = true
|
||||
} = options
|
||||
if (left !== undefined || top !== undefined) {
|
||||
scrollToPosition(left ?? 0, top ?? 0, 0, false, behavior)
|
||||
}
|
||||
if (el !== undefined) {
|
||||
scrollToPosition(0, el.offsetTop, el.offsetHeight, debounce, behavior)
|
||||
} else if (index !== undefined && elSize !== undefined) {
|
||||
scrollToPosition(0, index * elSize, elSize, debounce, behavior)
|
||||
} else if (position === 'bottom') {
|
||||
scrollToPosition(0, Number.MAX_SAFE_INTEGER, 0, false, behavior)
|
||||
} else if (position === 'top') {
|
||||
scrollToPosition(0, 0, 0, false, behavior)
|
||||
}
|
||||
}
|
||||
function scrollToPosition (
|
||||
left: number,
|
||||
top: number,
|
||||
elSize: number,
|
||||
debounce: boolean,
|
||||
behavior?: ScrollBehavior
|
||||
): void {
|
||||
const { value: container } = mergedContainerRef
|
||||
if (!container) return
|
||||
if (debounce) {
|
||||
const { scrollTop, offsetHeight } = container
|
||||
if (top > scrollTop) {
|
||||
if (top + elSize <= scrollTop + offsetHeight) {
|
||||
// do nothing
|
||||
} else {
|
||||
container.scrollTo({
|
||||
left,
|
||||
top: top + elSize - offsetHeight,
|
||||
behavior
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
container.scrollTo({
|
||||
left,
|
||||
top,
|
||||
behavior
|
||||
})
|
||||
}
|
||||
function handleMouseEnterWrapper (): void {
|
||||
showXBar()
|
||||
showYBar()
|
||||
sync()
|
||||
}
|
||||
function handleMouseLeaveWrapper (): void {
|
||||
hideBar()
|
||||
}
|
||||
function hideBar (): void {
|
||||
hideYBar()
|
||||
hideXBar()
|
||||
}
|
||||
function hideYBar (): void {
|
||||
if (yBarVanishTimerId !== undefined) {
|
||||
window.clearTimeout(yBarVanishTimerId)
|
||||
}
|
||||
yBarVanishTimerId = window.setTimeout(() => {
|
||||
isShowYBarRef.value = false
|
||||
}, props.duration)
|
||||
}
|
||||
function hideXBar (): void {
|
||||
if (xBarVanishTimerId !== undefined) {
|
||||
window.clearTimeout(xBarVanishTimerId)
|
||||
}
|
||||
xBarVanishTimerId = window.setTimeout(() => {
|
||||
isShowXBarRef.value = false
|
||||
}, props.duration)
|
||||
}
|
||||
function showXBar (): void {
|
||||
if (xBarVanishTimerId !== undefined) {
|
||||
window.clearTimeout(xBarVanishTimerId)
|
||||
}
|
||||
isShowXBarRef.value = true
|
||||
}
|
||||
function showYBar (): void {
|
||||
if (yBarVanishTimerId !== undefined) {
|
||||
window.clearTimeout(yBarVanishTimerId)
|
||||
}
|
||||
isShowYBarRef.value = true
|
||||
}
|
||||
function handleScroll (e: Event): void {
|
||||
const { onScroll } = props
|
||||
if (onScroll) onScroll(e)
|
||||
syncScrollState()
|
||||
}
|
||||
function syncScrollState (): void {
|
||||
// only collect scroll state, do not trigger any dom event
|
||||
const { value: container } = mergedContainerRef
|
||||
if (container) {
|
||||
containerScrollTopRef.value = container.scrollTop
|
||||
containerScrollLeftRef.value = container.scrollLeft
|
||||
}
|
||||
}
|
||||
function syncPositionState (): void {
|
||||
// only collect position state, do not trigger any dom event
|
||||
// Don't use getClientBoundingRect because element may be scale transformed
|
||||
const { value: content } = mergedContentRef
|
||||
if (content) {
|
||||
contentHeightRef.value = content.offsetHeight
|
||||
contentWidthRef.value = content.offsetWidth
|
||||
}
|
||||
const { value: container } = mergedContainerRef
|
||||
if (container) {
|
||||
containerHeightRef.value = container.offsetHeight
|
||||
containerWidthRef.value = container.offsetWidth
|
||||
}
|
||||
const { value: xRailEl } = xRailRef
|
||||
const { value: yRailEl } = yRailRef
|
||||
if (xRailEl) {
|
||||
xRailSizeRef.value = xRailEl.offsetWidth
|
||||
}
|
||||
if (yRailEl) {
|
||||
yRailSizeRef.value = yRailEl.offsetHeight
|
||||
}
|
||||
}
|
||||
function sync (): void {
|
||||
if (!props.scrollable) return
|
||||
syncPositionState()
|
||||
syncScrollState()
|
||||
}
|
||||
function handleXScrollMouseDown (e: MouseEvent): void {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
xBarPressed = true
|
||||
on('mousemove', window, handleXScrollMouseMove, true)
|
||||
on('mouseup', window, handleXScrollMouseUp, true)
|
||||
memoXLeft = containerScrollLeftRef.value as number
|
||||
memoMouseX = e.clientX
|
||||
}
|
||||
function handleXScrollMouseMove (e: MouseEvent): void {
|
||||
if (!xBarPressed) return
|
||||
if (xBarVanishTimerId !== undefined) {
|
||||
window.clearTimeout(xBarVanishTimerId)
|
||||
}
|
||||
if (yBarVanishTimerId !== undefined) {
|
||||
window.clearTimeout(yBarVanishTimerId)
|
||||
}
|
||||
const { value: containerWidth } = containerWidthRef
|
||||
const { value: contentWidth } = contentWidthRef
|
||||
const { value: xBarSize } = xBarSizeRef
|
||||
if (containerWidth === null || contentWidth === null) return
|
||||
const dX = e.clientX - memoMouseX
|
||||
const dScrollLeft =
|
||||
(dX * (contentWidth - containerWidth)) / (containerWidth - xBarSize)
|
||||
const toScrollLeftUpperBound = contentWidth - containerWidth
|
||||
let toScrollLeft = memoXLeft + dScrollLeft
|
||||
toScrollLeft = Math.min(toScrollLeftUpperBound, toScrollLeft)
|
||||
toScrollLeft = Math.max(toScrollLeft, 0)
|
||||
const { value: container } = mergedContainerRef
|
||||
if (container) {
|
||||
container.scrollLeft = toScrollLeft
|
||||
}
|
||||
}
|
||||
function handleXScrollMouseUp (e: MouseEvent): void {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
off('mousemove', window, handleXScrollMouseMove, true)
|
||||
off('mouseup', window, handleXScrollMouseUp, true)
|
||||
xBarPressed = false
|
||||
sync()
|
||||
const { value: container } = mergedContainerRef
|
||||
if (!container?.contains(e.target as any)) {
|
||||
hideBar()
|
||||
}
|
||||
}
|
||||
function handleYScrollMouseDown (e: MouseEvent): void {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
yBarPressed = true
|
||||
on('mousemove', window, handleYScrollMouseMove, true)
|
||||
on('mouseup', window, handleYScrollMouseUp, true)
|
||||
memoYTop = containerScrollTopRef.value as number
|
||||
memoMouseY = e.clientY
|
||||
}
|
||||
function handleYScrollMouseMove (e: MouseEvent): void {
|
||||
if (!yBarPressed) return
|
||||
if (xBarVanishTimerId !== undefined) {
|
||||
window.clearTimeout(xBarVanishTimerId)
|
||||
}
|
||||
if (yBarVanishTimerId !== undefined) {
|
||||
window.clearTimeout(yBarVanishTimerId)
|
||||
}
|
||||
const { value: containerHeight } = containerHeightRef
|
||||
const { value: contentHeight } = contentHeightRef
|
||||
const { value: yBarSize } = yBarSizeRef
|
||||
if (containerHeight === null || contentHeight === null) return
|
||||
const dY = e.clientY - memoMouseY
|
||||
const dScrollTop =
|
||||
(dY * (contentHeight - containerHeight)) / (containerHeight - yBarSize)
|
||||
const toScrollTopUpperBound = contentHeight - containerHeight
|
||||
let toScrollTop = memoYTop + dScrollTop
|
||||
toScrollTop = Math.min(toScrollTopUpperBound, toScrollTop)
|
||||
toScrollTop = Math.max(toScrollTop, 0)
|
||||
const { value: container } = mergedContainerRef
|
||||
if (container) {
|
||||
container.scrollTop = toScrollTop
|
||||
}
|
||||
}
|
||||
function handleYScrollMouseUp (e: MouseEvent): void {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
off('mousemove', window, handleYScrollMouseMove, true)
|
||||
off('mouseup', window, handleYScrollMouseUp, true)
|
||||
yBarPressed = false
|
||||
sync()
|
||||
const { value: container } = mergedContainerRef
|
||||
if (!container?.contains(e.target as any)) {
|
||||
hideBar()
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
// if container exist, it always can't be resolved when scrollbar is mounted
|
||||
// for example:
|
||||
// - component
|
||||
// - scrollbar
|
||||
// - inner
|
||||
// if you pass inner to scrollbar, you may use a ref inside component
|
||||
// however, when scrollbar is mounted, ref is not ready at component
|
||||
// you need to init by yourself
|
||||
if (props.container) return
|
||||
sync()
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
if (xBarVanishTimerId !== undefined) {
|
||||
window.clearTimeout(xBarVanishTimerId)
|
||||
}
|
||||
if (yBarVanishTimerId !== undefined) {
|
||||
window.clearTimeout(yBarVanishTimerId)
|
||||
}
|
||||
off('mousemove', window, handleYScrollMouseMove, true)
|
||||
off('mouseup', window, handleYScrollMouseUp, true)
|
||||
})
|
||||
const themeRef = useTheme(
|
||||
'Scrollbar',
|
||||
'Scrollbar',
|
||||
style,
|
||||
scrollbarLight,
|
||||
props
|
||||
)
|
||||
return {
|
||||
sync,
|
||||
scrollTo,
|
||||
containerRef,
|
||||
contentRef,
|
||||
yRailRef,
|
||||
xRailRef,
|
||||
needYBar: needYBarRef,
|
||||
needXBar: needXBarRef,
|
||||
sizePx: sizePxRef,
|
||||
yBarSizePx: yBarSizePxRef,
|
||||
xBarSizePx: xBarSizePxRef,
|
||||
yBarTopPx: yBarTopPxRef,
|
||||
xBarLeftPx: xBarLeftPxRef,
|
||||
isShowXBar: isShowXBarRef,
|
||||
isShowYBar: isShowYBarRef,
|
||||
isIos,
|
||||
handleScroll,
|
||||
handleContentResize,
|
||||
handleMouseEnterWrapper,
|
||||
handleMouseLeaveWrapper,
|
||||
handleYScrollMouseDown,
|
||||
handleXScrollMouseDown,
|
||||
cssVars: computed(() => {
|
||||
const {
|
||||
common: { cubicBezierEaseInOut },
|
||||
self: { color, colorHover }
|
||||
} = themeRef.value
|
||||
return {
|
||||
'--bezier': cubicBezierEaseInOut,
|
||||
'--color': color,
|
||||
'--color-hover': colorHover
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
render () {
|
||||
const { $slots } = this
|
||||
if (!this.scrollable) return renderSlot($slots, 'default')
|
||||
return (
|
||||
<VResizeObserver onResize={this.handleContentResize}>
|
||||
{{
|
||||
default: () =>
|
||||
h(
|
||||
'div',
|
||||
mergeProps(this.$attrs, {
|
||||
class: 'n-scrollbar',
|
||||
style: this.cssVars,
|
||||
onMouseenter: this.handleMouseEnterWrapper,
|
||||
onMouseleave: this.handleMouseLeaveWrapper
|
||||
}),
|
||||
[
|
||||
this.container ? (
|
||||
renderSlot($slots, 'default')
|
||||
) : (
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="n-scrollbar-container"
|
||||
style={this.containerStyle}
|
||||
onScroll={this.handleScroll}
|
||||
>
|
||||
<VResizeObserver onResize={this.handleContentResize}>
|
||||
{{
|
||||
default: () => (
|
||||
<div
|
||||
ref="contentRef"
|
||||
style={
|
||||
[
|
||||
this.contentStyle,
|
||||
{
|
||||
width: this.xScrollable ? 'fit-content' : null
|
||||
}
|
||||
] as any
|
||||
}
|
||||
class={['n-scrollbar-content', this.contentClass]}
|
||||
>
|
||||
{renderSlot($slots, 'default')}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</VResizeObserver>
|
||||
</div>
|
||||
),
|
||||
<div
|
||||
ref="yRailRef"
|
||||
class={[
|
||||
'n-scrollbar-rail n-scrollbar-rail--vertical',
|
||||
{
|
||||
'n-scrollbar-rail--disabled': !this.needYBar
|
||||
}
|
||||
]}
|
||||
style={
|
||||
[this.horizontalRailStyle, { width: this.sizePx }] as any
|
||||
}
|
||||
>
|
||||
<Transition name="n-fade-in-transition">
|
||||
{{
|
||||
default: () =>
|
||||
this.needYBar && this.isShowYBar && !this.isIos ? (
|
||||
<div
|
||||
class="n-scrollbar-rail__scrollbar"
|
||||
style={{
|
||||
height: this.yBarSizePx,
|
||||
top: this.yBarTopPx,
|
||||
width: this.sizePx,
|
||||
borderRadius: this.sizePx
|
||||
}}
|
||||
onMousedown={this.handleYScrollMouseDown}
|
||||
/>
|
||||
) : null
|
||||
}}
|
||||
</Transition>
|
||||
</div>,
|
||||
<div
|
||||
ref="xRailRef"
|
||||
class={[
|
||||
'n-scrollbar-rail n-scrollbar-rail--horizontal',
|
||||
{
|
||||
'n-scrollbar-rail--disabled': !this.needXBar
|
||||
}
|
||||
]}
|
||||
style={
|
||||
[this.verticalRailStyle, { height: this.sizePx }] as any
|
||||
}
|
||||
>
|
||||
<Transition name="n-fade-in-transition">
|
||||
{{
|
||||
default: () =>
|
||||
this.needXBar && this.isShowXBar && !this.isIos ? (
|
||||
<div
|
||||
class="n-scrollbar-rail__scrollbar"
|
||||
style={{
|
||||
height: this.sizePx,
|
||||
width: this.xBarSizePx,
|
||||
left: this.xBarLeftPx,
|
||||
borderRadius: this.sizePx
|
||||
}}
|
||||
onMousedown={this.handleXScrollMouseDown}
|
||||
/>
|
||||
) : null
|
||||
}}
|
||||
</Transition>
|
||||
</div>
|
||||
] as VNode[]
|
||||
)
|
||||
}}
|
||||
</VResizeObserver>
|
||||
)
|
||||
}
|
||||
})
|
@ -1,537 +0,0 @@
|
||||
<template>
|
||||
<slot v-if="!scrollable" />
|
||||
<v-resize-observer v-else @resize="handleContentResize">
|
||||
<div
|
||||
v-bind="$attrs"
|
||||
class="n-scrollbar"
|
||||
:style="cssVars"
|
||||
@mouseenter="handleMouseEnterWrapper"
|
||||
@mouseleave="handleMouseLeaveWrapper"
|
||||
>
|
||||
<div
|
||||
v-if="!container"
|
||||
ref="containerRef"
|
||||
class="n-scrollbar-container"
|
||||
:style="containerStyle"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<v-resize-observer @resize="handleContentResize">
|
||||
<div
|
||||
ref="contentRef"
|
||||
:style="[
|
||||
contentStyle,
|
||||
{
|
||||
width: xScrollable ? 'fit-content' : null
|
||||
}
|
||||
]"
|
||||
:class="['n-scrollbar-content', contentClass]"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</v-resize-observer>
|
||||
</div>
|
||||
<template v-else>
|
||||
<slot />
|
||||
</template>
|
||||
<div
|
||||
ref="yRailRef"
|
||||
class="n-scrollbar-rail n-scrollbar-rail--vertical"
|
||||
:class="{
|
||||
'n-scrollbar-rail--disabled': !needyBar
|
||||
}"
|
||||
:style="[horizontalRailStyle, { width: sizePx }]"
|
||||
>
|
||||
<transition name="n-fade-in-transition">
|
||||
<div
|
||||
v-if="needyBar && isShowYBar && !isIos"
|
||||
class="n-scrollbar-rail__scrollbar"
|
||||
:style="{
|
||||
height: yBarSizePx,
|
||||
top: yBarTopPx,
|
||||
width: sizePx,
|
||||
borderRadius: scrollbarBorderRadius
|
||||
}"
|
||||
@mousedown="handleYScrollMouseDown"
|
||||
@mouseup="handleYScrollMouseUp"
|
||||
@mousemove="handleYScrollMouseMove"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
<div
|
||||
ref="xRailRef"
|
||||
class="n-scrollbar-rail n-scrollbar-rail--horizontal"
|
||||
:class="{
|
||||
'n-scrollbar-rail--disabled': !needxBar
|
||||
}"
|
||||
:style="{ ...verticalRailStyle, height: sizePx }"
|
||||
>
|
||||
<transition name="n-fade-in-transition">
|
||||
<div
|
||||
v-if="needxBar && isShowXBar && !isIos"
|
||||
class="n-scrollbar-rail__scrollbar"
|
||||
:style="{
|
||||
height: sizePx,
|
||||
width: xBarSizePx,
|
||||
left: xBarLeftPx,
|
||||
borderRadius: scrollbarBorderRadius
|
||||
}"
|
||||
@mousedown="handleXScrollMouseDown"
|
||||
@mouseup="handleXScrollMouseUp"
|
||||
@mousemove="handleXScrollMouseMove"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</v-resize-observer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, defineComponent, computed } from 'vue'
|
||||
import { on, off } from 'evtd'
|
||||
import { VResizeObserver } from 'vueuc'
|
||||
import { useIsIos } from 'vooks'
|
||||
import { useTheme } from '../../_mixins'
|
||||
import { scrollbarLight } from '../styles'
|
||||
import style from './styles/index.cssr.js'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Scrollbar',
|
||||
components: {
|
||||
VResizeObserver
|
||||
},
|
||||
props: {
|
||||
...useTheme.props,
|
||||
size: {
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
scrollable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
xScrollable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
container: {
|
||||
type: Function,
|
||||
default: undefined
|
||||
},
|
||||
content: {
|
||||
type: Function,
|
||||
default: undefined
|
||||
},
|
||||
containerStyle: {
|
||||
type: Object,
|
||||
default: undefined
|
||||
},
|
||||
contentClass: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
contentStyle: {
|
||||
type: Object,
|
||||
default: undefined
|
||||
},
|
||||
horizontalRailStyle: {
|
||||
type: Object,
|
||||
default: undefined
|
||||
},
|
||||
verticalRailStyle: {
|
||||
type: Object,
|
||||
default: undefined
|
||||
},
|
||||
onScroll: {
|
||||
type: Function,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
const themeRef = useTheme(
|
||||
'Scrollbar',
|
||||
'Scrollbar',
|
||||
style,
|
||||
scrollbarLight,
|
||||
props
|
||||
)
|
||||
return {
|
||||
containerRef: ref(null),
|
||||
contentRef: ref(null),
|
||||
yRailRef: ref(null),
|
||||
xRailRef: ref(null),
|
||||
cssVars: computed(() => {
|
||||
const {
|
||||
common: { cubicBezierEaseInOut },
|
||||
self: { color, colorHover }
|
||||
} = themeRef.value
|
||||
return {
|
||||
'--bezier': cubicBezierEaseInOut,
|
||||
'--color': color,
|
||||
'--color-hover': colorHover
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
contentHeight: null,
|
||||
contentWidth: null,
|
||||
containerHeight: null,
|
||||
containerWidth: null,
|
||||
yRailSize: null,
|
||||
xRailWidth: null,
|
||||
containerScrollTop: null,
|
||||
containerScrollLeft: null,
|
||||
xBarVanishTimerId: null,
|
||||
yBarVanishTimerId: null,
|
||||
isShowXBar: false,
|
||||
isShowYBar: false,
|
||||
yBarPressed: false,
|
||||
xBarPressed: false,
|
||||
memoYTop: null,
|
||||
memoXLeft: null,
|
||||
memoMouseX: null,
|
||||
memoMouseY: null,
|
||||
isIos: useIsIos()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
yBarSize () {
|
||||
if (
|
||||
this.containerHeight === null ||
|
||||
this.contentHeight === null ||
|
||||
this.yRailSize === null
|
||||
) {
|
||||
return 0
|
||||
} else {
|
||||
return Math.min(
|
||||
this.containerHeight,
|
||||
(this.yRailSize * this.containerHeight) / this.contentHeight +
|
||||
this.size * 1.5
|
||||
)
|
||||
}
|
||||
},
|
||||
yBarSizePx () {
|
||||
return this.yBarSize + 'px'
|
||||
},
|
||||
xBarSize () {
|
||||
if (
|
||||
this.containerWidth === null ||
|
||||
this.contentWidth === null ||
|
||||
this.xRailSize === null
|
||||
) {
|
||||
return 0
|
||||
} else {
|
||||
return (
|
||||
(this.xRailSize * this.containerWidth) / this.contentWidth +
|
||||
this.size * 1.5
|
||||
)
|
||||
}
|
||||
},
|
||||
xBarSizePx () {
|
||||
return this.xBarSize + 'px'
|
||||
},
|
||||
yBarTop () {
|
||||
if (
|
||||
this.containerHeight === null ||
|
||||
this.containerScrollTop === null ||
|
||||
this.contentHeight === null ||
|
||||
this.yRailSize === null
|
||||
) {
|
||||
return 0
|
||||
} else {
|
||||
return (
|
||||
(this.containerScrollTop /
|
||||
(this.contentHeight - this.containerHeight)) *
|
||||
(this.yRailSize - this.yBarSize)
|
||||
)
|
||||
}
|
||||
},
|
||||
yBarTopPx () {
|
||||
return this.yBarTop + 'px'
|
||||
},
|
||||
xBarLeft () {
|
||||
if (
|
||||
this.containerWidth === null ||
|
||||
this.containerScrollLeft === null ||
|
||||
this.contentWidth === null
|
||||
) {
|
||||
return 0
|
||||
} else {
|
||||
return (
|
||||
(this.containerScrollLeft /
|
||||
(this.contentWidth - this.containerWidth)) *
|
||||
(this.xRailSize - this.xBarSize)
|
||||
)
|
||||
}
|
||||
},
|
||||
xBarLeftPx () {
|
||||
return this.xBarLeft + 'px'
|
||||
},
|
||||
sizePx () {
|
||||
return this.size + 'px'
|
||||
},
|
||||
scrollbarBorderRadius () {
|
||||
return this.size / 2 + 'px'
|
||||
},
|
||||
needyBar () {
|
||||
return (
|
||||
this.containerHeight !== null &&
|
||||
this.contentHeight !== null &&
|
||||
this.contentHeight > this.containerHeight
|
||||
)
|
||||
},
|
||||
needxBar () {
|
||||
return (
|
||||
this.containerWidth !== null &&
|
||||
this.contentWidth !== null &&
|
||||
this.contentWidth > this.containerWidth
|
||||
)
|
||||
}
|
||||
},
|
||||
beforeUnmount () {
|
||||
window.clearTimeout(this.xBarVanishTimerId)
|
||||
window.clearTimeout(this.yBarVanishTimerId)
|
||||
off('mousemove', window, this.handleYScrollMouseMove, true)
|
||||
off('mouseup', window, this.handleYScrollMouseUp, true)
|
||||
},
|
||||
mounted () {
|
||||
// if container exist, it always can't be resolved when scrollbar is mounted
|
||||
// for example:
|
||||
// - component
|
||||
// - scrollbar
|
||||
// - inner
|
||||
// if you pass inner to scrollbar, you may use a ref inside component
|
||||
// however, when scrollbar is mounted, ref is not ready at component
|
||||
// you need to init by yourself
|
||||
if (this.container) return
|
||||
this.sync()
|
||||
},
|
||||
methods: {
|
||||
mergedContainerRef () {
|
||||
const { container } = this
|
||||
if (container) return container()
|
||||
return this.containerRef
|
||||
},
|
||||
mergedContentRef () {
|
||||
const { content } = this
|
||||
if (content) return content()
|
||||
return this.contentRef
|
||||
},
|
||||
handleContentResize () {
|
||||
this.sync()
|
||||
},
|
||||
scrollTo (options, y) {
|
||||
if (!this.scrollable) return
|
||||
if (typeof options === 'number') {
|
||||
this.scrollToPosition(options, y, 0, false, 'auto')
|
||||
}
|
||||
const {
|
||||
left,
|
||||
top,
|
||||
index,
|
||||
elSize,
|
||||
position,
|
||||
behavior,
|
||||
el,
|
||||
debounce = true
|
||||
} = options
|
||||
if (left !== undefined || top !== undefined) {
|
||||
this.scrollToPosition(left, top, 0, false, behavior)
|
||||
}
|
||||
if (el !== undefined) {
|
||||
this.scrollToPosition(
|
||||
0,
|
||||
el.offsetTop,
|
||||
el.offsetHeight,
|
||||
debounce,
|
||||
behavior
|
||||
)
|
||||
} else if (index !== undefined && elSize !== undefined) {
|
||||
this.scrollToPosition(0, index * elSize, elSize, debounce, behavior)
|
||||
} else if (position === 'bottom') {
|
||||
this.scrollToPosition(0, Number.MAX_SAFE_INTEGER, 0, false, behavior)
|
||||
} else if (position === 'top') {
|
||||
this.scrollToPosition(0, 0, 0, false, behavior)
|
||||
}
|
||||
},
|
||||
scrollToPosition (left, top, elSize, debounce, behavior) {
|
||||
const container = this.mergedContainerRef()
|
||||
if (!container) return
|
||||
if (debounce) {
|
||||
const { scrollTop, offsetHeight } = container
|
||||
if (top > scrollTop) {
|
||||
if (top + elSize <= scrollTop + offsetHeight) {
|
||||
// do nothing
|
||||
} else {
|
||||
container.scrollTo({
|
||||
left,
|
||||
top: top + elSize - offsetHeight,
|
||||
behavior
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
container.scrollTo({
|
||||
left,
|
||||
top,
|
||||
behavior
|
||||
})
|
||||
},
|
||||
handleMouseEnterWrapper () {
|
||||
this.showXBar()
|
||||
this.showYBar()
|
||||
this.sync()
|
||||
},
|
||||
handleMouseLeaveWrapper () {
|
||||
this.hideBar()
|
||||
},
|
||||
hideBar () {
|
||||
this.hideYBar()
|
||||
this.hideXBar()
|
||||
},
|
||||
hideYBar () {
|
||||
if (this.yBarVanishTimerId !== null) {
|
||||
window.clearTimeout(this.yBarVanishTimerId)
|
||||
}
|
||||
this.yBarVanishTimerId = window.setTimeout(() => {
|
||||
this.isShowYBar = false
|
||||
}, this.duration)
|
||||
},
|
||||
hideXBar () {
|
||||
if (this.xBarVanishTimerId !== null) {
|
||||
window.clearTimeout(this.xBarVanishTimerId)
|
||||
}
|
||||
this.xBarVanishTimerId = window.setTimeout(() => {
|
||||
this.isShowXBar = false
|
||||
}, this.duration)
|
||||
},
|
||||
showXBar () {
|
||||
if (this.xBarVanishTimerId !== null) {
|
||||
window.clearTimeout(this.xBarVanishTimerId)
|
||||
}
|
||||
this.isShowXBar = true
|
||||
},
|
||||
showYBar () {
|
||||
if (this.yBarVanishTimerId !== null) {
|
||||
window.clearTimeout(this.yBarVanishTimerId)
|
||||
}
|
||||
this.isShowYBar = true
|
||||
},
|
||||
handleScroll (e) {
|
||||
const { onScroll } = this
|
||||
if (onScroll) onScroll(e)
|
||||
this.syncScrollState()
|
||||
},
|
||||
syncScrollState () {
|
||||
// only collect scroll state, do not trigger any dom event
|
||||
const container = this.mergedContainerRef()
|
||||
if (container) {
|
||||
this.containerScrollTop = container.scrollTop
|
||||
this.containerScrollLeft = container.scrollLeft
|
||||
}
|
||||
},
|
||||
syncPositionState () {
|
||||
// only collect position state, do not trigger any dom event
|
||||
// Don't use getClientBoundingRect because element may be scale transformed
|
||||
const content = this.mergedContentRef()
|
||||
if (content) {
|
||||
this.contentHeight = content.offsetHeight
|
||||
this.contentWidth = content.offsetWidth
|
||||
}
|
||||
const container = this.mergedContainerRef()
|
||||
if (container) {
|
||||
this.containerHeight = container.offsetHeight
|
||||
this.containerWidth = container.offsetWidth
|
||||
}
|
||||
const { xRailRef, yRailRef } = this
|
||||
if (xRailRef) {
|
||||
this.xRailSize = xRailRef.offsetWidth
|
||||
}
|
||||
if (yRailRef) {
|
||||
this.yRailSize = yRailRef.offsetHeight
|
||||
}
|
||||
},
|
||||
sync () {
|
||||
if (!this.scrollable) return
|
||||
this.syncPositionState()
|
||||
this.syncScrollState()
|
||||
},
|
||||
handleXScrollMouseDown (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
this.xBarPressed = true
|
||||
on('mousemove', window, this.handleXScrollMouseMove, true)
|
||||
on('mouseup', window, this.handleXScrollMouseUp, true)
|
||||
this.memoXLeft = this.containerScrollLeft
|
||||
this.memoMouseX = e.clientX
|
||||
},
|
||||
handleXScrollMouseMove (e) {
|
||||
if (!this.xBarPressed) return
|
||||
window.clearTimeout(this.xBarVanishTimerId)
|
||||
window.clearTimeout(this.yBarVanishTimerId)
|
||||
const dX = e.clientX - this.memoMouseX
|
||||
const dScrollLeft =
|
||||
(dX * (this.contentWidth - this.containerWidth)) /
|
||||
(this.containerWidth - this.xBarSize)
|
||||
const toScrollLeftUpperBound = this.contentWidth - this.containerWidth
|
||||
let toScrollLeft = this.memoXLeft + dScrollLeft
|
||||
toScrollLeft = Math.min(toScrollLeftUpperBound, toScrollLeft)
|
||||
toScrollLeft = Math.max(toScrollLeft, 0)
|
||||
this.mergedContainerRef().scrollLeft = toScrollLeft
|
||||
},
|
||||
handleXScrollMouseUp (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
off('mousemove', window, this.handleXScrollMouseMove, true)
|
||||
off('mouseup', window, this.handleXScrollMouseUp, true)
|
||||
this.xBarPressed = false
|
||||
this.sync()
|
||||
if (!this.mergedContainerRef().contains(e.target)) {
|
||||
this.hideBar()
|
||||
}
|
||||
},
|
||||
handleYScrollMouseDown (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
this.yBarPressed = true
|
||||
on('mousemove', window, this.handleYScrollMouseMove, true)
|
||||
on('mouseup', window, this.handleYScrollMouseUp, true)
|
||||
this.memoYTop = this.containerScrollTop
|
||||
this.memoMouseY = e.clientY
|
||||
},
|
||||
handleYScrollMouseMove (e) {
|
||||
if (!this.yBarPressed) return
|
||||
window.clearTimeout(this.xBarVanishTimerId)
|
||||
window.clearTimeout(this.yBarVanishTimerId)
|
||||
const dY = e.clientY - this.memoMouseY
|
||||
const dScrollTop =
|
||||
(dY * (this.contentHeight - this.containerHeight)) /
|
||||
(this.containerHeight - this.yBarSize)
|
||||
const toScrollTopUpperBound = this.contentHeight - this.containerHeight
|
||||
let toScrollTop = this.memoYTop + dScrollTop
|
||||
toScrollTop = Math.min(toScrollTopUpperBound, toScrollTop)
|
||||
toScrollTop = Math.max(toScrollTop, 0)
|
||||
this.mergedContainerRef().scrollTop = toScrollTop
|
||||
},
|
||||
handleYScrollMouseUp (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const { onScrollEnd } = this
|
||||
if (onScrollEnd) onScrollEnd()
|
||||
off('mousemove', window, this.handleYScrollMouseMove, true)
|
||||
off('mouseup', window, this.handleYScrollMouseUp, true)
|
||||
this.yBarPressed = false
|
||||
this.sync()
|
||||
if (!this.mergedContainerRef().contains(e.target)) {
|
||||
this.hideBar()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
@ -1,6 +1,7 @@
|
||||
import { commonDark } from '../../_styles/new-common'
|
||||
import type { ScrollbarTheme } from './light'
|
||||
|
||||
export default {
|
||||
const scrollbarDark: ScrollbarTheme = {
|
||||
name: 'Scrollbar',
|
||||
common: commonDark,
|
||||
self (vars) {
|
||||
@ -11,3 +12,5 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default scrollbarDark
|
@ -1,2 +0,0 @@
|
||||
export { default as scrollbarDark } from './dark.js'
|
||||
export { default as scrollbarLight } from './light.js'
|
3
src/scrollbar/styles/index.ts
Normal file
3
src/scrollbar/styles/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { default as scrollbarDark } from './dark'
|
||||
export { default as scrollbarLight } from './light'
|
||||
export type { ScrollbarTheme, ScrollbarThemeVars } from './light'
|
@ -1,13 +0,0 @@
|
||||
import { commonLight } from '../../_styles/new-common'
|
||||
|
||||
export default {
|
||||
name: 'Scrollbar',
|
||||
common: commonLight,
|
||||
self (vars) {
|
||||
const { scrollbarColorOverlay, scrollbarColorHoverOverlay } = vars
|
||||
return {
|
||||
color: scrollbarColorOverlay,
|
||||
colorHover: scrollbarColorHoverOverlay
|
||||
}
|
||||
}
|
||||
}
|
22
src/scrollbar/styles/light.ts
Normal file
22
src/scrollbar/styles/light.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { commonLight } from '../../_styles/new-common'
|
||||
import type { ThemeCommonVars } from '../../_styles/new-common'
|
||||
import type { Theme } from '../../_mixins'
|
||||
|
||||
const self = (vars: ThemeCommonVars) => {
|
||||
const { scrollbarColorOverlay, scrollbarColorHoverOverlay } = vars
|
||||
return {
|
||||
color: scrollbarColorOverlay,
|
||||
colorHover: scrollbarColorHoverOverlay
|
||||
}
|
||||
}
|
||||
|
||||
export type ScrollbarThemeVars = ReturnType<typeof self>
|
||||
|
||||
const scrollbarLight: Theme<ScrollbarThemeVars> = {
|
||||
name: 'Scrollbar',
|
||||
common: commonLight,
|
||||
self
|
||||
}
|
||||
|
||||
export default scrollbarLight
|
||||
export type ScrollbarTheme = typeof scrollbarLight
|
Loading…
Reference in New Issue
Block a user