fix(tasks): show drag handle icon on mobile devices (#2286)

Resolves https://github.com/go-vikunja/vikunja/issues/2228
This commit is contained in:
kolaente
2026-02-24 14:37:33 +01:00
committed by GitHub
parent 0c7c07b3b8
commit 4325eae4d4
2 changed files with 84 additions and 3 deletions
@@ -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;