From 0e9925ef42c57ccb0cc0caf38aa41be1b1292e33 Mon Sep 17 00:00:00 2001 From: zzj3720 Date: Mon, 9 Dec 2024 19:46:23 +0800 Subject: [PATCH 1/5] feat(database): kanban supports long-press drag-and-drop on mobile devices --- .../src/view-presets/kanban/mobile/card.ts | 565 +++++++++++++++++- .../src/view-presets/kanban/mobile/group.ts | 71 +++ .../playground/apps/starter/data/database.ts | 18 +- 3 files changed, 644 insertions(+), 10 deletions(-) diff --git a/packages/affine/data-view/src/view-presets/kanban/mobile/card.ts b/packages/affine/data-view/src/view-presets/kanban/mobile/card.ts index a56068436577..272c7905ef16 100644 --- a/packages/affine/data-view/src/view-presets/kanban/mobile/card.ts +++ b/packages/affine/data-view/src/view-presets/kanban/mobile/card.ts @@ -22,7 +22,7 @@ const styles = css` border: 0.5px solid var(--affine-border-color); box-shadow: 0px 2px 3px 0px rgba(0, 0, 0, 0.05); border-radius: 8px; - background-color: var(--affine-background-kanban-card-color); + background-color: var(--affine-background-primary-color); } .mobile-card-header { @@ -85,6 +85,64 @@ const styles = css` font-size: 16px; color: ${unsafeCSSVarV2('icon/primary')}; } + + mobile-kanban-card.dragging { + opacity: 0.6; + transform: scale(1.02); + box-shadow: var(--affine-shadow-2); + pointer-events: none; + position: relative; + z-index: 1000; + background-color: var(--affine-background-primary-color); + } + + mobile-kanban-card.drag-over { + position: relative; + } + + mobile-kanban-card.drag-over::before { + content: ''; + position: absolute; + left: -4px; + right: -4px; + height: 2px; + background: var(--affine-primary-color); + z-index: 1; + } + + mobile-kanban-card.drag-over-top::before { + top: -6px; + } + + mobile-kanban-card.drag-over-bottom::before { + bottom: -6px; + } + + .mobile-group-body.drag-over::before { + content: ''; + position: absolute; + left: 0; + right: 0; + height: 2px; + background: var(--affine-primary-color); + z-index: 1; + top: 50%; + } + + .mobile-add-card { + position: relative; + } + + .mobile-add-card.drag-over::before { + content: ''; + position: absolute; + left: -4px; + right: -4px; + height: 2px; + background: var(--affine-primary-color); + z-index: 1; + top: -6px; + } `; export class MobileKanbanCard extends SignalWatcher( @@ -111,6 +169,466 @@ export class MobileKanbanCard extends SignalWatcher( ); }; + private currentTouch: Touch | null = null; + + private handleTouchEnd = (e: TouchEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (this.longPressTimeout) { + clearTimeout(this.longPressTimeout); + this.longPressTimeout = null; + } + + if (this.isDragging) { + this.isDragging = false; + this.classList.remove('dragging'); + + // 重置 transform + this.style.transform = ''; + + // 获取目标(可能是卡片或空 group) + const target = document.querySelector( + '.drag-over-top, .drag-over-bottom, .mobile-group-body.drag-over' + ) as HTMLElement; + + if (target) { + // 获取目标所在的 group + const targetGroup = target.closest( + 'mobile-kanban-group' + ) as HTMLElement; + const targetGroupKey = targetGroup?.dataset.key; + + const event = new CustomEvent('dragend', { + detail: { + targetId: target.classList.contains('mobile-group-body') + ? null + : target.dataset.cardId, + position: target.classList.contains('drag-over-top') + ? 'top' + : 'bottom', + targetGroupKey: targetGroupKey, + }, + bubbles: true, + composed: true, + }); + + this.dispatchEvent(event); + } + + // 清除所有提示样式 + document + .querySelectorAll('.drag-over, .drag-over-top, .drag-over-bottom') + .forEach(el => { + el.classList.remove('drag-over'); + el.classList.remove('drag-over-top'); + el.classList.remove('drag-over-bottom'); + }); + } + + // 停止自动滚动 + this.stopAutoScroll(); + this.currentTouch = null; + + // 重置所有状态 + this.scrollOffset = { x: 0, y: 0 }; + this.initialPosition = { x: 0, y: 0 }; + this.touchOffset = { x: 0, y: 0 }; + }; + + private handleTouchMove = (e: TouchEvent) => { + if (!this.isDragging) { + if (this.longPressTimeout) { + clearTimeout(this.longPressTimeout); + this.longPressTimeout = null; + } + return; + } + + e.preventDefault(); + e.stopPropagation(); + + const touch = e.touches[0]; + this.currentTouch = touch; + + // 计算基础移动距离 + const deltaX = + touch.clientX - (this.initialPosition.x + this.touchOffset.x); + const deltaY = + touch.clientY - (this.initialPosition.y + this.touchOffset.y); + + // 计算滚动偏移 + const horizontalContainer = this.findScrollableParent(this, 'horizontal'); + const verticalContainer = this.findScrollableParent(this, 'vertical'); + const scrollLeft = horizontalContainer?.scrollLeft || 0; + const scrollTop = verticalContainer?.scrollTop || 0; + const scrollDeltaX = scrollLeft - this.initialScroll.x; + const scrollDeltaY = scrollTop - this.initialScroll.y; + + // 使用 transform 移动卡片,加上滚动偏移 + this.style.transform = `translate(${deltaX + scrollDeltaX}px, ${deltaY + scrollDeltaY}px) scale(1.02)`; + + // 启动自动滚动 + this.startAutoScroll(); + + // 获取所有 group + const allGroups = Array.from( + document.querySelectorAll('mobile-kanban-group') + ) as HTMLElement[]; + + // 清除之前的所有提示样式 + document.querySelectorAll('.drag-over').forEach(el => { + el.classList.remove('drag-over', 'drag-over-top', 'drag-over-bottom'); + }); + + // 获取当前触摸点下的元素 + const elementUnderTouch = document.elementFromPoint( + touch.clientX, + touch.clientY + ); + const cardUnderTouch = elementUnderTouch?.closest( + 'mobile-kanban-card' + ) as HTMLElement; + + // 如果触摸点在当前拖动的卡片上,或者是当前卡片的前后位置,不显示提示线 + if ( + elementUnderTouch === this || + this.contains(elementUnderTouch) || + (cardUnderTouch && cardUnderTouch.dataset.cardId === this.cardId) + ) { + return; + } + + // 找到当前触摸点所在的 group + let targetGroup: HTMLElement | null = null; + for (const group of allGroups) { + const rect = group.getBoundingClientRect(); + if (touch.clientX >= rect.left && touch.clientX <= rect.right) { + targetGroup = group; + break; + } + } + + if (!targetGroup) { + return; + } + + // 获取目标 group 中的所有卡片 + const cards = Array.from( + targetGroup.querySelectorAll('mobile-kanban-card') + ).filter(card => card !== this) as HTMLElement[]; + + let closestCard: HTMLElement | null = null; + let position: 'top' | 'bottom' = 'bottom'; + + const groupBody = targetGroup.querySelector( + '.mobile-group-body' + ) as HTMLElement; + if (!groupBody) return; + + const groupRect = groupBody.getBoundingClientRect(); + + if (cards.length === 0) { + // group 为空的情况 + if (touch.clientY >= groupRect.top && touch.clientY <= groupRect.bottom) { + const addCardButton = targetGroup.querySelector( + '.mobile-add-card' + ) as HTMLElement; + if (addCardButton) { + // 只有在不同 group 时才显示提示线 + if (targetGroup.dataset.key !== this.groupKey) { + addCardButton.classList.add('drag-over'); + closestCard = addCardButton; + position = 'top'; + } + } + } + } else { + // 获取当前卡片在目标组中的位置 + const currentIndex = cards.findIndex( + card => card.dataset.cardId === this.cardId + ); + const isInSameGroup = targetGroup.dataset.key === this.groupKey; + + // 检查是否在第一张卡片上方 + const firstCard = cards[0]; + const firstCardRect = firstCard.getBoundingClientRect(); + + if (touch.clientY < firstCardRect.top) { + // 如果是同一个 group,且当前卡片是第一张,不显示提示线 + if (!isInSameGroup || currentIndex > 0) { + closestCard = firstCard; + position = 'top'; + } + } + // 检查是否在最后一张卡片下方 + else { + const lastCard = cards[cards.length - 1]; + const lastCardRect = lastCard.getBoundingClientRect(); + + if (touch.clientY > lastCardRect.bottom) { + // 如果是同一个 group,且当前卡片是最后一张,不显示提示线 + if (!isInSameGroup || currentIndex < cards.length - 1) { + closestCard = lastCard; + position = 'bottom'; + } + } + // 在卡片之间 + else { + for (let i = 0; i < cards.length; i++) { + const card = cards[i]; + const rect = card.getBoundingClientRect(); + + if (touch.clientY >= rect.top && touch.clientY <= rect.bottom) { + const midY = rect.top + rect.height / 2; + + if (isInSameGroup) { + // 在同一个 group 中 + if (touch.clientY < midY) { + // 上半部分:不能是当前卡片的下一张卡片 + if (i !== currentIndex + 1 || currentIndex === 0) { + closestCard = card; + position = 'top'; + } + } else { + // 下半部分:不能是当前卡片的上一张卡片 + if ( + i !== currentIndex - 1 || + currentIndex === cards.length - 1 + ) { + closestCard = card; + position = 'bottom'; + } + } + } else { + // 不同 group,没有限制 + closestCard = card; + position = touch.clientY < midY ? 'top' : 'bottom'; + } + break; + } + } + } + } + } + + // 显示提示线 + if (closestCard) { + closestCard.classList.add('drag-over', `drag-over-${position}`); + } + }; + + private handleTouchStart = (e: TouchEvent) => { + if (this.view.readonly$.value) return; + + e.stopPropagation(); + + const touch = e.touches[0]; + const rect = this.getBoundingClientRect(); + + this.touchOffset = { + x: touch.clientX - rect.left, + y: touch.clientY - rect.top, + }; + + // 记录初始滚动位置 + const horizontalContainer = this.findScrollableParent(this, 'horizontal'); + const verticalContainer = this.findScrollableParent(this, 'vertical'); + this.initialScroll = { + x: horizontalContainer?.scrollLeft || 0, + y: verticalContainer?.scrollTop || 0, + }; + + this.longPressTimeout = window.setTimeout(() => { + this.isDragging = true; + this.classList.add('dragging'); + + // 记录初始位置 + this.initialPosition = { + x: rect.left, + y: rect.top, + }; + + // 触发拖动开事件 + const event = new CustomEvent('dragstart', { + detail: { + cardId: this.cardId, + groupKey: this.groupKey, + }, + bubbles: true, + composed: true, + }); + + this.dispatchEvent(event); + }, 150); + }; + + private initialPosition = { + x: 0, + y: 0, + }; + + private initialScroll = { + x: 0, + y: 0, + }; + + private isDragging = false; + + private lastScrollPosition = { + x: 0, + y: 0, + }; + + private longPressTimeout: number | null = null; + + private readonly MAX_SCROLL_SPEED = 15; // 降低最大滚动速度 + + private readonly MIN_SCROLL_SPEED = 2; // 最小滚动速度 + + private readonly SCROLL_EDGE_SIZE = 100; // 增大边缘区域 + + private scrollAnimationFrame: number | null = null; + + private scrollOffset = { + x: 0, + y: 0, + }; + + private startAutoScroll = () => { + if (this.scrollAnimationFrame) return; + + const scroll = () => { + if (!this.isDragging || !this.currentTouch) { + this.stopAutoScroll(); + return; + } + + const touch = this.currentTouch; + const verticalContainer = this.findScrollableParent(this, 'vertical'); + const horizontalContainer = this.findScrollableParent(this, 'horizontal'); + + let scrollX = 0; + let scrollY = 0; + + if (verticalContainer) { + const viewportHeight = window.innerHeight; + const distanceFromTop = touch.clientY; + const distanceFromBottom = viewportHeight - touch.clientY; + + if (distanceFromTop < this.SCROLL_EDGE_SIZE) { + scrollY = -this.calculateScrollSpeed(distanceFromTop); + } else if (distanceFromBottom < this.SCROLL_EDGE_SIZE) { + scrollY = this.calculateScrollSpeed(distanceFromBottom); + } + + if (scrollY !== 0) { + verticalContainer.scrollTop += scrollY; + } + } + + if (horizontalContainer) { + const rect = horizontalContainer.getBoundingClientRect(); + const distanceFromLeft = touch.clientX - rect.left; + const distanceFromRight = rect.right - touch.clientX; + + if (distanceFromLeft < this.SCROLL_EDGE_SIZE) { + scrollX = -this.calculateScrollSpeed(distanceFromLeft); + } else if (distanceFromRight < this.SCROLL_EDGE_SIZE) { + scrollX = this.calculateScrollSpeed(distanceFromRight); + } + + if (scrollX !== 0) { + horizontalContainer.scrollLeft += scrollX; + } + } + + // 更新卡片位置 + if (scrollX !== 0 || scrollY !== 0) { + const deltaX = + touch.clientX - (this.initialPosition.x + this.touchOffset.x); + const deltaY = + touch.clientY - (this.initialPosition.y + this.touchOffset.y); + const scrollLeft = horizontalContainer?.scrollLeft || 0; + const scrollTop = verticalContainer?.scrollTop || 0; + const scrollDeltaX = scrollLeft - this.initialScroll.x; + const scrollDeltaY = scrollTop - this.initialScroll.y; + + this.style.transform = `translate(${deltaX + scrollDeltaX}px, ${deltaY + scrollDeltaY}px) scale(1.02)`; + + // 触发一个新的 touchmove 事件来更新插入位置 + const touchMoveEvent = new TouchEvent('touchmove', { + touches: [ + new Touch({ + identifier: touch.identifier, + target: touch.target as EventTarget, + clientX: touch.clientX, + clientY: touch.clientY, + screenX: touch.screenX, + screenY: touch.screenY, + pageX: touch.pageX, + pageY: touch.pageY, + radiusX: touch.radiusX, + radiusY: touch.radiusY, + rotationAngle: touch.rotationAngle, + force: touch.force, + }), + ], + }); + this.handleTouchMove(touchMoveEvent); + } + + this.scrollAnimationFrame = requestAnimationFrame(() => scroll()); + }; + + this.scrollAnimationFrame = requestAnimationFrame(scroll); + }; + + private touchOffset = { + x: 0, + y: 0, + }; + + private calculateScrollSpeed(distance: number): number { + // 使用线性插值计算速度,让速度变化更平滑 + const normalizedDistance = Math.max( + 0, + Math.min(distance, this.SCROLL_EDGE_SIZE) + ); + const factor = 1 - normalizedDistance / this.SCROLL_EDGE_SIZE; + return ( + this.MIN_SCROLL_SPEED + + (this.MAX_SCROLL_SPEED - this.MIN_SCROLL_SPEED) * factor + ); + } + + private findScrollableParent( + element: Element, + direction: 'vertical' | 'horizontal' + ): Element | null { + if (!element) return null; + + const style = window.getComputedStyle(element); + const overflow = + direction === 'vertical' ? style.overflowY : style.overflowX; + + // 检查是否真的可以滚动 + if ( + (overflow === 'auto' || overflow === 'scroll') && + (direction === 'vertical' + ? element.scrollHeight > element.clientHeight + : element.scrollWidth > element.clientWidth) + ) { + return element; + } + + if (element.parentElement) { + return this.findScrollableParent(element.parentElement, direction); + } + + return document.scrollingElement as Element; + } + private renderBody(columns: KanbanColumn[]) { if (columns.length === 0) { return ''; @@ -123,6 +641,21 @@ export class MobileKanbanCard extends SignalWatcher( if (this.view.isInHeader(column.id)) { return ''; } + + // 获取字段值 + const cell = column.cellGet(this.cardId); + const value = cell.value$.value; + + // 如果值为空,不渲染该字段 + if ( + value == null || + value === '' || + (Array.isArray(value) && value.length === 0) || + (typeof value === 'object' && Object.keys(value).length === 0) + ) { + return ''; + } + return html` `; } + private stopAutoScroll() { + if (this.scrollAnimationFrame) { + cancelAnimationFrame(this.scrollAnimationFrame); + this.scrollAnimationFrame = null; + } + } + + override connectedCallback() { + super.connectedCallback(); + + // 加触摸事件监听,使用 passive: false 允许阻止默认行为 + this.addEventListener('touchstart', this.handleTouchStart, { + passive: false, + }); + this.addEventListener('touchmove', this.handleTouchMove, { + passive: false, + }); + this.addEventListener('touchend', this.handleTouchEnd, { passive: false }); + this.addEventListener('contextmenu', e => e.preventDefault()); + } + + override disconnectedCallback() { + super.disconnectedCallback(); + + this.stopAutoScroll(); + this.removeEventListener('touchstart', this.handleTouchStart); + this.removeEventListener('touchmove', this.handleTouchMove); + this.removeEventListener('touchend', this.handleTouchEnd); + } + override render() { const columns = this.view.properties$.value.filter( v => !this.view.isInHeader(v.id) diff --git a/packages/affine/data-view/src/view-presets/kanban/mobile/group.ts b/packages/affine/data-view/src/view-presets/kanban/mobile/group.ts index 1cdead055cf0..0c9858d2dce6 100644 --- a/packages/affine/data-view/src/view-presets/kanban/mobile/group.ts +++ b/packages/affine/data-view/src/view-presets/kanban/mobile/group.ts @@ -94,6 +94,77 @@ export class MobileKanbanGroup extends SignalWatcher( ]); }; + private draggedCard: { cardId: string; groupKey: string } | null = null; + + private handleCardDragEnd = (e: CustomEvent) => { + if (!this.draggedCard) return; + + const targetGroupKey = e.detail.targetGroupKey; + if (!targetGroupKey) { + this.draggedCard = null; + return; + } + + const targetCardId = e.detail.targetId; + const position = e.detail.position; + + if (targetCardId && this.draggedCard.cardId === targetCardId) { + this.draggedCard = null; + return; + } + + // 如果目标 group 是空的,直接移动到该 group + if (!targetCardId) { + this.view.groupTrait.moveCardTo( + this.draggedCard.cardId, + this.draggedCard.groupKey, + targetGroupKey, + 'start' + ); + } else { + this.view.groupTrait.moveCardTo( + this.draggedCard.cardId, + this.draggedCard.groupKey, + targetGroupKey, + { + id: targetCardId, + before: position === 'top', + } + ); + } + + this.draggedCard = null; + }; + + private handleCardDragStart = (e: CustomEvent) => { + if (!e.detail?.cardId || !e.detail?.groupKey) return; + this.draggedCard = e.detail; + }; + + override connectedCallback() { + super.connectedCallback(); + + this.addEventListener( + 'dragstart', + this.handleCardDragStart as EventListener + ); + this.addEventListener('dragend', this.handleCardDragEnd as EventListener); + this.addEventListener('contextmenu', e => e.preventDefault()); + } + + override disconnectedCallback() { + super.disconnectedCallback(); + + this.removeEventListener( + 'dragstart', + this.handleCardDragStart as EventListener + ); + this.removeEventListener( + 'dragend', + this.handleCardDragEnd as EventListener + ); + } + override render() { const cards = this.group.rows; return html` diff --git a/packages/playground/apps/starter/data/database.ts b/packages/playground/apps/starter/data/database.ts index 4e928af4a5d2..17da86fc00d4 100644 --- a/packages/playground/apps/starter/data/database.ts +++ b/packages/playground/apps/starter/data/database.ts @@ -143,15 +143,15 @@ export const database: InitFn = (collection: DocCollection, id: string) => { }; // Add database block inside note block addDatabase('Database 1', false); - addDatabase('Database 2'); - addDatabase('Database 3'); - addDatabase('Database 4'); - addDatabase('Database 5'); - addDatabase('Database 6'); - addDatabase('Database 7'); - addDatabase('Database 8'); - addDatabase('Database 9'); - addDatabase('Database 10'); + // addDatabase('Database 2'); + // addDatabase('Database 3'); + // addDatabase('Database 4'); + // addDatabase('Database 5'); + // addDatabase('Database 6'); + // addDatabase('Database 7'); + // addDatabase('Database 8'); + // addDatabase('Database 9'); + // addDatabase('Database 10'); }); }; From 1a9182e34d3f2ba99f380bdee12eee37a9e97027 Mon Sep 17 00:00:00 2001 From: zzj3720 Date: Mon, 9 Dec 2024 19:53:33 +0800 Subject: [PATCH 2/5] feat(database): kanban supports long-press drag-and-drop on mobile devices --- .../src/view-presets/kanban/mobile/card.ts | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/affine/data-view/src/view-presets/kanban/mobile/card.ts b/packages/affine/data-view/src/view-presets/kanban/mobile/card.ts index 272c7905ef16..272b5eea8948 100644 --- a/packages/affine/data-view/src/view-presets/kanban/mobile/card.ts +++ b/packages/affine/data-view/src/view-presets/kanban/mobile/card.ts @@ -271,17 +271,17 @@ export class MobileKanbanCard extends SignalWatcher( // 启动自动滚动 this.startAutoScroll(); - // 获取所有 group + // Get all groups const allGroups = Array.from( document.querySelectorAll('mobile-kanban-group') ) as HTMLElement[]; - // 清除之前的所有提示样式 + // Clear all previous indicator styles document.querySelectorAll('.drag-over').forEach(el => { el.classList.remove('drag-over', 'drag-over-top', 'drag-over-bottom'); }); - // 获取当前触摸点下的元素 + // Get the element under touch point const elementUnderTouch = document.elementFromPoint( touch.clientX, touch.clientY @@ -290,7 +290,7 @@ export class MobileKanbanCard extends SignalWatcher( 'mobile-kanban-card' ) as HTMLElement; - // 如果触摸点在当前拖动的卡片上,或者是当前卡片的前后位置,不显示提示线 + // Don't show indicator if touching the dragging card itself if ( elementUnderTouch === this || this.contains(elementUnderTouch) || @@ -299,7 +299,7 @@ export class MobileKanbanCard extends SignalWatcher( return; } - // 找到当前触摸点所在的 group + // Find the group under touch point let targetGroup: HTMLElement | null = null; for (const group of allGroups) { const rect = group.getBoundingClientRect(); @@ -313,7 +313,7 @@ export class MobileKanbanCard extends SignalWatcher( return; } - // 获取目标 group 中的所有卡片 + // Get all cards in target group const cards = Array.from( targetGroup.querySelectorAll('mobile-kanban-card') ).filter(card => card !== this) as HTMLElement[]; @@ -329,13 +329,13 @@ export class MobileKanbanCard extends SignalWatcher( const groupRect = groupBody.getBoundingClientRect(); if (cards.length === 0) { - // group 为空的情况 + // Handle empty group case if (touch.clientY >= groupRect.top && touch.clientY <= groupRect.bottom) { const addCardButton = targetGroup.querySelector( '.mobile-add-card' ) as HTMLElement; if (addCardButton) { - // 只有在不同 group 时才显示提示线 + // Only show indicator when dragging to a different group if (targetGroup.dataset.key !== this.groupKey) { addCardButton.classList.add('drag-over'); closestCard = addCardButton; @@ -344,36 +344,36 @@ export class MobileKanbanCard extends SignalWatcher( } } } else { - // 获取当前卡片在目标组中的位置 + // Get current card's position in target group const currentIndex = cards.findIndex( card => card.dataset.cardId === this.cardId ); const isInSameGroup = targetGroup.dataset.key === this.groupKey; - // 检查是否在第一张卡片上方 + // Check if touching above first card const firstCard = cards[0]; const firstCardRect = firstCard.getBoundingClientRect(); if (touch.clientY < firstCardRect.top) { - // 如果是同一个 group,且当前卡片是第一张,不显示提示线 + // Show indicator if not the first card or in different group if (!isInSameGroup || currentIndex > 0) { closestCard = firstCard; position = 'top'; } } - // 检查是否在最后一张卡片下方 + // Check if touching below last card else { const lastCard = cards[cards.length - 1]; const lastCardRect = lastCard.getBoundingClientRect(); if (touch.clientY > lastCardRect.bottom) { - // 如果是同一个 group,且当前卡片是最后一张,不显示提示线 + // Show indicator if not the last card or in different group if (!isInSameGroup || currentIndex < cards.length - 1) { closestCard = lastCard; position = 'bottom'; } } - // 在卡片之间 + // Between cards else { for (let i = 0; i < cards.length; i++) { const card = cards[i]; @@ -383,15 +383,15 @@ export class MobileKanbanCard extends SignalWatcher( const midY = rect.top + rect.height / 2; if (isInSameGroup) { - // 在同一个 group 中 + // In same group if (touch.clientY < midY) { - // 上半部分:不能是当前卡片的下一张卡片 + // Upper half: can't be next card of current card if (i !== currentIndex + 1 || currentIndex === 0) { closestCard = card; position = 'top'; } } else { - // 下半部分:不能是当前卡片的上一张卡片 + // Lower half: can't be previous card of current card if ( i !== currentIndex - 1 || currentIndex === cards.length - 1 @@ -401,7 +401,7 @@ export class MobileKanbanCard extends SignalWatcher( } } } else { - // 不同 group,没有限制 + // Different group, no restrictions closestCard = card; position = touch.clientY < midY ? 'top' : 'bottom'; } @@ -412,7 +412,7 @@ export class MobileKanbanCard extends SignalWatcher( } } - // 显示提示线 + // Show drop indicator if (closestCard) { closestCard.classList.add('drag-over', `drag-over-${position}`); } @@ -646,7 +646,7 @@ export class MobileKanbanCard extends SignalWatcher( const cell = column.cellGet(this.cardId); const value = cell.value$.value; - // 如果值为空,不渲染该字段 + // 如果值���空,不渲染该字段 if ( value == null || value === '' || From 7d930a6ef0f366c73647fc5431d1beb2458b2409 Mon Sep 17 00:00:00 2001 From: zzj3720 Date: Mon, 9 Dec 2024 19:55:00 +0800 Subject: [PATCH 3/5] feat(database): kanban supports long-press drag-and-drop on mobile devices --- .../affine/data-view/src/view-presets/kanban/mobile/group.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/affine/data-view/src/view-presets/kanban/mobile/group.ts b/packages/affine/data-view/src/view-presets/kanban/mobile/group.ts index 0c9858d2dce6..41c6dadb9874 100644 --- a/packages/affine/data-view/src/view-presets/kanban/mobile/group.ts +++ b/packages/affine/data-view/src/view-presets/kanban/mobile/group.ts @@ -113,7 +113,6 @@ export class MobileKanbanGroup extends SignalWatcher( return; } - // 如果目标 group 是空的,直接移动到该 group if (!targetCardId) { this.view.groupTrait.moveCardTo( this.draggedCard.cardId, From 146a303373484794a4cbd79019179652654809e1 Mon Sep 17 00:00:00 2001 From: zzj3720 Date: Mon, 9 Dec 2024 19:57:29 +0800 Subject: [PATCH 4/5] feat(database): kanban supports long-press drag-and-drop on mobile devices --- .../src/view-presets/kanban/mobile/card.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/affine/data-view/src/view-presets/kanban/mobile/card.ts b/packages/affine/data-view/src/view-presets/kanban/mobile/card.ts index 272b5eea8948..ccf6cb93113b 100644 --- a/packages/affine/data-view/src/view-presets/kanban/mobile/card.ts +++ b/packages/affine/data-view/src/view-presets/kanban/mobile/card.ts @@ -251,13 +251,13 @@ export class MobileKanbanCard extends SignalWatcher( const touch = e.touches[0]; this.currentTouch = touch; - // 计算基础移动距离 + // Calculate base movement distance const deltaX = touch.clientX - (this.initialPosition.x + this.touchOffset.x); const deltaY = touch.clientY - (this.initialPosition.y + this.touchOffset.y); - // 计算滚动偏移 + // Calculate scroll offset const horizontalContainer = this.findScrollableParent(this, 'horizontal'); const verticalContainer = this.findScrollableParent(this, 'vertical'); const scrollLeft = horizontalContainer?.scrollLeft || 0; @@ -265,10 +265,10 @@ export class MobileKanbanCard extends SignalWatcher( const scrollDeltaX = scrollLeft - this.initialScroll.x; const scrollDeltaY = scrollTop - this.initialScroll.y; - // 使用 transform 移动卡片,加上滚动偏移 + // Use transform to move card with scroll offset this.style.transform = `translate(${deltaX + scrollDeltaX}px, ${deltaY + scrollDeltaY}px) scale(1.02)`; - // 启动自动滚动 + // Start auto scroll this.startAutoScroll(); // Get all groups @@ -431,7 +431,7 @@ export class MobileKanbanCard extends SignalWatcher( y: touch.clientY - rect.top, }; - // 记录初始滚动位置 + // Record initial scroll position const horizontalContainer = this.findScrollableParent(this, 'horizontal'); const verticalContainer = this.findScrollableParent(this, 'vertical'); this.initialScroll = { @@ -443,13 +443,13 @@ export class MobileKanbanCard extends SignalWatcher( this.isDragging = true; this.classList.add('dragging'); - // 记录初始位置 + // Record initial position this.initialPosition = { x: rect.left, y: rect.top, }; - // 触发拖动开事件 + // Dispatch drag start event const event = new CustomEvent('dragstart', { detail: { cardId: this.cardId, @@ -482,11 +482,11 @@ export class MobileKanbanCard extends SignalWatcher( private longPressTimeout: number | null = null; - private readonly MAX_SCROLL_SPEED = 15; // 降低最大滚动速度 + private readonly MAX_SCROLL_SPEED = 15; // Maximum scroll speed - private readonly MIN_SCROLL_SPEED = 2; // 最小滚动速度 + private readonly MIN_SCROLL_SPEED = 2; // Minimum scroll speed - private readonly SCROLL_EDGE_SIZE = 100; // 增大边缘区域 + private readonly SCROLL_EDGE_SIZE = 100; // Edge trigger area size private scrollAnimationFrame: number | null = null; @@ -612,7 +612,7 @@ export class MobileKanbanCard extends SignalWatcher( const overflow = direction === 'vertical' ? style.overflowY : style.overflowX; - // 检查是否真的可以滚动 + // Check if element is actually scrollable if ( (overflow === 'auto' || overflow === 'scroll') && (direction === 'vertical' @@ -642,11 +642,11 @@ export class MobileKanbanCard extends SignalWatcher( return ''; } - // 获取字段值 + // Get field value const cell = column.cellGet(this.cardId); const value = cell.value$.value; - // 如果值���空,不渲染该字段 + // Skip empty fields if ( value == null || value === '' || @@ -735,7 +735,7 @@ export class MobileKanbanCard extends SignalWatcher( override connectedCallback() { super.connectedCallback(); - // 加触摸事件监听,使用 passive: false 允许阻止默认行为 + // Add touch event listeners with passive: false to allow preventDefault this.addEventListener('touchstart', this.handleTouchStart, { passive: false, }); From 22e35ae23b1910e088f63d8b7f986b600c51de8f Mon Sep 17 00:00:00 2001 From: zzj3720 Date: Mon, 9 Dec 2024 20:01:15 +0800 Subject: [PATCH 5/5] feat(database): kanban supports long-press drag-and-drop on mobile devices --- .../data-view/src/view-presets/kanban/mobile/card.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/affine/data-view/src/view-presets/kanban/mobile/card.ts b/packages/affine/data-view/src/view-presets/kanban/mobile/card.ts index ccf6cb93113b..47c17c72fb45 100644 --- a/packages/affine/data-view/src/view-presets/kanban/mobile/card.ts +++ b/packages/affine/data-view/src/view-presets/kanban/mobile/card.ts @@ -184,16 +184,13 @@ export class MobileKanbanCard extends SignalWatcher( this.isDragging = false; this.classList.remove('dragging'); - // 重置 transform this.style.transform = ''; - // 获取目标(可能是卡片或空 group) const target = document.querySelector( '.drag-over-top, .drag-over-bottom, .mobile-group-body.drag-over' ) as HTMLElement; if (target) { - // 获取目标所在的 group const targetGroup = target.closest( 'mobile-kanban-group' ) as HTMLElement; @@ -216,7 +213,6 @@ export class MobileKanbanCard extends SignalWatcher( this.dispatchEvent(event); } - // 清除所有提示样式 document .querySelectorAll('.drag-over, .drag-over-top, .drag-over-bottom') .forEach(el => { @@ -226,11 +222,9 @@ export class MobileKanbanCard extends SignalWatcher( }); } - // 停止自动滚动 this.stopAutoScroll(); this.currentTouch = null; - // 重置所有状态 this.scrollOffset = { x: 0, y: 0 }; this.initialPosition = { x: 0, y: 0 }; this.touchOffset = { x: 0, y: 0 }; @@ -543,7 +537,7 @@ export class MobileKanbanCard extends SignalWatcher( } } - // 更新卡片位置 + // Update card position if (scrollX !== 0 || scrollY !== 0) { const deltaX = touch.clientX - (this.initialPosition.x + this.touchOffset.x); @@ -556,7 +550,7 @@ export class MobileKanbanCard extends SignalWatcher( this.style.transform = `translate(${deltaX + scrollDeltaX}px, ${deltaY + scrollDeltaY}px) scale(1.02)`; - // 触发一个新的 touchmove 事件来更新插入位置 + // Trigger a new touchmove event to update insert position const touchMoveEvent = new TouchEvent('touchmove', { touches: [ new Touch({ @@ -590,7 +584,7 @@ export class MobileKanbanCard extends SignalWatcher( }; private calculateScrollSpeed(distance: number): number { - // 使用线性插值计算速度,让速度变化更平滑 + // Use linear interpolation to calculate speed for smoother changes const normalizedDistance = Math.max( 0, Math.min(distance, this.SCROLL_EDGE_SIZE)