mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 07:14:43 +00:00
custom fields
This commit is contained in:
@@ -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, {
|
||||
template: reactive(JSON.parse(this.dataset.template)),
|
||||
customFields: reactive(JSON.parse(this.dataset.customFields || '[]')),
|
||||
backgroundColor: '#faf7f5',
|
||||
locale: this.dataset.locale,
|
||||
withPhone: this.dataset.withPhone === 'true',
|
||||
@@ -164,6 +165,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
||||
withFieldsDetection: this.dataset.withFieldsDetection === 'true',
|
||||
editable: this.dataset.editable !== 'false',
|
||||
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||
withCustomFields: true,
|
||||
withPayment: this.dataset.withPayment === 'true',
|
||||
isPaymentConnected: this.dataset.isPaymentConnected === 'true',
|
||||
withFormula: this.dataset.withFormula === 'true',
|
||||
|
||||
@@ -145,7 +145,9 @@
|
||||
@click-formula="isShowFormulaModal = true"
|
||||
@click-font="isShowFontModal = true"
|
||||
@click-description="isShowDescriptionModal = true"
|
||||
@add-custom-field="$emit('add-custom-field')"
|
||||
@click-condition="isShowConditionsModal = true"
|
||||
@save="save"
|
||||
@scroll-to="[selectedAreasRef.value = [$event], $emit('scroll-to', $event)]"
|
||||
/>
|
||||
</ul>
|
||||
@@ -471,7 +473,7 @@ export default {
|
||||
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 () {
|
||||
return {
|
||||
isShowFormulaModal: false,
|
||||
|
||||
@@ -20,8 +20,9 @@
|
||||
/>
|
||||
<DragPlaceholder
|
||||
ref="dragPlaceholder"
|
||||
:field="fieldsDragFieldRef.value || toRaw(dragField)"
|
||||
:field="customDragFieldRef.value || fieldsDragFieldRef.value || toRaw(dragField)"
|
||||
:is-field="template.fields.includes(fieldsDragFieldRef.value)"
|
||||
:is-custom="!!customDragFieldRef.value"
|
||||
:is-default="defaultFields.includes(toRaw(dragField))"
|
||||
:is-required="defaultRequiredFields.includes(toRaw(dragField))"
|
||||
/>
|
||||
@@ -362,7 +363,7 @@
|
||||
:is-drag="!!dragField"
|
||||
:input-mode="inputMode"
|
||||
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
||||
:allow-draw="!onlyDefinedFields || drawField"
|
||||
:allow-draw="!onlyDefinedFields || drawField || drawCustomField"
|
||||
:with-signature-id="withSignatureId"
|
||||
:with-prefillable="withPrefillable"
|
||||
:data-document-uuid="document.uuid"
|
||||
@@ -371,14 +372,16 @@
|
||||
:with-field-placeholder="withFieldPlaceholder"
|
||||
:draw-field="drawField"
|
||||
:draw-field-type="drawFieldType"
|
||||
:draw-custom-field="drawCustomField"
|
||||
:editable="editable"
|
||||
:base-url="baseUrl"
|
||||
:with-fields-detection="withFieldsDetection"
|
||||
@draw="[onDraw($event), withSelectedFieldType ? '' : drawFieldType = '', showDrawField = false]"
|
||||
@draw="[onDraw($event), withSelectedFieldType ? '' : drawFieldType = '', drawCustomField = null, showDrawField = false]"
|
||||
@drop-field="onDropfield"
|
||||
@remove-area="removeArea"
|
||||
@paste-field="pasteField"
|
||||
@copy-field="copyField"
|
||||
@add-custom-field="addCustomField"
|
||||
@copy-selected-areas="copySelectedAreas"
|
||||
@delete-selected-areas="deleteSelectedAreas"
|
||||
@align-selected-areas="alignSelectedAreas"
|
||||
@@ -436,15 +439,15 @@
|
||||
v-if="withFieldsList && !isMobile"
|
||||
id="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
|
||||
v-if="showDrawField || drawField"
|
||||
v-if="showDrawField || drawField || drawCustomField"
|
||||
class="sticky inset-0 h-full z-20"
|
||||
:style="{ backgroundColor }"
|
||||
>
|
||||
<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') }}
|
||||
</p>
|
||||
<p v-else>
|
||||
@@ -458,10 +461,10 @@
|
||||
{{ t('cancel') }}
|
||||
</button>
|
||||
<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="#"
|
||||
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') }}
|
||||
</a>
|
||||
@@ -477,6 +480,8 @@
|
||||
:with-help="withHelp"
|
||||
:default-submitters="defaultSubmitters"
|
||||
:draw-field-type="drawFieldType"
|
||||
:custom-fields="customFields"
|
||||
:with-custom-fields="withCustomFields"
|
||||
:with-fields-search="withFieldsSearch"
|
||||
:default-fields="[...defaultRequiredFields, ...defaultFields]"
|
||||
:template="template"
|
||||
@@ -493,6 +498,7 @@
|
||||
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
||||
@select-submitter="selectedSubmitter = $event"
|
||||
@set-draw-type="[drawFieldType = $event, showDrawField = true]"
|
||||
@set-draw-custom-field="[drawCustomField = $event, showDrawField = true]"
|
||||
@set-drag="dragField = $event"
|
||||
@set-drag-placeholder="$refs.dragPlaceholder.dragPlaceholder = $event"
|
||||
@change-submitter="selectedSubmitter = $event"
|
||||
@@ -632,10 +638,12 @@ export default {
|
||||
isPaymentConnected: this.isPaymentConnected,
|
||||
withFormula: this.withFormula,
|
||||
withConditions: this.withConditions,
|
||||
withCustomFields: this.withCustomFields,
|
||||
isInlineSize: this.isInlineSize,
|
||||
defaultDrawFieldType: this.defaultDrawFieldType,
|
||||
selectedAreasRef: computed(() => this.selectedAreasRef),
|
||||
fieldsDragFieldRef: computed(() => this.fieldsDragFieldRef),
|
||||
customDragFieldRef: computed(() => this.customDragFieldRef),
|
||||
isSelectModeRef: computed(() => this.isSelectModeRef),
|
||||
isCmdKeyRef: computed(() => this.isCmdKeyRef)
|
||||
}
|
||||
@@ -705,6 +713,16 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withCustomFields: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
customFields: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
withAddPageButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -902,6 +920,7 @@ export default {
|
||||
pendingFieldAttachmentUuids: [],
|
||||
drawField: null,
|
||||
drawFieldType: null,
|
||||
drawCustomField: null,
|
||||
drawOption: null,
|
||||
dragField: null,
|
||||
isDragFile: false
|
||||
@@ -912,6 +931,7 @@ export default {
|
||||
isSelectModeRef: () => ref(false),
|
||||
isCmdKeyRef: () => ref(false),
|
||||
fieldsDragFieldRef: () => ref(),
|
||||
customDragFieldRef: () => ref(),
|
||||
selectedAreasRef: () => ref([]),
|
||||
language () {
|
||||
return this.locale.split('-')[0].toLowerCase()
|
||||
@@ -1062,6 +1082,30 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
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 () {
|
||||
this.isSelectModeRef.value = !this.isSelectModeRef.value
|
||||
|
||||
@@ -1514,6 +1558,7 @@ export default {
|
||||
clearDrawField () {
|
||||
this.drawField = null
|
||||
this.drawOption = null
|
||||
this.drawCustomField = null
|
||||
this.showDrawField = false
|
||||
|
||||
if (!this.withSelectedFieldType) {
|
||||
@@ -1951,6 +1996,10 @@ export default {
|
||||
}
|
||||
},
|
||||
onDraw ({ area, isTooSmall }) {
|
||||
if (this.drawCustomField) {
|
||||
return this.onDrawCustomField(area)
|
||||
}
|
||||
|
||||
if (this.drawField) {
|
||||
if (this.drawOption) {
|
||||
const areaWithoutOption = this.drawField.areas?.find((a) => !a.option_uuid)
|
||||
@@ -2061,6 +2110,10 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.customDragFieldRef.value) {
|
||||
return this.dropCustomField(area)
|
||||
}
|
||||
|
||||
const field = this.fieldsDragFieldRef.value || {
|
||||
name: '',
|
||||
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) {
|
||||
const fieldType = field.type || 'text'
|
||||
|
||||
|
||||
@@ -6,18 +6,24 @@
|
||||
<span
|
||||
ref="contenteditable"
|
||||
dir="auto"
|
||||
:contenteditable="editable"
|
||||
style="min-width: 2px"
|
||||
:class="[iconInline ? 'inline' : 'block', hideIcon ? 'focus:block' : '']"
|
||||
class="peer outline-none"
|
||||
:contenteditable="editable && (!editableOnButton || isEditable)"
|
||||
:data-placeholder="placeholder"
|
||||
:data-empty="isEmpty"
|
||||
: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"
|
||||
@keydown.enter.prevent="blurContenteditable"
|
||||
@input="updateInputValue"
|
||||
@cut="updateInputValue"
|
||||
@focus="$emit('focus', $event)"
|
||||
@blur="onBlur"
|
||||
@click="editable && (!editableOnButton || isEditable) ? '' : $emit('click-contenteditable')"
|
||||
>
|
||||
{{ value }}
|
||||
</span>
|
||||
<span
|
||||
v-if="withButton"
|
||||
class="relative inline"
|
||||
: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 }"
|
||||
:width="iconWidth + 4"
|
||||
:stroke-width="iconStrokeWidth"
|
||||
@click="[focusContenteditable(), selectOnEditClick && selectContent()]"
|
||||
@click="clickEdit"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
@@ -49,6 +55,16 @@ export default {
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
withButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
iconInline: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -74,6 +90,16 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
editableOnButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
minWidth: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '2px'
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -85,21 +111,34 @@ export default {
|
||||
default: 2
|
||||
}
|
||||
},
|
||||
emits: ['update:model-value', 'focus', 'blur'],
|
||||
emits: ['update:model-value', 'focus', 'blur', 'click-contenteditable'],
|
||||
data () {
|
||||
return {
|
||||
isEditable: false,
|
||||
inputValue: '',
|
||||
value: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isEmpty () {
|
||||
return !this.inputValue.replace(/\u200B/g, '').replace(/\u00A0/g, ' ').trim()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modelValue: {
|
||||
handler (value) {
|
||||
this.value = value
|
||||
this.value = value || ''
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.updateInputValue()
|
||||
},
|
||||
methods: {
|
||||
updateInputValue () {
|
||||
this.inputValue = this.$refs.contenteditable?.textContent || ''
|
||||
},
|
||||
onPaste (e) {
|
||||
const text = (e.clipboardData || window.clipboardData).getData('text/plain')
|
||||
|
||||
@@ -110,6 +149,20 @@ export default {
|
||||
selection.getRangeAt(0).insertNode(document.createTextNode(text))
|
||||
selection.collapseToEnd()
|
||||
}
|
||||
|
||||
this.updateInputValue()
|
||||
},
|
||||
clickEdit (e) {
|
||||
this.focusContenteditable()
|
||||
|
||||
if (this.selectOnEditClick) {
|
||||
this.selectContent()
|
||||
}
|
||||
},
|
||||
setText (text) {
|
||||
this.$refs.contenteditable.innerText = text
|
||||
|
||||
this.updateInputValue()
|
||||
},
|
||||
selectContent () {
|
||||
const el = this.$refs.contenteditable
|
||||
@@ -129,10 +182,18 @@ export default {
|
||||
this.value = this.$refs.contenteditable.innerText.trim() || this.modelValue
|
||||
this.$emit('update:model-value', this.value)
|
||||
this.$emit('blur', e)
|
||||
|
||||
this.isEditable = false
|
||||
}, 1)
|
||||
},
|
||||
focusContenteditable () {
|
||||
this.$refs.contenteditable.focus()
|
||||
this.isEditable = true
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$refs.contenteditable.focus()
|
||||
|
||||
this.updateInputValue()
|
||||
})
|
||||
},
|
||||
blurContenteditable () {
|
||||
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"
|
||||
:draw-field="drawField"
|
||||
:draw-field-type="drawFieldType"
|
||||
:draw-custom-field="drawCustomField"
|
||||
:selected-submitter="selectedSubmitter"
|
||||
:total-pages="sortedPreviewImages.length"
|
||||
:image="image"
|
||||
@@ -28,6 +29,7 @@
|
||||
@remove-area="$emit('remove-area', $event)"
|
||||
@copy-field="$emit('copy-field', $event)"
|
||||
@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')"
|
||||
@delete-selected-areas="$emit('delete-selected-areas')"
|
||||
@align-selected-areas="$emit('align-selected-areas', $event)"
|
||||
@@ -115,6 +117,11 @@ export default {
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
drawCustomField: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
baseUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
@@ -131,7 +138,7 @@ export default {
|
||||
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 () {
|
||||
return {
|
||||
pageRefs: []
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
class="fixed z-20 pointer-events-none"
|
||||
: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
|
||||
v-else-if="dragPlaceholder && (isDefault || isRequired) && !isMask && field"
|
||||
ref="dragPlaceholder"
|
||||
@@ -57,6 +64,7 @@
|
||||
|
||||
<script>
|
||||
import Field from './field'
|
||||
import CustomField from './custom_field'
|
||||
import IconDrag from './icon_drag'
|
||||
import FieldType from './field_type'
|
||||
|
||||
@@ -64,6 +72,7 @@ export default {
|
||||
name: 'DragPlaceholder',
|
||||
components: {
|
||||
Field,
|
||||
CustomField,
|
||||
IconDrag
|
||||
},
|
||||
inject: ['t', 'backgroundColor'],
|
||||
@@ -87,6 +96,11 @@ export default {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
isCustom: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="list-field group mb-2"
|
||||
class="list-field group"
|
||||
>
|
||||
<div
|
||||
class="border border-base-300 rounded relative group fields-list-item"
|
||||
@@ -18,7 +18,7 @@
|
||||
:button-width="20"
|
||||
:menu-classes="'mt-1.5'"
|
||||
:menu-style="{ backgroundColor: dropdownBgColor }"
|
||||
@update:model-value="[maybeUpdateOptions(), save()]"
|
||||
@update:model-value="[maybeUpdateOptions(), $emit('save')]"
|
||||
@click="scrollToFirstArea"
|
||||
/>
|
||||
<Contenteditable
|
||||
@@ -92,9 +92,12 @@
|
||||
<PaymentSettings
|
||||
v-if="field.type === 'payment'"
|
||||
:field="field"
|
||||
:with-custom-fields="withCustomFields"
|
||||
@click-condition="isShowConditionsModal = true"
|
||||
@click-description="isShowDescriptionModal = true"
|
||||
@add-custom-field="$emit('add-custom-field', $event)"
|
||||
@click-formula="isShowFormulaModal = true"
|
||||
@save="$emit('save')"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
@@ -128,12 +131,15 @@
|
||||
:with-signature-id="withSignatureId"
|
||||
:with-prefillable="withPrefillable"
|
||||
:background-color="dropdownBgColor"
|
||||
:with-custom-fields="withCustomFields"
|
||||
@click-formula="isShowFormulaModal = true"
|
||||
@click-font="isShowFontModal = true"
|
||||
@click-description="isShowDescriptionModal = true"
|
||||
@click-condition="isShowConditionsModal = true"
|
||||
@set-draw="$emit('set-draw', $event)"
|
||||
@add-custom-field="$emit('add-custom-field', $event)"
|
||||
@remove-area="removeArea"
|
||||
@save="$emit('save')"
|
||||
@scroll-to="$emit('scroll-to', $event)"
|
||||
/>
|
||||
</ul>
|
||||
@@ -186,7 +192,7 @@
|
||||
required
|
||||
:placeholder="`${t('option')} ${index + 1}`"
|
||||
@keydown.enter="option.value ? addOptionAt(index + 1) : null"
|
||||
@blur="save"
|
||||
@blur="$emit('save')"
|
||||
@paste="onOptionPaste($event, index)"
|
||||
>
|
||||
<button
|
||||
@@ -211,7 +217,7 @@
|
||||
dir="auto"
|
||||
@keydown.enter="option.value ? addOptionAt(index + 1) : null"
|
||||
@focus="maybeFocusOnOptionArea(option)"
|
||||
@blur="save"
|
||||
@blur="$emit('save')"
|
||||
@paste="onOptionPaste($event, index)"
|
||||
>
|
||||
<button
|
||||
@@ -334,7 +340,7 @@ export default {
|
||||
IconMathFunction,
|
||||
FieldType
|
||||
},
|
||||
inject: ['template', 'save', 'backgroundColor', 'selectedAreasRef', 't', 'locale'],
|
||||
inject: ['template', 'backgroundColor', 'selectedAreasRef', 't', 'locale'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
@@ -345,6 +351,11 @@ export default {
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
withCustomFields: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withPrefillable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -366,12 +377,11 @@ export default {
|
||||
default: true
|
||||
}
|
||||
},
|
||||
emits: ['set-draw', 'remove', 'scroll-to'],
|
||||
emits: ['set-draw', 'remove', 'scroll-to', 'save', 'add-custom-field'],
|
||||
data () {
|
||||
return {
|
||||
isExpandOptions: false,
|
||||
isNameFocus: false,
|
||||
showPaymentModal: false,
|
||||
isShowFormulaModal: false,
|
||||
isShowFontModal: false,
|
||||
isShowConditionsModal: false,
|
||||
@@ -420,7 +430,7 @@ export default {
|
||||
removeArea (area) {
|
||||
this.field.areas.splice(this.field.areas.indexOf(area), 1)
|
||||
|
||||
this.save()
|
||||
this.$emit('save')
|
||||
},
|
||||
buildDefaultName (field, fields) {
|
||||
if (field.type === 'payment' && field.preferences?.price && !field.preferences?.formula) {
|
||||
@@ -489,7 +499,7 @@ export default {
|
||||
inputs[index + newOptions.length]?.focus()
|
||||
})
|
||||
|
||||
this.save()
|
||||
this.$emit('save')
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -506,7 +516,7 @@ export default {
|
||||
inputs[insertAt]?.focus()
|
||||
})
|
||||
|
||||
this.save()
|
||||
this.$emit('save')
|
||||
},
|
||||
removeOption (option) {
|
||||
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.save()
|
||||
this.$emit('save')
|
||||
},
|
||||
maybeUpdateOptions () {
|
||||
delete this.field.default_value
|
||||
@@ -559,7 +569,7 @@ export default {
|
||||
|
||||
this.isNameFocus = false
|
||||
|
||||
this.save()
|
||||
this.$emit('save')
|
||||
},
|
||||
onOptionDragstart (event, option) {
|
||||
this.optionDragRef = option
|
||||
@@ -620,7 +630,7 @@ export default {
|
||||
if (newOrder.length === this.field.options.length) {
|
||||
this.field.options.splice(0, this.field.options.length, ...newOrder)
|
||||
|
||||
this.save()
|
||||
this.$emit('save')
|
||||
}
|
||||
|
||||
this.optionDragRef = null
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<select
|
||||
:placeholder="t('method')"
|
||||
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
|
||||
v-for="method in ['QeS', 'AeS']"
|
||||
@@ -33,7 +33,7 @@
|
||||
>
|
||||
<select
|
||||
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
|
||||
v-for="value in ['left', 'right', field.type === 'cells' ? null : 'center'].filter(Boolean)"
|
||||
@@ -61,7 +61,7 @@
|
||||
:placeholder="t('default_value')"
|
||||
dir="auto"
|
||||
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
|
||||
value=""
|
||||
@@ -97,7 +97,7 @@
|
||||
dir="auto"
|
||||
:type="field.type"
|
||||
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
|
||||
@blur="save"
|
||||
@blur="$emit('save')"
|
||||
>
|
||||
<label
|
||||
v-if="field.default_value"
|
||||
@@ -159,7 +159,7 @@
|
||||
:value="lengthValidation.min"
|
||||
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
||||
@input="field.validation.pattern = `.{${$event.target.value || 0},${lengthValidation.max || ''}}`"
|
||||
@blur="save"
|
||||
@blur="$emit('save')"
|
||||
>
|
||||
<label
|
||||
v-if="lengthValidation.min"
|
||||
@@ -178,7 +178,7 @@
|
||||
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
||||
:value="lengthValidation.max"
|
||||
@input="field.validation.pattern = `.{${lengthValidation.min},${$event.target.value || ''}}`"
|
||||
@blur="save"
|
||||
@blur="$emit('save')"
|
||||
>
|
||||
<label
|
||||
v-if="lengthValidation.max"
|
||||
@@ -203,7 +203,7 @@
|
||||
:value="field.validation?.min"
|
||||
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]"
|
||||
@blur="save"
|
||||
@blur="$emit('save')"
|
||||
>
|
||||
<label
|
||||
v-if="field.validation?.min"
|
||||
@@ -222,7 +222,7 @@
|
||||
class="input input-bordered w-full input-xs h-7 !outline-0 bg-transparent"
|
||||
:value="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
|
||||
v-if="field.validation?.max"
|
||||
@@ -242,7 +242,7 @@
|
||||
<select
|
||||
:placeholder="t('format')"
|
||||
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
|
||||
v-for="format in numberFormats"
|
||||
@@ -272,7 +272,7 @@
|
||||
:placeholder="t('regexp_validation')"
|
||||
dir="auto"
|
||||
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
|
||||
@blur="save"
|
||||
@blur="$emit('save')"
|
||||
>
|
||||
<label
|
||||
v-if="field.validation.pattern"
|
||||
@@ -293,7 +293,7 @@
|
||||
:placeholder="t('error_message')"
|
||||
dir="auto"
|
||||
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
|
||||
@blur="save"
|
||||
@blur="$emit('save')"
|
||||
>
|
||||
<label
|
||||
v-if="field.validation.message"
|
||||
@@ -313,7 +313,7 @@
|
||||
v-model="field.preferences.format"
|
||||
:placeholder="t('format')"
|
||||
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
|
||||
v-for="format in dateFormats"
|
||||
@@ -339,7 +339,7 @@
|
||||
<select
|
||||
:placeholder="t('format')"
|
||||
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
|
||||
value="any"
|
||||
@@ -374,7 +374,7 @@
|
||||
type="checkbox"
|
||||
:disabled="!editable || (defaultField && [true, false].includes(defaultField.required))"
|
||||
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>
|
||||
</label>
|
||||
@@ -389,7 +389,7 @@
|
||||
type="checkbox"
|
||||
:disabled="!editable || (defaultField && [true, false].includes(defaultField.required))"
|
||||
class="toggle toggle-xs"
|
||||
@update:model-value="save"
|
||||
@update:model-value="$emit('save')"
|
||||
>
|
||||
<span class="label-text">{{ t('required') }}</span>
|
||||
</label>
|
||||
@@ -403,7 +403,7 @@
|
||||
:checked="field.preferences?.with_logo != false"
|
||||
type="checkbox"
|
||||
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>
|
||||
</label>
|
||||
@@ -417,7 +417,7 @@
|
||||
v-model="field.default_value"
|
||||
type="checkbox"
|
||||
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>
|
||||
</label>
|
||||
@@ -431,7 +431,7 @@
|
||||
v-model="field.readonly"
|
||||
type="checkbox"
|
||||
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>
|
||||
</label>
|
||||
@@ -446,7 +446,7 @@
|
||||
type="checkbox"
|
||||
class="toggle toggle-xs"
|
||||
: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>
|
||||
</label>
|
||||
@@ -461,7 +461,7 @@
|
||||
type="checkbox"
|
||||
:disabled="!editable || (defaultField && [true, false].includes(defaultField.prefillable))"
|
||||
class="toggle toggle-xs"
|
||||
@update:model-value="save"
|
||||
@update:model-value="$emit('save')"
|
||||
>
|
||||
<span class="label-text">{{ t('prefillable') }}</span>
|
||||
</label>
|
||||
@@ -499,7 +499,7 @@
|
||||
</label>
|
||||
</li>
|
||||
<li
|
||||
v-if="field.type != 'stamp' && field.type != 'heading'"
|
||||
v-if="withCondition && field.type != 'stamp' && field.type != 'heading'"
|
||||
>
|
||||
<label
|
||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||
@@ -526,7 +526,10 @@
|
||||
</span>
|
||||
</label>
|
||||
</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">
|
||||
<li
|
||||
v-for="(area, index) in sortedAreas"
|
||||
@@ -564,7 +567,7 @@
|
||||
</a>
|
||||
</li>
|
||||
</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
|
||||
href="#"
|
||||
class="text-sm py-1 px-2"
|
||||
@@ -577,10 +580,23 @@
|
||||
{{ t('copy_to_all_pages') }}
|
||||
</a>
|
||||
</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>
|
||||
|
||||
<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 {
|
||||
name: 'FieldSettings',
|
||||
@@ -589,17 +605,33 @@ export default {
|
||||
IconInfoCircle,
|
||||
IconMathFunction,
|
||||
IconRouteAltLeft,
|
||||
IconForms,
|
||||
IconCopy,
|
||||
IconNewSection,
|
||||
IconTypography,
|
||||
IconX
|
||||
},
|
||||
inject: ['template', 'save', 't'],
|
||||
inject: ['template', 't'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
withCondition: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
withCustomFields: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withCopyToAllPages: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
withSignatureId: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -636,7 +668,7 @@ export default {
|
||||
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 () {
|
||||
return {
|
||||
selectedValidation: ''
|
||||
@@ -730,7 +762,7 @@ export default {
|
||||
delete this.field.validation
|
||||
}
|
||||
|
||||
this.save()
|
||||
this.$emit('save')
|
||||
},
|
||||
copyToAllPages (field) {
|
||||
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.save()
|
||||
this.$emit('save')
|
||||
},
|
||||
formatNumber (number, format) {
|
||||
if (format === 'comma') {
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
</div>
|
||||
<div
|
||||
ref="fields"
|
||||
class="fields mb-1 mt-2"
|
||||
class="fields mt-2"
|
||||
:class="{ 'mb-1': !withCustomFields || !customFields.length }"
|
||||
@dragover.prevent="onFieldDragover"
|
||||
@drop="fieldsDragFieldRef.value ? reorderFields() : null"
|
||||
>
|
||||
@@ -30,7 +31,11 @@
|
||||
:with-prefillable="withPrefillable"
|
||||
:default-field="defaultFieldsIndex[field.name]"
|
||||
:draggable="editable"
|
||||
:with-custom-fields="withCustomFields"
|
||||
class="mb-1.5"
|
||||
@add-custom-field="addCustomField"
|
||||
@dragstart="[fieldsDragFieldRef.value = field, removeDragOverlay($event), setDragPlaceholder($event)]"
|
||||
@save="save"
|
||||
@dragend="[fieldsDragFieldRef.value = null, $emit('set-drag-placeholder', null)]"
|
||||
@remove="removeField"
|
||||
@scroll-to="$emit('scroll-to-area', $event)"
|
||||
@@ -105,7 +110,92 @@
|
||||
</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"
|
||||
class="grid grid-cols-3 gap-1 pb-2 fields-grid"
|
||||
>
|
||||
@@ -266,15 +356,18 @@
|
||||
|
||||
<script>
|
||||
import Field from './field'
|
||||
import CustomField from './custom_field'
|
||||
import FieldType from './field_type'
|
||||
import FieldSubmitter from './field_submitter'
|
||||
import { IconLock, IconCirclePlus, IconInnerShadowTop, IconSparkles } from '@tabler/icons-vue'
|
||||
import IconDrag from './icon_drag'
|
||||
import { v4 } from 'uuid'
|
||||
|
||||
export default {
|
||||
name: 'TemplateFields',
|
||||
components: {
|
||||
Field,
|
||||
CustomField,
|
||||
FieldType,
|
||||
IconCirclePlus,
|
||||
IconSparkles,
|
||||
@@ -283,12 +376,22 @@ export default {
|
||||
IconDrag,
|
||||
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: {
|
||||
fields: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
customFields: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
withCustomFields: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withFieldsSearch: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -372,12 +475,15 @@ export default {
|
||||
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 () {
|
||||
return {
|
||||
fieldPagesLoaded: null,
|
||||
analyzingProgress: 0,
|
||||
defaultFieldsSearch: ''
|
||||
newCustomField: null,
|
||||
showCustomTab: false,
|
||||
defaultFieldsSearch: '',
|
||||
customFieldsSearch: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -430,6 +536,23 @@ export default {
|
||||
} else {
|
||||
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: {
|
||||
@@ -440,6 +563,61 @@ export default {
|
||||
|
||||
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 () {
|
||||
const fields = []
|
||||
|
||||
@@ -617,6 +795,47 @@ export default {
|
||||
if (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 = {
|
||||
default: 'Default',
|
||||
save_as_custom_field: 'Save as Custom Field',
|
||||
kba: 'KBA',
|
||||
analyzing_: 'Analyzing...',
|
||||
download: 'Download',
|
||||
downloading_: 'Downloading...',
|
||||
view: 'View',
|
||||
autodetect_fields: 'Autodetect fields',
|
||||
autodetect_fields: 'Autodetect Fields',
|
||||
payment_link: 'Payment link',
|
||||
strikeout: 'Strikeout',
|
||||
draw_strikethrough_the_document: 'Draw strikethrough the document',
|
||||
@@ -200,6 +202,8 @@ const en = {
|
||||
}
|
||||
|
||||
const es = {
|
||||
default: 'Predeterminado',
|
||||
save_as_custom_field: 'Guardar como personalizado',
|
||||
kba: 'KBA',
|
||||
autodetect_fields: 'Autodetectar campos',
|
||||
analyzing_: 'Analizando...',
|
||||
@@ -401,6 +405,8 @@ const es = {
|
||||
}
|
||||
|
||||
const it = {
|
||||
default: 'Predefinito',
|
||||
save_as_custom_field: 'Salva come personalizzato',
|
||||
kba: 'KBA',
|
||||
autodetect_fields: 'Rileva campi',
|
||||
analyzing_: 'Analisi...',
|
||||
@@ -602,6 +608,8 @@ const it = {
|
||||
}
|
||||
|
||||
const pt = {
|
||||
default: 'Padrão',
|
||||
save_as_custom_field: 'Salvar como personalizado',
|
||||
kba: 'KBA',
|
||||
autodetect_fields: 'Detectar campos',
|
||||
analyzing_: 'Analisando...',
|
||||
@@ -803,6 +811,8 @@ const pt = {
|
||||
}
|
||||
|
||||
const fr = {
|
||||
default: 'Par défaut',
|
||||
save_as_custom_field: 'Enregistrer comme personnalisé',
|
||||
kba: 'KBA',
|
||||
autodetect_fields: 'Détecter les champs',
|
||||
analyzing_: 'Analyse...',
|
||||
@@ -1004,6 +1014,8 @@ const fr = {
|
||||
}
|
||||
|
||||
const de = {
|
||||
default: 'Standard',
|
||||
save_as_custom_field: 'Als benutzerdefiniert speichern',
|
||||
kba: 'KBA',
|
||||
autodetect_fields: 'Felder erkennen',
|
||||
analyzing_: 'Analysiere...',
|
||||
@@ -1205,6 +1217,8 @@ const de = {
|
||||
}
|
||||
|
||||
const nl = {
|
||||
default: 'Standaard',
|
||||
save_as_custom_field: 'Opslaan als aangepast',
|
||||
kba: 'KBA',
|
||||
autodetect_fields: 'Velden detecteren',
|
||||
analyzing_: 'Analyseren...',
|
||||
|
||||
@@ -52,15 +52,22 @@
|
||||
@stop-resize="resizeDirection = null"
|
||||
@remove="$emit('remove-area', item.area)"
|
||||
@scroll-to="$emit('scroll-to', $event)"
|
||||
@add-custom-field="$emit('add-custom-field', $event)"
|
||||
@contextmenu="openAreaContextMenu($event, item.area, item.field)"
|
||||
/>
|
||||
<FieldArea
|
||||
v-if="newArea"
|
||||
v-for="(area, index) in newAreas"
|
||||
:key="index"
|
||||
:is-draw="true"
|
||||
:page-width="width"
|
||||
:page-height="height"
|
||||
:field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || dragFieldPlaceholder?.type || defaultFieldType }"
|
||||
:area="newArea"
|
||||
:field="{ submitter_uuid: selectedSubmitter.uuid, type: newAreaFieldType }"
|
||||
:area="area"
|
||||
/>
|
||||
<div
|
||||
v-if="newAreas.length > 1"
|
||||
class="absolute outline-dashed outline-gray-400 pointer-events-none z-20"
|
||||
:style="newAreasBoxStyle"
|
||||
/>
|
||||
<div
|
||||
v-if="selectionRect"
|
||||
@@ -93,7 +100,7 @@
|
||||
/>
|
||||
</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"
|
||||
ref="mask"
|
||||
class="top-0 bottom-0 left-0 right-0 absolute"
|
||||
@@ -103,7 +110,7 @@
|
||||
@contextmenu="openContextMenu"
|
||||
@dragover.prevent="onDragover"
|
||||
@dragenter="onDragenter"
|
||||
@dragleave="newArea = null"
|
||||
@dragleave="newAreas = []"
|
||||
@drop="onDrop"
|
||||
@pointerup="onPointerup"
|
||||
/>
|
||||
@@ -122,7 +129,7 @@ export default {
|
||||
ContextMenu,
|
||||
SelectionBox
|
||||
},
|
||||
inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'assignDropAreaSize', 'selectedAreasRef', 'template', 'isSelectModeRef'],
|
||||
inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'customDragFieldRef', 'assignDropAreaSize', 'selectedAreasRef', 'template', 'isSelectModeRef'],
|
||||
props: {
|
||||
image: {
|
||||
type: Object,
|
||||
@@ -191,6 +198,11 @@ export default {
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
drawCustomField: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -216,13 +228,13 @@ export default {
|
||||
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 () {
|
||||
return {
|
||||
areaRefs: [],
|
||||
showMask: false,
|
||||
resizeDirection: null,
|
||||
newArea: null,
|
||||
newAreas: [],
|
||||
contextMenu: null,
|
||||
selectionRect: null,
|
||||
selectionContextMenu: null
|
||||
@@ -274,6 +286,14 @@ export default {
|
||||
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 () {
|
||||
if (this.drawFieldType) {
|
||||
return this.drawFieldType
|
||||
@@ -302,6 +322,21 @@ export default {
|
||||
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 () {
|
||||
if (!this.selectionRect) return {}
|
||||
|
||||
@@ -331,7 +366,7 @@ export default {
|
||||
|
||||
const rect = this.$refs.image.getBoundingClientRect()
|
||||
|
||||
this.newArea = null
|
||||
this.newAreas = []
|
||||
this.showMask = false
|
||||
|
||||
this.contextMenu = {
|
||||
@@ -351,7 +386,7 @@ export default {
|
||||
|
||||
const rect = this.$refs.image.getBoundingClientRect()
|
||||
|
||||
this.newArea = null
|
||||
this.newAreas = []
|
||||
this.showMask = false
|
||||
|
||||
this.contextMenu = {
|
||||
@@ -390,7 +425,7 @@ export default {
|
||||
},
|
||||
closeContextMenu () {
|
||||
this.contextMenu = null
|
||||
this.newArea = null
|
||||
this.newAreas = []
|
||||
this.showMask = false
|
||||
},
|
||||
handleCopy () {
|
||||
@@ -410,7 +445,7 @@ export default {
|
||||
this.closeContextMenu()
|
||||
},
|
||||
handlePaste () {
|
||||
this.newArea = null
|
||||
this.newAreas = []
|
||||
this.showMask = false
|
||||
|
||||
this.$emit('paste-field', {
|
||||
@@ -435,22 +470,66 @@ export default {
|
||||
}
|
||||
},
|
||||
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
|
||||
|
||||
this.assignDropAreaSize(this.newArea, this.dragFieldPlaceholder, {
|
||||
maskW: this.$refs.mask.clientWidth,
|
||||
maskH: this.$refs.mask.clientHeight
|
||||
})
|
||||
if (customAreas.length > 1) {
|
||||
const refArea = customAreas[0]
|
||||
|
||||
this.newArea.x = (e.offsetX - 6) / this.$refs.mask.clientWidth
|
||||
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight - this.newArea.h / 2
|
||||
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 = {}
|
||||
|
||||
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,
|
||||
maskH: this.$refs.mask.clientHeight
|
||||
})
|
||||
}
|
||||
|
||||
newArea.x = dropX
|
||||
newArea.y = dropY - newArea.h / 2
|
||||
|
||||
this.newAreas = [newArea]
|
||||
}
|
||||
},
|
||||
onDragover (e) {
|
||||
this.newArea.x = (e.offsetX - 6) / this.$refs.mask.clientWidth
|
||||
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight - this.newArea.h / 2
|
||||
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.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) {
|
||||
this.newArea = null
|
||||
this.newAreas = []
|
||||
|
||||
this.$emit('drop-field', {
|
||||
x: e.offsetX,
|
||||
@@ -479,7 +558,7 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.isMobile && !this.drawField) {
|
||||
if (this.isMobile && !this.drawField && !this.drawCustomField) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -490,14 +569,14 @@ export default {
|
||||
this.showMask = true
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.newArea = {
|
||||
this.newAreas = [{
|
||||
initialX: e.offsetX / this.$refs.mask.clientWidth,
|
||||
initialY: e.offsetY / this.$refs.mask.clientHeight,
|
||||
x: e.offsetX / this.$refs.mask.clientWidth,
|
||||
y: e.offsetY / this.$refs.mask.clientHeight,
|
||||
w: 0,
|
||||
h: 0
|
||||
}
|
||||
}]
|
||||
})
|
||||
},
|
||||
startSelectionRect (e) {
|
||||
@@ -563,28 +642,30 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.newArea) {
|
||||
const dx = e.offsetX / this.$refs.mask.clientWidth - this.newArea.initialX
|
||||
const dy = e.offsetY / this.$refs.mask.clientHeight - this.newArea.initialY
|
||||
const drawArea = this.newAreas[0]
|
||||
|
||||
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) {
|
||||
this.newArea.x = this.newArea.initialX
|
||||
drawArea.x = drawArea.initialX
|
||||
} else {
|
||||
this.newArea.x = e.offsetX / this.$refs.mask.clientWidth
|
||||
drawArea.x = e.offsetX / this.$refs.mask.clientWidth
|
||||
}
|
||||
|
||||
if (dy > 0) {
|
||||
this.newArea.y = this.newArea.initialY
|
||||
drawArea.y = drawArea.initialY
|
||||
} 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') {
|
||||
this.newArea.cell_w = this.newArea.h * (this.$refs.mask.clientHeight / this.$refs.mask.clientWidth)
|
||||
if ((this.drawField?.type || this.drawCustomField?.type || this.drawFieldType) === 'cells') {
|
||||
drawArea.cell_w = drawArea.h * (this.$refs.mask.clientHeight / this.$refs.mask.clientWidth)
|
||||
}
|
||||
|
||||
this.newArea.w = Math.abs(dx)
|
||||
this.newArea.h = Math.abs(dy)
|
||||
drawArea.w = Math.abs(dx)
|
||||
drawArea.h = Math.abs(dy)
|
||||
}
|
||||
},
|
||||
onPointerup (e) {
|
||||
@@ -601,29 +682,33 @@ export default {
|
||||
})
|
||||
|
||||
this.selectionRect = null
|
||||
} else if (this.newArea) {
|
||||
const area = {
|
||||
x: this.newArea.x,
|
||||
y: this.newArea.y,
|
||||
w: this.newArea.w,
|
||||
h: this.newArea.h,
|
||||
page: this.number
|
||||
} else {
|
||||
const drawArea = this.newAreas[0]
|
||||
|
||||
if (drawArea?.initialX !== undefined) {
|
||||
const area = {
|
||||
x: drawArea.x,
|
||||
y: drawArea.y,
|
||||
w: drawArea.w,
|
||||
h: drawArea.h,
|
||||
page: this.number
|
||||
}
|
||||
|
||||
if ('cell_w' in drawArea) {
|
||||
area.cell_w = drawArea.cell_w
|
||||
}
|
||||
|
||||
const dx = Math.abs(e.offsetX - this.$refs.mask.clientWidth * drawArea.initialX)
|
||||
const dy = Math.abs(e.offsetY - this.$refs.mask.clientHeight * drawArea.initialY)
|
||||
|
||||
const isTooSmall = dx < 8 && dy < 8
|
||||
|
||||
this.$emit('draw', { area, isTooSmall })
|
||||
}
|
||||
|
||||
if ('cell_w' in this.newArea) {
|
||||
area.cell_w = this.newArea.cell_w
|
||||
}
|
||||
|
||||
const dx = Math.abs(e.offsetX - this.$refs.mask.clientWidth * this.newArea.initialX)
|
||||
const dy = Math.abs(e.offsetY - this.$refs.mask.clientHeight * this.newArea.initialY)
|
||||
|
||||
const isTooSmall = dx < 8 && dy < 8
|
||||
|
||||
this.$emit('draw', { area, isTooSmall })
|
||||
}
|
||||
|
||||
this.showMask = false
|
||||
this.newArea = null
|
||||
this.newAreas = []
|
||||
},
|
||||
rectsOverlap (r1, r2) {
|
||||
return !(
|
||||
|
||||
@@ -231,7 +231,10 @@
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
<li class="mt-1">
|
||||
<li
|
||||
v-if="withCondition"
|
||||
class="mt-1"
|
||||
>
|
||||
<label
|
||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||
@click="$emit('click-condition')"
|
||||
@@ -244,12 +247,29 @@
|
||||
</span>
|
||||
</label>
|
||||
</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>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<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'
|
||||
|
||||
const isConnected = ref(false)
|
||||
@@ -261,6 +281,7 @@ export default {
|
||||
IconCircleCheck,
|
||||
IconRouteAltLeft,
|
||||
IconInfoCircle,
|
||||
IconForms,
|
||||
IconMathFunction,
|
||||
IconInnerShadowTop,
|
||||
IconBrandStripe
|
||||
@@ -270,9 +291,19 @@ export default {
|
||||
field: {
|
||||
type: Object,
|
||||
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 () {
|
||||
return {
|
||||
isLoading: false
|
||||
|
||||
@@ -54,6 +54,7 @@ class AccountConfig < ApplicationRecord
|
||||
WITH_FIELD_LABELS_KEY = 'with_field_labels'
|
||||
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
|
||||
DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format'
|
||||
TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields'
|
||||
POLICY_LINKS_KEY = 'policy_links'
|
||||
|
||||
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' %>
|
||||
<% 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]
|
||||
resource :mfa_setup, only: %i[show new edit create destroy], controller: 'mfa_setup'
|
||||
resources :account_configs, only: %i[create destroy]
|
||||
resources :account_custom_fields, only: %i[create]
|
||||
resources :user_configs, only: %i[create]
|
||||
resources :encrypted_user_configs, only: %i[destroy]
|
||||
resources :timestamp_server, only: %i[create]
|
||||
|
||||
Reference in New Issue
Block a user