|
|
|
@ -1,7 +1,13 @@
|
|
|
|
<template>
|
|
|
|
<template>
|
|
|
|
<div
|
|
|
|
<div
|
|
|
|
class="relative bg-white select-none mb-4 before:border before:rounded before:top-0 before:bottom-0 before:left-0 before:right-0 before:absolute"
|
|
|
|
class="relative bg-white 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': isDrawMode && editable }"
|
|
|
|
>
|
|
|
|
>
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
|
|
|
v-if="isDrawMode && editable && cursorHighlightCoords"
|
|
|
|
|
|
|
|
class="absolute pointer-events-none z-10 bg-black"
|
|
|
|
|
|
|
|
:style="{ width: '1px', height: cursorHighlightCoords.height + 'px', left: cursorHighlightCoords.x + 'px', top: cursorHighlightCoords.y + 'px' }"
|
|
|
|
|
|
|
|
/>
|
|
|
|
<div :style="{ zoom: containerWidth / sectionWidthPx }">
|
|
|
|
<div :style="{ zoom: containerWidth / sectionWidthPx }">
|
|
|
|
<section
|
|
|
|
<section
|
|
|
|
:id="section.id"
|
|
|
|
:id="section.id"
|
|
|
|
@ -45,7 +51,10 @@
|
|
|
|
:field="contextMenuField"
|
|
|
|
:field="contextMenuField"
|
|
|
|
:with-copy-to-all-pages="false"
|
|
|
|
:with-copy-to-all-pages="false"
|
|
|
|
@close="closeContextMenu"
|
|
|
|
@close="closeContextMenu"
|
|
|
|
|
|
|
|
@copy="onContextMenuCopy"
|
|
|
|
@delete="onContextMenuDelete"
|
|
|
|
@delete="onContextMenuDelete"
|
|
|
|
|
|
|
|
@add-custom-field="$emit('add-custom-field', $event)"
|
|
|
|
|
|
|
|
@set-draw="$emit('set-draw', $event)"
|
|
|
|
@save="save"
|
|
|
|
@save="save"
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
</Teleport>
|
|
|
|
</Teleport>
|
|
|
|
@ -54,6 +63,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
<script>
|
|
|
|
import { shallowRef } from 'vue'
|
|
|
|
import { shallowRef } from 'vue'
|
|
|
|
|
|
|
|
import { DOMSerializer, Fragment } from '@tiptap/pm/model'
|
|
|
|
import { v4 } from 'uuid'
|
|
|
|
import { v4 } from 'uuid'
|
|
|
|
import FieldContextMenu from './field_context_menu.vue'
|
|
|
|
import FieldContextMenu from './field_context_menu.vue'
|
|
|
|
import AreaTitle from './area_title.vue'
|
|
|
|
import AreaTitle from './area_title.vue'
|
|
|
|
@ -73,6 +83,11 @@ export default {
|
|
|
|
type: Object,
|
|
|
|
type: Object,
|
|
|
|
required: true
|
|
|
|
required: true
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
sectionRefs: {
|
|
|
|
|
|
|
|
type: Array,
|
|
|
|
|
|
|
|
required: false,
|
|
|
|
|
|
|
|
default: () => []
|
|
|
|
|
|
|
|
},
|
|
|
|
editable: {
|
|
|
|
editable: {
|
|
|
|
type: Boolean,
|
|
|
|
type: Boolean,
|
|
|
|
required: false,
|
|
|
|
required: false,
|
|
|
|
@ -101,19 +116,45 @@ export default {
|
|
|
|
required: false,
|
|
|
|
required: false,
|
|
|
|
default: null
|
|
|
|
default: null
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
drawField: {
|
|
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
|
|
required: false,
|
|
|
|
|
|
|
|
default: null
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
drawFieldType: {
|
|
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
|
|
required: false,
|
|
|
|
|
|
|
|
default: ''
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
drawCustomField: {
|
|
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
|
|
required: false,
|
|
|
|
|
|
|
|
default: null
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
renderHtmlForSaveRef: {
|
|
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
|
|
required: false,
|
|
|
|
|
|
|
|
default: null
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
drawOption: {
|
|
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
|
|
required: false,
|
|
|
|
|
|
|
|
default: null
|
|
|
|
|
|
|
|
},
|
|
|
|
attachmentUuid: {
|
|
|
|
attachmentUuid: {
|
|
|
|
type: String,
|
|
|
|
type: String,
|
|
|
|
required: false,
|
|
|
|
required: false,
|
|
|
|
default: null
|
|
|
|
default: null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
emits: ['update'],
|
|
|
|
emits: ['update', 'draw', 'set-draw', 'add-custom-field'],
|
|
|
|
data () {
|
|
|
|
data () {
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
isAreaDrag: false,
|
|
|
|
isAreaDrag: false,
|
|
|
|
areaToolbarCoords: null,
|
|
|
|
areaToolbarCoords: null,
|
|
|
|
dynamicMenuCoords: null,
|
|
|
|
dynamicMenuCoords: null,
|
|
|
|
contextMenu: null
|
|
|
|
contextMenu: null,
|
|
|
|
|
|
|
|
cursorHighlightCoords: null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
computed: {
|
|
|
|
@ -165,6 +206,9 @@ export default {
|
|
|
|
isDraggingField () {
|
|
|
|
isDraggingField () {
|
|
|
|
return !!(this.fieldsDragFieldRef?.value || this.customDragFieldRef?.value || this.dragField)
|
|
|
|
return !!(this.fieldsDragFieldRef?.value || this.customDragFieldRef?.value || this.dragField)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
isDrawMode () {
|
|
|
|
|
|
|
|
return !!(this.drawField || this.drawCustomField || this.drawFieldType)
|
|
|
|
|
|
|
|
},
|
|
|
|
selectedArea () {
|
|
|
|
selectedArea () {
|
|
|
|
return this.selectedAreasRef.value[0]
|
|
|
|
return this.selectedAreasRef.value[0]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
@ -193,15 +237,6 @@ export default {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
mounted () {
|
|
|
|
mounted () {
|
|
|
|
this.initEditor()
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
beforeUnmount () {
|
|
|
|
|
|
|
|
if (this.editor) {
|
|
|
|
|
|
|
|
this.editor.destroy()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
methods: {
|
|
|
|
|
|
|
|
async initEditor () {
|
|
|
|
|
|
|
|
this.editorRef.value = buildEditor({
|
|
|
|
this.editorRef.value = buildEditor({
|
|
|
|
dynamicAreaProps: {
|
|
|
|
dynamicAreaProps: {
|
|
|
|
template: this.template,
|
|
|
|
template: this.template,
|
|
|
|
@ -217,6 +252,7 @@ export default {
|
|
|
|
attachmentsIndex: this.attachmentsIndex,
|
|
|
|
attachmentsIndex: this.attachmentsIndex,
|
|
|
|
onFieldDrop: this.onFieldDrop,
|
|
|
|
onFieldDrop: this.onFieldDrop,
|
|
|
|
onFieldDestroy: this.onFieldDestroy,
|
|
|
|
onFieldDestroy: this.onFieldDestroy,
|
|
|
|
|
|
|
|
renderHtmlForSaveRef: this.renderHtmlForSaveRef,
|
|
|
|
editorOptions: {
|
|
|
|
editorOptions: {
|
|
|
|
element: this.$refs.editorElement,
|
|
|
|
element: this.$refs.editorElement,
|
|
|
|
editable: this.editable,
|
|
|
|
editable: this.editable,
|
|
|
|
@ -226,12 +262,96 @@ export default {
|
|
|
|
onBlur: () => { this.dynamicMenuCoords = null }
|
|
|
|
onBlur: () => { this.dynamicMenuCoords = null }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.editor.view.dom.addEventListener('paste', this.onEditorPaste, true)
|
|
|
|
|
|
|
|
this.editor.view.dom.addEventListener('pointerdown', this.onEditorPointerDown, true)
|
|
|
|
|
|
|
|
this.editor.view.dom.addEventListener('mousemove', this.onEditorMouseMove)
|
|
|
|
|
|
|
|
this.editor.view.dom.addEventListener('mouseleave', this.onEditorMouseLeave)
|
|
|
|
|
|
|
|
this.editor.view.dom.addEventListener('keydown', this.onEditorKeyDown)
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
beforeUnmount () {
|
|
|
|
|
|
|
|
if (this.editor) {
|
|
|
|
|
|
|
|
this.editor.view.dom.removeEventListener('paste', this.onEditorPaste, true)
|
|
|
|
|
|
|
|
this.editor.view.dom.removeEventListener('pointerdown', this.onEditorPointerDown, true)
|
|
|
|
|
|
|
|
this.editor.view.dom.removeEventListener('mousemove', this.onEditorMouseMove)
|
|
|
|
|
|
|
|
this.editor.view.dom.removeEventListener('mouseleave', this.onEditorMouseLeave)
|
|
|
|
|
|
|
|
this.editor.view.dom.removeEventListener('keydown', this.onEditorKeyDown)
|
|
|
|
|
|
|
|
this.editor.destroy()
|
|
|
|
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
methods: {
|
|
|
|
findAreaNodePos (areaUuid) {
|
|
|
|
findAreaNodePos (areaUuid) {
|
|
|
|
const el = this.editor.view.dom.querySelector(`[data-area-uuid="${areaUuid}"]`)
|
|
|
|
const el = this.editor.view.dom.querySelector(`[data-area-uuid="${areaUuid}"]`)
|
|
|
|
|
|
|
|
|
|
|
|
return this.editor.view.posAtDOM(el, 0)
|
|
|
|
return this.editor.view.posAtDOM(el, 0)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
getFieldInsertIndex (pos) {
|
|
|
|
|
|
|
|
const view = this.editor.view
|
|
|
|
|
|
|
|
const fields = this.template.fields || []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!fields.length) {
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let previousField = null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
view.state.doc.nodesBetween(0, pos, (node, nodePos) => {
|
|
|
|
|
|
|
|
if (node.type.name !== 'fieldNode') {
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (nodePos + node.nodeSize <= pos) {
|
|
|
|
|
|
|
|
previousField = this.fieldAreaIndex[node.attrs.areaUuid]?.field || previousField
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!previousField) {
|
|
|
|
|
|
|
|
previousField = this.getPreviousSectionField()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!previousField) {
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const previousFieldIndex = fields.indexOf(previousField)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return previousFieldIndex === -1 ? fields.length : previousFieldIndex + 1
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
insertFieldInTemplate (field, index) {
|
|
|
|
|
|
|
|
const currentFieldIndex = this.template.fields.indexOf(field)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (currentFieldIndex !== -1) {
|
|
|
|
|
|
|
|
return currentFieldIndex
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.template.fields.splice(index, 0, field)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return index
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
getLastFieldInSection (sectionRef) {
|
|
|
|
|
|
|
|
let lastField = null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sectionRef.editor.state.doc.descendants((node) => {
|
|
|
|
|
|
|
|
if (node.type.name === 'fieldNode') {
|
|
|
|
|
|
|
|
lastField = this.fieldAreaIndex[node.attrs.areaUuid]?.field || lastField
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return lastField
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
getPreviousSectionField () {
|
|
|
|
|
|
|
|
const sectionIndex = this.sectionRefs.indexOf(this)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (let index = sectionIndex - 1; index >= 0; index -= 1) {
|
|
|
|
|
|
|
|
const previousField = this.getLastFieldInSection(this.sectionRefs[index])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (previousField) {
|
|
|
|
|
|
|
|
return previousField
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
|
|
|
},
|
|
|
|
removeArea (area) {
|
|
|
|
removeArea (area) {
|
|
|
|
const { field } = this.fieldAreaIndex[area.uuid]
|
|
|
|
const { field } = this.fieldAreaIndex[area.uuid]
|
|
|
|
const areaIndex = field.areas.indexOf(area)
|
|
|
|
const areaIndex = field.areas.indexOf(area)
|
|
|
|
@ -362,6 +482,220 @@ export default {
|
|
|
|
closeContextMenu () {
|
|
|
|
closeContextMenu () {
|
|
|
|
this.contextMenu = null
|
|
|
|
this.contextMenu = null
|
|
|
|
},
|
|
|
|
},
|
|
|
|
|
|
|
|
onEditorMouseLeave () {
|
|
|
|
|
|
|
|
this.cursorHighlightCoords = null
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
onEditorKeyDown (event) {
|
|
|
|
|
|
|
|
if (event.key === 'Escape') {
|
|
|
|
|
|
|
|
this.editor.chain().setNodeSelection(0).blur().run()
|
|
|
|
|
|
|
|
this.deselectArea()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
onEditorMouseMove (event) {
|
|
|
|
|
|
|
|
if (!this.isDrawMode || !this.editable) {
|
|
|
|
|
|
|
|
this.cursorHighlightCoords = null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const view = this.editor?.view
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!view) return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const pos = view.posAtCoords({ left: event.clientX, top: event.clientY })
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!pos) {
|
|
|
|
|
|
|
|
this.cursorHighlightCoords = null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const coords = view.coordsAtPos(pos.pos)
|
|
|
|
|
|
|
|
const outerRect = this.$el.getBoundingClientRect()
|
|
|
|
|
|
|
|
const lineHeight = coords.bottom - coords.top
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.cursorHighlightCoords = {
|
|
|
|
|
|
|
|
x: coords.left - outerRect.left,
|
|
|
|
|
|
|
|
y: coords.top - outerRect.top,
|
|
|
|
|
|
|
|
height: lineHeight
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
getFieldNode (areaUuid) {
|
|
|
|
|
|
|
|
const pos = this.findAreaNodePos(areaUuid)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return this.editor.state.doc.nodeAt(pos)
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
serializeFieldNodeHtml (areaUuid) {
|
|
|
|
|
|
|
|
const node = this.getFieldNode(areaUuid)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!node) {
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const serializer = DOMSerializer.fromSchema(this.editor.state.schema)
|
|
|
|
|
|
|
|
const container = document.createElement('div')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
container.appendChild(serializer.serializeFragment(Fragment.from(node)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return container.innerHTML
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
async writeHtmlToClipboard ({ html, text }, clipboardData = null) {
|
|
|
|
|
|
|
|
if (clipboardData) {
|
|
|
|
|
|
|
|
clipboardData.setData('text/html', html)
|
|
|
|
|
|
|
|
clipboardData.setData('text/plain', text || html)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (navigator.clipboard?.write && window.ClipboardItem) {
|
|
|
|
|
|
|
|
await navigator.clipboard.write([
|
|
|
|
|
|
|
|
new ClipboardItem({
|
|
|
|
|
|
|
|
'text/html': new Blob([html], { type: 'text/html' }),
|
|
|
|
|
|
|
|
'text/plain': new Blob([text || html], { type: 'text/plain' })
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (navigator.clipboard?.writeText) {
|
|
|
|
|
|
|
|
await navigator.clipboard.writeText(text || html)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
async copyFieldAreaToClipboard (areaUuid, clipboardData = null) {
|
|
|
|
|
|
|
|
const html = this.serializeFieldNodeHtml(areaUuid)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!html) {
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
await this.writeHtmlToClipboard({ html, text: html }, clipboardData)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
|
|
console.error('Failed to copy dynamic field:', e)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
async onContextMenuCopy () {
|
|
|
|
|
|
|
|
await this.copyFieldAreaToClipboard(this.contextMenu.areaUuid)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.closeContextMenu()
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
buildCopiedField (payload) {
|
|
|
|
|
|
|
|
const field = JSON.parse(JSON.stringify(payload.field))
|
|
|
|
|
|
|
|
const area = {
|
|
|
|
|
|
|
|
...JSON.parse(JSON.stringify(payload.area)),
|
|
|
|
|
|
|
|
uuid: v4(),
|
|
|
|
|
|
|
|
attachment_uuid: this.attachmentUuid
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (payload.templateId !== this.template.id) {
|
|
|
|
|
|
|
|
delete field.conditions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (field.preferences) {
|
|
|
|
|
|
|
|
delete field.preferences.formula
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const newField = {
|
|
|
|
|
|
|
|
...field,
|
|
|
|
|
|
|
|
uuid: v4(),
|
|
|
|
|
|
|
|
submitter_uuid: this.selectedSubmitter.uuid,
|
|
|
|
|
|
|
|
areas: [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 }
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
area.option_uuid = optionsMap[oldOptionUuid] || newField.options[0]?.uuid
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { field: newField, area }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
onEditorPaste (event) {
|
|
|
|
|
|
|
|
const clipboardData = event.clipboardData
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!clipboardData) {
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const html = clipboardData.getData('text/html')
|
|
|
|
|
|
|
|
const text = clipboardData.getData('text/plain')
|
|
|
|
|
|
|
|
const clipboardHtml = html || (text.includes('<dynamic-field') ? text : '')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!clipboardHtml || !clipboardHtml.includes('data-field=')) {
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const container = document.createElement('div')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
container.innerHTML = clipboardHtml
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const fieldNodes = [...container.querySelectorAll('dynamic-field[data-field][data-area]')]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!fieldNodes.length) {
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const { selection } = this.editor.state
|
|
|
|
|
|
|
|
const { from, to } = selection
|
|
|
|
|
|
|
|
let insertIndex = this.getFieldInsertIndex(selection.node?.type.name === 'fieldNode' ? to : from)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let lastArea = null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fieldNodes.forEach((fieldNode) => {
|
|
|
|
|
|
|
|
const fieldValue = fieldNode.dataset.field
|
|
|
|
|
|
|
|
const areaValue = fieldNode.dataset.area
|
|
|
|
|
|
|
|
const templateId = fieldNode.dataset.templateId
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!fieldValue || !areaValue) {
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const { field, area } = this.buildCopiedField({
|
|
|
|
|
|
|
|
field: JSON.parse(fieldValue),
|
|
|
|
|
|
|
|
area: JSON.parse(areaValue),
|
|
|
|
|
|
|
|
templateId: Number(templateId)
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.insertFieldInTemplate(field, insertIndex)
|
|
|
|
|
|
|
|
insertIndex += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fieldNode.setAttribute('uuid', field.uuid)
|
|
|
|
|
|
|
|
fieldNode.setAttribute('area-uuid', area.uuid)
|
|
|
|
|
|
|
|
fieldNode.removeAttribute('data-field')
|
|
|
|
|
|
|
|
fieldNode.removeAttribute('data-area')
|
|
|
|
|
|
|
|
fieldNode.removeAttribute('data-template-id')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lastArea = area
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.editor.chain().focus().insertContentAt({ from, to }, container.innerHTML).run()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (lastArea) {
|
|
|
|
|
|
|
|
this.editor.commands.setNodeSelection(this.findAreaNodePos(lastArea.uuid))
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.closeContextMenu()
|
|
|
|
|
|
|
|
this.save()
|
|
|
|
|
|
|
|
},
|
|
|
|
onContextMenuDelete () {
|
|
|
|
onContextMenuDelete () {
|
|
|
|
const menu = this.contextMenu
|
|
|
|
const menu = this.contextMenu
|
|
|
|
const fieldArea = this.fieldAreaIndex[menu.areaUuid]
|
|
|
|
const fieldArea = this.fieldAreaIndex[menu.areaUuid]
|
|
|
|
@ -399,44 +733,100 @@ export default {
|
|
|
|
|
|
|
|
|
|
|
|
if (!pos) return false
|
|
|
|
if (!pos) return false
|
|
|
|
|
|
|
|
|
|
|
|
const fieldType = draggedField.type || 'text'
|
|
|
|
this.insertFieldAtRange({
|
|
|
|
const dims = this.defaultSizes[fieldType] || this.defaultSizes.text
|
|
|
|
sourceField: draggedField,
|
|
|
|
const areaUuid = v4()
|
|
|
|
existingField: this.fieldsDragFieldRef?.value,
|
|
|
|
|
|
|
|
from: pos.pos
|
|
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const existingField = this.fieldsDragFieldRef?.value
|
|
|
|
this.fieldsDragFieldRef.value = null
|
|
|
|
|
|
|
|
this.customDragFieldRef.value = null
|
|
|
|
|
|
|
|
|
|
|
|
if (existingField) {
|
|
|
|
return true
|
|
|
|
if (!this.template.fields.includes(existingField)) {
|
|
|
|
},
|
|
|
|
this.template.fields.push(existingField)
|
|
|
|
onEditorPointerDown (event) {
|
|
|
|
|
|
|
|
if (!this.isDrawMode || !this.editable || this.isDraggingField) {
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
existingField.areas = existingField.areas || []
|
|
|
|
if (event.button === 2) {
|
|
|
|
existingField.areas.push({ uuid: areaUuid, attachment_uuid: this.attachmentUuid })
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const nodeType = view.state.schema.nodes.fieldNode
|
|
|
|
const view = this.editor?.view
|
|
|
|
const fieldNode = nodeType.create({
|
|
|
|
|
|
|
|
uuid: existingField.uuid,
|
|
|
|
if (!view) {
|
|
|
|
areaUuid,
|
|
|
|
return
|
|
|
|
width: dims.width,
|
|
|
|
}
|
|
|
|
height: dims.height
|
|
|
|
|
|
|
|
|
|
|
|
const selection = view.state.selection
|
|
|
|
|
|
|
|
const isTextRangeSelection = !selection.empty && !selection.node
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let from = selection.from
|
|
|
|
|
|
|
|
let to = selection.to
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!isTextRangeSelection) {
|
|
|
|
|
|
|
|
const pos = view.posAtCoords({ left: event.clientX, top: event.clientY })
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!pos) {
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
from = pos.pos
|
|
|
|
|
|
|
|
to = pos.pos
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const sourceField = this.drawField || this.drawCustomField || { type: this.drawFieldType }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
|
|
event.stopPropagation()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (this.drawOption && this.drawField) {
|
|
|
|
|
|
|
|
const areaWithoutOption = this.drawField.areas?.find((a) => !a.option_uuid)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (areaWithoutOption && !this.drawField.areas.find((a) => a.option_uuid === this.drawField.options[0].uuid)) {
|
|
|
|
|
|
|
|
areaWithoutOption.option_uuid = this.drawField.options[0].uuid
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const inserted = this.insertFieldAtRange({
|
|
|
|
|
|
|
|
sourceField,
|
|
|
|
|
|
|
|
existingField: this.drawField,
|
|
|
|
|
|
|
|
optionUuid: this.drawOption?.uuid,
|
|
|
|
|
|
|
|
from,
|
|
|
|
|
|
|
|
to
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const tr = view.state.tr.insert(pos.pos, fieldNode)
|
|
|
|
if (inserted) {
|
|
|
|
|
|
|
|
this.$emit('draw', inserted)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
buildFieldArea ({ optionUuid = null } = {}) {
|
|
|
|
|
|
|
|
const area = {
|
|
|
|
|
|
|
|
uuid: v4(),
|
|
|
|
|
|
|
|
attachment_uuid: this.attachmentUuid
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
view.dispatch(tr)
|
|
|
|
if (optionUuid) {
|
|
|
|
} else {
|
|
|
|
area.option_uuid = optionUuid
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return area
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
buildNewField (sourceField, area) {
|
|
|
|
|
|
|
|
const fieldType = sourceField?.type || 'text'
|
|
|
|
const newField = {
|
|
|
|
const newField = {
|
|
|
|
name: draggedField.name || '',
|
|
|
|
name: sourceField?.name || '',
|
|
|
|
uuid: v4(),
|
|
|
|
uuid: v4(),
|
|
|
|
required: fieldType !== 'checkbox',
|
|
|
|
required: fieldType !== 'checkbox',
|
|
|
|
submitter_uuid: this.selectedSubmitter.uuid,
|
|
|
|
submitter_uuid: this.selectedSubmitter.uuid,
|
|
|
|
type: fieldType,
|
|
|
|
type: fieldType,
|
|
|
|
areas: [{ uuid: areaUuid, attachment_uuid: this.attachmentUuid }]
|
|
|
|
areas: [area]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (['select', 'multiple', 'radio'].includes(fieldType)) {
|
|
|
|
if (['select', 'multiple', 'radio'].includes(fieldType)) {
|
|
|
|
if (draggedField.options?.length) {
|
|
|
|
if (sourceField?.options?.length) {
|
|
|
|
newField.options = draggedField.options.map((opt) => ({
|
|
|
|
newField.options = sourceField.options.map((opt) => ({
|
|
|
|
value: typeof opt === 'string' ? opt : opt.value,
|
|
|
|
value: typeof opt === 'string' ? opt : opt.value,
|
|
|
|
uuid: v4()
|
|
|
|
uuid: v4()
|
|
|
|
}))
|
|
|
|
}))
|
|
|
|
@ -459,29 +849,51 @@ export default {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.template.fields.push(newField)
|
|
|
|
return newField
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
insertFieldAtRange ({ sourceField, existingField = null, optionUuid = null, from, to = from }) {
|
|
|
|
|
|
|
|
if (!sourceField) {
|
|
|
|
|
|
|
|
return null
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const fieldType = sourceField.type || existingField?.type || 'text'
|
|
|
|
|
|
|
|
const dims = this.defaultSizes[fieldType]
|
|
|
|
|
|
|
|
const area = this.buildFieldArea({ optionUuid })
|
|
|
|
|
|
|
|
const insertIndex = this.getFieldInsertIndex(from)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let field = existingField
|
|
|
|
|
|
|
|
const view = this.editor.view
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (field) {
|
|
|
|
|
|
|
|
if (!this.template.fields.includes(field)) {
|
|
|
|
|
|
|
|
this.insertFieldInTemplate(field, insertIndex)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
field.areas = field.areas || []
|
|
|
|
|
|
|
|
field.areas.push(area)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
field = this.buildNewField(sourceField, area)
|
|
|
|
|
|
|
|
this.insertFieldInTemplate(field, insertIndex)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const nodeType = view.state.schema.nodes.fieldNode
|
|
|
|
const nodeType = view.state.schema.nodes.fieldNode
|
|
|
|
const fieldNode = nodeType.create({
|
|
|
|
const fieldNode = nodeType.create({
|
|
|
|
uuid: newField.uuid,
|
|
|
|
uuid: field.uuid,
|
|
|
|
areaUuid,
|
|
|
|
areaUuid: area.uuid,
|
|
|
|
width: dims.width,
|
|
|
|
width: dims.width,
|
|
|
|
height: dims.height
|
|
|
|
height: dims.height
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const tr = view.state.tr.insert(pos.pos, fieldNode)
|
|
|
|
const tr = from !== to
|
|
|
|
|
|
|
|
? view.state.tr.replaceWith(from, to, fieldNode)
|
|
|
|
|
|
|
|
: view.state.tr.insert(from, fieldNode)
|
|
|
|
|
|
|
|
|
|
|
|
view.dispatch(tr)
|
|
|
|
view.dispatch(tr)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.fieldsDragFieldRef.value = null
|
|
|
|
|
|
|
|
this.customDragFieldRef.value = null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.editor.chain().focus().setNodeSelection(pos.pos).run()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.editor.chain().focus().setNodeSelection(from).run()
|
|
|
|
this.save()
|
|
|
|
this.save()
|
|
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
return { area, field, pos: from }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|