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