mirror of https://github.com/docusealco/docuseal
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1103 lines
35 KiB
1103 lines
35 KiB
<template>
|
|
<div
|
|
class="absolute overflow-visible group field-area-container"
|
|
:style="positionStyle"
|
|
:class="{ 'z-[1]': isMoved || isDragged }"
|
|
@pointerdown.stop
|
|
@mousedown="startMouseMove"
|
|
@touchstart="startTouchDrag"
|
|
>
|
|
<div
|
|
v-if="isSelected || isDraw"
|
|
class="top-0 bottom-0 right-0 left-0 absolute border border-1.5 pointer-events-none"
|
|
:class="activeBorderClasses"
|
|
/>
|
|
<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"
|
|
:class="field.type === 'heading' ? '' : borderColors[submitterIndex % borderColors.length]"
|
|
:style="{ left: (cellW / area.w * 100) + '%' }"
|
|
>
|
|
<span
|
|
v-if="index === 0 && editable"
|
|
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>
|
|
<div
|
|
v-if="field?.type && (isSelected || isNameFocus)"
|
|
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap flex z-10 field-area-controls"
|
|
style="top: -25px; height: 25px"
|
|
@mousedown.stop
|
|
@pointerdown.stop
|
|
>
|
|
<FieldSubmitter
|
|
v-if="field.type != 'heading' && field.type != 'strikethrough'"
|
|
v-model="field.submitter_uuid"
|
|
class="border-r roles-dropdown"
|
|
:compact="true"
|
|
:editable="editable && (!defaultField || defaultField.role !== submitter?.name)"
|
|
:allow-add-new="!defaultSubmitters.length"
|
|
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px] mt-[1px]'"
|
|
:submitters="template.submitters"
|
|
@update:model-value="save"
|
|
@click="selectedAreaRef.value = area"
|
|
/>
|
|
<FieldType
|
|
v-model="field.type"
|
|
:button-width="27"
|
|
:editable="editable && !defaultField"
|
|
:button-classes="'px-1'"
|
|
:menu-classes="'bg-white rounded-t-none'"
|
|
@update:model-value="[maybeUpdateOptions(), save()]"
|
|
@click="selectedAreaRef.value = area"
|
|
/>
|
|
<span
|
|
v-if="field.type !== 'checkbox' || field.name"
|
|
ref="name"
|
|
:contenteditable="editable && !defaultField && field.type !== 'heading'"
|
|
dir="auto"
|
|
class="pr-1 cursor-text outline-none block"
|
|
style="min-width: 2px"
|
|
@paste.prevent="onPaste"
|
|
@keydown.enter.prevent="onNameEnter"
|
|
@focus="onNameFocus"
|
|
@blur="onNameBlur"
|
|
>{{ optionIndexText }} {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || defaultName }}</span>
|
|
<div
|
|
v-if="isSettingsFocus || isSelectInput || (isValueInput && field.type !== 'heading') || (isNameFocus && !['checkbox', 'phone'].includes(field.type))"
|
|
class="flex items-center ml-1.5"
|
|
>
|
|
<input
|
|
v-if="!isValueInput && !isSelectInput"
|
|
:id="`required-checkbox-${field.uuid}`"
|
|
v-model="field.required"
|
|
type="checkbox"
|
|
class="checkbox checkbox-xs no-animation rounded"
|
|
@mousedown.prevent
|
|
>
|
|
<label
|
|
v-if="!isValueInput && !isSelectInput"
|
|
:for="`required-checkbox-${field.uuid}`"
|
|
class="label text-xs"
|
|
@click.prevent="field.required = !field.required"
|
|
@mousedown.prevent
|
|
>{{ t('required') }}</label>
|
|
<input
|
|
v-if="isValueInput || isSelectInput"
|
|
:id="`readonly-checkbox-${field.uuid}`"
|
|
type="checkbox"
|
|
class="checkbox checkbox-xs no-animation rounded"
|
|
:checked="!(field.readonly ?? true)"
|
|
@change="field.readonly = !(field.readonly ?? true)"
|
|
@mousedown.prevent
|
|
>
|
|
<label
|
|
v-if="isValueInput || isSelectInput"
|
|
:for="`readonly-checkbox-${field.uuid}`"
|
|
class="label text-xs"
|
|
@click.prevent="field.readonly = !(field.readonly ?? true)"
|
|
@mousedown.prevent
|
|
>{{ t('editable') }}</label>
|
|
<span
|
|
v-if="field.type !== 'payment' && !isValueInput"
|
|
class="dropdown dropdown-end field-area-settings-dropdown"
|
|
@mouseenter="renderDropdown = true"
|
|
@touchstart="renderDropdown = true"
|
|
>
|
|
<label
|
|
ref="settingsButton"
|
|
tabindex="0"
|
|
:title="t('settings')"
|
|
class="cursor-pointer flex items-center"
|
|
style="height: 25px"
|
|
@focus="isSettingsFocus = true"
|
|
@blur="maybeBlurSettings"
|
|
>
|
|
<IconDotsVertical class="w-5 h-5" />
|
|
</label>
|
|
<ul
|
|
v-if="renderDropdown"
|
|
ref="settingsDropdown"
|
|
tabindex="0"
|
|
class="dropdown-content menu menu-xs px-2 pb-2 pt-1 shadow rounded-box w-52 z-10 rounded-t-none"
|
|
:style="{ backgroundColor: 'white' }"
|
|
@dragstart.prevent.stop
|
|
@click="closeDropdown"
|
|
@focusout="maybeBlurSettings"
|
|
>
|
|
<FieldSettings
|
|
:field="field"
|
|
:default-field="defaultField"
|
|
:editable="editable"
|
|
:background-color="'white'"
|
|
:with-required="false"
|
|
:with-areas="false"
|
|
:with-signature-id="withSignatureId"
|
|
:with-prefillable="withPrefillable"
|
|
@click-formula="isShowFormulaModal = true"
|
|
@click-font="isShowFontModal = true"
|
|
@click-description="isShowDescriptionModal = true"
|
|
@click-condition="isShowConditionsModal = true"
|
|
@scroll-to="[selectedAreaRef.value = $event, $emit('scroll-to', $event)]"
|
|
/>
|
|
</ul>
|
|
</span>
|
|
</div>
|
|
<button
|
|
v-else-if="editable"
|
|
class="pr-1"
|
|
:title="t('remove')"
|
|
@click.prevent="$emit('remove')"
|
|
>
|
|
<IconX width="14" />
|
|
</button>
|
|
</div>
|
|
<div
|
|
ref="touchValueTarget"
|
|
class="flex h-full w-full field-area"
|
|
dir="auto"
|
|
:class="[isValueInput ? 'cursor-text' : '', isValueInput || isCheckboxInput || isSelectInput ? 'bg-opacity-50' : 'bg-opacity-80', bgClasses, isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center items-center']"
|
|
@click="focusValueInput"
|
|
>
|
|
<span
|
|
v-if="field"
|
|
class="flex justify-center items-center space-x-1"
|
|
:class="{ 'w-full': isWFullType, 'h-full': !isValueInput && (!isDefaultValuePresent || field.type === 'strikethrough') }"
|
|
>
|
|
<div
|
|
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')"
|
|
:class="{ 'w-full h-full': isWFullType }"
|
|
:style="fontStyle"
|
|
>
|
|
<div
|
|
ref="textContainer"
|
|
class="flex items-center px-0.5"
|
|
:style="{ color: field.preferences?.color }"
|
|
:class="{ 'w-full h-full': isWFullType }"
|
|
>
|
|
<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 }"
|
|
/>
|
|
<template
|
|
v-else-if="(field.type === 'radio' || field.type === 'multiple') && field?.areas?.length > 1"
|
|
>
|
|
<IconCheck
|
|
v-if="field.type === 'multiple' ? field.default_value.includes(buildAreaOptionValue(area)) : buildAreaOptionValue(area) === field.default_value"
|
|
class="aspect-square mx-auto"
|
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
|
/>
|
|
</template>
|
|
<span
|
|
v-else-if="field.type === 'number' && !isValueInput && (field.default_value || field.default_value == 0)"
|
|
class="whitespace-pre-wrap"
|
|
>{{ formatNumber(field.default_value, field.preferences?.format) }}</span>
|
|
<span
|
|
v-else-if="field.default_value === '{{date}}'"
|
|
>
|
|
{{ t('signing_date') }}
|
|
</span>
|
|
<div
|
|
v-else-if="field.type === 'cells' && field.default_value"
|
|
class="w-full flex items-center"
|
|
>
|
|
<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>
|
|
<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()]"
|
|
@focus="selectedAreaRef.value = area"
|
|
@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>
|
|
<span
|
|
v-else
|
|
ref="defaultValue"
|
|
:contenteditable="isValueInput"
|
|
class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-base-content/30"
|
|
:class="{ 'cursor-text': isValueInput }"
|
|
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : (field.type === 'date' ? field.preferences?.format || t('type_value') : t('type_value'))"
|
|
@blur="onDefaultValueBlur"
|
|
@focus="selectedAreaRef.value = area"
|
|
@paste.prevent="onPaste"
|
|
@keydown.enter="onDefaultValueEnter"
|
|
>{{ field.default_value }}</span>
|
|
</div>
|
|
</div>
|
|
<component
|
|
:is="fieldIcons[field.type]"
|
|
v-else-if="!isCheckboxInput"
|
|
width="100%"
|
|
height="100%"
|
|
class="max-h-10 opacity-50"
|
|
/>
|
|
</span>
|
|
</div>
|
|
<div
|
|
v-if="!isValueInput && !isSelectInput"
|
|
ref="touchTarget"
|
|
class="absolute top-0 bottom-0 right-0 left-0"
|
|
:class="isDragged ? 'cursor-grab' : 'cursor-pointer'"
|
|
@dblclick="maybeToggleDefaultValue"
|
|
@click="inputMode && maybeToggleCheckboxValue()"
|
|
/>
|
|
<span
|
|
v-if="field?.type && editable"
|
|
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"
|
|
@mousedown.stop="startResize"
|
|
@touchstart="startTouchResize"
|
|
/>
|
|
<Teleport
|
|
v-if="isShowFormulaModal"
|
|
:to="modalContainerEl"
|
|
>
|
|
<FormulaModal
|
|
:field="field"
|
|
:editable="editable && !defaultField"
|
|
:default-field="defaultField"
|
|
:build-default-name="buildDefaultName"
|
|
@close="isShowFormulaModal = false"
|
|
/>
|
|
</Teleport>
|
|
<Teleport
|
|
v-if="isShowFontModal"
|
|
:to="modalContainerEl"
|
|
>
|
|
<FontModal
|
|
:field="field"
|
|
:editable="editable && !defaultField"
|
|
:default-field="defaultField"
|
|
:build-default-name="buildDefaultName"
|
|
@close="isShowFontModal = false"
|
|
/>
|
|
</Teleport>
|
|
<Teleport
|
|
v-if="isShowConditionsModal"
|
|
:to="modalContainerEl"
|
|
>
|
|
<ConditionsModal
|
|
:item="field"
|
|
:build-default-name="buildDefaultName"
|
|
:default-field="defaultField"
|
|
@close="isShowConditionsModal = false"
|
|
/>
|
|
</Teleport>
|
|
<Teleport
|
|
v-if="isShowDescriptionModal"
|
|
:to="modalContainerEl"
|
|
>
|
|
<DescriptionModal
|
|
:field="field"
|
|
:editable="editable && !defaultField"
|
|
:default-field="defaultField"
|
|
:build-default-name="buildDefaultName"
|
|
@close="isShowDescriptionModal = false"
|
|
/>
|
|
</Teleport>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import FieldSubmitter from './field_submitter'
|
|
import FieldType from './field_type'
|
|
import Field from './field'
|
|
import FieldSettings from './field_settings'
|
|
import FormulaModal from './formula_modal'
|
|
import FontModal from './font_modal'
|
|
import ConditionsModal from './conditions_modal'
|
|
import DescriptionModal from './description_modal'
|
|
import { IconX, IconCheck, IconDotsVertical } from '@tabler/icons-vue'
|
|
import { v4 } from 'uuid'
|
|
|
|
export default {
|
|
name: 'FieldArea',
|
|
components: {
|
|
FieldType,
|
|
IconCheck,
|
|
FieldSettings,
|
|
FormulaModal,
|
|
FontModal,
|
|
IconDotsVertical,
|
|
DescriptionModal,
|
|
ConditionsModal,
|
|
FieldSubmitter,
|
|
IconX
|
|
},
|
|
inject: ['template', 'selectedAreaRef', 'save', 't', 'isInlineSize'],
|
|
props: {
|
|
area: {
|
|
type: Object,
|
|
required: true
|
|
},
|
|
inputMode: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
},
|
|
isDraw: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
},
|
|
maxPage: {
|
|
type: Number,
|
|
required: false,
|
|
default: null
|
|
},
|
|
defaultField: {
|
|
type: Object,
|
|
required: false,
|
|
default: null
|
|
},
|
|
withFieldPlaceholder: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
},
|
|
withSignatureId: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: null
|
|
},
|
|
withPrefillable: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
},
|
|
pageWidth: {
|
|
type: Number,
|
|
required: false,
|
|
default: 0
|
|
},
|
|
pageHeight: {
|
|
type: Number,
|
|
required: false,
|
|
default: 0
|
|
},
|
|
defaultSubmitters: {
|
|
type: Array,
|
|
required: false,
|
|
default: () => []
|
|
},
|
|
editable: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: true
|
|
},
|
|
field: {
|
|
type: Object,
|
|
required: false,
|
|
default: null
|
|
}
|
|
},
|
|
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to'],
|
|
data () {
|
|
return {
|
|
isShowFormulaModal: false,
|
|
isShowFontModal: false,
|
|
isShowConditionsModal: false,
|
|
isContenteditable: false,
|
|
isSettingsFocus: false,
|
|
isShowDescriptionModal: false,
|
|
isResize: false,
|
|
isDragged: false,
|
|
isMoved: false,
|
|
renderDropdown: false,
|
|
isNameFocus: false,
|
|
isHeadingSelected: false,
|
|
textOverflowChars: 0,
|
|
dragFrom: { x: 0, y: 0 }
|
|
}
|
|
},
|
|
computed: {
|
|
fieldNames: FieldType.computed.fieldNames,
|
|
fieldLabels: FieldType.computed.fieldLabels,
|
|
fieldIcons: FieldType.computed.fieldIcons,
|
|
bgClasses () {
|
|
if (this.field.type === 'heading') {
|
|
return 'bg-gray-50'
|
|
} else if (this.field.type === 'strikethrough') {
|
|
return 'bg-transparent'
|
|
} 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'
|
|
} else {
|
|
return this.borderColors[this.submitterIndex % this.borderColors.length]
|
|
}
|
|
},
|
|
isWFullType () {
|
|
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)'
|
|
}
|
|
},
|
|
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})` }
|
|
},
|
|
optionsUuidIndex () {
|
|
return this.field.options.reduce((acc, option) => {
|
|
acc[option.uuid] = option
|
|
|
|
return acc
|
|
}, {})
|
|
},
|
|
fontSizePx () {
|
|
return parseInt(this.field?.preferences?.font_size || 11) * this.fontScale
|
|
},
|
|
lineHeight () {
|
|
return 1.3
|
|
},
|
|
basePageWidth () {
|
|
return 1040.0
|
|
},
|
|
fontScale () {
|
|
return this.basePageWidth / 612.0
|
|
},
|
|
isDefaultValuePresent () {
|
|
return this.field?.default_value || this.field?.default_value === 0
|
|
},
|
|
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))
|
|
},
|
|
isValueInput () {
|
|
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}}')))
|
|
},
|
|
modalContainerEl () {
|
|
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
|
},
|
|
defaultName () {
|
|
return this.buildDefaultName(this.field, this.template.fields)
|
|
},
|
|
fontClasses () {
|
|
if (!this.field.preferences) {
|
|
return { 'items-center': true }
|
|
}
|
|
|
|
return {
|
|
'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',
|
|
'justify-center': this.field.preferences.align === 'center',
|
|
'justify-start': this.field.preferences.align === 'left',
|
|
'justify-end': this.field.preferences.align === 'right',
|
|
'font-courier': this.field.preferences.font === 'Courier',
|
|
'font-times': this.field.preferences.font === 'Times',
|
|
'font-bold': ['bold_italic', 'bold'].includes(this.field.preferences.font_type),
|
|
italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type)
|
|
}
|
|
},
|
|
optionIndexText () {
|
|
if (this.area.option_uuid && this.field.options) {
|
|
return `${this.field.options.findIndex((o) => o.uuid === this.area.option_uuid) + 1}.`
|
|
} else {
|
|
return ''
|
|
}
|
|
},
|
|
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
|
|
},
|
|
submitter () {
|
|
return this.template.submitters.find((s) => s.uuid === this.field.submitter_uuid)
|
|
},
|
|
submitterIndex () {
|
|
return this.template.submitters.indexOf(this.submitter)
|
|
},
|
|
borderColors () {
|
|
return [
|
|
'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',
|
|
'border-orange-500/80',
|
|
'border-lime-500/80',
|
|
'border-indigo-500/80'
|
|
]
|
|
},
|
|
bgColors () {
|
|
return [
|
|
'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'
|
|
]
|
|
},
|
|
isSelected () {
|
|
return this.selectedAreaRef.value === this.area
|
|
},
|
|
positionStyle () {
|
|
const { x, y, w, h } = this.area
|
|
|
|
return {
|
|
top: y * 100 + '%',
|
|
left: x * 100 + '%',
|
|
width: w * 100 + '%',
|
|
height: h * 100 + '%'
|
|
}
|
|
}
|
|
},
|
|
watch: {
|
|
'field.default_value' () {
|
|
this.$nextTick(() => {
|
|
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)) {
|
|
this.textOverflowChars = (this.$el.clientHeight + 1) < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
|
|
}
|
|
})
|
|
}
|
|
},
|
|
mounted () {
|
|
this.$nextTick(() => {
|
|
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)) {
|
|
this.textOverflowChars = (this.$el.clientHeight + 1) < this.$refs.textContainer.clientHeight ? `${this.field.default_value}`.length : 0
|
|
}
|
|
})
|
|
},
|
|
methods: {
|
|
buildDefaultName: Field.methods.buildDefaultName,
|
|
closeDropdown () {
|
|
this.$el.getRootNode().activeElement.blur()
|
|
},
|
|
buildAreaOptionValue (area) {
|
|
const option = this.optionsUuidIndex[area.option_uuid]
|
|
|
|
return option.value || `${this.t('option')} ${this.field.options.indexOf(option) + 1}`
|
|
},
|
|
maybeToggleDefaultValue () {
|
|
if (!this.editable) {
|
|
return
|
|
}
|
|
|
|
if (['text', 'number'].includes(this.field.type)) {
|
|
this.isContenteditable = true
|
|
|
|
this.focusValueInput()
|
|
} 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()
|
|
} else {
|
|
this.maybeToggleCheckboxValue()
|
|
}
|
|
},
|
|
maybeToggleCheckboxValue () {
|
|
if (this.field.type === 'checkbox') {
|
|
this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true
|
|
this.field.readonly = this.field.default_value === true
|
|
|
|
this.save()
|
|
} else if (this.field.type === 'radio' && this.area.option_uuid) {
|
|
const value = this.buildAreaOptionValue(this.area)
|
|
|
|
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) {
|
|
const value = this.buildAreaOptionValue(this.area)
|
|
|
|
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
|
|
|
|
this.save()
|
|
}
|
|
},
|
|
focusValueInput (e) {
|
|
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
|
|
)
|
|
}
|
|
}
|
|
})
|
|
},
|
|
formatNumber (number, format) {
|
|
if (format === 'comma') {
|
|
return new Intl.NumberFormat('en-US').format(number)
|
|
} 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)
|
|
} 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
|
|
}
|
|
},
|
|
maybeBlurSettings (e) {
|
|
if (!e.relatedTarget || !this.$refs.settingsDropdown.contains(e.relatedTarget)) {
|
|
this.isSettingsFocus = false
|
|
}
|
|
},
|
|
onNameFocus (e) {
|
|
this.selectedAreaRef.value = this.area
|
|
|
|
this.isNameFocus = true
|
|
this.$refs.name.style.minWidth = this.$refs.name.clientWidth + 'px'
|
|
|
|
if (!this.field.name) {
|
|
setTimeout(() => {
|
|
this.$refs.name.innerText = ' '
|
|
}, 1)
|
|
}
|
|
},
|
|
startResizeCell (e) {
|
|
this.$el.getRootNode().addEventListener('mousemove', this.onResizeCell)
|
|
this.$el.getRootNode().addEventListener('mouseup', this.stopResizeCell)
|
|
|
|
this.$emit('start-resize', 'ew')
|
|
},
|
|
stopResizeCell (e) {
|
|
this.$el.getRootNode().removeEventListener('mousemove', this.onResizeCell)
|
|
this.$el.getRootNode().removeEventListener('mouseup', this.stopResizeCell)
|
|
|
|
this.$emit('stop-resize')
|
|
|
|
this.save()
|
|
},
|
|
onPaste (e) {
|
|
const text = (e.clipboardData || window.clipboardData).getData('text/plain')
|
|
|
|
const selection = this.$el.getRootNode().getSelection()
|
|
|
|
if (selection.rangeCount) {
|
|
selection.deleteFromDocument()
|
|
selection.getRangeAt(0).insertNode(document.createTextNode(text))
|
|
selection.collapseToEnd()
|
|
}
|
|
},
|
|
onResizeCell (e) {
|
|
if (e.target.id === 'mask') {
|
|
const positionX = e.offsetX / (e.target.clientWidth - 1)
|
|
|
|
if (positionX > this.area.x) {
|
|
this.area.cell_w = positionX - this.area.x
|
|
}
|
|
}
|
|
},
|
|
maybeUpdateOptions () {
|
|
delete this.field.default_value
|
|
|
|
if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
|
|
delete this.field.options
|
|
}
|
|
|
|
if (this.field.type === 'heading') {
|
|
this.field.readonly = true
|
|
}
|
|
|
|
if (this.field.type === 'strikethrough') {
|
|
this.field.readonly = true
|
|
this.field.default_value = true
|
|
}
|
|
|
|
if (['select', 'multiple', 'radio'].includes(this.field.type)) {
|
|
this.field.options ||= [{ value: '', uuid: v4() }]
|
|
}
|
|
|
|
(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
|
|
}
|
|
})
|
|
},
|
|
onNameBlur (e) {
|
|
if (e.relatedTarget === this.$refs.settingsButton) {
|
|
this.isSettingsFocus = true
|
|
}
|
|
|
|
const text = this.$refs.name.innerText.trim()
|
|
|
|
this.isNameFocus = false
|
|
this.$refs.name.style.minWidth = ''
|
|
|
|
if (text) {
|
|
this.field.name = text
|
|
} else {
|
|
this.field.name = ''
|
|
this.$refs.name.innerText = this.defaultName
|
|
}
|
|
|
|
this.save()
|
|
},
|
|
onDefaultValueBlur (e) {
|
|
const text = this.$refs.defaultValue.innerText.trim()
|
|
|
|
this.isContenteditable = false
|
|
this.isHeadingSelected = false
|
|
|
|
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) {
|
|
if (this.field.type !== 'heading') {
|
|
e.preventDefault()
|
|
|
|
this.$refs.defaultValue.blur()
|
|
}
|
|
},
|
|
onNameEnter (e) {
|
|
this.$refs.name.blur()
|
|
},
|
|
resize (e) {
|
|
if (e.target.id === 'mask') {
|
|
this.area.w = e.offsetX / e.target.clientWidth - this.area.x
|
|
this.area.h = e.offsetY / e.target.clientHeight - this.area.y
|
|
}
|
|
},
|
|
drag (e) {
|
|
if (e.target.id === 'mask' && this.editable) {
|
|
this.isDragged = true
|
|
|
|
this.area.x = (e.offsetX - this.dragFrom.x) / e.target.clientWidth
|
|
this.area.y = (e.offsetY - this.dragFrom.y) / e.target.clientHeight
|
|
}
|
|
},
|
|
startTouchDrag (e) {
|
|
if (e.target !== this.$refs.touchTarget && e.target !== this.$refs.touchValueTarget) {
|
|
return
|
|
}
|
|
|
|
document.activeElement?.blur()
|
|
|
|
e.preventDefault()
|
|
|
|
if (this.editable) {
|
|
this.isDragged = true
|
|
}
|
|
|
|
const rect = e.target.getBoundingClientRect()
|
|
|
|
this.selectedAreaRef.value = this.area
|
|
|
|
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) {
|
|
if (!this.editable) {
|
|
return
|
|
}
|
|
|
|
const page = this.$parent.$refs.mask.previousSibling
|
|
const rect = page.getBoundingClientRect()
|
|
|
|
this.area.x = Math.min(Math.max((this.dragFrom.x + e.touches[0].clientX - rect.left) / rect.width, 0), 1 - this.area.w)
|
|
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)
|
|
}
|
|
},
|
|
stopTouchDrag () {
|
|
this.$el.getRootNode().removeEventListener('touchmove', this.touchDrag)
|
|
this.$el.getRootNode().removeEventListener('touchend', this.stopTouchDrag)
|
|
|
|
this.maybeChangeAreaPage(this.area)
|
|
|
|
if (this.isDragged) {
|
|
this.save()
|
|
}
|
|
|
|
this.isDragged = false
|
|
|
|
this.$emit('stop-drag')
|
|
},
|
|
startMouseMove (e) {
|
|
if (e.target !== this.$refs.touchTarget && e.target !== this.$refs.touchValueTarget) {
|
|
return
|
|
}
|
|
|
|
if (document.activeElement !== this.$refs.defaultValue) {
|
|
document.activeElement?.blur()
|
|
}
|
|
|
|
e.preventDefault()
|
|
|
|
if (this.editable) {
|
|
this.isDragged = true
|
|
}
|
|
|
|
const rect = e.target.getBoundingClientRect()
|
|
|
|
this.selectedAreaRef.value = this.area
|
|
|
|
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) {
|
|
if (!this.editable) {
|
|
return
|
|
}
|
|
|
|
this.isMoved = true
|
|
|
|
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)
|
|
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)
|
|
}
|
|
},
|
|
stopMouseMove (e) {
|
|
this.$el.getRootNode().removeEventListener('mousemove', this.mouseMove)
|
|
this.$el.getRootNode().removeEventListener('mouseup', this.stopMouseMove)
|
|
|
|
this.maybeChangeAreaPage(this.area)
|
|
|
|
if (this.isMoved) {
|
|
this.save()
|
|
}
|
|
|
|
if (this.field.type === 'heading') {
|
|
this.isHeadingSelected = !this.isMoved
|
|
|
|
this.focusValueInput()
|
|
}
|
|
|
|
this.isDragged = false
|
|
this.isMoved = false
|
|
|
|
this.$emit('stop-drag')
|
|
},
|
|
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)
|
|
}
|
|
},
|
|
stopDrag () {
|
|
this.$el.getRootNode().removeEventListener('mousemove', this.drag)
|
|
this.$el.getRootNode().removeEventListener('mouseup', this.stopDrag)
|
|
|
|
if (this.isDragged) {
|
|
this.save()
|
|
}
|
|
|
|
this.isDragged = false
|
|
|
|
this.$emit('stop-drag')
|
|
},
|
|
startResize () {
|
|
this.selectedAreaRef.value = this.area
|
|
|
|
this.$el.getRootNode().addEventListener('mousemove', this.resize)
|
|
this.$el.getRootNode().addEventListener('mouseup', this.stopResize)
|
|
|
|
this.$emit('start-resize', 'nwse')
|
|
},
|
|
stopResize () {
|
|
this.$el.getRootNode().removeEventListener('mousemove', this.resize)
|
|
this.$el.getRootNode().removeEventListener('mouseup', this.stopResize)
|
|
|
|
this.$emit('stop-resize')
|
|
|
|
this.save()
|
|
},
|
|
startTouchResize (e) {
|
|
this.selectedAreaRef.value = this.area
|
|
|
|
this.$refs?.name?.blur()
|
|
|
|
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
|
|
},
|
|
stopTouchResize () {
|
|
this.$el.getRootNode().removeEventListener('touchmove', this.touchResize)
|
|
this.$el.getRootNode().removeEventListener('touchend', this.stopTouchResize)
|
|
|
|
this.$emit('stop-resize')
|
|
|
|
this.save()
|
|
}
|
|
}
|
|
}
|
|
</script>
|