From adfcddd74cab11c92eaf441d76469d4943d2474a Mon Sep 17 00:00:00 2001 From: Poluboiarinov Anton <50020771+poanse@users.noreply.github.com> Date: Tue, 9 Jun 2026 03:47:23 +0300 Subject: [PATCH] Removed task name character limit and made task size expand --- manifest.json | 2 +- package-lock.json | 4 +- package.json | 2 +- scripts/perf-tasks.ts | 1 + src/Context.svelte.ts | 41 +++ src/NodePositionsCalculator.ts | 423 ++++++++++++------------- src/SaveManager.ts | 3 + src/components/AddTaskButton.svelte | 8 +- src/components/Connection.svelte | 26 +- src/components/HideBranchButton.svelte | 12 +- src/components/Task.svelte | 16 +- src/components/TaskText.svelte | 80 ++--- src/components/TaskmapContainer.svelte | 30 +- src/data/Action.ts | 24 +- src/data/ProjectData.svelte.ts | 53 +++- src/data/ProjectDataSchema.ts | 16 +- src/data/VersionedData.ts | 35 +- 17 files changed, 445 insertions(+), 331 deletions(-) diff --git a/manifest.json b/manifest.json index f0508b0..e19c9b3 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "taskmap", "name": "Taskmap", - "version": "0.1.7", + "version": "0.1.8", "minAppVersion": "1.12.4", "description": "Plan projects via interactive GUI task trees with automatic layout.", "author": "poanse", diff --git a/package-lock.json b/package-lock.json index 7e4fc7c..64096e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-taskmap", - "version": "0.1.7", + "version": "0.1.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-taskmap", - "version": "0.1.5", + "version": "0.1.8", "license": "MIT", "dependencies": { "@napolab/alpha-blend": "^2.0.0", diff --git a/package.json b/package.json index 0a9d2ac..9c582f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-taskmap", - "version": "0.1.7", + "version": "0.1.8", "description": "Taskmap plugin for Obsidian (https://obsidian.md)", "main": "main.js", "scripts": { diff --git a/scripts/perf-tasks.ts b/scripts/perf-tasks.ts index 2b63867..27cf8a5 100644 --- a/scripts/perf-tasks.ts +++ b/scripts/perf-tasks.ts @@ -92,6 +92,7 @@ function createProjectData(tasks: TaskData[]): ProjectData { blockerPairs: [], folderPath: undefined, curTaskId: tasks.length, + taskSizeOverrides: [], }); } diff --git a/src/Context.svelte.ts b/src/Context.svelte.ts index 55aa69e..617c579 100644 --- a/src/Context.svelte.ts +++ b/src/Context.svelte.ts @@ -106,6 +106,12 @@ export class Context { this.hoveredBlockedId = NoTaskId; this.pressedButtonCode = -1; + for (const taskId of projectData.taskSizeOverrides.keys()) { + const size = projectData.taskSizeOverrides.get(taskId); + if (size) { + this.versionedData.setTaskSizeOverride(taskId, size); + } + } const sel = this.versionedData.getTaskOption(this.selectedTaskId); if (sel === undefined || sel.deleted) { this.selectedTaskId = NoTaskId; @@ -173,6 +179,10 @@ export class Context { } }; + public canStartTaskDragging = (taskId: TaskId) => { + return this.editingTaskId == NoTaskId && taskId != RootTaskId; + }; + public startTaskDragging = (e: PointerEvent, taskId: TaskId) => { this.draggedTaskId = taskId; this.taskDraggingManager.onPointerDown(e); @@ -286,6 +296,36 @@ export class Context { .includes(taskId); } + public setTaskHeightOverride(taskId: TaskId, heightPx: number) { + this.versionedData.setTaskSizeOverride(taskId, { + x: TASK_SIZE.width, + y: heightPx, + }); + this.updateTaskPositions(); + this.save(); + } + + public hasTaskHeightOverride(taskId: TaskId) { + return this.versionedData.hasTaskSizeOverride(taskId); + } + + public clearTaskHeightOverride(taskId: TaskId) { + if (this.versionedData.hasTaskSizeOverride(taskId)) { + this.versionedData.clearTaskSizeOverride(taskId); + this.updateTaskPositions(); + this.save(); + } + } + + public getTaskSize(taskId: TaskId) { + return ( + this.versionedData.getTaskSizeOverride(taskId) ?? { + x: TASK_SIZE.width, + y: TASK_SIZE.height, + } + ); + } + public updateTaskPositions(draggingOnly = false) { if (!draggingOnly) { const newPositions = @@ -297,6 +337,7 @@ export class Context { x: 0, y: (innerHeight.current ?? 0) / 2 - TASK_SIZE.height, }, + this.versionedData.getTaskSizeOverrides(), ); if (this.taskDraggingManager.isDragging) { diff --git a/src/NodePositionsCalculator.ts b/src/NodePositionsCalculator.ts index 428212b..400923b 100644 --- a/src/NodePositionsCalculator.ts +++ b/src/NodePositionsCalculator.ts @@ -1,5 +1,6 @@ // Assumed types and constants based on usage import type { TaskData, TaskId, Vector2 } from "./types"; +import { TASK_SIZE } from "./Constants"; export const NoTaskId = -1 as TaskId; export const RootTaskId = 0 as TaskId; @@ -28,10 +29,36 @@ export enum AlgorithmEnum { DoubleRow, } -export const ParentToChildHorizontalShift = 400; +// Horizontal shift between parent and child: width + gap +export const ParentToChildHorizontalGap = 120; + +type Node = { + id: TaskId; + size: Vector2; + subtreeSize: Vector2 | undefined; + siblingShift: Vector2 | undefined; + parentAlignmentShift: Vector2 | undefined; + finalShiftInParentFrame: Vector2 | undefined; +}; export class NodePositionsCalculator { - public readonly SiblingDelta = 90; + /** + * Vertical shift between siblings: height + gap + */ + public readonly SiblingVerticalGap = 10; + /** + * Additional gap between subtrees to distinguish between subtrees in a long vertical list of tasks + */ + public readonly SiblingParentsVerticalGap = 5; + /** + * Position of the parent node relative to the height of the subtree. 0 - top, 0.5 - center, 1 - bottom + */ + public readonly AlignmentRatio = 0.5; + + private readonly DefaultNodeSize = { + x: TASK_SIZE.width, + y: TASK_SIZE.height, + }; public Algorithm: AlgorithmEnum = AlgorithmEnum.DefaultTree; public subtreeWidthByHalfPriority: Map = new Map< number, @@ -47,8 +74,9 @@ export class NodePositionsCalculator { public CalculatePositionsInGlobalFrame( tasks: TaskData[], rootPosition: Vector2, + sizes: Map | undefined = undefined, ): Map { - const rootFrame = this.CalculatePositionsInRootFrame(tasks); + const rootFrame = this.CalculatePositionsInRootFrame(tasks, sizes); const result: Map = new Map(); for (const [key, value] of rootFrame) { @@ -62,9 +90,12 @@ export class NodePositionsCalculator { */ private CalculatePositionsInRootFrame( tasks: TaskData[], + sizes: Map | undefined = undefined, ): Map { - const parentFramePositions = - this.CalculatePositionsInParentFrame(tasks); + const parentFramePositions = this.CalculatePositionsInParentFrame( + tasks, + sizes, + ); const positions: Map = new Map(); positions.set(NoTaskId, V2.Zero); @@ -92,6 +123,16 @@ export class NodePositionsCalculator { } positions.set(t.taskId, V2.add(parentPos, relativePos)); }); + // Make root unmovable + const rootShift = positions.get(RootTaskId); + if (rootShift) { + tasks.forEach((t) => { + positions.set( + t.taskId, + V2.sub(positions.get(t.taskId)!, rootShift), + ); + }); + } this.RootFramePositions = positions; return positions; @@ -102,15 +143,10 @@ export class NodePositionsCalculator { */ private CalculatePositionsInParentFrame( tasks: TaskData[], + sizes: Map | undefined = undefined, ): Map { - // Position of the parent node relative to the height of the subtree. 0 - top, 0.5 - center, 1 - bottom - const alignmentRatio: Vector2 = { x: 0, y: 0.5 }; - // Additional shift in the form of a fraction of siblingDelta, so that siblings' children do not merge into a single list - const parentDelta: Vector2 = { x: 0, y: 0.15 }; - - //// Helper data structures + // Helper data structures // Sort tasks to simplify the implementation of DP: depth DESC, parentId ASC/DESC, priority ASC - // TODO: maybe support order sorting in DataModel? const sortedTasks = [...tasks].sort((a, b) => { if (b.depth !== a.depth) { return b.depth - a.depth; @@ -140,235 +176,166 @@ export class NodePositionsCalculator { //// Calculations // key - root id of the subtree - const subtreeSizeByNodeId: Map = new Map< - TaskId, - Vector2 - >(); + const nodes: Map = new Map(); + tasks.forEach((t) => + nodes.set(t.taskId, { + id: t.taskId, + size: sizes?.get(t.taskId) ?? this.DefaultNodeSize, + subtreeSize: undefined, + finalShiftInParentFrame: undefined, + parentAlignmentShift: undefined, + siblingShift: undefined, + }), + ); - const allIdsToProcess = [...sortedTasks.map((t) => t.taskId), NoTaskId]; + this.CalculateSubtreeSizes(nodes, sortedTasks, childrenIdsByParentId); + // Shift of children nodes relative to the top-left point of the subtree rectangle + this.CalculateSiblingShift(nodes, parentIds, childrenIdsByParentId); + + // Shift of the parent relative to the top-left point of the subtree rectangle. + // Subtract parentDelta, so that the parent is aligned with the children + this.CalculateParentAlignmentShift(nodes, parentIds); + + // Shift of children nodes relative to the parent + // in pixels - not in relative coordinates + this.CalculateFinalShiftInParentFrame(nodes, sortedTasks); + + const res = new Map(); + nodes.forEach((node) => { + res.set(node.id, node.finalShiftInParentFrame!); + }); + return res; + } + /** + * Size of subtree in integer amounts of tasks + */ + private CalculateSubtreeSizes( + nodes: Map, + sortedTasks: TaskData[], + childrenIdsByParentId: Map, + ): void { + const allIdsToProcess = sortedTasks.map((t) => t.taskId); allIdsToProcess.forEach((id) => { - if (childrenIdsByParentId.has(id)) { - const childrenIds = childrenIdsByParentId.get(id)!; - const xAgg = - Math.max( - ...childrenIds.map( - (x) => subtreeSizeByNodeId.get(x)!.x, - ), - ) + 1; - const yAgg = childrenIds - .map((x) => subtreeSizeByNodeId.get(x)!.y) - .reduce((a, b) => a + b, 0); - subtreeSizeByNodeId.set( - id, - V2.add({ x: xAgg, y: yAgg }, parentDelta), - ); + const childrenIds = childrenIdsByParentId.get(id) ?? []; + const node = nodes.get(id); + if (node === undefined) { + throw new Error(`${id} not found in nodes`); + } + if (childrenIds.length == 0) { + node.subtreeSize = node.size; } else { - subtreeSizeByNodeId.set(id, V2.One); + const childrenSizes = childrenIds.map( + (x) => nodes.get(x)!.subtreeSize!, + ); + const maxSubtreeXSize = Math.max( + ...childrenSizes.map((s) => s.x), + ); + const nodeYsize = + childrenSizes.map((s) => s.y).reduce((a, b) => a + b, 0) + + (childrenSizes.length - 1) * this.SiblingVerticalGap; + node.subtreeSize = { + x: + maxSubtreeXSize + + node.size.x + + ParentToChildHorizontalGap, + y: Math.max(nodeYsize, node.size.y), + }; } }); + } - // Shift of children nodes relative to the left-top point of the rectangle - const siblingShiftsRel: Map = new Map< - TaskId, - Vector2 - >(); + private CalculateSiblingShift( + nodes: Map, + parentIds: TaskId[], + childrenIdsByParentId: Map, + ): void { parentIds.forEach((parentId) => { const children = childrenIdsByParentId.get(parentId)!; children.forEach((childId, idx) => { - let s = V2.mult( - subtreeSizeByNodeId.get(childId)!, - alignmentRatio, - ); - if (idx > 0) { - const previousSibling = children[idx - 1]; - const prevSize = V2.mult( - subtreeSizeByNodeId.get(previousSibling)!, - V2.sub(V2.One, alignmentRatio), - ); - s = V2.add(s, prevSize); - s = V2.add(s, siblingShiftsRel.get(previousSibling)!); - } - // Additional shift for drawing blockers - // var blockerCount = BlockerDataManager.GetConnections() - // .Count(x => x.Item1 == childId); - // s += blockerCount; - siblingShiftsRel.set(childId, s); - }); - }); + const childNode = nodes.get(childId)!; + const xShift = childNode.size.x + ParentToChildHorizontalGap; + if (idx == 0) { + const parentNodeSize = + nodes.get(parentId)?.size ?? this.DefaultNodeSize; // in case of -1 + const childrenYSumWithGaps = + children + .map((x) => nodes.get(x)!.subtreeSize!.y) + .reduce((a, b) => a + b, 0) + + (children.length - 1) * this.SiblingVerticalGap; + childNode.siblingShift = { + x: xShift, + y: Math.max( + 0, + (parentNodeSize.y - childrenYSumWithGaps) / 2, + ), + }; + } else if (idx > 0) { + const prevSiblingId = children[idx - 1]; + const prevSiblingNode = nodes.get(prevSiblingId)!; + const siblingBranchesHaveChildren = + (childrenIdsByParentId.get(childId)?.length ?? 0) > 0 && + (childrenIdsByParentId.get(prevSiblingId)?.length ?? + 0) > 0; - // Shift of the parent relative to the left-top point of the rectangle. - // Subtract parentDelta, so that the parent is aligned with the children - const parentAlignmentShift: Map = new Map< - TaskId, - Vector2 - >(); - parentIds.forEach((parentId) => { - if (!subtreeSizeByNodeId.has(parentId)) { - throw new Error( - `Failed to calculate layout: taskId [${parentId}] not in subtreeSizeByNodeId`, - ); - } - parentAlignmentShift.set( - parentId, - V2.mult( - V2.sub(subtreeSizeByNodeId.get(parentId)!, parentDelta), - alignmentRatio, - ), - ); - }); - - // Shift of children nodes relative to the parent and already in normal coordinates - const finalChildShifts: Map = new Map< - TaskId, - Vector2 - >(); - sortedTasks.forEach((t) => { - const parentShift = parentAlignmentShift.get(t.parentId); - const siblingShift = siblingShiftsRel.get(t.taskId); - if (parentShift === undefined) { - throw new Error(); - } - if (siblingShift === undefined) { - throw new Error(); - } - - const verticalComponent = V2.mult( - { x: 0, y: this.SiblingDelta }, - V2.sub(siblingShift, parentShift), - ); - - finalChildShifts.set( - t.taskId, - V2.add( - { x: ParentToChildHorizontalShift, y: 0 }, - verticalComponent, - ), - ); - }); - - const rootTaskHasMultipleVisibleChildren = - (childrenIdsByParentId.get(RootTaskId)! || []).length > 1; - - if ( - rootTaskHasMultipleVisibleChildren && - [AlgorithmEnum.SingleRow, AlgorithmEnum.DoubleRow].includes( - this.Algorithm, - ) - ) { - const rowMinYShift = 0.3; - const depthOneTasks = sortedTasks.filter((x) => x.depth === 1); - - // Group by RowIndex - const depthOneTasksByRow: Map = new Map< - number, - TaskData[] - >(); - depthOneTasks.forEach((t) => { - const idx = this.RowIndex(t); - if (!depthOneTasksByRow.has(idx)) { - depthOneTasksByRow.set(idx, []); - } - depthOneTasksByRow.get(idx)!.push(t); - }); - - const yShiftByRowId: Map = new Map< - number, - number - >(); - [...depthOneTasksByRow.keys()].forEach((key) => { - const rowIdx = Number(key); - const elements = depthOneTasksByRow.get(rowIdx)!; - - const shifts = elements.map((n) => - parentAlignmentShift.has(n.taskId) - ? parentAlignmentShift.get(n.taskId)!.y - : V2.mult(alignmentRatio, V2.One).y, - ); - - const maxYShift = shifts.length > 0 ? Math.max(...shifts) : 0; // MaxOrDefault(0) equivalent - yShiftByRowId.set( - rowIdx, - (maxYShift + rowMinYShift) * this.SiblingDelta, - ); - }); - - if (this.Algorithm === AlgorithmEnum.SingleRow) { - let subtreeRelWidthAccumulator = this.xshift; - depthOneTasks.forEach((x) => { - finalChildShifts.set(x.taskId, { - x: - finalChildShifts.get(x.taskId)!.x + - subtreeRelWidthAccumulator * - ParentToChildHorizontalShift, - y: yShiftByRowId.get(this.RowIndex(x))!, - }); - subtreeRelWidthAccumulator += - subtreeSizeByNodeId.get(x.taskId)!.x + this.xshift; - }); - } else if (this.Algorithm === AlgorithmEnum.DoubleRow) { - // Sizes of subtrees by x. If 2 tasks are above each other, the larger one is used. - this.subtreeWidthByHalfPriority = new Map(); - - const rootChildrenByPriority: Map = new Map< - number, - TaskId - >(); - const rootChildren = - childrenIdsByParentId.get(RootTaskId) ?? []; - rootChildren.forEach((id, idx) => { - // reenumerate priorities based on index - rootChildrenByPriority.set(idx, id); - }); - - rootChildrenByPriority.forEach((_priority, chId) => { - const x = taskById.get(chId)!; - if (this.RowIndex(x) === 0) { - const neighbourId = - rootChildrenByPriority.get(x.priority + 1) ?? - x.taskId; - - this.subtreeWidthByHalfPriority.set( - Math.floor(x.priority / 2), - Math.max( - subtreeSizeByNodeId.get(x.taskId)!.x, - subtreeSizeByNodeId.get(neighbourId)!.x, - ), - ); - } - }); - - depthOneTasks.forEach((x) => { - const t = [...this.subtreeWidthByHalfPriority] - .filter(([k, _]) => k < Math.floor(x.priority / 2)) - .map(([_, v]) => v); - - const tSum = t.reduce((a, b) => a + b, 0); - - finalChildShifts.set(x.taskId, { - x: - finalChildShifts.get(x.taskId)!.x + - (this.xshift + this.xshift * t.length + tSum) * - ParentToChildHorizontalShift, + childNode.siblingShift = { + x: xShift, y: - this.RowIndex(x) === 0 - ? yShiftByRowId.get(this.RowIndex(x))! - : -yShiftByRowId.get(this.RowIndex(x))!, - }); - }); - } - } - return finalChildShifts; + prevSiblingNode.siblingShift!.y + + prevSiblingNode.subtreeSize!.y + + this.SiblingVerticalGap + + (siblingBranchesHaveChildren + ? this.SiblingParentsVerticalGap + : 0), + }; + } + }); + }); } - public RowIndex(node: TaskData): number { - switch (this.Algorithm) { - case AlgorithmEnum.SingleRow: - return 0; - case AlgorithmEnum.DoubleRow: - return node.priority % 2; - default: - throw new Error("AssumptionViolation"); - } + private CalculateParentAlignmentShift( + nodes: Map, + parentIds: TaskId[], + ) { + parentIds + .filter((p) => p != NoTaskId) + .forEach((taskId) => { + const node = nodes.get(taskId); + if (node === undefined) { + throw new Error(`Task id ${taskId} is missing from nodes`); + } + if (!node?.subtreeSize) { + throw new Error( + `Failed to calculate layout: taskId [${taskId}] not in subtreeSizeByNodeId`, + ); + } + if (this.AlignmentRatio == 0.5) { + const yShift = (node.subtreeSize.y - node.size.y) / 2; + node.parentAlignmentShift = { x: 0, y: yShift }; + } else { + throw new Error("Not implemented"); + } + }); + } + + private CalculateFinalShiftInParentFrame( + nodes: Map, + sortedTasks: TaskData[], + ) { + sortedTasks + .filter((t) => t.taskId != NoTaskId) + .forEach((t) => { + const parentNode = nodes.get(t.parentId); + const parentShift = parentNode?.parentAlignmentShift ?? V2.Zero; + const taskNode = nodes.get(t.taskId)!; + + taskNode.finalShiftInParentFrame = V2.sub( + V2.add( + taskNode.siblingShift!, + taskNode.parentAlignmentShift ?? V2.Zero, + ), + parentShift, + ); + }); } } diff --git a/src/SaveManager.ts b/src/SaveManager.ts index 439373f..10a7c3e 100644 --- a/src/SaveManager.ts +++ b/src/SaveManager.ts @@ -16,6 +16,9 @@ export function serializeProjectData(projectData: ProjectData) { blockerPairs: projectData.blockerPairs, folderPath: projectData.folderPath, curTaskId: projectData.curTaskId, + taskSizeOverrides: [...projectData.taskSizeOverrides.entries()].map( + ([taskId, { x, y }]) => ({ taskId, x, y }), + ), }, null, 2, diff --git a/src/components/AddTaskButton.svelte b/src/components/AddTaskButton.svelte index 869b64b..ea834e2 100644 --- a/src/components/AddTaskButton.svelte +++ b/src/components/AddTaskButton.svelte @@ -1,5 +1,4 @@ 
entered = true} onpointerleave={() => entered = false} style=" - left: {TASK_SIZE.width / 2 + ParentToChildHorizontalShift / 2 - 50/2}px; - top: {TASK_SIZE.height / 2 - 50 / 2}px; + left: {left}px; + top: {top}px; " > {#if taskData.hidden} diff --git a/src/components/Task.svelte b/src/components/Task.svelte index f0c7514..b243802 100644 --- a/src/components/Task.svelte +++ b/src/components/Task.svelte @@ -27,7 +27,7 @@ viewport?.focus(); }; }); - + let taskData = $derived(context.versionedData.getTask(taskId)); // let coords = $derived(context.getTaskPosition(taskId)); let isHovered = $state(false); @@ -126,8 +126,6 @@ left: {coords.x}px; pointer-events={context.taskDraggingManager.isDragging ? 'none' : 'auto'} " - onpointerenter={() => isHovered = true} - onpointerleave={() => isHovered = false} >
isHovered = true} + onpointerleave={() => isHovered = false} onpointerdown={(e: PointerEvent) => { - if (context.editingTaskId === NoTaskId) { + if (context.canStartTaskDragging(taskId)) { context.startTaskDragging(e, taskId); } e.stopPropagation(); @@ -152,13 +152,13 @@ tabindex="-1" > -
{#if !context.taskDraggingManager.isDragging && !context.isReparentingOn() && !(context.chosenBlockedId !== NoTaskId) && !(context.chosenBlockerId !== NoTaskId)} {/if} +
{#if (context.versionedData.getChildren(taskId).length > 0) && !context.isReparentingOn() && !(context.chosenBlockedId !== NoTaskId) @@ -222,7 +222,9 @@ font-family: var(--font-text); line-height: 1.5; width: 280px; - height: 80px; + height: auto; + min-height: 80px; + padding: 10px 0; display: flex; justify-content: center; align-items: center; @@ -234,7 +236,7 @@ } .task.hovered { width: 284px; - height: 84px; + min-height: 84px; border-width: 4px; transform: translate3d(-2px,-2px,0); } diff --git a/src/components/TaskText.svelte b/src/components/TaskText.svelte index 5c98be5..f821ba7 100644 --- a/src/components/TaskText.svelte +++ b/src/components/TaskText.svelte @@ -11,6 +11,7 @@ taskPathFromFile } from "../LinkManager"; import {NoTaskId} from "../NodePositionsCalculator"; + import {TASK_SIZE} from "../Constants"; let { taskId, @@ -31,29 +32,25 @@ let textEditEl = $state(undefined); let component = new Component(); // Required by Obsidian to manage render lifecycle let isDragging = $derived(context.taskDraggingManager.isDragging); + onMount(() => { - if(!isEditing && textPreviewEl) { - renderMarkdown(); - } + renderMarkdown(); return () => { // Clean up Obsidian component references to prevent memory leaks component.unload(); }; }); $effect(() => { - if (textPreviewEl) { - renderMarkdown(); - } + renderMarkdown(); if (context.taskDraggingManager.isDragging) { document.body.classList.add('is-dragging-task'); } else { document.body.classList.remove('is-dragging-task'); } - // Cleanup function for when component is unmounted return () => document.body.classList.remove('is-dragging-task'); }); - + function handlePreviewClick(e: PointerEvent) { if (isDragging) { return; @@ -98,7 +95,7 @@ } async function renderMarkdown() { - if (!textPreviewEl) { + if (!textPreviewEl || isEditing) { return; } textPreviewEl.empty(); // Clear previous render @@ -117,6 +114,22 @@ // Optional: prevent dragging on the specific link level too (link as HTMLElement).ondragstart = (e) => e.preventDefault(); }); + + // Guard after async gap: isEditing may have become true while renderMarkdown was awaiting. + // textPreviewEl would be unmounted at that point, so offsetHeight would be zero/stale. + // The override set before editing remains valid while editing. + if (isEditing) return; + const taskEl = textPreviewEl.closest('.task') as HTMLElement | null; + if (taskEl) { + const height = taskEl.offsetHeight; + const isHeightExpanded = height > TASK_SIZE.height_hovered; + if (isHeightExpanded) { + context.setTaskHeightOverride(taskId, height); + } else { + // noop if no override present + context.clearTaskHeightOverride(taskId); + } + } } async function toggleEdit() { @@ -134,10 +147,7 @@ if (!el) { return; } - if (e.key === "Enter") { - e.preventDefault(); - el.blur(); // Triggers handleBlur - } else if (e.key === "Tab" && suggest !== null) { + if (e.key === "Tab" && suggest !== null) { // another hack to select suggest on tab e.preventDefault(); el.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'})); @@ -201,7 +211,6 @@