diff --git a/README.md b/README.md index 9d49e1a..d47901c 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,12 @@ Example: ### Events - `pdf-elements:end-init` - Emitted when PDF is loaded +- `pdf-elements:adding-ended` - Emitted when interactive placement ends. Payload: `{ reason: 'placed', object, docIndex, pageIndex }` on success or `{ reason: 'cancelled' }` when the placement is cancelled. + +### Exposed methods + +- `startAddingElement(templateObject)` - Starts interactive placement mode. +- `cancelAdding()` - Cancels the current placement session and emits `pdf-elements:adding-ended` with `{ reason: 'cancelled' }` when a session was active. ### Slots diff --git a/package-lock.json b/package-lock.json index a20a6f6..3617574 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@libresign/pdf-elements", - "version": "1.1.6", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@libresign/pdf-elements", - "version": "1.1.6", + "version": "1.2.0", "license": "AGPL-3.0-or-later", "dependencies": { "pdfjs-dist": "^5.4.624", diff --git a/package.json b/package.json index f240351..5e46917 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@libresign/pdf-elements", "description": "PDF viewer with draggable and resizable element overlays for Vue 3", - "version": "1.1.6", + "version": "1.2.0", "author": "LibreCode ", "private": false, "main": "dist/index.mjs", diff --git a/src/components/PDFElements.vue b/src/components/PDFElements.vue index 28769dc..6e06811 100644 --- a/src/components/PDFElements.vue +++ b/src/components/PDFElements.vue @@ -154,11 +154,11 @@ import { getViewportWindow, isPageInViewport } from '../utils/pageBounds' import { applyScaleToDocs } from '../utils/zoom' import { objectIdExistsInDoc, findObjectPageIndex, updateObjectInDoc, removeObjectFromDoc } from '../utils/objectStore' import { getCachedMeasurement } from '../utils/measurements' -import type { PDFDocumentEntry, PDFElementObject } from '../types' +import type { PDFDocumentEntry, PDFElementObject, PDFElementsAddingEndedPayload } from '../types' export default defineComponent({ name: 'PDFElements', - emits: ['pdf-elements:end-init', 'pdf-elements:delete-object', 'pdf-elements:object-click'], + emits: ['pdf-elements:end-init', 'pdf-elements:delete-object', 'pdf-elements:object-click', 'pdf-elements:adding-ended'], components: { PDFPage, DraggableElement, @@ -738,10 +738,14 @@ export default defineComponent({ handleKeyDown(event) { if (event.key === 'Escape' && this.isAddingMode) { - this.cancelAdding() + this.cancelAdding({ reason: 'cancelled' }) } }, + emitAddingEnded(payload: PDFElementsAddingEndedPayload) { + this.$emit('pdf-elements:adding-ended', payload) + }, + getOverlayAriaLabel(docIndex, pageIndex) { const doc = this.pdfDocuments?.[docIndex] const docName = doc?.name ?? `Document ${docIndex + 1}` @@ -827,7 +831,7 @@ export default defineComponent({ if (objectToAdd.x < 0 || objectToAdd.y < 0 || objectToAdd.x + objectToAdd.width > pageWidth || objectToAdd.y + objectToAdd.height > pageHeight) { - this.cancelAdding() + this.cancelAdding({ reason: 'cancelled' }) return } @@ -837,7 +841,12 @@ export default defineComponent({ const docIndex = this.previewPageDocIndex const objectId = objectToAdd.id - this.cancelAdding() + this.cancelAdding({ + reason: 'placed', + object: objectToAdd, + docIndex, + pageIndex, + }) this.$nextTick(() => { const refKey = `draggable${docIndex}-${pageIndex}-${objectId}` @@ -848,11 +857,17 @@ export default defineComponent({ }) }, - cancelAdding() { + cancelAdding(payload: PDFElementsAddingEndedPayload = { reason: 'cancelled' }) { + const hadPendingAdd = this.isAddingMode || this.previewElement !== null || this.previewVisible + this.isAddingMode = false this.previewElement = null this.previewVisible = false this.detachAddingListeners() + + if (hadPendingAdd) { + this.emitAddingEnded(payload) + } }, generateObjectId() { const counter = this.nextObjectCounter++ diff --git a/src/index.ts b/src/index.ts index f58b856..c23835c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,15 @@ import type { App } from 'vue' import PDFElements from './components/PDFElements.vue' export { ensureWorkerReady, setWorkerPath } from './utils/asyncReader' -export type { PDFDocumentEntry, PDFElementObject, PDFElementsPublicApi } from './types' +export type { + PDFDocumentEntry, + PDFElementObject, + PDFElementsAddingEndedCancelledPayload, + PDFElementsAddingEndedPayload, + PDFElementsAddingEndedPlacedPayload, + PDFElementsAddingEndedReason, + PDFElementsPublicApi, +} from './types' const install = (app: App) => { const name = PDFElements.name || 'PDFElements' diff --git a/src/types.ts b/src/types.ts index 42befe1..2e76326 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,6 +14,23 @@ export interface PDFElementObject { [key: string]: unknown } +export type PDFElementsAddingEndedReason = 'placed' | 'cancelled' + +export interface PDFElementsAddingEndedPlacedPayload { + reason: 'placed' + object: PDFElementObject + docIndex: number + pageIndex: number +} + +export interface PDFElementsAddingEndedCancelledPayload { + reason: 'cancelled' +} + +export type PDFElementsAddingEndedPayload = + | PDFElementsAddingEndedPlacedPayload + | PDFElementsAddingEndedCancelledPayload + export interface PDFDocumentEntry { name: string file: unknown @@ -32,4 +49,5 @@ export interface PDFElementsPublicApi { updateObject: (docIndex: number, objectId: string, payload: Partial) => void deleteObject: (docIndex: number, objectId: string) => void duplicateObject: (docIndex: number, objectId: string) => void + cancelAdding: () => void } diff --git a/tests/components/PDFElements.spec.ts b/tests/components/PDFElements.spec.ts index 15e0d65..707ca54 100644 --- a/tests/components/PDFElements.spec.ts +++ b/tests/components/PDFElements.spec.ts @@ -297,7 +297,7 @@ describe('PDFElements business rules', () => { }) it('finishes adding only when preview is visible and within bounds', () => { - const { ctx } = makeWrapper() + const { wrapper, ctx } = makeWrapper() const doc = makeDoc() ctx.pdfDocuments = [doc] @@ -316,6 +316,17 @@ describe('PDFElements business rules', () => { ctx.finishAdding() expect(doc.allObjects[0].length).toBe(1) + expect(wrapper.emitted()['pdf-elements:adding-ended']?.[0][0]).toEqual({ + reason: 'placed', + object: expect.objectContaining({ + x: 10, + y: 10, + width: 20, + height: 20, + }), + docIndex: 0, + pageIndex: 0, + }) ctx.isAddingMode = true ctx.previewVisible = true @@ -324,6 +335,9 @@ describe('PDFElements business rules', () => { ctx.finishAdding() expect(doc.allObjects[0].length).toBe(1) + expect(wrapper.emitted()['pdf-elements:adding-ended']?.[1][0]).toEqual({ + reason: 'cancelled', + }) }) it('uses changedTouches coordinates on touchend pointer position', () => { @@ -368,8 +382,8 @@ describe('PDFElements business rules', () => { expect(doc.allObjects[0][0].y).toBe(20) }) - it('cancels adding resets preview state', () => { - const { ctx } = makeWrapper() + it('cancels adding resets preview state and emits cancellation once', () => { + const { wrapper, ctx } = makeWrapper() ctx.isAddingMode = true ctx.previewElement = { width: 10, height: 10 } ctx.previewVisible = true @@ -379,10 +393,19 @@ describe('PDFElements business rules', () => { expect(ctx.isAddingMode).toBe(false) expect(ctx.previewElement).toBeNull() expect(ctx.previewVisible).toBe(false) + expect(wrapper.emitted()['pdf-elements:adding-ended']).toEqual([[{ reason: 'cancelled' }]]) }) - it('cancels adding on escape key', () => { - const { ctx } = makeWrapper() + it('does not emit cancellation when no add session is active', () => { + const { wrapper, ctx } = makeWrapper() + + ctx.cancelAdding() + + expect(wrapper.emitted()['pdf-elements:adding-ended']).toBeUndefined() + }) + + it('cancels adding on escape key and emits cancellation', () => { + const { wrapper, ctx } = makeWrapper() ctx.isAddingMode = true ctx.previewElement = { width: 10, height: 10 } ctx.previewVisible = true @@ -390,6 +413,7 @@ describe('PDFElements business rules', () => { ctx.handleKeyDown({ key: 'Escape' }) expect(ctx.isAddingMode).toBe(false) + expect(wrapper.emitted()['pdf-elements:adding-ended']).toEqual([[{ reason: 'cancelled' }]]) }) it('starts adding element and prepares preview state', () => {