diff --git a/apps/client/src/widgets/view_widgets/board_view/column_drag_handler.ts b/apps/client/src/widgets/view_widgets/board_view/column_drag_handler.ts deleted file mode 100644 index 2812120aa..000000000 --- a/apps/client/src/widgets/view_widgets/board_view/column_drag_handler.ts +++ /dev/null @@ -1,278 +0,0 @@ -import BoardApi from "./api"; -import { DragContext, BaseDragHandler } from "./drag_types"; - -export class ColumnDragHandler 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; - } - - setupColumnDrag($columnEl: JQuery, columnValue: string) { - const $titleEl = $columnEl.find('h3[data-column-value]'); - - $titleEl.attr("draggable", "true"); - - // Delay drag start to allow click detection - let dragStartTimer: number | null = null; - - $titleEl.on("mousedown", (e) => { - // Don't interfere with editing mode or input field interactions - if ($titleEl.hasClass('editing') || $(e.target).is('input')) { - return; - } - - // Clear any existing timer - if (dragStartTimer) { - clearTimeout(dragStartTimer); - dragStartTimer = null; - } - - // Set a short delay before enabling dragging - dragStartTimer = window.setTimeout(() => { - $titleEl.attr("draggable", "true"); - dragStartTimer = null; - }, 150); - }); - - $titleEl.on("mouseup mouseleave", (e) => { - // Don't interfere with editing mode - if ($titleEl.hasClass('editing') || $(e.target).is('input')) { - return; - } - - // Cancel drag start timer on mouse up or leave - if (dragStartTimer) { - clearTimeout(dragStartTimer); - dragStartTimer = null; - } - }); - - $titleEl.on("dragstart", (e) => { - // Only start dragging if the target is not an input (for inline editing) - if ($(e.target).is('input') || $titleEl.hasClass('editing')) { - e.preventDefault(); - return false; - } - - this.context.draggedColumn = columnValue; - this.context.draggedColumnElement = $columnEl; - $columnEl.addClass("column-dragging"); - - const originalEvent = e.originalEvent as DragEvent; - if (originalEvent.dataTransfer) { - originalEvent.dataTransfer.effectAllowed = "move"; - originalEvent.dataTransfer.setData("text/plain", columnValue); - } - - // Prevent note dragging when column is being dragged - e.stopPropagation(); - - // Setup global drag tracking for better drop indicator positioning - this.setupGlobalColumnDragTracking(); - }); - - $titleEl.on("dragend", () => { - $columnEl.removeClass("column-dragging"); - this.context.draggedColumn = null; - this.context.draggedColumnElement = null; - this.cleanupColumnDropIndicators(); - this.cleanupGlobalColumnDragTracking(); - - // Re-enable draggable - $titleEl.attr("draggable", "true"); - }); - } - - setupColumnDropZone($columnEl: JQuery) { - $columnEl.on("dragover", (e) => { - // Only handle column drops when a column is being dragged - if (this.context.draggedColumn && !this.context.draggedNote) { - e.preventDefault(); - const originalEvent = e.originalEvent as DragEvent; - if (originalEvent.dataTransfer) { - originalEvent.dataTransfer.dropEffect = "move"; - } - - // Don't highlight columns - we only care about the drop indicator position - } - }); - - $columnEl.on("drop", async (e) => { - if (this.context.draggedColumn && !this.context.draggedNote) { - e.preventDefault(); - console.log("Column drop event triggered for column:", this.context.draggedColumn); - - // Use the drop indicator position to determine where to place the column - await this.handleColumnDrop(); - } - }); - } - - cleanup() { - this.cleanupColumnDropIndicators(); - this.context.draggedColumn = null; - this.context.draggedColumnElement = null; - this.cleanupGlobalColumnDragTracking(); - } - - private setupGlobalColumnDragTracking() { - // Add container-level drag tracking for better indicator positioning - this.$container.on("dragover.columnDrag", (e) => { - if (this.context.draggedColumn) { - e.preventDefault(); - const originalEvent = e.originalEvent as DragEvent; - this.showColumnDropIndicator(originalEvent.clientX); - } - }); - - // Add container-level drop handler for column reordering - this.$container.on("drop.columnDrag", async (e) => { - if (this.context.draggedColumn) { - e.preventDefault(); - console.log("Container drop event triggered for column:", this.context.draggedColumn); - await this.handleColumnDrop(); - } - }); - } - - private cleanupGlobalColumnDragTracking() { - this.$container.off("dragover.columnDrag"); - this.$container.off("drop.columnDrag"); - } - - private cleanupColumnDropIndicators() { - // Remove column drop indicators - this.$container.find(".column-drop-indicator").remove(); - } - - private showColumnDropIndicator(mouseX: number) { - // Clean up existing indicators - this.cleanupColumnDropIndicators(); - - // Get all columns (excluding the dragged one if it exists) - let $allColumns = this.$container.find('.board-column'); - if (this.context.draggedColumnElement) { - $allColumns = $allColumns.not(this.context.draggedColumnElement); - } - - let $targetColumn: JQuery = $(); - let insertBefore = false; - - // Find which column the mouse is closest to - $allColumns.each((_, columnEl) => { - const $column = $(columnEl); - const rect = columnEl.getBoundingClientRect(); - const columnMiddle = rect.left + rect.width / 2; - - if (mouseX >= rect.left && mouseX <= rect.right) { - // Mouse is over this column - $targetColumn = $column; - insertBefore = mouseX < columnMiddle; - return false; // Break the loop - } - }); - - // If no column found under mouse, find the closest one - if ($targetColumn.length === 0) { - let closestDistance = Infinity; - $allColumns.each((_, columnEl) => { - const $column = $(columnEl); - const rect = columnEl.getBoundingClientRect(); - const columnCenter = rect.left + rect.width / 2; - const distance = Math.abs(mouseX - columnCenter); - - if (distance < closestDistance) { - closestDistance = distance; - $targetColumn = $column; - insertBefore = mouseX < columnCenter; - } - }); - } - - if ($targetColumn.length > 0) { - const $dropIndicator = $("
").addClass("column-drop-indicator"); - - if (insertBefore) { - $targetColumn.before($dropIndicator); - } else { - $targetColumn.after($dropIndicator); - } - - $dropIndicator.addClass("show"); - } - } - - private async handleColumnDrop() { - console.log("handleColumnDrop called for:", this.context.draggedColumn); - - if (!this.context.draggedColumn || !this.context.draggedColumnElement) { - console.log("No dragged column or element found"); - return; - } - - try { - // Find the drop indicator to determine insert position - const $dropIndicator = this.$container.find(".column-drop-indicator.show"); - console.log("Drop indicator found:", $dropIndicator.length > 0); - - if ($dropIndicator.length > 0) { - // Get current column order from the API (source of truth) - const currentOrder = [...this.api.columns]; - - let newOrder = [...currentOrder]; - - // Remove dragged column from current position - newOrder = newOrder.filter(col => col !== this.context.draggedColumn); - - // Determine insertion position based on drop indicator position - const $nextColumn = $dropIndicator.next('.board-column'); - const $prevColumn = $dropIndicator.prev('.board-column'); - - let insertIndex = -1; - - if ($nextColumn.length > 0) { - // Insert before the next column - const nextColumnValue = $nextColumn.attr('data-column'); - if (nextColumnValue) { - insertIndex = newOrder.indexOf(nextColumnValue); - } - } else if ($prevColumn.length > 0) { - // Insert after the previous column - const prevColumnValue = $prevColumn.attr('data-column'); - if (prevColumnValue) { - insertIndex = newOrder.indexOf(prevColumnValue) + 1; - } - } else { - // Insert at the beginning - insertIndex = 0; - } - - // Insert the dragged column at the determined position - if (insertIndex >= 0 && insertIndex <= newOrder.length) { - newOrder.splice(insertIndex, 0, this.context.draggedColumn); - } else { - // Fallback: insert at the end - newOrder.push(this.context.draggedColumn); - } - - // Update column order in API - await this.api.reorderColumns(newOrder); - } else { - console.warn("No drop indicator found for column drop"); - } - } catch (error) { - console.error("Failed to reorder columns:", error); - } finally { - this.cleanupColumnDropIndicators(); - } - } -} diff --git a/apps/client/src/widgets/view_widgets/board_view/index.ts b/apps/client/src/widgets/view_widgets/board_view/index.ts deleted file mode 100644 index 4d548d8cb..000000000 --- a/apps/client/src/widgets/view_widgets/board_view/index.ts +++ /dev/null @@ -1,253 +0,0 @@ -import ViewMode, { ViewModeArgs } from "../view_mode"; -import { BoardData } from "./config"; -import SpacedUpdate from "../../../services/spaced_update"; -import { setupContextMenu } from "./context_menu"; -import BoardApi from "./api"; -import { BoardDragHandler, DragContext } from "./drag_handler"; -import { DifferentialBoardRenderer } from "./differential_renderer"; - -export default class BoardView extends ViewMode { - - private $root: JQuery; - private $container: JQuery; - private spacedUpdate: SpacedUpdate; - private dragContext: DragContext; - private persistentData: BoardData; - private api?: BoardApi; - private dragHandler?: BoardDragHandler; - private renderer?: DifferentialBoardRenderer; - - constructor(args: ViewModeArgs) { - super(args, "board"); - - this.$root = $(TPL); - this.$container = this.$root.find(".board-view-container"); - this.spacedUpdate = new SpacedUpdate(() => this.onSave(), 5_000); - this.persistentData = { - columns: [] - }; - this.dragContext = { - draggedNote: null, - draggedBranch: null, - draggedNoteElement: null, - draggedColumn: null, - draggedColumnElement: null - }; - - args.$parent.append(this.$root); - } - - async renderList(): Promise | undefined> { - if (!this.renderer) { - // First time setup - this.$container.empty(); - await this.initializeRenderer(); - } - - await this.renderer!.renderBoard(); - return this.$root; - } - - private async initializeRenderer() { - this.api = await BoardApi.build(this.parentNote, this.viewStorage); - this.dragHandler = new BoardDragHandler( - this.$container, - this.api, - this.dragContext - ); - - this.renderer = new DifferentialBoardRenderer( - this.$container, - this.api, - this.dragHandler, - this.parentNote, - this.viewStorage, - () => this.refreshApi() - ); - - setupContextMenu({ - $container: this.$container, - api: this.api, - boardView: this - }); - - // Setup column title editing and add column functionality - this.setupBoardInteractions(); - } - - private async refreshApi(): Promise { - if (!this.api) { - throw new Error("API not initialized"); - } - - await this.api.refresh(this.parentNote); - } - - private setupBoardInteractions() { - // Handle column title editing with click detection that works with dragging - this.$container.on('mousedown', 'h3[data-column-value]', (e) => { - const $titleEl = $(e.currentTarget); - - // Don't interfere with editing mode - if ($titleEl.hasClass('editing') || $(e.target).is('input')) { - return; - } - - const startTime = Date.now(); - let hasMoved = false; - const startX = e.clientX; - const startY = e.clientY; - - const handleMouseMove = (moveEvent: JQuery.MouseMoveEvent) => { - const deltaX = Math.abs(moveEvent.clientX - startX); - const deltaY = Math.abs(moveEvent.clientY - startY); - if (deltaX > 5 || deltaY > 5) { - hasMoved = true; - } - }; - - const handleMouseUp = (upEvent: JQuery.MouseUpEvent) => { - const duration = Date.now() - startTime; - $(document).off('mousemove', handleMouseMove); - $(document).off('mouseup', handleMouseUp); - - // If it was a quick click without much movement, treat as edit request - if (duration < 500 && !hasMoved && upEvent.button === 0) { - const columnValue = $titleEl.attr('data-column-value'); - if (columnValue) { - const columnItems = this.api?.getColumn(columnValue) || []; - this.startEditingColumnTitle($titleEl, columnValue, columnItems); - } - } - }; - - $(document).on('mousemove', handleMouseMove); - $(document).on('mouseup', handleMouseUp); - }); - - // Handle add column button - this.$container.on('click', '.board-add-column', (e) => { - e.stopPropagation(); - this.startCreatingNewColumn($(e.currentTarget)); - }); - } - - private startEditingColumnTitle($titleEl: JQuery, columnValue: string, columnItems: { branch: any; note: any; }[]) { - if ($titleEl.hasClass("editing")) { - return; // Already editing - } - - const $titleSpan = $titleEl.find("span").first(); // Get the text span - const currentTitle = $titleSpan.text(); - $titleEl.addClass("editing"); - - // Disable dragging while editing - $titleEl.attr("draggable", "false"); - - const $input = $("") - .attr("type", "text") - .val(currentTitle) - .attr("placeholder", "Column title"); - - // Prevent events from bubbling to parent drag handlers - $input.on('mousedown mouseup click', (e) => { - e.stopPropagation(); - }); - - $titleEl.empty().append($input); - $input.focus().select(); - - const finishEdit = async (save: boolean = true) => { - if (!$titleEl.hasClass("editing")) { - return; // Already finished - } - - $titleEl.removeClass("editing"); - - // Re-enable dragging after editing - $titleEl.attr("draggable", "true"); - - let finalTitle = currentTitle; - if (save) { - const newTitle = $input.val() as string; - if (newTitle.trim() && newTitle !== currentTitle) { - await this.renameColumn(columnValue, newTitle.trim(), columnItems); - finalTitle = newTitle.trim(); - } - } - - // Recreate the title structure - const { $titleText, $editIcon } = this.createTitleStructure(finalTitle); - $titleEl.empty().append($titleText, $editIcon); - }; - - $input.on("blur", () => finishEdit(true)); - $input.on("keydown", (e) => { - if (e.key === "Enter") { - e.preventDefault(); - finishEdit(true); - } else if (e.key === "Escape") { - e.preventDefault(); - finishEdit(false); - } - }); - } - - private async renameColumn(oldValue: string, newValue: string, columnItems: { branch: any; note: any; }[]) { - try { - // Get all note IDs in this column - const noteIds = columnItems.map(item => item.note.noteId); - - // Use the API to rename the column (update all notes) - // This will trigger onEntitiesReloaded which will automatically refresh the board - await this.api?.renameColumn(oldValue, newValue, noteIds); - } catch (error) { - console.error("Failed to rename column:", error); - } - } - - forceFullRefresh() { - this.renderer?.forceFullRender(); - return this.renderList(); - } - - private startCreatingNewColumn($addColumnEl: JQuery) { - $addColumnEl.empty().append($input); - $input.focus(); - - const finishEdit = async (save: boolean = true) => { - if (!$addColumnEl.hasClass("editing")) { - return; // Already finished - } - - $addColumnEl.removeClass("editing"); - - if (save) { - const columnName = $input.val() as string; - if (columnName.trim()) { - await this.createNewColumn(columnName.trim()); - } - } - }; - } - - private async createNewColumn(columnName: string) { - try { - // Check if column already exists - if (this.api?.columns.includes(columnName)) { - console.warn("A column with this name already exists."); - return; - } - - // Create the new column - await this.api?.createColumn(columnName); - - // Refresh the board to show the new column - await this.renderList(); - } catch (error) { - console.error("Failed to create new column:", error); - } - } - - -}