Files
docuseal/app/javascript/template_builder/builder.vue
T

876 lines
25 KiB
Vue
Raw Normal View History

2023-05-21 17:59:36 +03:00
<template>
<div
2023-05-31 00:50:43 +03:00
style="max-width: 1600px"
2023-10-06 03:21:14 +03:00
class="mx-auto pl-3 md:pl-4 h-full"
2023-05-21 17:59:36 +03:00
>
2023-10-08 19:28:01 +03:00
<div
2023-11-24 22:44:27 +02:00
v-if="$slots.buttons || withTitle"
2023-10-08 19:28:01 +03:00
class="flex justify-between py-1.5 items-center pr-4 sticky top-0 z-10"
:style="{ backgroundColor }"
>
2023-11-30 01:50:22 +02:00
<div class="flex items-center space-x-3">
2023-08-19 21:05:54 +03:00
<a
2023-09-30 15:14:29 +03:00
v-if="withLogo"
2023-08-19 21:05:54 +03:00
href="/"
>
2023-06-01 00:46:50 +03:00
<Logo />
</a>
<Contenteditable
2023-11-24 22:44:27 +02:00
v-if="withTitle"
2023-06-01 00:46:50 +03:00
:model-value="template.name"
2023-11-04 01:19:32 +02:00
:editable="editable"
2023-11-30 01:50:22 +02:00
class="text-xl md:text-3xl font-semibold focus:text-clip"
2023-06-04 01:08:45 +03:00
:icon-stroke-width="2.3"
2023-06-01 00:46:50 +03:00
@update:model-value="updateName"
/>
</div>
2023-05-31 00:50:43 +03:00
<div class="space-x-3 flex items-center">
2023-08-19 21:05:54 +03:00
<slot
v-if="$slots.buttons"
name="buttons"
/>
<template v-else>
2023-11-30 01:50:22 +02:00
<a
:href="template.submitters.length > 1 ? `/templates/${template.id}/submissions/new?selfsign=true` : `/d/${template.slug}`"
class="btn btn-primary btn-ghost text-base hidden md:flex"
:target="template.submitters.length > 1 ? '' : '_blank'"
:data-turbo-frame="template.submitters.length > 1 ? 'modal' : ''"
@click="maybeShowEmptyTemplateAlert"
>
<IconWritingSign
width="20"
class="inline"
/>
<span class="hidden md:inline">
Sign Yourself
</span>
</a>
2023-08-19 21:05:54 +03:00
<a
:href="`/templates/${template.id}/submissions/new`"
data-turbo-frame="modal"
2023-11-30 01:50:22 +02:00
class="white-button md:!px-6"
2023-10-28 23:07:18 +03:00
@click="maybeShowEmptyTemplateAlert"
2023-08-19 21:05:54 +03:00
>
<IconUsersPlus
width="20"
class="inline"
/>
<span class="hidden md:inline">
2023-11-30 01:50:22 +02:00
Send
2023-08-19 21:05:54 +03:00
</span>
</a>
<button
class="base-button"
:class="{ disabled: isSaving }"
v-bind="isSaving ? { disabled: true } : {}"
@click.prevent="onSaveClick"
>
<IconInnerShadowTop
v-if="isSaving"
2023-09-20 00:13:14 +03:00
width="22"
2023-08-19 21:05:54 +03:00
class="animate-spin"
/>
<IconDeviceFloppy
v-else
2023-09-20 00:13:14 +03:00
width="22"
2023-08-19 21:05:54 +03:00
/>
<span class="hidden md:inline">
Save
</span>
</button>
</template>
2023-05-31 00:50:43 +03:00
</div>
2023-05-21 17:59:36 +03:00
</div>
2023-11-24 22:44:27 +02:00
<div
class="flex"
:class="$slots.buttons || withTitle ? 'md:max-h-[calc(100%_-_60px)]' : 'md:max-h-[100%]'"
>
2023-05-21 17:59:36 +03:00
<div
2023-12-20 22:13:19 +02:00
v-if="withDocumentsList"
2023-05-31 00:50:43 +03:00
ref="previews"
2023-08-19 21:05:54 +03:00
:style="{ 'display': isBreakpointLg ? 'none' : 'initial' }"
2023-07-02 00:14:26 +03:00
class="overflow-y-auto overflow-x-hidden w-52 flex-none pr-3 mt-0.5 pt-0.5 hidden lg:block"
2023-05-21 17:59:36 +03:00
>
2023-05-31 00:50:43 +03:00
<DocumentPreview
2023-05-31 23:19:20 +03:00
v-for="(item, index) in template.schema"
2023-05-31 00:50:43 +03:00
:key="index"
2023-05-31 23:19:20 +03:00
:with-arrows="template.schema.length > 1"
2023-05-31 00:50:43 +03:00
:item="item"
:document="sortedDocuments[index]"
2023-09-05 20:33:40 +03:00
:accept-file-types="acceptFileTypes"
2023-11-09 10:54:56 +02:00
:with-replace-button="withUploadButton"
2023-11-04 01:19:32 +02:00
:editable="editable"
2023-07-26 20:35:53 +03:00
:template="template"
:is-direct-upload="isDirectUpload"
2023-05-31 00:50:43 +03:00
@scroll-to="scrollIntoDocument(item)"
@remove="onDocumentRemove"
2023-07-26 20:35:53 +03:00
@replace="onDocumentReplace"
2023-05-31 00:50:43 +03:00
@up="moveDocument(item, -1)"
@down="moveDocument(item, 1)"
@change="save"
2023-05-21 17:59:36 +03:00
/>
2023-08-28 23:43:33 +03:00
<div
class="sticky bottom-0 py-2"
2023-11-24 22:44:27 +02:00
:style="withStickySubmitters ? { backgroundColor } : {}"
2023-08-28 23:43:33 +03:00
>
2023-05-31 00:50:43 +03:00
<Upload
2023-11-09 10:54:56 +02:00
v-if="sortedDocuments.length && editable && withUploadButton"
2023-09-05 20:33:40 +03:00
:accept-file-types="acceptFileTypes"
2023-05-31 23:19:20 +03:00
:template-id="template.id"
:is-direct-upload="isDirectUpload"
2023-05-31 00:50:43 +03:00
@success="updateFromUpload"
/>
</div>
</div>
2023-10-06 03:21:14 +03:00
<div class="w-full overflow-y-hidden md:overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
2023-06-04 01:08:45 +03:00
<div
ref="documents"
class="pr-3.5 pl-0.5"
>
<Dropzone
2023-11-09 10:54:56 +02:00
v-if="!sortedDocuments.length && withUploadButton"
:template-id="template.id"
2023-09-05 20:33:40 +03:00
:accept-file-types="acceptFileTypes"
:is-direct-upload="isDirectUpload"
@success="updateFromUpload"
/>
2023-07-01 00:15:15 +03:00
<template v-else>
2023-08-19 21:05:54 +03:00
<template
2023-07-01 00:15:15 +03:00
v-for="document in sortedDocuments"
:key="document.uuid"
2023-08-19 21:05:54 +03:00
>
<Document
:ref="setDocumentRefs"
:areas-index="fieldAreasIndex[document.uuid]"
:selected-submitter="selectedSubmitter"
:document="document"
2023-11-11 23:36:03 +02:00
:is-drag="!!dragField"
2023-11-27 20:24:58 +02:00
:default-fields="defaultFields"
2023-08-19 21:05:54 +03:00
:draw-field="drawField"
2023-11-04 01:19:32 +02:00
:editable="editable"
2023-11-11 21:16:29 +02:00
:base-url="baseUrl"
2023-08-19 21:05:54 +03:00
@draw="onDraw"
@drop-field="onDropfield"
@remove-area="removeArea"
/>
<DocumentControls
2023-11-04 01:19:32 +02:00
v-if="isBreakpointLg && editable"
2023-08-19 21:05:54 +03:00
:with-arrows="template.schema.length > 1"
:item="template.schema.find((item) => item.attachment_uuid === document.uuid)"
2023-11-09 10:54:56 +02:00
:with-replace-button="withUploadButton"
2023-08-19 21:05:54 +03:00
:document="document"
:template="template"
:is-direct-upload="isDirectUpload"
class="pb-2 mb-2 border-b border-base-300 border-dashed"
@remove="onDocumentRemove"
@replace="onDocumentReplace"
@up="moveDocument(template.schema.find((item) => item.attachment_uuid === document.uuid), -1)"
@down="moveDocument(template.schema.find((item) => item.attachment_uuid === document.uuid), 1)"
@change="save"
/>
</template>
<div
2023-11-04 01:19:32 +02:00
v-if="sortedDocuments.length && isBreakpointLg && editable"
2023-08-19 21:05:54 +03:00
class="pb-4"
>
<Upload
2023-11-09 10:54:56 +02:00
v-if="withUploadButton"
2023-08-19 21:05:54 +03:00
:template-id="template.id"
:is-direct-upload="isDirectUpload"
@success="updateFromUpload"
/>
</div>
2023-07-01 00:15:15 +03:00
</template>
2023-05-31 00:50:43 +03:00
</div>
2023-11-06 20:49:44 +02:00
<MobileDrawField
2023-11-06 21:23:46 +02:00
v-if="drawField && isBreakpointLg"
2023-11-06 20:49:44 +02:00
:draw-field="drawField"
:fields="template.fields"
:submitters="template.submitters"
:selected-submitter="selectedSubmitter"
2023-11-06 21:23:46 +02:00
class="md:hidden"
2023-11-06 20:49:44 +02:00
:editable="editable"
2023-11-14 01:30:00 +02:00
@cancel="[drawField = null, drawOption = null]"
2023-11-06 20:49:44 +02:00
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
/>
2023-10-06 03:21:14 +03:00
<FieldType
2023-11-04 01:19:32 +02:00
v-if="sortedDocuments.length && !drawField && editable"
2023-10-06 03:21:14 +03:00
class="dropdown-top dropdown-end fixed bottom-4 right-4 z-10 md:hidden"
:model-value="''"
@update:model-value="startFieldDraw($event)"
>
<label
class="btn btn-neutral text-white btn-circle btn-lg group"
tabindex="0"
>
<IconPlus
class="group-focus:hidden"
width="28"
height="28"
/>
<IconX
class="hidden group-focus:inline"
width="28"
height="28"
/>
</label>
</FieldType>
2023-05-31 00:50:43 +03:00
</div>
<div
2023-12-20 22:13:19 +02:00
v-if="withFieldsList"
2023-08-19 21:05:54 +03:00
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block"
2023-05-31 00:50:43 +03:00
:class="drawField ? 'overflow-hidden' : 'overflow-auto'"
>
<div
v-if="drawField"
2023-11-24 22:44:27 +02:00
class="sticky inset-0 h-full"
:style="{ backgroundColor }"
2023-05-31 00:50:43 +03:00
>
2023-06-08 00:20:42 +03:00
<div class="bg-base-300 rounded-lg p-5 text-center space-y-4">
<p>
Draw {{ drawField.name }} field on the document
</p>
<p>
<button
class="base-button"
2023-11-14 01:30:00 +02:00
@click="[drawField = null, drawOption = null]"
2023-06-08 00:20:42 +03:00
>
Cancel
</button>
</p>
</div>
2023-05-31 00:50:43 +03:00
</div>
<div>
<Fields
ref="fields"
2023-06-07 01:16:43 +03:00
:fields="template.fields"
:submitters="template.submitters"
:selected-submitter="selectedSubmitter"
2023-12-17 16:10:17 +02:00
:default-submitters="defaultSubmitters"
2023-11-11 23:36:03 +02:00
:default-fields="defaultFields"
2023-08-19 21:05:54 +03:00
:with-sticky-submitters="withStickySubmitters"
2023-11-04 01:19:32 +02:00
:editable="editable"
2023-11-14 01:30:00 +02:00
@set-draw="[drawField = $event.field, drawOption = $event.option]"
2023-11-11 23:36:03 +02:00
@set-drag="dragField = $event"
2023-06-07 01:16:43 +03:00
@change-submitter="selectedSubmitter = $event"
2023-11-11 23:36:03 +02:00
@drag-end="dragField = null"
2023-06-03 00:53:46 +03:00
@scroll-to-area="scrollToArea"
2023-05-31 00:50:43 +03:00
/>
</div>
2023-05-21 17:59:36 +03:00
</div>
</div>
</div>
</template>
<script>
import Upload from './upload'
2023-07-01 00:15:15 +03:00
import Dropzone from './dropzone'
2023-05-21 17:59:36 +03:00
import Fields from './fields'
2023-11-06 20:49:44 +02:00
import MobileDrawField from './mobile_draw_field'
2023-05-21 17:59:36 +03:00
import Document from './document'
2023-06-01 00:46:50 +03:00
import Logo from './logo'
2023-05-31 00:50:43 +03:00
import Contenteditable from './contenteditable'
import DocumentPreview from './preview'
2023-08-19 21:05:54 +03:00
import DocumentControls from './controls'
2023-10-06 03:21:14 +03:00
import FieldType from './field_type'
2023-11-30 01:50:22 +02:00
import { IconUsersPlus, IconDeviceFloppy, IconWritingSign, IconInnerShadowTop, IconPlus, IconX } from '@tabler/icons-vue'
2023-06-03 00:53:46 +03:00
import { v4 } from 'uuid'
2023-06-04 01:08:45 +03:00
import { ref, computed } from 'vue'
2023-05-21 17:59:36 +03:00
export default {
2023-05-31 23:19:20 +03:00
name: 'TemplateBuilder',
2023-05-21 17:59:36 +03:00
components: {
Upload,
Document,
2023-05-31 00:50:43 +03:00
Fields,
2023-11-06 20:49:44 +02:00
MobileDrawField,
2023-10-06 03:21:14 +03:00
IconPlus,
FieldType,
IconX,
2023-11-30 01:50:22 +02:00
IconWritingSign,
2023-06-01 00:46:50 +03:00
Logo,
2023-07-01 00:15:15 +03:00
Dropzone,
2023-05-31 00:50:43 +03:00
DocumentPreview,
2023-08-19 21:05:54 +03:00
DocumentControls,
2023-06-24 16:17:07 +03:00
IconInnerShadowTop,
2023-05-31 00:50:43 +03:00
Contenteditable,
IconUsersPlus,
IconDeviceFloppy
2023-05-21 17:59:36 +03:00
},
2023-06-04 01:08:45 +03:00
provide () {
return {
template: this.template,
2023-06-24 16:17:07 +03:00
save: this.save,
2023-08-19 21:05:54 +03:00
baseFetch: this.baseFetch,
2023-08-28 23:43:33 +03:00
backgroundColor: this.backgroundColor,
2023-09-05 01:00:21 +03:00
withPhone: this.withPhone,
2023-12-14 21:22:26 +02:00
withPayment: this.withPayment,
2023-06-09 01:43:18 +03:00
selectedAreaRef: computed(() => this.selectedAreaRef)
2023-06-04 01:08:45 +03:00
}
},
2023-05-21 17:59:36 +03:00
props: {
2023-05-31 23:19:20 +03:00
template: {
2023-05-25 22:47:52 +03:00
type: Object,
required: true
},
isDirectUpload: {
type: Boolean,
2023-08-19 21:05:54 +03:00
required: false,
default: false
2023-08-19 21:05:54 +03:00
},
2023-08-28 23:43:33 +03:00
backgroundColor: {
type: String,
required: false,
default: ''
},
2023-11-04 01:19:32 +02:00
editable: {
type: Boolean,
required: false,
default: true
},
2023-11-11 23:36:03 +02:00
defaultFields: {
type: Array,
required: false,
default: () => []
},
defaultSubmitters: {
type: Array,
required: false,
default: () => []
},
2023-09-05 20:33:40 +03:00
acceptFileTypes: {
type: String,
required: false,
default: 'image/*, application/pdf'
},
2023-08-19 21:05:54 +03:00
baseUrl: {
type: String,
required: false,
default: ''
},
2023-09-30 15:14:29 +03:00
withLogo: {
2023-08-19 21:05:54 +03:00
type: Boolean,
required: false,
default: true
},
2023-12-19 19:27:07 +02:00
onUpload: {
type: Function,
required: false,
default () {
return () => {}
}
},
2023-08-19 21:05:54 +03:00
withStickySubmitters: {
type: Boolean,
required: false,
default: true
},
2023-11-09 10:54:56 +02:00
withUploadButton: {
type: Boolean,
required: false,
default: true
},
2023-11-24 22:44:27 +02:00
withTitle: {
type: Boolean,
required: false,
default: true
},
2023-12-20 22:13:19 +02:00
withFieldsList: {
type: Boolean,
required: false,
default: true
},
withDocumentsList: {
type: Boolean,
required: false,
default: true
},
2023-09-05 01:00:21 +03:00
withPhone: {
type: Boolean,
required: false,
default: false
},
2023-12-14 21:22:26 +02:00
withPayment: {
type: Boolean,
required: false,
default: false
},
2023-08-19 21:05:54 +03:00
fetchOptions: {
type: Object,
required: false,
default: () => ({ headers: {} })
2023-05-21 17:59:36 +03:00
}
},
data () {
return {
2023-05-31 00:50:43 +03:00
documentRefs: [],
2023-08-19 21:05:54 +03:00
isBreakpointLg: false,
2023-05-31 00:50:43 +03:00
isSaving: false,
2023-06-07 01:16:43 +03:00
selectedSubmitter: null,
2023-05-21 17:59:36 +03:00
drawField: null,
2023-11-14 01:30:00 +02:00
drawOption: null,
2023-11-11 23:36:03 +02:00
dragField: null
2023-05-21 17:59:36 +03:00
}
},
computed: {
2023-10-06 03:21:14 +03:00
fieldIcons: FieldType.computed.fieldIcons,
fieldNames: FieldType.computed.fieldNames,
2023-06-09 01:43:18 +03:00
selectedAreaRef: () => ref(),
2023-05-21 17:59:36 +03:00
fieldAreasIndex () {
const areas = {}
2023-05-31 23:19:20 +03:00
this.template.fields.forEach((f) => {
2023-05-21 17:59:36 +03:00
(f.areas || []).forEach((a) => {
areas[a.attachment_uuid] ||= {}
const acc = (areas[a.attachment_uuid][a.page] ||= [])
acc.push({ area: a, field: f })
})
})
return areas
},
2023-06-04 01:08:45 +03:00
selectedField () {
2023-06-09 01:43:18 +03:00
return this.template.fields.find((f) => f.areas?.includes(this.selectedAreaRef.value))
2023-06-04 01:08:45 +03:00
},
2023-05-21 17:59:36 +03:00
sortedDocuments () {
2023-05-31 23:19:20 +03:00
return this.template.schema.map((item) => {
return this.template.documents.find(doc => doc.uuid === item.attachment_uuid)
2023-05-21 17:59:36 +03:00
})
}
},
2023-06-07 01:16:43 +03:00
created () {
2023-11-11 23:36:03 +02:00
this.defaultSubmitters.forEach((name, index) => {
const submitter = (this.template.submitters[index] ||= {})
submitter.name = name
submitter.uuid ||= v4()
})
2023-06-07 01:16:43 +03:00
this.selectedSubmitter = this.template.submitters[0]
},
2023-05-21 17:59:36 +03:00
mounted () {
2023-09-28 01:48:40 +03:00
this.undoStack = [JSON.stringify(this.template)]
this.redoStack = []
2023-08-19 21:05:54 +03:00
this.$nextTick(() => {
this.onWindowResize()
})
2023-06-07 01:16:43 +03:00
document.addEventListener('keyup', this.onKeyUp)
2023-09-28 01:48:40 +03:00
window.addEventListener('keydown', this.onKeyDown)
2023-08-19 21:05:54 +03:00
window.addEventListener('resize', this.onWindowResize)
2023-12-14 21:22:26 +02:00
this.$nextTick(() => {
if (document.location.search?.includes('stripe_connect_success')) {
document.querySelector('form[action="/auth/stripe_connect"]')?.closest('.dropdown')?.querySelector('label')?.focus()
}
})
2023-05-21 17:59:36 +03:00
},
unmounted () {
2023-06-07 01:16:43 +03:00
document.removeEventListener('keyup', this.onKeyUp)
2023-09-28 01:48:40 +03:00
window.removeEventListener('keydown', this.onKeyDown)
2023-08-19 21:05:54 +03:00
window.removeEventListener('resize', this.onWindowResize)
2023-05-21 17:59:36 +03:00
},
2023-05-31 00:50:43 +03:00
beforeUpdate () {
this.documentRefs = []
},
2023-05-21 17:59:36 +03:00
methods: {
2023-10-06 03:21:14 +03:00
startFieldDraw (type) {
const field = {
name: '',
uuid: v4(),
required: type !== 'checkbox',
areas: [],
submitter_uuid: this.selectedSubmitter.uuid,
type
}
if (['select', 'multiple', 'radio'].includes(type)) {
2023-11-13 01:46:28 +02:00
field.options = [{ value: '', uuid: v4() }]
2023-10-06 03:21:14 +03:00
}
2023-12-22 13:33:53 +02:00
if (type === 'stamp') {
field.readonly = true
}
2023-12-17 23:32:05 +02:00
if (type === 'date') {
field.preferences = {
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
}
}
2023-10-06 03:21:14 +03:00
this.drawField = field
2023-11-14 01:30:00 +02:00
this.drawOption = null
2023-10-06 03:21:14 +03:00
},
2023-09-28 01:48:40 +03:00
undo () {
if (this.undoStack.length > 1) {
this.undoStack.pop()
const stringData = this.undoStack[this.undoStack.length - 1]
const currentStringData = JSON.stringify(this.template)
if (stringData && stringData !== currentStringData) {
this.redoStack.push(currentStringData)
Object.assign(this.template, JSON.parse(stringData))
this.save()
}
}
},
redo () {
const stringData = this.redoStack.pop()
this.lastRedoData = stringData
const currentStringData = JSON.stringify(this.template)
if (stringData && stringData !== currentStringData) {
if (this.undoStack[this.undoStack.length - 1] !== currentStringData) {
this.undoStack.push(currentStringData)
}
Object.assign(this.template, JSON.parse(stringData))
this.save()
}
},
2023-08-19 21:05:54 +03:00
onWindowResize (e) {
const breakpointLg = 1024
2023-09-14 01:43:08 +03:00
this.isBreakpointLg = this.$el.getRootNode().querySelector('div[data-v-app]').offsetWidth < breakpointLg
2023-08-19 21:05:54 +03:00
},
2023-05-31 00:50:43 +03:00
setDocumentRefs (el) {
if (el) {
this.documentRefs.push(el)
}
},
scrollIntoDocument (item) {
const ref = this.documentRefs.find((e) => e.document.uuid === item.attachment_uuid)
ref.$el.scrollIntoView({ behavior: 'smooth', block: 'start' })
},
2023-06-07 01:16:43 +03:00
onKeyUp (e) {
2023-05-21 17:59:36 +03:00
if (e.code === 'Escape') {
this.drawField = null
2023-11-14 01:30:00 +02:00
this.drawOption = null
2023-06-09 01:43:18 +03:00
this.selectedAreaRef.value = null
2023-06-07 01:16:43 +03:00
}
2023-11-29 21:59:14 +02:00
if (this.editable && ['Backspace', 'Delete'].includes(e.key) && this.selectedAreaRef.value && document.activeElement === document.body) {
2023-06-09 01:43:18 +03:00
this.removeArea(this.selectedAreaRef.value)
2023-06-07 01:16:43 +03:00
2023-06-09 01:43:18 +03:00
this.selectedAreaRef.value = null
2023-05-21 17:59:36 +03:00
}
},
2023-09-28 01:48:40 +03:00
onKeyDown (event) {
if ((event.metaKey && event.shiftKey && event.key === 'z') || (event.ctrlKey && event.key === 'Z')) {
event.stopImmediatePropagation()
event.preventDefault()
this.redo()
} else if ((event.ctrlKey || event.metaKey) && event.key === 'z') {
event.stopImmediatePropagation()
event.preventDefault()
this.undo()
}
},
2023-06-09 01:43:18 +03:00
removeArea (area) {
2023-06-07 01:16:43 +03:00
const field = this.template.fields.find((f) => f.areas?.includes(area))
2023-06-04 01:08:45 +03:00
field.areas.splice(field.areas.indexOf(area), 1)
if (!field.areas.length) {
this.template.fields.splice(this.template.fields.indexOf(field), 1)
}
2023-06-24 16:17:07 +03:00
this.save()
2023-06-04 01:08:45 +03:00
},
2023-09-28 01:48:40 +03:00
pushUndo () {
const stringData = JSON.stringify(this.template)
if (this.undoStack[this.undoStack.length - 1] !== stringData) {
this.undoStack.push(stringData)
if (this.lastRedoData !== stringData) {
this.redoStack = []
}
}
},
2023-05-21 17:59:36 +03:00
onDraw (area) {
2023-06-03 00:53:46 +03:00
if (this.drawField) {
2023-11-14 01:30:00 +02:00
if (this.drawOption) {
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
}
area.option_uuid = this.drawOption.uuid
}
2023-06-03 00:53:46 +03:00
this.drawField.areas ||= []
this.drawField.areas.push(area)
2023-05-21 17:59:36 +03:00
2023-10-06 03:21:14 +03:00
if (this.template.fields.indexOf(this.drawField) === -1) {
this.template.fields.push(this.drawField)
}
2023-06-03 00:53:46 +03:00
this.drawField = null
2023-11-14 01:30:00 +02:00
this.drawOption = null
2023-09-20 19:19:21 +03:00
this.selectedAreaRef.value = area
this.save()
2023-06-03 00:53:46 +03:00
} else {
2023-06-04 01:08:45 +03:00
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
const pageMask = documentRef.pageRefs[area.page].$refs.mask
const type = (pageMask.clientWidth * area.w) < 35 ? 'checkbox' : 'text'
if (type === 'checkbox') {
2023-06-08 00:20:42 +03:00
const previousField = [...this.template.fields].reverse().find((f) => f.type === type)
const previousArea = previousField?.areas?.[previousField.areas.length - 1]
2023-09-20 19:19:21 +03:00
if (previousArea || area.w) {
const areaW = previousArea?.w || (30 / pageMask.clientWidth)
const areaH = previousArea?.h || (30 / pageMask.clientHeight)
2023-06-08 00:20:42 +03:00
2023-09-20 19:19:21 +03:00
if ((pageMask.clientWidth * area.w) < 5) {
area.x = area.x - (areaW / 2)
area.y = area.y - (areaH / 2)
}
2023-06-08 00:20:42 +03:00
2023-09-20 19:19:21 +03:00
area.w = areaW
area.h = areaH
}
2023-06-04 01:08:45 +03:00
}
2023-09-20 19:19:21 +03:00
if (area.w) {
const field = {
name: '',
uuid: v4(),
required: type !== 'checkbox',
type,
submitter_uuid: this.selectedSubmitter.uuid,
areas: [area]
}
2023-06-03 00:53:46 +03:00
2023-09-20 19:19:21 +03:00
this.template.fields.push(field)
2023-06-07 01:16:43 +03:00
2023-09-20 19:19:21 +03:00
this.selectedAreaRef.value = area
2023-06-24 16:17:07 +03:00
2023-09-20 19:19:21 +03:00
this.save()
}
}
2023-05-21 17:59:36 +03:00
},
2023-05-23 00:03:26 +03:00
onDropfield (area) {
2023-06-04 01:08:45 +03:00
const field = {
name: '',
uuid: v4(),
2023-06-07 01:16:43 +03:00
submitter_uuid: this.selectedSubmitter.uuid,
2023-11-11 23:36:03 +02:00
required: this.dragField.type !== 'checkbox',
...this.dragField
2023-06-04 01:08:45 +03:00
}
2023-11-11 23:36:03 +02:00
if (['select', 'multiple', 'radio'].includes(field.type)) {
2023-11-13 01:46:28 +02:00
field.options = [{ value: '', uuid: v4() }]
2023-06-15 01:05:04 +03:00
}
2023-12-22 13:33:53 +02:00
if (field.type === 'stamp') {
field.readonly = true
}
2023-12-17 23:32:05 +02:00
if (field.type === 'date') {
field.preferences = {
format: Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY'
}
}
2023-06-04 01:08:45 +03:00
const fieldArea = {
x: (area.x - 6) / area.maskW,
y: area.y / area.maskH,
page: area.page,
attachment_uuid: area.attachment_uuid
}
const previousField = [...this.template.fields].reverse().find((f) => f.type === field.type)
let baseArea
2023-11-11 23:36:03 +02:00
if (this.selectedField?.type === field.type) {
2023-06-09 01:43:18 +03:00
baseArea = this.selectedAreaRef.value
2023-07-26 23:17:24 +03:00
} else if (previousField?.areas?.length) {
2023-06-04 01:08:45 +03:00
baseArea = previousField.areas[previousField.areas.length - 1]
} else {
2023-11-11 23:36:03 +02:00
if (['checkbox'].includes(field.type)) {
2023-06-04 01:08:45 +03:00
baseArea = {
w: area.maskW / 30 / area.maskW,
h: area.maskW / 30 / area.maskW * (area.maskW / area.maskH)
}
2023-11-11 23:36:03 +02:00
} else if (field.type === 'image') {
2023-06-04 01:08:45 +03:00
baseArea = {
w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
}
2023-12-22 13:33:53 +02:00
} else if (field.type === 'signature' || field.type === 'stamp') {
2023-06-20 00:24:39 +03:00
baseArea = {
w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
}
2023-11-11 23:36:03 +02:00
} else if (field.type === 'initials') {
2023-09-20 00:13:14 +03:00
baseArea = {
w: area.maskW / 10 / area.maskW,
h: area.maskW / 35 / area.maskW
}
2023-06-04 01:08:45 +03:00
} else {
baseArea = {
w: area.maskW / 5 / area.maskW,
h: area.maskW / 35 / area.maskW
}
}
}
fieldArea.w = baseArea.w
fieldArea.h = baseArea.h
fieldArea.y = fieldArea.y - baseArea.h / 2
2023-11-11 23:36:03 +02:00
if (field.type === 'cells') {
2023-06-27 00:57:29 +03:00
fieldArea.cell_w = baseArea.cell_w || (baseArea.w / 5)
}
2023-06-04 01:08:45 +03:00
field.areas = [fieldArea]
2023-06-09 01:43:18 +03:00
this.selectedAreaRef.value = fieldArea
2023-06-04 01:08:45 +03:00
this.template.fields.push(field)
2023-06-24 16:17:07 +03:00
this.save()
2023-05-23 00:03:26 +03:00
},
2023-05-21 17:59:36 +03:00
updateFromUpload ({ schema, documents }) {
2023-05-31 23:19:20 +03:00
this.template.schema.push(...schema)
this.template.documents.push(...documents)
2023-05-21 17:59:36 +03:00
2023-05-31 00:50:43 +03:00
this.$nextTick(() => {
this.$refs.previews.scrollTop = this.$refs.previews.scrollHeight
this.scrollIntoDocument(schema[0])
})
2023-12-19 19:27:07 +02:00
if (this.onUpload) {
this.onUpload(this.template)
}
2023-05-31 00:50:43 +03:00
this.save()
},
updateName (value) {
2023-05-31 23:19:20 +03:00
this.template.name = value
2023-05-31 00:50:43 +03:00
this.save()
},
onDocumentRemove (item) {
if (window.confirm('Are you sure?')) {
2023-05-31 23:19:20 +03:00
this.template.schema.splice(this.template.schema.indexOf(item), 1)
2023-05-31 00:50:43 +03:00
}
2023-07-26 23:17:24 +03:00
this.template.fields.forEach((field) => {
[...(field.areas || [])].forEach((area) => {
2023-07-26 23:17:24 +03:00
if (area.attachment_uuid === item.attachment_uuid) {
field.areas.splice(field.areas.indexOf(area), 1)
}
})
})
2023-05-21 17:59:36 +03:00
this.save()
},
2023-07-26 20:35:53 +03:00
onDocumentReplace ({ replaceSchemaItem, schema, documents }) {
this.template.schema.splice(this.template.schema.indexOf(replaceSchemaItem), 1, schema[0])
this.template.documents.push(...documents)
this.template.fields.forEach((field) => {
(field.areas || []).forEach((area) => {
2023-07-26 20:35:53 +03:00
if (area.attachment_uuid === replaceSchemaItem.attachment_uuid) {
area.attachment_uuid = schema[0].attachment_uuid
}
})
})
2023-12-19 19:27:07 +02:00
if (this.onUpload) {
this.onUpload(this.template)
}
2023-07-26 20:35:53 +03:00
this.save()
},
2023-05-31 00:50:43 +03:00
moveDocument (item, direction) {
2023-05-31 23:19:20 +03:00
const currentIndex = this.template.schema.indexOf(item)
2023-05-31 00:50:43 +03:00
2023-05-31 23:19:20 +03:00
this.template.schema.splice(currentIndex, 1)
2023-05-31 00:50:43 +03:00
2023-05-31 23:19:20 +03:00
if (currentIndex + direction > this.template.schema.length) {
this.template.schema.unshift(item)
2023-05-31 00:50:43 +03:00
} else if (currentIndex + direction < 0) {
2023-05-31 23:19:20 +03:00
this.template.schema.push(item)
2023-05-31 00:50:43 +03:00
} else {
2023-05-31 23:19:20 +03:00
this.template.schema.splice(currentIndex + direction, 0, item)
2023-05-31 00:50:43 +03:00
}
this.save()
},
2023-10-28 23:07:18 +03:00
maybeShowEmptyTemplateAlert (e) {
if (!this.template.fields.length) {
e.preventDefault()
alert('Please draw fields to prepare the document.')
}
},
2023-05-31 00:50:43 +03:00
onSaveClick () {
2023-10-28 23:07:18 +03:00
if (this.template.fields.length) {
this.isSaving = true
2023-05-31 00:50:43 +03:00
2023-10-28 23:07:18 +03:00
this.save().then(() => {
window.Turbo.visit(`/templates/${this.template.id}`)
}).finally(() => {
this.isSaving = false
})
} else {
alert('Please draw fields to prepare the document.')
}
2023-05-31 00:50:43 +03:00
},
2023-06-03 00:53:46 +03:00
scrollToArea (area) {
const documentRef = this.documentRefs.find((a) => a.document.uuid === area.attachment_uuid)
documentRef.scrollToArea(area)
2023-06-07 01:16:43 +03:00
2023-06-09 01:43:18 +03:00
this.selectedAreaRef.value = area
2023-06-03 00:53:46 +03:00
},
2023-08-19 21:05:54 +03:00
baseFetch (path, options = {}) {
return fetch(this.baseUrl + path, {
...options,
headers: { ...this.fetchOptions.headers, ...options.headers }
})
},
2023-05-21 17:59:36 +03:00
save () {
2023-08-19 21:05:54 +03:00
if (this.$el.closest('template-builder')) {
this.$el.closest('template-builder').dataset.template = JSON.stringify(this.template)
}
2023-06-04 01:08:45 +03:00
2023-09-28 01:48:40 +03:00
this.pushUndo()
2023-08-19 21:05:54 +03:00
return this.baseFetch(`/api/templates/${this.template.id}`, {
2023-05-21 17:59:36 +03:00
method: 'PUT',
2023-06-24 16:17:07 +03:00
body: JSON.stringify({
template: {
name: this.template.name,
schema: this.template.schema,
submitters: this.template.submitters,
fields: this.template.fields
}
}),
2023-05-21 17:59:36 +03:00
headers: { 'Content-Type': 'application/json' }
})
}
}
}
</script>