add context menu to template builder
This commit is contained in:
@@ -376,6 +376,8 @@
|
|||||||
@draw="[onDraw($event), withSelectedFieldType ? '' : drawFieldType = '', showDrawField = false]"
|
@draw="[onDraw($event), withSelectedFieldType ? '' : drawFieldType = '', showDrawField = false]"
|
||||||
@drop-field="onDropfield"
|
@drop-field="onDropfield"
|
||||||
@remove-area="removeArea"
|
@remove-area="removeArea"
|
||||||
|
@paste-field="pasteField"
|
||||||
|
@copy-field="copyField"
|
||||||
/>
|
/>
|
||||||
<DocumentControls
|
<DocumentControls
|
||||||
v-if="isBreakpointLg && editable"
|
v-if="isBreakpointLg && editable"
|
||||||
@@ -852,7 +854,6 @@ export default {
|
|||||||
showDrawField: false,
|
showDrawField: false,
|
||||||
pendingFieldAttachmentUuids: [],
|
pendingFieldAttachmentUuids: [],
|
||||||
drawField: null,
|
drawField: null,
|
||||||
copiedArea: null,
|
|
||||||
drawFieldType: null,
|
drawFieldType: null,
|
||||||
drawOption: null,
|
drawOption: null,
|
||||||
dragField: null,
|
dragField: null,
|
||||||
@@ -1414,8 +1415,8 @@ export default {
|
|||||||
} else if ((event.ctrlKey || event.metaKey) && event.key === 'c' && document.activeElement === document.body) {
|
} else if ((event.ctrlKey || event.metaKey) && event.key === 'c' && document.activeElement === document.body) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
this.copiedArea = this.selectedAreaRef?.value
|
this.copyField()
|
||||||
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v' && this.copiedArea && document.activeElement === document.body) {
|
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v' && this.hasClipboardData() && document.activeElement === document.body) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
this.pasteField()
|
this.pasteField()
|
||||||
@@ -1497,47 +1498,116 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
pasteField () {
|
copyField () {
|
||||||
const field = this.template.fields.find((f) => f.areas?.includes(this.copiedArea))
|
const area = this.selectedAreaRef.value
|
||||||
const currentArea = this.selectedAreaRef?.value || this.copiedArea
|
|
||||||
|
if (!area) return
|
||||||
|
|
||||||
|
const field = this.template.fields.find((f) => f.areas?.includes(area))
|
||||||
|
|
||||||
|
if (!field) return
|
||||||
|
|
||||||
|
const clipboardData = {
|
||||||
|
field: JSON.parse(JSON.stringify(field)),
|
||||||
|
area: JSON.parse(JSON.stringify(area)),
|
||||||
|
templateId: this.template.id,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
delete clipboardData.field.areas
|
||||||
|
delete clipboardData.field.uuid
|
||||||
|
delete clipboardData.field.submitter_uuid
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem('docuseal_clipboard', JSON.stringify(clipboardData))
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to save clipboard:', e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pasteField (targetPosition = null) {
|
||||||
|
let field = null
|
||||||
|
let area = null
|
||||||
|
let isSameTemplate = false
|
||||||
|
|
||||||
|
const clipboard = localStorage.getItem('docuseal_clipboard')
|
||||||
|
|
||||||
|
if (clipboard) {
|
||||||
|
const data = JSON.parse(clipboard)
|
||||||
|
|
||||||
|
if (Date.now() - data.timestamp < 3600000) {
|
||||||
|
field = data.field
|
||||||
|
area = data.area
|
||||||
|
isSameTemplate = data.templateId === this.template.id
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('docuseal_clipboard')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!field || !area) return
|
||||||
|
|
||||||
|
if (!isSameTemplate) {
|
||||||
|
delete field.conditions
|
||||||
|
delete field.preferences?.formula
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentArea = this.selectedAreaRef.value
|
||||||
|
const defaultAttachmentUuid = this.template.schema[0]?.attachment_uuid
|
||||||
|
|
||||||
if (field && currentArea) {
|
if (field && currentArea) {
|
||||||
const area = {
|
const attachmentUuid = targetPosition?.attachment_uuid ||
|
||||||
...JSON.parse(JSON.stringify(this.copiedArea)),
|
(this.template.documents.find((d) => d.uuid === currentArea.attachment_uuid) ? currentArea.attachment_uuid : null) ||
|
||||||
attachment_uuid: currentArea.attachment_uuid,
|
defaultAttachmentUuid
|
||||||
page: currentArea.page,
|
|
||||||
x: currentArea.x,
|
const newArea = {
|
||||||
y: currentArea.y + currentArea.h * 1.3
|
...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)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['radio', 'multiple'].includes(field.type)) {
|
const newField = {
|
||||||
this.copiedArea.option_uuid ||= field.options[0].uuid
|
...JSON.parse(JSON.stringify(field)),
|
||||||
area.option_uuid = v4()
|
uuid: v4(),
|
||||||
|
submitter_uuid: this.selectedSubmitter.uuid,
|
||||||
const lastOption = field.options[field.options.length - 1]
|
areas: [newArea]
|
||||||
|
|
||||||
if (!field.areas.find((a) => lastOption.uuid === a.option_uuid)) {
|
|
||||||
area.option_uuid = lastOption.uuid
|
|
||||||
} else {
|
|
||||||
field.options.push({ uuid: area.option_uuid })
|
|
||||||
}
|
|
||||||
|
|
||||||
field.areas.push(area)
|
|
||||||
} else {
|
|
||||||
const newField = {
|
|
||||||
...JSON.parse(JSON.stringify(field)),
|
|
||||||
uuid: v4(),
|
|
||||||
areas: [area]
|
|
||||||
}
|
|
||||||
|
|
||||||
this.insertField(newField)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.selectedAreaRef.value = area
|
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)
|
||||||
|
|
||||||
|
this.selectedAreaRef.value = newArea
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
hasClipboardData () {
|
||||||
|
try {
|
||||||
|
const clipboard = localStorage.getItem('docuseal_clipboard')
|
||||||
|
|
||||||
|
if (clipboard) {
|
||||||
|
const data = JSON.parse(clipboard)
|
||||||
|
|
||||||
|
return Date.now() - data.timestamp < 3600000
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
pushUndo () {
|
pushUndo () {
|
||||||
const stringData = JSON.stringify(this.template)
|
const stringData = JSON.stringify(this.template)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,357 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-if="!isShowFormulaModal && !isShowFontModal && !isShowConditionsModal && !isShowDescriptionModal"
|
||||||
|
ref="menu"
|
||||||
|
class="fixed z-50 p-1 bg-white shadow-lg rounded-lg border border-base-300 min-w-[170px] cursor-default"
|
||||||
|
:style="menuStyle"
|
||||||
|
@mousedown.stop
|
||||||
|
@pointerdown.stop
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
v-if="showRequired"
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm cursor-pointer"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:checked="isRequired"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@change="handleToggleRequired($event.target.checked)"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<span>{{ t('required') }}</span>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
v-if="showReadOnly"
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm cursor-pointer"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:checked="isReadOnly"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
@change="handleToggleReadOnly($event.target.checked)"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<span>{{ t('read_only') }}</span>
|
||||||
|
</label>
|
||||||
|
<hr
|
||||||
|
v-if="(showRequired || showReadOnly) && (showFont || showDescription || showCondition || showFormula)"
|
||||||
|
class="my-1 border-base-300"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="showFont"
|
||||||
|
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="showDescription"
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="openDescriptionModal"
|
||||||
|
>
|
||||||
|
<IconInfoCircle class="w-4 h-4" />
|
||||||
|
<span>{{ t('description') }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="showCondition"
|
||||||
|
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>
|
||||||
|
<button
|
||||||
|
v-if="showFormula"
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="openFormulaModal"
|
||||||
|
>
|
||||||
|
<IconMathFunction class="w-4 h-4" />
|
||||||
|
<span>{{ t('formula') }}</span>
|
||||||
|
</button>
|
||||||
|
<hr
|
||||||
|
v-if="(showFont || showDescription || showCondition || showFormula) && (showCopy || showDelete || showPaste)"
|
||||||
|
class="my-1 border-base-300"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="showCopy"
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center justify-between text-sm"
|
||||||
|
@click.stop="$emit('copy')"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x-2">
|
||||||
|
<IconCopy class="w-4 h-4" />
|
||||||
|
<span>{{ t('copy') }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘C' : 'Ctrl+C' }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="showDelete"
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center justify-between text-sm text-red-600"
|
||||||
|
@click.stop="$emit('delete')"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x-2">
|
||||||
|
<IconTrashX class="w-4 h-4" />
|
||||||
|
<span>{{ t('remove') }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-base-content/60 ml-4">Del</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="showPaste"
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center justify-between text-sm"
|
||||||
|
:class="!hasClipboardData ? 'opacity-50 cursor-not-allowed' : 'hover:bg-base-100'"
|
||||||
|
:disabled="!hasClipboardData"
|
||||||
|
@click.stop="!hasClipboardData ? null : $emit('paste')"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x-2">
|
||||||
|
<IconClipboard class="w-4 h-4" />
|
||||||
|
<span>{{ t('paste') }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘V' : 'Ctrl+V' }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowFormulaModal"
|
||||||
|
to="#docuseal_modal_container"
|
||||||
|
>
|
||||||
|
<FormulaModal
|
||||||
|
:field="field"
|
||||||
|
:editable="editable"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
@close="closeModal"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowFontModal"
|
||||||
|
to="#docuseal_modal_container"
|
||||||
|
>
|
||||||
|
<FontModal
|
||||||
|
:field="field"
|
||||||
|
:area="contextMenu.area"
|
||||||
|
:editable="editable"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
@close="closeModal"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowConditionsModal"
|
||||||
|
to="#docuseal_modal_container"
|
||||||
|
>
|
||||||
|
<ConditionsModal
|
||||||
|
:item="field"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
@close="closeModal"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowDescriptionModal"
|
||||||
|
to="#docuseal_modal_container"
|
||||||
|
>
|
||||||
|
<DescriptionModal
|
||||||
|
:field="field"
|
||||||
|
:editable="editable"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
@close="closeModal"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconCopy, IconClipboard, IconTrashX, IconTypography, IconInfoCircle, IconRouteAltLeft, IconMathFunction } from '@tabler/icons-vue'
|
||||||
|
import FormulaModal from './formula_modal'
|
||||||
|
import FontModal from './font_modal'
|
||||||
|
import ConditionsModal from './conditions_modal'
|
||||||
|
import DescriptionModal from './description_modal'
|
||||||
|
import Field from './field'
|
||||||
|
import FieldType from './field_type.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ContextMenu',
|
||||||
|
components: {
|
||||||
|
IconCopy,
|
||||||
|
IconClipboard,
|
||||||
|
IconTrashX,
|
||||||
|
IconTypography,
|
||||||
|
IconInfoCircle,
|
||||||
|
IconRouteAltLeft,
|
||||||
|
IconMathFunction,
|
||||||
|
FormulaModal,
|
||||||
|
FontModal,
|
||||||
|
ConditionsModal,
|
||||||
|
DescriptionModal
|
||||||
|
},
|
||||||
|
inject: ['t', 'save'],
|
||||||
|
props: {
|
||||||
|
contextMenu: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['copy', 'paste', 'delete', 'close'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isShowFormulaModal: false,
|
||||||
|
isShowFontModal: false,
|
||||||
|
isShowConditionsModal: false,
|
||||||
|
isShowDescriptionModal: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
|
fieldLabels: FieldType.computed.fieldLabels,
|
||||||
|
isMac () {
|
||||||
|
return (navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')
|
||||||
|
},
|
||||||
|
hasClipboardData () {
|
||||||
|
try {
|
||||||
|
const clipboard = localStorage.getItem('docuseal_clipboard')
|
||||||
|
|
||||||
|
if (clipboard) {
|
||||||
|
const data = JSON.parse(clipboard)
|
||||||
|
|
||||||
|
return Date.now() - data.timestamp < 3600000
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
menuStyle () {
|
||||||
|
return {
|
||||||
|
left: this.contextMenu.x + 'px',
|
||||||
|
top: this.contextMenu.y + 'px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showCopy () {
|
||||||
|
return !!this.contextMenu.area
|
||||||
|
},
|
||||||
|
showPaste () {
|
||||||
|
return !this.contextMenu.area
|
||||||
|
},
|
||||||
|
showDelete () {
|
||||||
|
return !!this.contextMenu.area
|
||||||
|
},
|
||||||
|
showFont () {
|
||||||
|
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.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 () {
|
||||||
|
return this.field?.required || false
|
||||||
|
},
|
||||||
|
isReadOnly () {
|
||||||
|
return this.field?.readonly || false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
document.addEventListener('keydown', this.onKeyDown)
|
||||||
|
document.addEventListener('mousedown', this.handleClickOutside)
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.checkMenuPosition()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
document.removeEventListener('keydown', this.onKeyDown)
|
||||||
|
document.removeEventListener('mousedown', this.handleClickOutside)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
buildDefaultName: Field.methods.buildDefaultName,
|
||||||
|
checkMenuPosition () {
|
||||||
|
if (this.$refs.menu) {
|
||||||
|
const rect = this.$refs.menu.getBoundingClientRect()
|
||||||
|
|
||||||
|
if (rect.bottom > window.innerHeight) {
|
||||||
|
this.contextMenu.y = this.contextMenu.y - rect.height
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rect.right > window.innerWidth) {
|
||||||
|
this.contextMenu.x = this.contextMenu.x - rect.width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleToggleRequired (value) {
|
||||||
|
if (this.field) {
|
||||||
|
this.field.required = value
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleToggleReadOnly (value) {
|
||||||
|
if (this.field) {
|
||||||
|
this.field.readonly = value
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onKeyDown (event) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
this.$emit('close')
|
||||||
|
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v' && this.hasClipboardData) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
this.$emit('paste')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleClickOutside (event) {
|
||||||
|
if (this.$refs.menu && !this.$refs.menu.contains(event.target)) {
|
||||||
|
this.$emit('close')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openFontModal () {
|
||||||
|
this.isShowFontModal = true
|
||||||
|
},
|
||||||
|
openDescriptionModal () {
|
||||||
|
this.isShowDescriptionModal = true
|
||||||
|
},
|
||||||
|
openConditionModal () {
|
||||||
|
this.isShowConditionsModal = true
|
||||||
|
},
|
||||||
|
openFormulaModal () {
|
||||||
|
this.isShowFormulaModal = true
|
||||||
|
},
|
||||||
|
closeModal () {
|
||||||
|
this.isShowFormulaModal = false
|
||||||
|
this.isShowFontModal = false
|
||||||
|
this.isShowConditionsModal = false
|
||||||
|
this.isShowDescriptionModal = false
|
||||||
|
|
||||||
|
this.$emit('close')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -22,8 +22,10 @@
|
|||||||
:selected-submitter="selectedSubmitter"
|
:selected-submitter="selectedSubmitter"
|
||||||
:total-pages="sortedPreviewImages.length"
|
:total-pages="sortedPreviewImages.length"
|
||||||
:image="image"
|
:image="image"
|
||||||
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
|
@drop-field="$emit('drop-field', { ...$event, attachment_uuid: document.uuid })"
|
||||||
@remove-area="$emit('remove-area', $event)"
|
@remove-area="$emit('remove-area', $event)"
|
||||||
|
@copy-field="$emit('copy-field', $event)"
|
||||||
|
@paste-field="$emit('paste-field', { ...$event, attachment_uuid: document.uuid })"
|
||||||
@scroll-to="scrollToArea"
|
@scroll-to="scrollToArea"
|
||||||
@draw="$emit('draw', { area: {...$event.area, attachment_uuid: document.uuid }, isTooSmall: $event.isTooSmall })"
|
@draw="$emit('draw', { area: {...$event.area, attachment_uuid: document.uuid }, isTooSmall: $event.isTooSmall })"
|
||||||
/>
|
/>
|
||||||
@@ -118,7 +120,7 @@ export default {
|
|||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['draw', 'drop-field', 'remove-area'],
|
emits: ['draw', 'drop-field', 'remove-area', 'paste-field', 'copy-field'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
pageRefs: []
|
pageRefs: []
|
||||||
|
|||||||
@@ -185,7 +185,9 @@ const en = {
|
|||||||
start_tour: 'Start Tour',
|
start_tour: 'Start Tour',
|
||||||
or_add_from: 'Or add from',
|
or_add_from: 'Or add from',
|
||||||
sync: 'Sync',
|
sync: 'Sync',
|
||||||
syncing: 'Syncing...'
|
syncing: 'Syncing...',
|
||||||
|
copy: 'Copy',
|
||||||
|
paste: 'Paste'
|
||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
@@ -375,7 +377,9 @@ const es = {
|
|||||||
start_tour: 'Iniciar guía',
|
start_tour: 'Iniciar guía',
|
||||||
or_add_from: 'O agregar desde',
|
or_add_from: 'O agregar desde',
|
||||||
sync: 'Sincronizar',
|
sync: 'Sincronizar',
|
||||||
syncing: 'Sincronizando...'
|
syncing: 'Sincronizando...',
|
||||||
|
copy: 'Copiar',
|
||||||
|
paste: 'Pegar'
|
||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
@@ -565,7 +569,9 @@ const it = {
|
|||||||
start_tour: 'Inizia il tour',
|
start_tour: 'Inizia il tour',
|
||||||
or_add_from: 'O aggiungi da',
|
or_add_from: 'O aggiungi da',
|
||||||
sync: 'Sincronizza',
|
sync: 'Sincronizza',
|
||||||
syncing: 'Sincronizzazione...'
|
syncing: 'Sincronizzazione...',
|
||||||
|
copy: 'Copia',
|
||||||
|
paste: 'Incolla'
|
||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
@@ -755,7 +761,9 @@ const pt = {
|
|||||||
start_tour: 'Iniciar tour',
|
start_tour: 'Iniciar tour',
|
||||||
or_add_from: 'Ou adicionar de',
|
or_add_from: 'Ou adicionar de',
|
||||||
sync: 'Sincronizar',
|
sync: 'Sincronizar',
|
||||||
syncing: 'Sincronizando...'
|
syncing: 'Sincronizando...',
|
||||||
|
copy: 'Copiar',
|
||||||
|
paste: 'Colar'
|
||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
@@ -945,7 +953,9 @@ const fr = {
|
|||||||
start_tour: 'Démarrer',
|
start_tour: 'Démarrer',
|
||||||
or_add_from: 'Ou ajouter depuis',
|
or_add_from: 'Ou ajouter depuis',
|
||||||
sync: 'Synchroniser',
|
sync: 'Synchroniser',
|
||||||
syncing: 'Synchronisation...'
|
syncing: 'Synchronisation...',
|
||||||
|
copy: 'Copier',
|
||||||
|
paste: 'Coller'
|
||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
@@ -1135,7 +1145,9 @@ const de = {
|
|||||||
start_tour: 'Tour starten',
|
start_tour: 'Tour starten',
|
||||||
or_add_from: 'Oder hinzufügen aus',
|
or_add_from: 'Oder hinzufügen aus',
|
||||||
sync: 'Synchronisieren',
|
sync: 'Synchronisieren',
|
||||||
syncing: 'Synchronisiere...'
|
syncing: 'Synchronisiere...',
|
||||||
|
copy: 'Kopieren',
|
||||||
|
paste: 'Einfügen'
|
||||||
}
|
}
|
||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
@@ -1325,7 +1337,9 @@ const nl = {
|
|||||||
start_tour: 'Rondleiding starten',
|
start_tour: 'Rondleiding starten',
|
||||||
or_add_from: 'Of toevoegen van',
|
or_add_from: 'Of toevoegen van',
|
||||||
sync: 'Synchroniseren',
|
sync: 'Synchroniseren',
|
||||||
syncing: 'Synchroniseren...'
|
syncing: 'Synchroniseren...',
|
||||||
|
copy: 'Kopiëren',
|
||||||
|
paste: 'Plakken'
|
||||||
}
|
}
|
||||||
|
|
||||||
export { en, es, it, pt, fr, de, nl }
|
export { en, es, it, pt, fr, de, nl }
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
<div
|
<div
|
||||||
class="top-0 bottom-0 left-0 right-0 absolute"
|
class="top-0 bottom-0 left-0 right-0 absolute"
|
||||||
@pointerdown="onStartDraw"
|
@pointerdown="onStartDraw"
|
||||||
|
@contextmenu="openContextMenu"
|
||||||
>
|
>
|
||||||
<FieldArea
|
<FieldArea
|
||||||
v-for="(item, i) in areas"
|
v-for="(item, i) in areas"
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
@stop-resize="resizeDirection = null"
|
@stop-resize="resizeDirection = null"
|
||||||
@remove="$emit('remove-area', item.area)"
|
@remove="$emit('remove-area', item.area)"
|
||||||
@scroll-to="$emit('scroll-to', $event)"
|
@scroll-to="$emit('scroll-to', $event)"
|
||||||
|
@contextmenu="openAreaContextMenu($event, item.area, item.field)"
|
||||||
/>
|
/>
|
||||||
<FieldArea
|
<FieldArea
|
||||||
v-if="newArea"
|
v-if="newArea"
|
||||||
@@ -47,6 +49,16 @@
|
|||||||
:field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || dragFieldPlaceholder?.type || defaultFieldType }"
|
:field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || dragFieldPlaceholder?.type || defaultFieldType }"
|
||||||
:area="newArea"
|
:area="newArea"
|
||||||
/>
|
/>
|
||||||
|
<ContextMenu
|
||||||
|
v-if="contextMenu"
|
||||||
|
:context-menu="contextMenu"
|
||||||
|
:field="contextMenu.field"
|
||||||
|
:editable="editable"
|
||||||
|
@copy="handleCopy"
|
||||||
|
@delete="handleDelete"
|
||||||
|
@paste="handlePaste"
|
||||||
|
@close="closeContextMenu"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-show="resizeDirection || isDrag || showMask || (drawField && isMobile) || fieldsDragFieldRef.value"
|
v-show="resizeDirection || isDrag || showMask || (drawField && isMobile) || fieldsDragFieldRef.value"
|
||||||
@@ -56,6 +68,7 @@
|
|||||||
: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, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
|
||||||
@pointermove="onPointermove"
|
@pointermove="onPointermove"
|
||||||
@pointerdown="onStartDraw"
|
@pointerdown="onStartDraw"
|
||||||
|
@contextmenu="openContextMenu"
|
||||||
@dragover.prevent="onDragover"
|
@dragover.prevent="onDragover"
|
||||||
@dragenter="onDragenter"
|
@dragenter="onDragenter"
|
||||||
@dragleave="newArea = null"
|
@dragleave="newArea = null"
|
||||||
@@ -67,13 +80,15 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import FieldArea from './area'
|
import FieldArea from './area'
|
||||||
|
import ContextMenu from './context_menu'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplatePage',
|
name: 'TemplatePage',
|
||||||
components: {
|
components: {
|
||||||
FieldArea
|
FieldArea,
|
||||||
|
ContextMenu
|
||||||
},
|
},
|
||||||
inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'assignDropAreaSize'],
|
inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'assignDropAreaSize', 'selectedAreaRef'],
|
||||||
props: {
|
props: {
|
||||||
image: {
|
image: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -157,13 +172,14 @@ export default {
|
|||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['draw', 'drop-field', 'remove-area', 'scroll-to'],
|
emits: ['draw', 'drop-field', 'remove-area', 'copy-field', 'paste-field', 'scroll-to'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
areaRefs: [],
|
areaRefs: [],
|
||||||
showMask: false,
|
showMask: false,
|
||||||
resizeDirection: null,
|
resizeDirection: null,
|
||||||
newArea: null
|
newArea: null,
|
||||||
|
contextMenu: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -211,6 +227,81 @@ export default {
|
|||||||
this.image.metadata.width = e.target.naturalWidth
|
this.image.metadata.width = e.target.naturalWidth
|
||||||
this.image.metadata.height = e.target.naturalHeight
|
this.image.metadata.height = e.target.naturalHeight
|
||||||
},
|
},
|
||||||
|
openContextMenu (event) {
|
||||||
|
if (!this.editable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
const rect = this.$refs.image.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.newArea = null
|
||||||
|
this.showMask = false
|
||||||
|
|
||||||
|
this.contextMenu = {
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY,
|
||||||
|
relativeX: (event.clientX - rect.left) / rect.width,
|
||||||
|
relativeY: (event.clientY - rect.top) / rect.height
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openAreaContextMenu (event, area, field) {
|
||||||
|
if (!this.editable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
const rect = this.$refs.image.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.newArea = null
|
||||||
|
this.showMask = false
|
||||||
|
|
||||||
|
this.contextMenu = {
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY,
|
||||||
|
relativeX: (event.clientX - rect.left) / rect.width,
|
||||||
|
relativeY: (event.clientY - rect.top) / rect.height,
|
||||||
|
area,
|
||||||
|
field
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeContextMenu () {
|
||||||
|
this.contextMenu = null
|
||||||
|
this.newArea = null
|
||||||
|
this.showMask = false
|
||||||
|
},
|
||||||
|
handleCopy () {
|
||||||
|
if (this.contextMenu.area) {
|
||||||
|
this.selectedAreaRef.value = this.contextMenu.area
|
||||||
|
|
||||||
|
this.$emit('copy-field')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.closeContextMenu()
|
||||||
|
},
|
||||||
|
handleDelete () {
|
||||||
|
if (this.contextMenu.area) {
|
||||||
|
this.$emit('remove-area', this.contextMenu.area)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.closeContextMenu()
|
||||||
|
},
|
||||||
|
handlePaste () {
|
||||||
|
this.newArea = null
|
||||||
|
this.showMask = false
|
||||||
|
|
||||||
|
this.$emit('paste-field', {
|
||||||
|
page: this.number,
|
||||||
|
x: this.contextMenu.relativeX,
|
||||||
|
y: this.contextMenu.relativeY
|
||||||
|
})
|
||||||
|
|
||||||
|
this.closeContextMenu()
|
||||||
|
},
|
||||||
setAreaRefs (el) {
|
setAreaRefs (el) {
|
||||||
if (el) {
|
if (el) {
|
||||||
this.areaRefs.push(el)
|
this.areaRefs.push(el)
|
||||||
@@ -243,6 +334,10 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onStartDraw (e) {
|
onStartDraw (e) {
|
||||||
|
if (e.button === 2) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.allowDraw) {
|
if (!this.allowDraw) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user