diff --git a/frontend/src/components/project/views/ProjectKanban.vue b/frontend/src/components/project/views/ProjectKanban.vue
index ae1d65182..651a90484 100644
--- a/frontend/src/components/project/views/ProjectKanban.vue
+++ b/frontend/src/components/project/views/ProjectKanban.vue
@@ -148,6 +148,8 @@
+
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(() => 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 {
diff --git a/frontend/src/components/project/views/ProjectList.vue b/frontend/src/components/project/views/ProjectList.vue
index 71460bce4..270f99d14 100644
--- a/frontend/src/components/project/views/ProjectList.vue
+++ b/frontend/src/components/project/views/ProjectList.vue
@@ -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"
- />
+ >
+
+
+
+
@@ -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(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;