style builder ui
This commit is contained in:
@@ -1,14 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="bg-red-100 absolute opacity-70"
|
class="absolute overflow-visible group"
|
||||||
:style="positionStyle"
|
:style="positionStyle"
|
||||||
@mousedown="startDrag"
|
@pointerdown.stop
|
||||||
|
@mousedown.stop="startDrag"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="field"
|
v-if="field"
|
||||||
class="flex items-center justify-center h-full w-full"
|
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap hidden group-hover:block group-hover:z-10"
|
||||||
|
style="top: -25px; height: 25px"
|
||||||
|
@mousedown.stop
|
||||||
|
@pointerdown.stop
|
||||||
>
|
>
|
||||||
{{ field?.name || field.type }}
|
<button
|
||||||
|
v-for="(component, type, index) in iconComponents"
|
||||||
|
:key="type"
|
||||||
|
class="px-0.5 hover:text-base-100 hover:bg-base-content transition-colors"
|
||||||
|
:class="{ 'bg-base-content text-base-100': field.type === type, 'rounded-tl': index === 0, 'rounded-tr': index === 4 }"
|
||||||
|
@click="changeTypeTo(type)"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="component"
|
||||||
|
:width="20"
|
||||||
|
stroke-width="1.5"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="bg-red-100 opacity-70 flex items-center justify-center h-full w-full"
|
||||||
|
>
|
||||||
|
{{ field?.name || field?.type }}
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="h-2 w-2 right-0 bottom-0 bg-red-900 absolute cursor-nwse-resize"
|
class="h-2 w-2 right-0 bottom-0 bg-red-900 absolute cursor-nwse-resize"
|
||||||
@@ -18,6 +39,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { IconTextSize, IconWriting, IconCalendarEvent, IconPhoto, IconCheckbox } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldArea',
|
name: 'FieldArea',
|
||||||
props: {
|
props: {
|
||||||
@@ -47,6 +70,15 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
iconComponents () {
|
||||||
|
return {
|
||||||
|
text: IconTextSize,
|
||||||
|
signature: IconWriting,
|
||||||
|
date: IconCalendarEvent,
|
||||||
|
image: IconPhoto,
|
||||||
|
checkbox: IconCheckbox
|
||||||
|
}
|
||||||
|
},
|
||||||
positionStyle () {
|
positionStyle () {
|
||||||
const { x, y, w, h } = this.bounds
|
const { x, y, w, h } = this.bounds
|
||||||
|
|
||||||
@@ -59,9 +91,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
changeTypeTo (type) {
|
||||||
|
this.field.type = type
|
||||||
|
},
|
||||||
resize (e) {
|
resize (e) {
|
||||||
this.bounds.w = e.layerX / e.toElement.clientWidth - this.bounds.x
|
if (e.toElement.id === 'mask') {
|
||||||
this.bounds.h = e.layerY / e.toElement.clientHeight - this.bounds.y
|
this.bounds.w = e.layerX / e.toElement.clientWidth - this.bounds.x
|
||||||
|
this.bounds.h = e.layerY / e.toElement.clientHeight - this.bounds.y
|
||||||
|
}
|
||||||
},
|
},
|
||||||
drag (e) {
|
drag (e) {
|
||||||
if (e.toElement.id === 'mask') {
|
if (e.toElement.id === 'mask') {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
style="max-width: 1600px"
|
style="max-width: 1600px"
|
||||||
class="mx-auto px-4"
|
class="mx-auto pl-4"
|
||||||
>
|
>
|
||||||
<div class="flex justify-between py-1.5 items-center">
|
<div class="flex justify-between py-1.5 items-center pr-4">
|
||||||
<div class="flex space-x-3">
|
<div class="flex space-x-3">
|
||||||
<a href="/">
|
<a href="/">
|
||||||
<Logo />
|
<Logo />
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref="previews"
|
ref="previews"
|
||||||
class="overflow-auto w-52 flex-none pr-4 mt-0.5 pt-0.5"
|
class="overflow-auto w-52 flex-none pr-3 mt-0.5 pt-0.5"
|
||||||
>
|
>
|
||||||
<DocumentPreview
|
<DocumentPreview
|
||||||
v-for="(item, index) in template.schema"
|
v-for="(item, index) in template.schema"
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
|
<div class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
|
||||||
<div class="px-3">
|
<div class="pr-3.5 pl-0.5">
|
||||||
<Document
|
<Document
|
||||||
v-for="document in sortedDocuments"
|
v-for="document in sortedDocuments"
|
||||||
:key="document.uuid"
|
:key="document.uuid"
|
||||||
@@ -79,12 +79,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="relative w-72 flex-none"
|
class="relative w-80 flex-none pt-0.5 pr-4"
|
||||||
:class="drawField ? 'overflow-hidden' : 'overflow-auto'"
|
:class="drawField ? 'overflow-hidden' : 'overflow-auto'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="drawField"
|
v-if="drawField"
|
||||||
class="sticky inset-0 bg-white h-full"
|
class="sticky inset-0 bg-base-100 h-full"
|
||||||
>
|
>
|
||||||
Draw {{ drawField.name }} field on the page
|
Draw {{ drawField.name }} field on the page
|
||||||
<button @click="drawField = false">
|
<button @click="drawField = false">
|
||||||
@@ -92,13 +92,13 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
FIelds
|
|
||||||
<Fields
|
<Fields
|
||||||
ref="fields"
|
ref="fields"
|
||||||
v-model:fields="template.fields"
|
v-model:fields="template.fields"
|
||||||
@set-draw="drawField = $event"
|
@set-draw="drawField = $event"
|
||||||
@set-drag="dragFieldType = $event"
|
@set-drag="dragFieldType = $event"
|
||||||
@drag-end="dragFieldType = null"
|
@drag-end="dragFieldType = null"
|
||||||
|
@scroll-to-area="scrollToArea"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,6 +114,7 @@ import Logo from './logo'
|
|||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
import DocumentPreview from './preview'
|
import DocumentPreview from './preview'
|
||||||
import { IconUsersPlus, IconDeviceFloppy } from '@tabler/icons-vue'
|
import { IconUsersPlus, IconDeviceFloppy } from '@tabler/icons-vue'
|
||||||
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplateBuilder',
|
name: 'TemplateBuilder',
|
||||||
@@ -189,10 +190,22 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDraw (area) {
|
onDraw (area) {
|
||||||
this.drawField.areas ||= []
|
if (this.drawField) {
|
||||||
this.drawField.areas.push(area)
|
this.drawField.areas ||= []
|
||||||
|
this.drawField.areas.push(area)
|
||||||
|
|
||||||
this.drawField = null
|
this.drawField = null
|
||||||
|
} else {
|
||||||
|
const field = {
|
||||||
|
name: '',
|
||||||
|
uuid: v4(),
|
||||||
|
required: true,
|
||||||
|
type: 'text',
|
||||||
|
areas: [area]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.template.fields.push(field)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onDropfield (area) {
|
onDropfield (area) {
|
||||||
this.$refs.fields.addField(this.dragFieldType, area)
|
this.$refs.fields.addField(this.dragFieldType, area)
|
||||||
@@ -245,6 +258,11 @@ export default {
|
|||||||
this.isSaving = false
|
this.isSaving = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
scrollToArea (area) {
|
||||||
|
const documentRef = this.documentRefs.find((a) => a.document.uuid === area.attachment_uuid)
|
||||||
|
|
||||||
|
documentRef.scrollToArea(area)
|
||||||
|
},
|
||||||
save () {
|
save () {
|
||||||
return fetch(`/api/templates/${this.template.id}`, {
|
return fetch(`/api/templates/${this.template.id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="group flex items-center relative overflow-visible">
|
<div
|
||||||
<div
|
class="group relative overflow-visible"
|
||||||
|
:class="{ 'flex items-center': !iconInline }"
|
||||||
|
>
|
||||||
|
<span
|
||||||
ref="contenteditable"
|
ref="contenteditable"
|
||||||
contenteditable
|
contenteditable
|
||||||
style="min-width: 2px"
|
style="min-width: 2px"
|
||||||
class="peer outline-none"
|
:class="iconInline ? 'inline' : 'block'"
|
||||||
|
class="peer outline-none focus:block"
|
||||||
@keydown.enter.prevent="onEnter"
|
@keydown.enter.prevent="onEnter"
|
||||||
|
@focus="$emit('focus', $event)"
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
>
|
>
|
||||||
{{ value }}
|
{{ value }}
|
||||||
</div>
|
</span>
|
||||||
<IconPencil
|
<IconPencil
|
||||||
contenteditable="false"
|
contenteditable="false"
|
||||||
class="absolute ml-1 cursor-pointer inline opacity-0 group-hover:opacity-100 peer-focus:opacity-0 align-middle"
|
class="cursor-pointer ml-1 flex-none opacity-0 group-hover:opacity-100 align-middle peer-focus:hidden"
|
||||||
:style="{ right: -(1.1 * iconWidth) + 'px' }"
|
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
||||||
|
:class="{ 'absolute': !iconInline, 'inline align-bottom': iconInline }"
|
||||||
:width="iconWidth"
|
:width="iconWidth"
|
||||||
@click="onPencilClick"
|
@click="onPencilClick"
|
||||||
/>
|
/>
|
||||||
@@ -34,13 +40,18 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
iconInline: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
iconWidth: {
|
iconWidth: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
default: 30
|
default: 30
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['update:model-value'],
|
emits: ['update:model-value', 'focus', 'blur'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
value: ''
|
value: ''
|
||||||
@@ -59,6 +70,7 @@ export default {
|
|||||||
this.value = this.$refs.contenteditable.innerText.trim() || this.modelValue
|
this.value = this.$refs.contenteditable.innerText.trim() || this.modelValue
|
||||||
|
|
||||||
this.$emit('update:model-value', this.value)
|
this.$emit('update:model-value', this.value)
|
||||||
|
this.$emit('blur', e)
|
||||||
},
|
},
|
||||||
onPencilClick () {
|
onPencilClick () {
|
||||||
this.$refs.contenteditable.focus()
|
this.$refs.contenteditable.focus()
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
<Page
|
<Page
|
||||||
v-for="(image, index) in sortedPreviewImages"
|
v-for="(image, index) in sortedPreviewImages"
|
||||||
:key="image.id"
|
:key="image.id"
|
||||||
|
:ref="setPageRefs"
|
||||||
:number="index"
|
:number="index"
|
||||||
:areas="areasIndex[index]"
|
:areas="areasIndex[index]"
|
||||||
:is-draw="isDraw"
|
:is-draw="isDraw"
|
||||||
:is-drag="isDrag"
|
:is-drag="isDrag"
|
||||||
:class="{ 'cursor-crosshair': isDraw }"
|
|
||||||
:image="image"
|
:image="image"
|
||||||
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
|
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
|
||||||
@draw="$emit('draw', {...$event, attachment_uuid: document.uuid })"
|
@draw="$emit('draw', {...$event, attachment_uuid: document.uuid })"
|
||||||
@@ -44,10 +44,28 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['draw', 'drop-field'],
|
emits: ['draw', 'drop-field'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
pageRefs: []
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
sortedPreviewImages () {
|
sortedPreviewImages () {
|
||||||
return [...this.document.preview_images].sort((a, b) => parseInt(a.filename) - parseInt(b.filename))
|
return [...this.document.preview_images].sort((a, b) => parseInt(a.filename) - parseInt(b.filename))
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
beforeUpdate () {
|
||||||
|
this.pageRefs = []
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
scrollToArea (area) {
|
||||||
|
this.pageRefs[area.page].areaRefs.find((e) => e.bounds === area).$el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||||
|
},
|
||||||
|
setPageRefs (el) {
|
||||||
|
if (el) {
|
||||||
|
this.pageRefs.push(el)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,86 +1,243 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div
|
||||||
<button @click="$emit('remove', field)">
|
class="group pb-2"
|
||||||
Remove
|
@mouseleave="closeDropdown"
|
||||||
</button>
|
>
|
||||||
<div>
|
<div
|
||||||
{{ field.type }}
|
class="border border-base-content rounded rounded-tr-none relative group"
|
||||||
</div>
|
>
|
||||||
<div v-if="field.type !== 'signature'">
|
<div class="flex items-center justify-between space-x-1">
|
||||||
<label>Name</label>
|
<div class="flex items-center p-1 space-x-1">
|
||||||
<input
|
<span class="dropdown">
|
||||||
v-model="field.name"
|
<label
|
||||||
type="text"
|
tabindex="0"
|
||||||
required
|
title="Type"
|
||||||
>
|
class="cursor-pointer"
|
||||||
</div>
|
>
|
||||||
<div>
|
<component
|
||||||
<div
|
:is="fieldIcons[field.type]"
|
||||||
v-for="(option, index) in field.options"
|
width="18"
|
||||||
:key="index"
|
:stroke-width="1.6"
|
||||||
class="flex"
|
/>
|
||||||
>
|
</label>
|
||||||
<input
|
<ul
|
||||||
v-model="field.options[index]"
|
tabindex="0"
|
||||||
type="text"
|
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow bg-base-100 rounded-box w-52"
|
||||||
required
|
@click="closeDropdown"
|
||||||
>
|
>
|
||||||
<button @click="field.options.splice(index, 1)">
|
<li
|
||||||
Remove
|
v-for="(name, type) in fieldNames"
|
||||||
</button>
|
:key="type"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="text-sm py-1 px-2"
|
||||||
|
:class="{ 'active': type === field.type }"
|
||||||
|
@click.prevent="field.type = type"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="fieldIcons[type]"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
:width="20"
|
||||||
|
/>
|
||||||
|
{{ name }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
<Contenteditable
|
||||||
|
ref="name"
|
||||||
|
:model-value="field.name || defaultName"
|
||||||
|
:icon-inline="true"
|
||||||
|
:icon-width="19"
|
||||||
|
@focus="onNameFocus"
|
||||||
|
@blur="onNameBlur"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-1 opacity-0 group-hover:opacity-100">
|
||||||
|
<span class="dropdown dropdown-end">
|
||||||
|
<label
|
||||||
|
tabindex="0"
|
||||||
|
title="Areas"
|
||||||
|
class="cursor-pointer"
|
||||||
|
>
|
||||||
|
<IconShape
|
||||||
|
:width="20"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
tabindex="0"
|
||||||
|
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow bg-base-100 rounded-box w-52"
|
||||||
|
@click="closeDropdown"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
v-for="(area, index) in field.areas || []"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="text-sm py-1 px-2"
|
||||||
|
@click.prevent="$emit('scroll-to', area)"
|
||||||
|
>
|
||||||
|
<IconShape
|
||||||
|
:width="20"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
Page {{ area.page + 1 }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="text-sm py-1 px-2"
|
||||||
|
@click.prevent="$emit('set-draw', field)"
|
||||||
|
>
|
||||||
|
<IconNewSection
|
||||||
|
:width="20"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
Draw New Area
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
<button @click="$emit('remove', field)">
|
||||||
|
<IconTrashX
|
||||||
|
:width="20"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div class="flex flex-col pr-1">
|
||||||
|
<button
|
||||||
|
title="Up"
|
||||||
|
style="font-size: 10px; margin-bottom: -2px"
|
||||||
|
@click="$emit('move-up')"
|
||||||
|
>
|
||||||
|
▲
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
title="Down"
|
||||||
|
style="font-size: 10px; margin-top: -2px"
|
||||||
|
@click="$emit('move-down')"
|
||||||
|
>
|
||||||
|
▼
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div
|
||||||
v-if="field.options"
|
v-if="field.options"
|
||||||
@click="field.options.push('')"
|
class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
|
||||||
>
|
>
|
||||||
Add option
|
<div
|
||||||
</button>
|
v-for="(option, index) in field.options"
|
||||||
</div>
|
:key="index"
|
||||||
<div>
|
class="flex space-x-1.5 items-center"
|
||||||
<div
|
>
|
||||||
v-for="(area, index) in areas"
|
<span class="text-sm">
|
||||||
:key="index"
|
{{ index + 1 }}.
|
||||||
>
|
</span>
|
||||||
Area {{ index + 1 }}
|
<input
|
||||||
<button @click="removeArea(area)">
|
v-model="field.options[index]"
|
||||||
×
|
class="w-full input input-primary input-xs text-sm"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="text-sm"
|
||||||
|
@click="field.options.splice(index, 1)"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="field.options"
|
||||||
|
class="text-center text-sm w-full pb-1"
|
||||||
|
@click="field.options.push('')"
|
||||||
|
>
|
||||||
|
+ Add option
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
class="block"
|
|
||||||
@click="$emit('set-draw', field)"
|
|
||||||
>
|
|
||||||
Draw area
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
:id="`field_required_${field.uuid}`"
|
|
||||||
v-model="field.required"
|
|
||||||
type="checkbox"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<label :for="`field_required_${field.uuid}`">Required</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Contenteditable from './contenteditable'
|
||||||
|
import { IconTextSize, IconWriting, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconShape, IconNewSection, IconTrashX } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplateField',
|
name: 'TemplateField',
|
||||||
|
components: {
|
||||||
|
Contenteditable,
|
||||||
|
IconShape,
|
||||||
|
IconNewSection,
|
||||||
|
IconTrashX
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
typeIndex: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['set-draw', 'remove'],
|
emits: ['set-draw', 'remove', 'move-up', 'move-down', 'scroll-to'],
|
||||||
computed: {
|
computed: {
|
||||||
|
defaultName () {
|
||||||
|
return `${this.fieldNames[this.field.type]} Field ${this.typeIndex + 1}`
|
||||||
|
},
|
||||||
areas () {
|
areas () {
|
||||||
return this.field.areas || []
|
return this.field.areas || []
|
||||||
|
},
|
||||||
|
fieldNames () {
|
||||||
|
return {
|
||||||
|
text: 'Text',
|
||||||
|
signature: 'Signature',
|
||||||
|
date: 'Date',
|
||||||
|
image: 'Image',
|
||||||
|
attachment: 'File',
|
||||||
|
select: 'Select',
|
||||||
|
checkbox: 'Checkbox',
|
||||||
|
radio: 'Radio'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fieldIcons () {
|
||||||
|
return {
|
||||||
|
text: IconTextSize,
|
||||||
|
signature: IconWriting,
|
||||||
|
date: IconCalendarEvent,
|
||||||
|
image: IconPhoto,
|
||||||
|
attachment: IconPaperclip,
|
||||||
|
select: IconSelect,
|
||||||
|
checkbox: IconCheckbox,
|
||||||
|
radio: IconCircleDot
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onNameFocus (e) {
|
||||||
|
if (!this.field.name) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$refs.name.$refs.contenteditable.innerText = ' '
|
||||||
|
}, 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeDropdown () {
|
||||||
|
document.activeElement.blur()
|
||||||
|
},
|
||||||
|
onNameBlur (e) {
|
||||||
|
if (e.target.innerText.trim()) {
|
||||||
|
this.field.name = e.target.innerText.trim()
|
||||||
|
} else {
|
||||||
|
this.field.name = ''
|
||||||
|
this.$refs.name.$refs.contenteditable.innerText = this.defaultName
|
||||||
|
}
|
||||||
|
},
|
||||||
removeArea (area) {
|
removeArea (area) {
|
||||||
this.field.areas.splice(this.field.areas.indexOf(area), 1)
|
this.field.areas.splice(this.field.areas.indexOf(area), 1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,67 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-2">
|
<div class="mb-2">
|
||||||
<Field
|
<Field
|
||||||
v-for="field in fields"
|
v-for="field in fields"
|
||||||
:key="field.uuid"
|
:key="field.uuid"
|
||||||
class="border"
|
|
||||||
:field="field"
|
:field="field"
|
||||||
|
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
|
||||||
@remove="fields.splice(fields.indexOf($event), 1)"
|
@remove="fields.splice(fields.indexOf($event), 1)"
|
||||||
|
@move-up="move(field, -1)"
|
||||||
|
@move-down="move(field, 1)"
|
||||||
|
@scroll-to="$emit('scroll-to-area', $event)"
|
||||||
@set-draw="$emit('set-draw', $event)"
|
@set-draw="$emit('set-draw', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div class="grid grid-cols-3 gap-1">
|
||||||
v-for="item in fieldTypes"
|
<button
|
||||||
:key="item.type"
|
v-for="item in fieldTypes"
|
||||||
draggable="true"
|
:key="item.type"
|
||||||
class="w-full flex items-center justify-center"
|
draggable="true"
|
||||||
@dragstart="onDragstart(item.value)"
|
class="flex items-center justify-center border border-dashed border-gray-300 bg-base-100 w-full rounded relative"
|
||||||
@dragend="$emit('drag-end')"
|
@dragstart="onDragstart(item.value)"
|
||||||
@click="addField(item.value)"
|
@dragend="$emit('drag-end')"
|
||||||
>
|
@click="addField(item.value)"
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="cursor-move"
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
fill="none"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
>
|
>
|
||||||
<path
|
<div class="w-0 absolute left-0">
|
||||||
stroke="none"
|
<svg
|
||||||
d="M0 0h24v24H0z"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
class="cursor-grab"
|
||||||
/>
|
width="18"
|
||||||
<path d="M4 6l16 0" />
|
height="18"
|
||||||
<path d="M4 12l16 0" />
|
viewBox="0 0 24 24"
|
||||||
<path d="M4 18l16 0" />
|
stroke-width="1.5"
|
||||||
</svg>
|
stroke="currentColor"
|
||||||
Add {{ item.label }}
|
fill="none"
|
||||||
+
|
stroke-linecap="round"
|
||||||
</button>
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="none"
|
||||||
|
d="M0 0h24v24H0z"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
<path d="M9 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
<path d="M9 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
<path d="M9 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
<path d="M15 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
<path d="M15 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
<path d="M15 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center flex-col px-2 py-2">
|
||||||
|
<component :is="item.icon" />
|
||||||
|
<span class="text-xs mt-1">
|
||||||
|
{{ item.label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Field from './field'
|
import Field from './field'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
|
import { IconTextSize, IconWriting, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplateFields',
|
name: 'TemplateFields',
|
||||||
@@ -59,18 +74,18 @@ export default {
|
|||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['set-draw', 'set-drag', 'drag-end'],
|
emits: ['set-draw', 'set-drag', 'drag-end', 'scroll-to-area'],
|
||||||
computed: {
|
computed: {
|
||||||
fieldTypes () {
|
fieldTypes () {
|
||||||
return [
|
return [
|
||||||
{ label: 'Text', value: 'text' },
|
{ label: 'Text', value: 'text', icon: IconTextSize },
|
||||||
{ label: 'Signature', value: 'signature' },
|
{ label: 'Signature', value: 'signature', icon: IconWriting },
|
||||||
{ label: 'Date', value: 'date' },
|
{ label: 'Date', value: 'date', icon: IconCalendarEvent },
|
||||||
{ label: 'Image', value: 'image' },
|
{ label: 'Image', value: 'image', icon: IconPhoto },
|
||||||
{ label: 'Attachment', value: 'attachment' },
|
{ label: 'File', value: 'attachment', icon: IconPaperclip },
|
||||||
{ label: 'Select', value: 'select' },
|
{ label: 'Select', value: 'select', icon: IconSelect },
|
||||||
{ label: 'Checkbox', value: 'checkbox' },
|
{ label: 'Checkbox', value: 'checkbox', icon: IconCheckbox },
|
||||||
{ label: 'Radio Group', value: 'radio' }
|
{ label: 'Radio', value: 'radio', icon: IconCircleDot }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -78,6 +93,19 @@ export default {
|
|||||||
onDragstart (fieldType) {
|
onDragstart (fieldType) {
|
||||||
this.$emit('set-drag', fieldType)
|
this.$emit('set-drag', fieldType)
|
||||||
},
|
},
|
||||||
|
move (field, direction) {
|
||||||
|
const currentIndex = this.fields.indexOf(field)
|
||||||
|
|
||||||
|
this.fields.splice(currentIndex, 1)
|
||||||
|
|
||||||
|
if (currentIndex + direction > this.fields.length) {
|
||||||
|
this.fields.unshift(field)
|
||||||
|
} else if (currentIndex + direction < 0) {
|
||||||
|
this.fields.push(field)
|
||||||
|
} else {
|
||||||
|
this.fields.splice(currentIndex + direction, 0, field)
|
||||||
|
}
|
||||||
|
},
|
||||||
addField (type, area = null) {
|
addField (type, area = null) {
|
||||||
const field = {
|
const field = {
|
||||||
name: type === 'signature' ? 'Signature' : '',
|
name: type === 'signature' ? 'Signature' : '',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div class="relative cursor-crosshair">
|
||||||
<img
|
<img
|
||||||
ref="image"
|
ref="image"
|
||||||
:src="image.url"
|
:src="image.url"
|
||||||
@@ -10,10 +10,12 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="top-0 bottom-0 left-0 right-0 absolute"
|
class="top-0 bottom-0 left-0 right-0 absolute"
|
||||||
|
@pointerdown="onStartDraw"
|
||||||
>
|
>
|
||||||
<FieldArea
|
<FieldArea
|
||||||
v-for="(item, i) in areas"
|
v-for="(item, i) in areas"
|
||||||
:key="i"
|
:key="i"
|
||||||
|
:ref="setAreaRefs"
|
||||||
:bounds="item.area"
|
:bounds="item.area"
|
||||||
:field="item.field"
|
:field="item.field"
|
||||||
@start-resize="showMask = true"
|
@start-resize="showMask = true"
|
||||||
@@ -27,11 +29,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-show="isDraw || isDrag || showMask"
|
v-show="isDrag || showMask"
|
||||||
id="mask"
|
id="mask"
|
||||||
ref="mask"
|
ref="mask"
|
||||||
class="top-0 bottom-0 left-0 right-0 absolute"
|
class="top-0 bottom-0 left-0 right-0 absolute"
|
||||||
@pointerdown="onPointerdown"
|
|
||||||
@pointermove="onPointermove"
|
@pointermove="onPointermove"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
@drop="onDrop"
|
@drop="onDrop"
|
||||||
@@ -58,11 +59,6 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
isDraw: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
isDrag: {
|
isDrag: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -76,6 +72,7 @@ export default {
|
|||||||
emits: ['draw', 'drop-field'],
|
emits: ['draw', 'drop-field'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
areaRefs: [],
|
||||||
showMask: false,
|
showMask: false,
|
||||||
newArea: null
|
newArea: null
|
||||||
}
|
}
|
||||||
@@ -88,7 +85,15 @@ export default {
|
|||||||
return this.image.metadata.height
|
return this.image.metadata.height
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
beforeUpdate () {
|
||||||
|
this.areaRefs = []
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setAreaRefs (el) {
|
||||||
|
if (el) {
|
||||||
|
this.areaRefs.push(el)
|
||||||
|
}
|
||||||
|
},
|
||||||
onDrop (e) {
|
onDrop (e) {
|
||||||
this.$emit('drop-field', {
|
this.$emit('drop-field', {
|
||||||
x: e.layerX / this.$refs.mask.clientWidth,
|
x: e.layerX / this.$refs.mask.clientWidth,
|
||||||
@@ -98,8 +103,10 @@ export default {
|
|||||||
page: this.number
|
page: this.number
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onPointerdown (e) {
|
onStartDraw (e) {
|
||||||
if (this.isDraw) {
|
this.showMask = true
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
this.newArea = {
|
this.newArea = {
|
||||||
initialX: e.layerX / this.$refs.mask.clientWidth,
|
initialX: e.layerX / this.$refs.mask.clientWidth,
|
||||||
initialY: e.layerY / this.$refs.mask.clientHeight,
|
initialY: e.layerY / this.$refs.mask.clientHeight,
|
||||||
@@ -108,7 +115,7 @@ export default {
|
|||||||
w: 0,
|
w: 0,
|
||||||
h: 0
|
h: 0
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
},
|
},
|
||||||
onPointermove (e) {
|
onPointermove (e) {
|
||||||
if (this.newArea) {
|
if (this.newArea) {
|
||||||
@@ -132,16 +139,17 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPointerup (e) {
|
onPointerup (e) {
|
||||||
if (this.isDraw && this.newArea) {
|
if (this.newArea) {
|
||||||
this.$emit('draw', {
|
this.$emit('draw', {
|
||||||
x: this.newArea.x,
|
x: this.newArea.x,
|
||||||
y: this.newArea.y,
|
y: this.newArea.y,
|
||||||
w: Math.max(this.newArea.w, this.$refs.mask.clientWidth / 5 / this.$refs.mask.clientWidth),
|
w: this.newArea.w,
|
||||||
h: Math.max(this.newArea.h, this.$refs.mask.clientWidth / 30 / this.$refs.mask.clientWidth),
|
h: this.newArea.h,
|
||||||
page: this.number
|
page: this.number
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.showMask = false
|
||||||
this.newArea = null
|
this.newArea = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,10 +46,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex py-2">
|
<div class="flex pb-2 pt-1.5">
|
||||||
<Contenteditable
|
<Contenteditable
|
||||||
:model-value="item.name"
|
:model-value="item.name"
|
||||||
:icon-width="16"
|
:icon-width="16"
|
||||||
|
style="max-width: 95%"
|
||||||
class="mx-auto"
|
class="mx-auto"
|
||||||
@update:model-value="onUpdateName"
|
@update:model-value="onUpdateName"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<turbo-frame id="modal"></turbo-frame>
|
<turbo-frame id="modal"></turbo-frame>
|
||||||
<%= render 'shared/navbar' %>
|
<%= render 'shared/navbar' %>
|
||||||
<% if flash.present? %>
|
<% if flash.present? %>
|
||||||
<div id="flash" class="absolute top-0 w-full">
|
<div id="flash" class="absolute top-0 w-full h-0">
|
||||||
<div class="max-w-xl mx-auto mt-1.5">
|
<div class="max-w-xl mx-auto mt-1.5">
|
||||||
<div class="alert py-3">
|
<div class="alert py-3">
|
||||||
<div class="flex w-full justify-between">
|
<div class="flex w-full justify-between">
|
||||||
|
|||||||
@@ -5599,9 +5599,9 @@ yaml@^1.10.0:
|
|||||||
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
|
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
|
||||||
|
|
||||||
yaml@^2.1.1:
|
yaml@^2.1.1:
|
||||||
version "2.2.2"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.2.2.tgz#ec551ef37326e6d42872dad1970300f8eb83a073"
|
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
|
||||||
integrity sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==
|
integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==
|
||||||
|
|
||||||
yocto-queue@^0.1.0:
|
yocto-queue@^0.1.0:
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user