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

474 lines
14 KiB
Vue
Raw Normal View History

2023-05-21 17:59:36 +03:00
<template>
2023-06-03 00:53:46 +03:00
<div
2024-03-07 18:59:19 +02:00
class="list-field group mb-2"
2023-06-03 00:53:46 +03:00
>
<div
2025-03-18 17:34:11 +02:00
class="border border-base-300 rounded rounded-tr-none relative group fields-list-item"
2023-11-04 14:06:59 +02:00
:style="{ backgroundColor: backgroundColor }"
2023-06-03 00:53:46 +03:00
>
2023-10-25 01:08:39 +03:00
<div class="flex items-center justify-between relative group/contenteditable-container">
2023-06-11 22:23:37 +03:00
<div
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
@click="scrollToFirstArea"
/>
2023-06-03 00:53:46 +03:00
<div class="flex items-center p-1 space-x-1">
2023-06-04 01:08:45 +03:00
<FieldType
v-model="field.type"
2024-07-11 21:40:15 +03:00
:editable="editable && !defaultField && field.type != 'heading'"
2023-06-04 01:08:45 +03:00
:button-width="20"
2024-04-23 01:57:45 +03:00
:menu-classes="'mt-1.5'"
:menu-style="{ backgroundColor: dropdownBgColor }"
2023-06-24 16:17:07 +03:00
@update:model-value="[maybeUpdateOptions(), save()]"
2023-06-08 00:20:42 +03:00
@click="scrollToFirstArea"
2023-06-04 01:08:45 +03:00
/>
2023-06-03 00:53:46 +03:00
<Contenteditable
ref="name"
2025-01-07 17:30:21 +02:00
:model-value="(defaultField ? (defaultField.title || field.title || field.name) : field.name) || defaultName"
2024-07-11 21:40:15 +03:00
:editable="editable && !defaultField && field.type != 'heading'"
2023-06-03 00:53:46 +03:00
:icon-inline="true"
2023-06-04 01:08:45 +03:00
:icon-width="18"
:icon-stroke-width="1.6"
2023-06-08 00:20:42 +03:00
@focus="[onNameFocus(), scrollToFirstArea()]"
2023-06-03 00:53:46 +03:00
@blur="onNameBlur"
/>
</div>
2023-06-20 00:24:39 +03:00
<div
v-if="isNameFocus"
class="flex items-center relative"
>
2023-11-11 23:59:26 +02:00
<template v-if="field.type != 'phone'">
<input
:id="`required-checkbox-${field.uuid}`"
v-model="field.required"
type="checkbox"
class="checkbox checkbox-xs no-animation rounded"
@mousedown.prevent
>
<label
:for="`required-checkbox-${field.uuid}`"
class="label text-xs"
@click.prevent="field.required = !field.required"
@mousedown.prevent
2024-01-04 01:05:36 +02:00
>{{ t('required') }}</label>
2023-11-11 23:59:26 +02:00
</template>
2023-06-20 00:24:39 +03:00
</div>
<div
2023-11-04 01:19:32 +02:00
v-else-if="editable"
2023-06-20 00:24:39 +03:00
class="flex items-center space-x-1"
>
2023-11-05 16:31:32 +02:00
<button
2024-05-22 15:39:46 +03:00
v-if="field && !field.areas?.length"
2024-02-16 19:55:53 +02:00
:title="t('draw')"
2023-11-05 16:31:32 +02:00
class="relative cursor-pointer text-transparent group-hover:text-base-content"
2023-11-14 01:30:00 +02:00
@click="$emit('set-draw', { field })"
2023-11-05 16:31:32 +02:00
>
<IconNewSection
:width="18"
:stroke-width="1.6"
/>
</button>
2024-02-24 01:01:36 +02:00
<button
v-if="field.preferences?.formula"
class="relative cursor-pointer text-transparent group-hover:text-base-content"
2024-03-01 23:29:37 +02:00
:title="t('formula')"
2024-02-24 01:01:36 +02:00
@click="isShowFormulaModal = true"
>
<IconMathFunction
:width="18"
:stroke-width="1.6"
/>
</button>
2024-03-01 23:29:37 +02:00
<button
v-if="field.conditions?.length"
class="relative cursor-pointer text-transparent group-hover:text-base-content"
:title="t('condition')"
@click="isShowConditionsModal = true"
>
<IconRouteAltLeft
:width="18"
:stroke-width="1.6"
/>
</button>
2023-12-14 21:22:26 +02:00
<PaymentSettings
v-if="field.type === 'payment'"
:field="field"
2024-06-05 21:09:08 +03:00
@click-condition="isShowConditionsModal = true"
2024-07-07 19:55:56 +03:00
@click-description="isShowDescriptionModal = true"
2024-07-31 22:56:18 +03:00
@click-formula="isShowFormulaModal = true"
2023-12-14 21:22:26 +02:00
/>
2023-06-11 22:34:03 +03:00
<span
2024-07-11 21:40:15 +03:00
v-else-if="field.type !== 'heading'"
2025-03-18 17:34:11 +02:00
class="dropdown dropdown-end field-settings-dropdown"
2023-12-31 17:23:27 +02:00
@mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true"
2023-06-11 22:34:03 +03:00
>
2023-06-03 00:53:46 +03:00
<label
tabindex="0"
2024-02-19 23:52:47 +02:00
:title="t('settings')"
2023-08-19 21:05:54 +03:00
class="cursor-pointer text-transparent group-hover:text-base-content"
2023-06-03 00:53:46 +03:00
>
2023-11-05 01:41:54 +02:00
<IconSettings
2023-06-04 01:08:45 +03:00
:width="18"
2023-06-03 00:53:46 +03:00
:stroke-width="1.6"
/>
</label>
<ul
2023-12-31 17:23:27 +02:00
v-if="renderDropdown"
2023-06-03 00:53:46 +03:00
tabindex="0"
2024-04-23 01:57:45 +03:00
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10"
:style="{ backgroundColor: dropdownBgColor }"
2023-11-05 01:41:54 +02:00
draggable="true"
@dragstart.prevent.stop
2023-06-03 00:53:46 +03:00
@click="closeDropdown"
>
2024-05-05 16:57:04 +03:00
<FieldSettings
:field="field"
:default-field="defaultField"
:editable="editable"
:background-color="dropdownBgColor"
@click-formula="isShowFormulaModal = true"
2025-03-09 02:08:44 +02:00
@click-font="isShowFontModal = true"
2024-05-05 16:57:04 +03:00
@click-description="isShowDescriptionModal = true"
@click-condition="isShowConditionsModal = true"
@set-draw="$emit('set-draw', $event)"
2024-08-07 23:50:52 +03:00
@remove-area="removeArea"
2024-05-05 16:57:04 +03:00
@scroll-to="$emit('scroll-to', $event)"
/>
2023-06-03 00:53:46 +03:00
</ul>
</span>
2023-06-04 01:08:45 +03:00
<button
2025-03-18 17:34:11 +02:00
class="relative text-transparent group-hover:text-base-content pr-1 field-remove-button"
2024-02-19 23:52:47 +02:00
:title="t('remove')"
2023-06-04 01:08:45 +03:00
@click="$emit('remove', field)"
>
2023-06-03 00:53:46 +03:00
<IconTrashX
2023-06-04 01:08:45 +03:00
:width="18"
2023-06-03 00:53:46 +03:00
:stroke-width="1.6"
/>
</button>
</div>
2023-05-21 17:59:36 +03:00
</div>
<div
2023-06-03 00:53:46 +03:00
v-if="field.options"
2023-11-15 19:48:51 +02:00
ref="options"
2023-06-03 00:53:46 +03:00
class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
2023-11-13 01:46:28 +02:00
draggable="true"
@dragstart.prevent.stop
2023-05-21 17:59:36 +03:00
>
2023-06-03 00:53:46 +03:00
<div
v-for="(option, index) in field.options"
2023-11-13 01:46:28 +02:00
:key="option.uuid"
2023-06-03 00:53:46 +03:00
class="flex space-x-1.5 items-center"
>
2023-06-04 01:08:45 +03:00
<span class="text-sm w-3.5">
2023-06-03 00:53:46 +03:00
{{ index + 1 }}.
</span>
2023-11-14 01:30:00 +02:00
<div
2024-02-28 00:40:34 +02:00
v-if="editable && ['radio', 'multiple'].includes(field.type) && (index > 0 || field.areas.find((a) => a.option_uuid) || !field.areas.length) && !field.areas.find((a) => a.option_uuid === option.uuid)"
2023-11-14 01:30:00 +02:00
class="items-center flex w-full"
>
<input
v-model="option.value"
class="w-full input input-primary input-xs text-sm bg-transparent !pr-7 -mr-6"
type="text"
2024-01-07 14:19:21 +02:00
dir="auto"
2023-11-14 01:30:00 +02:00
required
2024-01-04 01:05:36 +02:00
:placeholder="`${t('option')} ${index + 1}`"
2023-11-14 01:30:00 +02:00
@blur="save"
>
<button
2024-02-16 19:55:53 +02:00
:title="t('draw')"
2023-11-15 19:48:51 +02:00
tabindex="-1"
2023-11-14 01:30:00 +02:00
@click.prevent="$emit('set-draw', { field, option })"
>
<IconNewSection
:width="18"
:stroke-width="1.6"
/>
</button>
</div>
2023-06-03 00:53:46 +03:00
<input
2023-11-14 01:30:00 +02:00
v-else
2023-11-13 01:46:28 +02:00
v-model="option.value"
2023-08-19 21:05:54 +03:00
class="w-full input input-primary input-xs text-sm bg-transparent"
2024-01-04 01:05:36 +02:00
:placeholder="`${t('option')} ${index + 1}`"
2023-06-03 00:53:46 +03:00
type="text"
2025-01-16 00:40:22 +02:00
:readonly="!editable || defaultField"
2023-06-03 00:53:46 +03:00
required
2024-01-07 14:19:21 +02:00
dir="auto"
2023-11-14 01:30:00 +02:00
@focus="maybeFocusOnOptionArea(option)"
2023-06-24 16:17:07 +03:00
@blur="save"
2023-06-03 00:53:46 +03:00
>
<button
2025-01-16 00:40:22 +02:00
v-if="editable && !defaultField"
2023-06-04 01:08:45 +03:00
class="text-sm w-3.5"
2023-11-15 19:48:51 +02:00
tabindex="-1"
2023-11-14 01:30:00 +02:00
@click="removeOption(option)"
2023-06-03 00:53:46 +03:00
>
&times;
</button>
</div>
2024-02-28 00:40:34 +02:00
<div
2025-01-16 00:40:22 +02:00
v-if="field.options && (!editable || defaultField)"
2024-02-28 00:40:34 +02:00
class="pb-1"
/>
2023-06-03 00:53:46 +03:00
<button
2025-01-16 00:40:22 +02:00
v-else-if="field.options && editable && !defaultField"
2023-06-03 00:53:46 +03:00
class="text-center text-sm w-full pb-1"
2023-11-13 01:46:28 +02:00
@click="addOption"
2023-06-03 00:53:46 +03:00
>
2024-01-04 01:05:36 +02:00
+ {{ t('add_option') }}
2023-05-21 17:59:36 +03:00
</button>
</div>
</div>
2024-02-24 01:01:36 +02:00
<Teleport
v-if="isShowFormulaModal"
:to="modalContainerEl"
>
<FormulaModal
:field="field"
2024-04-15 16:34:44 +03:00
:editable="editable && !defaultField"
2024-02-24 01:01:36 +02:00
:build-default-name="buildDefaultName"
@close="isShowFormulaModal = false"
/>
</Teleport>
2025-03-09 02:08:44 +02:00
<Teleport
v-if="isShowFontModal"
:to="modalContainerEl"
>
<FontModal
:field="field"
:editable="editable && !defaultField"
:build-default-name="buildDefaultName"
@close="isShowFontModal = false"
/>
</Teleport>
2024-03-01 23:29:37 +02:00
<Teleport
v-if="isShowConditionsModal"
:to="modalContainerEl"
>
<ConditionsModal
2024-12-07 01:35:59 +02:00
:item="field"
2024-03-01 23:29:37 +02:00
:build-default-name="buildDefaultName"
@close="isShowConditionsModal = false"
/>
</Teleport>
2024-03-09 18:24:21 +02:00
<Teleport
v-if="isShowDescriptionModal"
:to="modalContainerEl"
>
<DescriptionModal
:field="field"
2024-04-15 16:34:44 +03:00
:editable="editable && !defaultField"
2024-03-09 18:24:21 +02:00
:build-default-name="buildDefaultName"
@close="isShowDescriptionModal = false"
/>
</Teleport>
2023-05-21 17:59:36 +03:00
</div>
</template>
<script>
2023-06-03 00:53:46 +03:00
import Contenteditable from './contenteditable'
2023-06-04 01:08:45 +03:00
import FieldType from './field_type'
2023-12-14 21:22:26 +02:00
import PaymentSettings from './payment_settings'
2024-05-05 16:57:04 +03:00
import FieldSettings from './field_settings'
2024-02-24 01:01:36 +02:00
import FormulaModal from './formula_modal'
2025-03-09 02:08:44 +02:00
import FontModal from './font_modal'
2024-03-01 23:29:37 +02:00
import ConditionsModal from './conditions_modal'
2024-03-09 18:24:21 +02:00
import DescriptionModal from './description_modal'
2024-05-05 16:57:04 +03:00
import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings } from '@tabler/icons-vue'
2023-11-13 01:46:28 +02:00
import { v4 } from 'uuid'
2023-06-03 00:53:46 +03:00
2023-05-21 17:59:36 +03:00
export default {
2023-05-31 23:19:20 +03:00
name: 'TemplateField',
2023-06-03 00:53:46 +03:00
components: {
Contenteditable,
2023-11-05 01:41:54 +02:00
IconSettings,
2024-05-05 16:57:04 +03:00
FieldSettings,
2023-12-14 21:22:26 +02:00
PaymentSettings,
2023-06-03 00:53:46 +03:00
IconNewSection,
2024-02-24 01:01:36 +02:00
FormulaModal,
2025-03-09 02:08:44 +02:00
FontModal,
2024-03-09 18:24:21 +02:00
DescriptionModal,
2024-03-01 23:29:37 +02:00
ConditionsModal,
IconRouteAltLeft,
2023-06-04 01:08:45 +03:00
IconTrashX,
2024-02-24 01:01:36 +02:00
IconMathFunction,
2023-06-04 01:08:45 +03:00
FieldType
2023-06-03 00:53:46 +03:00
},
2024-08-05 19:38:47 +03:00
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't', 'locale'],
2023-05-21 17:59:36 +03:00
props: {
field: {
type: Object,
required: true
2023-11-04 01:19:32 +02:00
},
2023-11-27 20:24:58 +02:00
defaultField: {
type: Object,
required: false,
default: null
},
2023-11-04 01:19:32 +02:00
editable: {
type: Boolean,
required: false,
default: true
2023-05-21 17:59:36 +03:00
}
},
2023-11-04 14:06:59 +02:00
emits: ['set-draw', 'remove', 'scroll-to'],
2023-06-20 00:24:39 +03:00
data () {
return {
2023-12-14 21:22:26 +02:00
isNameFocus: false,
2023-12-31 17:23:27 +02:00
showPaymentModal: false,
2024-02-24 01:01:36 +02:00
isShowFormulaModal: false,
2025-03-09 02:08:44 +02:00
isShowFontModal: false,
2024-03-01 23:29:37 +02:00
isShowConditionsModal: false,
2024-03-09 18:24:21 +02:00
isShowDescriptionModal: false,
2023-12-31 17:23:27 +02:00
renderDropdown: false
2023-06-20 00:24:39 +03:00
}
},
2023-05-21 17:59:36 +03:00
computed: {
2023-06-27 00:57:29 +03:00
fieldNames: FieldType.computed.fieldNames,
2024-10-02 17:57:31 +03:00
fieldLabels: FieldType.computed.fieldLabels,
2024-04-23 01:57:45 +03:00
dropdownBgColor () {
return ['', null, 'transparent'].includes(this.backgroundColor) ? 'white' : this.backgroundColor
},
2024-03-08 00:15:01 +02:00
schemaAttachmentsIndexes () {
return (this.template.schema || []).reduce((acc, item, index) => {
acc[item.attachment_uuid] = index
return acc
}, {})
},
sortedAreas () {
return (this.field.areas || []).sort((a, b) => {
return this.schemaAttachmentsIndexes[a.attachment_uuid] - this.schemaAttachmentsIndexes[b.attachment_uuid]
})
},
2024-02-24 01:01:36 +02:00
modalContainerEl () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
},
2023-06-03 00:53:46 +03:00
defaultName () {
2024-02-24 01:01:36 +02:00
return this.buildDefaultName(this.field, this.template.fields)
2023-06-03 00:53:46 +03:00
},
2023-05-21 17:59:36 +03:00
areas () {
return this.field.areas || []
}
},
2023-12-17 23:32:05 +02:00
created () {
this.field.preferences ||= {}
if (this.field.type === 'date') {
this.field.preferences.format ||=
2024-08-05 19:38:47 +03:00
({ 'de-DE': 'DD.MM.YYYY' }[this.locale] || ((Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') || new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/)) ? 'MM/DD/YYYY' : 'DD/MM/YYYY'))
2023-12-17 23:32:05 +02:00
}
},
2023-05-21 17:59:36 +03:00
methods: {
2024-08-07 23:50:52 +03:00
removeArea (area) {
this.field.areas.splice(this.field.areas.indexOf(area), 1)
this.save()
},
2024-02-24 01:01:36 +02:00
buildDefaultName (field, fields) {
2024-07-31 22:56:18 +03:00
if (field.type === 'payment' && field.preferences?.price && !field.preferences?.formula) {
2024-02-24 01:01:36 +02:00
const { price, currency } = field.preferences || {}
const formattedPrice = new Intl.NumberFormat([], {
style: 'currency',
currency
}).format(price)
return `${this.fieldNames[field.type]} ${formattedPrice}`
} else {
const typeIndex = fields.filter((f) => f.type === field.type).indexOf(field)
2024-12-07 01:35:59 +02:00
if (field.type === 'heading') {
2024-07-11 21:40:15 +03:00
return `${this.fieldNames[field.type]} ${typeIndex + 1}`
} else {
2024-10-02 17:57:31 +03:00
return `${this.fieldLabels[field.type]} ${typeIndex + 1}`
2024-07-11 21:40:15 +03:00
}
2024-02-24 01:01:36 +02:00
}
},
2023-06-03 00:53:46 +03:00
onNameFocus (e) {
2023-06-20 00:24:39 +03:00
this.isNameFocus = true
2023-06-03 00:53:46 +03:00
if (!this.field.name) {
setTimeout(() => {
this.$refs.name.$refs.contenteditable.innerText = ' '
}, 1)
}
},
2023-11-14 01:30:00 +02:00
maybeFocusOnOptionArea (option) {
const area = this.field.areas.find((a) => a.option_uuid === option.uuid)
if (area) {
this.selectedAreaRef.value = area
}
},
2023-06-08 00:20:42 +03:00
scrollToFirstArea () {
2024-03-08 00:15:01 +02:00
return this.sortedAreas[0] && this.$emit('scroll-to', this.sortedAreas[0])
2023-06-08 00:20:42 +03:00
},
2023-06-03 00:53:46 +03:00
closeDropdown () {
2025-01-14 00:10:50 +02:00
this.$el.getRootNode().activeElement.blur()
2023-06-03 00:53:46 +03:00
},
2023-11-13 01:46:28 +02:00
addOption () {
this.field.options.push({ value: '', uuid: v4() })
2023-11-15 19:48:51 +02:00
this.$nextTick(() => {
const inputs = this.$refs.options.querySelectorAll('input')
inputs[inputs.length - 1]?.focus()
})
2023-11-14 01:30:00 +02:00
this.save()
},
removeOption (option) {
this.field.options.splice(this.field.options.indexOf(option), 1)
2024-03-17 19:40:30 +02:00
const optionIndex = this.field.areas.findIndex((a) => a.option_uuid === option.uuid)
if (optionIndex !== -1) {
this.field.areas.splice(this.field.areas.findIndex((a) => a.option_uuid === option.uuid), 1)
}
2023-11-14 01:30:00 +02:00
2023-11-13 01:46:28 +02:00
this.save()
},
2023-06-08 00:20:42 +03:00
maybeUpdateOptions () {
2023-11-05 01:41:54 +02:00
delete this.field.default_value
2023-06-16 00:04:05 +03:00
if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
2023-06-07 01:16:43 +03:00
delete this.field.options
}
2023-06-08 00:20:42 +03:00
2023-06-16 00:04:05 +03:00
if (['radio', 'multiple', 'select'].includes(this.field.type)) {
2023-11-13 01:46:28 +02:00
this.field.options ||= [{ value: '', uuid: v4() }]
2023-06-08 00:20:42 +03:00
}
2023-06-27 00:57:29 +03:00
2024-07-11 21:40:15 +03:00
if (['heading'].includes(this.field.type)) {
this.field.readonly = true
}
2023-06-27 00:57:29 +03:00
(this.field.areas || []).forEach((area) => {
if (this.field.type === 'cells') {
area.cell_w = area.w * 2 / Math.floor(area.w / area.h)
} else {
delete area.cell_w
}
})
2023-06-07 01:16:43 +03:00
},
2023-06-03 00:53:46 +03:00
onNameBlur (e) {
2023-08-19 21:05:54 +03:00
const text = this.$refs.name.$refs.contenteditable.innerText.trim()
if (text) {
this.field.name = text
2023-06-03 00:53:46 +03:00
} else {
this.field.name = ''
this.$refs.name.$refs.contenteditable.innerText = this.defaultName
}
2023-06-20 00:24:39 +03:00
this.isNameFocus = false
2023-06-24 16:17:07 +03:00
this.save()
2023-05-21 17:59:36 +03:00
}
}
}
</script>