763 Zeilen
21 KiB
JavaScript
763 Zeilen
21 KiB
JavaScript
/**
|
|
* TASKMATE - Mobile Module
|
|
* ========================
|
|
* Touch-Gesten, Hamburger-Menu, Swipe-Navigation
|
|
*/
|
|
|
|
import { $, $$ } from './utils.js';
|
|
|
|
class MobileManager {
|
|
constructor() {
|
|
// State
|
|
this.isMenuOpen = false;
|
|
this.isMobile = false;
|
|
this.currentView = 'board';
|
|
|
|
// Swipe state
|
|
this.touchStartX = 0;
|
|
this.touchStartY = 0;
|
|
this.touchCurrentX = 0;
|
|
this.touchCurrentY = 0;
|
|
this.touchStartTime = 0;
|
|
this.isSwiping = false;
|
|
this.swipeDirection = null;
|
|
|
|
// Touch drag & drop state
|
|
this.touchDraggedElement = null;
|
|
this.touchDragPlaceholder = null;
|
|
this.touchDragStartX = 0;
|
|
this.touchDragStartY = 0;
|
|
this.touchDragOffsetX = 0;
|
|
this.touchDragOffsetY = 0;
|
|
this.touchDragScrollInterval = null;
|
|
this.longPressTimer = null;
|
|
|
|
// Constants
|
|
this.SWIPE_THRESHOLD = 50;
|
|
this.SWIPE_VELOCITY_THRESHOLD = 0.3;
|
|
this.MOBILE_BREAKPOINT = 768;
|
|
this.LONG_PRESS_DURATION = 300;
|
|
|
|
// View order for swipe navigation
|
|
this.viewOrder = ['board', 'list', 'calendar', 'proposals', 'gitea', 'knowledge'];
|
|
|
|
// DOM elements
|
|
this.hamburgerBtn = null;
|
|
this.mobileMenu = null;
|
|
this.mobileOverlay = null;
|
|
this.mainContent = null;
|
|
this.swipeIndicatorLeft = null;
|
|
this.swipeIndicatorRight = null;
|
|
}
|
|
|
|
/**
|
|
* Initialize mobile features
|
|
*/
|
|
init() {
|
|
// Check if mobile
|
|
this.checkMobile();
|
|
window.addEventListener('resize', () => this.checkMobile());
|
|
|
|
// Cache DOM elements
|
|
this.hamburgerBtn = $('#hamburger-btn');
|
|
this.mobileMenu = $('#mobile-menu');
|
|
this.mobileOverlay = $('#mobile-menu-overlay');
|
|
this.mainContent = $('.main-content');
|
|
this.swipeIndicatorLeft = $('#swipe-indicator-left');
|
|
this.swipeIndicatorRight = $('#swipe-indicator-right');
|
|
|
|
// Bind events
|
|
this.bindMenuEvents();
|
|
this.bindSwipeEvents();
|
|
this.bindTouchDragEvents();
|
|
|
|
// Listen for view changes
|
|
document.addEventListener('view:changed', (e) => {
|
|
this.currentView = e.detail?.view || 'board';
|
|
this.updateActiveNavItem(this.currentView);
|
|
});
|
|
|
|
// Listen for project changes
|
|
document.addEventListener('projects:loaded', () => {
|
|
this.populateMobileProjectSelect();
|
|
});
|
|
|
|
// Listen for user updates
|
|
document.addEventListener('user:updated', () => {
|
|
this.updateUserInfo();
|
|
});
|
|
|
|
console.log('[Mobile] Initialized');
|
|
}
|
|
|
|
/**
|
|
* Check if current viewport is mobile
|
|
*/
|
|
checkMobile() {
|
|
this.isMobile = window.innerWidth <= this.MOBILE_BREAKPOINT;
|
|
}
|
|
|
|
// =====================
|
|
// HAMBURGER MENU
|
|
// =====================
|
|
|
|
/**
|
|
* Bind menu events
|
|
*/
|
|
bindMenuEvents() {
|
|
// Hamburger button
|
|
this.hamburgerBtn?.addEventListener('click', () => this.toggleMenu());
|
|
|
|
// Close button
|
|
$('#mobile-menu-close')?.addEventListener('click', () => this.closeMenu());
|
|
|
|
// Overlay click
|
|
this.mobileOverlay?.addEventListener('click', () => this.closeMenu());
|
|
|
|
// Navigation items
|
|
$$('.mobile-nav-item').forEach(item => {
|
|
item.addEventListener('click', () => {
|
|
const view = item.dataset.view;
|
|
this.switchView(view);
|
|
this.closeMenu();
|
|
});
|
|
});
|
|
|
|
// Project selector
|
|
$('#mobile-project-select')?.addEventListener('change', (e) => {
|
|
const projectId = parseInt(e.target.value);
|
|
if (projectId) {
|
|
document.dispatchEvent(new CustomEvent('project:selected', {
|
|
detail: { projectId }
|
|
}));
|
|
this.closeMenu();
|
|
}
|
|
});
|
|
|
|
// Admin button
|
|
$('#mobile-admin-btn')?.addEventListener('click', () => {
|
|
this.closeMenu();
|
|
document.dispatchEvent(new CustomEvent('admin:open'));
|
|
});
|
|
|
|
// Logout button
|
|
$('#mobile-logout-btn')?.addEventListener('click', () => {
|
|
this.closeMenu();
|
|
document.dispatchEvent(new CustomEvent('auth:logout'));
|
|
});
|
|
|
|
// Escape key to close
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape' && this.isMenuOpen) {
|
|
this.closeMenu();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Toggle menu open/close
|
|
*/
|
|
toggleMenu() {
|
|
if (this.isMenuOpen) {
|
|
this.closeMenu();
|
|
} else {
|
|
this.openMenu();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Open mobile menu
|
|
*/
|
|
openMenu() {
|
|
this.isMenuOpen = true;
|
|
this.hamburgerBtn?.classList.add('active');
|
|
this.hamburgerBtn?.setAttribute('aria-expanded', 'true');
|
|
this.mobileMenu?.classList.add('open');
|
|
this.mobileMenu?.setAttribute('aria-hidden', 'false');
|
|
this.mobileOverlay?.classList.add('visible');
|
|
document.body.classList.add('mobile-menu-open');
|
|
|
|
// Update user info when menu opens
|
|
this.updateUserInfo();
|
|
this.populateMobileProjectSelect();
|
|
|
|
// Focus close button
|
|
setTimeout(() => {
|
|
$('#mobile-menu-close')?.focus();
|
|
}, 300);
|
|
}
|
|
|
|
/**
|
|
* Close mobile menu
|
|
*/
|
|
closeMenu() {
|
|
this.isMenuOpen = false;
|
|
this.hamburgerBtn?.classList.remove('active');
|
|
this.hamburgerBtn?.setAttribute('aria-expanded', 'false');
|
|
this.mobileMenu?.classList.remove('open');
|
|
this.mobileMenu?.setAttribute('aria-hidden', 'true');
|
|
this.mobileOverlay?.classList.remove('visible');
|
|
document.body.classList.remove('mobile-menu-open');
|
|
|
|
// Return focus
|
|
this.hamburgerBtn?.focus();
|
|
}
|
|
|
|
/**
|
|
* Switch to a different view
|
|
*/
|
|
switchView(view) {
|
|
if (!this.viewOrder.includes(view)) return;
|
|
|
|
this.currentView = view;
|
|
|
|
// Update desktop tabs
|
|
$$('.view-tab').forEach(tab => {
|
|
tab.classList.toggle('active', tab.dataset.view === view);
|
|
});
|
|
|
|
// Show/hide views
|
|
$$('.view').forEach(v => {
|
|
const viewName = v.id.replace('view-', '');
|
|
const isActive = viewName === view;
|
|
v.classList.toggle('active', isActive);
|
|
v.classList.toggle('hidden', !isActive);
|
|
});
|
|
|
|
// Update mobile nav
|
|
this.updateActiveNavItem(view);
|
|
|
|
// Dispatch event for other modules
|
|
document.dispatchEvent(new CustomEvent('view:changed', {
|
|
detail: { view }
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Update active nav item in mobile menu
|
|
*/
|
|
updateActiveNavItem(view) {
|
|
$$('.mobile-nav-item').forEach(item => {
|
|
item.classList.toggle('active', item.dataset.view === view);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Populate project select dropdown
|
|
*/
|
|
populateMobileProjectSelect() {
|
|
const select = $('#mobile-project-select');
|
|
const desktopSelect = $('#project-select');
|
|
if (!select || !desktopSelect) return;
|
|
|
|
// Copy options from desktop select
|
|
select.innerHTML = desktopSelect.innerHTML;
|
|
select.value = desktopSelect.value;
|
|
}
|
|
|
|
/**
|
|
* Update user info in mobile menu
|
|
*/
|
|
updateUserInfo() {
|
|
const avatar = $('#mobile-user-avatar');
|
|
const name = $('#mobile-user-name');
|
|
const role = $('#mobile-user-role');
|
|
const adminBtn = $('#mobile-admin-btn');
|
|
|
|
// Get user info from desktop user dropdown
|
|
const desktopAvatar = $('#user-avatar');
|
|
const desktopDropdown = $('.user-dropdown');
|
|
|
|
if (avatar && desktopAvatar) {
|
|
avatar.textContent = desktopAvatar.textContent;
|
|
avatar.style.backgroundColor = desktopAvatar.style.backgroundColor || 'var(--primary)';
|
|
}
|
|
|
|
if (name) {
|
|
const usernameEl = desktopDropdown?.querySelector('.user-info strong');
|
|
name.textContent = usernameEl?.textContent || 'Benutzer';
|
|
}
|
|
|
|
if (role) {
|
|
const roleEl = desktopDropdown?.querySelector('.user-info span:not(strong)');
|
|
role.textContent = roleEl?.textContent || 'Angemeldet';
|
|
}
|
|
|
|
// Show admin button for admins
|
|
if (adminBtn) {
|
|
const isAdmin = role?.textContent?.toLowerCase().includes('admin');
|
|
adminBtn.classList.toggle('hidden', !isAdmin);
|
|
}
|
|
}
|
|
|
|
// =====================
|
|
// SWIPE NAVIGATION
|
|
// =====================
|
|
|
|
/**
|
|
* Bind swipe events
|
|
*/
|
|
bindSwipeEvents() {
|
|
if (!this.mainContent) return;
|
|
|
|
this.mainContent.addEventListener('touchstart', (e) => this.handleSwipeStart(e), { passive: true });
|
|
this.mainContent.addEventListener('touchmove', (e) => this.handleSwipeMove(e), { passive: false });
|
|
this.mainContent.addEventListener('touchend', (e) => this.handleSwipeEnd(e), { passive: true });
|
|
this.mainContent.addEventListener('touchcancel', () => this.resetSwipe(), { passive: true });
|
|
}
|
|
|
|
/**
|
|
* Handle swipe start
|
|
*/
|
|
handleSwipeStart(e) {
|
|
if (!this.isMobile) return;
|
|
|
|
// Don't swipe if menu is open
|
|
if (this.isMenuOpen) return;
|
|
|
|
// Don't swipe if modal is open
|
|
if ($('.modal-overlay:not(.hidden)')) return;
|
|
|
|
// Don't swipe on specific interactive elements, but allow swipe in column-body
|
|
const target = e.target;
|
|
if (target.closest('.modal') ||
|
|
target.closest('.calendar-grid') ||
|
|
target.closest('.knowledge-entry-list') ||
|
|
target.closest('.list-table') ||
|
|
target.closest('input') ||
|
|
target.closest('textarea') ||
|
|
target.closest('select') ||
|
|
target.closest('button') ||
|
|
target.closest('a[href]') ||
|
|
target.closest('.task-card .priority-stars') ||
|
|
target.closest('.task-card .task-counts')) {
|
|
return;
|
|
}
|
|
|
|
// Only single touch
|
|
if (e.touches.length !== 1) return;
|
|
|
|
this.touchStartX = e.touches[0].clientX;
|
|
this.touchStartY = e.touches[0].clientY;
|
|
this.touchStartTime = Date.now();
|
|
this.isSwiping = false;
|
|
this.swipeDirection = null;
|
|
}
|
|
|
|
/**
|
|
* Handle swipe move
|
|
*/
|
|
handleSwipeMove(e) {
|
|
if (!this.isMobile || this.touchStartX === 0) return;
|
|
|
|
const touch = e.touches[0];
|
|
this.touchCurrentX = touch.clientX;
|
|
this.touchCurrentY = touch.clientY;
|
|
|
|
const deltaX = this.touchCurrentX - this.touchStartX;
|
|
const deltaY = this.touchCurrentY - this.touchStartY;
|
|
|
|
// Determine direction on first significant movement
|
|
if (!this.swipeDirection && (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10)) {
|
|
if (Math.abs(deltaX) > Math.abs(deltaY) * 1.5) {
|
|
this.swipeDirection = 'horizontal';
|
|
this.isSwiping = true;
|
|
document.body.classList.add('is-swiping');
|
|
} else {
|
|
this.swipeDirection = 'vertical';
|
|
this.resetSwipe();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (this.swipeDirection !== 'horizontal') return;
|
|
|
|
// Prevent scroll
|
|
e.preventDefault();
|
|
|
|
// Show indicators based on current view
|
|
if (this.currentView === 'board') {
|
|
// In board view: show column navigation indicators
|
|
const columns = $$('.column');
|
|
const currentColumnIndex = this.getCurrentColumnIndex();
|
|
|
|
if (deltaX > this.SWIPE_THRESHOLD && currentColumnIndex > 0) {
|
|
this.swipeIndicatorLeft?.classList.add('visible');
|
|
this.swipeIndicatorRight?.classList.remove('visible');
|
|
} else if (deltaX < -this.SWIPE_THRESHOLD && currentColumnIndex < columns.length - 1) {
|
|
this.swipeIndicatorRight?.classList.add('visible');
|
|
this.swipeIndicatorLeft?.classList.remove('visible');
|
|
} else {
|
|
this.swipeIndicatorLeft?.classList.remove('visible');
|
|
this.swipeIndicatorRight?.classList.remove('visible');
|
|
}
|
|
} else {
|
|
// In other views: show view navigation indicators
|
|
const currentIndex = this.viewOrder.indexOf(this.currentView);
|
|
|
|
if (deltaX > this.SWIPE_THRESHOLD && currentIndex > 0) {
|
|
this.swipeIndicatorLeft?.classList.add('visible');
|
|
this.swipeIndicatorRight?.classList.remove('visible');
|
|
} else if (deltaX < -this.SWIPE_THRESHOLD && currentIndex < this.viewOrder.length - 1) {
|
|
this.swipeIndicatorRight?.classList.add('visible');
|
|
this.swipeIndicatorLeft?.classList.remove('visible');
|
|
} else {
|
|
this.swipeIndicatorLeft?.classList.remove('visible');
|
|
this.swipeIndicatorRight?.classList.remove('visible');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle swipe end
|
|
*/
|
|
handleSwipeEnd() {
|
|
if (!this.isSwiping || this.swipeDirection !== 'horizontal') {
|
|
this.resetSwipe();
|
|
return;
|
|
}
|
|
|
|
const deltaX = this.touchCurrentX - this.touchStartX;
|
|
const deltaTime = Date.now() - this.touchStartTime;
|
|
const velocity = Math.abs(deltaX) / deltaTime;
|
|
|
|
// Valid swipe?
|
|
const isValidSwipe = Math.abs(deltaX) > this.SWIPE_THRESHOLD || velocity > this.SWIPE_VELOCITY_THRESHOLD;
|
|
|
|
if (isValidSwipe) {
|
|
if (this.currentView === 'board') {
|
|
// In board view: navigate between columns
|
|
const columns = $$('.column');
|
|
const currentColumnIndex = this.getCurrentColumnIndex();
|
|
|
|
if (deltaX > 0 && currentColumnIndex > 0) {
|
|
// Swipe right - previous column
|
|
this.scrollToColumn(currentColumnIndex - 1);
|
|
} else if (deltaX < 0 && currentColumnIndex < columns.length - 1) {
|
|
// Swipe left - next column
|
|
this.scrollToColumn(currentColumnIndex + 1);
|
|
}
|
|
} else {
|
|
// In other views: navigate between views
|
|
const currentIndex = this.viewOrder.indexOf(this.currentView);
|
|
|
|
if (deltaX > 0 && currentIndex > 0) {
|
|
// Swipe right - previous view
|
|
this.switchView(this.viewOrder[currentIndex - 1]);
|
|
} else if (deltaX < 0 && currentIndex < this.viewOrder.length - 1) {
|
|
// Swipe left - next view
|
|
this.switchView(this.viewOrder[currentIndex + 1]);
|
|
}
|
|
}
|
|
}
|
|
|
|
this.resetSwipe();
|
|
}
|
|
|
|
/**
|
|
* Reset swipe state
|
|
*/
|
|
resetSwipe() {
|
|
this.touchStartX = 0;
|
|
this.touchStartY = 0;
|
|
this.touchCurrentX = 0;
|
|
this.touchCurrentY = 0;
|
|
this.touchStartTime = 0;
|
|
this.isSwiping = false;
|
|
this.swipeDirection = null;
|
|
document.body.classList.remove('is-swiping');
|
|
this.swipeIndicatorLeft?.classList.remove('visible');
|
|
this.swipeIndicatorRight?.classList.remove('visible');
|
|
}
|
|
|
|
/**
|
|
* Get current visible column index in mobile board view
|
|
*/
|
|
getCurrentColumnIndex() {
|
|
const boardContainer = $('.board-container');
|
|
if (!boardContainer) return 0;
|
|
|
|
const containerWidth = boardContainer.offsetWidth;
|
|
const scrollLeft = boardContainer.scrollLeft;
|
|
const columnWidth = 300; // Approximate column width in mobile
|
|
|
|
return Math.round(scrollLeft / columnWidth);
|
|
}
|
|
|
|
/**
|
|
* Scroll to specific column in mobile board view
|
|
*/
|
|
scrollToColumn(columnIndex) {
|
|
const boardContainer = $('.board-container');
|
|
if (!boardContainer) return;
|
|
|
|
const columnWidth = 300; // Approximate column width in mobile
|
|
const targetScrollLeft = columnIndex * columnWidth;
|
|
|
|
boardContainer.scrollTo({
|
|
left: targetScrollLeft,
|
|
behavior: 'smooth'
|
|
});
|
|
}
|
|
|
|
// =====================
|
|
// TOUCH DRAG & DROP
|
|
// =====================
|
|
|
|
/**
|
|
* Bind touch drag events
|
|
*/
|
|
bindTouchDragEvents() {
|
|
const board = $('#board');
|
|
if (!board) return;
|
|
|
|
board.addEventListener('touchstart', (e) => this.handleTouchDragStart(e), { passive: false });
|
|
board.addEventListener('touchmove', (e) => this.handleTouchDragMove(e), { passive: false });
|
|
board.addEventListener('touchend', (e) => this.handleTouchDragEnd(e), { passive: true });
|
|
board.addEventListener('touchcancel', () => this.cancelTouchDrag(), { passive: true });
|
|
}
|
|
|
|
/**
|
|
* Handle touch drag start
|
|
*/
|
|
handleTouchDragStart(e) {
|
|
if (!this.isMobile) return;
|
|
|
|
const taskCard = e.target.closest('.task-card');
|
|
if (!taskCard) return;
|
|
|
|
// Cancel if multi-touch
|
|
if (e.touches.length > 1) {
|
|
this.cancelTouchDrag();
|
|
return;
|
|
}
|
|
|
|
const touch = e.touches[0];
|
|
this.touchDragStartX = touch.clientX;
|
|
this.touchDragStartY = touch.clientY;
|
|
|
|
// Long press to start drag
|
|
this.longPressTimer = setTimeout(() => {
|
|
this.startTouchDrag(taskCard, touch);
|
|
}, this.LONG_PRESS_DURATION);
|
|
}
|
|
|
|
/**
|
|
* Start touch drag
|
|
*/
|
|
startTouchDrag(taskCard, touch) {
|
|
this.touchDraggedElement = taskCard;
|
|
const rect = taskCard.getBoundingClientRect();
|
|
|
|
// Calculate offset
|
|
this.touchDragOffsetX = touch.clientX - rect.left;
|
|
this.touchDragOffsetY = touch.clientY - rect.top;
|
|
|
|
// Create placeholder
|
|
this.touchDragPlaceholder = document.createElement('div');
|
|
this.touchDragPlaceholder.className = 'task-card touch-drag-placeholder';
|
|
this.touchDragPlaceholder.style.height = rect.height + 'px';
|
|
taskCard.parentNode.insertBefore(this.touchDragPlaceholder, taskCard);
|
|
|
|
// Style dragged element
|
|
taskCard.classList.add('touch-dragging');
|
|
taskCard.style.position = 'fixed';
|
|
taskCard.style.left = rect.left + 'px';
|
|
taskCard.style.top = rect.top + 'px';
|
|
taskCard.style.width = rect.width + 'px';
|
|
taskCard.style.zIndex = '1000';
|
|
|
|
document.body.classList.add('is-touch-dragging');
|
|
|
|
// Haptic feedback
|
|
if (navigator.vibrate) {
|
|
navigator.vibrate(50);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle touch drag move
|
|
*/
|
|
handleTouchDragMove(e) {
|
|
// Cancel long press if finger moved
|
|
if (this.longPressTimer && !this.touchDraggedElement) {
|
|
const touch = e.touches[0];
|
|
const deltaX = Math.abs(touch.clientX - this.touchDragStartX);
|
|
const deltaY = Math.abs(touch.clientY - this.touchDragStartY);
|
|
if (deltaX > 10 || deltaY > 10) {
|
|
clearTimeout(this.longPressTimer);
|
|
this.longPressTimer = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!this.touchDraggedElement) return;
|
|
|
|
e.preventDefault();
|
|
|
|
const touch = e.touches[0];
|
|
const taskCard = this.touchDraggedElement;
|
|
|
|
// Move element
|
|
taskCard.style.left = (touch.clientX - this.touchDragOffsetX) + 'px';
|
|
taskCard.style.top = (touch.clientY - this.touchDragOffsetY) + 'px';
|
|
|
|
// Find drop target
|
|
taskCard.style.pointerEvents = 'none';
|
|
const elemBelow = document.elementFromPoint(touch.clientX, touch.clientY);
|
|
taskCard.style.pointerEvents = '';
|
|
|
|
const columnBody = elemBelow?.closest('.column-body');
|
|
|
|
// Remove previous indicators
|
|
$$('.column-body.touch-drag-over').forEach(el => el.classList.remove('touch-drag-over'));
|
|
|
|
if (columnBody) {
|
|
columnBody.classList.add('touch-drag-over');
|
|
}
|
|
|
|
// Auto-scroll
|
|
this.autoScrollWhileDragging(touch);
|
|
}
|
|
|
|
/**
|
|
* Auto-scroll while dragging near edges
|
|
*/
|
|
autoScrollWhileDragging(touch) {
|
|
const board = $('#board');
|
|
if (!board) return;
|
|
|
|
const boardRect = board.getBoundingClientRect();
|
|
const scrollThreshold = 50;
|
|
const scrollSpeed = 8;
|
|
|
|
// Clear existing interval
|
|
if (this.touchDragScrollInterval) {
|
|
clearInterval(this.touchDragScrollInterval);
|
|
this.touchDragScrollInterval = null;
|
|
}
|
|
|
|
// Scroll left
|
|
if (touch.clientX < boardRect.left + scrollThreshold) {
|
|
this.touchDragScrollInterval = setInterval(() => {
|
|
board.scrollLeft -= scrollSpeed;
|
|
}, 16);
|
|
}
|
|
// Scroll right
|
|
else if (touch.clientX > boardRect.right - scrollThreshold) {
|
|
this.touchDragScrollInterval = setInterval(() => {
|
|
board.scrollLeft += scrollSpeed;
|
|
}, 16);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle touch drag end
|
|
*/
|
|
handleTouchDragEnd(e) {
|
|
// Clear long press timer
|
|
if (this.longPressTimer) {
|
|
clearTimeout(this.longPressTimer);
|
|
this.longPressTimer = null;
|
|
}
|
|
|
|
if (!this.touchDraggedElement) return;
|
|
|
|
const touch = e.changedTouches[0];
|
|
|
|
// Find drop target
|
|
this.touchDraggedElement.style.pointerEvents = 'none';
|
|
const elemBelow = document.elementFromPoint(touch.clientX, touch.clientY);
|
|
this.touchDraggedElement.style.pointerEvents = '';
|
|
|
|
const columnBody = elemBelow?.closest('.column-body');
|
|
|
|
if (columnBody) {
|
|
const columnId = parseInt(columnBody.closest('.column').dataset.columnId);
|
|
const taskId = parseInt(this.touchDraggedElement.dataset.taskId);
|
|
const position = this.calculateDropPosition(columnBody, touch.clientY);
|
|
|
|
// Dispatch move event
|
|
document.dispatchEvent(new CustomEvent('task:move', {
|
|
detail: { taskId, columnId, position }
|
|
}));
|
|
}
|
|
|
|
this.cleanupTouchDrag();
|
|
}
|
|
|
|
/**
|
|
* Calculate drop position in column
|
|
*/
|
|
calculateDropPosition(columnBody, mouseY) {
|
|
const taskCards = Array.from(columnBody.querySelectorAll('.task-card:not(.touch-dragging):not(.touch-drag-placeholder)'));
|
|
let position = taskCards.length;
|
|
|
|
for (let i = 0; i < taskCards.length; i++) {
|
|
const rect = taskCards[i].getBoundingClientRect();
|
|
if (mouseY < rect.top + rect.height / 2) {
|
|
position = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return position;
|
|
}
|
|
|
|
/**
|
|
* Cancel touch drag
|
|
*/
|
|
cancelTouchDrag() {
|
|
if (this.longPressTimer) {
|
|
clearTimeout(this.longPressTimer);
|
|
this.longPressTimer = null;
|
|
}
|
|
this.cleanupTouchDrag();
|
|
}
|
|
|
|
/**
|
|
* Cleanup after touch drag
|
|
*/
|
|
cleanupTouchDrag() {
|
|
// Clear scroll interval
|
|
if (this.touchDragScrollInterval) {
|
|
clearInterval(this.touchDragScrollInterval);
|
|
this.touchDragScrollInterval = null;
|
|
}
|
|
|
|
// Reset dragged element
|
|
if (this.touchDraggedElement) {
|
|
this.touchDraggedElement.classList.remove('touch-dragging');
|
|
this.touchDraggedElement.style.position = '';
|
|
this.touchDraggedElement.style.left = '';
|
|
this.touchDraggedElement.style.top = '';
|
|
this.touchDraggedElement.style.width = '';
|
|
this.touchDraggedElement.style.zIndex = '';
|
|
this.touchDraggedElement.style.transform = '';
|
|
}
|
|
|
|
// Remove placeholder
|
|
if (this.touchDragPlaceholder) {
|
|
this.touchDragPlaceholder.remove();
|
|
this.touchDragPlaceholder = null;
|
|
}
|
|
|
|
// Remove indicators
|
|
$$('.column-body.touch-drag-over').forEach(el => el.classList.remove('touch-drag-over'));
|
|
|
|
document.body.classList.remove('is-touch-dragging');
|
|
|
|
// Reset state
|
|
this.touchDraggedElement = null;
|
|
this.touchDragStartX = 0;
|
|
this.touchDragStartY = 0;
|
|
this.touchDragOffsetX = 0;
|
|
this.touchDragOffsetY = 0;
|
|
}
|
|
}
|
|
|
|
// Create and export singleton
|
|
const mobileManager = new MobileManager();
|
|
|
|
export default mobileManager;
|