fixed warnings and added taskmap files cache

This commit is contained in:
poanse 2026-04-15 22:49:37 +03:00
parent 51d9175b16
commit b523367c7a
17 changed files with 334 additions and 252 deletions

View file

@ -1,4 +1,4 @@
import TaskmapPlugin from "./main";
import TaskmapPlugin from "./main";
import {
type BlockerPair,
MouseDown,
@ -14,7 +14,7 @@ import {
TFile,
type WorkspaceLeaf,
} from "obsidian";
import { TaskmapView } from "./TaskmapView";
import { TASKMAP_VIEW_TYPE, TaskmapView } from "./TaskmapView";
import {
type NodePositionsCalculator,
NoTaskId,
@ -371,7 +371,7 @@ export class Context {
}
private updateDraggedTaskPriority() {
// Если порядок тасок по оси Y отличается от приоритетов, то меняем приоритеты и пересчитываем порядок
// If task positions are different from priorities, change priorities and recalculate order
const draggedParentId = this.versionedData.getTask(
this.draggedTaskId,
).parentId;
@ -408,7 +408,11 @@ export class Context {
}
public save() {
this.app.workspace.getActiveViewOfType(TaskmapView)?.debouncedSave();
// Cannot have reference to the view, so save all opened views
this.app.workspace.getLeavesOfType(TASKMAP_VIEW_TYPE)
.map((leaf) => leaf.view)
.filter((view) => view instanceof TaskmapView)
.forEach((view) => view.debouncedSave());
}
public addTask(parentId: TaskId): void {

View file

@ -1,154 +0,0 @@
import { Notice, TAbstractFile, type App, TFile, TFolder } from "obsidian";
import type { ProjectData } from "./data/ProjectData.svelte";
import type { TaskData } from "./types";
import { TASKMAP_VIEW_TYPE, TaskmapView } from "./TaskmapView";
import { deserializeProjectData, updateFile } from "./SaveManager";
import { delink, generateMarkdownLink } from "./LinkManager";
export function getOnDelete(app: App) {
return async (file: TAbstractFile) => {
console.debug(`${file.path} deleted`);
let files: TFile[];
if (file instanceof TFile) {
files = [file];
} else if (file instanceof TFolder) {
files = app.vault
.getMarkdownFiles()
.filter((mdFile) =>
mdFile.path.startsWith(file.path + "/"),
);
} else {
return;
}
await handleDeleteFiles(
app,
files,
);
};
}
export function getOnRename(app: App) {
return async (file: TAbstractFile, oldPath: string) => {
console.debug(`${file.path} renamed`);
let files: TFile[];
if (file instanceof TFile) {
files = [file];
} else if (file instanceof TFolder) {
files = app.vault
.getMarkdownFiles()
.filter((mdFile) =>
mdFile.path.startsWith(file.path + "/"),
);
} else {
return;
}
await handleRenameFiles(
app,
files,
oldPath,
file.path,
);
};
}
export async function handleRenameFiles(
app: App,
changedMdFiles: TFile[],
oldPath: string,
newPath: string,
) {
const taskmapFiles = app.vault
.getFiles()
.filter((file) => file.path.endsWith(".taskmap"));
// update paths in taskmap files
for (const taskmapFile of taskmapFiles) {
console.debug(`handling ${taskmapFile.path}`);
let projectData: ProjectData;
try {
const projectDataRaw = await app.vault.read(taskmapFile);
projectData = deserializeProjectData(projectDataRaw);
} catch (e) {
console.error(`Taskmap rename hook: skipped invalid file ${taskmapFile.path}`);
new Notice(`Taskmap rename hook: skipped invalid file: ${String(e)}`);
continue;
}
const mapping = new Map<string, TaskData>();
projectData.tasks.forEach((t) => {
if (t.path) {
if (t.path.startsWith(oldPath)) {
t.path = newPath + t.path.slice(oldPath.length);
}
mapping.set(t.path, t);
}
});
console.debug("mapping " + JSON.stringify(mapping.keys()));
let changed = false;
changedMdFiles
.filter(mdFile => mapping.has(mdFile.path))
.forEach((mdFile) => {
const t = mapping.get(mdFile.path)!;
t.name = generateMarkdownLink(app, mdFile);
changed = true;
}
);
if (changed) {
console.debug(`${taskmapFile.path} changed`);
// resave file on the file system
await updateFile(app, taskmapFile, projectData);
} else {
console.debug(`${taskmapFile.path} not changed`);
}
}
// refresh all active views
for (const view of app.workspace
.getLeavesOfType(TASKMAP_VIEW_TYPE)
.map((l) => l.view)
.filter((view) => view instanceof TaskmapView)
) {
await view.refreshUi();
}
}
export async function handleDeleteFiles(
app: App,
files: TFile[],
) {
const deletedPaths = new Set<string>(files.map((f) => f.path));
const taskmapFiles = app.vault
.getFiles()
.filter((file) => file.path.endsWith(".taskmap"));
for (const taskmapFile of taskmapFiles) {
let projectData: ProjectData;
try {
const projectDataRaw = await app.vault.read(taskmapFile);
projectData = deserializeProjectData(projectDataRaw);
} catch (e) {
console.error(`Taskmap rename hook: skipped invalid file ${taskmapFile.path}`);
new Notice(`Taskmap rename hook: skipped invalid file: ${String(e)}`);
continue;
}
let taskmapFileChanged = false;
projectData.tasks.filter(t => t.path && deletedPaths.has(t.path)).forEach((t) => {
t.name = delink(t.name);
t.path = undefined;
taskmapFileChanged = true;
});
if (taskmapFileChanged) {
console.debug(`${taskmapFile.path} changed`);
// resave file on the file system
await updateFile(app, taskmapFile, projectData);
} else {
console.debug(`${taskmapFile.path} not changed`);
}
}
// refresh active views
for (const view of app.workspace
.getLeavesOfType(TASKMAP_VIEW_TYPE)
.map((l) => l.view)
.filter((view) => view instanceof TaskmapView)
) {
await view.refreshUi();
}
}

236
src/FileWatcherWithCache.ts Normal file
View file

@ -0,0 +1,236 @@
import {
Notice,
type Plugin,
TAbstractFile,
type App,
TFile,
TFolder,
} from "obsidian";
import type { ProjectData } from "./data/ProjectData.svelte";
import type { TaskData } from "./types";
import { TASKMAP_VIEW_TYPE, TaskmapView } from "./TaskmapView";
import { deserializeProjectData, updateFile } from "./SaveManager";
import { delink, generateMarkdownLink, pathIsUnderFolder } from "./LinkManager";
function taskPathAffectedByRename(taskPath: string, oldPath: string): boolean {
return taskPath === oldPath || pathIsUnderFolder(taskPath, oldPath);
}
function remapTaskPathAfterRename(taskPath: string, oldPath: string, newPath: string): string {
if (taskPath === oldPath) {
return newPath;
}
return newPath + taskPath.slice(oldPath.length);
}
/** In-memory index of `.taskmap` files; `TFile.path` stays in sync with vault renames. */
export class FileWatcherWithCache {
readonly cachedTaskmapFiles = new Set<TFile>();
initFromVault(app: App): void {
this.cachedTaskmapFiles.clear();
for (const f of app.vault.getFiles()) {
if (f.extension === "taskmap") {
this.cachedTaskmapFiles.add(f);
}
}
}
/**
* Registers vault hooks: taskmap index maintenance and path/link updates.
* Order: link handlers before cache on delete (cache still lists children under a deleted folder);
* cache before link on rename so `TFile.path` matches vault before we scan.
*/
registerTaskmapVaultHooks(plugin: Plugin): void {
const { app } = plugin;
plugin.registerEvent(
app.vault.on("delete", (file) => {
void this.onDeleteLinkUpdate(file, app);
}),
);
plugin.registerEvent(
app.vault.on("delete", (file) => {
this.onDeleteCacheUpdate(file);
}),
);
plugin.registerEvent(
app.vault.on("rename", (file) => {
this.onRenameCacheUpdate(file);
}),
);
plugin.registerEvent(
app.vault.on("rename", (file, oldPath) => {
void this.onRenameLinkUpdate(file, oldPath, app);
}),
);
plugin.registerEvent(
app.vault.on("create", (file) => {
this.onCreateCacheUpdate(file);
}),
);
}
onCreateCacheUpdate(file: TAbstractFile): void {
if (file instanceof TFile && file.extension === "taskmap") {
this.cachedTaskmapFiles.add(file);
}
// TFolder: each nested .taskmap emits its own "create"
}
onDeleteCacheUpdate(file: TAbstractFile): void {
if (file instanceof TFile && file.extension === "taskmap") {
this.cachedTaskmapFiles.delete(file);
} else if (file instanceof TFolder) {
for (const f of [...this.cachedTaskmapFiles]) {
if (pathIsUnderFolder(f.path, file.path)) {
this.cachedTaskmapFiles.delete(f);
}
}
}
}
/**
* Path-only renames update `TFile.path` on the same instance no Set mutation.
* Still handle extension changes (e.g. `.taskmap` `.md`).
*/
onRenameCacheUpdate(file: TAbstractFile): void {
if (!(file instanceof TFile)) {
return;
}
this.cachedTaskmapFiles.delete(file);
if (file.extension === "taskmap") {
this.cachedTaskmapFiles.add(file);
}
}
async onDeleteLinkUpdate(file: TAbstractFile, app: App) {
console.debug(`${file.path} deleted`);
let files: TFile[];
if (file instanceof TFile) {
files = [file];
} else if (file instanceof TFolder) {
files = [...this.cachedTaskmapFiles].filter((f) =>
pathIsUnderFolder(f.path, file.path),
);
} else {
return;
}
await this.handleDeleteFiles(app, files);
}
async onRenameLinkUpdate(
file: TAbstractFile,
oldPath: string,
app: App,
) {
console.debug(`${file.path} renamed`);
let files: TFile[];
if (file instanceof TFile) {
files = [file];
} else if (file instanceof TFolder) {
files = [...this.cachedTaskmapFiles].filter((f) =>
pathIsUnderFolder(f.path, file.path),
);
} else {
return;
}
await this.handleRenameFiles(app, files, oldPath, file.path);
}
async handleDeleteFiles(app: App, files: TFile[]) {
const deletedPaths = new Set<string>(files.map((f) => f.path));
for (const taskmapFile of [...this.cachedTaskmapFiles]) {
if (deletedPaths.has(taskmapFile.path)) {
// Entire .taskmap was removed; link handler runs before cache — skip read/write.
continue;
}
let projectData: ProjectData;
try {
const projectDataRaw = await app.vault.read(taskmapFile);
projectData = deserializeProjectData(projectDataRaw);
} catch (e) {
console.error(`Taskmap delete hook: skipped invalid file ${taskmapFile.path}`);
new Notice(`Taskmap delete hook: skipped invalid file: ${String(e)}`);
continue;
}
let taskmapFileChanged = false;
projectData.tasks
.filter((t) => t.path && deletedPaths.has(t.path))
.forEach((t) => {
t.name = delink(t.name);
t.path = undefined;
taskmapFileChanged = true;
});
if (taskmapFileChanged) {
console.debug(`${taskmapFile.path} changed`);
// resave file on the file system
await updateFile(app, taskmapFile, projectData);
} else {
console.debug(`${taskmapFile.path} not changed`);
}
}
// refresh all active views
for (const view of app.workspace
.getLeavesOfType(TASKMAP_VIEW_TYPE)
.map((l) => l.view)
.filter((view) => view instanceof TaskmapView)) {
await view.refreshUi();
}
}
async handleRenameFiles(
app: App,
changedMdFiles: TFile[],
oldPath: string,
newPath: string,
) {
// update paths in taskmap files
for (const taskmapFile of [...this.cachedTaskmapFiles]) {
console.debug(`handling ${taskmapFile.path}`);
let projectData: ProjectData;
try {
const projectDataRaw = await app.vault.read(taskmapFile);
projectData = deserializeProjectData(projectDataRaw);
} catch (e) {
console.error(`Taskmap rename hook: skipped invalid file ${taskmapFile.path}`);
new Notice(`Taskmap rename hook: skipped invalid file: ${String(e)}`);
continue;
}
const mapping = new Map<string, TaskData>();
projectData.tasks.forEach((t) => {
if (t.path) {
if (taskPathAffectedByRename(t.path, oldPath)) {
t.path = remapTaskPathAfterRename(t.path, oldPath, newPath);
}
mapping.set(t.path, t);
}
});
console.debug("mapping " + JSON.stringify(mapping.keys()));
let changed = false;
changedMdFiles
.filter((mdFile) => mapping.has(mdFile.path))
.forEach((mdFile) => {
const t = mapping.get(mdFile.path)!;
t.name = generateMarkdownLink(app, mdFile);
changed = true;
});
if (changed) {
console.debug(`${taskmapFile.path} changed`);
// resave file on the file system
await updateFile(app, taskmapFile, projectData);
} else {
console.debug(`${taskmapFile.path} not changed`);
}
}
// refresh all active views
for (const view of app.workspace
.getLeavesOfType(TASKMAP_VIEW_TYPE)
.map((l) => l.view)
.filter((view) => view instanceof TaskmapView)) {
await view.refreshUi();
}
}
}

View file

@ -15,15 +15,21 @@ export function delink(s: string) {
return isLink(s) ? s.slice(2, s.length - 2) : s;
}
// relativePath to TFile
/** relativePath to TFile */
export function getFromRelativePath(app: App, path: string) {
return app.vault.getFileByPath(path);
}
// TFile to wikilink
/** TFile to wikilink */
export function generateMarkdownLink(app: App, file: TFile) {
return app.fileManager.generateMarkdownLink(file, "");
}
/** True if `path` is a file inside `folderPath` (not a false `startsWith` on the folder segment). */
export function pathIsUnderFolder(path: string, folderPath: string): boolean {
return path.startsWith(folderPath + "/");
}
export class LinkManager {
private app: App;

View file

@ -58,7 +58,7 @@ export class NodePositionsCalculator {
}
/**
* Позиции относительно рута
* Positions relative to the root
*/
private CalculatePositionsInRootFrame(
tasks: TaskData[],
@ -98,19 +98,19 @@ export class NodePositionsCalculator {
}
/**
* Считаем позиции на основе веса поддеревьев и константы alignmentRatio
* Calculate positions based on the weight of subtrees and the alignmentRatio constant
*/
private CalculatePositionsInParentFrame(
tasks: TaskData[],
): Map<TaskId, Vector2> {
// Расположение ноды родителя относительно высоты поддерева. 0 - top, 0.5 - center, 1 - bottom
// 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 };
// Дополнительный сдвиг в виде доли от siblingDelta, чтобы дети сиблингов не сливались в один список
// 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 };
//// Вспомогательные структуры данных
// Сортируем таски, чтобы упросить реализацию DP: depth DESC, parentId ASC/DESC, priority ASC
// TODO: мб поддерживать порядок сортировки в DataModel?
//// 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;
@ -138,8 +138,8 @@ export class NodePositionsCalculator {
const parentIds = [...childrenIdsByParentId.keys()];
//// Расчеты
// ключ - айди рута поддерева
//// Calculations
// key - root id of the subtree
const subtreeSizeByNodeId: Map<TaskId, Vector2> = new Map<
TaskId,
Vector2
@ -168,7 +168,7 @@ export class NodePositionsCalculator {
}
});
// Шифт дочерних нод относительно левой-верхней точки прямоугольника
// Shift of children nodes relative to the left-top point of the rectangle
const siblingShiftsRel: Map<TaskId, Vector2> = new Map<
TaskId,
Vector2
@ -189,7 +189,7 @@ export class NodePositionsCalculator {
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;
@ -197,14 +197,8 @@ export class NodePositionsCalculator {
});
});
// TODO: походу в проекте что-то не так с данными.
// Нужно перейти на мета уровень и сделать удобный способ смотреть данные. Какой?
// - Логировать десятки тасок в виде json может быть полезно, но не удобно.
// - Можно добавить этап валидации данных, где будут проверяться предположения.
// - Можно добавить дебажную инфу с данными выбранной таски. Не поможет, если проблема в удаленных.
// Шифт родителя относительно левой-верхней точки прямоугольника.
// Вычитаем parentDelta, чтобы родитель был выровнен по детям
// 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<TaskId, Vector2> = new Map<
TaskId,
Vector2
@ -219,7 +213,7 @@ export class NodePositionsCalculator {
);
});
// Шифт дочерних нод относительно родителя и уже в нормальных координатах
// Shift of children nodes relative to the parent and already in normal coordinates
const finalChildShifts: Map<TaskId, Vector2> = new Map<
TaskId,
Vector2
@ -273,11 +267,8 @@ export class NodePositionsCalculator {
depthOneTasksByRow.get(idx)!.push(t);
});
const yShiftByRowId: Map<number, number> = {} as Map<
number,
number
>;
Object.keys(depthOneTasksByRow).forEach((key) => {
const yShiftByRowId: Map<number, number> = new Map<number, number>();
[...depthOneTasksByRow.keys()].forEach((key) => {
const rowIdx = Number(key);
const elements = depthOneTasksByRow.get(rowIdx)!;
@ -308,13 +299,10 @@ export class NodePositionsCalculator {
subtreeSizeByNodeId.get(x.taskId)!.x + this.xshift;
});
} else if (this.Algorithm === AlgorithmEnum.DoubleRow) {
// Размеры поддеревьев по иксу. Если 2 таски друг над другом, то используется большее.
this.subtreeWidthByHalfPriority = {} as Map<number, number>;
// Sizes of subtrees by x. If 2 tasks are above each other, the larger one is used.
this.subtreeWidthByHalfPriority = new Map<number, number>();
const rootChildrenByPriority: Map<number, TaskId> = {} as Map<
number,
TaskId
>;
const rootChildrenByPriority: Map<number, TaskId> = new Map<number, TaskId>();
const rootChildren =
childrenIdsByParentId.get(RootTaskId) ?? [];
rootChildren.forEach((id, idx) => {

View file

@ -16,7 +16,7 @@ export class TaskmapSettingTab extends PluginSettingTab {
new Setting(containerEl)
.setName("Zoom sensitivity (touchpad)")
.setDesc("In percents")
.setDesc("As a percentage")
.addText((text) =>
text
.setPlaceholder("100")
@ -28,7 +28,7 @@ export class TaskmapSettingTab extends PluginSettingTab {
);
new Setting(containerEl)
.setName("Zoom sensitivity (mouse)")
.setDesc("In percents")
.setDesc("As a percentage")
.addText((text) =>
text
.setPlaceholder("100")

View file

@ -18,6 +18,7 @@
<div
class="hover-container-add-task-button"
role="group"
onmouseenter={() => entered = true}
onmouseleave={() => entered = false}
style="
@ -28,7 +29,6 @@
{#if entered}
<svg
role="button"
aria-label="Add task"
tabindex="0"
class="button-add"
class:draft={taskData.status === StatusCode.DRAFT}

View file

@ -25,13 +25,13 @@
let isPressedDown = $state(false);
let isPressed = $derived(context.pressedButtonCode == iconCode);
const stateful = [
let stateful = $derived([
IconCode.REMOVE_SUBMENU,
IconCode.STATUS_SUBMENU,
IconCode.KEY,
IconCode.LOCK,
IconCode.REPARENT
].includes(iconCode);
].includes(iconCode));
function onpointerdown(event: MouseEvent) {
isPressedDown = true;

View file

@ -18,6 +18,7 @@
<div
class="hover-container-hide-branch-button"
role="group"
onmouseenter={() => entered = true}
onmouseleave={() => entered = false}
style="

View file

@ -100,10 +100,6 @@
stroke-linejoin: round;
will-change: transform,scale,translate;
}
.custom-svg {
stroke-width: 0.083;
fill: #bbb;
}
}
.button.disabled {
@ -131,11 +127,4 @@
stroke: white;
}
}
.button.is-pressed-up:not(.disabled){
background-color: #343434;
color: white;
:global(svg) {
stroke: white;
}
}
</style>

View file

@ -9,7 +9,10 @@
{#if !context.taskDraggingManager.isDragging}
<div
class="settings-panel"
role="toolbar"
tabindex="-1"
onclick={(e) => e.stopPropagation()}
onkeydown={(e) => e.stopPropagation()}
onpointerdown={(e) => e.stopPropagation()}
onpointerup={(e) => e.stopPropagation()}
>

View file

@ -18,14 +18,14 @@
coords: {x: number, y: number}
} = $props();
let self: HTMLElement;
let viewport: HTMLElement;
let self = $state<HTMLElement | undefined>(undefined);
let viewport = $state<HTMLElement | undefined>(undefined);
onMount(() => {
viewport = self?.closest('.viewport') as HTMLElement;
viewport = self?.closest(".viewport") as HTMLElement | undefined;
return () => {
viewport.focus();
}
viewport?.focus();
};
});
let taskData = $derived(context.versionedData.getTask(taskId));

View file

@ -21,8 +21,8 @@
let isSelected = $derived(context.isSelected(taskId));
let isEditing = $derived(context.editingTaskId === taskId);
let suggest: LinkSuggest | null = null;
let textPreviewEl: HTMLElement;
let textEditEl: HTMLTextAreaElement;
let textPreviewEl = $state<HTMLElement | undefined>(undefined);
let textEditEl = $state<HTMLTextAreaElement | undefined>(undefined);
let component = new Component(); // Required by Obsidian to manage render lifecycle
let isDragging = $derived(context.taskDraggingManager.isDragging);
onMount(() => {
@ -78,7 +78,7 @@
const target = e.target as HTMLElement;
const link = target.closest(".internal-link");
if (link) {
if (link && textPreviewEl) {
// Trigger the native hover preview
context.app.workspace.trigger("hover-link", {
event: e,
@ -92,6 +92,9 @@
}
async function renderMarkdown() {
if (!textPreviewEl) {
return;
}
textPreviewEl.empty(); // Clear previous render
const content = taskData.name;
await MarkdownRenderer.render(
@ -121,18 +124,22 @@
}
function handleKeydown(e: KeyboardEvent) {
const el = textEditEl;
if (!el) {
return;
}
if (e.key === "Enter") {
e.preventDefault();
textEditEl.blur(); // Triggers handleBlur
el.blur(); // Triggers handleBlur
} else if (e.key === "Tab" && suggest !== null) {
// another hack to select suggest on tab
e.preventDefault();
textEditEl.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
el.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
} else if (e.key == "Escape" && isEditing) {
e.stopPropagation();
textEditEl.blur();
el.blur();
} else if (e.key == "Escape") {
textEditEl.blur();
el.blur();
} else {
e.stopPropagation();
}
@ -148,7 +155,7 @@
}
function handleInput() {
if (textEditEl === null) {
if (textEditEl == null) {
return;
}
let path;
@ -174,6 +181,7 @@
class="task-text-container"
class:selected={isSelected}
class:not-selected={!isSelected}
role="group"
onpointerup={handlePreviewClick}
>
{#if isEditing}
@ -190,8 +198,9 @@
<div
class="text-preview tasktext"
class:unselect={isUnselected}
role="group"
bind:this={textPreviewEl}
onmouseover={handlePreviewMouseOver}
onmousemove={handlePreviewMouseOver}
>
</div>
{/if}
@ -218,9 +227,6 @@
.task-text-container.selected .tasktext:hover {
cursor: text;
}
.task-text-container.not-selected .tasktext:hover {
/*cursor: default;*/
}
.tasktext {
margin: 0;
padding: 0;
@ -269,19 +275,21 @@
overflow: visible;
white-space: pre-wrap;
word-wrap: break-word;
p {
top: 0;
width: var(--task-width);
height: var(--task-height);
line-height: var(--task-line-height);
margin: 0;
padding: 0;
gap: 0;
border: none;
text-align: center;
justify-content: center;
align-items: center;
overflow: visible;
:global {
p {
top: 0;
width: var(--task-width);
height: var(--task-height);
line-height: var(--task-line-height);
margin: 0;
padding: 0;
gap: 0;
border: none;
text-align: center;
justify-content: center;
align-items: center;
overflow: visible;
}
}
}
</style>

View file

@ -146,10 +146,12 @@
}
</script>
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
class="viewport"
class:is-panning={draggingManager.mouseDown === MouseDown.MIDDLE}
class:unselect={context.chosenBlockerId !== NoTaskId || context.chosenBlockedId !== NoTaskId}
role="application"
bind:this={viewportEl}
tabindex="-1"
{onwheel}

View file

@ -22,8 +22,8 @@
}: {taskId: TaskId,
context: Context} = $props();
let self: HTMLElement;
let viewport: HTMLElement;
let self: HTMLElement = $state(null! as HTMLElement);
let viewport: HTMLElement = $state(null! as HTMLElement);
onMount(() => {
viewport = self?.closest('.viewport') as HTMLElement;
@ -90,10 +90,13 @@
<div
class="toolbar"
class:no-pan={true}
role="toolbar"
tabindex="-1"
bind:this={self}
in:fade|global={{ duration: 500 }}
out:fade|global={{ duration: 300 }}
onclick={(e) => e.stopPropagation()}
onkeydown={(e) => e.stopPropagation()}
onpointerdown={(e) => e.stopPropagation()}
onpointerup={(e) => e.stopPropagation()}
style="
@ -111,6 +114,7 @@
{#if context.pressedButtonCode === IconCode.REMOVE_SUBMENU}
<div
class="subtoolbar"
role="group"
transition:slideCustom={{ duration: 300, easing: quintOut, axis: '-y' }}
style="
top: {subtoolbarTopShift(removeButtons)}px;
@ -135,6 +139,7 @@
{#if context.pressedButtonCode === IconCode.STATUS_SUBMENU}
<div
class="subtoolbar"
role="group"
transition:slideCustom={{ duration: 300, easing: quintOut, axis: '-y' }}
style="
top: {subtoolbarTopShift(statusButtons)}px;

View file

@ -38,6 +38,6 @@ export class HistoryManager {
}
lastAction() {
return this.undoStack.last();
return this.undoStack[this.undoStack.length - 1];
}
}

View file

@ -1,21 +1,19 @@
import {
addIcon,
Plugin,
} from "obsidian";
import { addIcon, Plugin } from "obsidian";
import { TASKMAP_VIEW_TYPE, TaskmapView } from "./TaskmapView";
import {
DEFAULT_SETTINGS,
type TaskmapPluginSettings,
} from "./TaskmapPluginSettings";
import { TaskmapSettingTab } from "./TaskmapSettingTab";
import { DEFAULT_DATA} from "./data/ProjectData.svelte";
import { DEFAULT_DATA } from "./data/ProjectData.svelte";
import { LOGO_CONTENT, LOGO_NAME } from "./IconService";
import { getOnDelete, getOnRename} from "./FileWatcher";
import { FileWatcherWithCache } from "./FileWatcherWithCache";
export const FILE_EXTENSION = "taskmap";
export default class TaskmapPlugin extends Plugin {
settings: TaskmapPluginSettings;
private readonly filewatcher = new FileWatcherWithCache();
async onload() {
await this.loadSettings();
@ -32,12 +30,8 @@ export default class TaskmapPlugin extends Plugin {
this.addSettingTab(new TaskmapSettingTab(this.app, this));
this.registerEvent(
this.app.vault.on("rename", getOnRename(this.app)),
);
this.registerEvent(
this.app.vault.on("delete", getOnDelete(this.app)),
);
this.filewatcher.initFromVault(this.app);
this.filewatcher.registerTaskmapVaultHooks(this);
}
public async createAndOpenDrawing(): Promise<string> {