make builder usable on mobile

pull/133/head
DocuSeal 2 years ago
parent bfece574b9
commit 2e98885f41

@ -76,7 +76,6 @@ document.addEventListener('turbo:submit-end', async (event) => {
window.customElements.define('template-builder', class extends HTMLElement {
connectedCallback () {
this.appElem = document.createElement('div')
this.appElem.classList.add('max-h-screen')
this.app = createApp(TemplateBuilder, {
template: reactive(JSON.parse(this.dataset.template)),

@ -4,6 +4,7 @@
:style="positionStyle"
@pointerdown.stop
@mousedown.stop="startDrag"
@touchstart="startTouchDrag"
>
<div
v-if="isSelected || isDraw"
@ -32,7 +33,7 @@
<div
v-if="field?.type"
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"
@mousedown.stop
@pointerdown.stop
@ -108,12 +109,14 @@
</span>
</div>
<div
ref="touchTarget"
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
/>
<span
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"
@touchstart="startTouchResize"
/>
</div>
</template>
@ -309,6 +312,47 @@ export default {
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 () {
this.$el.getRootNode().removeEventListener('mousemove', this.drag)
this.$el.getRootNode().removeEventListener('mouseup', this.stopDrag)
@ -335,6 +379,33 @@ export default {
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()
}
}

@ -1,9 +1,9 @@
<template>
<div
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">
<a
v-if="withLogo"
@ -59,10 +59,7 @@
</template>
</div>
</div>
<div
class="flex"
style="max-height: calc(100% - 60px)"
>
<div class="flex md:max-h-[calc(100vh-60px)]">
<div
ref="previews"
:style="{ 'display': isBreakpointLg ? 'none' : 'initial' }"
@ -97,7 +94,7 @@
/>
</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
ref="documents"
class="pr-3.5 pl-0.5"
@ -152,17 +149,54 @@
</div>
</template>
</div>
<div
v-if="sortedDocuments.length"
class="sticky md:hidden"
style="bottom: 100px"
<span
v-if="drawField"
class="fixed text-center w-full left-1/2 bottom-0 transform -translate-x-1/2"
>
<div class="px-4 py-3 rounded-2xl bg-base-200 flex items-center justify-between ml-4 mr-6">
<span class="w-full text-center text-lg">
You need a larger screen to use builder tools.
<span
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"
>
<component
:is="fieldIcons[drawField.type]"
:width="20"
:height="20"
class="inline"
:stroke-width="1.6"
/>
<span>
Draw {{ fieldNames[drawField.type] }} Field
</span>
</div>
</div>
<a
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
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 DocumentPreview from './preview'
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 { ref, computed } from 'vue'
@ -224,6 +259,9 @@ export default {
Upload,
Document,
Fields,
IconPlus,
FieldType,
IconX,
Logo,
Dropzone,
DocumentPreview,
@ -300,6 +338,8 @@ export default {
}
},
computed: {
fieldIcons: FieldType.computed.fieldIcons,
fieldNames: FieldType.computed.fieldNames,
selectedAreaRef: () => ref(),
fieldAreasIndex () {
const areas = {}
@ -351,6 +391,22 @@ export default {
this.documentRefs = []
},
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 () {
if (this.undoStack.length > 1) {
this.undoStack.pop()
@ -448,6 +504,10 @@ export default {
this.drawField.areas ||= []
this.drawField.areas.push(area)
if (this.template.fields.indexOf(this.drawField) === -1) {
this.template.fields.push(this.drawField)
}
this.drawField = null
this.selectedAreaRef.value = area

@ -1,20 +1,22 @@
<template>
<span class="dropdown">
<label
tabindex="0"
:title="fieldNames[modelValue]"
class="cursor-pointer"
>
<component
:is="fieldIcons[modelValue]"
:width="buttonWidth"
:class="buttonClasses"
:stroke-width="1.6"
/>
</label>
<slot>
<label
tabindex="0"
:title="fieldNames[modelValue]"
class="cursor-pointer"
>
<component
:is="fieldIcons[modelValue]"
:width="buttonWidth"
:class="buttonClasses"
:stroke-width="1.6"
/>
</label>
</slot>
<ul
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"
@click="closeDropdown"
>

@ -1,5 +1,8 @@
<template>
<div class="relative cursor-crosshair select-none">
<div
class="relative cursor-crosshair select-none"
:style="drawField ? 'touch-action: none' : ''"
>
<img
ref="image"
:src="image.url"
@ -32,12 +35,13 @@
/>
</div>
<div
v-show="resizeDirection || isMove || isDrag || showMask"
v-show="resizeDirection || isMove || isDrag || showMask || (drawField && isMobile)"
id="mask"
ref="mask"
class="top-0 bottom-0 left-0 right-0 absolute z-10"
:class="{ 'cursor-grab': isDrag || isMove, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
@pointermove="onPointermove"
@pointerdown="onStartDraw"
@dragover.prevent
@drop="onDrop"
@pointerup="onPointerup"
@ -93,6 +97,9 @@ export default {
}
},
computed: {
isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent)
},
resizeDirectionClasses () {
return {
nwse: 'cursor-nwse-resize',
@ -125,6 +132,10 @@ export default {
})
},
onStartDraw (e) {
if (this.isMobile && !this.drawField) {
return
}
this.showMask = true
this.$nextTick(() => {

Loading…
Cancel
Save