mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fix(reader): preserve touch text selection in PDF reader
Route touch selection through EmbedPDF pointer mode while keeping swipe pagination limited to pan mode. Keep NestJS testing aligned with the application major and prevent incompatible standalone major updates. Fixes #1388
This commit is contained in:
@@ -18,6 +18,9 @@ updates:
|
|||||||
# archiver v8 is ESM-only with a completely different API; pin to v7 until we migrate
|
# archiver v8 is ESM-only with a completely different API; pin to v7 until we migrate
|
||||||
- dependency-name: "archiver"
|
- dependency-name: "archiver"
|
||||||
update-types: ["version-update:semver-major"]
|
update-types: ["version-update:semver-major"]
|
||||||
|
# @nestjs/testing must stay on the same major as @nestjs/common and @nestjs/core
|
||||||
|
- dependency-name: "@nestjs/testing"
|
||||||
|
update-types: ["version-update:semver-major"]
|
||||||
groups:
|
groups:
|
||||||
production-dependencies:
|
production-dependencies:
|
||||||
dependency-type: "production"
|
dependency-type: "production"
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
const mockUsePan = vi.hoisted(() => vi.fn<() => unknown>())
|
||||||
|
const mockUseInteractionManager = vi.hoisted(() => vi.fn<() => unknown>())
|
||||||
|
|
||||||
|
vi.mock('@embedpdf/plugin-pan/vue', () => ({ usePan: mockUsePan }))
|
||||||
|
vi.mock('@embedpdf/plugin-interaction-manager/vue', () => ({ useInteractionManager: mockUseInteractionManager }))
|
||||||
|
|
||||||
|
const { usePdfInteractionMode } = await import('../composables/usePdfInteractionMode')
|
||||||
|
|
||||||
|
describe('usePdfInteractionMode', () => {
|
||||||
|
const togglePan = vi.fn<() => void>()
|
||||||
|
const disablePan = vi.fn<() => void>()
|
||||||
|
const activate = vi.fn<(mode: string) => void>()
|
||||||
|
const isPanning = ref(true)
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
mockUsePan.mockReturnValue({
|
||||||
|
isPanning,
|
||||||
|
provides: ref({ togglePan, disablePan }),
|
||||||
|
})
|
||||||
|
mockUseInteractionManager.mockReturnValue({
|
||||||
|
provides: ref({ activate }),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('activates pointer mode directly when selecting text on a touch device', () => {
|
||||||
|
const mode = usePdfInteractionMode(ref('document-1'))
|
||||||
|
|
||||||
|
mode.activateTextSelection()
|
||||||
|
|
||||||
|
expect(activate).toHaveBeenCalledExactlyOnceWith('pointerMode')
|
||||||
|
expect(disablePan).not.toHaveBeenCalled()
|
||||||
|
expect(mode.isPanning).toBe(isPanning)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toggles pan through the document-scoped pan capability', () => {
|
||||||
|
const mode = usePdfInteractionMode(ref('document-1'))
|
||||||
|
|
||||||
|
mode.togglePan()
|
||||||
|
|
||||||
|
expect(togglePan).toHaveBeenCalledOnce()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps tool actions safe while the document capabilities are unavailable', () => {
|
||||||
|
mockUsePan.mockReturnValue({ isPanning, provides: ref(null) })
|
||||||
|
mockUseInteractionManager.mockReturnValue({ provides: ref(null) })
|
||||||
|
const mode = usePdfInteractionMode(ref('document-1'))
|
||||||
|
|
||||||
|
expect(() => mode.togglePan()).not.toThrow()
|
||||||
|
expect(() => mode.activateTextSelection()).not.toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,9 +1,87 @@
|
|||||||
import { defineComponent, h, ref } from 'vue'
|
import { defineComponent, h, ref, type Ref } from 'vue'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount, type VueWrapper } from '@vue/test-utils'
|
||||||
|
import { Rotation, type PdfPageObjectWithRotatedSize } from '@embedpdf/models'
|
||||||
import type { ScrollScope } from '@embedpdf/plugin-scroll'
|
import type { ScrollScope } from '@embedpdf/plugin-scroll'
|
||||||
|
import type { PdfReaderSettings } from '@bookorbit/types'
|
||||||
import { usePdfPagination } from '../composables/usePdfPagination'
|
import { usePdfPagination } from '../composables/usePdfPagination'
|
||||||
|
|
||||||
|
interface PaginationHarness {
|
||||||
|
wrapper: VueWrapper
|
||||||
|
pagination: ReturnType<typeof usePdfPagination>
|
||||||
|
touchPageTurningEnabled: Ref<boolean>
|
||||||
|
onActivity: ReturnType<typeof vi.fn<() => void>>
|
||||||
|
scrollToPage: ReturnType<typeof vi.fn<(options: { pageNumber: number; behavior: 'instant' | 'smooth' }) => void>>
|
||||||
|
}
|
||||||
|
|
||||||
|
function page(index: number): PdfPageObjectWithRotatedSize {
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
size: { width: 600, height: 800 },
|
||||||
|
rotatedSize: { width: 600, height: 800 },
|
||||||
|
rotation: Rotation.Degree0,
|
||||||
|
objectNumber: index + 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function touchEvent(type: 'touchstart' | 'touchend', touches: Array<{ clientX: number; clientY: number }>, cancelable = true): TouchEvent {
|
||||||
|
const event = new Event(type, { bubbles: true, cancelable }) as TouchEvent
|
||||||
|
Object.defineProperties(event, {
|
||||||
|
touches: { value: type === 'touchstart' ? touches : [] },
|
||||||
|
changedTouches: { value: touches },
|
||||||
|
})
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountPagination({
|
||||||
|
scrollMode = 'page',
|
||||||
|
touchTurning = true,
|
||||||
|
currentPage = 2,
|
||||||
|
totalPages = 3,
|
||||||
|
}: {
|
||||||
|
scrollMode?: PdfReaderSettings['scrollMode']
|
||||||
|
touchTurning?: boolean
|
||||||
|
currentPage?: number
|
||||||
|
totalPages?: number
|
||||||
|
} = {}): PaginationHarness {
|
||||||
|
const mode = ref<PdfReaderSettings['scrollMode']>(scrollMode)
|
||||||
|
const touchPageTurningEnabled = ref(touchTurning)
|
||||||
|
const onActivity = vi.fn<() => void>()
|
||||||
|
const scrollToPage = vi.fn<(options: { pageNumber: number; behavior: 'instant' | 'smooth' }) => void>()
|
||||||
|
const scroll = ref({
|
||||||
|
getSpreadPagesWithRotatedSize: () => [[page(0)], [page(1)], [page(2)]],
|
||||||
|
getPageChangeState: () => ({ isChanging: false }),
|
||||||
|
getMetrics: () => ({ pageVisibilityMetrics: [] }),
|
||||||
|
scrollToPage,
|
||||||
|
} as unknown as Readonly<ScrollScope>)
|
||||||
|
let pagination!: ReturnType<typeof usePdfPagination>
|
||||||
|
|
||||||
|
const wrapper = mount(
|
||||||
|
defineComponent({
|
||||||
|
setup() {
|
||||||
|
pagination = usePdfPagination({
|
||||||
|
mode,
|
||||||
|
scrollState: ref({ currentPage, totalPages }),
|
||||||
|
scroll,
|
||||||
|
touchPageTurningEnabled,
|
||||||
|
onActivity,
|
||||||
|
})
|
||||||
|
return () => h('div')
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return { wrapper, pagination, touchPageTurningEnabled, onActivity, scrollToPage }
|
||||||
|
}
|
||||||
|
|
||||||
|
function swipe(pagination: ReturnType<typeof usePdfPagination>, from: [number, number], to: [number, number]) {
|
||||||
|
const start = touchEvent('touchstart', [{ clientX: from[0], clientY: from[1] }])
|
||||||
|
const end = touchEvent('touchend', [{ clientX: to[0], clientY: to[1] }])
|
||||||
|
pagination.handleTouchStart(start)
|
||||||
|
pagination.handleTouchEnd(end)
|
||||||
|
return { start, end }
|
||||||
|
}
|
||||||
|
|
||||||
describe('usePdfPagination', () => {
|
describe('usePdfPagination', () => {
|
||||||
it('stays stable while EmbedPDF unloads the document scope', () => {
|
it('stays stable while EmbedPDF unloads the document scope', () => {
|
||||||
const scroll = ref({
|
const scroll = ref({
|
||||||
@@ -23,6 +101,7 @@ describe('usePdfPagination', () => {
|
|||||||
mode: ref('page'),
|
mode: ref('page'),
|
||||||
scrollState: ref({ currentPage: 20, totalPages: 240 }),
|
scrollState: ref({ currentPage: 20, totalPages: 240 }),
|
||||||
scroll,
|
scroll,
|
||||||
|
touchPageTurningEnabled: ref(true),
|
||||||
onActivity: () => {},
|
onActivity: () => {},
|
||||||
})
|
})
|
||||||
return () => h('div')
|
return () => h('div')
|
||||||
@@ -36,4 +115,109 @@ describe('usePdfPagination', () => {
|
|||||||
|
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ name: 'previous', from: [100, 100], to: [180, 100], pageNumber: 1 },
|
||||||
|
{ name: 'next', from: [180, 100], to: [100, 100], pageNumber: 3 },
|
||||||
|
] as const)('turns to the $name page for a horizontal pan swipe', ({ from, to, pageNumber }) => {
|
||||||
|
const harness = mountPagination()
|
||||||
|
const { end } = swipe(harness.pagination, [...from], [...to])
|
||||||
|
|
||||||
|
expect(end.defaultPrevented).toBe(true)
|
||||||
|
expect(harness.scrollToPage).toHaveBeenCalledExactlyOnceWith({ pageNumber, behavior: 'smooth' })
|
||||||
|
expect(harness.onActivity).toHaveBeenCalledOnce()
|
||||||
|
|
||||||
|
harness.wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('turns the page without preventing a non-cancelable touchend', () => {
|
||||||
|
const harness = mountPagination()
|
||||||
|
const start = touchEvent('touchstart', [{ clientX: 180, clientY: 100 }])
|
||||||
|
const end = touchEvent('touchend', [{ clientX: 100, clientY: 100 }], false)
|
||||||
|
|
||||||
|
harness.pagination.handleTouchStart(start)
|
||||||
|
harness.pagination.handleTouchEnd(end)
|
||||||
|
|
||||||
|
expect(end.defaultPrevented).toBe(false)
|
||||||
|
expect(harness.scrollToPage).toHaveBeenCalledExactlyOnceWith({ pageNumber: 3, behavior: 'smooth' })
|
||||||
|
|
||||||
|
harness.wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('leaves touch drags available to text selection when the pan tool is inactive', () => {
|
||||||
|
const harness = mountPagination({ touchTurning: false })
|
||||||
|
const first = swipe(harness.pagination, [180, 100], [100, 100])
|
||||||
|
|
||||||
|
expect(first.end.defaultPrevented).toBe(false)
|
||||||
|
expect(harness.scrollToPage).not.toHaveBeenCalled()
|
||||||
|
expect(harness.onActivity).toHaveBeenCalledOnce()
|
||||||
|
|
||||||
|
harness.touchPageTurningEnabled.value = true
|
||||||
|
const staleEnd = touchEvent('touchend', [{ clientX: 100, clientY: 100 }])
|
||||||
|
harness.pagination.handleTouchEnd(staleEnd)
|
||||||
|
|
||||||
|
expect(staleEnd.defaultPrevented).toBe(false)
|
||||||
|
expect(harness.scrollToPage).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
harness.wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not finish a page turn after switching from pan to text selection mid-gesture', () => {
|
||||||
|
const harness = mountPagination()
|
||||||
|
harness.pagination.handleTouchStart(touchEvent('touchstart', [{ clientX: 180, clientY: 100 }]))
|
||||||
|
harness.touchPageTurningEnabled.value = false
|
||||||
|
const end = touchEvent('touchend', [{ clientX: 100, clientY: 100 }])
|
||||||
|
harness.pagination.handleTouchEnd(end)
|
||||||
|
|
||||||
|
expect(end.defaultPrevented).toBe(false)
|
||||||
|
expect(harness.scrollToPage).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
harness.wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ name: 'short', from: [100, 100], to: [159, 100] },
|
||||||
|
{ name: 'mostly vertical', from: [100, 100], to: [180, 180] },
|
||||||
|
] as const)('ignores $name touch gestures', ({ from, to }) => {
|
||||||
|
const harness = mountPagination()
|
||||||
|
const { end } = swipe(harness.pagination, [...from], [...to])
|
||||||
|
|
||||||
|
expect(end.defaultPrevented).toBe(false)
|
||||||
|
expect(harness.scrollToPage).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
harness.wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ignores touch page turns outside paginated mode', () => {
|
||||||
|
const harness = mountPagination({ scrollMode: 'vertical' })
|
||||||
|
const { end } = swipe(harness.pagination, [180, 100], [100, 100])
|
||||||
|
|
||||||
|
expect(end.defaultPrevented).toBe(false)
|
||||||
|
expect(harness.scrollToPage).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
harness.wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clears a pending swipe after touch cancellation or a multi-touch start', () => {
|
||||||
|
const harness = mountPagination()
|
||||||
|
harness.pagination.handleTouchStart(touchEvent('touchstart', [{ clientX: 180, clientY: 100 }]))
|
||||||
|
harness.pagination.handleTouchCancel()
|
||||||
|
const canceledEnd = touchEvent('touchend', [{ clientX: 100, clientY: 100 }])
|
||||||
|
harness.pagination.handleTouchEnd(canceledEnd)
|
||||||
|
|
||||||
|
harness.pagination.handleTouchStart(
|
||||||
|
touchEvent('touchstart', [
|
||||||
|
{ clientX: 180, clientY: 100 },
|
||||||
|
{ clientX: 200, clientY: 100 },
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
const multiTouchEnd = touchEvent('touchend', [{ clientX: 100, clientY: 100 }])
|
||||||
|
harness.pagination.handleTouchEnd(multiTouchEnd)
|
||||||
|
|
||||||
|
expect(canceledEnd.defaultPrevented).toBe(false)
|
||||||
|
expect(multiTouchEnd.defaultPrevented).toBe(false)
|
||||||
|
expect(harness.scrollToPage).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
harness.wrapper.unmount()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import type { DocumentState } from '@embedpdf/core'
|
|||||||
import { PdfErrorCode } from '@embedpdf/models'
|
import { PdfErrorCode } from '@embedpdf/models'
|
||||||
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/vue'
|
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/vue'
|
||||||
import { DocumentContent } from '@embedpdf/plugin-document-manager/vue'
|
import { DocumentContent } from '@embedpdf/plugin-document-manager/vue'
|
||||||
import { usePan } from '@embedpdf/plugin-pan/vue'
|
|
||||||
import { useRotate } from '@embedpdf/plugin-rotate/vue'
|
import { useRotate } from '@embedpdf/plugin-rotate/vue'
|
||||||
import { ScrollStrategy, useScroll, useScrollCapability } from '@embedpdf/plugin-scroll/vue'
|
import { ScrollStrategy, useScroll, useScrollCapability } from '@embedpdf/plugin-scroll/vue'
|
||||||
import { useSelectionCapability } from '@embedpdf/plugin-selection/vue'
|
import { useSelectionCapability } from '@embedpdf/plugin-selection/vue'
|
||||||
@@ -22,6 +21,7 @@ import PdfReaderToolbar from './PdfReaderToolbar.vue'
|
|||||||
import { fromRotation, safeExternalPdfUrl } from '../pdf-viewer-utils'
|
import { fromRotation, safeExternalPdfUrl } from '../pdf-viewer-utils'
|
||||||
import { usePdfFullscreenChrome } from '../composables/usePdfFullscreenChrome'
|
import { usePdfFullscreenChrome } from '../composables/usePdfFullscreenChrome'
|
||||||
import { usePdfHighlights } from '../composables/usePdfHighlights'
|
import { usePdfHighlights } from '../composables/usePdfHighlights'
|
||||||
|
import { usePdfInteractionMode } from '../composables/usePdfInteractionMode'
|
||||||
import { usePdfPagination } from '../composables/usePdfPagination'
|
import { usePdfPagination } from '../composables/usePdfPagination'
|
||||||
import { usePdfResponsiveSpread } from '../composables/usePdfResponsiveSpread'
|
import { usePdfResponsiveSpread } from '../composables/usePdfResponsiveSpread'
|
||||||
import { usePdfSidebarLayout, type PdfSidebarTab } from '../composables/usePdfSidebarLayout'
|
import { usePdfSidebarLayout, type PdfSidebarTab } from '../composables/usePdfSidebarLayout'
|
||||||
@@ -52,7 +52,7 @@ const { provides: scrollCapability } = useScrollCapability()
|
|||||||
const { state: zoomState, provides: zoom } = useZoom(() => props.documentId)
|
const { state: zoomState, provides: zoom } = useZoom(() => props.documentId)
|
||||||
const { provides: spread } = useSpread(() => props.documentId)
|
const { provides: spread } = useSpread(() => props.documentId)
|
||||||
const { rotation, provides: rotate } = useRotate(() => props.documentId)
|
const { rotation, provides: rotate } = useRotate(() => props.documentId)
|
||||||
const { isPanning, provides: pan } = usePan(() => props.documentId)
|
const { isPanning, togglePan, activateTextSelection } = usePdfInteractionMode(() => props.documentId)
|
||||||
const { provides: selectionCapability } = useSelectionCapability()
|
const { provides: selectionCapability } = useSelectionCapability()
|
||||||
const { provides: annotationCapability } = useAnnotationCapability()
|
const { provides: annotationCapability } = useAnnotationCapability()
|
||||||
const { isFullscreen, isFullscreenSupported, toggleFullscreen } = useFullscreen()
|
const { isFullscreen, isFullscreenSupported, toggleFullscreen } = useFullscreen()
|
||||||
@@ -91,10 +91,12 @@ const {
|
|||||||
handleWheel: handleViewportWheel,
|
handleWheel: handleViewportWheel,
|
||||||
handleTouchStart,
|
handleTouchStart,
|
||||||
handleTouchEnd,
|
handleTouchEnd,
|
||||||
|
handleTouchCancel,
|
||||||
} = usePdfPagination({
|
} = usePdfPagination({
|
||||||
mode: currentScrollMode,
|
mode: currentScrollMode,
|
||||||
scrollState,
|
scrollState,
|
||||||
scroll,
|
scroll,
|
||||||
|
touchPageTurningEnabled: isPanning,
|
||||||
onActivity: revealHeader,
|
onActivity: revealHeader,
|
||||||
})
|
})
|
||||||
const { apply: applyResponsiveSpread } = usePdfResponsiveSpread(viewerSurface, currentSpreadPreference, spread)
|
const { apply: applyResponsiveSpread } = usePdfResponsiveSpread(viewerSurface, currentSpreadPreference, spread)
|
||||||
@@ -135,11 +137,11 @@ function handleSidebarWidth(width: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleTogglePan() {
|
function handleTogglePan() {
|
||||||
pan.value?.togglePan()
|
togglePan()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSelectTool() {
|
function handleSelectTool() {
|
||||||
pan.value?.disablePan()
|
activateTextSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSettingsOpen(open: boolean) {
|
function handleSettingsOpen(open: boolean) {
|
||||||
@@ -475,6 +477,7 @@ onUnmounted(() => {
|
|||||||
@wheel="handleViewportWheel"
|
@wheel="handleViewportWheel"
|
||||||
@touchstart="handleTouchStart"
|
@touchstart="handleTouchStart"
|
||||||
@touchend="handleTouchEnd"
|
@touchend="handleTouchEnd"
|
||||||
|
@touchcancel="handleTouchCancel"
|
||||||
>
|
>
|
||||||
<DocumentContent :document-id="props.documentId">
|
<DocumentContent :document-id="props.documentId">
|
||||||
<template #default="{ documentState, isLoading, isError, isLoaded }">
|
<template #default="{ documentState, isLoading, isError, isLoaded }">
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { MaybeRefOrGetter } from 'vue'
|
||||||
|
import { useInteractionManager } from '@embedpdf/plugin-interaction-manager/vue'
|
||||||
|
import { usePan } from '@embedpdf/plugin-pan/vue'
|
||||||
|
|
||||||
|
const TEXT_SELECTION_MODE = 'pointerMode'
|
||||||
|
|
||||||
|
export function usePdfInteractionMode(documentId: MaybeRefOrGetter<string>) {
|
||||||
|
const { isPanning, provides: pan } = usePan(documentId)
|
||||||
|
const { provides: interactionManager } = useInteractionManager(documentId)
|
||||||
|
|
||||||
|
function togglePan() {
|
||||||
|
pan.value?.togglePan()
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateTextSelection() {
|
||||||
|
// EmbedPDF makes pan the default on touch devices, so disablePan() would reactivate pan.
|
||||||
|
interactionManager.value?.activate(TEXT_SELECTION_MODE)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isPanning, togglePan, activateTextSelection }
|
||||||
|
}
|
||||||
@@ -9,10 +9,11 @@ export interface UsePdfPaginationOptions {
|
|||||||
mode: ReadonlyRef<PdfReaderSettings['scrollMode']>
|
mode: ReadonlyRef<PdfReaderSettings['scrollMode']>
|
||||||
scrollState: ReadonlyRef<{ currentPage: number; totalPages: number }>
|
scrollState: ReadonlyRef<{ currentPage: number; totalPages: number }>
|
||||||
scroll: ReadonlyRef<Readonly<ScrollScope> | null>
|
scroll: ReadonlyRef<Readonly<ScrollScope> | null>
|
||||||
|
touchPageTurningEnabled: ReadonlyRef<boolean>
|
||||||
onActivity: () => void
|
onActivity: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePdfPagination({ mode, scrollState, scroll, onActivity }: UsePdfPaginationOptions) {
|
export function usePdfPagination({ mode, scrollState, scroll, touchPageTurningEnabled, onActivity }: UsePdfPaginationOptions) {
|
||||||
let wheelDelta = 0
|
let wheelDelta = 0
|
||||||
let wheelUnlockTimer: ReturnType<typeof setTimeout> | null = null
|
let wheelUnlockTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
let touchStart: { x: number; y: number } | null = null
|
let touchStart: { x: number; y: number } | null = null
|
||||||
@@ -119,7 +120,7 @@ export function usePdfPagination({ mode, scrollState, scroll, onActivity }: UseP
|
|||||||
|
|
||||||
function handleTouchStart(event: TouchEvent) {
|
function handleTouchStart(event: TouchEvent) {
|
||||||
onActivity()
|
onActivity()
|
||||||
if (event.touches.length !== 1) {
|
if (mode.value !== 'page' || !touchPageTurningEnabled.value || event.touches.length !== 1) {
|
||||||
touchStart = null
|
touchStart = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -127,7 +128,7 @@ export function usePdfPagination({ mode, scrollState, scroll, onActivity }: UseP
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleTouchEnd(event: TouchEvent) {
|
function handleTouchEnd(event: TouchEvent) {
|
||||||
if (mode.value !== 'page' || !touchStart || event.changedTouches.length !== 1) {
|
if (mode.value !== 'page' || !touchPageTurningEnabled.value || !touchStart || event.changedTouches.length !== 1) {
|
||||||
touchStart = null
|
touchStart = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -138,13 +139,17 @@ export function usePdfPagination({ mode, scrollState, scroll, onActivity }: UseP
|
|||||||
if (Math.abs(deltaX) < 60 || Math.abs(deltaX) < Math.abs(deltaY) * 1.2) return
|
if (Math.abs(deltaX) < 60 || Math.abs(deltaX) < Math.abs(deltaY) * 1.2) return
|
||||||
const direction: -1 | 1 = deltaX > 0 ? -1 : 1
|
const direction: -1 | 1 = deltaX > 0 ? -1 : 1
|
||||||
if (!canTurnPage(direction, 'horizontal')) return
|
if (!canTurnPage(direction, 'horizontal')) return
|
||||||
event.preventDefault()
|
if (event.cancelable) event.preventDefault()
|
||||||
turnPage(direction)
|
turnPage(direction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleTouchCancel() {
|
||||||
|
touchStart = null
|
||||||
|
}
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (wheelUnlockTimer) clearTimeout(wheelUnlockTimer)
|
if (wheelUnlockTimer) clearTimeout(wheelUnlockTimer)
|
||||||
})
|
})
|
||||||
|
|
||||||
return { pageRange, getPageRange, goToPage, previousPage, nextPage, handleWheel, handleTouchStart, handleTouchEnd }
|
return { pageRange, getPageRange, goToPage, previousPage, nextPage, handleWheel, handleTouchStart, handleTouchEnd, handleTouchCancel }
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+9
-9
@@ -516,8 +516,8 @@ importers:
|
|||||||
specifier: ^11.1.0
|
specifier: ^11.1.0
|
||||||
version: 11.1.0(chokidar@5.0.0)(prettier@3.9.6)(typescript@6.0.3)
|
version: 11.1.0(chokidar@5.0.0)(prettier@3.9.6)(typescript@6.0.3)
|
||||||
'@nestjs/testing':
|
'@nestjs/testing':
|
||||||
specifier: ^12.0.1
|
specifier: ^11.2.3
|
||||||
version: 12.0.1(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.3)
|
version: 11.2.3(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.3)
|
||||||
'@swc/cli':
|
'@swc/cli':
|
||||||
specifier: ^0.8.1
|
specifier: ^0.8.1
|
||||||
version: 0.8.1(@swc/core@1.16.1(@swc/helpers@0.5.23))(chokidar@5.0.0)(supports-color@10.2.2)
|
version: 0.8.1(@swc/core@1.16.1(@swc/helpers@0.5.23))(chokidar@5.0.0)(supports-color@10.2.2)
|
||||||
@@ -3090,13 +3090,13 @@ packages:
|
|||||||
typeorm:
|
typeorm:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@nestjs/testing@12.0.1':
|
'@nestjs/testing@11.2.3':
|
||||||
resolution: {integrity: sha512-prf3fsUaoKS6t04XcQt5aSTp68Po1GxUcbEBPv+3iqAef07hKQEO15r0jmmwA+rWZfONYT1UcV3YIylR++/tCg==}
|
resolution: {integrity: sha512-7ANDWlkm8Xw4CYIhCNZhtBzANsQUKqjteA2yx/6sjqGyWhekeBKz8wgCJykm0vo+ltrg6U34dZlm2NgiRcNHPQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@nestjs/common': ^12.0.0
|
'@nestjs/common': ^11.0.0
|
||||||
'@nestjs/core': ^12.0.0
|
'@nestjs/core': ^11.0.0
|
||||||
'@nestjs/microservices': ^12.0.0
|
'@nestjs/microservices': ^11.0.0
|
||||||
'@nestjs/platform-express': ^12.0.0
|
'@nestjs/platform-express': ^11.0.0
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
'@nestjs/microservices':
|
'@nestjs/microservices':
|
||||||
optional: true
|
optional: true
|
||||||
@@ -11851,7 +11851,7 @@ snapshots:
|
|||||||
reflect-metadata: 0.2.2
|
reflect-metadata: 0.2.2
|
||||||
rxjs: 7.8.2
|
rxjs: 7.8.2
|
||||||
|
|
||||||
'@nestjs/testing@12.0.1(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.3)':
|
'@nestjs/testing@11.2.3(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/core@11.2.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nestjs/common': 11.2.3(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2)
|
'@nestjs/common': 11.2.3(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2)
|
||||||
'@nestjs/core': 11.2.3(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.2.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/core': 11.2.3(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)(supports-color@10.2.2))(@nestjs/websockets@11.2.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
|||||||
+1
-1
@@ -114,7 +114,7 @@
|
|||||||
"@ffprobe-installer/ffprobe": "^2.1.2",
|
"@ffprobe-installer/ffprobe": "^2.1.2",
|
||||||
"@nestjs/cli": "^11.0.24",
|
"@nestjs/cli": "^11.0.24",
|
||||||
"@nestjs/schematics": "^11.1.0",
|
"@nestjs/schematics": "^11.1.0",
|
||||||
"@nestjs/testing": "^12.0.1",
|
"@nestjs/testing": "^11.2.3",
|
||||||
"@swc/cli": "^0.8.1",
|
"@swc/cli": "^0.8.1",
|
||||||
"@swc/core": "^1.16.1",
|
"@swc/core": "^1.16.1",
|
||||||
"@types/bcrypt": "^6.0.0",
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user