Datenbank bereinigt / Gitea-Integration gefixt
Dieser Commit ist enthalten in:
committet von
Server Deploy
Ursprung
395598c2b0
Commit
c21be47428
696
frontend/js/mobile.js
Normale Datei
696
frontend/js/mobile.js
Normale Datei
@ -0,0 +1,696 @@
|
||||
/**
|
||||
* 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 scrollable elements
|
||||
const target = e.target;
|
||||
if (target.closest('.column-body') ||
|
||||
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')) {
|
||||
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
|
||||
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) {
|
||||
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');
|
||||
}
|
||||
|
||||
// =====================
|
||||
// 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;
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren