custom fields

master
Pete Matsyburka 1 week ago
parent 9128f24270
commit 91814ca105

@ -0,0 +1,33 @@
# frozen_string_literal: true
class AccountCustomFieldsController < ApplicationController
before_action :load_account_config, only: :create
def create
authorize!(:create, Template)
@account_config.update!(account_config_params)
render json: @account_config.value
end
private
def load_account_config
@account_config =
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY)
end
def account_config_params
params.permit(
value: [[:uuid, :name, :type,
:required, :readonly, :default_value,
:title, :description,
{ preferences: {},
default_value: [],
options: [%i[value uuid]],
validation: %i[message pattern min max step],
areas: [%i[x y w h cell_w option_uuid]] }]]
)
end
end

@ -155,6 +155,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
this.app = createApp(TemplateBuilder, { this.app = createApp(TemplateBuilder, {
template: reactive(JSON.parse(this.dataset.template)), template: reactive(JSON.parse(this.dataset.template)),
customFields: reactive(JSON.parse(this.dataset.customFields || '[]')),
backgroundColor: '#faf7f5', backgroundColor: '#faf7f5',
locale: this.dataset.locale, locale: this.dataset.locale,
withPhone: this.dataset.withPhone === 'true', withPhone: this.dataset.withPhone === 'true',
@ -164,6 +165,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
withFieldsDetection: this.dataset.withFieldsDetection === 'true', withFieldsDetection: this.dataset.withFieldsDetection === 'true',
editable: this.dataset.editable !== 'false', editable: this.dataset.editable !== 'false',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content, authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
withCustomFields: true,
withPayment: this.dataset.withPayment === 'true', withPayment: this.dataset.withPayment === 'true',
isPaymentConnected: this.dataset.isPaymentConnected === 'true', isPaymentConnected: this.dataset.isPaymentConnected === 'true',
withFormula: this.dataset.withFormula === 'true', withFormula: this.dataset.withFormula === 'true',

@ -145,7 +145,9 @@
@click-formula="isShowFormulaModal = true" @click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true" @click-font="isShowFontModal = true"
@click-description="isShowDescriptionModal = true" @click-description="isShowDescriptionModal = true"
@add-custom-field="$emit('add-custom-field')"
@click-condition="isShowConditionsModal = true" @click-condition="isShowConditionsModal = true"
@save="save"
@scroll-to="[selectedAreasRef.value = [$event], $emit('scroll-to', $event)]" @scroll-to="[selectedAreasRef.value = [$event], $emit('scroll-to', $event)]"
/> />
</ul> </ul>
@ -471,7 +473,7 @@ export default {
default: false default: false
} }
}, },
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to'], emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field'],
data () { data () {
return { return {
isShowFormulaModal: false, isShowFormulaModal: false,

@ -20,8 +20,9 @@
/> />
<DragPlaceholder <DragPlaceholder
ref="dragPlaceholder" ref="dragPlaceholder"
:field="fieldsDragFieldRef.value || toRaw(dragField)" :field="customDragFieldRef.value || fieldsDragFieldRef.value || toRaw(dragField)"
:is-field="template.fields.includes(fieldsDragFieldRef.value)" :is-field="template.fields.includes(fieldsDragFieldRef.value)"
:is-custom="!!customDragFieldRef.value"
:is-default="defaultFields.includes(toRaw(dragField))" :is-default="defaultFields.includes(toRaw(dragField))"
:is-required="defaultRequiredFields.includes(toRaw(dragField))" :is-required="defaultRequiredFields.includes(toRaw(dragField))"
/> />
@ -362,7 +363,7 @@
:is-drag="!!dragField" :is-drag="!!dragField"
:input-mode="inputMode" :input-mode="inputMode"
:default-fields="[...defaultRequiredFields, ...defaultFields]" :default-fields="[...defaultRequiredFields, ...defaultFields]"
:allow-draw="!onlyDefinedFields || drawField" :allow-draw="!onlyDefinedFields || drawField || drawCustomField"
:with-signature-id="withSignatureId" :with-signature-id="withSignatureId"
:with-prefillable="withPrefillable" :with-prefillable="withPrefillable"
:data-document-uuid="document.uuid" :data-document-uuid="document.uuid"
@ -371,14 +372,16 @@
:with-field-placeholder="withFieldPlaceholder" :with-field-placeholder="withFieldPlaceholder"
:draw-field="drawField" :draw-field="drawField"
:draw-field-type="drawFieldType" :draw-field-type="drawFieldType"
:draw-custom-field="drawCustomField"
:editable="editable" :editable="editable"
:base-url="baseUrl" :base-url="baseUrl"
:with-fields-detection="withFieldsDetection" :with-fields-detection="withFieldsDetection"
@draw="[onDraw($event), withSelectedFieldType ? '' : drawFieldType = '', showDrawField = false]" @draw="[onDraw($event), withSelectedFieldType ? '' : drawFieldType = '', drawCustomField = null, showDrawField = false]"
@drop-field="onDropfield" @drop-field="onDropfield"
@remove-area="removeArea" @remove-area="removeArea"
@paste-field="pasteField" @paste-field="pasteField"
@copy-field="copyField" @copy-field="copyField"
@add-custom-field="addCustomField"
@copy-selected-areas="copySelectedAreas" @copy-selected-areas="copySelectedAreas"
@delete-selected-areas="deleteSelectedAreas" @delete-selected-areas="deleteSelectedAreas"
@align-selected-areas="alignSelectedAreas" @align-selected-areas="alignSelectedAreas"
@ -436,15 +439,15 @@
v-if="withFieldsList && !isMobile" v-if="withFieldsList && !isMobile"
id="fields_list_container" id="fields_list_container"
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block fields-list-container" class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block fields-list-container"
:class="drawField ? 'overflow-hidden' : 'overflow-y-auto overflow-x-hidden'" :class="drawField || drawCustomField ? 'overflow-hidden' : 'overflow-y-auto overflow-x-hidden'"
> >
<div <div
v-if="showDrawField || drawField" v-if="showDrawField || drawField || drawCustomField"
class="sticky inset-0 h-full z-20" class="sticky inset-0 h-full z-20"
:style="{ backgroundColor }" :style="{ backgroundColor }"
> >
<div class="bg-base-200 rounded-lg p-5 text-center space-y-4 draw-field-container"> <div class="bg-base-200 rounded-lg p-5 text-center space-y-4 draw-field-container">
<p v-if="(drawField?.type || drawFieldType) === 'strikethrough'"> <p v-if="(drawField?.type || drawFieldType || drawCustomField?.type) === 'strikethrough'">
{{ t('draw_strikethrough_the_document') }} {{ t('draw_strikethrough_the_document') }}
</p> </p>
<p v-else> <p v-else>
@ -458,10 +461,10 @@
{{ t('cancel') }} {{ t('cancel') }}
</button> </button>
<a <a
v-if="!drawField && !drawOption && !['stamp', 'signature', 'initials', 'heading', 'strikethrough'].includes(drawField?.type || drawFieldType)" v-if="!drawField && !drawOption && !['stamp', 'signature', 'initials', 'heading', 'strikethrough'].includes(drawField?.type || drawFieldType || drawCustomField?.type)"
href="#" href="#"
class="link block mt-3 text-sm" class="link block mt-3 text-sm"
@click.prevent="[addField(drawFieldType), drawField = null, drawOption = null, withSelectedFieldType ? '' : drawFieldType = '', showDrawField = false]" @click.prevent="drawCustomField ? addCustomFieldWithoutDraw() : [addField(drawFieldType), drawField = null, drawOption = null, withSelectedFieldType ? '' : drawFieldType = '', showDrawField = false]"
> >
{{ t('or_add_field_without_drawing') }} {{ t('or_add_field_without_drawing') }}
</a> </a>
@ -477,6 +480,8 @@
:with-help="withHelp" :with-help="withHelp"
:default-submitters="defaultSubmitters" :default-submitters="defaultSubmitters"
:draw-field-type="drawFieldType" :draw-field-type="drawFieldType"
:custom-fields="customFields"
:with-custom-fields="withCustomFields"
:with-fields-search="withFieldsSearch" :with-fields-search="withFieldsSearch"
:default-fields="[...defaultRequiredFields, ...defaultFields]" :default-fields="[...defaultRequiredFields, ...defaultFields]"
:template="template" :template="template"
@ -493,6 +498,7 @@
@set-draw="[drawField = $event.field, drawOption = $event.option]" @set-draw="[drawField = $event.field, drawOption = $event.option]"
@select-submitter="selectedSubmitter = $event" @select-submitter="selectedSubmitter = $event"
@set-draw-type="[drawFieldType = $event, showDrawField = true]" @set-draw-type="[drawFieldType = $event, showDrawField = true]"
@set-draw-custom-field="[drawCustomField = $event, showDrawField = true]"
@set-drag="dragField = $event" @set-drag="dragField = $event"
@set-drag-placeholder="$refs.dragPlaceholder.dragPlaceholder = $event" @set-drag-placeholder="$refs.dragPlaceholder.dragPlaceholder = $event"
@change-submitter="selectedSubmitter = $event" @change-submitter="selectedSubmitter = $event"
@ -632,10 +638,12 @@ export default {
isPaymentConnected: this.isPaymentConnected, isPaymentConnected: this.isPaymentConnected,
withFormula: this.withFormula, withFormula: this.withFormula,
withConditions: this.withConditions, withConditions: this.withConditions,
withCustomFields: this.withCustomFields,
isInlineSize: this.isInlineSize, isInlineSize: this.isInlineSize,
defaultDrawFieldType: this.defaultDrawFieldType, defaultDrawFieldType: this.defaultDrawFieldType,
selectedAreasRef: computed(() => this.selectedAreasRef), selectedAreasRef: computed(() => this.selectedAreasRef),
fieldsDragFieldRef: computed(() => this.fieldsDragFieldRef), fieldsDragFieldRef: computed(() => this.fieldsDragFieldRef),
customDragFieldRef: computed(() => this.customDragFieldRef),
isSelectModeRef: computed(() => this.isSelectModeRef), isSelectModeRef: computed(() => this.isSelectModeRef),
isCmdKeyRef: computed(() => this.isCmdKeyRef) isCmdKeyRef: computed(() => this.isCmdKeyRef)
} }
@ -705,6 +713,16 @@ export default {
required: false, required: false,
default: false default: false
}, },
withCustomFields: {
type: Boolean,
required: false,
default: false
},
customFields: {
type: Array,
required: false,
default: () => []
},
withAddPageButton: { withAddPageButton: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -902,6 +920,7 @@ export default {
pendingFieldAttachmentUuids: [], pendingFieldAttachmentUuids: [],
drawField: null, drawField: null,
drawFieldType: null, drawFieldType: null,
drawCustomField: null,
drawOption: null, drawOption: null,
dragField: null, dragField: null,
isDragFile: false isDragFile: false
@ -912,6 +931,7 @@ export default {
isSelectModeRef: () => ref(false), isSelectModeRef: () => ref(false),
isCmdKeyRef: () => ref(false), isCmdKeyRef: () => ref(false),
fieldsDragFieldRef: () => ref(), fieldsDragFieldRef: () => ref(),
customDragFieldRef: () => ref(),
selectedAreasRef: () => ref([]), selectedAreasRef: () => ref([]),
language () { language () {
return this.locale.split('-')[0].toLowerCase() return this.locale.split('-')[0].toLowerCase()
@ -1062,6 +1082,30 @@ export default {
}, },
methods: { methods: {
toRaw, toRaw,
addCustomField (field) {
return this.$refs.fields.addCustomField(field)
},
addCustomFieldWithoutDraw () {
const customField = this.drawCustomField
const field = JSON.parse(JSON.stringify(customField))
field.uuid = v4()
field.submitter_uuid = this.selectedSubmitter.uuid
field.areas = []
if (field.options?.length) {
field.options = field.options.map(opt => ({ ...opt, uuid: v4() }))
}
delete field.conditions
this.insertField(field)
this.save()
this.drawCustomField = null
this.showDrawField = false
},
toggleSelectMode () { toggleSelectMode () {
this.isSelectModeRef.value = !this.isSelectModeRef.value this.isSelectModeRef.value = !this.isSelectModeRef.value
@ -1514,6 +1558,7 @@ export default {
clearDrawField () { clearDrawField () {
this.drawField = null this.drawField = null
this.drawOption = null this.drawOption = null
this.drawCustomField = null
this.showDrawField = false this.showDrawField = false
if (!this.withSelectedFieldType) { if (!this.withSelectedFieldType) {
@ -1951,6 +1996,10 @@ export default {
} }
}, },
onDraw ({ area, isTooSmall }) { onDraw ({ area, isTooSmall }) {
if (this.drawCustomField) {
return this.onDrawCustomField(area)
}
if (this.drawField) { if (this.drawField) {
if (this.drawOption) { if (this.drawOption) {
const areaWithoutOption = this.drawField.areas?.find((a) => !a.option_uuid) const areaWithoutOption = this.drawField.areas?.find((a) => !a.option_uuid)
@ -2061,6 +2110,10 @@ export default {
return return
} }
if (this.customDragFieldRef.value) {
return this.dropCustomField(area)
}
const field = this.fieldsDragFieldRef.value || { const field = this.fieldsDragFieldRef.value || {
name: '', name: '',
uuid: v4(), uuid: v4(),
@ -2153,6 +2206,122 @@ export default {
}) })
} }
}, },
dropCustomField (area) {
const customField = this.customDragFieldRef.value
const customAreas = customField.areas || []
const field = JSON.parse(JSON.stringify(customField))
field.uuid = v4()
field.submitter_uuid = this.selectedSubmitter.uuid
field.areas = []
if (field.options?.length) {
field.options = field.options.map(opt => ({ ...opt, uuid: v4() }))
}
delete field.conditions
const dropX = (area.x - 6) / area.maskW
const dropY = area.y / area.maskH
if (customAreas.length > 0) {
const refArea = customAreas[0]
customAreas.forEach((customArea) => {
const fieldArea = {
x: dropX + (customArea.x - refArea.x),
y: dropY + (customArea.y - refArea.y) - (customArea.h / 2),
w: customArea.w,
h: customArea.h,
page: area.page,
attachment_uuid: area.attachment_uuid
}
if (customArea.cell_w) {
fieldArea.cell_w = customArea.cell_w
}
if (customArea.option_uuid && field.options?.length) {
const optionIndex = customField.options.findIndex(o => o.uuid === customArea.option_uuid)
if (optionIndex !== -1) {
fieldArea.option_uuid = field.options[optionIndex].uuid
}
}
field.areas.push(fieldArea)
})
} else {
const fieldArea = {
x: dropX,
y: dropY,
page: area.page,
attachment_uuid: area.attachment_uuid
}
this.assignDropAreaSize(fieldArea, field, area)
field.areas.push(fieldArea)
}
this.selectedAreasRef.value = [field.areas[0]]
this.insertField(field)
this.save()
document.activeElement?.blur()
},
onDrawCustomField (area) {
const customField = this.drawCustomField
const customAreas = customField.areas || []
const field = JSON.parse(JSON.stringify(customField))
field.uuid = v4()
field.submitter_uuid = this.selectedSubmitter.uuid
field.areas = []
if (field.options?.length) {
field.options = field.options.map(opt => ({ ...opt, uuid: v4() }))
}
delete field.conditions
const firstArea = {
x: area.x,
y: area.y,
w: area.w || customAreas[0]?.w,
h: area.h || customAreas[0]?.h,
page: area.page,
attachment_uuid: area.attachment_uuid
}
if (!firstArea.w || !firstArea.h) {
if (customAreas[0]) {
firstArea.w = customAreas[0].w
firstArea.h = customAreas[0].h
} else {
this.setDefaultAreaSize(firstArea, field.type)
}
firstArea.x -= firstArea.w / 2
firstArea.y -= firstArea.h / 2
}
if (field.options?.length) {
firstArea.option_uuid = field.options[0].uuid
}
field.areas.push(firstArea)
this.selectedAreasRef.value = [field.areas[0]]
this.insertField(field)
this.save()
this.drawCustomField = null
this.showDrawField = false
},
assignDropAreaSize (fieldArea, field, area) { assignDropAreaSize (fieldArea, field, area) {
const fieldType = field.type || 'text' const fieldType = field.type || 'text'

@ -6,18 +6,24 @@
<span <span
ref="contenteditable" ref="contenteditable"
dir="auto" dir="auto"
:contenteditable="editable" :contenteditable="editable && (!editableOnButton || isEditable)"
style="min-width: 2px" :data-placeholder="placeholder"
:class="[iconInline ? 'inline' : 'block', hideIcon ? 'focus:block' : '']" :data-empty="isEmpty"
class="peer outline-none" :style="{ minWidth }"
:class="[iconInline ? (isEmpty ? 'inline-block' : 'inline') : 'block', hideIcon ? 'focus:block' : '']"
class="peer relative inline-block outline-none before:pointer-events-none before:absolute before:left-0 before:top-0 before:select-none before:whitespace-pre before:text-neutral-400 before:content-[attr(data-placeholder)] before:opacity-0 data-[empty=true]:before:opacity-100"
@paste.prevent="onPaste" @paste.prevent="onPaste"
@keydown.enter.prevent="blurContenteditable" @keydown.enter.prevent="blurContenteditable"
@input="updateInputValue"
@cut="updateInputValue"
@focus="$emit('focus', $event)" @focus="$emit('focus', $event)"
@blur="onBlur" @blur="onBlur"
@click="editable && (!editableOnButton || isEditable) ? '' : $emit('click-contenteditable')"
> >
{{ value }} {{ value }}
</span> </span>
<span <span
v-if="withButton"
class="relative inline" class="relative inline"
:class="{ 'peer-focus:hidden': hideIcon, 'peer-focus:invisible': !hideIcon }" :class="{ 'peer-focus:hidden': hideIcon, 'peer-focus:invisible': !hideIcon }"
> >
@ -28,7 +34,7 @@
:class="{ invisible: !editable, 'absolute top-1/2 -translate-y-1/2': !iconInline || floatIcon, 'inline align-bottom': iconInline, 'left-0': floatIcon }" :class="{ invisible: !editable, 'absolute top-1/2 -translate-y-1/2': !iconInline || floatIcon, 'inline align-bottom': iconInline, 'left-0': floatIcon }"
:width="iconWidth + 4" :width="iconWidth + 4"
:stroke-width="iconStrokeWidth" :stroke-width="iconStrokeWidth"
@click="[focusContenteditable(), selectOnEditClick && selectContent()]" @click="clickEdit"
/> />
</span> </span>
</div> </div>
@ -49,6 +55,16 @@ export default {
required: false, required: false,
default: '' default: ''
}, },
placeholder: {
type: String,
required: false,
default: ''
},
withButton: {
type: Boolean,
required: false,
default: true
},
iconInline: { iconInline: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -74,6 +90,16 @@ export default {
required: false, required: false,
default: false default: false
}, },
editableOnButton: {
type: Boolean,
required: false,
default: false
},
minWidth: {
type: String,
required: false,
default: '2px'
},
editable: { editable: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -85,21 +111,34 @@ export default {
default: 2 default: 2
} }
}, },
emits: ['update:model-value', 'focus', 'blur'], emits: ['update:model-value', 'focus', 'blur', 'click-contenteditable'],
data () { data () {
return { return {
isEditable: false,
inputValue: '',
value: '' value: ''
} }
}, },
computed: {
isEmpty () {
return !this.inputValue.replace(/\u200B/g, '').replace(/\u00A0/g, ' ').trim()
}
},
watch: { watch: {
modelValue: { modelValue: {
handler (value) { handler (value) {
this.value = value this.value = value || ''
}, },
immediate: true immediate: true
} }
}, },
mounted () {
this.updateInputValue()
},
methods: { methods: {
updateInputValue () {
this.inputValue = this.$refs.contenteditable?.textContent || ''
},
onPaste (e) { onPaste (e) {
const text = (e.clipboardData || window.clipboardData).getData('text/plain') const text = (e.clipboardData || window.clipboardData).getData('text/plain')
@ -110,6 +149,20 @@ export default {
selection.getRangeAt(0).insertNode(document.createTextNode(text)) selection.getRangeAt(0).insertNode(document.createTextNode(text))
selection.collapseToEnd() selection.collapseToEnd()
} }
this.updateInputValue()
},
clickEdit (e) {
this.focusContenteditable()
if (this.selectOnEditClick) {
this.selectContent()
}
},
setText (text) {
this.$refs.contenteditable.innerText = text
this.updateInputValue()
}, },
selectContent () { selectContent () {
const el = this.$refs.contenteditable const el = this.$refs.contenteditable
@ -129,10 +182,18 @@ export default {
this.value = this.$refs.contenteditable.innerText.trim() || this.modelValue this.value = this.$refs.contenteditable.innerText.trim() || this.modelValue
this.$emit('update:model-value', this.value) this.$emit('update:model-value', this.value)
this.$emit('blur', e) this.$emit('blur', e)
this.isEditable = false
}, 1) }, 1)
}, },
focusContenteditable () { focusContenteditable () {
this.isEditable = true
this.$nextTick(() => {
this.$refs.contenteditable.focus() this.$refs.contenteditable.focus()
this.updateInputValue()
})
}, },
blurContenteditable () { blurContenteditable () {
this.$refs.contenteditable.blur() this.$refs.contenteditable.blur()

@ -0,0 +1,267 @@
<template>
<div
class="list-field group"
>
<div
class="border border-dashed border-base-300 hover:border-base-content/20 rounded relative group fields-list-item transition-colors"
:style="{ backgroundColor: backgroundColor }"
>
<div class="flex items-center justify-between relative group/contenteditable-container">
<div
v-if="!isNew"
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
@click="$emit('click', field)"
/>
<div
class="absolute top-0 bottom-0 left-0 flex items-center transition-all cursor-grab group-hover:bg-base-200/50"
@click="$emit('click', field)"
>
<IconDrag style="margin-left: 1px" />
</div>
<div class="flex items-center p-1 pl-6 space-x-1">
<FieldType
v-model="field.type"
:editable="false"
:button-width="20"
@click="$emit('click', field)"
/>
<Contenteditable
ref="name"
:model-value="field.name"
:placeholder="'Field Name'"
:icon-inline="true"
:icon-width="18"
:min-width="isNew ? '100px' : '2px'"
:icon-stroke-width="1.6"
:editable-on-button="!isNew"
:with-button="!isNew"
:class="{ 'cursor-pointer': !isNew }"
@click-contenteditable="$emit('click', field)"
@focus="onNameFocus"
@blur="onNameBlur"
/>
</div>
<div
class="flex items-center space-x-1"
>
<PaymentSettings
v-if="field.type === 'payment' && !isNew"
:field="field"
:with-condition="false"
@click-description="isShowDescriptionModal = true"
@click-formula="isShowFormulaModal = true"
/>
<span
v-else-if="!isNew"
class="dropdown dropdown-end field-settings-dropdown"
@mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true"
>
<label
tabindex="0"
:title="t('settings')"
class="cursor-pointer text-transparent group-hover:text-base-content"
>
<IconSettings
:width="18"
:stroke-width="1.6"
/>
</label>
<ul
v-if="renderDropdown"
tabindex="0"
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10"
:style="{ backgroundColor: dropdownBgColor }"
draggable="true"
@dragstart.prevent.stop
@click="closeDropdown"
>
<FieldSettings
:field="field"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:background-color="dropdownBgColor"
:with-areas="false"
:with-copy-to-all-pages="false"
:with-condition="false"
@click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true"
@click-description="isShowDescriptionModal = true"
@save="$emit('save')"
/>
</ul>
</span>
<button
v-if="isNew && !$refs.name"
class="relative text-base-content pr-1 field-save-button"
:title="t('save')"
@click="field.name ? $emit('save', field) : focusName()"
>
<IconCheck
:width="18"
:stroke-width="2"
/>
</button>
<button
class="relative group-hover:text-base-content pr-1 field-remove-button"
:class="isNew ? 'text-base-content' : 'text-transparent group-hover:text-base-content'"
:title="t('remove')"
@click="onRemoveClick"
>
<IconTrashX
:width="18"
:stroke-width="1.6"
/>
</button>
</div>
</div>
</div>
<Teleport
v-if="isShowFormulaModal"
:to="modalContainerEl"
>
<FormulaModal
:field="field"
:default-field="defaultField"
:build-default-name="buildDefaultName"
@close="isShowFormulaModal = false"
/>
</Teleport>
<Teleport
v-if="isShowFontModal"
:to="modalContainerEl"
>
<FontModal
:field="field"
:default-field="defaultField"
:build-default-name="buildDefaultName"
@close="isShowFontModal = false"
/>
</Teleport>
<Teleport
v-if="isShowDescriptionModal"
:to="modalContainerEl"
>
<DescriptionModal
:field="field"
:default-field="defaultField"
:build-default-name="buildDefaultName"
@close="isShowDescriptionModal = false"
/>
</Teleport>
</div>
</template>
<script>
import Contenteditable from './contenteditable'
import FieldType from './field_type'
import PaymentSettings from './payment_settings'
import FieldSettings from './field_settings'
import FormulaModal from './formula_modal'
import FontModal from './font_modal'
import DescriptionModal from './description_modal'
import { IconTrashX, IconSettings, IconCheck } from '@tabler/icons-vue'
import IconDrag from './icon_drag'
export default {
name: 'CustomField',
components: {
Contenteditable,
IconSettings,
IconCheck,
FieldSettings,
PaymentSettings,
IconDrag,
FormulaModal,
FontModal,
DescriptionModal,
IconTrashX,
FieldType
},
inject: ['template', 'backgroundColor', 'selectedAreasRef', 't', 'locale'],
props: {
field: {
type: Object,
required: true
},
isNew: {
type: Boolean,
required: false,
default: false
},
withSignatureId: {
type: Boolean,
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
}
},
emits: ['remove', 'save', 'click'],
data () {
return {
isShowFormulaModal: false,
isShowFontModal: false,
isShowDescriptionModal: false,
renderDropdown: false
}
},
computed: {
dropdownBgColor () {
return ['', null, 'transparent'].includes(this.backgroundColor) ? 'white' : this.backgroundColor
},
modalContainerEl () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
}
},
created () {
this.field.preferences ||= {}
},
mounted () {
if (this.isNew) {
this.focusName()
}
},
methods: {
buildDefaultName () {
return this.t('custom')
},
focusName () {
setTimeout(() => {
this.$refs.name.clickEdit()
}, 1)
},
onNameFocus (e) {
if (!this.field.name) {
setTimeout(() => {
this.$refs.name.$refs.contenteditable.innerText = ' '
}, 1)
}
},
closeDropdown () {
this.$el.getRootNode().activeElement.blur()
},
onRemoveClick () {
if (this.isNew || window.confirm(this.t('are_you_sure_'))) {
this.$emit('remove', this.field)
}
},
onNameBlur (e) {
const text = this.$refs.name.$refs.contenteditable.innerText.trim()
if (text) {
this.field.name = text
} else {
this.$refs.name.setText(this.field.name)
}
if (this.field.name) {
this.$emit('save')
}
}
}
}
</script>

@ -19,6 +19,7 @@
:default-submitters="defaultSubmitters" :default-submitters="defaultSubmitters"
:draw-field="drawField" :draw-field="drawField"
:draw-field-type="drawFieldType" :draw-field-type="drawFieldType"
:draw-custom-field="drawCustomField"
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
:total-pages="sortedPreviewImages.length" :total-pages="sortedPreviewImages.length"
:image="image" :image="image"
@ -28,6 +29,7 @@
@remove-area="$emit('remove-area', $event)" @remove-area="$emit('remove-area', $event)"
@copy-field="$emit('copy-field', $event)" @copy-field="$emit('copy-field', $event)"
@paste-field="$emit('paste-field', { ...$event, attachment_uuid: document.uuid })" @paste-field="$emit('paste-field', { ...$event, attachment_uuid: document.uuid })"
@add-custom-field="$emit('add-custom-field', $event)"
@copy-selected-areas="$emit('copy-selected-areas')" @copy-selected-areas="$emit('copy-selected-areas')"
@delete-selected-areas="$emit('delete-selected-areas')" @delete-selected-areas="$emit('delete-selected-areas')"
@align-selected-areas="$emit('align-selected-areas', $event)" @align-selected-areas="$emit('align-selected-areas', $event)"
@ -115,6 +117,11 @@ export default {
required: false, required: false,
default: null default: null
}, },
drawCustomField: {
type: Object,
required: false,
default: null
},
baseUrl: { baseUrl: {
type: String, type: String,
required: false, required: false,
@ -131,7 +138,7 @@ export default {
default: false default: false
} }
}, },
emits: ['draw', 'drop-field', 'remove-area', 'paste-field', 'copy-field', 'copy-selected-areas', 'delete-selected-areas', 'align-selected-areas', 'autodetect-fields'], emits: ['draw', 'drop-field', 'remove-area', 'paste-field', 'copy-field', 'copy-selected-areas', 'delete-selected-areas', 'align-selected-areas', 'autodetect-fields', 'add-custom-field'],
data () { data () {
return { return {
pageRefs: [] pageRefs: []

@ -8,6 +8,13 @@
class="fixed z-20 pointer-events-none" class="fixed z-20 pointer-events-none"
:editable="false" :editable="false"
/> />
<CustomField
v-else-if="dragPlaceholder && isCustom && !isMask && field"
ref="dragPlaceholder"
:style="dragPlaceholderStyle"
:field="field"
class="fixed z-20 pointer-events-none opacity-90"
/>
<div <div
v-else-if="dragPlaceholder && (isDefault || isRequired) && !isMask && field" v-else-if="dragPlaceholder && (isDefault || isRequired) && !isMask && field"
ref="dragPlaceholder" ref="dragPlaceholder"
@ -57,6 +64,7 @@
<script> <script>
import Field from './field' import Field from './field'
import CustomField from './custom_field'
import IconDrag from './icon_drag' import IconDrag from './icon_drag'
import FieldType from './field_type' import FieldType from './field_type'
@ -64,6 +72,7 @@ export default {
name: 'DragPlaceholder', name: 'DragPlaceholder',
components: { components: {
Field, Field,
CustomField,
IconDrag IconDrag
}, },
inject: ['t', 'backgroundColor'], inject: ['t', 'backgroundColor'],
@ -87,6 +96,11 @@ export default {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false
},
isCustom: {
type: Boolean,
required: false,
default: false
} }
}, },
data () { data () {

@ -1,6 +1,6 @@
<template> <template>
<div <div
class="list-field group mb-2" class="list-field group"
> >
<div <div
class="border border-base-300 rounded relative group fields-list-item" class="border border-base-300 rounded relative group fields-list-item"
@ -18,7 +18,7 @@
:button-width="20" :button-width="20"
:menu-classes="'mt-1.5'" :menu-classes="'mt-1.5'"
:menu-style="{ backgroundColor: dropdownBgColor }" :menu-style="{ backgroundColor: dropdownBgColor }"
@update:model-value="[maybeUpdateOptions(), save()]" @update:model-value="[maybeUpdateOptions(), $emit('save')]"
@click="scrollToFirstArea" @click="scrollToFirstArea"
/> />
<Contenteditable <Contenteditable
@ -92,9 +92,12 @@
<PaymentSettings <PaymentSettings
v-if="field.type === 'payment'" v-if="field.type === 'payment'"
:field="field" :field="field"
:with-custom-fields="withCustomFields"
@click-condition="isShowConditionsModal = true" @click-condition="isShowConditionsModal = true"
@click-description="isShowDescriptionModal = true" @click-description="isShowDescriptionModal = true"
@add-custom-field="$emit('add-custom-field', $event)"
@click-formula="isShowFormulaModal = true" @click-formula="isShowFormulaModal = true"
@save="$emit('save')"
/> />
<span <span
v-else v-else
@ -128,12 +131,15 @@
:with-signature-id="withSignatureId" :with-signature-id="withSignatureId"
:with-prefillable="withPrefillable" :with-prefillable="withPrefillable"
:background-color="dropdownBgColor" :background-color="dropdownBgColor"
:with-custom-fields="withCustomFields"
@click-formula="isShowFormulaModal = true" @click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true" @click-font="isShowFontModal = true"
@click-description="isShowDescriptionModal = true" @click-description="isShowDescriptionModal = true"
@click-condition="isShowConditionsModal = true" @click-condition="isShowConditionsModal = true"
@set-draw="$emit('set-draw', $event)" @set-draw="$emit('set-draw', $event)"
@add-custom-field="$emit('add-custom-field', $event)"
@remove-area="removeArea" @remove-area="removeArea"
@save="$emit('save')"
@scroll-to="$emit('scroll-to', $event)" @scroll-to="$emit('scroll-to', $event)"
/> />
</ul> </ul>
@ -186,7 +192,7 @@
required required
:placeholder="`${t('option')} ${index + 1}`" :placeholder="`${t('option')} ${index + 1}`"
@keydown.enter="option.value ? addOptionAt(index + 1) : null" @keydown.enter="option.value ? addOptionAt(index + 1) : null"
@blur="save" @blur="$emit('save')"
@paste="onOptionPaste($event, index)" @paste="onOptionPaste($event, index)"
> >
<button <button
@ -211,7 +217,7 @@
dir="auto" dir="auto"
@keydown.enter="option.value ? addOptionAt(index + 1) : null" @keydown.enter="option.value ? addOptionAt(index + 1) : null"
@focus="maybeFocusOnOptionArea(option)" @focus="maybeFocusOnOptionArea(option)"
@blur="save" @blur="$emit('save')"
@paste="onOptionPaste($event, index)" @paste="onOptionPaste($event, index)"
> >
<button <button
@ -334,7 +340,7 @@ export default {
IconMathFunction, IconMathFunction,
FieldType FieldType
}, },
inject: ['template', 'save', 'backgroundColor', 'selectedAreasRef', 't', 'locale'], inject: ['template', 'backgroundColor', 'selectedAreasRef', 't', 'locale'],
props: { props: {
field: { field: {
type: Object, type: Object,
@ -345,6 +351,11 @@ export default {
required: false, required: false,
default: null default: null
}, },
withCustomFields: {
type: Boolean,
required: false,
default: false
},
withPrefillable: { withPrefillable: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -366,12 +377,11 @@ export default {
default: true default: true
} }
}, },
emits: ['set-draw', 'remove', 'scroll-to'], emits: ['set-draw', 'remove', 'scroll-to', 'save', 'add-custom-field'],
data () { data () {
return { return {
isExpandOptions: false, isExpandOptions: false,
isNameFocus: false, isNameFocus: false,
showPaymentModal: false,
isShowFormulaModal: false, isShowFormulaModal: false,
isShowFontModal: false, isShowFontModal: false,
isShowConditionsModal: false, isShowConditionsModal: false,
@ -420,7 +430,7 @@ export default {
removeArea (area) { removeArea (area) {
this.field.areas.splice(this.field.areas.indexOf(area), 1) this.field.areas.splice(this.field.areas.indexOf(area), 1)
this.save() this.$emit('save')
}, },
buildDefaultName (field, fields) { buildDefaultName (field, fields) {
if (field.type === 'payment' && field.preferences?.price && !field.preferences?.formula) { if (field.type === 'payment' && field.preferences?.price && !field.preferences?.formula) {
@ -489,7 +499,7 @@ export default {
inputs[index + newOptions.length]?.focus() inputs[index + newOptions.length]?.focus()
}) })
this.save() this.$emit('save')
} }
} }
}, },
@ -506,7 +516,7 @@ export default {
inputs[insertAt]?.focus() inputs[insertAt]?.focus()
}) })
this.save() this.$emit('save')
}, },
removeOption (option) { removeOption (option) {
this.field.options.splice(this.field.options.indexOf(option), 1) this.field.options.splice(this.field.options.indexOf(option), 1)
@ -517,7 +527,7 @@ export default {
this.field.areas.splice(this.field.areas.findIndex((a) => a.option_uuid === option.uuid), 1) this.field.areas.splice(this.field.areas.findIndex((a) => a.option_uuid === option.uuid), 1)
} }
this.save() this.$emit('save')
}, },
maybeUpdateOptions () { maybeUpdateOptions () {
delete this.field.default_value delete this.field.default_value
@ -559,7 +569,7 @@ export default {
this.isNameFocus = false this.isNameFocus = false
this.save() this.$emit('save')
}, },
onOptionDragstart (event, option) { onOptionDragstart (event, option) {
this.optionDragRef = option this.optionDragRef = option
@ -620,7 +630,7 @@ export default {
if (newOrder.length === this.field.options.length) { if (newOrder.length === this.field.options.length) {
this.field.options.splice(0, this.field.options.length, ...newOrder) this.field.options.splice(0, this.field.options.length, ...newOrder)
this.save() this.$emit('save')
} }
this.optionDragRef = null this.optionDragRef = null

@ -7,7 +7,7 @@
<select <select
:placeholder="t('method')" :placeholder="t('method')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent" class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
@change="[field.preferences ||= {}, field.preferences.method = $event.target.value, save()]" @change="[field.preferences ||= {}, field.preferences.method = $event.target.value, $emit('save')]"
> >
<option <option
v-for="method in ['QeS', 'AeS']" v-for="method in ['QeS', 'AeS']"
@ -33,7 +33,7 @@
> >
<select <select
class="select select-bordered select-xs w-full max-w-xs h-7 !outline-0 font-normal bg-transparent" class="select select-bordered select-xs w-full max-w-xs h-7 !outline-0 font-normal bg-transparent"
@change="[field.preferences ||= {}, field.preferences.align = $event.target.value, save()]" @change="[field.preferences ||= {}, field.preferences.align = $event.target.value, $emit('save')]"
> >
<option <option
v-for="value in ['left', 'right', field.type === 'cells' ? null : 'center'].filter(Boolean)" v-for="value in ['left', 'right', field.type === 'cells' ? null : 'center'].filter(Boolean)"
@ -61,7 +61,7 @@
:placeholder="t('default_value')" :placeholder="t('default_value')"
dir="auto" dir="auto"
class="select select-bordered select-xs w-full max-w-xs h-7 !outline-0 font-normal bg-transparent" class="select select-bordered select-xs w-full max-w-xs h-7 !outline-0 font-normal bg-transparent"
@change="[field.default_value = $event.target.value, !field.default_value && delete field.default_value, save()]" @change="[field.default_value = $event.target.value, !field.default_value && delete field.default_value, $emit('save')]"
> >
<option <option
value="" value=""
@ -97,7 +97,7 @@
dir="auto" dir="auto"
:type="field.type" :type="field.type"
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent" class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
@blur="save" @blur="$emit('save')"
> >
<label <label
v-if="field.default_value" v-if="field.default_value"
@ -159,7 +159,7 @@
:value="lengthValidation.min" :value="lengthValidation.min"
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent" class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
@input="field.validation.pattern = `.{${$event.target.value || 0},${lengthValidation.max || ''}}`" @input="field.validation.pattern = `.{${$event.target.value || 0},${lengthValidation.max || ''}}`"
@blur="save" @blur="$emit('save')"
> >
<label <label
v-if="lengthValidation.min" v-if="lengthValidation.min"
@ -178,7 +178,7 @@
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent" class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
:value="lengthValidation.max" :value="lengthValidation.max"
@input="field.validation.pattern = `.{${lengthValidation.min},${$event.target.value || ''}}`" @input="field.validation.pattern = `.{${lengthValidation.min},${$event.target.value || ''}}`"
@blur="save" @blur="$emit('save')"
> >
<label <label
v-if="lengthValidation.max" v-if="lengthValidation.max"
@ -203,7 +203,7 @@
:value="field.validation?.min" :value="field.validation?.min"
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent" class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
@input="[field.validation ||= {}, $event.target.value ? field.validation.min = $event.target.value : delete field.validation.min]" @input="[field.validation ||= {}, $event.target.value ? field.validation.min = $event.target.value : delete field.validation.min]"
@blur="save" @blur="$emit('save')"
> >
<label <label
v-if="field.validation?.min" v-if="field.validation?.min"
@ -222,7 +222,7 @@
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent" class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
:value="field.validation?.max" :value="field.validation?.max"
@input="[field.validation ||= {}, $event.target.value ? field.validation.max = $event.target.value : delete field.validation.max]" @input="[field.validation ||= {}, $event.target.value ? field.validation.max = $event.target.value : delete field.validation.max]"
@blur="save" @blur="$emit('save')"
> >
<label <label
v-if="field.validation?.max" v-if="field.validation?.max"
@ -242,7 +242,7 @@
<select <select
:placeholder="t('format')" :placeholder="t('format')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent" class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
@change="[field.preferences ||= {}, field.preferences.format = $event.target.value, save()]" @change="[field.preferences ||= {}, field.preferences.format = $event.target.value, $emit('save')]"
> >
<option <option
v-for="format in numberFormats" v-for="format in numberFormats"
@ -272,7 +272,7 @@
:placeholder="t('regexp_validation')" :placeholder="t('regexp_validation')"
dir="auto" dir="auto"
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent" class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
@blur="save" @blur="$emit('save')"
> >
<label <label
v-if="field.validation.pattern" v-if="field.validation.pattern"
@ -293,7 +293,7 @@
:placeholder="t('error_message')" :placeholder="t('error_message')"
dir="auto" dir="auto"
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent" class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
@blur="save" @blur="$emit('save')"
> >
<label <label
v-if="field.validation.message" v-if="field.validation.message"
@ -313,7 +313,7 @@
v-model="field.preferences.format" v-model="field.preferences.format"
:placeholder="t('format')" :placeholder="t('format')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent" class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
@change="save" @change="$emit('save')"
> >
<option <option
v-for="format in dateFormats" v-for="format in dateFormats"
@ -339,7 +339,7 @@
<select <select
:placeholder="t('format')" :placeholder="t('format')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent" class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
@change="[field.preferences.format = $event.target.value, save()]" @change="[field.preferences.format = $event.target.value, $emit('save')]"
> >
<option <option
value="any" value="any"
@ -374,7 +374,7 @@
type="checkbox" type="checkbox"
:disabled="!editable || (defaultField && [true, false].includes(defaultField.required))" :disabled="!editable || (defaultField && [true, false].includes(defaultField.required))"
class="toggle toggle-xs" class="toggle toggle-xs"
@change="[field.preferences ||= {}, field.preferences.with_signature_id = $event.target.checked, save()]" @change="[field.preferences ||= {}, field.preferences.with_signature_id = $event.target.checked, $emit('save')]"
> >
<span class="label-text">{{ t('signature_id') }}</span> <span class="label-text">{{ t('signature_id') }}</span>
</label> </label>
@ -389,7 +389,7 @@
type="checkbox" type="checkbox"
:disabled="!editable || (defaultField && [true, false].includes(defaultField.required))" :disabled="!editable || (defaultField && [true, false].includes(defaultField.required))"
class="toggle toggle-xs" class="toggle toggle-xs"
@update:model-value="save" @update:model-value="$emit('save')"
> >
<span class="label-text">{{ t('required') }}</span> <span class="label-text">{{ t('required') }}</span>
</label> </label>
@ -403,7 +403,7 @@
:checked="field.preferences?.with_logo != false" :checked="field.preferences?.with_logo != false"
type="checkbox" type="checkbox"
class="toggle toggle-xs" class="toggle toggle-xs"
@change="[field.preferences ||= {}, field.preferences.with_logo = field.preferences.with_logo == false, save()]" @change="[field.preferences ||= {}, field.preferences.with_logo = field.preferences.with_logo == false, $emit('save')]"
> >
<span class="label-text">{{ t('with_logo') }}</span> <span class="label-text">{{ t('with_logo') }}</span>
</label> </label>
@ -417,7 +417,7 @@
v-model="field.default_value" v-model="field.default_value"
type="checkbox" type="checkbox"
class="toggle toggle-xs" class="toggle toggle-xs"
@update:model-value="[field.default_value = $event, field.readonly = $event, save()]" @update:model-value="[field.default_value = $event, field.readonly = $event, $emit('save')]"
> >
<span class="label-text">{{ t('checked') }}</span> <span class="label-text">{{ t('checked') }}</span>
</label> </label>
@ -431,7 +431,7 @@
v-model="field.readonly" v-model="field.readonly"
type="checkbox" type="checkbox"
class="toggle toggle-xs" class="toggle toggle-xs"
@update:model-value="[field.default_value = $event ? '{{date}}' : null, field.readonly = $event, save()]" @update:model-value="[field.default_value = $event ? '{{date}}' : null, field.readonly = $event, $emit('save')]"
> >
<span class="label-text">{{ t('set_signing_date') }}</span> <span class="label-text">{{ t('set_signing_date') }}</span>
</label> </label>
@ -446,7 +446,7 @@
type="checkbox" type="checkbox"
class="toggle toggle-xs" class="toggle toggle-xs"
:disabled="!editable || (defaultField && [true, false].includes(defaultField.readonly))" :disabled="!editable || (defaultField && [true, false].includes(defaultField.readonly))"
@update:model-value="save" @update:model-value="$emit('save')"
> >
<span class="label-text">{{ t('read_only') }}</span> <span class="label-text">{{ t('read_only') }}</span>
</label> </label>
@ -461,7 +461,7 @@
type="checkbox" type="checkbox"
:disabled="!editable || (defaultField && [true, false].includes(defaultField.prefillable))" :disabled="!editable || (defaultField && [true, false].includes(defaultField.prefillable))"
class="toggle toggle-xs" class="toggle toggle-xs"
@update:model-value="save" @update:model-value="$emit('save')"
> >
<span class="label-text">{{ t('prefillable') }}</span> <span class="label-text">{{ t('prefillable') }}</span>
</label> </label>
@ -499,7 +499,7 @@
</label> </label>
</li> </li>
<li <li
v-if="field.type != 'stamp' && field.type != 'heading'" v-if="withCondition && field.type != 'stamp' && field.type != 'heading'"
> >
<label <label
class="label-text cursor-pointer text-center w-full flex items-center" class="label-text cursor-pointer text-center w-full flex items-center"
@ -526,7 +526,10 @@
</span> </span>
</label> </label>
</li> </li>
<hr class="pb-0.5 mt-0.5"> <hr
v-if="withCopyToAllPages || withAreas || withCustomFields"
class="pb-0.5 mt-0.5"
>
<template v-if="withAreas"> <template v-if="withAreas">
<li <li
v-for="(area, index) in sortedAreas" v-for="(area, index) in sortedAreas"
@ -564,7 +567,7 @@
</a> </a>
</li> </li>
</template> </template>
<li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells', 'stamp'].includes(field.type)"> <li v-if="withCopyToAllPages && field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells', 'stamp'].includes(field.type)">
<a <a
href="#" href="#"
class="text-sm py-1 px-2" class="text-sm py-1 px-2"
@ -577,10 +580,23 @@
{{ t('copy_to_all_pages') }} {{ t('copy_to_all_pages') }}
</a> </a>
</li> </li>
<li v-if="withCustomFields">
<a
href="#"
class="text-sm py-1 px-2"
@click.prevent="$emit('add-custom-field', field)"
>
<IconForms
:width="20"
:stroke-width="1.6"
/>
{{ t('save_as_custom_field') }}
</a>
</li>
</template> </template>
<script> <script>
import { IconRouteAltLeft, IconTypography, IconShape, IconX, IconMathFunction, IconNewSection, IconInfoCircle, IconCopy } from '@tabler/icons-vue' import { IconRouteAltLeft, IconTypography, IconShape, IconX, IconMathFunction, IconNewSection, IconInfoCircle, IconCopy, IconForms } from '@tabler/icons-vue'
export default { export default {
name: 'FieldSettings', name: 'FieldSettings',
@ -589,17 +605,33 @@ export default {
IconInfoCircle, IconInfoCircle,
IconMathFunction, IconMathFunction,
IconRouteAltLeft, IconRouteAltLeft,
IconForms,
IconCopy, IconCopy,
IconNewSection, IconNewSection,
IconTypography, IconTypography,
IconX IconX
}, },
inject: ['template', 'save', 't'], inject: ['template', 't'],
props: { props: {
field: { field: {
type: Object, type: Object,
required: true required: true
}, },
withCondition: {
type: Boolean,
required: false,
default: true
},
withCustomFields: {
type: Boolean,
required: false,
default: false
},
withCopyToAllPages: {
type: Boolean,
required: false,
default: true
},
withSignatureId: { withSignatureId: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -636,7 +668,7 @@ export default {
default: true default: true
} }
}, },
emits: ['set-draw', 'scroll-to', 'click-formula', 'click-description', 'click-condition', 'click-font', 'remove-area'], emits: ['set-draw', 'scroll-to', 'click-formula', 'click-description', 'click-condition', 'click-font', 'remove-area', 'save', 'add-custom-field'],
data () { data () {
return { return {
selectedValidation: '' selectedValidation: ''
@ -730,7 +762,7 @@ export default {
delete this.field.validation delete this.field.validation
} }
this.save() this.$emit('save')
}, },
copyToAllPages (field) { copyToAllPages (field) {
const areaString = JSON.stringify(field.areas[0]) const areaString = JSON.stringify(field.areas[0])
@ -747,7 +779,7 @@ export default {
this.$emit('scroll-to', this.field.areas[this.field.areas.length - 1]) this.$emit('scroll-to', this.field.areas[this.field.areas.length - 1])
this.save() this.$emit('save')
}, },
formatNumber (number, format) { formatNumber (number, format) {
if (format === 'comma') { if (format === 'comma') {

@ -15,7 +15,8 @@
</div> </div>
<div <div
ref="fields" ref="fields"
class="fields mb-1 mt-2" class="fields mt-2"
:class="{ 'mb-1': !withCustomFields || !customFields.length }"
@dragover.prevent="onFieldDragover" @dragover.prevent="onFieldDragover"
@drop="fieldsDragFieldRef.value ? reorderFields() : null" @drop="fieldsDragFieldRef.value ? reorderFields() : null"
> >
@ -30,7 +31,11 @@
:with-prefillable="withPrefillable" :with-prefillable="withPrefillable"
:default-field="defaultFieldsIndex[field.name]" :default-field="defaultFieldsIndex[field.name]"
:draggable="editable" :draggable="editable"
:with-custom-fields="withCustomFields"
class="mb-1.5"
@add-custom-field="addCustomField"
@dragstart="[fieldsDragFieldRef.value = field, removeDragOverlay($event), setDragPlaceholder($event)]" @dragstart="[fieldsDragFieldRef.value = field, removeDragOverlay($event), setDragPlaceholder($event)]"
@save="save"
@dragend="[fieldsDragFieldRef.value = null, $emit('set-drag-placeholder', null)]" @dragend="[fieldsDragFieldRef.value = null, $emit('set-drag-placeholder', null)]"
@remove="removeField" @remove="removeField"
@scroll-to="$emit('scroll-to-area', $event)" @scroll-to="$emit('scroll-to-area', $event)"
@ -105,7 +110,92 @@
</div> </div>
</div> </div>
<div <div
v-if="editable && !onlyDefinedFields" v-if="editable && withCustomFields && (customFields.length || newCustomField)"
class="tabs w-full mb-1.5"
>
<a
class="tab tab-bordered w-1/2 border-base-300"
:class="{ 'tab-active': !showCustomTab }"
:style="{ '--tab-border': showCustomTab ? '0px' : '0.5px' }"
@click="setFieldsTab('default')"
>{{ t('default') }}</a>
<a
class="tab tab-bordered w-1/2 border-base-300"
:class="{ 'tab-active': showCustomTab }"
:style="{ '--tab-border': showCustomTab ? '0.5px' : '0px' }"
@click="setFieldsTab('custom')"
>{{ t('custom') }}</a>
</div>
<div
v-if="showCustomTab && editable"
ref="customFields"
class="custom-fields"
@dragover.prevent="onCustomFieldDragover"
@drop="customDragFieldRef.value ? reorderCustomFields() : null"
>
<template v-if="isShowCustomFieldSearch">
<input
v-model="customFieldsSearch"
:placeholder="t('search_field')"
class="input input-ghost input-xs px-0 text-base mb-1 !outline-0 !rounded bg-transparent w-full"
>
<hr class="mb-2">
</template>
<div
ref="customFieldsList"
class="overflow-auto relative"
:style="{
maxHeight: isShowCustomFieldSearch ? '320px' : '',
minHeight: '320px'
}"
>
<div
v-if="!filteredCustomFields.length && customFieldsSearch"
class="top-0 bottom-0 text-center absolute flex items-center justify-center w-full flex-col"
>
<div>
{{ t('field_not_found') }}
</div>
<a
href="#"
class="link"
@click.prevent="customFieldsSearch = ''"
>
{{ t('clear') }}
</a>
</div>
<CustomField
v-if="newCustomField"
:key="newCustomField.uuid"
ref="newCustomField"
:data-uuid="newCustomField.uuid"
:is-new="true"
:field="newCustomField"
:draggable="false"
class="mb-1.5"
@save="onNewCustomFieldSave"
@remove="newCustomField = null"
/>
<CustomField
v-for="field in filteredCustomFields"
:key="field.uuid"
:data-uuid="field.uuid"
:field="field"
:draggable="true"
class="mb-1.5"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
@save="saveCustomFields"
@click="$emit('set-draw-custom-field', field)"
@dragstart="[customDragFieldRef.value = field, removeDragOverlay($event), setDragPlaceholder($event)]"
@dragend="[customDragFieldRef.value = null, $emit('set-drag-placeholder', null)]"
@remove="removeCustomField"
@set-draw="$emit('set-draw', $event)"
/>
</div>
</div>
<div
v-if="editable && !onlyDefinedFields && (!showCustomTab || (!customFields.length && !newCustomField))"
id="field-types-grid" id="field-types-grid"
class="grid grid-cols-3 gap-1 pb-2 fields-grid" class="grid grid-cols-3 gap-1 pb-2 fields-grid"
> >
@ -266,15 +356,18 @@
<script> <script>
import Field from './field' import Field from './field'
import CustomField from './custom_field'
import FieldType from './field_type' import FieldType from './field_type'
import FieldSubmitter from './field_submitter' import FieldSubmitter from './field_submitter'
import { IconLock, IconCirclePlus, IconInnerShadowTop, IconSparkles } from '@tabler/icons-vue' import { IconLock, IconCirclePlus, IconInnerShadowTop, IconSparkles } from '@tabler/icons-vue'
import IconDrag from './icon_drag' import IconDrag from './icon_drag'
import { v4 } from 'uuid'
export default { export default {
name: 'TemplateFields', name: 'TemplateFields',
components: { components: {
Field, Field,
CustomField,
FieldType, FieldType,
IconCirclePlus, IconCirclePlus,
IconSparkles, IconSparkles,
@ -283,12 +376,22 @@ export default {
IconDrag, IconDrag,
IconLock IconLock
}, },
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withKba', 'withPayment', 't', 'fieldsDragFieldRef', 'baseFetch', 'selectedAreasRef'], inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withKba', 'withPayment', 't', 'fieldsDragFieldRef', 'customDragFieldRef', 'baseFetch', 'selectedAreasRef'],
props: { props: {
fields: { fields: {
type: Array, type: Array,
required: true required: true
}, },
customFields: {
type: Array,
required: false,
default: () => []
},
withCustomFields: {
type: Boolean,
required: false,
default: false
},
withFieldsSearch: { withFieldsSearch: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -372,12 +475,15 @@ export default {
default: false default: false
} }
}, },
emits: ['add-field', 'set-draw', 'set-draw-type', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter', 'set-drag-placeholder', 'select-submitter'], emits: ['add-field', 'set-draw', 'set-draw-type', 'set-draw-custom-field', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter', 'set-drag-placeholder', 'select-submitter'],
data () { data () {
return { return {
fieldPagesLoaded: null, fieldPagesLoaded: null,
analyzingProgress: 0, analyzingProgress: 0,
defaultFieldsSearch: '' newCustomField: null,
showCustomTab: false,
defaultFieldsSearch: '',
customFieldsSearch: ''
} }
}, },
computed: { computed: {
@ -430,6 +536,23 @@ export default {
} else { } else {
return this.submitterDefaultFields return this.submitterDefaultFields
} }
},
isShowCustomFieldSearch () {
return this.customFields.length > 8
},
filteredCustomFields () {
if (this.customFieldsSearch) {
return this.customFields.filter((f) => f.name.toLowerCase().includes(this.customFieldsSearch.toLowerCase()))
} else {
return this.customFields
}
}
},
mounted () {
try {
this.showCustomTab = localStorage.getItem('docuseal_builder_tab') === 'custom'
} catch (e) {
console.error(e)
} }
}, },
methods: { methods: {
@ -440,6 +563,61 @@ export default {
this.$emit('set-drag', field) this.$emit('set-drag', field)
}, },
onNewCustomFieldSave () {
if (this.newCustomField.name) {
this.customFields.unshift(this.newCustomField)
this.newCustomField = null
this.saveCustomFields()
} else {
this.newCustomField = null
}
},
addCustomField (field) {
const customField = JSON.parse(JSON.stringify(field))
customField.uuid = v4()
delete customField.submitter_uuid
delete customField.prefillable
delete customField.conditions
customField.areas?.forEach((area) => {
delete customField.attachment_uuid
delete customField.page
})
if (customField.name) {
this.customFields.unshift(customField)
this.saveCustomFields()
} else {
this.newCustomField = customField
}
this.setFieldsTab('custom')
},
setFieldsTab (type) {
try {
localStorage.setItem('docuseal_builder_tab', type)
} catch (e) {
console.error(e)
}
this.showCustomTab = type === 'custom'
},
saveCustomFields () {
return this.baseFetch('/account_custom_fields', {
method: 'POST',
body: JSON.stringify({
value: this.customFields
}),
headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => {
const fields = await resp.json()
this.customFields.splice(0, this.customFields.length, ...fields)
})
},
detectFields () { detectFields () {
const fields = [] const fields = []
@ -617,6 +795,47 @@ export default {
if (save) { if (save) {
this.save() this.save()
} }
},
removeCustomField (field) {
this.customFields.splice(this.customFields.indexOf(field), 1)
if (!this.customFields.length) {
this.setFieldsTab('default')
}
this.saveCustomFields()
},
onCustomFieldDragover (e) {
if (this.customDragFieldRef.value && this.customFields.includes(this.customDragFieldRef.value)) {
const container = this.$refs.customFieldsList
const targetField = e.target.closest('[data-uuid]')
const dragField = container.querySelector(`[data-uuid="${this.customDragFieldRef.value.uuid}"]`)
if (dragField && targetField && targetField !== dragField) {
const fields = Array.from(container.children)
const currentIndex = fields.indexOf(dragField)
const targetIndex = fields.indexOf(targetField)
if (currentIndex < targetIndex) {
targetField.after(dragField)
} else {
targetField.before(dragField)
}
}
}
},
reorderCustomFields () {
if (!this.customFields.includes(this.customDragFieldRef.value)) {
return
}
const reorderedFields = Array.from(this.$refs.customFieldsList.children).map((el) => {
return this.customFields.find((f) => f.uuid === el.dataset.uuid)
}).filter(Boolean)
this.customFields.splice(0, this.customFields.length, ...reorderedFields)
this.saveCustomFields()
} }
} }
} }

@ -1,10 +1,12 @@
const en = { const en = {
default: 'Default',
save_as_custom_field: 'Save as Custom Field',
kba: 'KBA', kba: 'KBA',
analyzing_: 'Analyzing...', analyzing_: 'Analyzing...',
download: 'Download', download: 'Download',
downloading_: 'Downloading...', downloading_: 'Downloading...',
view: 'View', view: 'View',
autodetect_fields: 'Autodetect fields', autodetect_fields: 'Autodetect Fields',
payment_link: 'Payment link', payment_link: 'Payment link',
strikeout: 'Strikeout', strikeout: 'Strikeout',
draw_strikethrough_the_document: 'Draw strikethrough the document', draw_strikethrough_the_document: 'Draw strikethrough the document',
@ -200,6 +202,8 @@ const en = {
} }
const es = { const es = {
default: 'Predeterminado',
save_as_custom_field: 'Guardar como personalizado',
kba: 'KBA', kba: 'KBA',
autodetect_fields: 'Autodetectar campos', autodetect_fields: 'Autodetectar campos',
analyzing_: 'Analizando...', analyzing_: 'Analizando...',
@ -401,6 +405,8 @@ const es = {
} }
const it = { const it = {
default: 'Predefinito',
save_as_custom_field: 'Salva come personalizzato',
kba: 'KBA', kba: 'KBA',
autodetect_fields: 'Rileva campi', autodetect_fields: 'Rileva campi',
analyzing_: 'Analisi...', analyzing_: 'Analisi...',
@ -602,6 +608,8 @@ const it = {
} }
const pt = { const pt = {
default: 'Padrão',
save_as_custom_field: 'Salvar como personalizado',
kba: 'KBA', kba: 'KBA',
autodetect_fields: 'Detectar campos', autodetect_fields: 'Detectar campos',
analyzing_: 'Analisando...', analyzing_: 'Analisando...',
@ -803,6 +811,8 @@ const pt = {
} }
const fr = { const fr = {
default: 'Par défaut',
save_as_custom_field: 'Enregistrer comme personnalisé',
kba: 'KBA', kba: 'KBA',
autodetect_fields: 'Détecter les champs', autodetect_fields: 'Détecter les champs',
analyzing_: 'Analyse...', analyzing_: 'Analyse...',
@ -1004,6 +1014,8 @@ const fr = {
} }
const de = { const de = {
default: 'Standard',
save_as_custom_field: 'Als benutzerdefiniert speichern',
kba: 'KBA', kba: 'KBA',
autodetect_fields: 'Felder erkennen', autodetect_fields: 'Felder erkennen',
analyzing_: 'Analysiere...', analyzing_: 'Analysiere...',
@ -1205,6 +1217,8 @@ const de = {
} }
const nl = { const nl = {
default: 'Standaard',
save_as_custom_field: 'Opslaan als aangepast',
kba: 'KBA', kba: 'KBA',
autodetect_fields: 'Velden detecteren', autodetect_fields: 'Velden detecteren',
analyzing_: 'Analyseren...', analyzing_: 'Analyseren...',

@ -52,15 +52,22 @@
@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)"
@add-custom-field="$emit('add-custom-field', $event)"
@contextmenu="openAreaContextMenu($event, item.area, item.field)" @contextmenu="openAreaContextMenu($event, item.area, item.field)"
/> />
<FieldArea <FieldArea
v-if="newArea" v-for="(area, index) in newAreas"
:key="index"
:is-draw="true" :is-draw="true"
:page-width="width" :page-width="width"
:page-height="height" :page-height="height"
:field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || dragFieldPlaceholder?.type || defaultFieldType }" :field="{ submitter_uuid: selectedSubmitter.uuid, type: newAreaFieldType }"
:area="newArea" :area="area"
/>
<div
v-if="newAreas.length > 1"
class="absolute outline-dashed outline-gray-400 pointer-events-none z-20"
:style="newAreasBoxStyle"
/> />
<div <div
v-if="selectionRect" v-if="selectionRect"
@ -93,7 +100,7 @@
/> />
</div> </div>
<div <div
v-show="resizeDirection || isDrag || showMask || (drawField && isMobile) || fieldsDragFieldRef.value || selectionRect" v-show="resizeDirection || isDrag || showMask || (drawField && isMobile) || fieldsDragFieldRef.value || customDragFieldRef?.value || selectionRect"
id="mask" id="mask"
ref="mask" ref="mask"
class="top-0 bottom-0 left-0 right-0 absolute" class="top-0 bottom-0 left-0 right-0 absolute"
@ -103,7 +110,7 @@
@contextmenu="openContextMenu" @contextmenu="openContextMenu"
@dragover.prevent="onDragover" @dragover.prevent="onDragover"
@dragenter="onDragenter" @dragenter="onDragenter"
@dragleave="newArea = null" @dragleave="newAreas = []"
@drop="onDrop" @drop="onDrop"
@pointerup="onPointerup" @pointerup="onPointerup"
/> />
@ -122,7 +129,7 @@ export default {
ContextMenu, ContextMenu,
SelectionBox SelectionBox
}, },
inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'assignDropAreaSize', 'selectedAreasRef', 'template', 'isSelectModeRef'], inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'customDragFieldRef', 'assignDropAreaSize', 'selectedAreasRef', 'template', 'isSelectModeRef'],
props: { props: {
image: { image: {
type: Object, type: Object,
@ -191,6 +198,11 @@ export default {
required: false, required: false,
default: null default: null
}, },
drawCustomField: {
type: Object,
required: false,
default: null
},
editable: { editable: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -216,13 +228,13 @@ export default {
default: false default: false
} }
}, },
emits: ['draw', 'drop-field', 'remove-area', 'copy-field', 'paste-field', 'scroll-to', 'copy-selected-areas', 'delete-selected-areas', 'align-selected-areas', 'autodetect-fields'], emits: ['draw', 'drop-field', 'remove-area', 'copy-field', 'paste-field', 'scroll-to', 'copy-selected-areas', 'delete-selected-areas', 'align-selected-areas', 'autodetect-fields', 'add-custom-field'],
data () { data () {
return { return {
areaRefs: [], areaRefs: [],
showMask: false, showMask: false,
resizeDirection: null, resizeDirection: null,
newArea: null, newAreas: [],
contextMenu: null, contextMenu: null,
selectionRect: null, selectionRect: null,
selectionContextMenu: null selectionContextMenu: null
@ -274,6 +286,14 @@ export default {
return acc return acc
}, {}) }, {})
}, },
newAreaFieldType () {
if (this.drawField?.type) return this.drawField.type
if (this.drawCustomField?.type) return this.drawCustomField.type
if (this.dragFieldPlaceholder?.type) return this.dragFieldPlaceholder.type
if (this.customDragFieldRef?.value?.type) return this.customDragFieldRef.value.type
return this.defaultFieldType
},
defaultFieldType () { defaultFieldType () {
if (this.drawFieldType) { if (this.drawFieldType) {
return this.drawFieldType return this.drawFieldType
@ -302,6 +322,21 @@ export default {
height () { height () {
return this.image.metadata.height return this.image.metadata.height
}, },
newAreasBoxStyle () {
if (this.newAreas.length < 2) return {}
const minX = Math.min(...this.newAreas.map(a => a.x))
const minY = Math.min(...this.newAreas.map(a => a.y))
const maxX = Math.max(...this.newAreas.map(a => a.x + a.w))
const maxY = Math.max(...this.newAreas.map(a => a.y + a.h))
return {
left: minX * 100 + '%',
top: minY * 100 + '%',
width: (maxX - minX) * 100 + '%',
height: (maxY - minY) * 100 + '%'
}
},
selectionRectStyle () { selectionRectStyle () {
if (!this.selectionRect) return {} if (!this.selectionRect) return {}
@ -331,7 +366,7 @@ export default {
const rect = this.$refs.image.getBoundingClientRect() const rect = this.$refs.image.getBoundingClientRect()
this.newArea = null this.newAreas = []
this.showMask = false this.showMask = false
this.contextMenu = { this.contextMenu = {
@ -351,7 +386,7 @@ export default {
const rect = this.$refs.image.getBoundingClientRect() const rect = this.$refs.image.getBoundingClientRect()
this.newArea = null this.newAreas = []
this.showMask = false this.showMask = false
this.contextMenu = { this.contextMenu = {
@ -390,7 +425,7 @@ export default {
}, },
closeContextMenu () { closeContextMenu () {
this.contextMenu = null this.contextMenu = null
this.newArea = null this.newAreas = []
this.showMask = false this.showMask = false
}, },
handleCopy () { handleCopy () {
@ -410,7 +445,7 @@ export default {
this.closeContextMenu() this.closeContextMenu()
}, },
handlePaste () { handlePaste () {
this.newArea = null this.newAreas = []
this.showMask = false this.showMask = false
this.$emit('paste-field', { this.$emit('paste-field', {
@ -435,22 +470,66 @@ export default {
} }
}, },
onDragenter (e) { onDragenter (e) {
this.newArea = {} const customField = this.customDragFieldRef?.value
const customAreas = customField?.areas || []
const dropX = (e.offsetX - 6) / this.$refs.mask.clientWidth
const dropY = e.offsetY / this.$refs.mask.clientHeight
if (customAreas.length > 1) {
const refArea = customAreas[0]
this.newAreas = customAreas.map((customArea) => ({
x: dropX + (customArea.x - refArea.x),
y: dropY + (customArea.y - refArea.y) - (customArea.h / 2),
w: customArea.w,
h: customArea.h
}))
} else {
const newArea = {}
this.assignDropAreaSize(this.newArea, this.dragFieldPlaceholder, { if (customAreas.length === 1) {
newArea.w = customAreas[0].w
newArea.h = customAreas[0].h
} else if (customField) {
this.assignDropAreaSize(newArea, customField, {
maskW: this.$refs.mask.clientWidth,
maskH: this.$refs.mask.clientHeight
})
} else {
this.assignDropAreaSize(newArea, this.dragFieldPlaceholder, {
maskW: this.$refs.mask.clientWidth, maskW: this.$refs.mask.clientWidth,
maskH: this.$refs.mask.clientHeight maskH: this.$refs.mask.clientHeight
}) })
}
this.newArea.x = (e.offsetX - 6) / this.$refs.mask.clientWidth newArea.x = dropX
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight - this.newArea.h / 2 newArea.y = dropY - newArea.h / 2
this.newAreas = [newArea]
}
}, },
onDragover (e) { onDragover (e) {
this.newArea.x = (e.offsetX - 6) / this.$refs.mask.clientWidth const customField = this.customDragFieldRef?.value
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight - this.newArea.h / 2 const customAreas = customField?.areas || []
const dropX = (e.offsetX - 6) / this.$refs.mask.clientWidth
const dropY = e.offsetY / this.$refs.mask.clientHeight
if (customAreas.length > 1) {
const refArea = customAreas[0]
this.newAreas.forEach((newArea, index) => {
const customArea = customAreas[index]
newArea.x = dropX + (customArea.x - refArea.x)
newArea.y = dropY + (customArea.y - refArea.y) - (customArea.h / 2)
})
} else if (this.newAreas.length) {
this.newAreas[0].x = dropX
this.newAreas[0].y = dropY - this.newAreas[0].h / 2
}
}, },
onDrop (e) { onDrop (e) {
this.newArea = null this.newAreas = []
this.$emit('drop-field', { this.$emit('drop-field', {
x: e.offsetX, x: e.offsetX,
@ -479,7 +558,7 @@ export default {
return return
} }
if (this.isMobile && !this.drawField) { if (this.isMobile && !this.drawField && !this.drawCustomField) {
return return
} }
@ -490,14 +569,14 @@ export default {
this.showMask = true this.showMask = true
this.$nextTick(() => { this.$nextTick(() => {
this.newArea = { this.newAreas = [{
initialX: e.offsetX / this.$refs.mask.clientWidth, initialX: e.offsetX / this.$refs.mask.clientWidth,
initialY: e.offsetY / this.$refs.mask.clientHeight, initialY: e.offsetY / this.$refs.mask.clientHeight,
x: e.offsetX / this.$refs.mask.clientWidth, x: e.offsetX / this.$refs.mask.clientWidth,
y: e.offsetY / this.$refs.mask.clientHeight, y: e.offsetY / this.$refs.mask.clientHeight,
w: 0, w: 0,
h: 0 h: 0
} }]
}) })
}, },
startSelectionRect (e) { startSelectionRect (e) {
@ -563,28 +642,30 @@ export default {
return return
} }
if (this.newArea) { const drawArea = this.newAreas[0]
const dx = e.offsetX / this.$refs.mask.clientWidth - this.newArea.initialX
const dy = e.offsetY / this.$refs.mask.clientHeight - this.newArea.initialY if (drawArea?.initialX !== undefined) {
const dx = e.offsetX / this.$refs.mask.clientWidth - drawArea.initialX
const dy = e.offsetY / this.$refs.mask.clientHeight - drawArea.initialY
if (dx > 0) { if (dx > 0) {
this.newArea.x = this.newArea.initialX drawArea.x = drawArea.initialX
} else { } else {
this.newArea.x = e.offsetX / this.$refs.mask.clientWidth drawArea.x = e.offsetX / this.$refs.mask.clientWidth
} }
if (dy > 0) { if (dy > 0) {
this.newArea.y = this.newArea.initialY drawArea.y = drawArea.initialY
} else { } else {
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight drawArea.y = e.offsetY / this.$refs.mask.clientHeight
} }
if ((this.drawField?.type || this.drawFieldType) === 'cells') { if ((this.drawField?.type || this.drawCustomField?.type || this.drawFieldType) === 'cells') {
this.newArea.cell_w = this.newArea.h * (this.$refs.mask.clientHeight / this.$refs.mask.clientWidth) drawArea.cell_w = drawArea.h * (this.$refs.mask.clientHeight / this.$refs.mask.clientWidth)
} }
this.newArea.w = Math.abs(dx) drawArea.w = Math.abs(dx)
this.newArea.h = Math.abs(dy) drawArea.h = Math.abs(dy)
} }
}, },
onPointerup (e) { onPointerup (e) {
@ -601,29 +682,33 @@ export default {
}) })
this.selectionRect = null this.selectionRect = null
} else if (this.newArea) { } else {
const drawArea = this.newAreas[0]
if (drawArea?.initialX !== undefined) {
const area = { const area = {
x: this.newArea.x, x: drawArea.x,
y: this.newArea.y, y: drawArea.y,
w: this.newArea.w, w: drawArea.w,
h: this.newArea.h, h: drawArea.h,
page: this.number page: this.number
} }
if ('cell_w' in this.newArea) { if ('cell_w' in drawArea) {
area.cell_w = this.newArea.cell_w area.cell_w = drawArea.cell_w
} }
const dx = Math.abs(e.offsetX - this.$refs.mask.clientWidth * this.newArea.initialX) const dx = Math.abs(e.offsetX - this.$refs.mask.clientWidth * drawArea.initialX)
const dy = Math.abs(e.offsetY - this.$refs.mask.clientHeight * this.newArea.initialY) const dy = Math.abs(e.offsetY - this.$refs.mask.clientHeight * drawArea.initialY)
const isTooSmall = dx < 8 && dy < 8 const isTooSmall = dx < 8 && dy < 8
this.$emit('draw', { area, isTooSmall }) this.$emit('draw', { area, isTooSmall })
} }
}
this.showMask = false this.showMask = false
this.newArea = null this.newAreas = []
}, },
rectsOverlap (r1, r2) { rectsOverlap (r1, r2) {
return !( return !(

@ -231,7 +231,10 @@
</span> </span>
</label> </label>
</li> </li>
<li class="mt-1"> <li
v-if="withCondition"
class="mt-1"
>
<label <label
class="label-text cursor-pointer text-center w-full flex items-center" class="label-text cursor-pointer text-center w-full flex items-center"
@click="$emit('click-condition')" @click="$emit('click-condition')"
@ -244,12 +247,29 @@
</span> </span>
</label> </label>
</li> </li>
<hr
v-if="withCustomFields"
class="pb-0.5 mt-0.5"
>
<li v-if="withCustomFields">
<a
href="#"
class="text-sm py-1 px-2"
@click.prevent="$emit('add-custom-field', field)"
>
<IconForms
:width="20"
:stroke-width="1.6"
/>
{{ t('save_as_custom_field') }}
</a>
</li>
</ul> </ul>
</span> </span>
</template> </template>
<script> <script>
import { IconMathFunction, IconSettings, IconCircleCheck, IconInfoCircle, IconBrandStripe, IconInnerShadowTop, IconRouteAltLeft } from '@tabler/icons-vue' import { IconMathFunction, IconSettings, IconCircleCheck, IconInfoCircle, IconBrandStripe, IconInnerShadowTop, IconRouteAltLeft, IconForms } from '@tabler/icons-vue'
import { ref } from 'vue' import { ref } from 'vue'
const isConnected = ref(false) const isConnected = ref(false)
@ -261,6 +281,7 @@ export default {
IconCircleCheck, IconCircleCheck,
IconRouteAltLeft, IconRouteAltLeft,
IconInfoCircle, IconInfoCircle,
IconForms,
IconMathFunction, IconMathFunction,
IconInnerShadowTop, IconInnerShadowTop,
IconBrandStripe IconBrandStripe
@ -270,9 +291,19 @@ export default {
field: { field: {
type: Object, type: Object,
required: true required: true
},
withCustomFields: {
type: Boolean,
required: false,
default: false
},
withCondition: {
type: Boolean,
required: false,
default: true
} }
}, },
emits: ['click-condition', 'click-description', 'click-formula'], emits: ['click-condition', 'click-description', 'click-formula', 'add-custom-field'],
data () { data () {
return { return {
isLoading: false isLoading: false

@ -54,6 +54,7 @@ class AccountConfig < ApplicationRecord
WITH_FIELD_LABELS_KEY = 'with_field_labels' WITH_FIELD_LABELS_KEY = 'with_field_labels'
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key' COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format' DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format'
TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields'
POLICY_LINKS_KEY = 'policy_links' POLICY_LINKS_KEY = 'policy_links'
DEFAULT_VALUES = { DEFAULT_VALUES = {

@ -6,4 +6,4 @@
<%= button_to nil, user_configs_path, method: :post, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'hidden', id: 'start_tour_button' %> <%= button_to nil, user_configs_path, method: :post, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'hidden', id: 'start_tour_button' %>
<% end %> <% end %>
<% end %> <% end %>
<template-builder class="grid" data-template="<%= @template_data %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-fields-detection="true" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-locale="<%= I18n.locale %>" data-show-tour-start-form="<%= @show_tour_start_form %>"></template-builder> <template-builder class="grid" data-template="<%= @template_data %>" data-custom-fields="<%= (current_account.account_configs.find_or_initialize_by(key: AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY).value || []).to_json %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-fields-detection="true" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-locale="<%= I18n.locale %>" data-show-tour-start-form="<%= @show_tour_start_form %>"></template-builder>

@ -53,6 +53,7 @@ Rails.application.routes.draw do
resources :verify_pdf_signature, only: %i[create] resources :verify_pdf_signature, only: %i[create]
resource :mfa_setup, only: %i[show new edit create destroy], controller: 'mfa_setup' resource :mfa_setup, only: %i[show new edit create destroy], controller: 'mfa_setup'
resources :account_configs, only: %i[create destroy] resources :account_configs, only: %i[create destroy]
resources :account_custom_fields, only: %i[create]
resources :user_configs, only: %i[create] resources :user_configs, only: %i[create]
resources :encrypted_user_configs, only: %i[destroy] resources :encrypted_user_configs, only: %i[destroy]
resources :timestamp_server, only: %i[create] resources :timestamp_server, only: %i[create]

Loading…
Cancel
Save