Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ A Vue 2 component for rendering PDFs with draggable and resizable element overla
| `showSelectionHandles` | Boolean | `true` | Show resize/move handles on selected elements |
| `showElementActions` | Boolean | `true` | Show action buttons on selected elements |
| `readOnly` | Boolean | `false` | Disable drag, resize, and actions for elements |
| `ignoreClickOutsideSelectors` | Array | `[]` | CSS selectors that keep the selection active when clicking outside the element |
| `pageCountFormat` | String | `'{currentPage} of {totalPages}'` | Format string for page counter |
| `autoFitZoom` | Boolean | `false` | Automatically adjust zoom to fit viewport on window resize |

Expand All @@ -37,4 +38,3 @@ A Vue 2 component for rendering PDFs with draggable and resizable element overla
- `element-{type}` - Custom element rendering (e.g., `element-signature`)
- `custom` - Fallback for elements without specific type
- `actions` - Custom action buttons

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@libresign/pdf-elements",
"description": "PDF viewer with draggable and resizable element overlays for Vue 2",
"version": "0.2.5",
"version": "0.3.0",
"author": "LibreCode <contact@librecode.coop>",
"private": false,
"main": "dist/pdf-elements.umd.js",
Expand Down
25 changes: 23 additions & 2 deletions src/components/DraggableElement.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ SPDX-License-Identifier: AGPL-3.0-or-later
class="actions-toolbar"
:style="toolbarStyle"
>
<slot name="actions" :object="object" :onDelete="onDelete">
<slot name="actions" :object="object" :onDelete="onDelete" :onDuplicate="onDuplicate">
<button class="action-btn" type="button" title="Duplicate" @click.stop="onDuplicate">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M16 1H6a2 2 0 0 0-2 2v12h2V3h10V1zm3 4H10a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2zm0 16H10V7h9v14z"/>
</svg>
</button>
<button class="action-btn" type="button" title="Delete" @click.stop="onDelete">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M6.5 1h3a.5.5 0 0 1 .5.5v1H6v-1a.5.5 0 0 1 .5-.5ZM11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3A1.5 1.5 0 0 0 5 1.5v1H2.5a.5.5 0 0 0 0 1h.5v10.5A1.5 1.5 0 0 0 4.5 15h7a1.5 1.5 0 0 0 1.5-1.5V3.5h.5a.5.5 0 0 0 0-1H11Zm1 1v10.5a.5.5 0 0 1-.5.5h-7a.5.5 0 0 1-.5-.5V3.5h8Z"/>
Expand Down Expand Up @@ -77,6 +82,10 @@ export default {
type: Function,
default: () => {},
},
onDuplicate: {
type: Function,
default: () => {},
},
onDragStart: {
type: Function,
default: () => {},
Expand Down Expand Up @@ -124,7 +133,11 @@ export default {
readOnly: {
type: Boolean,
default: false,
}
},
ignoreClickOutsideSelectors: {
type: Array,
default: () => [],
},
},
data() {
return {
Expand Down Expand Up @@ -237,6 +250,14 @@ export default {
this.startDrag(event)
},
handleClickOutside(event) {
const selectors = Array.isArray(this.ignoreClickOutsideSelectors)
? this.ignoreClickOutsideSelectors
: []
for (const selector of selectors) {
if (selector && event?.target?.closest?.(selector)) {
return
}
}
if (this.$el && !this.$el.contains(event.target)) {
this.isSelected = false
}
Expand Down
65 changes: 65 additions & 0 deletions src/components/PDFElements.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
:read-only="readOnly"
:on-update="(payload) => updateObject(docIndex, object.id, payload)"
:on-delete="() => deleteObject(docIndex, object.id)"
:on-duplicate="() => duplicateObject(docIndex, object.id)"
:on-drag-start="(mouseX, mouseY, pointerOffset, dragShift) => startDraggingElement(docIndex, pIndex, object, mouseX, mouseY, pointerOffset, dragShift)"
:on-drag-move="updateDraggingPosition"
:on-drag-end="stopDraggingElement"
Expand All @@ -71,6 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
:global-drag-page-index="draggingPageIndex"
:show-selection-ui="showSelectionHandles && !hideSelectionUI && object.resizable !== false"
:show-default-actions="showElementActions && !hideSelectionUI"
:ignore-click-outside-selectors="ignoreClickOutsideSelectors"
>
<template #default="slotProps">
<slot
Expand All @@ -92,6 +94,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
name="actions"
:object="slotProps.object"
:onDelete="slotProps.onDelete"
:onDuplicate="slotProps.onDuplicate"
/>
</template>
</DraggableElement>
Expand Down Expand Up @@ -191,6 +194,10 @@ export default {
type: Boolean,
default: false,
},
ignoreClickOutsideSelectors: {
type: Array,
default: () => [],
},
pageCountFormat: {
type: String,
default: '{currentPage} of {totalPages}',
Expand Down Expand Up @@ -940,6 +947,64 @@ export default {
})
}
},
duplicateObject(docIndex, objectId) {
if (docIndex < 0 || docIndex >= this.pdfDocuments.length) return
const doc = this.pdfDocuments[docIndex]

const cacheKey = `${docIndex}-${objectId}`
let pageIndex = this.objectIndexCache[cacheKey]

if (pageIndex === undefined) {
pageIndex = findObjectPageIndex(doc, objectId)
if (pageIndex !== undefined) {
this.objectIndexCache[cacheKey] = pageIndex
}
}

if (pageIndex === undefined) return

const sourceObject = doc.allObjects[pageIndex]?.find(o => o.id === objectId)
if (!sourceObject) return

const { width: pageWidth, height: pageHeight } = this.getPageSize(docIndex, pageIndex)
const offset = 12
const { x, y } = clampPosition(
sourceObject.x + offset,
sourceObject.y + offset,
sourceObject.width,
sourceObject.height,
pageWidth,
pageHeight,
)

let duplicatedSigner = sourceObject.signer
if (duplicatedSigner?.element && Object.prototype.hasOwnProperty.call(duplicatedSigner.element, 'elementId')) {
duplicatedSigner = {
...duplicatedSigner,
element: { ...duplicatedSigner.element },
}
delete duplicatedSigner.element.elementId
}

const duplicatedObject = {
...sourceObject,
id: this.generateObjectId(),
x,
y,
signer: duplicatedSigner,
}

doc.allObjects[pageIndex].push(duplicatedObject)
this.objectIndexCache[`${docIndex}-${duplicatedObject.id}`] = pageIndex

this.$nextTick(() => {
const refKey = `draggable${docIndex}-${pageIndex}-${duplicatedObject.id}`
const draggableRefs = this.$refs[refKey]
if (draggableRefs && Array.isArray(draggableRefs) && draggableRefs[0]) {
draggableRefs[0].isSelected = true
}
})
},

checkAndMoveObjectPage(docIndex, objectId, mouseX, mouseY) {
if (docIndex < 0 || docIndex >= this.pdfDocuments.length) return undefined
Expand Down