mirror of
https://github.com/poanse/obsidian-taskmap.git
synced 2026-07-22 06:05:58 +00:00
added undo redo buttons
This commit is contained in:
parent
2d25f61717
commit
9bf3bb6e13
11 changed files with 277 additions and 17 deletions
|
|
@ -7,7 +7,7 @@ import TaskmapContainer from "./components/TaskmapContainer.svelte";
|
|||
import { deserializeProjectData, updateFile } from "./SaveManager";
|
||||
import type TaskmapPlugin from "./main";
|
||||
import { VersionedData } from "./data/VersionedData";
|
||||
import { HistoryManager } from "./data/HistoryManager";
|
||||
import { HistoryManager } from "./data/HistoryManager.svelte";
|
||||
|
||||
export const TASKMAP_VIEW_TYPE = "taskmap-view";
|
||||
|
||||
|
|
@ -47,6 +47,7 @@ export class TaskmapView extends TextFileView {
|
|||
this.app,
|
||||
new NodePositionsCalculator(),
|
||||
);
|
||||
this.contentEl.addClass("taskmap-view-container");
|
||||
this.taskmapContainer = mount(TaskmapContainer, {
|
||||
target: this.contentEl,
|
||||
props: {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { setTooltip } from "obsidian";
|
||||
import { IconCode } from "./types";
|
||||
import { IconCode, SettingsIconCode } from "./types";
|
||||
|
||||
/**
|
||||
* Svelte Action to add an Obsidian-native tooltip to an element.
|
||||
|
|
@ -54,3 +54,16 @@ export function getTooltipText(code: IconCode) {
|
|||
return "placeholder tooltip";
|
||||
}
|
||||
}
|
||||
|
||||
export function getTooltipTextSettings(code: SettingsIconCode) {
|
||||
switch (code) {
|
||||
case SettingsIconCode.SETTINGS_MENU:
|
||||
return "WIP (Settings)";
|
||||
case SettingsIconCode.SETTINGS_REDO:
|
||||
return "Redo";
|
||||
case SettingsIconCode.SETTINGS_UNDO:
|
||||
return "Undo";
|
||||
default:
|
||||
return "placeholder tooltip";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
141
src/components/SettingsButton.svelte
Normal file
141
src/components/SettingsButton.svelte
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
<script lang="ts">
|
||||
import type {Context} from "../Context.svelte.js";
|
||||
import {SettingsIconCode} from "../types";
|
||||
import {getTooltipTextSettings, tooltip} from "../Tooltip";
|
||||
import {Redo2, Settings, Undo2} from 'lucide-svelte';
|
||||
|
||||
let { iconCode, context }: { iconCode: SettingsIconCode, context: Context } = $props();
|
||||
|
||||
let isPressedDown = $state(false);
|
||||
|
||||
function onpointerdown(event: MouseEvent) {
|
||||
isPressedDown = true;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
function onBlur() {
|
||||
isPressedDown = false;
|
||||
}
|
||||
|
||||
let isButtonDisabled = $derived(
|
||||
(iconCode == SettingsIconCode.NONE)
|
||||
|| (iconCode == SettingsIconCode.SETTINGS_MENU)
|
||||
|| (iconCode == SettingsIconCode.SETTINGS_UNDO && !context.versionedData.canUndo())
|
||||
|| (iconCode == SettingsIconCode.SETTINGS_REDO && !context.versionedData.canRedo())
|
||||
);
|
||||
|
||||
function onpointerup(event: MouseEvent) {
|
||||
if (!isPressedDown) {
|
||||
return;
|
||||
}
|
||||
if (isButtonDisabled) {
|
||||
return;
|
||||
}
|
||||
isPressedDown = false;
|
||||
event.stopPropagation();
|
||||
|
||||
if (iconCode == SettingsIconCode.SETTINGS_UNDO) {
|
||||
context.versionedData.undo();
|
||||
} else if (iconCode == SettingsIconCode.SETTINGS_REDO) {
|
||||
context.versionedData.redo();
|
||||
}
|
||||
}
|
||||
|
||||
let classString = $derived(`
|
||||
${(isPressedDown && !isButtonDisabled) ? 'is-pressed-down ': '' }
|
||||
`);
|
||||
</script>
|
||||
|
||||
<div class="button"
|
||||
use:tooltip={getTooltipTextSettings(iconCode)}
|
||||
class:disabled={isButtonDisabled}
|
||||
class:no-pan={true}
|
||||
class:is-pressed-down={isPressedDown}
|
||||
{onpointerdown}
|
||||
{onpointerup}
|
||||
onmouseleave={onBlur}
|
||||
onclick={(event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
role="presentation"
|
||||
tabindex="-1"
|
||||
>
|
||||
{#if iconCode === SettingsIconCode.SETTINGS_MENU}
|
||||
<Settings class={classString}/>
|
||||
{:else if iconCode === SettingsIconCode.SETTINGS_UNDO}
|
||||
<Undo2 class={classString}/>
|
||||
{:else if iconCode === SettingsIconCode.SETTINGS_REDO}
|
||||
<Redo2 class={classString}/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0px;
|
||||
padding: 0px;
|
||||
/*background-color: #e0e0e0;*/
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
user-select: none;
|
||||
|
||||
/*background-color: #1E1E1E;*/
|
||||
|
||||
/* This handles the smooth color change for the SVG and Text */
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
|
||||
:global(svg) {
|
||||
transition: stroke 0.2s;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
stroke: #bbb;
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
will-change: transform,scale,translate;
|
||||
}
|
||||
.custom-svg {
|
||||
stroke-width: 0.083;
|
||||
fill: #bbb;
|
||||
}
|
||||
}
|
||||
|
||||
.button.disabled {
|
||||
color: color-mix(in srgb, #7E7E7E 100%, #000000 50%);
|
||||
|
||||
:global(svg) {
|
||||
stroke: grey;
|
||||
}
|
||||
:global(svg.done) {
|
||||
stroke: color-mix(in srgb, #30623E 100%, #000000 50%);
|
||||
}
|
||||
}
|
||||
|
||||
.button:hover:not(.disabled){
|
||||
background-color: #343434;
|
||||
/*border-color: red;*/
|
||||
border-width: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.button.is-pressed-down:not(.disabled){
|
||||
background-color: #343434;
|
||||
color: white;
|
||||
:global(svg) {
|
||||
stroke: white;
|
||||
}
|
||||
}
|
||||
.button.is-pressed-up:not(.disabled){
|
||||
background-color: #343434;
|
||||
color: white;
|
||||
:global(svg) {
|
||||
stroke: white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
55
src/components/SettingsPanel.svelte
Normal file
55
src/components/SettingsPanel.svelte
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<script lang="ts">
|
||||
import type {Context} from "../Context.svelte.js";
|
||||
import {SettingsIconCode} from "../types";
|
||||
import SettingsButton from "./SettingsButton.svelte";
|
||||
|
||||
let { context }: { context: Context } = $props();
|
||||
</script>
|
||||
|
||||
{#if !context.taskDraggingManager.isDragging}
|
||||
<div
|
||||
class="settings-panel"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onpointerdown={(e) => e.stopPropagation()}
|
||||
onpointerup={(e) => e.stopPropagation()}
|
||||
>
|
||||
<SettingsButton iconCode={SettingsIconCode.SETTINGS_MENU} {context}/>
|
||||
<div
|
||||
class="undo-redo-container"
|
||||
>
|
||||
<SettingsButton iconCode={SettingsIconCode.SETTINGS_UNDO} {context}/>
|
||||
<SettingsButton iconCode={SettingsIconCode.SETTINGS_REDO} {context}/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
/* Figma: x 0, y 2, blur 4, spread 0, color #000000 (use the same opacity as in Figma) */
|
||||
.settings-panel {
|
||||
box-shadow: 0px 4px 8px 0px rgba(0, 0, 0, 1);
|
||||
gap: 72px;
|
||||
padding: 4px 4px;
|
||||
background: #1E1E1E;
|
||||
border-radius: 4px;
|
||||
border-color: #1E1E1E;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
will-change: transform,scale,translate;
|
||||
width: 148px;
|
||||
height: 28px;
|
||||
bottom: 15px;
|
||||
left: 15px;
|
||||
/*transform: translate(-50%, -50%);*/
|
||||
}
|
||||
.undo-redo-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
import HideBranchButton from "./HideBranchButton.svelte";
|
||||
import {NoTaskId} from "../NodePositionsCalculator";
|
||||
import {KeyRound, LockKeyhole} from "lucide-svelte";
|
||||
import {onMount} from "svelte";
|
||||
|
||||
const {
|
||||
taskId,
|
||||
|
|
@ -17,6 +18,15 @@
|
|||
coords: {x: number, y: number}
|
||||
} = $props();
|
||||
|
||||
let self: HTMLElement;
|
||||
let viewport: HTMLElement;
|
||||
|
||||
onMount(() => {
|
||||
viewport = self?.closest('.viewport') as HTMLElement;
|
||||
return () => {
|
||||
viewport.focus();
|
||||
}
|
||||
});
|
||||
|
||||
let taskData = $derived(context.versionedData.getTask(taskId));
|
||||
// let coords = $derived(context.getTaskPosition(taskId));
|
||||
|
|
@ -109,6 +119,7 @@
|
|||
{#key context.updateOnZoomCounter}
|
||||
<div
|
||||
class="task-container"
|
||||
bind:this={self}
|
||||
style="
|
||||
top: {coords.y}px;
|
||||
left: {coords.x}px;
|
||||
|
|
@ -138,11 +149,7 @@
|
|||
onblur={() => finishEditing()}
|
||||
role="presentation"
|
||||
>
|
||||
<TaskText
|
||||
{taskId}
|
||||
{isUnselected}
|
||||
{context}
|
||||
/>
|
||||
<TaskText {taskId} {isUnselected} {context}/>
|
||||
</div>
|
||||
{#if !context.taskDraggingManager.isDragging
|
||||
&& !context.isReparentingOn()
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@
|
|||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
textEditEl.blur(); // Triggers handleBlur
|
||||
// TODO: body gets focused instead of viewport
|
||||
} else if (e.key === "Tab" && suggest !== null) {
|
||||
// another hack to select suggest on tab
|
||||
e.preventDefault();
|
||||
|
|
@ -179,7 +180,6 @@
|
|||
<div
|
||||
class="text-preview tasktext"
|
||||
class:unselect={isUnselected}
|
||||
tabindex="0"
|
||||
bind:this={textPreviewEl}
|
||||
onpointerup={handlePreviewClick}
|
||||
onmouseover={handlePreviewMouseOver}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
import {NoTaskId, RootTaskId} from "../NodePositionsCalculator";
|
||||
import {DraggingManager} from "../DraggingManager.svelte";
|
||||
import {parseNumber} from "../Constants";
|
||||
import SettingsPanel from "./SettingsPanel.svelte";
|
||||
|
||||
let {context}: {context: Context} = $props();
|
||||
|
||||
|
|
@ -63,7 +64,7 @@
|
|||
return;
|
||||
}
|
||||
context.save();
|
||||
context.incrementUpdateOnZoomCounter();// required for TaskText update
|
||||
context.incrementUpdateOnZoomCounter(); // required for TaskText update
|
||||
context.updateTaskPositions();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
|
@ -191,7 +192,6 @@
|
|||
|
||||
<div
|
||||
class="task-layer"
|
||||
tabindex="-1"
|
||||
role="presentation"
|
||||
>
|
||||
{#each context.versionedData.getTasks().filter(t => !context.isTaskHidden(t.taskId)) as task (task.taskId)}
|
||||
|
|
@ -236,8 +236,12 @@
|
|||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<SettingsPanel {context}/>
|
||||
|
||||
<style>
|
||||
:global(.taskmap-view-container) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.viewport {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
|
|||
|
|
@ -186,6 +186,16 @@ export class SetTaskNameAction implements Action {
|
|||
this.oldName = undefined;
|
||||
this.oldPath = undefined;
|
||||
}
|
||||
|
||||
shouldCombine(newAction: SetTaskNameAction) {
|
||||
if (this.newPath !== newAction.newPath) {
|
||||
return false;
|
||||
}
|
||||
// If both actions add one symbol or both remove 1 symbol then combine into one
|
||||
const deltaLeft = this.newName.length - this.oldName!.length;
|
||||
const deltaRight = newAction.newName.length - this.newName.length;
|
||||
return deltaLeft === deltaRight;
|
||||
}
|
||||
}
|
||||
|
||||
export class SetTaskPriorityAction implements Action {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
import type { Action } from "./Action";
|
||||
|
||||
export class HistoryManager {
|
||||
private undoStack: Action[] = [];
|
||||
private redoStack: Action[] = [];
|
||||
private undoStack: Action[] = $state([]);
|
||||
private redoStack: Action[] = $state([]);
|
||||
|
||||
execute(action: Action, data: ProjectData): void {
|
||||
action.do(data);
|
||||
|
|
@ -36,4 +36,8 @@ export class HistoryManager {
|
|||
canRedo(): boolean {
|
||||
return this.redoStack.length > 0;
|
||||
}
|
||||
|
||||
lastAction() {
|
||||
return this.undoStack.last();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { HistoryManager } from "./HistoryManager";
|
||||
import { HistoryManager } from "./HistoryManager.svelte";
|
||||
import { type BlockerPair, StatusCode, type TaskId } from "../types";
|
||||
import { ProjectData } from "./ProjectData.svelte";
|
||||
import {
|
||||
|
|
@ -35,6 +35,14 @@ export class VersionedData {
|
|||
}
|
||||
};
|
||||
|
||||
public canUndo = () => {
|
||||
return this.history.canUndo();
|
||||
};
|
||||
|
||||
public canRedo = () => {
|
||||
return this.history.canRedo();
|
||||
};
|
||||
|
||||
public addBlockerPair = (blockerPair: BlockerPair) => {
|
||||
this.history.execute(new AddBlockerPairAction(blockerPair), this.data);
|
||||
};
|
||||
|
|
@ -93,10 +101,20 @@ export class VersionedData {
|
|||
value: string,
|
||||
path: string | undefined,
|
||||
) => {
|
||||
this.history.execute(
|
||||
new SetTaskNameAction(taskId, value, path),
|
||||
this.data,
|
||||
);
|
||||
const lastAction = this.history.lastAction();
|
||||
const newAction = new SetTaskNameAction(taskId, value, path);
|
||||
if (
|
||||
lastAction instanceof SetTaskNameAction &&
|
||||
lastAction.shouldCombine(newAction)
|
||||
) {
|
||||
this.history.undo(this.data);
|
||||
this.history.execute(
|
||||
new SetTaskNameAction(taskId, value, path),
|
||||
this.data,
|
||||
);
|
||||
} else {
|
||||
this.history.execute(newAction, this.data);
|
||||
}
|
||||
};
|
||||
|
||||
public changeParent = (taskId: TaskId, newParentId: TaskId) => {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,13 @@ export enum IconCode {
|
|||
STATUS_DONE,
|
||||
}
|
||||
|
||||
export enum SettingsIconCode {
|
||||
NONE,
|
||||
SETTINGS_MENU,
|
||||
SETTINGS_UNDO,
|
||||
SETTINGS_REDO,
|
||||
}
|
||||
|
||||
export enum StatusCode {
|
||||
DRAFT,
|
||||
READY,
|
||||
|
|
|
|||
Loading…
Reference in a new issue