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

933 lines
30 KiB
Vue
Raw Normal View History

2023-05-21 17:59:36 +03:00
<template>
<div
2025-03-19 15:12:22 +02:00
class="absolute overflow-visible group field-area-container"
2023-05-21 17:59:36 +03:00
:style="positionStyle"
2024-12-28 13:08:00 +02:00
:class="{ 'z-[1]': isMoved || isDragged }"
2023-06-03 00:53:46 +03:00
@pointerdown.stop
2024-06-12 20:05:55 +03:00
@mousedown="startMouseMove"
2023-10-06 03:21:14 +03:00
@touchstart="startTouchDrag"
2023-05-21 17:59:36 +03:00
>
<div
2026-01-17 19:04:43 +02:00
v-if="isSelected || isDraw || isInMultiSelection"
2023-06-07 01:16:43 +03:00
class="top-0 bottom-0 right-0 left-0 absolute border border-1.5 pointer-events-none"
2025-09-26 20:52:44 +03:00
:class="activeBorderClasses"
2023-06-07 01:16:43 +03:00
/>
2023-06-27 00:57:29 +03:00
<div
v-if="field.type === 'cells' && (isSelected || isDraw)"
class="top-0 bottom-0 right-0 left-0 absolute"
>
<div
v-for="(cellW, index) in cells"
:key="index"
class="absolute top-0 bottom-0 border-r"
2025-01-10 15:12:09 +02:00
:class="field.type === 'heading' ? '' : borderColors[submitterIndex % borderColors.length]"
2023-06-27 00:57:29 +03:00
:style="{ left: (cellW / area.w * 100) + '%' }"
>
<span
2026-01-17 19:04:43 +02:00
v-if="index === 0 && editable && !isInMultiSelection"
2023-06-27 00:57:29 +03:00
class="h-2.5 w-2.5 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-ew-resize z-10"
style="left: -4px"
@mousedown.stop="startResizeCell"
/>
</div>
</div>
2026-02-12 09:22:08 +02:00
<AreaTitle
ref="title"
:area="area"
:field="field"
:template="template"
:selected-areas-ref="selectedAreasRef"
:get-field-type-index="getFieldTypeIndex"
:default-field="defaultField"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:default-submitters="defaultSubmitters"
:editable="editable"
:is-mobile="isMobile"
:is-value-input="isValueInput"
:is-select-input="isSelectInput"
@change="save"
@remove="$emit('remove')"
@scroll-to="$emit('scroll-to', $event)"
@add-custom-field="$emit('add-custom-field')"
/>
2023-06-03 00:53:46 +03:00
<div
2024-07-11 21:40:15 +03:00
ref="touchValueTarget"
2025-01-24 23:08:44 +02:00
class="flex h-full w-full field-area"
2024-02-05 23:03:45 +02:00
dir="auto"
2025-09-26 20:52:44 +03:00
:class="[isValueInput ? 'cursor-text' : '', isValueInput || isCheckboxInput || isSelectInput ? 'bg-opacity-50' : 'bg-opacity-80', bgClasses, isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center items-center']"
2024-07-11 21:40:15 +03:00
@click="focusValueInput"
2023-06-03 00:53:46 +03:00
>
2023-06-04 01:08:45 +03:00
<span
v-if="field"
2024-07-11 21:40:15 +03:00
class="flex justify-center items-center space-x-1"
2025-09-26 20:52:44 +03:00
:class="{ 'w-full': isWFullType, 'h-full': !isValueInput && (!isDefaultValuePresent || field.type === 'strikethrough') }"
2023-06-04 01:08:45 +03:00
>
2023-11-05 01:41:54 +02:00
<div
2025-09-26 20:52:44 +03:00
v-if="field.type === 'strikethrough'"
class="w-full h-full flex items-center justify-center"
>
<svg
v-if="(((basePageWidth / pageWidth) * pageHeight) * area.h) < 41.6"
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
>
<line
x1="0"
y1="50%"
x2="100%"
y2="50%"
:stroke="field.preferences?.color || 'red'"
:stroke-width="strikethroughWidth"
/>
</svg>
<svg
v-else
xmlns="http://www.w3.org/2000/svg"
:style="{ overflow: 'visible', width: `calc(100% - ${strikethroughWidth})`, height: `calc(100% - ${strikethroughWidth})` }"
>
<line
x1="0"
y1="0"
x2="100%"
y2="100%"
:stroke="field.preferences?.color || 'red'"
:stroke-width="strikethroughWidth"
/>
<line
x1="100%"
y1="0"
x2="0"
y2="100%"
:stroke="field.preferences?.color || 'red'"
:stroke-width="strikethroughWidth"
/>
</svg>
</div>
<div
v-else-if="isDefaultValuePresent || isValueInput || isSelectInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
2025-08-23 20:11:10 +03:00
:class="{ 'w-full h-full': isWFullType }"
2025-07-14 12:53:15 +03:00
:style="fontStyle"
2023-11-05 01:41:54 +02:00
>
2023-11-07 11:36:11 +02:00
<div
ref="textContainer"
class="flex items-center px-0.5"
2026-04-21 19:54:18 +03:00
:style="{ color: isConditionMatch ? field.preferences?.color : '#9ca3af' }"
2025-08-23 20:11:10 +03:00
:class="{ 'w-full h-full': isWFullType }"
2023-11-07 11:36:11 +02:00
>
2024-02-05 23:03:45 +02:00
<IconCheck
v-if="field.type == 'checkbox'"
class="aspect-square mx-auto"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
/>
2025-08-23 20:11:10 +03:00
<template
v-else-if="(field.type === 'radio' || field.type === 'multiple') && field?.areas?.length > 1"
>
<IconCheck
2025-08-23 21:33:46 +03:00
v-if="field.type === 'multiple' ? field.default_value.includes(buildAreaOptionValue(area)) : buildAreaOptionValue(area) === field.default_value"
2025-08-23 20:11:10 +03:00
class="aspect-square mx-auto"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
/>
</template>
2024-05-12 11:12:07 +03:00
<span
2026-04-21 19:54:18 +03:00
v-else-if="field.type === 'number' && !isContenteditable && (displayValue || displayValue == 0)"
2024-05-12 11:12:07 +03:00
class="whitespace-pre-wrap"
2026-04-21 19:54:18 +03:00
>{{ formatNumber(displayValue, field.preferences?.format) }}</span>
2024-06-27 12:40:10 +03:00
<span
v-else-if="field.default_value === '{{date}}'"
>
2026-04-23 20:25:41 +03:00
{{ /[HhAasz]/.test(field.preferences?.format || '') ? t('signing_date_and_time') : t('signing_date') }}
2024-06-27 12:40:10 +03:00
</span>
2024-06-22 13:16:22 +03:00
<div
v-else-if="field.type === 'cells' && field.default_value"
2026-01-30 11:21:47 +02:00
class="w-full flex"
:class="fontClasses"
2024-06-22 13:16:22 +03:00
>
<div
v-for="(char, index) in field.default_value"
:key="index"
class="text-center flex-none"
:style="{ width: (area.cell_w / area.w * 100) + '%' }"
>
{{ char }}
</div>
</div>
2025-08-23 20:11:10 +03:00
<select
v-else-if="isSelectInput"
ref="defaultValueSelect"
class="bg-transparent outline-none focus:outline-none w-full"
@change="[field.default_value = $event.target.value, field.readonly = !!field.default_value?.length, save()]"
2026-01-17 19:04:43 +02:00
@focus="selectedAreasRef.value = [area]"
2025-08-23 20:11:10 +03:00
@keydown.enter="onDefaultValueEnter"
>
<option
:disabled="!field.default_value?.length"
:selected="!field.default_value?.length"
:value="''"
>
{{ t(field.default_value?.length ? 'none' : 'select') }}
</option>
<option
v-for="(option, index) in field.options"
:key="index"
:selected="field.default_value === option.value"
:value="option.value"
>
{{ option.value }}
</option>
</select>
2024-02-05 23:03:45 +02:00
<span
v-else
2024-06-12 20:05:55 +03:00
ref="defaultValue"
2024-07-11 21:40:15 +03:00
:contenteditable="isValueInput"
2025-08-23 20:11:10 +03:00
class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-base-content/30"
2024-07-11 21:40:15 +03:00
:class="{ 'cursor-text': isValueInput }"
2026-04-21 19:54:18 +03:00
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : (isConditionMatch ? (field.type === 'date' ? field.preferences?.format || t('type_value') : t('type_value')) : '')"
2024-06-12 20:05:55 +03:00
@blur="onDefaultValueBlur"
2026-01-17 19:04:43 +02:00
@focus="selectedAreasRef.value = [area]"
2024-06-13 10:26:36 +03:00
@paste.prevent="onPaste"
2024-07-11 21:40:15 +03:00
@keydown.enter="onDefaultValueEnter"
2026-04-21 19:54:18 +03:00
>{{ displayValue }}</span>
2023-11-05 01:41:54 +02:00
</div>
</div>
2023-06-08 00:20:42 +03:00
<component
:is="fieldIcons[field.type]"
2025-08-23 20:11:10 +03:00
v-else-if="!isCheckboxInput"
2023-06-08 00:20:42 +03:00
width="100%"
height="100%"
2023-11-05 01:41:54 +02:00
class="max-h-10 opacity-50"
2023-06-08 00:20:42 +03:00
/>
2023-06-04 01:08:45 +03:00
</span>
2023-05-21 17:59:36 +03:00
</div>
2023-06-04 01:08:45 +03:00
<div
2025-08-23 20:11:10 +03:00
v-if="!isValueInput && !isSelectInput"
2023-10-06 03:21:14 +03:00
ref="touchTarget"
2024-06-12 20:05:55 +03:00
class="absolute top-0 bottom-0 right-0 left-0"
:class="isDragged ? 'cursor-grab' : 'cursor-pointer'"
2024-06-27 12:40:10 +03:00
@dblclick="maybeToggleDefaultValue"
2025-08-23 20:11:10 +03:00
@click="inputMode && maybeToggleCheckboxValue()"
2023-06-04 01:08:45 +03:00
/>
2023-05-21 17:59:36 +03:00
<span
2023-11-04 01:19:32 +02:00
v-if="field?.type && editable"
2024-12-08 22:34:52 +02:00
class="h-4 w-4 lg:h-2.5 lg:w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize"
2026-01-17 19:04:43 +02:00
:class="{ 'z-30': isInMultiSelection }"
2023-05-21 17:59:36 +03:00
@mousedown.stop="startResize"
2023-10-06 03:21:14 +03:00
@touchstart="startTouchResize"
2023-05-21 17:59:36 +03:00
/>
</div>
</template>
<script>
2023-06-04 01:08:45 +03:00
import FieldType from './field_type'
import Field from './field'
2026-02-12 09:22:08 +02:00
import AreaTitle from './area_title'
import { IconCheck } from '@tabler/icons-vue'
2023-06-03 00:53:46 +03:00
2023-05-21 17:59:36 +03:00
export default {
name: 'FieldArea',
2023-06-04 01:08:45 +03:00
components: {
2024-02-05 23:03:45 +02:00
IconCheck,
2026-02-12 09:22:08 +02:00
AreaTitle
2023-06-04 01:08:45 +03:00
},
2026-01-30 09:58:30 +02:00
inject: ['template', 'save', 't', 'isInlineSize', 'selectedAreasRef', 'isCmdKeyRef', 'getFieldTypeIndex'],
2023-05-21 17:59:36 +03:00
props: {
2023-06-04 01:08:45 +03:00
area: {
2023-05-21 17:59:36 +03:00
type: Object,
2023-06-04 01:08:45 +03:00
required: true
2023-05-21 17:59:36 +03:00
},
2024-07-16 19:01:44 +03:00
inputMode: {
type: Boolean,
required: false,
default: false
},
2026-04-21 19:54:18 +03:00
conditionalFieldIndex: {
type: Object,
required: false,
default: () => ({})
},
formulaValuesIndex: {
type: Object,
required: false,
default: () => ({})
},
2023-06-27 00:57:29 +03:00
isDraw: {
type: Boolean,
required: false,
default: false
},
2024-12-28 13:08:00 +02:00
maxPage: {
type: Number,
required: false,
default: null
},
2023-11-27 20:24:58 +02:00
defaultField: {
type: Object,
required: false,
default: null
},
2024-07-15 14:03:36 +03:00
withFieldPlaceholder: {
type: Boolean,
required: false,
default: false
},
2025-07-17 10:54:44 +03:00
withSignatureId: {
type: Boolean,
required: false,
default: null
},
2025-07-27 10:27:12 +03:00
withPrefillable: {
type: Boolean,
required: false,
default: false
},
2025-09-26 20:52:44 +03:00
pageWidth: {
type: Number,
required: false,
default: 0
},
pageHeight: {
type: Number,
required: false,
default: 0
},
2024-01-23 23:20:19 +02:00
defaultSubmitters: {
type: Array,
required: false,
default: () => []
},
2023-11-04 01:19:32 +02:00
editable: {
type: Boolean,
required: false,
default: true
},
2023-05-21 17:59:36 +03:00
field: {
type: Object,
required: false,
default: null
2026-01-17 19:04:43 +02:00
},
2026-01-30 16:17:49 +02:00
isMobile: {
type: Boolean,
required: false,
default: false
},
2026-01-17 19:04:43 +02:00
isSelectMode: {
type: Boolean,
required: false,
default: false
2023-05-21 17:59:36 +03:00
}
},
2026-01-26 11:11:19 +02:00
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field'],
2023-05-21 17:59:36 +03:00
data () {
return {
2024-06-12 20:05:55 +03:00
isContenteditable: false,
2023-05-21 17:59:36 +03:00
isResize: false,
2023-06-24 16:17:07 +03:00
isDragged: false,
2024-07-02 10:25:05 +03:00
isMoved: false,
2025-01-24 22:32:20 +02:00
isHeadingSelected: false,
2023-11-07 11:36:11 +02:00
textOverflowChars: 0,
2023-05-21 17:59:36 +03:00
dragFrom: { x: 0, y: 0 }
}
},
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,
2023-06-04 01:08:45 +03:00
fieldIcons: FieldType.computed.fieldIcons,
2026-04-21 19:54:18 +03:00
isConditionMatch () {
return !this.inputMode || this.conditionalFieldIndex[this.field.uuid] !== false
},
displayValue () {
if (this.field.preferences?.formula && this.field.type !== 'payment') {
const computed = this.formulaValuesIndex[this.field.uuid]
if (computed != null) {
return computed
}
}
return this.field.default_value
},
2025-09-26 20:52:44 +03:00
bgClasses () {
if (this.field.type === 'heading') {
return 'bg-gray-50'
} else if (this.field.type === 'strikethrough') {
return 'bg-transparent'
2026-04-21 19:54:18 +03:00
} else if (!this.isConditionMatch) {
return 'bg-gray-100'
2025-09-26 20:52:44 +03:00
} else {
return this.bgColors[this.submitterIndex % this.bgColors.length]
}
},
activeBorderClasses () {
if (this.field.type === 'heading') {
return ''
} else if (this.field.type === 'strikethrough') {
return 'border-dashed border-gray-300'
2026-04-21 19:54:18 +03:00
} else if (!this.isConditionMatch) {
return 'border-gray-300'
2025-09-26 20:52:44 +03:00
} else {
return this.borderColors[this.submitterIndex % this.borderColors.length]
}
},
2025-08-23 20:11:10 +03:00
isWFullType () {
2025-09-26 20:52:44 +03:00
return ['cells', 'checkbox', 'radio', 'multiple', 'select', 'strikethrough'].includes(this.field.type)
},
strikethroughWidth () {
if (this.isInlineSize) {
return '0.6cqmin'
} else {
return 'clamp(0px, 0.5vw, 6px)'
}
2025-08-23 20:11:10 +03:00
},
2025-07-14 12:53:15 +03:00
fontStyle () {
let fontSize = ''
if (this.isInlineSize) {
if (this.textOverflowChars) {
fontSize = `${this.fontSizePx / 1.5 / 10}cqmin`
} else {
fontSize = `${this.fontSizePx / 10}cqmin`
}
} else {
if (this.textOverflowChars) {
fontSize = `clamp(1pt, ${this.fontSizePx / 1.5 / 10}vw, ${this.fontSizePx / 1.5}px)`
} else {
fontSize = `clamp(1pt, ${this.fontSizePx / 10}vw, ${this.fontSizePx}px)`
}
}
return { fontSize, lineHeight: `calc(${fontSize} * ${this.lineHeight})` }
},
2025-08-23 20:11:10 +03:00
optionsUuidIndex () {
return this.field.options.reduce((acc, option) => {
acc[option.uuid] = option
return acc
}, {})
},
2025-07-14 12:53:15 +03:00
fontSizePx () {
return parseInt(this.field?.preferences?.font_size || 11) * this.fontScale
},
lineHeight () {
return 1.3
},
2025-09-26 20:52:44 +03:00
basePageWidth () {
return 1040.0
},
2025-07-14 12:53:15 +03:00
fontScale () {
2025-09-26 20:52:44 +03:00
return this.basePageWidth / 612.0
2025-07-14 12:53:15 +03:00
},
2024-06-12 20:05:55 +03:00
isDefaultValuePresent () {
2026-04-21 19:54:18 +03:00
return this.field?.default_value || this.field?.default_value === 0 || this.displayValue || this.displayValue === 0
2025-08-23 20:11:10 +03:00
},
isSelectInput () {
return this.inputMode && (this.field.type === 'select' || (this.field.type === 'radio' && this.field.areas?.length < 2))
},
isCheckboxInput () {
return this.inputMode && (this.field.type === 'checkbox' || (['radio', 'multiple'].includes(this.field.type) && this.area.option_uuid))
2024-06-12 20:05:55 +03:00
},
2024-07-11 21:40:15 +03:00
isValueInput () {
2026-04-21 19:54:18 +03:00
if (this.inputMode && this.field.preferences?.formula) return false
2025-08-23 20:11:10 +03:00
return (this.field.type === 'heading' && this.isHeadingSelected) || this.isContenteditable ||
(this.inputMode && (['text', 'number'].includes(this.field.type) || (this.field.type === 'date' && this.field.default_value !== '{{date}}')))
2024-07-11 21:40:15 +03:00
},
2024-02-24 01:01:36 +02:00
defaultName () {
2026-01-30 09:58:30 +02:00
return this.buildDefaultName(this.field)
2024-02-24 01:01:36 +02:00
},
2025-03-05 12:12:16 +02:00
fontClasses () {
if (!this.field.preferences) {
2025-01-24 23:08:44 +02:00
return { 'items-center': true }
2025-03-05 12:12:16 +02:00
}
2024-03-17 19:06:00 +02:00
return {
2025-01-24 23:08:44 +02:00
'items-center': !this.field.preferences.valign || this.field.preferences.valign === 'center',
'items-start': this.field.preferences.valign === 'top',
'items-end': this.field.preferences.valign === 'bottom',
2025-03-05 12:12:16 +02:00
'justify-center': this.field.preferences.align === 'center',
'justify-start': this.field.preferences.align === 'left',
'justify-end': this.field.preferences.align === 'right',
2025-07-14 12:53:15 +03:00
'font-courier': this.field.preferences.font === 'Courier',
'font-times': this.field.preferences.font === 'Times',
2025-03-05 12:12:16 +02:00
'font-bold': ['bold_italic', 'bold'].includes(this.field.preferences.font_type),
italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type)
2024-03-17 19:06:00 +02:00
}
},
2023-06-27 00:57:29 +03:00
cells () {
const cells = []
let currentWidth = 0
while (currentWidth + (this.area.cell_w + this.area.cell_w / 4) < this.area.w) {
currentWidth += this.area.cell_w || 9999999
cells.push(currentWidth)
}
return cells
},
2023-06-07 01:16:43 +03:00
submitter () {
return this.template.submitters.find((s) => s.uuid === this.field.submitter_uuid)
},
submitterIndex () {
return this.template.submitters.indexOf(this.submitter)
},
borderColors () {
return [
2024-02-08 11:54:54 +02:00
'border-red-500/80',
'border-sky-500/80',
'border-emerald-500/80',
'border-yellow-300/80',
'border-purple-600/80',
'border-pink-500/80',
'border-cyan-500/80',
2024-02-10 01:19:02 +02:00
'border-orange-500/80',
'border-lime-500/80',
'border-indigo-500/80'
2023-06-07 01:16:43 +03:00
]
},
bgColors () {
return [
2024-06-12 20:05:55 +03:00
'bg-red-100',
'bg-sky-100',
'bg-emerald-100',
'bg-yellow-100',
'bg-purple-100',
'bg-pink-100',
'bg-cyan-100',
'bg-orange-100',
'bg-lime-100',
'bg-indigo-100'
2023-06-07 01:16:43 +03:00
]
},
2023-06-04 01:08:45 +03:00
isSelected () {
2026-01-17 19:04:43 +02:00
return this.selectedAreasRef.value.includes(this.area)
},
isInMultiSelection () {
return this.selectedAreasRef.value.length >= 2 && this.isSelected
2023-06-03 00:53:46 +03:00
},
2023-05-21 17:59:36 +03:00
positionStyle () {
2023-06-04 01:08:45 +03:00
const { x, y, w, h } = this.area
2023-05-21 17:59:36 +03:00
return {
2023-05-23 00:22:17 +03:00
top: y * 100 + '%',
left: x * 100 + '%',
width: w * 100 + '%',
height: h * 100 + '%'
2023-05-21 17:59:36 +03:00
}
}
},
2023-11-07 11:36:11 +02:00
watch: {
'field.default_value' () {
this.$nextTick(() => {
2024-03-17 17:04:39 +02:00
if (['date', 'text', 'number'].includes(this.field.type) && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.field.default_value}`.length)) {
2025-07-14 12:53:15 +03:00
this.textOverflowChars = (this.$el.clientHeight + 1) < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
2024-03-15 22:46:07 +02:00
}
2023-11-07 11:36:11 +02:00
})
}
},
2024-03-15 22:46:07 +02:00
mounted () {
this.$nextTick(() => {
2024-03-17 17:04:39 +02:00
if (['date', 'text', 'number'].includes(this.field.type) && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.field.default_value}`.length)) {
2025-07-14 12:53:15 +03:00
this.textOverflowChars = (this.$el.clientHeight + 1) < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
2024-03-15 22:46:07 +02:00
}
})
},
2023-05-21 17:59:36 +03:00
methods: {
2024-02-24 01:01:36 +02:00
buildDefaultName: Field.methods.buildDefaultName,
2025-08-23 21:33:46 +03:00
buildAreaOptionValue (area) {
const option = this.optionsUuidIndex[area.option_uuid]
2026-01-17 19:04:43 +02:00
return option?.value || `${this.t('option')} ${this.field.options.indexOf(option) + 1}`
2025-08-23 21:33:46 +03:00
},
2024-06-27 12:40:10 +03:00
maybeToggleDefaultValue () {
2026-04-21 19:54:18 +03:00
if (!this.editable || this.isCmdKeyRef.value || this.field.preferences?.formula) {
2025-03-04 19:46:55 +02:00
return
}
2024-06-12 20:05:55 +03:00
if (['text', 'number'].includes(this.field.type)) {
this.isContenteditable = true
2025-01-24 22:32:20 +02:00
this.focusValueInput()
2024-06-27 12:40:10 +03:00
} else if (this.field.type === 'date') {
this.field.readonly = !this.field.readonly
this.field.default_value === '{{date}}' ? delete this.field.default_value : this.field.default_value = '{{date}}'
this.save()
2025-08-23 20:11:10 +03:00
} else {
this.maybeToggleCheckboxValue()
2024-06-12 20:05:55 +03:00
}
},
2024-07-16 19:01:44 +03:00
maybeToggleCheckboxValue () {
2025-08-23 20:11:10 +03:00
if (this.field.type === 'checkbox') {
2024-07-16 19:01:44 +03:00
this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true
2025-08-23 20:11:10 +03:00
this.field.readonly = this.field.default_value === true
this.save()
} else if (this.field.type === 'radio' && this.area.option_uuid) {
2025-08-23 21:33:46 +03:00
const value = this.buildAreaOptionValue(this.area)
2025-08-23 20:11:10 +03:00
this.field.default_value === value ? delete this.field.default_value : this.field.default_value = value
this.field.readonly = !!this.field.default_value?.length
this.save()
} else if (this.field.type === 'multiple' && this.area.option_uuid) {
2025-08-23 21:33:46 +03:00
const value = this.buildAreaOptionValue(this.area)
2025-08-23 20:11:10 +03:00
if (this.field.default_value?.includes(value)) {
this.field.default_value.splice(this.field.default_value.indexOf(value), 1)
if (!this.field.default_value?.length) delete this.field.default_value
} else {
Array.isArray(this.field.default_value) ? this.field.default_value.push(value) : this.field.default_value = [value]
}
this.field.readonly = !!this.field.default_value?.length
2024-07-16 19:01:44 +03:00
this.save()
}
},
focusValueInput (e) {
2026-04-21 19:54:18 +03:00
if (this.inputMode && this.field.type === 'number' && !this.isContenteditable && !this.field.preferences?.formula) {
this.isContenteditable = true
}
2025-01-24 22:32:20 +02:00
this.$nextTick(() => {
if (this.$refs.defaultValue && this.$refs.defaultValue !== document.activeElement) {
this.$refs.defaultValue.focus()
if (this.$refs.defaultValue.innerText.length && this.$refs.defaultValue !== e?.target) {
window.getSelection().collapse(
this.$refs.defaultValue.firstChild,
this.$refs.defaultValue.innerText.length
)
}
2024-07-11 21:40:15 +03:00
}
2025-01-24 22:32:20 +02:00
})
2024-07-11 21:40:15 +03:00
},
2024-05-12 11:12:07 +03:00
formatNumber (number, format) {
if (format === 'comma') {
return new Intl.NumberFormat('en-US').format(number)
2025-01-17 21:08:13 +02:00
} else if (format === 'usd') {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'gbp') {
return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'eur') {
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
2024-05-12 11:12:07 +03:00
} else if (format === 'dot') {
return new Intl.NumberFormat('de-DE').format(number)
} else if (format === 'space') {
return new Intl.NumberFormat('fr-FR').format(number)
} else {
return number
}
},
2023-06-27 00:57:29 +03:00
startResizeCell (e) {
2023-08-19 21:05:54 +03:00
this.$el.getRootNode().addEventListener('mousemove', this.onResizeCell)
this.$el.getRootNode().addEventListener('mouseup', this.stopResizeCell)
2023-06-27 00:57:29 +03:00
this.$emit('start-resize', 'ew')
},
stopResizeCell (e) {
2023-08-19 21:05:54 +03:00
this.$el.getRootNode().removeEventListener('mousemove', this.onResizeCell)
this.$el.getRootNode().removeEventListener('mouseup', this.stopResizeCell)
2023-06-27 00:57:29 +03:00
this.$emit('stop-resize')
this.save()
},
2024-06-13 10:26:36 +03:00
onPaste (e) {
2024-06-27 15:14:32 +03:00
const text = (e.clipboardData || window.clipboardData).getData('text/plain')
2024-07-18 10:58:12 +03:00
const selection = this.$el.getRootNode().getSelection()
2024-06-27 15:14:32 +03:00
if (selection.rangeCount) {
selection.deleteFromDocument()
selection.getRangeAt(0).insertNode(document.createTextNode(text))
selection.collapseToEnd()
}
2024-06-13 10:26:36 +03:00
},
2023-06-27 00:57:29 +03:00
onResizeCell (e) {
2023-06-30 23:22:57 +03:00
if (e.target.id === 'mask') {
2024-03-05 20:53:54 +02:00
const positionX = e.offsetX / (e.target.clientWidth - 1)
2023-06-27 00:57:29 +03:00
if (positionX > this.area.x) {
this.area.cell_w = positionX - this.area.x
}
}
},
2024-06-12 20:05:55 +03:00
onDefaultValueBlur (e) {
2026-04-21 19:54:18 +03:00
if (this.field.preferences?.formula) {
this.isContenteditable = false
return
}
2024-06-12 20:05:55 +03:00
const text = this.$refs.defaultValue.innerText.trim()
this.isContenteditable = false
2025-01-24 22:32:20 +02:00
this.isHeadingSelected = false
2024-06-12 20:05:55 +03:00
if (text) {
if (this.field.type === 'number') {
const number = parseFloat(text)
if (number || number === 0) {
this.field.default_value = parseFloat(text)
}
} else {
this.field.default_value = text
}
if (![true, false].includes(this.field.readonly)) {
this.field.readonly = true
}
this.$refs.defaultValue.innerText = text
} else {
delete this.field.readonly
delete this.field.default_value
this.$refs.defaultValue.innerText = ''
}
this.save()
},
onDefaultValueEnter (e) {
2024-07-11 21:40:15 +03:00
if (this.field.type !== 'heading') {
e.preventDefault()
this.$refs.defaultValue.blur()
}
2024-06-12 20:05:55 +03:00
},
2023-05-21 17:59:36 +03:00
resize (e) {
2023-06-30 23:22:57 +03:00
if (e.target.id === 'mask') {
2024-03-05 20:53:54 +02:00
this.area.w = e.offsetX / e.target.clientWidth - this.area.x
this.area.h = e.offsetY / e.target.clientHeight - this.area.y
2026-01-17 19:04:43 +02:00
if (this.isInMultiSelection) {
this.selectedAreasRef.value.forEach((area) => {
if (area !== this.area) {
area.w = this.area.w
area.h = this.area.h
}
})
}
2023-06-03 00:53:46 +03:00
}
2023-05-21 17:59:36 +03:00
},
drag (e) {
2025-08-23 20:11:10 +03:00
if (e.target.id === 'mask' && this.editable) {
2023-06-24 16:17:07 +03:00
this.isDragged = true
2024-03-05 20:53:54 +02:00
this.area.x = (e.offsetX - this.dragFrom.x) / e.target.clientWidth
this.area.y = (e.offsetY - this.dragFrom.y) / e.target.clientHeight
2023-05-21 17:59:36 +03:00
}
},
2023-10-06 03:21:14 +03:00
startTouchDrag (e) {
2024-07-11 21:40:15 +03:00
if (e.target !== this.$refs.touchTarget && e.target !== this.$refs.touchValueTarget) {
2023-10-06 03:21:14 +03:00
return
}
2024-06-13 10:54:07 +03:00
document.activeElement?.blur()
2023-10-06 03:21:14 +03:00
e.preventDefault()
2025-08-23 20:11:10 +03:00
if (this.editable) {
this.isDragged = true
}
2023-10-06 03:21:14 +03:00
const rect = e.target.getBoundingClientRect()
2026-01-17 19:04:43 +02:00
this.selectedAreasRef.value = [this.area]
2023-10-06 03:21:14 +03:00
this.dragFrom = { x: rect.left - e.touches[0].clientX, y: rect.top - e.touches[0].clientY }
this.$el.getRootNode().addEventListener('touchmove', this.touchDrag)
this.$el.getRootNode().addEventListener('touchend', this.stopTouchDrag)
this.$emit('start-drag')
},
touchDrag (e) {
2024-07-02 13:17:39 +03:00
if (!this.editable) {
return
}
2023-10-06 03:21:14 +03:00
const page = this.$parent.$refs.mask.previousSibling
const rect = page.getBoundingClientRect()
2024-06-12 20:05:55 +03:00
this.area.x = Math.min(Math.max((this.dragFrom.x + e.touches[0].clientX - rect.left) / rect.width, 0), 1 - this.area.w)
2024-12-28 13:08:00 +02:00
this.area.y = (this.dragFrom.y + e.touches[0].clientY - rect.top) / rect.height
if ((this.area.page === 0 && this.area.y < 0) || (this.area.page === this.maxPage && this.area.y > 1 - this.area.h)) {
this.area.y = Math.min(Math.max(this.area.y, 0), 1 - this.area.h)
}
2023-10-06 03:21:14 +03:00
},
stopTouchDrag () {
this.$el.getRootNode().removeEventListener('touchmove', this.touchDrag)
this.$el.getRootNode().removeEventListener('touchend', this.stopTouchDrag)
2024-12-28 13:08:00 +02:00
this.maybeChangeAreaPage(this.area)
2023-10-06 03:21:14 +03:00
if (this.isDragged) {
this.save()
}
this.isDragged = false
this.$emit('stop-drag')
},
2024-06-12 20:05:55 +03:00
startMouseMove (e) {
2024-07-11 21:40:15 +03:00
if (e.target !== this.$refs.touchTarget && e.target !== this.$refs.touchValueTarget) {
2024-06-12 20:05:55 +03:00
return
}
2024-07-11 21:40:15 +03:00
if (document.activeElement !== this.$refs.defaultValue) {
document.activeElement?.blur()
}
2024-06-12 20:05:55 +03:00
e.preventDefault()
2026-01-17 19:04:43 +02:00
if (e.metaKey || e.ctrlKey) {
if (!this.selectedAreasRef.value.includes(this.area)) {
this.selectedAreasRef.value.push(this.area)
} else {
this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(this.area), 1)
}
return
}
2025-08-23 20:11:10 +03:00
if (this.editable) {
this.isDragged = true
}
2024-06-12 20:05:55 +03:00
const rect = e.target.getBoundingClientRect()
2026-01-17 19:04:43 +02:00
this.selectedAreasRef.value = [this.area]
2024-06-12 20:05:55 +03:00
this.dragFrom = { x: rect.left - e.clientX, y: rect.top - e.clientY }
this.$el.getRootNode().addEventListener('mousemove', this.mouseMove)
this.$el.getRootNode().addEventListener('mouseup', this.stopMouseMove)
this.$emit('start-drag')
},
mouseMove (e) {
2024-07-02 13:17:39 +03:00
if (!this.editable) {
return
}
2024-07-02 10:25:05 +03:00
this.isMoved = true
2024-06-12 20:05:55 +03:00
const page = this.$parent.$refs.mask.previousSibling
const rect = page.getBoundingClientRect()
this.area.x = Math.min(Math.max((this.dragFrom.x + e.clientX - rect.left) / rect.width, 0), 1 - this.area.w)
2024-12-28 13:08:00 +02:00
this.area.y = (this.dragFrom.y + e.clientY - rect.top) / rect.height
if ((this.area.page === 0 && this.area.y < 0) || (this.area.page === this.maxPage && this.area.y > 1 - this.area.h)) {
this.area.y = Math.min(Math.max(this.area.y, 0), 1 - this.area.h)
}
2024-06-12 20:05:55 +03:00
},
stopMouseMove (e) {
this.$el.getRootNode().removeEventListener('mousemove', this.mouseMove)
this.$el.getRootNode().removeEventListener('mouseup', this.stopMouseMove)
2024-12-28 13:08:00 +02:00
this.maybeChangeAreaPage(this.area)
2024-07-02 10:25:05 +03:00
if (this.isMoved) {
2024-06-12 20:05:55 +03:00
this.save()
}
2025-01-24 22:32:20 +02:00
if (this.field.type === 'heading') {
this.isHeadingSelected = !this.isMoved
this.focusValueInput()
}
2024-06-12 20:05:55 +03:00
this.isDragged = false
2024-07-02 10:25:05 +03:00
this.isMoved = false
2024-06-12 20:05:55 +03:00
this.$emit('stop-drag')
},
2024-12-28 13:08:00 +02:00
maybeChangeAreaPage (area) {
if (area.y < -(area.h / 2)) {
area.page -= 1
area.y = 1 + area.y + (16.0 / this.$parent.$refs.mask.previousSibling.offsetHeight)
} else if (area.y > 1 - (area.h / 2)) {
area.page += 1
area.y = area.y - 1 - (16.0 / this.$parent.$refs.mask.previousSibling.offsetHeight)
}
},
2023-05-21 17:59:36 +03:00
stopDrag () {
2023-08-19 21:05:54 +03:00
this.$el.getRootNode().removeEventListener('mousemove', this.drag)
this.$el.getRootNode().removeEventListener('mouseup', this.stopDrag)
2023-05-21 17:59:36 +03:00
2023-06-24 16:17:07 +03:00
if (this.isDragged) {
this.save()
}
this.isDragged = false
2023-05-21 17:59:36 +03:00
this.$emit('stop-drag')
},
startResize () {
2026-01-17 19:04:43 +02:00
if (!this.selectedAreasRef.value.includes(this.area)) {
this.selectedAreasRef.value = [this.area]
}
2023-06-04 01:08:45 +03:00
2023-08-19 21:05:54 +03:00
this.$el.getRootNode().addEventListener('mousemove', this.resize)
this.$el.getRootNode().addEventListener('mouseup', this.stopResize)
2023-05-21 17:59:36 +03:00
2023-06-27 00:57:29 +03:00
this.$emit('start-resize', 'nwse')
2023-05-21 17:59:36 +03:00
},
stopResize () {
2023-08-19 21:05:54 +03:00
this.$el.getRootNode().removeEventListener('mousemove', this.resize)
this.$el.getRootNode().removeEventListener('mouseup', this.stopResize)
2023-05-21 17:59:36 +03:00
this.$emit('stop-resize')
2023-06-24 16:17:07 +03:00
2023-10-06 03:21:14 +03:00
this.save()
},
startTouchResize (e) {
2026-01-17 19:04:43 +02:00
if (!this.selectedAreasRef.value.includes(this.area)) {
this.selectedAreasRef.value = [this.area]
}
2023-10-06 03:21:14 +03:00
2026-02-12 09:22:08 +02:00
this.$refs?.title?.$refs?.name?.blur()
2023-10-06 03:21:14 +03:00
e.preventDefault()
this.$el.getRootNode().addEventListener('touchmove', this.touchResize)
this.$el.getRootNode().addEventListener('touchend', this.stopTouchResize)
this.$emit('start-resize', 'nwse')
},
touchResize (e) {
const page = this.$parent.$refs.mask.previousSibling
const rect = page.getBoundingClientRect()
this.area.w = (e.touches[0].clientX - rect.left) / rect.width - this.area.x
this.area.h = (e.touches[0].clientY - rect.top) / rect.height - this.area.y
2026-01-17 19:04:43 +02:00
if (this.isInMultiSelection) {
this.selectedAreasRef.value.forEach((area) => {
if (area !== this.area) {
area.w = this.area.w
area.h = this.area.h
}
})
}
2023-10-06 03:21:14 +03:00
},
stopTouchResize () {
this.$el.getRootNode().removeEventListener('touchmove', this.touchResize)
this.$el.getRootNode().removeEventListener('touchend', this.stopTouchResize)
this.$emit('stop-resize')
2023-06-24 16:17:07 +03:00
this.save()
2023-05-21 17:59:36 +03:00
}
}
}
</script>