make builder usable on mobile
This commit is contained in:
@@ -76,7 +76,6 @@ document.addEventListener('turbo:submit-end', async (event) => {
|
|||||||
window.customElements.define('template-builder', class extends HTMLElement {
|
window.customElements.define('template-builder', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
this.appElem.classList.add('max-h-screen')
|
|
||||||
|
|
||||||
this.app = createApp(TemplateBuilder, {
|
this.app = createApp(TemplateBuilder, {
|
||||||
template: reactive(JSON.parse(this.dataset.template)),
|
template: reactive(JSON.parse(this.dataset.template)),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
:style="positionStyle"
|
:style="positionStyle"
|
||||||
@pointerdown.stop
|
@pointerdown.stop
|
||||||
@mousedown.stop="startDrag"
|
@mousedown.stop="startDrag"
|
||||||
|
@touchstart="startTouchDrag"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="isSelected || isDraw"
|
v-if="isSelected || isDraw"
|
||||||
@@ -32,7 +33,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="field?.type"
|
v-if="field?.type"
|
||||||
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap group-hover:flex group-hover:z-10"
|
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap group-hover:flex group-hover:z-10"
|
||||||
:class="{ 'flex z-10': isNameFocus || isSelected, hidden: !isNameFocus && !isSelected }"
|
:class="{ 'flex z-10': isNameFocus || isSelected, invisible: !isNameFocus && !isSelected }"
|
||||||
style="top: -25px; height: 25px"
|
style="top: -25px; height: 25px"
|
||||||
@mousedown.stop
|
@mousedown.stop
|
||||||
@pointerdown.stop
|
@pointerdown.stop
|
||||||
@@ -108,12 +109,14 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
ref="touchTarget"
|
||||||
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
|
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="field?.type"
|
v-if="field?.type"
|
||||||
class="h-2.5 w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize"
|
class="h-4 w-4 md:h-2.5 md:w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize"
|
||||||
@mousedown.stop="startResize"
|
@mousedown.stop="startResize"
|
||||||
|
@touchstart="startTouchResize"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -309,6 +312,47 @@ export default {
|
|||||||
|
|
||||||
this.$emit('start-drag')
|
this.$emit('start-drag')
|
||||||
},
|
},
|
||||||
|
startTouchDrag (e) {
|
||||||
|
if (e.target !== this.$refs.touchTarget) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$refs?.name?.blur()
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.isDragged = true
|
||||||
|
|
||||||
|
const rect = e.target.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.selectedAreaRef.value = this.area
|
||||||
|
|
||||||
|
this.dragFrom = { x: rect.left - e.touches[0].clientX, y: rect.top - e.touches[0].clientY }
|
||||||
|
|
||||||
|
this.$el.getRootNode().addEventListener('touchmove', this.touchDrag)
|
||||||
|
this.$el.getRootNode().addEventListener('touchend', this.stopTouchDrag)
|
||||||
|
|
||||||
|
this.$emit('start-drag')
|
||||||
|
},
|
||||||
|
touchDrag (e) {
|
||||||
|
const page = this.$parent.$refs.mask.previousSibling
|
||||||
|
const rect = page.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.area.x = (this.dragFrom.x + e.touches[0].clientX - rect.left) / rect.width
|
||||||
|
this.area.y = (this.dragFrom.y + e.touches[0].clientY - rect.top) / rect.height
|
||||||
|
},
|
||||||
|
stopTouchDrag () {
|
||||||
|
this.$el.getRootNode().removeEventListener('touchmove', this.touchDrag)
|
||||||
|
this.$el.getRootNode().removeEventListener('touchend', this.stopTouchDrag)
|
||||||
|
|
||||||
|
if (this.isDragged) {
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isDragged = false
|
||||||
|
|
||||||
|
this.$emit('stop-drag')
|
||||||
|
},
|
||||||
stopDrag () {
|
stopDrag () {
|
||||||
this.$el.getRootNode().removeEventListener('mousemove', this.drag)
|
this.$el.getRootNode().removeEventListener('mousemove', this.drag)
|
||||||
this.$el.getRootNode().removeEventListener('mouseup', this.stopDrag)
|
this.$el.getRootNode().removeEventListener('mouseup', this.stopDrag)
|
||||||
@@ -335,6 +379,33 @@ export default {
|
|||||||
|
|
||||||
this.$emit('stop-resize')
|
this.$emit('stop-resize')
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
startTouchResize (e) {
|
||||||
|
this.selectedAreaRef.value = this.area
|
||||||
|
|
||||||
|
this.$refs?.name?.blur()
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.$el.getRootNode().addEventListener('touchmove', this.touchResize)
|
||||||
|
this.$el.getRootNode().addEventListener('touchend', this.stopTouchResize)
|
||||||
|
|
||||||
|
this.$emit('start-resize', 'nwse')
|
||||||
|
},
|
||||||
|
touchResize (e) {
|
||||||
|
const page = this.$parent.$refs.mask.previousSibling
|
||||||
|
const rect = page.getBoundingClientRect()
|
||||||
|
|
||||||
|
this.area.w = (e.touches[0].clientX - rect.left) / rect.width - this.area.x
|
||||||
|
this.area.h = (e.touches[0].clientY - rect.top) / rect.height - this.area.y
|
||||||
|
},
|
||||||
|
stopTouchResize () {
|
||||||
|
this.$el.getRootNode().removeEventListener('touchmove', this.touchResize)
|
||||||
|
this.$el.getRootNode().removeEventListener('touchend', this.stopTouchResize)
|
||||||
|
|
||||||
|
this.$emit('stop-resize')
|
||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
style="max-width: 1600px"
|
style="max-width: 1600px"
|
||||||
class="mx-auto pl-4 h-full"
|
class="mx-auto pl-3 md:pl-4 h-full"
|
||||||
>
|
>
|
||||||
<div class="flex justify-between py-1.5 items-center pr-4">
|
<div class="flex justify-between py-1.5 items-center pr-4 sticky top-0 z-10 bg-base-100">
|
||||||
<div class="flex space-x-3">
|
<div class="flex space-x-3">
|
||||||
<a
|
<a
|
||||||
v-if="withLogo"
|
v-if="withLogo"
|
||||||
@@ -59,10 +59,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="flex md:max-h-[calc(100vh-60px)]">
|
||||||
class="flex"
|
|
||||||
style="max-height: calc(100% - 60px)"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
ref="previews"
|
ref="previews"
|
||||||
:style="{ 'display': isBreakpointLg ? 'none' : 'initial' }"
|
:style="{ 'display': isBreakpointLg ? 'none' : 'initial' }"
|
||||||
@@ -97,7 +94,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-hidden md:overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
|
||||||
<div
|
<div
|
||||||
ref="documents"
|
ref="documents"
|
||||||
class="pr-3.5 pl-0.5"
|
class="pr-3.5 pl-0.5"
|
||||||
@@ -152,17 +149,54 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<span
|
||||||
v-if="sortedDocuments.length"
|
v-if="drawField"
|
||||||
class="sticky md:hidden"
|
class="fixed text-center w-full left-1/2 bottom-0 transform -translate-x-1/2"
|
||||||
style="bottom: 100px"
|
|
||||||
>
|
>
|
||||||
<div class="px-4 py-3 rounded-2xl bg-base-200 flex items-center justify-between ml-4 mr-6">
|
<span
|
||||||
<span class="w-full text-center text-lg">
|
class="rounded bg-base-200 px-4 py-2 rounded-full inline-flex space-x-2 mx-auto items-center mb-4 z-20 md:hidden"
|
||||||
You need a larger screen to use builder tools.
|
>
|
||||||
|
<component
|
||||||
|
:is="fieldIcons[drawField.type]"
|
||||||
|
:width="20"
|
||||||
|
:height="20"
|
||||||
|
class="inline"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
Draw {{ fieldNames[drawField.type] }} Field
|
||||||
</span>
|
</span>
|
||||||
</div>
|
<a
|
||||||
</div>
|
href="#"
|
||||||
|
class="link block text-center"
|
||||||
|
@click.prevent="drawField = null"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<FieldType
|
||||||
|
v-if="sortedDocuments.length && !drawField"
|
||||||
|
class="dropdown-top dropdown-end fixed bottom-4 right-4 z-10 md:hidden"
|
||||||
|
:model-value="''"
|
||||||
|
@update:model-value="startFieldDraw($event)"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="btn btn-neutral text-white btn-circle btn-lg group"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<IconPlus
|
||||||
|
class="group-focus:hidden"
|
||||||
|
width="28"
|
||||||
|
height="28"
|
||||||
|
/>
|
||||||
|
<IconX
|
||||||
|
class="hidden group-focus:inline"
|
||||||
|
width="28"
|
||||||
|
height="28"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</FieldType>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block"
|
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block"
|
||||||
@@ -214,7 +248,8 @@ import Logo from './logo'
|
|||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
import DocumentPreview from './preview'
|
import DocumentPreview from './preview'
|
||||||
import DocumentControls from './controls'
|
import DocumentControls from './controls'
|
||||||
import { IconUsersPlus, IconDeviceFloppy, IconInnerShadowTop } from '@tabler/icons-vue'
|
import FieldType from './field_type'
|
||||||
|
import { IconUsersPlus, IconDeviceFloppy, IconInnerShadowTop, IconPlus, IconX } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
@@ -224,6 +259,9 @@ export default {
|
|||||||
Upload,
|
Upload,
|
||||||
Document,
|
Document,
|
||||||
Fields,
|
Fields,
|
||||||
|
IconPlus,
|
||||||
|
FieldType,
|
||||||
|
IconX,
|
||||||
Logo,
|
Logo,
|
||||||
Dropzone,
|
Dropzone,
|
||||||
DocumentPreview,
|
DocumentPreview,
|
||||||
@@ -300,6 +338,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
selectedAreaRef: () => ref(),
|
selectedAreaRef: () => ref(),
|
||||||
fieldAreasIndex () {
|
fieldAreasIndex () {
|
||||||
const areas = {}
|
const areas = {}
|
||||||
@@ -351,6 +391,22 @@ export default {
|
|||||||
this.documentRefs = []
|
this.documentRefs = []
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
startFieldDraw (type) {
|
||||||
|
const field = {
|
||||||
|
name: '',
|
||||||
|
uuid: v4(),
|
||||||
|
required: type !== 'checkbox',
|
||||||
|
areas: [],
|
||||||
|
submitter_uuid: this.selectedSubmitter.uuid,
|
||||||
|
type
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['select', 'multiple', 'radio'].includes(type)) {
|
||||||
|
field.options = ['']
|
||||||
|
}
|
||||||
|
|
||||||
|
this.drawField = field
|
||||||
|
},
|
||||||
undo () {
|
undo () {
|
||||||
if (this.undoStack.length > 1) {
|
if (this.undoStack.length > 1) {
|
||||||
this.undoStack.pop()
|
this.undoStack.pop()
|
||||||
@@ -448,6 +504,10 @@ export default {
|
|||||||
this.drawField.areas ||= []
|
this.drawField.areas ||= []
|
||||||
this.drawField.areas.push(area)
|
this.drawField.areas.push(area)
|
||||||
|
|
||||||
|
if (this.template.fields.indexOf(this.drawField) === -1) {
|
||||||
|
this.template.fields.push(this.drawField)
|
||||||
|
}
|
||||||
|
|
||||||
this.drawField = null
|
this.drawField = null
|
||||||
|
|
||||||
this.selectedAreaRef.value = area
|
this.selectedAreaRef.value = area
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<span class="dropdown">
|
<span class="dropdown">
|
||||||
<label
|
<slot>
|
||||||
tabindex="0"
|
<label
|
||||||
:title="fieldNames[modelValue]"
|
tabindex="0"
|
||||||
class="cursor-pointer"
|
:title="fieldNames[modelValue]"
|
||||||
>
|
class="cursor-pointer"
|
||||||
<component
|
>
|
||||||
:is="fieldIcons[modelValue]"
|
<component
|
||||||
:width="buttonWidth"
|
:is="fieldIcons[modelValue]"
|
||||||
:class="buttonClasses"
|
:width="buttonWidth"
|
||||||
:stroke-width="1.6"
|
:class="buttonClasses"
|
||||||
/>
|
:stroke-width="1.6"
|
||||||
</label>
|
/>
|
||||||
|
</label>
|
||||||
|
</slot>
|
||||||
<ul
|
<ul
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10"
|
class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10 mb-3"
|
||||||
:class="menuClasses"
|
:class="menuClasses"
|
||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative cursor-crosshair select-none">
|
<div
|
||||||
|
class="relative cursor-crosshair select-none"
|
||||||
|
:style="drawField ? 'touch-action: none' : ''"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
ref="image"
|
ref="image"
|
||||||
:src="image.url"
|
:src="image.url"
|
||||||
@@ -32,12 +35,13 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-show="resizeDirection || isMove || isDrag || showMask"
|
v-show="resizeDirection || isMove || isDrag || showMask || (drawField && isMobile)"
|
||||||
id="mask"
|
id="mask"
|
||||||
ref="mask"
|
ref="mask"
|
||||||
class="top-0 bottom-0 left-0 right-0 absolute z-10"
|
class="top-0 bottom-0 left-0 right-0 absolute z-10"
|
||||||
:class="{ 'cursor-grab': isDrag || isMove, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
|
:class="{ 'cursor-grab': isDrag || isMove, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
|
||||||
@pointermove="onPointermove"
|
@pointermove="onPointermove"
|
||||||
|
@pointerdown="onStartDraw"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
@drop="onDrop"
|
@drop="onDrop"
|
||||||
@pointerup="onPointerup"
|
@pointerup="onPointerup"
|
||||||
@@ -93,6 +97,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isMobile () {
|
||||||
|
return /android|iphone|ipad/i.test(navigator.userAgent)
|
||||||
|
},
|
||||||
resizeDirectionClasses () {
|
resizeDirectionClasses () {
|
||||||
return {
|
return {
|
||||||
nwse: 'cursor-nwse-resize',
|
nwse: 'cursor-nwse-resize',
|
||||||
@@ -125,6 +132,10 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onStartDraw (e) {
|
onStartDraw (e) {
|
||||||
|
if (this.isMobile && !this.drawField) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.showMask = true
|
this.showMask = true
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user