mirror of https://github.com/docusealco/docuseal
commit
6ab68cc36b
@ -1,546 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
v-if="!isShowFormulaModal && !isShowFontModal && !isShowConditionsModal && !isShowDescriptionModal"
|
|
||||||
ref="menu"
|
|
||||||
class="fixed z-50 p-1 bg-white shadow-lg rounded-lg border border-base-300 min-w-[170px] cursor-default"
|
|
||||||
:style="menuStyle"
|
|
||||||
@mousedown.stop
|
|
||||||
@pointerdown.stop
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
v-if="showRequired"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm cursor-pointer"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
:checked="isRequired"
|
|
||||||
type="checkbox"
|
|
||||||
class="toggle toggle-xs"
|
|
||||||
@change="handleToggleRequired($event.target.checked)"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<span>{{ t('required') }}</span>
|
|
||||||
</label>
|
|
||||||
<label
|
|
||||||
v-if="showReadOnly"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm cursor-pointer"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
:checked="isReadOnly"
|
|
||||||
type="checkbox"
|
|
||||||
class="toggle toggle-xs"
|
|
||||||
@change="handleToggleReadOnly($event.target.checked)"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<span>{{ t('read_only') }}</span>
|
|
||||||
</label>
|
|
||||||
<hr
|
|
||||||
v-if="(showRequired || showReadOnly) && (showFont || showDescription || showCondition || showFormula)"
|
|
||||||
class="my-1 border-base-300"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-if="showFont && !isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="openFontModal"
|
|
||||||
>
|
|
||||||
<IconTypography class="w-4 h-4" />
|
|
||||||
<span>{{ t('font') }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="showDescription"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="openDescriptionModal"
|
|
||||||
>
|
|
||||||
<IconInfoCircle class="w-4 h-4" />
|
|
||||||
<span>{{ t('description') }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="showCondition && !isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="openConditionModal"
|
|
||||||
>
|
|
||||||
<IconRouteAltLeft class="w-4 h-4" />
|
|
||||||
<span>{{ t('condition') }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="showFormula"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="openFormulaModal"
|
|
||||||
>
|
|
||||||
<IconMathFunction class="w-4 h-4" />
|
|
||||||
<span>{{ t('formula') }}</span>
|
|
||||||
</button>
|
|
||||||
<hr
|
|
||||||
v-if="((showFont && !isMultiSelection) || showDescription || (showCondition && !isMultiSelection) || showFormula) && (showCopy || showDelete || showPaste)"
|
|
||||||
class="my-1 border-base-300"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-if="isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="$emit('align', 'left')"
|
|
||||||
>
|
|
||||||
<IconLayoutAlignLeft class="w-4 h-4" />
|
|
||||||
<span>{{ t('align_left') }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="$emit('align', 'right')"
|
|
||||||
>
|
|
||||||
<IconLayoutAlignRight class="w-4 h-4" />
|
|
||||||
<span>{{ t('align_right') }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="$emit('align', 'top')"
|
|
||||||
>
|
|
||||||
<IconLayoutAlignTop class="w-4 h-4" />
|
|
||||||
<span>{{ t('align_top') }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="$emit('align', 'bottom')"
|
|
||||||
>
|
|
||||||
<IconLayoutAlignBottom class="w-4 h-4" />
|
|
||||||
<span>{{ t('align_bottom') }}</span>
|
|
||||||
</button>
|
|
||||||
<hr
|
|
||||||
v-if="isMultiSelection && (showFont || showCondition)"
|
|
||||||
class="my-1 border-base-300"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-if="showFont && isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="openFontModal"
|
|
||||||
>
|
|
||||||
<IconTypography class="w-4 h-4" />
|
|
||||||
<span>{{ t('font') }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="showCondition && isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="openConditionModal"
|
|
||||||
>
|
|
||||||
<IconRouteAltLeft class="w-4 h-4" />
|
|
||||||
<span>{{ t('condition') }}</span>
|
|
||||||
</button>
|
|
||||||
<hr
|
|
||||||
v-if="isMultiSelection"
|
|
||||||
class="my-1 border-base-300"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-if="showCopy"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center justify-between text-sm"
|
|
||||||
@click.stop="$emit('copy')"
|
|
||||||
>
|
|
||||||
<span class="flex items-center space-x-2">
|
|
||||||
<IconCopy class="w-4 h-4" />
|
|
||||||
<span>{{ t('copy') }}</span>
|
|
||||||
</span>
|
|
||||||
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘C' : 'Ctrl+C' }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="showDelete"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center justify-between text-sm text-red-600"
|
|
||||||
@click.stop="$emit('delete')"
|
|
||||||
>
|
|
||||||
<span class="flex items-center space-x-2">
|
|
||||||
<IconTrashX class="w-4 h-4" />
|
|
||||||
<span>{{ t('remove') }}</span>
|
|
||||||
</span>
|
|
||||||
<span class="text-xs text-base-content/60 ml-4">Del</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="showPaste"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center justify-between text-sm"
|
|
||||||
:class="!hasClipboardData ? 'opacity-50 cursor-not-allowed' : 'hover:bg-base-100'"
|
|
||||||
:disabled="!hasClipboardData"
|
|
||||||
@click.stop="!hasClipboardData ? null : $emit('paste')"
|
|
||||||
>
|
|
||||||
<span class="flex items-center space-x-2">
|
|
||||||
<IconClipboard class="w-4 h-4" />
|
|
||||||
<span>{{ t('paste') }}</span>
|
|
||||||
</span>
|
|
||||||
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘V' : 'Ctrl+V' }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="showSelectFields"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center justify-between text-sm"
|
|
||||||
@click.stop="handleToggleSelectMode"
|
|
||||||
>
|
|
||||||
<span class="flex items-center space-x-2">
|
|
||||||
<IconClick
|
|
||||||
v-if="!isSelectModeRef.value"
|
|
||||||
class="w-4 h-4"
|
|
||||||
/>
|
|
||||||
<IconNewSection
|
|
||||||
v-else
|
|
||||||
class="w-4 h-4"
|
|
||||||
/>
|
|
||||||
<span>{{ isSelectModeRef.value ? t('draw_fields') : t('select_fields') }}</span>
|
|
||||||
</span>
|
|
||||||
<span class="text-xs text-base-content/60 ml-4">Tab</span>
|
|
||||||
</button>
|
|
||||||
<hr
|
|
||||||
v-if="showAutodetectFields"
|
|
||||||
class="my-1 border-base-300"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-if="showAutodetectFields"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="$emit('autodetect-fields')"
|
|
||||||
>
|
|
||||||
<IconSparkles class="w-4 h-4" />
|
|
||||||
<span>{{ t('autodetect_fields') }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<Teleport
|
|
||||||
v-if="isShowFormulaModal"
|
|
||||||
:to="modalContainerEl"
|
|
||||||
>
|
|
||||||
<FormulaModal
|
|
||||||
:field="field"
|
|
||||||
:editable="editable"
|
|
||||||
:build-default-name="buildDefaultName"
|
|
||||||
@close="closeModal"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
<Teleport
|
|
||||||
v-if="isShowFontModal"
|
|
||||||
:to="modalContainerEl"
|
|
||||||
>
|
|
||||||
<FontModal
|
|
||||||
:field="multiSelectField || field"
|
|
||||||
:area="contextMenu.area"
|
|
||||||
:editable="editable"
|
|
||||||
:build-default-name="buildDefaultName"
|
|
||||||
:with-click-save-event="isMultiSelection"
|
|
||||||
@click-save="handleSaveMultiSelectFontModal"
|
|
||||||
@close="closeModal"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
<Teleport
|
|
||||||
v-if="isShowConditionsModal"
|
|
||||||
:to="modalContainerEl"
|
|
||||||
>
|
|
||||||
<ConditionsModal
|
|
||||||
:item="multiSelectField || field"
|
|
||||||
:build-default-name="buildDefaultName"
|
|
||||||
:exclude-field-uuids="isMultiSelection ? selectedFields.map(f => f.uuid) : []"
|
|
||||||
:with-click-save-event="isMultiSelection"
|
|
||||||
@click-save="handleSaveMultiSelectConditionsModal"
|
|
||||||
@close="closeModal"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
<Teleport
|
|
||||||
v-if="isShowDescriptionModal"
|
|
||||||
:to="modalContainerEl"
|
|
||||||
>
|
|
||||||
<DescriptionModal
|
|
||||||
:field="field"
|
|
||||||
:editable="editable"
|
|
||||||
:build-default-name="buildDefaultName"
|
|
||||||
@close="closeModal"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { IconCopy, IconClipboard, IconTrashX, IconTypography, IconInfoCircle, IconRouteAltLeft, IconMathFunction, IconClick, IconNewSection, IconLayoutAlignLeft, IconLayoutAlignRight, IconLayoutAlignTop, IconLayoutAlignBottom, IconSparkles } from '@tabler/icons-vue'
|
|
||||||
import FormulaModal from './formula_modal'
|
|
||||||
import FontModal from './font_modal'
|
|
||||||
import ConditionsModal from './conditions_modal'
|
|
||||||
import DescriptionModal from './description_modal'
|
|
||||||
import Field from './field'
|
|
||||||
import FieldType from './field_type.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'ContextMenu',
|
|
||||||
components: {
|
|
||||||
IconCopy,
|
|
||||||
IconClipboard,
|
|
||||||
IconTrashX,
|
|
||||||
IconTypography,
|
|
||||||
IconInfoCircle,
|
|
||||||
IconRouteAltLeft,
|
|
||||||
IconMathFunction,
|
|
||||||
IconClick,
|
|
||||||
IconNewSection,
|
|
||||||
IconLayoutAlignLeft,
|
|
||||||
IconLayoutAlignRight,
|
|
||||||
IconLayoutAlignTop,
|
|
||||||
IconLayoutAlignBottom,
|
|
||||||
IconSparkles,
|
|
||||||
FormulaModal,
|
|
||||||
FontModal,
|
|
||||||
ConditionsModal,
|
|
||||||
DescriptionModal
|
|
||||||
},
|
|
||||||
inject: ['t', 'save', 'selectedAreasRef', 'isSelectModeRef'],
|
|
||||||
props: {
|
|
||||||
contextMenu: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
field: {
|
|
||||||
type: Object,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
editable: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
isMultiSelection: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
selectedAreas: {
|
|
||||||
type: Array,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
template: {
|
|
||||||
type: Object,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
withFieldsDetection: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
emits: ['copy', 'paste', 'delete', 'close', 'align', 'autodetect-fields'],
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
isShowFormulaModal: false,
|
|
||||||
isShowFontModal: false,
|
|
||||||
isShowConditionsModal: false,
|
|
||||||
isShowDescriptionModal: false,
|
|
||||||
multiSelectField: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
|
||||||
fieldLabels: FieldType.computed.fieldLabels,
|
|
||||||
modalContainerEl () {
|
|
||||||
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
|
||||||
},
|
|
||||||
selectedFields () {
|
|
||||||
if (!this.isMultiSelection) return []
|
|
||||||
|
|
||||||
return this.selectedAreasRef.value.map((area) => {
|
|
||||||
return this.template.fields.find((f) => f.areas?.includes(area))
|
|
||||||
}).filter(Boolean)
|
|
||||||
},
|
|
||||||
isMac () {
|
|
||||||
return (navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')
|
|
||||||
},
|
|
||||||
hasClipboardData () {
|
|
||||||
try {
|
|
||||||
const clipboard = localStorage.getItem('docuseal_clipboard')
|
|
||||||
|
|
||||||
if (clipboard) {
|
|
||||||
const data = JSON.parse(clipboard)
|
|
||||||
|
|
||||||
return Date.now() - data.timestamp < 3600000
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
menuStyle () {
|
|
||||||
return {
|
|
||||||
left: this.contextMenu.x + 'px',
|
|
||||||
top: this.contextMenu.y + 'px'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
showCopy () {
|
|
||||||
return !!this.contextMenu.area || this.isMultiSelection
|
|
||||||
},
|
|
||||||
showPaste () {
|
|
||||||
return !this.contextMenu.area && !this.isMultiSelection
|
|
||||||
},
|
|
||||||
showDelete () {
|
|
||||||
return !!this.contextMenu.area || this.isMultiSelection
|
|
||||||
},
|
|
||||||
showFont () {
|
|
||||||
if (this.isMultiSelection) return true
|
|
||||||
if (!this.field) return false
|
|
||||||
|
|
||||||
return ['text', 'number', 'date', 'select', 'heading'].includes(this.field.type)
|
|
||||||
},
|
|
||||||
showDescription () {
|
|
||||||
if (!this.field) return false
|
|
||||||
|
|
||||||
return !['stamp', 'heading', 'strikethrough'].includes(this.field.type)
|
|
||||||
},
|
|
||||||
showCondition () {
|
|
||||||
if (this.isMultiSelection) return true
|
|
||||||
if (!this.field) return false
|
|
||||||
|
|
||||||
return !['stamp', 'heading'].includes(this.field.type)
|
|
||||||
},
|
|
||||||
showFormula () {
|
|
||||||
if (!this.field) return false
|
|
||||||
|
|
||||||
return this.field.type === 'number'
|
|
||||||
},
|
|
||||||
showRequired () {
|
|
||||||
if (!this.field) return false
|
|
||||||
|
|
||||||
return !['phone', 'stamp', 'verification', 'strikethrough', 'heading'].includes(this.field.type)
|
|
||||||
},
|
|
||||||
showReadOnly () {
|
|
||||||
if (!this.field) return false
|
|
||||||
|
|
||||||
return ['text', 'number'].includes(this.field.type)
|
|
||||||
},
|
|
||||||
isRequired () {
|
|
||||||
return this.field?.required || false
|
|
||||||
},
|
|
||||||
isReadOnly () {
|
|
||||||
return this.field?.readonly || false
|
|
||||||
},
|
|
||||||
showSelectFields () {
|
|
||||||
return !this.contextMenu.area && !this.isMultiSelection
|
|
||||||
},
|
|
||||||
showAutodetectFields () {
|
|
||||||
return this.withFieldsDetection && this.editable && !this.contextMenu.area && !this.isMultiSelection
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
document.addEventListener('keydown', this.onKeyDown)
|
|
||||||
document.addEventListener('mousedown', this.handleClickOutside)
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.checkMenuPosition()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
beforeUnmount () {
|
|
||||||
document.removeEventListener('keydown', this.onKeyDown)
|
|
||||||
document.removeEventListener('mousedown', this.handleClickOutside)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
buildDefaultName: Field.methods.buildDefaultName,
|
|
||||||
checkMenuPosition () {
|
|
||||||
if (this.$refs.menu) {
|
|
||||||
const rect = this.$refs.menu.getBoundingClientRect()
|
|
||||||
|
|
||||||
if (rect.bottom > window.innerHeight) {
|
|
||||||
this.contextMenu.y = this.contextMenu.y - rect.height
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rect.right > window.innerWidth) {
|
|
||||||
this.contextMenu.x = this.contextMenu.x - rect.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleToggleRequired (value) {
|
|
||||||
if (this.field) {
|
|
||||||
this.field.required = value
|
|
||||||
this.save()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleToggleReadOnly (value) {
|
|
||||||
if (this.field) {
|
|
||||||
this.field.readonly = value
|
|
||||||
this.save()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onKeyDown (event) {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
|
|
||||||
this.$emit('close')
|
|
||||||
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v' && this.hasClipboardData) {
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
|
|
||||||
this.$emit('paste')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleClickOutside (event) {
|
|
||||||
if (this.$refs.menu && !this.$refs.menu.contains(event.target)) {
|
|
||||||
this.$emit('close')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openFontModal () {
|
|
||||||
if (this.isMultiSelection) {
|
|
||||||
this.multiSelectField = {
|
|
||||||
name: this.t('fields_selected').replace('{count}', this.selectedFields.length),
|
|
||||||
preferences: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const preferencesStrings = this.selectedFields.map((f) => JSON.stringify(f.preferences || {}))
|
|
||||||
|
|
||||||
if (preferencesStrings.every((s) => s === preferencesStrings[0])) {
|
|
||||||
this.multiSelectField.preferences = JSON.parse(preferencesStrings[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isShowFontModal = true
|
|
||||||
},
|
|
||||||
openDescriptionModal () {
|
|
||||||
this.isShowDescriptionModal = true
|
|
||||||
},
|
|
||||||
openConditionModal () {
|
|
||||||
if (this.isMultiSelection) {
|
|
||||||
this.multiSelectField = {
|
|
||||||
name: this.t('fields_selected').replace('{count}', this.selectedFields.length),
|
|
||||||
conditions: []
|
|
||||||
}
|
|
||||||
|
|
||||||
const conditionStrings = this.selectedFields.map((f) => JSON.stringify(f.conditions || []))
|
|
||||||
|
|
||||||
if (conditionStrings.every((s) => s === conditionStrings[0])) {
|
|
||||||
this.multiSelectField.conditions = JSON.parse(conditionStrings[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isShowConditionsModal = true
|
|
||||||
},
|
|
||||||
openFormulaModal () {
|
|
||||||
this.isShowFormulaModal = true
|
|
||||||
},
|
|
||||||
closeModal () {
|
|
||||||
this.isShowFormulaModal = false
|
|
||||||
this.isShowFontModal = false
|
|
||||||
this.isShowConditionsModal = false
|
|
||||||
this.isShowDescriptionModal = false
|
|
||||||
this.multiSelectField = null
|
|
||||||
|
|
||||||
this.$emit('close')
|
|
||||||
},
|
|
||||||
handleSaveMultiSelectFontModal () {
|
|
||||||
this.selectedFields.forEach((field) => {
|
|
||||||
field.preferences = { ...field.preferences, ...this.multiSelectField.preferences }
|
|
||||||
})
|
|
||||||
|
|
||||||
this.save()
|
|
||||||
|
|
||||||
this.closeModal()
|
|
||||||
},
|
|
||||||
handleSaveMultiSelectConditionsModal () {
|
|
||||||
this.selectedFields.forEach((field) => {
|
|
||||||
field.conditions = JSON.parse(JSON.stringify(this.multiSelectField.conditions))
|
|
||||||
})
|
|
||||||
|
|
||||||
this.save()
|
|
||||||
|
|
||||||
this.closeModal()
|
|
||||||
},
|
|
||||||
handleToggleSelectMode () {
|
|
||||||
this.isSelectModeRef.value = !this.isSelectModeRef.value
|
|
||||||
|
|
||||||
this.$emit('close')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<Teleport :to="modalContainerEl">
|
||||||
|
<div class="modal modal-open items-start !animate-none overflow-y-auto">
|
||||||
|
<div
|
||||||
|
class="absolute top-0 bottom-0 right-0 left-0"
|
||||||
|
@click.prevent="$emit('close')"
|
||||||
|
/>
|
||||||
|
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||||
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
|
<span class="modal-title">
|
||||||
|
{{ title }}
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="text-xl modal-close-button"
|
||||||
|
@click.prevent="$emit('close')"
|
||||||
|
>×</a>
|
||||||
|
</div>
|
||||||
|
<form @submit.prevent="$emit('save')">
|
||||||
|
<slot />
|
||||||
|
<button
|
||||||
|
class="base-button w-full mt-4 modal-save-button"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{{ t('save') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ContextModal',
|
||||||
|
inject: ['t'],
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
modalContainerEl: {
|
||||||
|
type: Element,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['close', 'save']
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -0,0 +1,147 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="relative"
|
||||||
|
@mouseenter="handleMouseEnter"
|
||||||
|
@mouseleave="handleMouseLeave"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
||||||
|
@click.stop="isOpen ? close() : open()"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x-2">
|
||||||
|
<component
|
||||||
|
:is="icon || 'span'"
|
||||||
|
class="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<span>{{ label }}</span>
|
||||||
|
</span>
|
||||||
|
<IconChevronRight class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="isOpen"
|
||||||
|
ref="submenu"
|
||||||
|
class="absolute p-1 z-50 left-full bg-white shadow-lg rounded-lg border border-neutral-200 cursor-default"
|
||||||
|
style="min-width: 170px"
|
||||||
|
:style="submenuStyle"
|
||||||
|
:class="menuClass"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<button
|
||||||
|
v-for="option in options"
|
||||||
|
:key="option.value"
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between space-x-2 text-sm cursor-pointer"
|
||||||
|
@click="handleSelect(option.value)"
|
||||||
|
>
|
||||||
|
<span class="whitespace-nowrap">{{ option.label }}</span>
|
||||||
|
<IconCheck
|
||||||
|
v-if="modelValue === option.value"
|
||||||
|
class="w-4 h-4"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconChevronRight, IconCheck } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ContextSubmenu',
|
||||||
|
components: {
|
||||||
|
IconChevronRight,
|
||||||
|
IconCheck
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
icon: {
|
||||||
|
type: [Function],
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
menuClass: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['select', 'update:modelValue'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isOpen: false,
|
||||||
|
topOffset: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
submenuStyle () {
|
||||||
|
return {
|
||||||
|
top: this.topOffset + 'px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
this.clearTimeout()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleMouseEnter () {
|
||||||
|
clearTimeout(this.closeTimeout)
|
||||||
|
|
||||||
|
this.openTimeout = setTimeout(() => this.open(), 200)
|
||||||
|
},
|
||||||
|
handleMouseLeave () {
|
||||||
|
clearTimeout(this.openTimeout)
|
||||||
|
|
||||||
|
this.closeTimeout = setTimeout(() => this.close(), 200)
|
||||||
|
},
|
||||||
|
open () {
|
||||||
|
this.clearTimeout()
|
||||||
|
|
||||||
|
this.isOpen = true
|
||||||
|
this.topOffset = 0
|
||||||
|
|
||||||
|
this.$nextTick(() => setTimeout(() => this.adjustPosition(), 0))
|
||||||
|
},
|
||||||
|
clearTimeout () {
|
||||||
|
if (this.openTimeout) {
|
||||||
|
clearTimeout(this.openTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.closeTimeout) {
|
||||||
|
clearTimeout(this.closeTimeout)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close () {
|
||||||
|
this.clearTimeout()
|
||||||
|
|
||||||
|
this.isOpen = false
|
||||||
|
},
|
||||||
|
handleSelect (value) {
|
||||||
|
this.$emit('select', value)
|
||||||
|
this.$emit('update:modelValue', value)
|
||||||
|
},
|
||||||
|
adjustPosition () {
|
||||||
|
if (!this.$refs.submenu) return
|
||||||
|
|
||||||
|
const rect = this.$refs.submenu.getBoundingClientRect()
|
||||||
|
const overflow = rect.bottom - window.innerHeight
|
||||||
|
|
||||||
|
if (overflow > 0) {
|
||||||
|
this.topOffset = -overflow - 4
|
||||||
|
} else {
|
||||||
|
this.topOffset = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -0,0 +1,159 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="menu"
|
||||||
|
class="fixed z-50 p-1 bg-white shadow-lg rounded-lg border border-neutral-200 cursor-default"
|
||||||
|
style="min-width: 170px"
|
||||||
|
:style="menuStyle"
|
||||||
|
@mousedown.stop
|
||||||
|
@pointerdown.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
||||||
|
:class="!hasClipboardData ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-100'"
|
||||||
|
:disabled="!hasClipboardData"
|
||||||
|
@click.stop="!hasClipboardData ? null : $emit('paste')"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x-2">
|
||||||
|
<IconClipboard class="w-4 h-4" />
|
||||||
|
<span>{{ t('paste') }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘V' : 'Ctrl+V' }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
||||||
|
@click.stop="handleToggleSelectMode"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x-2">
|
||||||
|
<IconClick
|
||||||
|
v-if="!isSelectModeRef.value"
|
||||||
|
class="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<IconNewSection
|
||||||
|
v-else
|
||||||
|
class="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<span>{{ isSelectModeRef.value ? t('draw_fields') : t('select_fields') }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-base-content/60 ml-4">Tab</span>
|
||||||
|
</button>
|
||||||
|
<hr
|
||||||
|
v-if="showAutodetectFields"
|
||||||
|
class="my-1 border-neutral-200"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="showAutodetectFields"
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="$emit('autodetect-fields')"
|
||||||
|
>
|
||||||
|
<IconSparkles class="w-4 h-4" />
|
||||||
|
<span>{{ t('autodetect_fields') }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconClipboard, IconClick, IconNewSection, IconSparkles } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PageContextMenu',
|
||||||
|
components: {
|
||||||
|
IconClipboard,
|
||||||
|
IconClick,
|
||||||
|
IconNewSection,
|
||||||
|
IconSparkles
|
||||||
|
},
|
||||||
|
inject: ['t', 'isSelectModeRef'],
|
||||||
|
props: {
|
||||||
|
contextMenu: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
withFieldsDetection: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['paste', 'close', 'autodetect-fields'],
|
||||||
|
computed: {
|
||||||
|
isMac () {
|
||||||
|
return (navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')
|
||||||
|
},
|
||||||
|
hasClipboardData () {
|
||||||
|
try {
|
||||||
|
const clipboard = localStorage.getItem('docuseal_clipboard')
|
||||||
|
|
||||||
|
if (clipboard) {
|
||||||
|
const data = JSON.parse(clipboard)
|
||||||
|
|
||||||
|
return Date.now() - data.timestamp < 3600000
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
menuStyle () {
|
||||||
|
return {
|
||||||
|
left: this.contextMenu.x + 'px',
|
||||||
|
top: this.contextMenu.y + 'px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showAutodetectFields () {
|
||||||
|
return this.withFieldsDetection && this.editable
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
document.addEventListener('keydown', this.onKeyDown)
|
||||||
|
document.addEventListener('mousedown', this.handleClickOutside)
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.checkMenuPosition()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
document.removeEventListener('keydown', this.onKeyDown)
|
||||||
|
document.removeEventListener('mousedown', this.handleClickOutside)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
checkMenuPosition () {
|
||||||
|
if (this.$refs.menu) {
|
||||||
|
const rect = this.$refs.menu.getBoundingClientRect()
|
||||||
|
const overflow = rect.bottom - window.innerHeight
|
||||||
|
|
||||||
|
if (overflow > 0) {
|
||||||
|
this.contextMenu.y = this.contextMenu.y - overflow - 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onKeyDown (event) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
this.$emit('close')
|
||||||
|
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v' && this.hasClipboardData) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
this.$emit('paste')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleClickOutside (event) {
|
||||||
|
if (this.$refs.menu && !this.$refs.menu.contains(event.target)) {
|
||||||
|
this.$emit('close')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleToggleSelectMode () {
|
||||||
|
this.isSelectModeRef.value = !this.isSelectModeRef.value
|
||||||
|
|
||||||
|
this.$emit('close')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -0,0 +1,348 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-if="!isShowFontModal && !isShowConditionsModal"
|
||||||
|
ref="menu"
|
||||||
|
class="fixed z-50 p-1 bg-white shadow-lg rounded-lg border border-neutral-200 cursor-default"
|
||||||
|
style="min-width: 170px"
|
||||||
|
:style="menuStyle"
|
||||||
|
@mousedown.stop
|
||||||
|
@pointerdown.stop
|
||||||
|
>
|
||||||
|
<ContextSubmenu
|
||||||
|
:icon="IconLayoutAlignMiddle"
|
||||||
|
:label="t('align')"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="alignSelectedAreas('left')"
|
||||||
|
>
|
||||||
|
<IconLayoutAlignLeft class="w-4 h-4" />
|
||||||
|
<span>{{ t('align_left') }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="alignSelectedAreas('right')"
|
||||||
|
>
|
||||||
|
<IconLayoutAlignRight class="w-4 h-4" />
|
||||||
|
<span>{{ t('align_right') }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="alignSelectedAreas('top')"
|
||||||
|
>
|
||||||
|
<IconLayoutAlignTop class="w-4 h-4" />
|
||||||
|
<span>{{ t('align_top') }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="alignSelectedAreas('bottom')"
|
||||||
|
>
|
||||||
|
<IconLayoutAlignBottom class="w-4 h-4" />
|
||||||
|
<span>{{ t('align_bottom') }}</span>
|
||||||
|
</button>
|
||||||
|
</ContextSubmenu>
|
||||||
|
<ContextSubmenu
|
||||||
|
:icon="IconAspectRatio"
|
||||||
|
:label="t('resize')"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="resizeSelectedAreas('width')"
|
||||||
|
>
|
||||||
|
<IconArrowsHorizontal class="w-4 h-4" />
|
||||||
|
<span>{{ t('width') }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="resizeSelectedAreas('height')"
|
||||||
|
>
|
||||||
|
<IconArrowsVertical class="w-4 h-4" />
|
||||||
|
<span>{{ t('height') }}</span>
|
||||||
|
</button>
|
||||||
|
</ContextSubmenu>
|
||||||
|
<hr
|
||||||
|
v-if="showFont || showCondition"
|
||||||
|
class="my-1 border-neutral-200"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="showFont"
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="openFontModal"
|
||||||
|
>
|
||||||
|
<IconTypography class="w-4 h-4" />
|
||||||
|
<span>{{ t('font') }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="showCondition"
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="openConditionModal"
|
||||||
|
>
|
||||||
|
<IconRouteAltLeft class="w-4 h-4" />
|
||||||
|
<span>{{ t('condition') }}</span>
|
||||||
|
</button>
|
||||||
|
<hr class="my-1 border-neutral-200">
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
||||||
|
@click.stop="$emit('copy')"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x-2">
|
||||||
|
<IconCopy class="w-4 h-4" />
|
||||||
|
<span>{{ t('copy') }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘C' : 'Ctrl+C' }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm text-red-600"
|
||||||
|
@click.stop="$emit('delete')"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x-2">
|
||||||
|
<IconTrashX class="w-4 h-4" />
|
||||||
|
<span>{{ t('remove') }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-base-content/60 ml-4">Del</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowFontModal"
|
||||||
|
:to="modalContainerEl"
|
||||||
|
>
|
||||||
|
<FontModal
|
||||||
|
:field="multiSelectField"
|
||||||
|
:area="contextMenu.area"
|
||||||
|
:editable="editable"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="handleSaveMultiSelectFontModal"
|
||||||
|
@close="closeModal"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowConditionsModal"
|
||||||
|
:to="modalContainerEl"
|
||||||
|
>
|
||||||
|
<ConditionsModal
|
||||||
|
:item="multiSelectField"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
:exclude-field-uuids="selectedFields.map(f => f.uuid)"
|
||||||
|
@save="handleSaveMultiSelectConditionsModal"
|
||||||
|
@close="closeModal"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconCopy, IconTrashX, IconTypography, IconRouteAltLeft, IconLayoutAlignLeft, IconLayoutAlignRight, IconLayoutAlignTop, IconLayoutAlignBottom, IconLayoutAlignMiddle, IconAspectRatio, IconArrowsHorizontal, IconArrowsVertical } from '@tabler/icons-vue'
|
||||||
|
import FontModal from './font_modal'
|
||||||
|
import ConditionsModal from './conditions_modal'
|
||||||
|
import ContextSubmenu from './field_context_submenu'
|
||||||
|
import Field from './field'
|
||||||
|
import FieldType from './field_type'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SelectionContextMenu',
|
||||||
|
components: {
|
||||||
|
IconCopy,
|
||||||
|
IconTrashX,
|
||||||
|
IconTypography,
|
||||||
|
IconRouteAltLeft,
|
||||||
|
IconLayoutAlignLeft,
|
||||||
|
IconLayoutAlignRight,
|
||||||
|
IconLayoutAlignTop,
|
||||||
|
IconLayoutAlignBottom,
|
||||||
|
FontModal,
|
||||||
|
IconArrowsHorizontal,
|
||||||
|
IconArrowsVertical,
|
||||||
|
ConditionsModal,
|
||||||
|
ContextSubmenu
|
||||||
|
},
|
||||||
|
inject: ['t', 'save', 'selectedAreasRef', 'getFieldTypeIndex'],
|
||||||
|
props: {
|
||||||
|
contextMenu: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
template: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
withCondition: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['copy', 'delete', 'close'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isShowFontModal: false,
|
||||||
|
isShowConditionsModal: false,
|
||||||
|
multiSelectField: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
modalContainerEl () {
|
||||||
|
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||||
|
},
|
||||||
|
selectedFields () {
|
||||||
|
return this.selectedAreasRef.value.map((area) => {
|
||||||
|
return this.template.fields.find((f) => f.areas?.includes(area))
|
||||||
|
}).filter(Boolean)
|
||||||
|
},
|
||||||
|
isMac () {
|
||||||
|
return (navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')
|
||||||
|
},
|
||||||
|
menuStyle () {
|
||||||
|
return {
|
||||||
|
left: this.contextMenu.x + 'px',
|
||||||
|
top: this.contextMenu.y + 'px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showFont () {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
showCondition () {
|
||||||
|
return this.withCondition
|
||||||
|
},
|
||||||
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
|
fieldLabels: FieldType.computed.fieldLabels
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
document.addEventListener('keydown', this.onKeyDown)
|
||||||
|
document.addEventListener('mousedown', this.handleClickOutside)
|
||||||
|
|
||||||
|
this.$nextTick(() => this.checkMenuPosition())
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
document.removeEventListener('keydown', this.onKeyDown)
|
||||||
|
document.removeEventListener('mousedown', this.handleClickOutside)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
IconLayoutAlignMiddle,
|
||||||
|
IconAspectRatio,
|
||||||
|
buildDefaultName: Field.methods.buildDefaultName,
|
||||||
|
checkMenuPosition () {
|
||||||
|
if (this.$refs.menu) {
|
||||||
|
const rect = this.$refs.menu.getBoundingClientRect()
|
||||||
|
const overflow = rect.bottom - window.innerHeight
|
||||||
|
|
||||||
|
if (overflow > 0) {
|
||||||
|
this.contextMenu.y = this.contextMenu.y - overflow - 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onKeyDown (event) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
this.$emit('close')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleClickOutside (event) {
|
||||||
|
if (this.$refs.menu && !this.$refs.menu.contains(event.target)) {
|
||||||
|
this.$emit('close')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openFontModal () {
|
||||||
|
this.multiSelectField = {
|
||||||
|
name: this.t('fields_selected').replace('{count}', this.selectedFields.length),
|
||||||
|
preferences: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferencesStrings = this.selectedFields.map((f) => JSON.stringify(f.preferences || {}))
|
||||||
|
|
||||||
|
if (preferencesStrings.every((s) => s === preferencesStrings[0])) {
|
||||||
|
this.multiSelectField.preferences = JSON.parse(preferencesStrings[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isShowFontModal = true
|
||||||
|
},
|
||||||
|
openConditionModal () {
|
||||||
|
this.multiSelectField = {
|
||||||
|
name: this.t('fields_selected').replace('{count}', this.selectedFields.length),
|
||||||
|
conditions: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const conditionStrings = this.selectedFields.map((f) => JSON.stringify(f.conditions || []))
|
||||||
|
|
||||||
|
if (conditionStrings.every((s) => s === conditionStrings[0])) {
|
||||||
|
this.multiSelectField.conditions = JSON.parse(conditionStrings[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isShowConditionsModal = true
|
||||||
|
},
|
||||||
|
closeModal () {
|
||||||
|
this.isShowFontModal = false
|
||||||
|
this.isShowConditionsModal = false
|
||||||
|
this.multiSelectField = null
|
||||||
|
|
||||||
|
this.$emit('close')
|
||||||
|
},
|
||||||
|
alignSelectedAreas (direction) {
|
||||||
|
const areas = this.selectedAreasRef.value
|
||||||
|
|
||||||
|
let targetValue
|
||||||
|
|
||||||
|
if (direction === 'left') {
|
||||||
|
targetValue = Math.min(...areas.map(a => a.x))
|
||||||
|
areas.forEach((area) => { area.x = targetValue })
|
||||||
|
} else if (direction === 'right') {
|
||||||
|
targetValue = Math.max(...areas.map(a => a.x + a.w))
|
||||||
|
areas.forEach((area) => { area.x = targetValue - area.w })
|
||||||
|
} else if (direction === 'top') {
|
||||||
|
targetValue = Math.min(...areas.map(a => a.y))
|
||||||
|
areas.forEach((area) => { area.y = targetValue })
|
||||||
|
} else if (direction === 'bottom') {
|
||||||
|
targetValue = Math.max(...areas.map(a => a.y + a.h))
|
||||||
|
areas.forEach((area) => { area.y = targetValue - area.h })
|
||||||
|
}
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
this.$emit('close')
|
||||||
|
},
|
||||||
|
resizeSelectedAreas (dimension) {
|
||||||
|
const areas = this.selectedAreasRef.value
|
||||||
|
|
||||||
|
const values = areas.map(a => dimension === 'width' ? a.w : a.h).sort((a, b) => a - b)
|
||||||
|
const medianValue = values[Math.floor(values.length / 2)]
|
||||||
|
|
||||||
|
if (dimension === 'width') {
|
||||||
|
areas.forEach((area) => { area.w = medianValue })
|
||||||
|
} else if (dimension === 'height') {
|
||||||
|
areas.forEach((area) => {
|
||||||
|
const diff = medianValue - area.h
|
||||||
|
area.y = area.y - diff
|
||||||
|
area.h = medianValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
this.$emit('close')
|
||||||
|
},
|
||||||
|
handleSaveMultiSelectFontModal () {
|
||||||
|
this.selectedFields.forEach((field) => {
|
||||||
|
field.preferences = { ...field.preferences, ...this.multiSelectField.preferences }
|
||||||
|
})
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
this.closeModal()
|
||||||
|
},
|
||||||
|
handleSaveMultiSelectConditionsModal () {
|
||||||
|
this.selectedFields.forEach((field) => {
|
||||||
|
field.conditions = JSON.parse(JSON.stringify(this.multiSelectField.conditions))
|
||||||
|
})
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
this.closeModal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Loading…
Reference in new issue