mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-02-25 06:23:49 +00:00
fix(tasks): show drag handle icon on mobile devices (#2286)
Resolves https://github.com/go-vikunja/vikunja/issues/2228
This commit is contained in:
@@ -148,6 +148,8 @@
|
||||
|
||||
<draggable
|
||||
v-bind="DRAG_OPTIONS"
|
||||
:handle="taskDragHandle"
|
||||
:delay="isTouchDevice ? 300 : 1000"
|
||||
:model-value="bucket.tasks"
|
||||
:group="{name: 'tasks', put: shouldAcceptDrop(bucket) && !dragBucket}"
|
||||
:disabled="!canWrite"
|
||||
@@ -214,6 +216,13 @@
|
||||
class="task-item"
|
||||
:data-task-id="task.id"
|
||||
>
|
||||
<span
|
||||
v-if="canWrite && isTouchDevice"
|
||||
class="handle"
|
||||
@click="openTask(task)"
|
||||
@touchstart.passive="onHandleTouchStart"
|
||||
@touchmove.passive="onHandleTouchMove"
|
||||
/>
|
||||
<KanbanCard
|
||||
class="kanban-card"
|
||||
:task="task"
|
||||
@@ -281,6 +290,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, nextTick, ref, watch, toRef} from 'vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
import {useRouteQuery} from '@vueuse/router'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import draggable from 'zhyswan-vuedraggable'
|
||||
@@ -436,6 +446,40 @@ const project = computed(() => projectId.value ? projectStore.projects[projectId
|
||||
const view = computed(() => project.value?.views.find(v => v.id === props.viewId) as IProjectView || null)
|
||||
const canWrite = computed(() => baseStore.currentProject?.maxPermission > Permissions.READ && view.value.bucketConfigurationMode === 'manual')
|
||||
const canCreateTasks = computed(() => canWrite.value && projectId.value > 0)
|
||||
|
||||
const isTouchDevice = ref(false)
|
||||
if (typeof window !== 'undefined') {
|
||||
isTouchDevice.value = !window.matchMedia('(hover: hover) and (pointer: fine)').matches
|
||||
}
|
||||
const taskDragHandle = computed(() => isTouchDevice.value ? '.handle' : undefined)
|
||||
|
||||
const router = useRouter()
|
||||
const touchStartY = ref(0)
|
||||
|
||||
function openTask(task: ITask) {
|
||||
router.push({
|
||||
name: 'task.detail',
|
||||
params: {id: task.id},
|
||||
state: {backdropView: router.currentRoute.value.fullPath},
|
||||
})
|
||||
}
|
||||
|
||||
function onHandleTouchStart(e: TouchEvent) {
|
||||
touchStartY.value = e.touches[0].clientY
|
||||
}
|
||||
|
||||
function onHandleTouchMove(e: TouchEvent) {
|
||||
if (drag.value) return
|
||||
|
||||
const currentY = e.touches[0].clientY
|
||||
const deltaY = touchStartY.value - currentY
|
||||
const scrollContainer = (e.target as HTMLElement).closest('.tasks') as HTMLElement | null
|
||||
if (scrollContainer) {
|
||||
scrollContainer.scrollTop += deltaY
|
||||
touchStartY.value = currentY
|
||||
}
|
||||
}
|
||||
|
||||
const buckets = computed(() => kanbanStore.buckets)
|
||||
const loading = computed(() => kanbanStore.isLoading)
|
||||
const projectIdWithFallback = computed<number>(() => project.value?.id || projectId.value)
|
||||
@@ -954,6 +998,7 @@ $filter-container-height: '1rem - #{$switch-view-height}';
|
||||
.task-item {
|
||||
background-color: var(--grey-100);
|
||||
padding: .25rem .5rem;
|
||||
position: relative;
|
||||
|
||||
&:first-of-type {
|
||||
padding-block-start: .5rem;
|
||||
@@ -962,6 +1007,16 @@ $filter-container-height: '1rem - #{$switch-view-height}';
|
||||
&:last-of-type {
|
||||
padding-block-end: .5rem;
|
||||
}
|
||||
|
||||
.handle {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
touch-action: none;
|
||||
-webkit-touch-callout: none;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.no-move {
|
||||
|
||||
@@ -60,8 +60,9 @@
|
||||
type: 'transition-group'
|
||||
}"
|
||||
:animation="100"
|
||||
:delay-on-touch-only="true"
|
||||
:delay="1000"
|
||||
:handle="dragHandle"
|
||||
:delay-on-touch-only="!isTouchDevice"
|
||||
:delay="isTouchDevice ? 0 : 1000"
|
||||
ghost-class="task-ghost"
|
||||
@start="handleDragStart"
|
||||
@end="saveTaskPosition"
|
||||
@@ -75,7 +76,14 @@
|
||||
:the-task="t"
|
||||
:all-tasks="allTasks"
|
||||
@taskUpdated="updateTasks"
|
||||
/>
|
||||
>
|
||||
<span
|
||||
v-if="canDragTasks"
|
||||
class="icon handle"
|
||||
>
|
||||
<Icon icon="grip-lines" />
|
||||
</span>
|
||||
</SingleTaskInProject>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
@@ -193,6 +201,12 @@ onMounted(async () => {
|
||||
|
||||
const canDragTasks = computed(() => canWrite.value || isSavedFilter(project.value))
|
||||
|
||||
const isTouchDevice = ref(false)
|
||||
if (typeof window !== 'undefined') {
|
||||
isTouchDevice.value = !window.matchMedia('(hover: hover) and (pointer: fine)').matches
|
||||
}
|
||||
const dragHandle = computed(() => isTouchDevice.value ? '.handle' : undefined)
|
||||
|
||||
const addTaskRef = ref<typeof AddTask | null>(null)
|
||||
|
||||
function focusNewTaskInput() {
|
||||
@@ -373,6 +387,18 @@ onBeforeUnmount(() => {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:deep(.single-task .handle) {
|
||||
cursor: grab;
|
||||
margin-inline-end: .25rem;
|
||||
color: var(--grey-400);
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
:deep(.single-task .handle) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.tasks:not(.dragging-disabled) .single-task) {
|
||||
cursor: grab;
|
||||
-webkit-touch-callout: none;
|
||||
|
||||
Reference in New Issue
Block a user