add select fields mode
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
@touchstart="startTouchDrag"
|
||||
>
|
||||
<div
|
||||
v-if="isSelected || isDraw"
|
||||
v-if="isSelected || isDraw || isInMultiSelection"
|
||||
class="top-0 bottom-0 right-0 left-0 absolute border border-1.5 pointer-events-none"
|
||||
:class="activeBorderClasses"
|
||||
/>
|
||||
@@ -24,7 +24,7 @@
|
||||
:style="{ left: (cellW / area.w * 100) + '%' }"
|
||||
>
|
||||
<span
|
||||
v-if="index === 0 && editable"
|
||||
v-if="index === 0 && editable && !isInMultiSelection"
|
||||
class="h-2.5 w-2.5 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-ew-resize z-10"
|
||||
style="left: -4px"
|
||||
@mousedown.stop="startResizeCell"
|
||||
@@ -32,7 +32,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="field?.type && (isSelected || isNameFocus)"
|
||||
v-if="field?.type && (isSelected || isNameFocus) && !isInMultiSelection"
|
||||
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap flex z-10 field-area-controls"
|
||||
style="top: -25px; height: 25px"
|
||||
@mousedown.stop
|
||||
@@ -48,7 +48,7 @@
|
||||
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px] mt-[1px]'"
|
||||
:submitters="template.submitters"
|
||||
@update:model-value="save"
|
||||
@click="selectedAreaRef.value = area"
|
||||
@click="selectedAreasRef.value = [area]"
|
||||
/>
|
||||
<FieldType
|
||||
v-model="field.type"
|
||||
@@ -57,7 +57,7 @@
|
||||
:button-classes="'px-1'"
|
||||
:menu-classes="'bg-white rounded-t-none'"
|
||||
@update:model-value="[maybeUpdateOptions(), save()]"
|
||||
@click="selectedAreaRef.value = area"
|
||||
@click="selectedAreasRef.value = [area]"
|
||||
/>
|
||||
<span
|
||||
v-if="field.type !== 'checkbox' || field.name"
|
||||
@@ -146,7 +146,7 @@
|
||||
@click-font="isShowFontModal = true"
|
||||
@click-description="isShowDescriptionModal = true"
|
||||
@click-condition="isShowConditionsModal = true"
|
||||
@scroll-to="[selectedAreaRef.value = $event, $emit('scroll-to', $event)]"
|
||||
@scroll-to="[selectedAreasRef.value = [$event], $emit('scroll-to', $event)]"
|
||||
/>
|
||||
</ul>
|
||||
</span>
|
||||
@@ -266,7 +266,7 @@
|
||||
ref="defaultValueSelect"
|
||||
class="bg-transparent outline-none focus:outline-none w-full"
|
||||
@change="[field.default_value = $event.target.value, field.readonly = !!field.default_value?.length, save()]"
|
||||
@focus="selectedAreaRef.value = area"
|
||||
@focus="selectedAreasRef.value = [area]"
|
||||
@keydown.enter="onDefaultValueEnter"
|
||||
>
|
||||
<option
|
||||
@@ -293,7 +293,7 @@
|
||||
:class="{ 'cursor-text': isValueInput }"
|
||||
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : (field.type === 'date' ? field.preferences?.format || t('type_value') : t('type_value'))"
|
||||
@blur="onDefaultValueBlur"
|
||||
@focus="selectedAreaRef.value = area"
|
||||
@focus="selectedAreasRef.value = [area]"
|
||||
@paste.prevent="onPaste"
|
||||
@keydown.enter="onDefaultValueEnter"
|
||||
>{{ field.default_value }}</span>
|
||||
@@ -319,6 +319,7 @@
|
||||
<span
|
||||
v-if="field?.type && editable"
|
||||
class="h-4 w-4 lg:h-2.5 lg:w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize"
|
||||
:class="{ 'z-30': isInMultiSelection }"
|
||||
@mousedown.stop="startResize"
|
||||
@touchstart="startTouchResize"
|
||||
/>
|
||||
@@ -398,7 +399,7 @@ export default {
|
||||
FieldSubmitter,
|
||||
IconX
|
||||
},
|
||||
inject: ['template', 'selectedAreaRef', 'save', 't', 'isInlineSize'],
|
||||
inject: ['template', 'save', 't', 'isInlineSize', 'selectedAreasRef', 'isCmdKeyRef'],
|
||||
props: {
|
||||
area: {
|
||||
type: Object,
|
||||
@@ -463,6 +464,11 @@ export default {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
isSelectMode: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to'],
|
||||
@@ -646,7 +652,10 @@ export default {
|
||||
]
|
||||
},
|
||||
isSelected () {
|
||||
return this.selectedAreaRef.value === this.area
|
||||
return this.selectedAreasRef.value.includes(this.area)
|
||||
},
|
||||
isInMultiSelection () {
|
||||
return this.selectedAreasRef.value.length >= 2 && this.isSelected
|
||||
},
|
||||
positionStyle () {
|
||||
const { x, y, w, h } = this.area
|
||||
@@ -683,10 +692,10 @@ export default {
|
||||
buildAreaOptionValue (area) {
|
||||
const option = this.optionsUuidIndex[area.option_uuid]
|
||||
|
||||
return option.value || `${this.t('option')} ${this.field.options.indexOf(option) + 1}`
|
||||
return option?.value || `${this.t('option')} ${this.field.options.indexOf(option) + 1}`
|
||||
},
|
||||
maybeToggleDefaultValue () {
|
||||
if (!this.editable) {
|
||||
if (!this.editable || this.isCmdKeyRef.value) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -770,7 +779,7 @@ export default {
|
||||
}
|
||||
},
|
||||
onNameFocus (e) {
|
||||
this.selectedAreaRef.value = this.area
|
||||
this.selectedAreasRef.value = [this.area]
|
||||
|
||||
this.isNameFocus = true
|
||||
this.$refs.name.style.minWidth = this.$refs.name.clientWidth + 'px'
|
||||
@@ -906,6 +915,15 @@ export default {
|
||||
if (e.target.id === 'mask') {
|
||||
this.area.w = e.offsetX / e.target.clientWidth - this.area.x
|
||||
this.area.h = e.offsetY / e.target.clientHeight - this.area.y
|
||||
|
||||
if (this.isInMultiSelection) {
|
||||
this.selectedAreasRef.value.forEach((area) => {
|
||||
if (area !== this.area) {
|
||||
area.w = this.area.w
|
||||
area.h = this.area.h
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
drag (e) {
|
||||
@@ -931,7 +949,7 @@ export default {
|
||||
|
||||
const rect = e.target.getBoundingClientRect()
|
||||
|
||||
this.selectedAreaRef.value = this.area
|
||||
this.selectedAreasRef.value = [this.area]
|
||||
|
||||
this.dragFrom = { x: rect.left - e.touches[0].clientX, y: rect.top - e.touches[0].clientY }
|
||||
|
||||
@@ -980,13 +998,23 @@ export default {
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
if (!this.selectedAreasRef.value.includes(this.area)) {
|
||||
this.selectedAreasRef.value.push(this.area)
|
||||
} else {
|
||||
this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(this.area), 1)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (this.editable) {
|
||||
this.isDragged = true
|
||||
}
|
||||
|
||||
const rect = e.target.getBoundingClientRect()
|
||||
|
||||
this.selectedAreaRef.value = this.area
|
||||
this.selectedAreasRef.value = [this.area]
|
||||
|
||||
this.dragFrom = { x: rect.left - e.clientX, y: rect.top - e.clientY }
|
||||
|
||||
@@ -1055,7 +1083,9 @@ export default {
|
||||
this.$emit('stop-drag')
|
||||
},
|
||||
startResize () {
|
||||
this.selectedAreaRef.value = this.area
|
||||
if (!this.selectedAreasRef.value.includes(this.area)) {
|
||||
this.selectedAreasRef.value = [this.area]
|
||||
}
|
||||
|
||||
this.$el.getRootNode().addEventListener('mousemove', this.resize)
|
||||
this.$el.getRootNode().addEventListener('mouseup', this.stopResize)
|
||||
@@ -1071,7 +1101,9 @@ export default {
|
||||
this.save()
|
||||
},
|
||||
startTouchResize (e) {
|
||||
this.selectedAreaRef.value = this.area
|
||||
if (!this.selectedAreasRef.value.includes(this.area)) {
|
||||
this.selectedAreasRef.value = [this.area]
|
||||
}
|
||||
|
||||
this.$refs?.name?.blur()
|
||||
|
||||
@@ -1088,6 +1120,15 @@ export default {
|
||||
|
||||
this.area.w = (e.touches[0].clientX - rect.left) / rect.width - this.area.x
|
||||
this.area.h = (e.touches[0].clientY - rect.top) / rect.height - this.area.y
|
||||
|
||||
if (this.isInMultiSelection) {
|
||||
this.selectedAreasRef.value.forEach((area) => {
|
||||
if (area !== this.area) {
|
||||
area.w = this.area.w
|
||||
area.h = this.area.h
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
stopTouchResize () {
|
||||
this.$el.getRootNode().removeEventListener('touchmove', this.touchResize)
|
||||
|
||||
@@ -378,6 +378,9 @@
|
||||
@remove-area="removeArea"
|
||||
@paste-field="pasteField"
|
||||
@copy-field="copyField"
|
||||
@copy-selected-areas="copySelectedAreas"
|
||||
@delete-selected-areas="deleteSelectedAreas"
|
||||
@align-selected-areas="alignSelectedAreas"
|
||||
/>
|
||||
<DocumentControls
|
||||
v-if="isBreakpointLg && editable"
|
||||
@@ -592,8 +595,10 @@ export default {
|
||||
withConditions: this.withConditions,
|
||||
isInlineSize: this.isInlineSize,
|
||||
defaultDrawFieldType: this.defaultDrawFieldType,
|
||||
selectedAreaRef: computed(() => this.selectedAreaRef),
|
||||
fieldsDragFieldRef: computed(() => this.fieldsDragFieldRef)
|
||||
selectedAreasRef: computed(() => this.selectedAreasRef),
|
||||
fieldsDragFieldRef: computed(() => this.fieldsDragFieldRef),
|
||||
isSelectModeRef: computed(() => this.isSelectModeRef),
|
||||
isCmdKeyRef: computed(() => this.isCmdKeyRef)
|
||||
}
|
||||
},
|
||||
props: {
|
||||
@@ -862,8 +867,10 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
submitterDefaultNames: FieldSubmitter.computed.names,
|
||||
selectedAreaRef: () => ref(),
|
||||
isSelectModeRef: () => ref(false),
|
||||
isCmdKeyRef: () => ref(false),
|
||||
fieldsDragFieldRef: () => ref(),
|
||||
selectedAreasRef: () => ref([]),
|
||||
language () {
|
||||
return this.locale.split('-')[0].toLowerCase()
|
||||
},
|
||||
@@ -877,6 +884,14 @@ export default {
|
||||
isInlineSize () {
|
||||
return CSS.supports('container-type: size')
|
||||
},
|
||||
lowestSelectedArea () {
|
||||
return this.selectedAreasRef.value.reduce((acc, area) => {
|
||||
return area.y + area.h < acc.y + acc.h ? acc : area
|
||||
}, this.selectedAreasRef.value[0])
|
||||
},
|
||||
lastSelectedArea () {
|
||||
return this.selectedAreasRef.value[this.selectedAreasRef.value.length - 1]
|
||||
},
|
||||
isMobile () {
|
||||
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
|
||||
|
||||
@@ -918,7 +933,7 @@ export default {
|
||||
})
|
||||
},
|
||||
selectedField () {
|
||||
return this.template.fields.find((f) => f.areas?.includes(this.selectedAreaRef.value))
|
||||
return this.template.fields.find((f) => f.areas?.includes(this.lastSelectedArea))
|
||||
},
|
||||
sortedDocuments () {
|
||||
return this.template.schema.map((item) => {
|
||||
@@ -1005,6 +1020,81 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
toRaw,
|
||||
toggleSelectMode () {
|
||||
this.isSelectModeRef.value = !this.isSelectModeRef.value
|
||||
|
||||
if (!this.isSelectModeRef.value && this.selectedAreasRef.value.length > 1) {
|
||||
this.selectedAreasRef.value = []
|
||||
}
|
||||
},
|
||||
deleteSelectedAreas () {
|
||||
[...this.selectedAreasRef.value].forEach((area) => {
|
||||
this.removeArea(area, false)
|
||||
})
|
||||
|
||||
this.save()
|
||||
},
|
||||
moveSelectedAreas (dx, dy) {
|
||||
let clampedDx = dx
|
||||
let clampedDy = dy
|
||||
|
||||
const rectIndex = {}
|
||||
|
||||
this.selectedAreasRef.value.map((area) => {
|
||||
const key = `${area.attachment_uuid}-${area.page}`
|
||||
|
||||
let rect = rectIndex[key]
|
||||
|
||||
if (!rect) {
|
||||
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
|
||||
const page = documentRef.pageRefs[area.page].$refs.image
|
||||
rect = page.getBoundingClientRect()
|
||||
|
||||
rectIndex[key] = rect
|
||||
}
|
||||
|
||||
const normalizedDx = dx / rect.width
|
||||
const normalizedDy = dy / rect.height
|
||||
|
||||
const maxDxLeft = -area.x
|
||||
const maxDxRight = 1 - area.w - area.x
|
||||
const maxDyTop = -area.y
|
||||
const maxDyBottom = 1 - area.h - area.y
|
||||
|
||||
if (normalizedDx < maxDxLeft) clampedDx = Math.max(clampedDx, maxDxLeft * rect.width)
|
||||
if (normalizedDx > maxDxRight) clampedDx = Math.min(clampedDx, maxDxRight * rect.width)
|
||||
if (normalizedDy < maxDyTop) clampedDy = Math.max(clampedDy, maxDyTop * rect.height)
|
||||
if (normalizedDy > maxDyBottom) clampedDy = Math.min(clampedDy, maxDyBottom * rect.height)
|
||||
|
||||
return [area, rect]
|
||||
}).forEach(([area, rect]) => {
|
||||
area.x += clampedDx / rect.width
|
||||
area.y += clampedDy / rect.height
|
||||
})
|
||||
|
||||
this.debouncedSave()
|
||||
},
|
||||
alignSelectedAreas (direction) {
|
||||
const areas = this.selectedAreasRef.value
|
||||
|
||||
let targetValue
|
||||
|
||||
if (direction === 'left') {
|
||||
targetValue = Math.min(...areas.map(a => a.x))
|
||||
areas.forEach((area) => { area.x = targetValue })
|
||||
} else if (direction === 'right') {
|
||||
targetValue = Math.max(...areas.map(a => a.x + a.w))
|
||||
areas.forEach((area) => { area.x = targetValue - area.w })
|
||||
} else if (direction === 'top') {
|
||||
targetValue = Math.min(...areas.map(a => a.y))
|
||||
areas.forEach((area) => { area.y = targetValue })
|
||||
} else if (direction === 'bottom') {
|
||||
targetValue = Math.max(...areas.map(a => a.y + a.h))
|
||||
areas.forEach((area) => { area.y = targetValue - area.h })
|
||||
}
|
||||
|
||||
this.save()
|
||||
},
|
||||
download () {
|
||||
this.isDownloading = true
|
||||
|
||||
@@ -1389,49 +1479,92 @@ export default {
|
||||
}
|
||||
},
|
||||
onKeyUp (e) {
|
||||
if (e.code === 'Escape') {
|
||||
this.clearDrawField()
|
||||
this.isCmdKeyRef.value = false
|
||||
|
||||
this.selectedAreaRef.value = null
|
||||
if (e.code === 'Escape') {
|
||||
this.selectedAreasRef.value = []
|
||||
this.clearDrawField()
|
||||
}
|
||||
|
||||
if (this.editable && ['Backspace', 'Delete'].includes(e.key) && this.selectedAreaRef.value && document.activeElement === document.body) {
|
||||
this.removeArea(this.selectedAreaRef.value)
|
||||
|
||||
this.selectedAreaRef.value = null
|
||||
if (this.editable && ['Backspace', 'Delete'].includes(e.key) && document.activeElement === document.body) {
|
||||
if (this.selectedAreasRef.value.length > 1) {
|
||||
this.deleteSelectedAreas()
|
||||
} else if (this.selectedAreasRef.value.length) {
|
||||
this.removeArea(this.lastSelectedArea)
|
||||
}
|
||||
}
|
||||
},
|
||||
onKeyDown (event) {
|
||||
if ((event.metaKey && event.shiftKey && event.key === 'z') || (event.ctrlKey && event.key === 'Z')) {
|
||||
if (event.key === 'Tab' && document.activeElement === document.body) {
|
||||
event.stopImmediatePropagation()
|
||||
event.preventDefault()
|
||||
|
||||
this.toggleSelectMode()
|
||||
} else if ((event.metaKey && event.shiftKey && event.key === 'z') || (event.ctrlKey && event.key === 'Z')) {
|
||||
event.stopImmediatePropagation()
|
||||
event.preventDefault()
|
||||
|
||||
this.selectedAreasRef.value = []
|
||||
|
||||
this.redo()
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key === 'z') {
|
||||
event.stopImmediatePropagation()
|
||||
event.preventDefault()
|
||||
|
||||
this.selectedAreasRef.value = []
|
||||
|
||||
this.undo()
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key === 'c' && document.activeElement === document.body) {
|
||||
event.preventDefault()
|
||||
|
||||
this.copyField()
|
||||
if (this.selectedAreasRef.value.length > 1) {
|
||||
event.preventDefault()
|
||||
this.copySelectedAreas()
|
||||
} else if (this.selectedAreasRef.value.length) {
|
||||
event.preventDefault()
|
||||
this.copyField()
|
||||
}
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v' && this.hasClipboardData() && document.activeElement === document.body) {
|
||||
event.preventDefault()
|
||||
|
||||
this.pasteField()
|
||||
} else if (this.selectedAreaRef.value && ['ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown'].includes(event.key) && document.activeElement === document.body) {
|
||||
event.preventDefault()
|
||||
|
||||
this.handleAreaArrows(event)
|
||||
} else if (['ArrowLeft', 'ArrowUp', 'ArrowRight', 'ArrowDown'].includes(event.key) && document.activeElement === document.body) {
|
||||
if (this.selectedAreasRef.value.length > 1) {
|
||||
event.preventDefault()
|
||||
this.handleSelectedAreasArrows(event)
|
||||
} else if (this.selectedAreasRef.value.length) {
|
||||
event.preventDefault()
|
||||
this.handleAreaArrows(event)
|
||||
}
|
||||
} else if (event.metaKey || event.ctrlKey) {
|
||||
this.isCmdKeyRef.value = true
|
||||
}
|
||||
},
|
||||
handleSelectedAreasArrows (event) {
|
||||
if (!this.editable) {
|
||||
return
|
||||
}
|
||||
|
||||
const diff = (event.shiftKey ? 5.0 : 1.0)
|
||||
let dx = 0
|
||||
let dy = 0
|
||||
|
||||
if (event.key === 'ArrowRight') {
|
||||
dx = diff
|
||||
} else if (event.key === 'ArrowLeft') {
|
||||
dx = -diff
|
||||
} else if (event.key === 'ArrowUp') {
|
||||
dy = -diff
|
||||
} else if (event.key === 'ArrowDown') {
|
||||
dy = diff
|
||||
}
|
||||
|
||||
this.moveSelectedAreas(dx, dy)
|
||||
},
|
||||
handleAreaArrows (event) {
|
||||
if (!this.editable) {
|
||||
return
|
||||
}
|
||||
|
||||
const area = this.selectedAreaRef.value
|
||||
const area = this.lastSelectedArea
|
||||
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
|
||||
const page = documentRef.pageRefs[area.page].$refs.image
|
||||
const rect = page.getBoundingClientRect()
|
||||
@@ -1464,7 +1597,7 @@ export default {
|
||||
this.save()
|
||||
}, 700)
|
||||
},
|
||||
removeArea (area) {
|
||||
removeArea (area, save = true) {
|
||||
const field = this.template.fields.find((f) => f.areas?.includes(area))
|
||||
|
||||
field.areas.splice(field.areas.indexOf(area), 1)
|
||||
@@ -1475,7 +1608,11 @@ export default {
|
||||
this.removeFieldConditions(field)
|
||||
}
|
||||
|
||||
this.save()
|
||||
this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(area), 1)
|
||||
|
||||
if (save) {
|
||||
this.save()
|
||||
}
|
||||
},
|
||||
removeFieldConditions (field) {
|
||||
this.template.fields.forEach((f) => {
|
||||
@@ -1499,7 +1636,7 @@ export default {
|
||||
})
|
||||
},
|
||||
copyField () {
|
||||
const area = this.selectedAreaRef.value
|
||||
const area = this.lastSelectedArea
|
||||
|
||||
if (!area) return
|
||||
|
||||
@@ -1524,25 +1661,68 @@ export default {
|
||||
console.error('Failed to save clipboard:', e)
|
||||
}
|
||||
},
|
||||
pasteField (targetPosition = null) {
|
||||
let field = null
|
||||
let area = null
|
||||
let isSameTemplate = false
|
||||
copySelectedAreas () {
|
||||
const items = []
|
||||
|
||||
const areas = this.selectedAreasRef.value
|
||||
|
||||
const minX = Math.min(...areas.map(a => a.x))
|
||||
const minY = Math.min(...areas.map(a => a.y))
|
||||
|
||||
areas.forEach((area) => {
|
||||
const field = this.template.fields.find((f) => f.areas?.includes(area))
|
||||
|
||||
if (!field) return
|
||||
|
||||
const fieldCopy = JSON.parse(JSON.stringify(field))
|
||||
const areaCopy = JSON.parse(JSON.stringify(area))
|
||||
|
||||
delete fieldCopy.areas
|
||||
delete fieldCopy.uuid
|
||||
delete fieldCopy.submitter_uuid
|
||||
|
||||
areaCopy.relativeX = area.x - minX
|
||||
areaCopy.relativeY = area.y - minY
|
||||
|
||||
items.push({ field: fieldCopy, area: areaCopy })
|
||||
})
|
||||
|
||||
const clipboardData = {
|
||||
items,
|
||||
templateId: this.template.id,
|
||||
timestamp: Date.now(),
|
||||
isGroup: true
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.setItem('docuseal_clipboard', JSON.stringify(clipboardData))
|
||||
} catch (e) {
|
||||
console.error('Failed to save clipboard:', e)
|
||||
}
|
||||
},
|
||||
pasteField (targetPosition = null) {
|
||||
const clipboard = localStorage.getItem('docuseal_clipboard')
|
||||
|
||||
if (clipboard) {
|
||||
const data = JSON.parse(clipboard)
|
||||
if (!clipboard) return
|
||||
|
||||
if (Date.now() - data.timestamp < 3600000) {
|
||||
field = data.field
|
||||
area = data.area
|
||||
isSameTemplate = data.templateId === this.template.id
|
||||
} else {
|
||||
localStorage.removeItem('docuseal_clipboard')
|
||||
}
|
||||
const data = JSON.parse(clipboard)
|
||||
|
||||
if (Date.now() - data.timestamp >= 3600000) {
|
||||
localStorage.removeItem('docuseal_clipboard')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (data.isGroup && data.items?.length) {
|
||||
this.pasteFieldGroup(data, targetPosition)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const field = data.field
|
||||
const area = data.area
|
||||
const isSameTemplate = data.templateId === this.template.id
|
||||
|
||||
if (!field || !area) return
|
||||
|
||||
if (!isSameTemplate) {
|
||||
@@ -1550,20 +1730,19 @@ export default {
|
||||
delete field.preferences?.formula
|
||||
}
|
||||
|
||||
const currentArea = this.selectedAreaRef.value
|
||||
const defaultAttachmentUuid = this.template.schema[0]?.attachment_uuid
|
||||
|
||||
if (field && (currentArea || targetPosition)) {
|
||||
if (field && (this.lowestSelectedArea || targetPosition)) {
|
||||
const attachmentUuid = targetPosition?.attachment_uuid ||
|
||||
(this.template.documents.find((d) => d.uuid === currentArea.attachment_uuid) ? currentArea.attachment_uuid : null) ||
|
||||
(this.template.documents.find((d) => d.uuid === this.lowestSelectedArea.attachment_uuid) ? this.lowestSelectedArea.attachment_uuid : null) ||
|
||||
defaultAttachmentUuid
|
||||
|
||||
const newArea = {
|
||||
...JSON.parse(JSON.stringify(area)),
|
||||
attachment_uuid: attachmentUuid,
|
||||
page: targetPosition?.page ?? (attachmentUuid === currentArea.attachment_uuid ? currentArea.page : 0),
|
||||
x: targetPosition ? (targetPosition.x - area.w / 2) : currentArea.x,
|
||||
y: targetPosition ? (targetPosition.y - area.h / 2) : (currentArea.y + currentArea.h * 1.3)
|
||||
page: targetPosition?.page ?? (attachmentUuid === this.lowestSelectedArea.attachment_uuid ? this.lowestSelectedArea.page : 0),
|
||||
x: targetPosition ? (targetPosition.x - area.w / 2) : Math.min(...this.selectedAreasRef.value.map((area) => area.x)),
|
||||
y: targetPosition ? (targetPosition.y - area.h / 2) : (this.lowestSelectedArea.y + this.lowestSelectedArea.h * 1.3)
|
||||
}
|
||||
|
||||
const newField = {
|
||||
@@ -1588,11 +1767,85 @@ export default {
|
||||
|
||||
this.insertField(newField)
|
||||
|
||||
this.selectedAreaRef.value = newArea
|
||||
this.selectedAreasRef.value = [newArea]
|
||||
|
||||
this.save()
|
||||
}
|
||||
},
|
||||
pasteFieldGroup (data, targetPosition) {
|
||||
const isSameTemplate = data.templateId === this.template.id
|
||||
const defaultAttachmentUuid = this.template.schema[0]?.attachment_uuid
|
||||
|
||||
const attachmentUuid = targetPosition?.attachment_uuid ||
|
||||
(this.lowestSelectedArea && this.template.documents.find((d) => d.uuid === this.lowestSelectedArea.attachment_uuid) ? this.lowestSelectedArea.attachment_uuid : null) ||
|
||||
defaultAttachmentUuid
|
||||
|
||||
const page = targetPosition?.page ?? (this.lowestSelectedArea && attachmentUuid === this.lowestSelectedArea.attachment_uuid ? this.lowestSelectedArea.page : 0)
|
||||
|
||||
let baseX, baseY
|
||||
|
||||
if (targetPosition) {
|
||||
baseX = targetPosition.x
|
||||
baseY = targetPosition.y
|
||||
} else if (this.lowestSelectedArea) {
|
||||
baseX = Math.min(...this.selectedAreasRef.value.map((area) => area.x))
|
||||
baseY = this.lowestSelectedArea.y + this.lowestSelectedArea.h * 1.3
|
||||
} else {
|
||||
baseX = 0.1
|
||||
baseY = 0.1
|
||||
}
|
||||
|
||||
const newAreas = []
|
||||
|
||||
data.items.forEach((item) => {
|
||||
const field = JSON.parse(JSON.stringify(item.field))
|
||||
const area = JSON.parse(JSON.stringify(item.area))
|
||||
|
||||
if (!isSameTemplate) {
|
||||
delete field.conditions
|
||||
delete field.preferences?.formula
|
||||
}
|
||||
|
||||
const newArea = {
|
||||
...area,
|
||||
attachment_uuid: attachmentUuid,
|
||||
page,
|
||||
x: baseX + (area.relativeX || 0),
|
||||
y: baseY + (area.relativeY || 0)
|
||||
}
|
||||
|
||||
delete newArea.relativeX
|
||||
delete newArea.relativeY
|
||||
|
||||
const newField = {
|
||||
...field,
|
||||
uuid: v4(),
|
||||
submitter_uuid: this.selectedSubmitter.uuid,
|
||||
areas: [newArea]
|
||||
}
|
||||
|
||||
if (['radio', 'multiple'].includes(field.type) && field.options?.length) {
|
||||
const oldOptionUuid = area.option_uuid
|
||||
const optionsMap = {}
|
||||
|
||||
newField.options = field.options.map((opt) => {
|
||||
const newUuid = v4()
|
||||
optionsMap[opt.uuid] = newUuid
|
||||
|
||||
return { ...opt, uuid: newUuid }
|
||||
})
|
||||
|
||||
newArea.option_uuid = optionsMap[oldOptionUuid] || newField.options[0].uuid
|
||||
}
|
||||
|
||||
this.insertField(newField)
|
||||
newAreas.push(newArea)
|
||||
})
|
||||
|
||||
this.selectedAreasRef.value = [...newAreas]
|
||||
|
||||
this.save()
|
||||
},
|
||||
hasClipboardData () {
|
||||
try {
|
||||
const clipboard = localStorage.getItem('docuseal_clipboard')
|
||||
@@ -1659,8 +1912,8 @@ export default {
|
||||
const previousArea = this.drawField.areas?.[this.drawField.areas.length - 1]
|
||||
|
||||
if (this.selectedField?.type === this.drawField.type) {
|
||||
area.w = this.selectedAreaRef.value.w
|
||||
area.h = this.selectedAreaRef.value.h
|
||||
area.w = this.lastSelectedArea.w
|
||||
area.h = this.lastSelectedArea.h
|
||||
} else if (previousArea) {
|
||||
area.w = previousArea.w
|
||||
area.h = previousArea.h
|
||||
@@ -1691,7 +1944,7 @@ export default {
|
||||
this.drawField = null
|
||||
this.drawOption = null
|
||||
|
||||
this.selectedAreaRef.value = area
|
||||
this.selectedAreasRef.value = [area]
|
||||
|
||||
this.save()
|
||||
} else {
|
||||
@@ -1728,8 +1981,8 @@ export default {
|
||||
|
||||
if (this.drawFieldType && (area.w === 0 || area.h === 0)) {
|
||||
if (this.selectedField?.type === this.drawFieldType) {
|
||||
area.w = this.selectedAreaRef.value.w
|
||||
area.h = this.selectedAreaRef.value.h
|
||||
area.w = this.lastSelectedArea.w
|
||||
area.h = this.lastSelectedArea.h
|
||||
} else {
|
||||
this.setDefaultAreaSize(area, this.drawFieldType)
|
||||
}
|
||||
@@ -1741,7 +1994,7 @@ export default {
|
||||
if (area.w && (type !== 'checkbox' || this.drawFieldType || !isTooSmall)) {
|
||||
this.addField(type, area)
|
||||
|
||||
this.selectedAreaRef.value = area
|
||||
this.selectedAreasRef.value = [area]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1821,7 +2074,11 @@ export default {
|
||||
|
||||
field.areas.push(fieldArea)
|
||||
|
||||
this.selectedAreaRef.value = fieldArea
|
||||
if (this.selectedAreasRef.value.length < 2) {
|
||||
this.selectedAreasRef.value = [fieldArea]
|
||||
} else {
|
||||
this.selectedAreasRef.value.push(fieldArea)
|
||||
}
|
||||
|
||||
if (this.template.fields.indexOf(field) === -1) {
|
||||
this.insertField(field)
|
||||
@@ -1834,7 +2091,7 @@ export default {
|
||||
if (field.type === 'heading') {
|
||||
this.$nextTick(() => {
|
||||
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
|
||||
const areaRef = documentRef.pageRefs[area.page].areaRefs.find((ref) => ref.area === this.selectedAreaRef.value)
|
||||
const areaRef = documentRef.pageRefs[area.page].areaRefs.find((ref) => ref.area === fieldArea)
|
||||
|
||||
areaRef.isHeadingSelected = true
|
||||
|
||||
@@ -1850,7 +2107,7 @@ export default {
|
||||
let baseArea
|
||||
|
||||
if (this.selectedField?.type === fieldType) {
|
||||
baseArea = this.selectedAreaRef.value
|
||||
baseArea = this.lastSelectedArea
|
||||
} else if (previousField?.areas?.length) {
|
||||
baseArea = previousField.areas[previousField.areas.length - 1]
|
||||
} else {
|
||||
@@ -2175,7 +2432,7 @@ export default {
|
||||
|
||||
documentRef.scrollToArea(area)
|
||||
|
||||
this.selectedAreaRef.value = area
|
||||
this.selectedAreasRef.value = [area]
|
||||
},
|
||||
baseFetch (path, options = {}) {
|
||||
return fetch(this.baseUrl + path, {
|
||||
|
||||
@@ -168,9 +168,19 @@ export default {
|
||||
buildDefaultName: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
withClickSaveEvent: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
excludeFieldUuids: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
emits: ['close', 'click-save'],
|
||||
data () {
|
||||
return {
|
||||
conditions: this.item.conditions?.[0] ? JSON.parse(JSON.stringify(this.item.conditions)) : [{}]
|
||||
@@ -183,14 +193,14 @@ export default {
|
||||
fields () {
|
||||
if (this.item.submitter_uuid) {
|
||||
return this.template.fields.reduce((acc, f) => {
|
||||
if (f !== this.item && !this.excludeTypes.includes(f.type) && (!f.conditions?.length || !f.conditions.find((c) => c.field_uuid === this.item.uuid))) {
|
||||
if (f !== this.item && !this.excludeTypes.includes(f.type) && !this.excludeFieldUuids.includes(f.uuid) && (!f.conditions?.length || !f.conditions.find((c) => c.field_uuid === this.item.uuid))) {
|
||||
acc.push(f)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [])
|
||||
} else {
|
||||
return this.template.fields
|
||||
return this.template.fields.filter((f) => !this.excludeFieldUuids.includes(f.uuid))
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -234,7 +244,12 @@ export default {
|
||||
delete this.item.conditions
|
||||
}
|
||||
|
||||
this.save()
|
||||
if (this.withClickSaveEvent) {
|
||||
this.$emit('click-save')
|
||||
} else {
|
||||
this.save()
|
||||
}
|
||||
|
||||
this.$emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
class="my-1 border-base-300"
|
||||
>
|
||||
<button
|
||||
v-if="showFont"
|
||||
v-if="showFont && !isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="openFontModal"
|
||||
>
|
||||
@@ -57,7 +57,7 @@
|
||||
<span>{{ t('description') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="showCondition"
|
||||
v-if="showCondition && !isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="openConditionModal"
|
||||
>
|
||||
@@ -73,7 +73,63 @@
|
||||
<span>{{ t('formula') }}</span>
|
||||
</button>
|
||||
<hr
|
||||
v-if="(showFont || showDescription || showCondition || showFormula) && (showCopy || showDelete || showPaste)"
|
||||
v-if="((showFont && !isMultiSelection) || showDescription || (showCondition && !isMultiSelection) || showFormula) && (showCopy || showDelete || showPaste)"
|
||||
class="my-1 border-base-300"
|
||||
>
|
||||
<button
|
||||
v-if="isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="$emit('align', 'left')"
|
||||
>
|
||||
<IconLayoutAlignLeft class="w-4 h-4" />
|
||||
<span>{{ t('align_left') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="$emit('align', 'right')"
|
||||
>
|
||||
<IconLayoutAlignRight class="w-4 h-4" />
|
||||
<span>{{ t('align_right') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="$emit('align', 'top')"
|
||||
>
|
||||
<IconLayoutAlignTop class="w-4 h-4" />
|
||||
<span>{{ t('align_top') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="$emit('align', 'bottom')"
|
||||
>
|
||||
<IconLayoutAlignBottom class="w-4 h-4" />
|
||||
<span>{{ t('align_bottom') }}</span>
|
||||
</button>
|
||||
<hr
|
||||
v-if="isMultiSelection && (showFont || showCondition)"
|
||||
class="my-1 border-base-300"
|
||||
>
|
||||
<button
|
||||
v-if="showFont && isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="openFontModal"
|
||||
>
|
||||
<IconTypography class="w-4 h-4" />
|
||||
<span>{{ t('font') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="showCondition && isMultiSelection"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||
@click.stop="openConditionModal"
|
||||
>
|
||||
<IconRouteAltLeft class="w-4 h-4" />
|
||||
<span>{{ t('condition') }}</span>
|
||||
</button>
|
||||
<hr
|
||||
v-if="isMultiSelection"
|
||||
class="my-1 border-base-300"
|
||||
>
|
||||
<button
|
||||
@@ -111,6 +167,24 @@
|
||||
</span>
|
||||
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘V' : 'Ctrl+V' }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="showSelectFields"
|
||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center justify-between text-sm"
|
||||
@click.stop="handleToggleSelectMode"
|
||||
>
|
||||
<span class="flex items-center space-x-2">
|
||||
<IconClick
|
||||
v-if="!isSelectModeRef.value"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
<IconNewSection
|
||||
v-else
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
<span>{{ isSelectModeRef.value ? t('draw_fields') : t('select_fields') }}</span>
|
||||
</span>
|
||||
<span class="text-xs text-base-content/60 ml-4">Tab</span>
|
||||
</button>
|
||||
</div>
|
||||
<Teleport
|
||||
v-if="isShowFormulaModal"
|
||||
@@ -128,10 +202,12 @@
|
||||
to="#docuseal_modal_container"
|
||||
>
|
||||
<FontModal
|
||||
:field="field"
|
||||
:field="multiSelectField || field"
|
||||
:area="contextMenu.area"
|
||||
:editable="editable"
|
||||
:build-default-name="buildDefaultName"
|
||||
:with-click-save-event="isMultiSelection"
|
||||
@click-save="handleSaveMultiSelectFontModal"
|
||||
@close="closeModal"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -140,8 +216,11 @@
|
||||
to="#docuseal_modal_container"
|
||||
>
|
||||
<ConditionsModal
|
||||
:item="field"
|
||||
:item="multiSelectField || field"
|
||||
:build-default-name="buildDefaultName"
|
||||
:exclude-field-uuids="isMultiSelection ? selectedFields.map(f => f.uuid) : []"
|
||||
:with-click-save-event="isMultiSelection"
|
||||
@click-save="handleSaveMultiSelectConditionsModal"
|
||||
@close="closeModal"
|
||||
/>
|
||||
</Teleport>
|
||||
@@ -160,7 +239,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconCopy, IconClipboard, IconTrashX, IconTypography, IconInfoCircle, IconRouteAltLeft, IconMathFunction } from '@tabler/icons-vue'
|
||||
import { IconCopy, IconClipboard, IconTrashX, IconTypography, IconInfoCircle, IconRouteAltLeft, IconMathFunction, IconClick, IconNewSection, IconLayoutAlignLeft, IconLayoutAlignRight, IconLayoutAlignTop, IconLayoutAlignBottom } from '@tabler/icons-vue'
|
||||
import FormulaModal from './formula_modal'
|
||||
import FontModal from './font_modal'
|
||||
import ConditionsModal from './conditions_modal'
|
||||
@@ -178,12 +257,18 @@ export default {
|
||||
IconInfoCircle,
|
||||
IconRouteAltLeft,
|
||||
IconMathFunction,
|
||||
IconClick,
|
||||
IconNewSection,
|
||||
IconLayoutAlignLeft,
|
||||
IconLayoutAlignRight,
|
||||
IconLayoutAlignTop,
|
||||
IconLayoutAlignBottom,
|
||||
FormulaModal,
|
||||
FontModal,
|
||||
ConditionsModal,
|
||||
DescriptionModal
|
||||
},
|
||||
inject: ['t', 'save'],
|
||||
inject: ['t', 'save', 'selectedAreasRef', 'isSelectModeRef'],
|
||||
props: {
|
||||
contextMenu: {
|
||||
type: Object,
|
||||
@@ -197,20 +282,40 @@ export default {
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
isMultiSelection: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selectedAreas: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
template: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
emits: ['copy', 'paste', 'delete', 'close'],
|
||||
emits: ['copy', 'paste', 'delete', 'close', 'align'],
|
||||
data () {
|
||||
return {
|
||||
isShowFormulaModal: false,
|
||||
isShowFontModal: false,
|
||||
isShowConditionsModal: false,
|
||||
isShowDescriptionModal: false
|
||||
isShowDescriptionModal: false,
|
||||
multiSelectField: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fieldNames: FieldType.computed.fieldNames,
|
||||
fieldLabels: FieldType.computed.fieldLabels,
|
||||
selectedFields () {
|
||||
if (!this.isMultiSelection) return []
|
||||
|
||||
return this.selectedAreasRef.value.map((area) => {
|
||||
return this.template.fields.find((f) => f.areas?.includes(area))
|
||||
}).filter(Boolean)
|
||||
},
|
||||
isMac () {
|
||||
return (navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')
|
||||
},
|
||||
@@ -236,36 +341,44 @@ export default {
|
||||
}
|
||||
},
|
||||
showCopy () {
|
||||
return !!this.contextMenu.area
|
||||
return !!this.contextMenu.area || this.isMultiSelection
|
||||
},
|
||||
showPaste () {
|
||||
return !this.contextMenu.area
|
||||
return !this.contextMenu.area && !this.isMultiSelection
|
||||
},
|
||||
showDelete () {
|
||||
return !!this.contextMenu.area
|
||||
return !!this.contextMenu.area || this.isMultiSelection
|
||||
},
|
||||
showFont () {
|
||||
if (this.isMultiSelection) return true
|
||||
if (!this.field) return false
|
||||
|
||||
return ['text', 'number', 'date', 'select', 'heading'].includes(this.field.type)
|
||||
},
|
||||
showDescription () {
|
||||
if (!this.field) return false
|
||||
|
||||
return !['stamp', 'heading', 'strikethrough'].includes(this.field.type)
|
||||
},
|
||||
showCondition () {
|
||||
if (this.isMultiSelection) return true
|
||||
if (!this.field) return false
|
||||
|
||||
return !['stamp', 'heading'].includes(this.field.type)
|
||||
},
|
||||
showFormula () {
|
||||
if (!this.field) return false
|
||||
|
||||
return this.field.type === 'number'
|
||||
},
|
||||
showRequired () {
|
||||
if (!this.field) return false
|
||||
|
||||
return !['phone', 'stamp', 'verification', 'strikethrough', 'heading'].includes(this.field.type)
|
||||
},
|
||||
showReadOnly () {
|
||||
if (!this.field) return false
|
||||
|
||||
return ['text', 'number'].includes(this.field.type)
|
||||
},
|
||||
isRequired () {
|
||||
@@ -273,6 +386,9 @@ export default {
|
||||
},
|
||||
isReadOnly () {
|
||||
return this.field?.readonly || false
|
||||
},
|
||||
showSelectFields () {
|
||||
return !this.contextMenu.area && !this.isMultiSelection
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
@@ -333,12 +449,38 @@ export default {
|
||||
}
|
||||
},
|
||||
openFontModal () {
|
||||
if (this.isMultiSelection) {
|
||||
this.multiSelectField = {
|
||||
name: this.t('fields_selected').replace('{count}', this.selectedFields.length),
|
||||
preferences: {}
|
||||
}
|
||||
|
||||
const preferencesStrings = this.selectedFields.map((f) => JSON.stringify(f.preferences || {}))
|
||||
|
||||
if (preferencesStrings.every((s) => s === preferencesStrings[0])) {
|
||||
this.multiSelectField.preferences = JSON.parse(preferencesStrings[0])
|
||||
}
|
||||
}
|
||||
|
||||
this.isShowFontModal = true
|
||||
},
|
||||
openDescriptionModal () {
|
||||
this.isShowDescriptionModal = true
|
||||
},
|
||||
openConditionModal () {
|
||||
if (this.isMultiSelection) {
|
||||
this.multiSelectField = {
|
||||
name: this.t('fields_selected').replace('{count}', this.selectedFields.length),
|
||||
conditions: []
|
||||
}
|
||||
|
||||
const conditionStrings = this.selectedFields.map((f) => JSON.stringify(f.conditions || []))
|
||||
|
||||
if (conditionStrings.every((s) => s === conditionStrings[0])) {
|
||||
this.multiSelectField.conditions = JSON.parse(conditionStrings[0])
|
||||
}
|
||||
}
|
||||
|
||||
this.isShowConditionsModal = true
|
||||
},
|
||||
openFormulaModal () {
|
||||
@@ -349,6 +491,30 @@ export default {
|
||||
this.isShowFontModal = false
|
||||
this.isShowConditionsModal = false
|
||||
this.isShowDescriptionModal = false
|
||||
this.multiSelectField = null
|
||||
|
||||
this.$emit('close')
|
||||
},
|
||||
handleSaveMultiSelectFontModal () {
|
||||
this.selectedFields.forEach((field) => {
|
||||
field.preferences = { ...field.preferences, ...this.multiSelectField.preferences }
|
||||
})
|
||||
|
||||
this.save()
|
||||
|
||||
this.closeModal()
|
||||
},
|
||||
handleSaveMultiSelectConditionsModal () {
|
||||
this.selectedFields.forEach((field) => {
|
||||
field.conditions = JSON.parse(JSON.stringify(this.multiSelectField.conditions))
|
||||
})
|
||||
|
||||
this.save()
|
||||
|
||||
this.closeModal()
|
||||
},
|
||||
handleToggleSelectMode () {
|
||||
this.isSelectModeRef.value = !this.isSelectModeRef.value
|
||||
|
||||
this.$emit('close')
|
||||
}
|
||||
|
||||
@@ -22,10 +22,14 @@
|
||||
:selected-submitter="selectedSubmitter"
|
||||
:total-pages="sortedPreviewImages.length"
|
||||
:image="image"
|
||||
:attachment-uuid="document.uuid"
|
||||
@drop-field="$emit('drop-field', { ...$event, attachment_uuid: document.uuid })"
|
||||
@remove-area="$emit('remove-area', $event)"
|
||||
@copy-field="$emit('copy-field', $event)"
|
||||
@paste-field="$emit('paste-field', { ...$event, attachment_uuid: document.uuid })"
|
||||
@copy-selected-areas="$emit('copy-selected-areas')"
|
||||
@delete-selected-areas="$emit('delete-selected-areas')"
|
||||
@align-selected-areas="$emit('align-selected-areas', $event)"
|
||||
@scroll-to="scrollToArea"
|
||||
@draw="$emit('draw', { area: {...$event.area, attachment_uuid: document.uuid }, isTooSmall: $event.isTooSmall })"
|
||||
/>
|
||||
@@ -120,7 +124,7 @@ export default {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['draw', 'drop-field', 'remove-area', 'paste-field', 'copy-field'],
|
||||
emits: ['draw', 'drop-field', 'remove-area', 'paste-field', 'copy-field', 'copy-selected-areas', 'delete-selected-areas', 'align-selected-areas'],
|
||||
data () {
|
||||
return {
|
||||
pageRefs: []
|
||||
|
||||
@@ -330,7 +330,7 @@ export default {
|
||||
IconMathFunction,
|
||||
FieldType
|
||||
},
|
||||
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't', 'locale'],
|
||||
inject: ['template', 'save', 'backgroundColor', 'selectedAreasRef', 't', 'locale'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
@@ -451,7 +451,7 @@ export default {
|
||||
const area = this.field.areas.find((a) => a.option_uuid === option.uuid)
|
||||
|
||||
if (area) {
|
||||
this.selectedAreaRef.value = area
|
||||
this.selectedAreasRef.value = [area]
|
||||
}
|
||||
},
|
||||
scrollToFirstArea () {
|
||||
|
||||
@@ -283,7 +283,7 @@ export default {
|
||||
IconDrag,
|
||||
IconLock
|
||||
},
|
||||
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withKba', 'withPayment', 't', 'fieldsDragFieldRef', 'baseFetch'],
|
||||
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withKba', 'withPayment', 't', 'fieldsDragFieldRef', 'baseFetch', 'selectedAreasRef'],
|
||||
props: {
|
||||
fields: {
|
||||
type: Array,
|
||||
@@ -610,6 +610,10 @@ export default {
|
||||
})
|
||||
})
|
||||
|
||||
field.areas?.forEach((area) => {
|
||||
this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(area), 1)
|
||||
})
|
||||
|
||||
if (save) {
|
||||
this.save()
|
||||
}
|
||||
|
||||
@@ -212,12 +212,17 @@ export default {
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
withClickSaveEvent: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
buildDefaultName: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['close'],
|
||||
emits: ['close', 'click-save'],
|
||||
data () {
|
||||
return {
|
||||
preferences: {}
|
||||
@@ -322,7 +327,11 @@ export default {
|
||||
|
||||
Object.assign(this.field.preferences, this.preferences)
|
||||
|
||||
this.save()
|
||||
if (this.withClickSaveEvent) {
|
||||
this.$emit('click-save')
|
||||
} else {
|
||||
this.save()
|
||||
}
|
||||
|
||||
this.$emit('close')
|
||||
}
|
||||
|
||||
@@ -187,7 +187,14 @@ const en = {
|
||||
sync: 'Sync',
|
||||
syncing: 'Syncing...',
|
||||
copy: 'Copy',
|
||||
paste: 'Paste'
|
||||
paste: 'Paste',
|
||||
select_fields: 'Select Fields',
|
||||
draw_fields: 'Draw Fields',
|
||||
align_left: 'Align Left',
|
||||
align_right: 'Align Right',
|
||||
align_top: 'Align Top',
|
||||
align_bottom: 'Align Bottom',
|
||||
fields_selected: '{count} Fields Selected'
|
||||
}
|
||||
|
||||
const es = {
|
||||
@@ -379,7 +386,14 @@ const es = {
|
||||
sync: 'Sincronizar',
|
||||
syncing: 'Sincronizando...',
|
||||
copy: 'Copiar',
|
||||
paste: 'Pegar'
|
||||
paste: 'Pegar',
|
||||
select_fields: 'Seleccionar Campos',
|
||||
draw_fields: 'Dibujar Campos',
|
||||
align_left: 'Alinear a la izquierda',
|
||||
align_right: 'Alinear a la derecha',
|
||||
align_top: 'Alinear arriba',
|
||||
align_bottom: 'Alinear abajo',
|
||||
fields_selected: '{count} Campos Seleccionados'
|
||||
}
|
||||
|
||||
const it = {
|
||||
@@ -571,7 +585,14 @@ const it = {
|
||||
sync: 'Sincronizza',
|
||||
syncing: 'Sincronizzazione...',
|
||||
copy: 'Copia',
|
||||
paste: 'Incolla'
|
||||
paste: 'Incolla',
|
||||
select_fields: 'Seleziona Campi',
|
||||
draw_fields: 'Disegna Campi',
|
||||
align_left: 'Allinea a sinistra',
|
||||
align_right: 'Allinea a destra',
|
||||
align_top: 'Allinea in alto',
|
||||
align_bottom: 'Allinea in basso',
|
||||
fields_selected: '{count} Campi Selezionati'
|
||||
}
|
||||
|
||||
const pt = {
|
||||
@@ -763,7 +784,14 @@ const pt = {
|
||||
sync: 'Sincronizar',
|
||||
syncing: 'Sincronizando...',
|
||||
copy: 'Copiar',
|
||||
paste: 'Colar'
|
||||
paste: 'Colar',
|
||||
select_fields: 'Selecionar Campos',
|
||||
draw_fields: 'Desenhar Campos',
|
||||
align_left: 'Alinhar à esquerda',
|
||||
align_right: 'Alinhar à direita',
|
||||
align_top: 'Alinhar ao topo',
|
||||
align_bottom: 'Alinhar à parte inferior',
|
||||
fields_selected: '{count} Campos Selecionados'
|
||||
}
|
||||
|
||||
const fr = {
|
||||
@@ -955,7 +983,14 @@ const fr = {
|
||||
sync: 'Synchroniser',
|
||||
syncing: 'Synchronisation...',
|
||||
copy: 'Copier',
|
||||
paste: 'Coller'
|
||||
paste: 'Coller',
|
||||
select_fields: 'Sélectionner Champs',
|
||||
draw_fields: 'Dessiner Champs',
|
||||
align_left: 'Aligner à gauche',
|
||||
align_right: 'Aligner à droite',
|
||||
align_top: 'Aligner en haut',
|
||||
align_bottom: 'Aligner en bas',
|
||||
fields_selected: '{count} Champs Sélectionnés'
|
||||
}
|
||||
|
||||
const de = {
|
||||
@@ -1147,7 +1182,14 @@ const de = {
|
||||
sync: 'Synchronisieren',
|
||||
syncing: 'Synchronisiere...',
|
||||
copy: 'Kopieren',
|
||||
paste: 'Einfügen'
|
||||
paste: 'Einfügen',
|
||||
select_fields: 'Felder Auswählen',
|
||||
draw_fields: 'Felder Zeichnen',
|
||||
align_left: 'Links ausrichten',
|
||||
align_right: 'Rechts ausrichten',
|
||||
align_top: 'Oben ausrichten',
|
||||
align_bottom: 'Unten ausrichten',
|
||||
fields_selected: '{count} Felder Ausgewählt'
|
||||
}
|
||||
|
||||
const nl = {
|
||||
@@ -1339,7 +1381,14 @@ const nl = {
|
||||
sync: 'Synchroniseren',
|
||||
syncing: 'Synchroniseren...',
|
||||
copy: 'Kopiëren',
|
||||
paste: 'Plakken'
|
||||
paste: 'Plakken',
|
||||
select_fields: 'Velden Selecteren',
|
||||
draw_fields: 'Velden Tekenen',
|
||||
align_left: 'Links uitlijnen',
|
||||
align_right: 'Rechts uitlijnen',
|
||||
align_top: 'Boven uitlijnen',
|
||||
align_bottom: 'Onder uitlijnen',
|
||||
fields_selected: '{count} Velden Geselecteerd'
|
||||
}
|
||||
|
||||
export { en, es, it, pt, fr, de, nl }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="relative select-none mb-4 before:border before:rounded before:top-0 before:bottom-0 before:left-0 before:right-0 before:absolute"
|
||||
:class="{ 'cursor-crosshair': allowDraw && editable, 'touch-none': !!drawField }"
|
||||
:class="{ 'cursor-crosshair': allowDraw && editable && !isSelectMode, 'touch-none': !!drawField }"
|
||||
style="container-type: size"
|
||||
:style="{ aspectRatio: `${width} / ${height}`}"
|
||||
>
|
||||
@@ -19,6 +19,18 @@
|
||||
@pointerdown="onStartDraw"
|
||||
@contextmenu="openContextMenu"
|
||||
>
|
||||
<SelectionBox
|
||||
v-if="showSelectionBox"
|
||||
:selection-box="selectionBox"
|
||||
:page-width="width"
|
||||
:page-height="height"
|
||||
:is-resizing="!!resizeDirection"
|
||||
:is-drawing="!!drawFieldType"
|
||||
:is-drag="isDrag"
|
||||
@move="onSelectionBoxMove"
|
||||
@contextmenu="openSelectionContextMenu"
|
||||
@close-context-menu="closeSelectionContextMenu"
|
||||
/>
|
||||
<FieldArea
|
||||
v-for="(item, i) in areas"
|
||||
:key="i"
|
||||
@@ -35,6 +47,7 @@
|
||||
:default-field="defaultFieldsIndex[item.field.name]"
|
||||
:default-submitters="defaultSubmitters"
|
||||
:max-page="totalPages - 1"
|
||||
:is-select-mode="isSelectMode"
|
||||
@start-resize="resizeDirection = $event"
|
||||
@stop-resize="resizeDirection = null"
|
||||
@remove="$emit('remove-area', item.area)"
|
||||
@@ -49,6 +62,11 @@
|
||||
:field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || dragFieldPlaceholder?.type || defaultFieldType }"
|
||||
:area="newArea"
|
||||
/>
|
||||
<div
|
||||
v-if="selectionRect"
|
||||
class="absolute outline-dashed outline-gray-400 pointer-events-none z-20"
|
||||
:style="selectionRectStyle"
|
||||
/>
|
||||
<ContextMenu
|
||||
v-if="contextMenu"
|
||||
:context-menu="contextMenu"
|
||||
@@ -59,13 +77,25 @@
|
||||
@paste="handlePaste"
|
||||
@close="closeContextMenu"
|
||||
/>
|
||||
<ContextMenu
|
||||
v-if="selectionContextMenu"
|
||||
:context-menu="selectionContextMenu"
|
||||
:editable="editable"
|
||||
:is-multi-selection="true"
|
||||
:selected-areas="selectedAreasRef.value"
|
||||
:template="template"
|
||||
@copy="handleSelectionCopy"
|
||||
@delete="handleSelectionDelete"
|
||||
@align="handleSelectionAlign"
|
||||
@close="closeSelectionContextMenu"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-show="resizeDirection || isDrag || showMask || (drawField && isMobile) || fieldsDragFieldRef.value"
|
||||
v-show="resizeDirection || isDrag || showMask || (drawField && isMobile) || fieldsDragFieldRef.value || selectionRect"
|
||||
id="mask"
|
||||
ref="mask"
|
||||
class="top-0 bottom-0 left-0 right-0 absolute"
|
||||
:class="{ 'z-10': !isMobile, 'cursor-grab': isDrag, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
|
||||
:class="{ 'z-10': !isMobile, 'cursor-grab': isDrag, 'cursor-nwse-resize': drawField && !isSelectMode, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
|
||||
@pointermove="onPointermove"
|
||||
@pointerdown="onStartDraw"
|
||||
@contextmenu="openContextMenu"
|
||||
@@ -81,14 +111,16 @@
|
||||
<script>
|
||||
import FieldArea from './area'
|
||||
import ContextMenu from './context_menu'
|
||||
import SelectionBox from './selection_box'
|
||||
|
||||
export default {
|
||||
name: 'TemplatePage',
|
||||
components: {
|
||||
FieldArea,
|
||||
ContextMenu
|
||||
ContextMenu,
|
||||
SelectionBox
|
||||
},
|
||||
inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'assignDropAreaSize', 'selectedAreaRef'],
|
||||
inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'assignDropAreaSize', 'selectedAreasRef', 'template', 'isSelectModeRef'],
|
||||
props: {
|
||||
image: {
|
||||
type: Object,
|
||||
@@ -170,19 +202,64 @@ export default {
|
||||
number: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
attachmentUuid: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['draw', 'drop-field', 'remove-area', 'copy-field', 'paste-field', 'scroll-to'],
|
||||
emits: ['draw', 'drop-field', 'remove-area', 'copy-field', 'paste-field', 'scroll-to', 'copy-selected-areas', 'delete-selected-areas', 'align-selected-areas'],
|
||||
data () {
|
||||
return {
|
||||
areaRefs: [],
|
||||
showMask: false,
|
||||
resizeDirection: null,
|
||||
newArea: null,
|
||||
contextMenu: null
|
||||
contextMenu: null,
|
||||
selectionRect: null,
|
||||
selectionContextMenu: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isSelectMode () {
|
||||
return this.isSelectModeRef.value && !this.drawFieldType && this.editable
|
||||
},
|
||||
pageSelectedAreas () {
|
||||
if (!this.selectedAreasRef.value) return []
|
||||
|
||||
return this.selectedAreasRef.value.filter((a) =>
|
||||
a.attachment_uuid === this.attachmentUuid && a.page === this.number
|
||||
)
|
||||
},
|
||||
showSelectionBox () {
|
||||
return this.pageSelectedAreas.length >= 2 && this.editable
|
||||
},
|
||||
minSelectionBoxHeight () {
|
||||
const ys = this.pageSelectedAreas.map((a) => a.y)
|
||||
|
||||
return Math.max(...ys) - Math.min(...ys)
|
||||
},
|
||||
minSelectionBoxWidth () {
|
||||
const xs = this.pageSelectedAreas.map((a) => a.x)
|
||||
|
||||
return Math.max(...xs) - Math.min(...xs)
|
||||
},
|
||||
selectionBox () {
|
||||
if (!this.pageSelectedAreas.length) return null
|
||||
|
||||
const minX = Math.min(...this.pageSelectedAreas.map((a) => a.x))
|
||||
const minY = Math.min(...this.pageSelectedAreas.map((a) => a.y))
|
||||
const maxX = Math.max(...this.pageSelectedAreas.map((a) => a.x + a.w))
|
||||
const maxY = Math.max(...this.pageSelectedAreas.map((a) => a.y + a.h))
|
||||
|
||||
return {
|
||||
x: minX,
|
||||
y: minY,
|
||||
w: Math.max(maxX - minX, this.minSelectionBoxWidth),
|
||||
h: Math.max(maxY - minY, this.minSelectionBoxHeight)
|
||||
}
|
||||
},
|
||||
defaultFieldsIndex () {
|
||||
return this.defaultFields.reduce((acc, field) => {
|
||||
acc[field.name] = field
|
||||
@@ -217,6 +294,16 @@ export default {
|
||||
},
|
||||
height () {
|
||||
return this.image.metadata.height
|
||||
},
|
||||
selectionRectStyle () {
|
||||
if (!this.selectionRect) return {}
|
||||
|
||||
return {
|
||||
left: this.selectionRect.x * 100 + '%',
|
||||
top: this.selectionRect.y * 100 + '%',
|
||||
width: this.selectionRect.w * 100 + '%',
|
||||
height: this.selectionRect.h * 100 + '%'
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUpdate () {
|
||||
@@ -269,6 +356,31 @@ export default {
|
||||
field
|
||||
}
|
||||
},
|
||||
openSelectionContextMenu (event) {
|
||||
const rect = this.$el.getBoundingClientRect()
|
||||
|
||||
this.selectionContextMenu = {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
relativeX: (event.clientX - rect.left) / rect.width,
|
||||
relativeY: (event.clientY - rect.top) / rect.height
|
||||
}
|
||||
},
|
||||
closeSelectionContextMenu () {
|
||||
this.selectionContextMenu = null
|
||||
},
|
||||
handleSelectionCopy () {
|
||||
this.$emit('copy-selected-areas')
|
||||
this.closeSelectionContextMenu()
|
||||
},
|
||||
handleSelectionDelete () {
|
||||
this.$emit('delete-selected-areas')
|
||||
this.closeSelectionContextMenu()
|
||||
},
|
||||
handleSelectionAlign (direction) {
|
||||
this.$emit('align-selected-areas', direction)
|
||||
this.closeSelectionContextMenu()
|
||||
},
|
||||
closeContextMenu () {
|
||||
this.contextMenu = null
|
||||
this.newArea = null
|
||||
@@ -276,7 +388,7 @@ export default {
|
||||
},
|
||||
handleCopy () {
|
||||
if (this.contextMenu.area) {
|
||||
this.selectedAreaRef.value = this.contextMenu.area
|
||||
this.selectedAreasRef.value = [this.contextMenu.area]
|
||||
|
||||
this.$emit('copy-field')
|
||||
}
|
||||
@@ -338,6 +450,16 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.selectedAreasRef.value.length >= 2) {
|
||||
this.selectedAreasRef.value = []
|
||||
}
|
||||
|
||||
if (this.isSelectMode) {
|
||||
this.startSelectionRect(e)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.allowDraw) {
|
||||
return
|
||||
}
|
||||
@@ -363,7 +485,69 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
startSelectionRect (e) {
|
||||
this.selectedAreasRef.value = []
|
||||
|
||||
this.showMask = true
|
||||
|
||||
this.$nextTick(() => {
|
||||
const x = e.offsetX / this.$refs.mask.clientWidth
|
||||
const y = e.offsetY / this.$refs.mask.clientHeight
|
||||
|
||||
this.selectionRect = {
|
||||
initialX: x,
|
||||
initialY: y,
|
||||
x,
|
||||
y,
|
||||
w: 0,
|
||||
h: 0
|
||||
}
|
||||
})
|
||||
},
|
||||
onSelectionBoxMove (dx, dy) {
|
||||
let clampedDx = dx
|
||||
let clampedDy = dy
|
||||
|
||||
this.pageSelectedAreas.forEach((area) => {
|
||||
const maxDxLeft = -area.x
|
||||
const maxDxRight = 1 - area.w - area.x
|
||||
const maxDyTop = -area.y
|
||||
const maxDyBottom = 1 - area.h - area.y
|
||||
|
||||
if (dx < maxDxLeft) clampedDx = Math.max(clampedDx, maxDxLeft)
|
||||
if (dx > maxDxRight) clampedDx = Math.min(clampedDx, maxDxRight)
|
||||
if (dy < maxDyTop) clampedDy = Math.max(clampedDy, maxDyTop)
|
||||
if (dy > maxDyBottom) clampedDy = Math.min(clampedDy, maxDyBottom)
|
||||
})
|
||||
|
||||
this.pageSelectedAreas.forEach((area) => {
|
||||
area.x += clampedDx
|
||||
area.y += clampedDy
|
||||
})
|
||||
},
|
||||
onPointermove (e) {
|
||||
if (this.selectionRect) {
|
||||
const dx = e.offsetX / this.$refs.mask.clientWidth - this.selectionRect.initialX
|
||||
const dy = e.offsetY / this.$refs.mask.clientHeight - this.selectionRect.initialY
|
||||
|
||||
if (dx > 0) {
|
||||
this.selectionRect.x = this.selectionRect.initialX
|
||||
} else {
|
||||
this.selectionRect.x = e.offsetX / this.$refs.mask.clientWidth
|
||||
}
|
||||
|
||||
if (dy > 0) {
|
||||
this.selectionRect.y = this.selectionRect.initialY
|
||||
} else {
|
||||
this.selectionRect.y = e.offsetY / this.$refs.mask.clientHeight
|
||||
}
|
||||
|
||||
this.selectionRect.w = Math.abs(dx)
|
||||
this.selectionRect.h = Math.abs(dy)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (this.newArea) {
|
||||
const dx = e.offsetX / this.$refs.mask.clientWidth - this.newArea.initialX
|
||||
const dy = e.offsetY / this.$refs.mask.clientHeight - this.newArea.initialY
|
||||
@@ -389,7 +573,20 @@ export default {
|
||||
}
|
||||
},
|
||||
onPointerup (e) {
|
||||
if (this.newArea) {
|
||||
if (this.selectionRect) {
|
||||
const selRect = this.selectionRect
|
||||
const areasToSelect = this.areas || []
|
||||
|
||||
areasToSelect.forEach((item) => {
|
||||
const area = item.area
|
||||
|
||||
if (this.rectsOverlap(selRect, area)) {
|
||||
this.selectedAreasRef.value.push(area)
|
||||
}
|
||||
})
|
||||
|
||||
this.selectionRect = null
|
||||
} else if (this.newArea) {
|
||||
const area = {
|
||||
x: this.newArea.x,
|
||||
y: this.newArea.y,
|
||||
@@ -412,6 +609,14 @@ export default {
|
||||
|
||||
this.showMask = false
|
||||
this.newArea = null
|
||||
},
|
||||
rectsOverlap (r1, r2) {
|
||||
return !(
|
||||
r1.x + r1.w < r2.x ||
|
||||
r2.x + r2.w < r1.x ||
|
||||
r1.y + r1.h < r2.y ||
|
||||
r2.y + r2.h < r1.y
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div
|
||||
class="absolute outline-dashed outline-gray-400 cursor-move"
|
||||
:class="[isResizing || isCmdKeyRef.value ? 'z-0' : 'z-20', { 'pointer-events-none': isDrawing || fieldsDragFieldRef.value || isDrag }]"
|
||||
:style="positionStyle"
|
||||
@pointerdown.stop.prevent="onPointerDown"
|
||||
@contextmenu.stop.prevent="openContextMenu"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SelectionBox',
|
||||
inject: ['save', 'selectedAreasRef', 'isCmdKeyRef', 'fieldsDragFieldRef'],
|
||||
props: {
|
||||
selectionBox: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
pageWidth: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
pageHeight: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
isResizing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isDrag: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isDrawing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['move', 'contextmenu', 'close-context-menu'],
|
||||
data () {
|
||||
return {
|
||||
isDragging: false,
|
||||
dragStart: { x: 0, y: 0 }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
positionStyle () {
|
||||
const { x, y, w, h } = this.selectionBox
|
||||
|
||||
return {
|
||||
top: y * 100 + '%',
|
||||
left: x * 100 + '%',
|
||||
width: w * 100 + '%',
|
||||
height: h * 100 + '%'
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openContextMenu (e) {
|
||||
this.$emit('contextmenu', e)
|
||||
},
|
||||
onPointerDown (e) {
|
||||
this.$emit('close-context-menu')
|
||||
|
||||
this.startDrag(e)
|
||||
},
|
||||
startDrag (e) {
|
||||
this.isDragging = true
|
||||
this.dragStart = { x: e.clientX, y: e.clientY }
|
||||
|
||||
document.addEventListener('pointermove', this.onDrag)
|
||||
document.addEventListener('pointerup', this.stopDrag)
|
||||
},
|
||||
onDrag (e) {
|
||||
if (!this.isDragging) return
|
||||
|
||||
const parent = this.$el.parentElement
|
||||
|
||||
const rect = parent.getBoundingClientRect()
|
||||
|
||||
const dx = (e.clientX - this.dragStart.x) / rect.width
|
||||
const dy = (e.clientY - this.dragStart.y) / rect.height
|
||||
|
||||
this.$emit('move', dx, dy)
|
||||
|
||||
this.dragStart = { x: e.clientX, y: e.clientY }
|
||||
},
|
||||
stopDrag () {
|
||||
this.isDragging = false
|
||||
|
||||
document.removeEventListener('pointermove', this.onDrag)
|
||||
document.removeEventListener('pointerup', this.stopDrag)
|
||||
|
||||
this.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user