Skip to content

Commit 4a8e649

Browse files
authored
Merge pull request #26 from LibreSign/feat/duplicate-element
feat: duplicate element
2 parents 072066d + 9e499ea commit 4a8e649

4 files changed

Lines changed: 90 additions & 4 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ A Vue 2 component for rendering PDFs with draggable and resizable element overla
2525
| `showSelectionHandles` | Boolean | `true` | Show resize/move handles on selected elements |
2626
| `showElementActions` | Boolean | `true` | Show action buttons on selected elements |
2727
| `readOnly` | Boolean | `false` | Disable drag, resize, and actions for elements |
28+
| `ignoreClickOutsideSelectors` | Array | `[]` | CSS selectors that keep the selection active when clicking outside the element |
2829
| `pageCountFormat` | String | `'{currentPage} of {totalPages}'` | Format string for page counter |
2930
| `autoFitZoom` | Boolean | `false` | Automatically adjust zoom to fit viewport on window resize |
3031

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@libresign/pdf-elements",
33
"description": "PDF viewer with draggable and resizable element overlays for Vue 2",
4-
"version": "0.2.5",
4+
"version": "0.3.0",
55
"author": "LibreCode <contact@librecode.coop>",
66
"private": false,
77
"main": "dist/pdf-elements.umd.js",

src/components/DraggableElement.vue

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ SPDX-License-Identifier: AGPL-3.0-or-later
1010
class="actions-toolbar"
1111
:style="toolbarStyle"
1212
>
13-
<slot name="actions" :object="object" :onDelete="onDelete">
13+
<slot name="actions" :object="object" :onDelete="onDelete" :onDuplicate="onDuplicate">
14+
<button class="action-btn" type="button" title="Duplicate" @click.stop="onDuplicate">
15+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
16+
<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"/>
17+
</svg>
18+
</button>
1419
<button class="action-btn" type="button" title="Delete" @click.stop="onDelete">
1520
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
1621
<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"/>
@@ -77,6 +82,10 @@ export default {
7782
type: Function,
7883
default: () => {},
7984
},
85+
onDuplicate: {
86+
type: Function,
87+
default: () => {},
88+
},
8089
onDragStart: {
8190
type: Function,
8291
default: () => {},
@@ -124,7 +133,11 @@ export default {
124133
readOnly: {
125134
type: Boolean,
126135
default: false,
127-
}
136+
},
137+
ignoreClickOutsideSelectors: {
138+
type: Array,
139+
default: () => [],
140+
},
128141
},
129142
data() {
130143
return {
@@ -237,6 +250,14 @@ export default {
237250
this.startDrag(event)
238251
},
239252
handleClickOutside(event) {
253+
const selectors = Array.isArray(this.ignoreClickOutsideSelectors)
254+
? this.ignoreClickOutsideSelectors
255+
: []
256+
for (const selector of selectors) {
257+
if (selector && event?.target?.closest?.(selector)) {
258+
return
259+
}
260+
}
240261
if (this.$el && !this.$el.contains(event.target)) {
241262
this.isSelected = false
242263
}

src/components/PDFElements.vue

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
6060
:read-only="readOnly"
6161
:on-update="(payload) => updateObject(docIndex, object.id, payload)"
6262
:on-delete="() => deleteObject(docIndex, object.id)"
63+
:on-duplicate="() => duplicateObject(docIndex, object.id)"
6364
:on-drag-start="(mouseX, mouseY, pointerOffset, dragShift) => startDraggingElement(docIndex, pIndex, object, mouseX, mouseY, pointerOffset, dragShift)"
6465
:on-drag-move="updateDraggingPosition"
6566
:on-drag-end="stopDraggingElement"
@@ -71,6 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
7172
:global-drag-page-index="draggingPageIndex"
7273
:show-selection-ui="showSelectionHandles && !hideSelectionUI && object.resizable !== false"
7374
:show-default-actions="showElementActions && !hideSelectionUI"
75+
:ignore-click-outside-selectors="ignoreClickOutsideSelectors"
7476
>
7577
<template #default="slotProps">
7678
<slot
@@ -92,6 +94,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
9294
name="actions"
9395
:object="slotProps.object"
9496
:onDelete="slotProps.onDelete"
97+
:onDuplicate="slotProps.onDuplicate"
9598
/>
9699
</template>
97100
</DraggableElement>
@@ -191,6 +194,10 @@ export default {
191194
type: Boolean,
192195
default: false,
193196
},
197+
ignoreClickOutsideSelectors: {
198+
type: Array,
199+
default: () => [],
200+
},
194201
pageCountFormat: {
195202
type: String,
196203
default: '{currentPage} of {totalPages}',
@@ -940,6 +947,64 @@ export default {
940947
})
941948
}
942949
},
950+
duplicateObject(docIndex, objectId) {
951+
if (docIndex < 0 || docIndex >= this.pdfDocuments.length) return
952+
const doc = this.pdfDocuments[docIndex]
953+
954+
const cacheKey = `${docIndex}-${objectId}`
955+
let pageIndex = this.objectIndexCache[cacheKey]
956+
957+
if (pageIndex === undefined) {
958+
pageIndex = findObjectPageIndex(doc, objectId)
959+
if (pageIndex !== undefined) {
960+
this.objectIndexCache[cacheKey] = pageIndex
961+
}
962+
}
963+
964+
if (pageIndex === undefined) return
965+
966+
const sourceObject = doc.allObjects[pageIndex]?.find(o => o.id === objectId)
967+
if (!sourceObject) return
968+
969+
const { width: pageWidth, height: pageHeight } = this.getPageSize(docIndex, pageIndex)
970+
const offset = 12
971+
const { x, y } = clampPosition(
972+
sourceObject.x + offset,
973+
sourceObject.y + offset,
974+
sourceObject.width,
975+
sourceObject.height,
976+
pageWidth,
977+
pageHeight,
978+
)
979+
980+
let duplicatedSigner = sourceObject.signer
981+
if (duplicatedSigner?.element && Object.prototype.hasOwnProperty.call(duplicatedSigner.element, 'elementId')) {
982+
duplicatedSigner = {
983+
...duplicatedSigner,
984+
element: { ...duplicatedSigner.element },
985+
}
986+
delete duplicatedSigner.element.elementId
987+
}
988+
989+
const duplicatedObject = {
990+
...sourceObject,
991+
id: this.generateObjectId(),
992+
x,
993+
y,
994+
signer: duplicatedSigner,
995+
}
996+
997+
doc.allObjects[pageIndex].push(duplicatedObject)
998+
this.objectIndexCache[`${docIndex}-${duplicatedObject.id}`] = pageIndex
999+
1000+
this.$nextTick(() => {
1001+
const refKey = `draggable${docIndex}-${pageIndex}-${duplicatedObject.id}`
1002+
const draggableRefs = this.$refs[refKey]
1003+
if (draggableRefs && Array.isArray(draggableRefs) && draggableRefs[0]) {
1004+
draggableRefs[0].isSelected = true
1005+
}
1006+
})
1007+
},
9431008
9441009
checkAndMoveObjectPage(docIndex, objectId, mouseX, mouseY) {
9451010
if (docIndex < 0 || docIndex >= this.pdfDocuments.length) return undefined

0 commit comments

Comments
 (0)