diff --git a/package.json b/package.json index 4b6538a..0ec397f 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "build:watch": "pnpm clean && pnpm check:types && vite build --watch", "check:types": "vue-tsc --noEmit", "prepare": "husky install", - "auto:publish": "git checkout dev | pnpm build | git branch -D release | git checkout -b release | git merge dev | npm version patch | pnpm build | pnpm publish | git push --set-upstream origin release | git checkout dev | git merge release | git push origin dev" + "auto:publish": "git checkout dev | git branch -D release | git checkout -b release | git merge dev | npm version patch | pnpm build | pnpm publish | git push --set-upstream origin release | git checkout dev | git merge release | git push origin dev" }, "dependencies": { "@vueuse/core": "^10.7.0", diff --git a/play/src/vxe-table.vue b/play/src/vxe-table.vue index 43f5f1e..de442de 100644 --- a/play/src/vxe-table.vue +++ b/play/src/vxe-table.vue @@ -139,7 +139,7 @@ const formatterSex = ({ cellValue }: any) => { let count = 0 let parentId = 0 -const listCount = 2000 +const listCount = 500 const getTableData = () => { const res = Array(listCount).fill(0).map((x) => { const res = { diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 55f2b9d..61a557f 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,17 +1,19 @@ import useElementSize from './useElementSize' import useElementHover from './useElementHover' +import useScroll from './useScroll' import useScrollbar from './useScrollbar' import useNativeScrollbar from './useNativeScrollbar' export * from './useElementSize' export * from './useElementHover' +export * from './useScroll' export * from './useScrollbar' export * from './useNativeScrollbar' const useScrollbars = useScrollbar -export { useElementSize, useElementHover, useScrollbar, useScrollbars, useNativeScrollbar } +export { useElementSize, useElementHover, useScroll, useScrollbar, useScrollbars, useNativeScrollbar } -const index = { useElementSize, useElementHover, useScrollbar, useScrollbars, useNativeScrollbar } +const index = { useElementSize, useElementHover, useScroll, useScrollbar, useScrollbars, useNativeScrollbar } export default index diff --git a/src/hooks/useScroll/index.ts b/src/hooks/useScroll/index.ts new file mode 100644 index 0000000..f90bd2c --- /dev/null +++ b/src/hooks/useScroll/index.ts @@ -0,0 +1,213 @@ +import { computed, ref } from 'vue-demi' +import { useEventListener } from '@vueuse/core' +import type { MaybeRefOrGetter } from '@vueuse/shared' +import { tryOnMounted, noop, toValue, useDebounceFn, useThrottleFn } from '@vueuse/shared' + +const defaultWindow = typeof window === 'undefined' ? undefined : window + +export interface UseScrollOptions { + window?: any + /** + * Throttle time for scroll event, it’s disabled by default. + * + * @default 0 + */ + throttle?: number + + /** + * The check time when scrolling ends. + * This configuration will be setting to (throttle + idle) when the `throttle` is configured. + * + * @default 200 + */ + idle?: number + + /** + * Offset arrived states by x pixels + * + */ + offset?: { + left?: number + right?: number + top?: number + bottom?: number + } + + /** + * Trigger it when scrolling. + * + */ + onScroll?: (e: Event) => void + + /** + * Trigger it when scrolling ends. + * + */ + onStop?: (e: Event) => void + + /** + * Listener options for scroll event. + * + * @default {capture: false, passive: true} + */ + eventListenerOptions?: boolean | AddEventListenerOptions + + /** + * Optionally specify a scroll behavior of `auto` (default, not smooth scrolling) or + * `smooth` (for smooth scrolling) which takes effect when changing the `x` or `y` refs. + * + * @default 'auto' + */ + behavior?: MaybeRefOrGetter +} + +/** + * perf reactive use-scroll instead of @vueuse/core's + * + * @see https://vueuse.org/useScroll + * @param element + * @param options + */ + +export default function useScroll( + element: MaybeRefOrGetter, + options: UseScrollOptions = {}, +) { + const { + throttle = 0, + idle = 200, + onStop = noop, + onScroll = noop, + eventListenerOptions = { + capture: false, + passive: true, + }, + behavior = 'auto', + window = defaultWindow, + } = options + + const internalX = ref(0) + const internalY = ref(0) + + // Use a computed for x and y because we want to write the value to the refs + // during a `scrollTo()` without firing additional `scrollTo()`s in the process. + const x = computed({ + get() { + return internalX.value + }, + set(x) { + scrollTo(x, undefined) + }, + }) + + const y = computed({ + get() { + return internalY.value + }, + set(y) { + scrollTo(undefined, y) + }, + }) + + function scrollTo(_x: number | undefined, _y: number | undefined) { + if (!window) + return + + const _element = toValue(element) + if (!_element) + return + + (_element instanceof Document ? window.document.body : _element)?.scrollTo({ + top: toValue(_y) ?? y.value, + left: toValue(_x) ?? x.value, + behavior: toValue(behavior), + }) + } + + const isScrolling = ref(false) + + const onScrollEnd = (e: Event) => { + // dedupe if support native scrollend event + if (!isScrolling.value) + return + + isScrolling.value = false + onStop(e) + } + const onScrollEndDebounced = useDebounceFn(onScrollEnd, throttle + idle) + + const setArrivedState = (target: HTMLElement | SVGElement | Window | Document | null | undefined) => { + if (!window) + return + + const el = ( + (target as Window).document + ? (target as Window).document.documentElement + : (target as Document).documentElement ?? target + ) as HTMLElement + + const scrollLeft = el.scrollLeft + + internalX.value = scrollLeft + + let scrollTop = el.scrollTop + + // patch for mobile compatible + if (target === window.document && !scrollTop) + scrollTop = window.document.body.scrollTop + + internalY.value = scrollTop + } + + const onScrollHandler = (e: Event) => { + if (!window) + return + + const eventTarget = ( + (e.target as Document).documentElement ?? e.target + ) as HTMLElement + + setArrivedState(eventTarget) + + isScrolling.value = true + onScrollEndDebounced(e) + onScroll(e) + } + + useEventListener( + element, + 'scroll', + throttle ? useThrottleFn(onScrollHandler, throttle, true, false) : onScrollHandler, + eventListenerOptions, + ) + + tryOnMounted(() => { + const _element = toValue(element) + if (!_element) + return + + setArrivedState(_element) + }) + + useEventListener( + element, + 'scrollend', + onScrollEnd, + eventListenerOptions, + ) + + return { + x, + y, + isScrolling, + measure() { + const _element = toValue(element) + + if (window && _element) + setArrivedState(_element) + }, + } +} + +export type UseScrollReturn = ReturnType + diff --git a/src/hooks/useScrollbar/index.ts b/src/hooks/useScrollbar/index.ts index 2335325..5577f8c 100644 --- a/src/hooks/useScrollbar/index.ts +++ b/src/hooks/useScrollbar/index.ts @@ -1,6 +1,7 @@ import { ref, unref, watchEffect, computed, watch, reactive, onUnmounted } from 'vue-demi' -import { useEventListener, useScroll, unrefElement } from '@vueuse/core' -import { useElementHover, useElementSize } from '@/hooks' +import { useEventListener, unrefElement } from '@vueuse/core' +import { tryOnScopeDispose } from '@vueuse/shared' +import { useElementHover, useElementSize, useScroll } from '@/hooks' import { createComponent } from "./scrollbars"; import { notEmpty, safeRatio, safePrecicion, SCROLLBAR_GAP, findScrollElement } from './utils' import getOpts from './states' @@ -63,18 +64,11 @@ export default function useScrollbar( // console.log('states', states) - const stops = [] as (() => void)[] function destroy() { - stops.map((s) => s()) - clean() instance.value?.destroy?.() - // init() TODO - } - const watchEffectGathered = (...args: Parameters) => { - const stop = watchEffect(...args) - stops.push(stop) - return stop } + tryOnScopeDispose(destroy) + // 监听元素的 hover 事件以改变滚动条的显示隐藏状态 function visibleOnHover($hoverOn: MaybeComputedElementRef) { @@ -117,7 +111,7 @@ export default function useScrollbar( const $elms = args.filter(notEmpty) $elms.map(($elm) => { const { width, height } = useElementSize($elm, undefined, { box: 'border-box' }) - watchEffectGathered(() => { + watchEffect(() => { if (opts?.x?.left === $elm) states.offset.x.left = safePrecicion(width.value + gap) if (opts?.x?.bottom === $elm) states.offset.x.bottom = safePrecicion(height.value + gap) if (opts?.y?.right === $elm) states.offset.y.right = safePrecicion(width.value + gap) @@ -176,10 +170,10 @@ export default function useScrollbar( ws.push(width) hs.push(height) }) - watchEffectGathered(() => { + watchEffect(() => { viewportW.value = Math.max(...ws.map(unref)) }) - watchEffectGathered(() => { + watchEffect(() => { viewportH.value = Math.max(...hs.map(unref)) }) } @@ -191,10 +185,10 @@ export default function useScrollbar( ws.push(width) hs.push(height) }) - watchEffectGathered(() => { + watchEffect(() => { contentW.value = Math.max(...ws.map(unref)) }) - watchEffectGathered(() => { + watchEffect(() => { contentH.value = Math.max(...hs.map(unref)) }) } @@ -206,13 +200,13 @@ export default function useScrollbar( tops.push(top) lefts.push(left) }) - watchEffectGathered(() => { + watchEffect(() => { scrollTop.value = Math.max(...tops.map(unref)) }) - watchEffectGathered(() => { + watchEffect(() => { scrollLeft.value = Math.max(...lefts.map(unref)) }) - watchEffectGathered(() => { + watchEffect(() => { states.scrollTo = (x: number, y: number) => { (opts.wrapper as MaybeElem[]).map(($elm) => (unref($elm) as HTMLElement)?.scrollTo?.(x, y), @@ -223,10 +217,10 @@ export default function useScrollbar( /* 计算滚动条的显隐状态 */ - watchEffectGathered(() => { + watchEffect(() => { states.visible.x = states.isDragging.x || (!states.isHidden.x && inTrigger.value.x) }) - watchEffectGathered(() => { + watchEffect(() => { states.visible.y = states.isDragging.y || (!states.isHidden.y && inTrigger.value.y) }) @@ -238,7 +232,7 @@ export default function useScrollbar( * 3. 当内容超出更多,尺寸从 size.base 逼近 size.min * @FIXME 当容器尺寸大于滚动条尺寸时 */ - watchEffectGathered(() => { + watchEffect(() => { const top = states.offset.y.top || 0 const base = Math.min(config.size.base, states.mountOnH - top) const max = Math.min(config.size.max, states.mountOnH - top) @@ -266,7 +260,7 @@ export default function useScrollbar( const safeHeight = Math.min(Math.max(height, config.size.min), max) states.size.y.height = safeHeight }) - watchEffectGathered(() => { + watchEffect(() => { const left = states.offset.x.left || 0 const base = Math.min(config.size.base, states.mountOnW - left) const max = Math.min(config.size.max, states.mountOnW - left) @@ -296,11 +290,11 @@ export default function useScrollbar( /* 滚动条轨道高度 */ - watchEffectGathered(() => { + watchEffect(() => { states.size.y.path = viewportH.value - states.size.y.height - SCROLLBAR_GAP * 2 // console.log("[debug] scrollbar y path", viewportH.value, states.size.y.height); }) - watchEffectGathered(() => { + watchEffect(() => { states.size.x.path = viewportW.value - states.size.x.width - SCROLLBAR_GAP * 2 // console.log("[debug] scrollbar x path", viewportW.value, states.size.x.width); }) @@ -314,18 +308,18 @@ export default function useScrollbar( /* 计算滚动条距边缘的距离 */ - watchEffectGathered(() => { + watchEffect(() => { const top = states.size.y.path * scrollbarToEdgeRatio.value.y const safeTop = Math.min(states.size.y.path, Math.max(top, 0)) states.position.y.top = safeTop }) - watchEffectGathered(() => { + watchEffect(() => { const left = states.size.x.path * scrollbarToEdgeRatio.value.x const safeLeft = Math.min(states.size.x.path, Math.max(left, 0)) states.position.x.left = safeLeft }) - watchEffectGathered(() => { + watchEffect(() => { mount(opts.mount) visibleOnHover(opts.mount) }) @@ -357,52 +351,52 @@ export default function useScrollbar( /* 将部分值代理为状态 */ - watchEffectGathered(() => { + watchEffect(() => { if (states.mountOnW !== mountOnW.value) { states.mountOnW = mountOnW.value } }) - watchEffectGathered(() => { + watchEffect(() => { if (states.mountOnH !== mountOnH.value) { states.mountOnH = mountOnH.value } }) - watchEffectGathered(() => { + watchEffect(() => { if (states.viewportH !== viewportH.value) { states.viewportH = viewportH.value } }) - watchEffectGathered(() => { + watchEffect(() => { if (states.viewportW !== viewportW.value) { states.viewportW = viewportW.value } }) - watchEffectGathered(() => { + watchEffect(() => { if (states.contentH !== contentH.value) { states.contentH = contentH.value } }) - watchEffectGathered(() => { + watchEffect(() => { if (states.contentW !== contentW.value) { states.contentW = contentW.value } }) - watchEffectGathered(() => { + watchEffect(() => { if (states.scrollTop !== scrollTop.value) { states.scrollTop = scrollTop.value } }) - watchEffectGathered(() => { + watchEffect(() => { if (states.scrollLeft !== scrollLeft.value) { states.scrollLeft = scrollLeft.value } }) - watchEffectGathered(() => { + watchEffect(() => { if (states.isScrolling.x !== Boolean(scrollXTick.value)) { states.isScrolling.x = Boolean(scrollXTick.value) } }) - watchEffectGathered(() => { + watchEffect(() => { if (states.isScrolling.y !== Boolean(scrollYTick.value)) { states.isScrolling.y = Boolean(scrollYTick.value) }