add cell field type
This commit is contained in:
@@ -17,7 +17,7 @@ module Api
|
||||
schema: [%i[attachment_uuid name]],
|
||||
submitters: [%i[name uuid]],
|
||||
fields: [[:uuid, :submitter_uuid, :name, :type, :required,
|
||||
{ options: [], areas: [%i[x y w h attachment_uuid page]] }]])
|
||||
{ options: [], areas: [%i[x y w h cell_w attachment_uuid page]] }]])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -41,8 +41,6 @@ window.customElements.define('template-builder', class extends HTMLElement {
|
||||
template: reactive(JSON.parse(this.dataset.template))
|
||||
})
|
||||
|
||||
this.app.config.globalProperties.$t = (key) => TemplateBuilder.i18n[key] || key
|
||||
|
||||
this.app.mount(this.appElem)
|
||||
|
||||
this.appendChild(this.appElem)
|
||||
|
||||
@@ -82,6 +82,19 @@
|
||||
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="field.type === 'cells'"
|
||||
class="w-full flex items-center"
|
||||
>
|
||||
<div
|
||||
v-for="(char, index) in modelValue"
|
||||
:key="index"
|
||||
class="text-center flex-none"
|
||||
:style="{ width: (area.cell_w / area.w * 100) + '%' }"
|
||||
>
|
||||
{{ char }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center px-0.5"
|
||||
@@ -100,7 +113,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck } from '@tabler/icons-vue'
|
||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3 } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'FieldArea',
|
||||
@@ -153,6 +166,7 @@ export default {
|
||||
fieldNames () {
|
||||
return {
|
||||
text: 'Text',
|
||||
cells: 'Text',
|
||||
signature: 'Signature',
|
||||
date: 'Date',
|
||||
image: 'Image',
|
||||
@@ -173,6 +187,7 @@ export default {
|
||||
select: IconSelect,
|
||||
checkbox: IconCheckbox,
|
||||
radio: IconCircleDot,
|
||||
cells: IconColumns3,
|
||||
multiple: IconChecks
|
||||
}
|
||||
},
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
type="hidden"
|
||||
>
|
||||
<div class="mt-4">
|
||||
<div v-if="currentField.type === 'text'">
|
||||
<div v-if="['cells', 'text'].includes(currentField.type)">
|
||||
<label
|
||||
v-if="currentField.name"
|
||||
:for="currentField.uuid"
|
||||
@@ -369,10 +369,14 @@ export default {
|
||||
})
|
||||
},
|
||||
saveStep () {
|
||||
return fetch(this.submitPath, {
|
||||
method: 'POST',
|
||||
body: new FormData(this.$refs.form)
|
||||
})
|
||||
if (this.isCompleted) {
|
||||
return Promise.resolve({})
|
||||
} else {
|
||||
return fetch(this.submitPath, {
|
||||
method: 'POST',
|
||||
body: new FormData(this.$refs.form)
|
||||
})
|
||||
}
|
||||
},
|
||||
async submitStep () {
|
||||
this.isSubmitting = true
|
||||
|
||||
@@ -6,10 +6,29 @@
|
||||
@mousedown.stop="startDrag"
|
||||
>
|
||||
<div
|
||||
v-if="isSelected || !field?.type"
|
||||
v-if="isSelected || isDraw"
|
||||
class="top-0 bottom-0 right-0 left-0 absolute border border-1.5 pointer-events-none"
|
||||
:class="borderColors[submitterIndex]"
|
||||
/>
|
||||
<div
|
||||
v-if="field.type === 'cells' && (isSelected || isDraw)"
|
||||
class="top-0 bottom-0 right-0 left-0 absolute"
|
||||
>
|
||||
<div
|
||||
v-for="(cellW, index) in cells"
|
||||
:key="index"
|
||||
class="absolute top-0 bottom-0 border-r"
|
||||
:class="borderColors[submitterIndex]"
|
||||
:style="{ left: (cellW / area.w * 100) + '%' }"
|
||||
>
|
||||
<span
|
||||
v-if="index === 0"
|
||||
class="h-2.5 w-2.5 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-ew-resize z-10"
|
||||
style="left: -4px"
|
||||
@mousedown.stop="startResizeCell"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="field?.type"
|
||||
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap group-hover:flex group-hover:z-10"
|
||||
@@ -118,6 +137,11 @@ export default {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isDraw: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
field: {
|
||||
type: Object,
|
||||
required: false,
|
||||
@@ -135,7 +159,21 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
defaultName: Field.computed.defaultName,
|
||||
fieldNames: FieldType.computed.fieldNames,
|
||||
fieldIcons: FieldType.computed.fieldIcons,
|
||||
cells () {
|
||||
const cells = []
|
||||
|
||||
let currentWidth = 0
|
||||
|
||||
while (currentWidth + (this.area.cell_w + this.area.cell_w / 4) < this.area.w) {
|
||||
currentWidth += this.area.cell_w || 9999999
|
||||
|
||||
cells.push(currentWidth)
|
||||
}
|
||||
|
||||
return cells
|
||||
},
|
||||
submitter () {
|
||||
return this.template.submitters.find((s) => s.uuid === this.field.submitter_uuid)
|
||||
},
|
||||
@@ -187,6 +225,29 @@ export default {
|
||||
}, 1)
|
||||
}
|
||||
},
|
||||
startResizeCell (e) {
|
||||
document.addEventListener('mousemove', this.onResizeCell)
|
||||
document.addEventListener('mouseup', this.stopResizeCell)
|
||||
|
||||
this.$emit('start-resize', 'ew')
|
||||
},
|
||||
stopResizeCell (e) {
|
||||
document.removeEventListener('mousemove', this.onResizeCell)
|
||||
document.removeEventListener('mouseup', this.stopResizeCell)
|
||||
|
||||
this.$emit('stop-resize')
|
||||
|
||||
this.save()
|
||||
},
|
||||
onResizeCell (e) {
|
||||
if (e.toElement.id === 'mask') {
|
||||
const positionX = e.layerX / (e.toElement.clientWidth - 1)
|
||||
|
||||
if (positionX > this.area.x) {
|
||||
this.area.cell_w = positionX - this.area.x
|
||||
}
|
||||
}
|
||||
},
|
||||
maybeUpdateOptions () {
|
||||
if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
|
||||
delete this.field.options
|
||||
@@ -195,6 +256,14 @@ export default {
|
||||
if (['select', 'multiple', 'radio'].includes(this.field.type)) {
|
||||
this.field.options ||= ['']
|
||||
}
|
||||
|
||||
(this.field.areas || []).forEach((area) => {
|
||||
if (this.field.type === 'cells') {
|
||||
area.cell_w = area.w * 2 / Math.floor(area.w / area.h)
|
||||
} else {
|
||||
delete area.cell_w
|
||||
}
|
||||
})
|
||||
},
|
||||
onNameBlur (e) {
|
||||
this.isNameFocus = false
|
||||
@@ -256,7 +325,7 @@ export default {
|
||||
document.addEventListener('mousemove', this.resize)
|
||||
document.addEventListener('mouseup', this.stopResize)
|
||||
|
||||
this.$emit('start-resize')
|
||||
this.$emit('start-resize', 'nwse')
|
||||
},
|
||||
stopResize () {
|
||||
document.removeEventListener('mousemove', this.resize)
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
:selected-submitter="selectedSubmitter"
|
||||
:document="document"
|
||||
:is-drag="!!dragFieldType"
|
||||
:is-draw="!!drawField"
|
||||
:draw-field="drawField"
|
||||
@draw="onDraw"
|
||||
@drop-field="onDropfield"
|
||||
@remove-area="removeArea"
|
||||
@@ -141,12 +141,10 @@ import Contenteditable from './contenteditable'
|
||||
import DocumentPreview from './preview'
|
||||
import { IconUsersPlus, IconDeviceFloppy, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||
import { v4 } from 'uuid'
|
||||
import i18n from './i18n'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'TemplateBuilder',
|
||||
i18n,
|
||||
components: {
|
||||
Upload,
|
||||
Document,
|
||||
@@ -352,6 +350,10 @@ export default {
|
||||
fieldArea.h = baseArea.h
|
||||
fieldArea.y = fieldArea.y - baseArea.h / 2
|
||||
|
||||
if (this.dragFieldType === 'cells') {
|
||||
fieldArea.cell_w = baseArea.cell_w || (baseArea.w / 5)
|
||||
}
|
||||
|
||||
field.areas = [fieldArea]
|
||||
|
||||
this.selectedAreaRef.value = fieldArea
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
:number="index"
|
||||
:areas="areasIndex[index]"
|
||||
:is-drag="isDrag"
|
||||
:is-draw="isDraw"
|
||||
:draw-field="drawField"
|
||||
:selected-submitter="selectedSubmitter"
|
||||
:image="image"
|
||||
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
|
||||
@@ -38,10 +38,10 @@ export default {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isDraw: {
|
||||
type: Boolean,
|
||||
drawField: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: false
|
||||
default: null
|
||||
},
|
||||
isDrag: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -206,12 +206,13 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fieldNames: FieldType.computed.fieldNames,
|
||||
defaultName () {
|
||||
const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field)
|
||||
|
||||
const suffix = { multiple: 'Select', radio: 'Group' }[this.field.type] || 'Field'
|
||||
|
||||
return `${this.$t(this.field.type)} ${suffix} ${typeIndex + 1}`
|
||||
return `${this.fieldNames[this.field.type]} ${suffix} ${typeIndex + 1}`
|
||||
},
|
||||
areas () {
|
||||
return this.field.areas || []
|
||||
@@ -241,6 +242,14 @@ export default {
|
||||
if (['radio', 'multiple', 'select'].includes(this.field.type)) {
|
||||
this.field.options ||= ['']
|
||||
}
|
||||
|
||||
(this.field.areas || []).forEach((area) => {
|
||||
if (this.field.type === 'cells') {
|
||||
area.cell_w = area.w * 2 / Math.floor(area.w / area.h)
|
||||
} else {
|
||||
delete area.cell_w
|
||||
}
|
||||
})
|
||||
},
|
||||
onNameBlur (e) {
|
||||
if (e.target.innerText.trim()) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<span class="dropdown">
|
||||
<label
|
||||
tabindex="0"
|
||||
:title="$t(modelValue)"
|
||||
:title="fieldNames[modelValue]"
|
||||
class="cursor-pointer"
|
||||
>
|
||||
<component
|
||||
@@ -33,7 +33,7 @@
|
||||
:stroke-width="1.6"
|
||||
:width="20"
|
||||
/>
|
||||
{{ $t(type) }}
|
||||
{{ fieldNames[type] }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -41,7 +41,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks } from '@tabler/icons-vue'
|
||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3 } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'FiledTypeDropdown',
|
||||
@@ -68,6 +68,20 @@ export default {
|
||||
},
|
||||
emits: ['update:model-value'],
|
||||
computed: {
|
||||
fieldNames () {
|
||||
return {
|
||||
text: 'Text',
|
||||
signature: 'Signature',
|
||||
date: 'Date',
|
||||
image: 'Image',
|
||||
file: 'File',
|
||||
select: 'Select',
|
||||
checkbox: 'Checkbox',
|
||||
multiple: 'Multiple',
|
||||
radio: 'Radio',
|
||||
cells: 'Cells'
|
||||
}
|
||||
},
|
||||
fieldIcons () {
|
||||
return {
|
||||
text: IconTextSize,
|
||||
@@ -77,6 +91,7 @@ export default {
|
||||
file: IconPaperclip,
|
||||
select: IconSelect,
|
||||
checkbox: IconCheckbox,
|
||||
cells: IconColumns3,
|
||||
multiple: IconChecks,
|
||||
radio: IconCircleDot
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<div class="flex items-center flex-col px-2 py-2">
|
||||
<component :is="icon" />
|
||||
<span class="text-xs mt-1">
|
||||
{{ $t(type) }}
|
||||
{{ fieldNames[type] }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
@@ -98,6 +98,7 @@ export default {
|
||||
},
|
||||
emits: ['set-draw', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter'],
|
||||
computed: {
|
||||
fieldNames: FieldType.computed.fieldNames,
|
||||
fieldIcons: FieldType.computed.fieldIcons,
|
||||
submitterFields () {
|
||||
return this.fields.filter((f) => f.submitter_uuid === this.selectedSubmitter.uuid)
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
export default {
|
||||
text: 'Text',
|
||||
signature: 'Signature',
|
||||
date: 'Date',
|
||||
image: 'Image',
|
||||
file: 'File',
|
||||
select: 'Select',
|
||||
checkbox: 'Checkbox',
|
||||
multiple: 'Multiple',
|
||||
radio: 'Radio'
|
||||
}
|
||||
@@ -18,24 +18,25 @@
|
||||
:ref="setAreaRefs"
|
||||
:area="item.area"
|
||||
:field="item.field"
|
||||
@start-resize="[showMask = true, isResize = true]"
|
||||
@stop-resize="[showMask = false, isResize = false]"
|
||||
@start-drag="[showMask = true, isMove = true]"
|
||||
@stop-drag="[showMask = false, isMove = false]"
|
||||
@start-resize="resizeDirection = $event"
|
||||
@stop-resize="resizeDirection = null"
|
||||
@start-drag="isMove = true"
|
||||
@stop-drag="isMove = false"
|
||||
@remove="$emit('remove-area', item.area)"
|
||||
/>
|
||||
<FieldArea
|
||||
v-if="newArea"
|
||||
:field="{ submitter_uuid: selectedSubmitter.uuid }"
|
||||
:is-draw="true"
|
||||
:field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || 'text' }"
|
||||
:area="newArea"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-show="isDrag || showMask"
|
||||
v-show="resizeDirection || isMove || isDrag || showMask"
|
||||
id="mask"
|
||||
ref="mask"
|
||||
class="top-0 bottom-0 left-0 right-0 absolute z-10"
|
||||
:class="{ 'cursor-grab': isDrag || isMove, 'cursor-nwse-resize': isResize || isDraw }"
|
||||
:class="{ 'cursor-grab': isDrag || isMove, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
|
||||
@pointermove="onPointermove"
|
||||
@dragover.prevent
|
||||
@drop="onDrop"
|
||||
@@ -66,10 +67,10 @@ export default {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isDraw: {
|
||||
type: Boolean,
|
||||
drawField: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: false
|
||||
default: null
|
||||
},
|
||||
isDrag: {
|
||||
type: Boolean,
|
||||
@@ -87,11 +88,17 @@ export default {
|
||||
areaRefs: [],
|
||||
showMask: false,
|
||||
isMove: false,
|
||||
isResize: false,
|
||||
resizeDirection: null,
|
||||
newArea: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
resizeDirectionClasses () {
|
||||
return {
|
||||
nwse: 'cursor-nwse-resize',
|
||||
ew: 'cursor-ew-resize'
|
||||
}
|
||||
},
|
||||
width () {
|
||||
return this.image.metadata.width
|
||||
},
|
||||
@@ -148,19 +155,29 @@ export default {
|
||||
this.newArea.y = e.layerY / this.$refs.mask.clientHeight
|
||||
}
|
||||
|
||||
if (this.drawField?.type === 'cells') {
|
||||
this.newArea.cell_w = this.newArea.h * (this.$refs.mask.clientHeight / this.$refs.mask.clientWidth)
|
||||
}
|
||||
|
||||
this.newArea.w = Math.abs(dx)
|
||||
this.newArea.h = Math.abs(dy)
|
||||
}
|
||||
},
|
||||
onPointerup (e) {
|
||||
if (this.newArea) {
|
||||
this.$emit('draw', {
|
||||
const area = {
|
||||
x: this.newArea.x,
|
||||
y: this.newArea.y,
|
||||
w: this.newArea.w,
|
||||
h: this.newArea.h,
|
||||
page: this.number
|
||||
})
|
||||
}
|
||||
|
||||
if ('cell_w' in this.newArea) {
|
||||
area.cell_w = this.newArea.cell_w
|
||||
}
|
||||
|
||||
this.$emit('draw', area)
|
||||
}
|
||||
|
||||
this.showMask = false
|
||||
|
||||
@@ -14,6 +14,15 @@
|
||||
<div class="w-full p-[0.2vw] flex items-center justify-center">
|
||||
<%= svg_icon('check', class: "aspect-square #{area['w'] > area['h'] ? '!w-auto !h-full' : '!w-full !h-auto'}") %>
|
||||
</div>
|
||||
<% elsif field['type'] == 'cells' %>
|
||||
<% cell_width = area['cell_w'] / area['w'] * 100 %>
|
||||
<div class="w-full flex items-center">
|
||||
<% (0..(area['w'] / area['cell_w']).ceil).each do |index| %>
|
||||
<% if value[index] %>
|
||||
<div class="text-center flex-none" style="width: <%= cell_width %>%;"><%= value[index] %></div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% elsif field['type'] == 'date' %>
|
||||
<div class="flex items-center px-0.5">
|
||||
<%= l(Date.parse(value)) %>
|
||||
|
||||
@@ -19,6 +19,8 @@ module Submissions
|
||||
# rubocop:disable Metrics
|
||||
def call(submitter)
|
||||
layouter = HexaPDF::Layout::TextLayouter.new(valign: :center)
|
||||
cell_layouter = HexaPDF::Layout::TextLayouter.new(valign: :center, align: :center)
|
||||
|
||||
template = submitter.submission.template
|
||||
|
||||
cert = submitter.submission.template.account.encrypted_configs
|
||||
@@ -129,6 +131,17 @@ module Submissions
|
||||
width: PdfIcons::WIDTH * scale,
|
||||
height: PdfIcons::HEIGHT * scale
|
||||
)
|
||||
when 'cells'
|
||||
cell_width = area['cell_w'] * width
|
||||
|
||||
value.chars.each_with_index do |char, index|
|
||||
text = HexaPDF::Layout::TextFragment.create(char, font: pdf.fonts.add(FONT_NAME),
|
||||
font_size: FONT_SIZE)
|
||||
|
||||
cell_layouter.fit([text], cell_width, area['h'] * height)
|
||||
.draw(canvas, ((area['x'] * width) + (cell_width * index)),
|
||||
height - (area['y'] * height))
|
||||
end
|
||||
else
|
||||
value = I18n.l(Date.parse(value)) if field['type'] == 'date'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user