From b93cc2cb8fd042189b132bb04538b77d1dac0e44 Mon Sep 17 00:00:00 2001 From: Phillip Date: Tue, 31 Mar 2026 19:49:47 +0200 Subject: [PATCH] fix(touch): support touchend placement via changedTouches fallback Signed-off-by: Phillip --- src/components/PDFElements.vue | 133 ++++++++++++++------------- tests/components/PDFElements.spec.ts | 42 +++++++++ 2 files changed, 113 insertions(+), 62 deletions(-) diff --git a/src/components/PDFElements.vue b/src/components/PDFElements.vue index 949edbf..7f5e602 100644 --- a/src/components/PDFElements.vue +++ b/src/components/PDFElements.vue @@ -541,9 +541,10 @@ export default defineComponent({ }, getPointerPosition(event) { if (event?.type?.includes?.('touch')) { + const touch = event.touches?.[0] || event.changedTouches?.[0] return { - x: event.touches?.[0]?.clientX, - y: event.touches?.[0]?.clientY, + x: touch?.clientX, + y: touch?.clientY, } } return { @@ -551,6 +552,66 @@ export default defineComponent({ y: event?.clientY, } }, + updatePreviewFromClientPoint(cursorX, cursorY) { + let target = null + + if (this.lastHoverRect && + cursorX >= this.lastHoverRect.left && cursorX <= this.lastHoverRect.right && + cursorY >= this.lastHoverRect.top && cursorY <= this.lastHoverRect.bottom) { + target = { + docIndex: this.previewPageDocIndex, + pageIndex: this.previewPageIndex, + rect: this.lastHoverRect, + } + } else { + const rectEntries = this.getPageBoundsList().length + ? this.getPageBoundsList() + : Object.values(this.getPageBoundsMap()) + for (let i = 0; i < rectEntries.length; i++) { + const entry = rectEntries[i] + const rect = entry.rect + if (cursorX >= rect.left && cursorX <= rect.right && + cursorY >= rect.top && cursorY <= rect.bottom) { + target = entry + break + } + } + } + + if (!target) { + this.previewVisible = false + this.previewScale = { x: 1, y: 1 } + this.lastHoverRect = null + return false + } + + this.previewPageDocIndex = target.docIndex + this.previewPageIndex = target.pageIndex + this.lastHoverRect = target.rect + const canvasEl = this.getPageCanvasElement(target.docIndex, target.pageIndex) + const pagesScale = this.pdfDocuments[target.docIndex]?.pagesScale?.[target.pageIndex] || 1 + const renderWidth = canvasEl?.width || target.rect.width + const renderHeight = canvasEl?.height || target.rect.height + const layoutScaleX = renderWidth ? target.rect.width / renderWidth : 1 + const layoutScaleY = renderHeight ? target.rect.height / renderHeight : 1 + const relX = (cursorX - target.rect.left) / layoutScaleX / pagesScale + const relY = (cursorY - target.rect.top) / layoutScaleY / pagesScale + + const pageWidth = renderWidth / pagesScale + const pageHeight = renderHeight / pagesScale + this.previewScale.x = pagesScale + this.previewScale.y = pagesScale + let x = relX - this.previewElement.width / 2 + let y = relY - this.previewElement.height / 2 + + x = Math.max(0, Math.min(x, pageWidth - this.previewElement.width)) + y = Math.max(0, Math.min(y, pageHeight - this.previewElement.height)) + + this.previewPosition.x = x + this.previewPosition.y = y + this.previewVisible = true + return true + }, getDisplayedPageScale(docIndex, pageIndex) { void this.pageBoundsVersion @@ -628,65 +689,7 @@ export default defineComponent({ const pending = this.pendingHoverClientPos if (!pending) return - const cursorX = pending.x - const cursorY = pending.y - let target = null - - if (this.lastHoverRect && - cursorX >= this.lastHoverRect.left && cursorX <= this.lastHoverRect.right && - cursorY >= this.lastHoverRect.top && cursorY <= this.lastHoverRect.bottom) { - target = { - docIndex: this.previewPageDocIndex, - pageIndex: this.previewPageIndex, - rect: this.lastHoverRect, - } - } else { - const rectEntries = this.getPageBoundsList().length - ? this.getPageBoundsList() - : Object.values(this.getPageBoundsMap()) - for (let i = 0; i < rectEntries.length; i++) { - const entry = rectEntries[i] - const rect = entry.rect - if (cursorX >= rect.left && cursorX <= rect.right && - cursorY >= rect.top && cursorY <= rect.bottom) { - target = entry - break - } - } - } - - if (!target) { - this.previewVisible = false - this.previewScale = { x: 1, y: 1 } - this.lastHoverRect = null - return - } - - this.previewPageDocIndex = target.docIndex - this.previewPageIndex = target.pageIndex - this.lastHoverRect = target.rect - const canvasEl = this.getPageCanvasElement(target.docIndex, target.pageIndex) - const pagesScale = this.pdfDocuments[target.docIndex]?.pagesScale?.[target.pageIndex] || 1 - const renderWidth = canvasEl?.width || target.rect.width - const renderHeight = canvasEl?.height || target.rect.height - const layoutScaleX = renderWidth ? target.rect.width / renderWidth : 1 - const layoutScaleY = renderHeight ? target.rect.height / renderHeight : 1 - const relX = (cursorX - target.rect.left) / layoutScaleX / pagesScale - const relY = (cursorY - target.rect.top) / layoutScaleY / pagesScale - - const pageWidth = renderWidth / pagesScale - const pageHeight = renderHeight / pagesScale - this.previewScale.x = pagesScale - this.previewScale.y = pagesScale - let x = relX - this.previewElement.width / 2 - let y = relY - this.previewElement.height / 2 - - x = Math.max(0, Math.min(x, pageWidth - this.previewElement.width)) - y = Math.max(0, Math.min(y, pageHeight - this.previewElement.height)) - - this.previewPosition.x = x - this.previewPosition.y = y - this.previewVisible = true + this.updatePreviewFromClientPoint(pending.x, pending.y) }) }, handleOverlayClick(docIndex, pageIndex, event) { @@ -800,8 +803,14 @@ export default defineComponent({ this.cachePageBounds() }, - finishAdding() { + finishAdding(event) { if (!this.isAddingMode || !this.previewElement) return + if (!this.previewVisible && event) { + const { x, y } = this.getPointerPosition(event) + if (Number.isFinite(x) && Number.isFinite(y)) { + this.updatePreviewFromClientPoint(x, y) + } + } if (!this.previewVisible) return const objectToAdd = { diff --git a/tests/components/PDFElements.spec.ts b/tests/components/PDFElements.spec.ts index 452b147..15e0d65 100644 --- a/tests/components/PDFElements.spec.ts +++ b/tests/components/PDFElements.spec.ts @@ -326,6 +326,48 @@ describe('PDFElements business rules', () => { expect(doc.allObjects[0].length).toBe(1) }) + it('uses changedTouches coordinates on touchend pointer position', () => { + const { ctx } = makeWrapper() + const pointer = ctx.getPointerPosition({ + type: 'touchend', + touches: [], + changedTouches: [{ clientX: 44, clientY: 55 }], + }) + + expect(pointer).toEqual({ x: 44, y: 55 }) + }) + + it('places object on touchend even without prior touchmove', () => { + const { ctx } = makeWrapper() + const doc = makeDoc() + ctx.pdfDocuments = [doc] + ctx._pagesBoundingRects = { + '0-0': { + docIndex: 0, + pageIndex: 0, + rect: { left: 0, right: 100, top: 0, bottom: 100, width: 100, height: 100 }, + }, + } + ctx._pagesBoundingRectsList = Object.values(ctx._pagesBoundingRects) + + ctx.isAddingMode = true + ctx.previewVisible = false + ctx.previewElement = { width: 20, height: 20 } + ctx.previewPageDocIndex = 0 + ctx.previewPageIndex = 0 + ctx.previewPosition = { x: 0, y: 0 } + + ctx.finishAdding({ + type: 'touchend', + touches: [], + changedTouches: [{ clientX: 30, clientY: 30 }], + }) + + expect(doc.allObjects[0].length).toBe(1) + expect(doc.allObjects[0][0].x).toBe(20) + expect(doc.allObjects[0][0].y).toBe(20) + }) + it('cancels adding resets preview state', () => { const { ctx } = makeWrapper() ctx.isAddingMode = true