diff --git a/apps/client/src/widgets/collections/board/index.css b/apps/client/src/widgets/collections/board/index.css index 1b03a4273..81d0ca1e5 100644 --- a/apps/client/src/widgets/collections/board/index.css +++ b/apps/client/src/widgets/collections/board/index.css @@ -140,16 +140,6 @@ to { opacity: 0; transform: translateY(-10px); } } -.board-view-container .board-note.card-updated { - animation: cardUpdate 0.3s ease-in-out; -} - -@keyframes cardUpdate { - 0% { transform: scale(1); } - 50% { transform: scale(1.02); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } - 100% { transform: scale(1); } -} - .board-view-container .board-note:hover { transform: translateY(-2px); box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.35); diff --git a/apps/client/src/widgets/view_widgets/board_view/differential_renderer.ts b/apps/client/src/widgets/view_widgets/board_view/differential_renderer.ts deleted file mode 100644 index 3641fc50d..000000000 --- a/apps/client/src/widgets/view_widgets/board_view/differential_renderer.ts +++ /dev/null @@ -1,383 +0,0 @@ -import { BoardDragHandler } from "./drag_handler"; -import BoardApi from "./api"; -import appContext from "../../../components/app_context"; -import FNote from "../../../entities/fnote"; -import ViewModeStorage from "../view_mode_storage"; -import { BoardData } from "./config"; -import { t } from "../../../services/i18n.js"; - -export interface BoardState { - columns: { [key: string]: { note: any; branch: any }[] }; - columnOrder: string[]; -} - -export class DifferentialBoardRenderer { - private $container: JQuery; - private api: BoardApi; - private dragHandler: BoardDragHandler; - private lastState: BoardState | null = null; - private onCreateNewItem: (column: string) => void; - private updateTimeout: number | null = null; - private pendingUpdate = false; - private parentNote: FNote; - private viewStorage: ViewModeStorage; - private onRefreshApi: () => Promise; - - constructor( - $container: JQuery, - api: BoardApi, - dragHandler: BoardDragHandler, - onCreateNewItem: (column: string) => void, - parentNote: FNote, - viewStorage: ViewModeStorage, - onRefreshApi: () => Promise - ) { - this.$container = $container; - this.api = api; - this.dragHandler = dragHandler; - this.onCreateNewItem = onCreateNewItem; - this.parentNote = parentNote; - this.viewStorage = viewStorage; - this.onRefreshApi = onRefreshApi; - } - - async renderBoard(refreshApi = false): Promise { - // Refresh API data if requested - if (refreshApi) { - await this.onRefreshApi(); - } - - // Debounce rapid updates - if (this.updateTimeout) { - clearTimeout(this.updateTimeout); - } - - this.updateTimeout = window.setTimeout(async () => { - await this.performUpdate(); - this.updateTimeout = null; - }, 16); // ~60fps - } - - private async performUpdate(): Promise { - // Clean up any stray drag indicators before updating - this.dragHandler.cleanup(); - - const currentState = this.getCurrentState(); - - if (!this.lastState) { - // First render - do full render - await this.fullRender(currentState); - } else { - // Differential render - only update what changed - await this.differentialRender(this.lastState, currentState); - } - - this.lastState = currentState; - } - - private getCurrentState(): BoardState { - const columns: { [key: string]: { note: any; branch: any }[] } = {}; - const columnOrder: string[] = []; - - for (const column of this.api.columns) { - columnOrder.push(column); - columns[column] = this.api.getColumn(column) || []; - } - - return { columns, columnOrder }; - } - - private async fullRender(state: BoardState): Promise { - this.$container.empty(); - - for (const column of state.columnOrder) { - const columnItems = state.columns[column]; - const $columnEl = this.createColumn(column, columnItems); - this.$container.append($columnEl); - } - } - - private async differentialRender(oldState: BoardState, newState: BoardState): Promise { - // Store scroll positions before making changes - const scrollPositions = this.saveScrollPositions(); - - // Handle column additions/removals - this.updateColumns(oldState, newState); - - // Handle card updates within existing columns - for (const column of newState.columnOrder) { - this.updateColumnCards(column, oldState.columns[column] || [], newState.columns[column]); - } - - // Restore scroll positions - this.restoreScrollPositions(scrollPositions); - } - - private saveScrollPositions(): { [column: string]: number } { - const positions: { [column: string]: number } = {}; - this.$container.find('.board-column').each((_, el) => { - const column = $(el).attr('data-column'); - if (column) { - positions[column] = el.scrollTop; - } - }); - return positions; - } - - private restoreScrollPositions(positions: { [column: string]: number }): void { - this.$container.find('.board-column').each((_, el) => { - const column = $(el).attr('data-column'); - if (column && positions[column] !== undefined) { - el.scrollTop = positions[column]; - } - }); - } - - private updateColumns(oldState: BoardState, newState: BoardState): void { - // Check if column order has changed - const orderChanged = !this.arraysEqual(oldState.columnOrder, newState.columnOrder); - - if (orderChanged) { - // If order changed, we need to reorder the columns in the DOM - this.reorderColumns(newState.columnOrder); - } - - // Remove columns that no longer exist - for (const oldColumn of oldState.columnOrder) { - if (!newState.columnOrder.includes(oldColumn)) { - this.$container.find(`[data-column="${oldColumn}"]`).remove(); - } - } - - // Add new columns - for (const newColumn of newState.columnOrder) { - if (!oldState.columnOrder.includes(newColumn)) { - const columnItems = newState.columns[newColumn]; - const $columnEl = this.createColumn(newColumn, columnItems); - - // Insert at correct position - const insertIndex = newState.columnOrder.indexOf(newColumn); - const $existingColumns = this.$container.find('.board-column'); - - if (insertIndex === 0) { - this.$container.prepend($columnEl); - } else if (insertIndex >= $existingColumns.length) { - this.$container.find('.board-add-column').before($columnEl); - } else { - $($existingColumns[insertIndex - 1]).after($columnEl); - } - } - } - } - - private arraysEqual(a: string[], b: string[]): boolean { - return a.length === b.length && a.every((val, index) => val === b[index]); - } - - private reorderColumns(newOrder: string[]): void { - // Get all existing column elements - const $columns = this.$container.find('.board-column'); - const $addColumnButton = this.$container.find('.board-add-column'); - - // Create a map of column elements by their data-column attribute - const columnElements = new Map>(); - $columns.each((_, el) => { - const $el = $(el); - const columnValue = $el.attr('data-column'); - if (columnValue) { - columnElements.set(columnValue, $el); - } - }); - - // Remove all columns from DOM (but keep references) - $columns.detach(); - - // Re-insert columns in the new order - let $insertAfter: JQuery | null = null; - for (const columnValue of newOrder) { - const $columnEl = columnElements.get(columnValue); - if ($columnEl) { - if ($insertAfter) { - $insertAfter.after($columnEl); - } else { - // Insert at the beginning - this.$container.prepend($columnEl); - } - $insertAfter = $columnEl; - } - } - - // Ensure add column button is at the end - if ($addColumnButton.length) { - this.$container.append($addColumnButton); - } - } - - private updateColumnCards(column: string, oldCards: { note: any; branch: any }[], newCards: { note: any; branch: any }[]): void { - const $column = this.$container.find(`[data-column="${column}"]`); - if (!$column.length) return; - - const $cardContainer = $column; - const oldCardIds = oldCards.map(item => item.note.noteId); - const newCardIds = newCards.map(item => item.note.noteId); - - // Remove cards that no longer exist - $cardContainer.find('.board-note').each((_, el) => { - const noteId = $(el).attr('data-note-id'); - if (noteId && !newCardIds.includes(noteId)) { - $(el).addClass('fade-out'); - setTimeout(() => $(el).remove(), 150); - } - }); - - // Add or update cards - for (let i = 0; i < newCards.length; i++) { - const item = newCards[i]; - const noteId = item.note.noteId; - const $existingCard = $cardContainer.find(`[data-note-id="${noteId}"]`); - const isNewCard = !oldCardIds.includes(noteId); - - if ($existingCard.length) { - // Check for changes in title, icon, or color - const currentTitle = $existingCard.text().trim(); - const currentIconClass = $existingCard.attr('data-icon-class'); - const currentColorClass = $existingCard.attr('data-color-class') || ''; - - const newIconClass = item.note.getIcon(); - const newColorClass = item.note.getColorClass() || ''; - - let hasChanges = false; - - // Update title if changed - if (currentTitle !== item.note.title) { - $existingCard.contents().filter(function() { - return this.nodeType === 3; // Text nodes - }).remove(); - $existingCard.append(document.createTextNode(item.note.title)); - hasChanges = true; - } - - // Update icon if changed - if (currentIconClass !== newIconClass) { - const $icon = $existingCard.find('.icon'); - $icon.removeClass().addClass('icon').addClass(newIconClass); - $existingCard.attr('data-icon-class', newIconClass); - hasChanges = true; - } - - // Update color if changed - if (currentColorClass !== newColorClass) { - // Remove old color class if it exists - if (currentColorClass) { - $existingCard.removeClass(currentColorClass); - } - // Add new color class if it exists - if (newColorClass) { - $existingCard.addClass(newColorClass); - } - $existingCard.attr('data-color-class', newColorClass); - hasChanges = true; - } - - // Add subtle animation if there were changes - if (hasChanges) { - $existingCard.addClass('card-updated'); - setTimeout(() => $existingCard.removeClass('card-updated'), 300); - } - - // Ensure card is in correct position - this.ensureCardPosition($existingCard, i, $cardContainer); - } else { - // Create new card - const $newCard = this.createCard(item.note, item.branch, column); - $newCard.addClass('fade-in').css('opacity', '0'); - - // Insert at correct position - if (i === 0) { - $cardContainer.find('h3').after($newCard); - } else { - const $prevCard = $cardContainer.find('.board-note').eq(i - 1); - if ($prevCard.length) { - $prevCard.after($newCard); - } else { - $cardContainer.find('.board-new-item').before($newCard); - } - } - - // Trigger fade in animation - setTimeout(() => $newCard.css('opacity', '1'), 10); - } - } - } - - private ensureCardPosition($card: JQuery, targetIndex: number, $container: JQuery): void { - const $allCards = $container.find('.board-note'); - const currentIndex = $allCards.index($card); - - if (currentIndex !== targetIndex) { - if (targetIndex === 0) { - $container.find('h3').after($card); - } else { - const $targetPrev = $allCards.eq(targetIndex - 1); - if ($targetPrev.length) { - $targetPrev.after($card); - } - } - } - } - - private createColumn(column: string, columnItems: { note: any; branch: any }[]): JQuery { - // Setup column dragging - this.dragHandler.setupColumnDrag($columnEl, column); - - // Handle wheel events for scrolling - $columnEl.on("wheel", (event) => { - const el = $columnEl[0]; - const needsScroll = el.scrollHeight > el.clientHeight; - if (needsScroll) { - event.stopPropagation(); - } - }); - - // Setup drop zones for both notes and columns - this.dragHandler.setupNoteDropZone($columnEl, column); - this.dragHandler.setupColumnDropZone($columnEl); - - // Add "New item" button - const $newItemEl = $("
") - .addClass("board-new-item") - .attr("data-column", column) - .html(` ${}`); - - $columnEl.append($newItemEl); - - return $columnEl; - } - - private createCard(note: any, branch: any, column: string): JQuery { - $noteEl.prepend($iconEl); - $noteEl.on("click", () => appContext.triggerCommand("openInPopup", { noteIdOrPath: note.noteId })); - - // Setup drag functionality - this.dragHandler.setupNoteDrag($noteEl, note, branch); - - return $noteEl; - } - - forceFullRender(): void { - this.lastState = null; - if (this.updateTimeout) { - clearTimeout(this.updateTimeout); - this.updateTimeout = null; - } - } - - async flushPendingUpdates(): Promise { - if (this.updateTimeout) { - clearTimeout(this.updateTimeout); - this.updateTimeout = null; - await this.performUpdate(); - } - } - -} diff --git a/apps/client/src/widgets/view_widgets/board_view/drag_handler.ts b/apps/client/src/widgets/view_widgets/board_view/drag_handler.ts deleted file mode 100644 index 11be8f9f2..000000000 --- a/apps/client/src/widgets/view_widgets/board_view/drag_handler.ts +++ /dev/null @@ -1,45 +0,0 @@ -import BoardApi from "./api"; -import { DragContext } from "./drag_types"; -import { NoteDragHandler } from "./note_drag_handler"; -import { ColumnDragHandler } from "./column_drag_handler"; - -export class BoardDragHandler { - private noteDragHandler: NoteDragHandler; - private columnDragHandler: ColumnDragHandler; - - constructor( - $container: JQuery, - api: BoardApi, - context: DragContext, - ) { - // Initialize specialized drag handlers - this.noteDragHandler = new NoteDragHandler($container, api, context); - this.columnDragHandler = new ColumnDragHandler($container, api, context); - } - - // Note drag methods - delegate to NoteDragHandler - setupNoteDrag($noteEl: JQuery, note: any, branch: any) { - this.noteDragHandler.setupNoteDrag($noteEl, note, branch); - } - - setupNoteDropZone($columnEl: JQuery, column: string) { - this.noteDragHandler.setupNoteDropZone($columnEl, column); - } - - // Column drag methods - delegate to ColumnDragHandler - setupColumnDrag($columnEl: JQuery, columnValue: string) { - this.columnDragHandler.setupColumnDrag($columnEl, columnValue); - } - - setupColumnDropZone($columnEl: JQuery) { - this.columnDragHandler.setupColumnDropZone($columnEl); - } - - cleanup() { - this.noteDragHandler.cleanup(); - this.columnDragHandler.cleanup(); - } -} - -// Export the drag context type for external use -export type { DragContext } from "./drag_types"; diff --git a/apps/client/src/widgets/view_widgets/board_view/drag_types.ts b/apps/client/src/widgets/view_widgets/board_view/drag_types.ts deleted file mode 100644 index 3957ee2e9..000000000 --- a/apps/client/src/widgets/view_widgets/board_view/drag_types.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface DragContext { - draggedNote: any; - draggedBranch: any; - draggedNoteElement: JQuery | null; - draggedColumn: string | null; - draggedColumnElement: JQuery | null; -} - -export interface BaseDragHandler { - cleanup(): void; -} diff --git a/apps/client/src/widgets/view_widgets/board_view/note_drag_handler.ts b/apps/client/src/widgets/view_widgets/board_view/note_drag_handler.ts deleted file mode 100644 index ef08d7900..000000000 --- a/apps/client/src/widgets/view_widgets/board_view/note_drag_handler.ts +++ /dev/null @@ -1,322 +0,0 @@ -import branchService from "../../../services/branches"; -import BoardApi from "./api"; -import { DragContext, BaseDragHandler } from "./drag_types"; - -export class NoteDragHandler implements BaseDragHandler { - private $container: JQuery; - private api: BoardApi; - private context: DragContext; - - constructor( - $container: JQuery, - api: BoardApi, - context: DragContext, - ) { - this.$container = $container; - this.api = api; - this.context = context; - } - - setupNoteDrag($noteEl: JQuery, note: any, branch: any) { - $noteEl.attr("draggable", "true"); - - // Mouse drag events - this.setupMouseDrag($noteEl, note, branch); - - // Touch drag events - this.setupTouchDrag($noteEl, note, branch); - } - - setupNoteDropZone($columnEl: JQuery, column: string) { - $columnEl.on("dragover", (e) => { - // Only handle note drops when a note is being dragged - if (this.context.draggedNote && !this.context.draggedColumn) { - e.preventDefault(); - const originalEvent = e.originalEvent as DragEvent; - if (originalEvent.dataTransfer) { - originalEvent.dataTransfer.dropEffect = "move"; - } - - $columnEl.addClass("drag-over"); - this.showDropIndicator($columnEl, e); - } - }); - - $columnEl.on("dragleave", (e) => { - // Only remove drag-over if we're leaving the column entirely - const rect = $columnEl[0].getBoundingClientRect(); - const originalEvent = e.originalEvent as DragEvent; - const x = originalEvent.clientX; - const y = originalEvent.clientY; - - if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) { - $columnEl.removeClass("drag-over"); - this.cleanupNoteDropIndicators($columnEl); - } - }); - - $columnEl.on("drop", async (e) => { - if (this.context.draggedNote && !this.context.draggedColumn) { - e.preventDefault(); - $columnEl.removeClass("drag-over"); - - if (this.context.draggedNote && this.context.draggedNoteElement && this.context.draggedBranch) { - await this.handleNoteDrop($columnEl, column); - } - } - }); - } - - cleanup() { - this.cleanupAllDropIndicators(); - this.$container.find('.board-column').removeClass('drag-over'); - } - - private setupMouseDrag($noteEl: JQuery, note: any, branch: any) { - $noteEl.on("dragstart", (e) => { - this.context.draggedNote = note; - this.context.draggedBranch = branch; - this.context.draggedNoteElement = $noteEl; - $noteEl.addClass("dragging"); - - // Set drag data - const originalEvent = e.originalEvent as DragEvent; - if (originalEvent.dataTransfer) { - originalEvent.dataTransfer.effectAllowed = "move"; - originalEvent.dataTransfer.setData("text/plain", note.noteId); - } - }); - - $noteEl.on("dragend", () => { - $noteEl.removeClass("dragging"); - this.context.draggedNote = null; - this.context.draggedBranch = null; - this.context.draggedNoteElement = null; - - // Clean up all drop indicators properly - this.cleanupAllDropIndicators(); - }); - } - - private setupTouchDrag($noteEl: JQuery, note: any, branch: any) { - let isDragging = false; - let startY = 0; - let startX = 0; - let dragThreshold = 10; // Minimum distance to start dragging - let $dragPreview: JQuery | null = null; - - $noteEl.on("touchstart", (e) => { - const touch = (e.originalEvent as TouchEvent).touches[0]; - startX = touch.clientX; - startY = touch.clientY; - isDragging = false; - $dragPreview = null; - }); - - $noteEl.on("touchmove", (e) => { - e.preventDefault(); // Prevent scrolling - const touch = (e.originalEvent as TouchEvent).touches[0]; - const deltaX = Math.abs(touch.clientX - startX); - const deltaY = Math.abs(touch.clientY - startY); - - // Start dragging if we've moved beyond threshold - if (!isDragging && (deltaX > dragThreshold || deltaY > dragThreshold)) { - isDragging = true; - this.context.draggedNote = note; - this.context.draggedBranch = branch; - this.context.draggedNoteElement = $noteEl; - $noteEl.addClass("dragging"); - - // Create drag preview - $dragPreview = this.createDragPreview($noteEl, touch.clientX, touch.clientY); - } - - if (isDragging && $dragPreview) { - // Update drag preview position - $dragPreview.css({ - left: touch.clientX - ($dragPreview.outerWidth() || 0) / 2, - top: touch.clientY - ($dragPreview.outerHeight() || 0) / 2 - }); - - // Find element under touch point - const elementBelow = document.elementFromPoint(touch.clientX, touch.clientY); - if (elementBelow) { - const $columnEl = $(elementBelow).closest('.board-column'); - - if ($columnEl.length > 0) { - // Remove drag-over from all columns - this.$container.find('.board-column').removeClass('drag-over'); - $columnEl.addClass('drag-over'); - - // Show drop indicator - this.showDropIndicatorAtPoint($columnEl, touch.clientY); - } else { - // Remove all drag indicators if not over a column - this.$container.find('.board-column').removeClass('drag-over'); - this.cleanupAllDropIndicators(); - } - } - } - }); - - $noteEl.on("touchend", async (e) => { - if (isDragging) { - const touch = (e.originalEvent as TouchEvent).changedTouches[0]; - const elementBelow = document.elementFromPoint(touch.clientX, touch.clientY); - if (elementBelow) { - const $columnEl = $(elementBelow).closest('.board-column'); - - if ($columnEl.length > 0) { - const column = $columnEl.attr('data-column'); - if (column && this.context.draggedNote && this.context.draggedNoteElement && this.context.draggedBranch) { - await this.handleNoteDrop($columnEl, column); - } - } - } - - // Clean up - $noteEl.removeClass("dragging"); - this.context.draggedNote = null; - this.context.draggedBranch = null; - this.context.draggedNoteElement = null; - this.$container.find('.board-column').removeClass('drag-over'); - this.cleanupAllDropIndicators(); - - // Remove drag preview - if ($dragPreview) { - $dragPreview.remove(); - $dragPreview = null; - } - } - isDragging = false; - }); - } - - private createDragPreview($noteEl: JQuery, x: number, y: number): JQuery { - // Clone the note element for the preview - const $preview = $noteEl.clone(); - - $preview - .addClass('board-drag-preview') - .css({ - position: 'fixed', - left: x - ($noteEl.outerWidth() || 0) / 2, - top: y - ($noteEl.outerHeight() || 0) / 2, - pointerEvents: 'none', - zIndex: 10000 - }) - .appendTo('body'); - - return $preview; - } - - private showDropIndicator($columnEl: JQuery, e: JQuery.DragOverEvent) { - const originalEvent = e.originalEvent as DragEvent; - const mouseY = originalEvent.clientY; - this.showDropIndicatorAtY($columnEl, mouseY); - } - - private showDropIndicatorAtPoint($columnEl: JQuery, touchY: number) { - this.showDropIndicatorAtY($columnEl, touchY); - } - - private showDropIndicatorAtY($columnEl: JQuery, y: number) { - const columnRect = $columnEl[0].getBoundingClientRect(); - const relativeY = y - columnRect.top; - - // Clean up any existing drop indicators in this column first - this.cleanupNoteDropIndicators($columnEl); - - // Create a new drop indicator - const $dropIndicator = $("
").addClass("board-drop-indicator"); - - // Find the best position to insert the note - const $notes = this.context.draggedNoteElement ? - $columnEl.find(".board-note").not(this.context.draggedNoteElement) : - $columnEl.find(".board-note"); - let insertAfterElement: HTMLElement | null = null; - - $notes.each((_, noteEl) => { - const noteRect = noteEl.getBoundingClientRect(); - const noteMiddle = noteRect.top + noteRect.height / 2 - columnRect.top; - - if (relativeY > noteMiddle) { - insertAfterElement = noteEl; - } - }); - - // Position the drop indicator - if (insertAfterElement) { - $(insertAfterElement).after($dropIndicator); - } else { - // Insert at the beginning (after the header) - const $header = $columnEl.find("h3"); - $header.after($dropIndicator); - } - - $dropIndicator.addClass("show"); - } - - private async handleNoteDrop($columnEl: JQuery, column: string) { - const draggedNoteElement = this.context.draggedNoteElement; - const draggedNote = this.context.draggedNote; - const draggedBranch = this.context.draggedBranch; - - if (draggedNote && draggedNoteElement && draggedBranch) { - const currentColumn = draggedNoteElement.attr("data-current-column"); - - // Capture drop indicator position BEFORE removing it - const dropIndicator = $columnEl.find(".board-drop-indicator.show"); - let targetBranchId: string | null = null; - let moveType: "before" | "after" | null = null; - - if (dropIndicator.length > 0) { - // Find the note element that the drop indicator is positioned relative to - const nextNote = dropIndicator.next(".board-note"); - const prevNote = dropIndicator.prev(".board-note"); - - if (nextNote.length > 0) { - targetBranchId = nextNote.attr("data-branch-id") || null; - moveType = "before"; - } else if (prevNote.length > 0) { - targetBranchId = prevNote.attr("data-branch-id") || null; - moveType = "after"; - } - } - - try { - // Handle column change - if (currentColumn !== column) { - await this.api.changeColumn(draggedNote.noteId, column); - } - - // Handle position change (works for both same column and different column moves) - if (targetBranchId && moveType) { - if (moveType === "before") { - await branchService.moveBeforeBranch([draggedBranch.branchId], targetBranchId); - } else if (moveType === "after") { - await branchService.moveAfterBranch([draggedBranch.branchId], targetBranchId); - } - } - - // Update the data attributes - draggedNoteElement.attr("data-current-column", column); - } catch (error) { - console.error("Failed to update note position:", error); - } finally { - // Always clean up drop indicators after drop operation - this.cleanupAllDropIndicators(); - } - } - } - - private cleanupAllDropIndicators() { - // Remove all drop indicators from the DOM to prevent layout issues - this.$container.find(".board-drop-indicator").remove(); - } - - private cleanupNoteDropIndicators($columnEl: JQuery) { - // Remove note drop indicators from a specific column - $columnEl.find(".board-drop-indicator").remove(); - } -}