Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion src/gui/GenericInputPrompt/GenericInputPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ import type { App } from "obsidian";
import { ButtonComponent, Modal, TextComponent } from "obsidian";
import { FileSuggester } from "../suggesters/fileSuggester";
import { TagSuggester } from "../suggesters/tagSuggester";
import { InputPromptDraftStore } from "../../utils/InputPromptDraftStore";

export default class GenericInputPrompt extends Modal {
public waitForClose: Promise<string>;

private resolvePromise: (input: string) => void;
private rejectPromise: (reason?: unknown) => void;
private didSubmit = false;
private didChange = false;
private inputComponent: TextComponent;
protected input: string;
private readonly placeholder: string;
private readonly initialValue: string;
private readonly draftKey: string;
private readonly draftStore = InputPromptDraftStore.getInstance();
private fileSuggester: FileSuggester;
private tagSuggester: TagSuggester;

Expand Down Expand Up @@ -58,6 +63,17 @@ export default class GenericInputPrompt extends Modal {
super(app);
this.placeholder = placeholder ?? "";
this.input = value ?? "";
this.draftKey = this.draftStore.makeKey({
kind: "single",
header: this.header,
placeholder: this.placeholder,
linkSourcePath: this.linkSourcePath,
});
const draft = this.draftStore.get(this.draftKey);
if (draft !== undefined) {
this.input = draft;
}
this.initialValue = this.input;

this.waitForClose = new Promise<string>((resolve, reject) => {
this.resolvePromise = resolve;
Expand Down Expand Up @@ -103,7 +119,7 @@ export default class GenericInputPrompt extends Modal {
textComponent
.setPlaceholder(placeholder ?? "")
.setValue(value ?? "")
.onChange((value) => (this.input = value))
.onChange((value) => this.onInputChanged(value))
.inputEl.addEventListener("keydown", this.submitEnterCallback);

return textComponent;
Expand Down Expand Up @@ -152,6 +168,7 @@ export default class GenericInputPrompt extends Modal {
};

private submit() {
this.input = this.inputComponent?.inputEl?.value ?? this.input;
this.didSubmit = true;

this.close();
Expand All @@ -166,6 +183,33 @@ export default class GenericInputPrompt extends Modal {
else this.resolvePromise(this.input);
}

protected onInputChanged(value: string) {
this.didChange = true;
this.input = value;
}

private syncInputFromEl() {
if (this.inputComponent?.inputEl) {
this.input = this.inputComponent.inputEl.value;
}
}

private persistDraft() {
if (this.didSubmit) {
this.draftStore.clear(this.draftKey);
return;
}

if (!this.didChange || this.input === this.initialValue) return;

if (!this.input.trim()) {
this.draftStore.clear(this.draftKey);
return;
}

this.draftStore.set(this.draftKey, this.input);
}

private removeInputListener() {
this.inputComponent.inputEl.removeEventListener(
"keydown",
Expand All @@ -182,6 +226,10 @@ export default class GenericInputPrompt extends Modal {

onClose() {
super.onClose();
if (!this.didSubmit) {
this.syncInputFromEl();
}
this.persistDraft();
this.resolveInput();
this.removeInputListener();
}
Expand Down
50 changes: 49 additions & 1 deletion src/gui/GenericWideInputPrompt/GenericWideInputPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ import type { App } from "obsidian";
import { ButtonComponent, Modal, TextAreaComponent } from "obsidian";
import { FileSuggester } from "../suggesters/fileSuggester";
import { TagSuggester } from "../suggesters/tagSuggester";
import { InputPromptDraftStore } from "../../utils/InputPromptDraftStore";

export default class GenericWideInputPrompt extends Modal {
public waitForClose: Promise<string>;

private resolvePromise: (input: string) => void;
private rejectPromise: (reason?: unknown) => void;
private didSubmit = false;
private didChange = false;
private inputComponent: TextAreaComponent;
private input: string;
private readonly placeholder: string;
private readonly initialValue: string;
private readonly draftKey: string;
private readonly draftStore = InputPromptDraftStore.getInstance();
private fileSuggester: FileSuggester;
private tagSuggester: TagSuggester;

Expand Down Expand Up @@ -58,6 +63,17 @@ export default class GenericWideInputPrompt extends Modal {
super(app);
this.placeholder = placeholder ?? "";
this.input = value ?? "";
this.draftKey = this.draftStore.makeKey({
kind: "multi",
header: this.header,
placeholder: this.placeholder,
linkSourcePath: this.linkSourcePath,
});
const draft = this.draftStore.get(this.draftKey);
if (draft !== undefined) {
this.input = draft;
}
this.initialValue = this.input;

this.waitForClose = new Promise<string>((resolve, reject) => {
this.resolvePromise = resolve;
Expand Down Expand Up @@ -101,7 +117,7 @@ export default class GenericWideInputPrompt extends Modal {
textComponent
.setPlaceholder(placeholder ?? "")
.setValue(value ?? "")
.onChange((value) => (this.input = value))
.onChange((value) => this.onInputChanged(value))
.inputEl.addEventListener("keydown", this.submitEnterCallback);

return textComponent;
Expand Down Expand Up @@ -155,6 +171,7 @@ export default class GenericWideInputPrompt extends Modal {

private submit() {
if (this.didSubmit) return;
this.input = this.inputComponent?.inputEl?.value ?? this.input;
this.didSubmit = true;
this.input = this.escapeBackslashes(this.input);

Expand All @@ -170,6 +187,33 @@ export default class GenericWideInputPrompt extends Modal {
else this.resolvePromise(this.input);
}

private onInputChanged(value: string) {
this.didChange = true;
this.input = value;
}

private syncInputFromEl() {
if (this.inputComponent?.inputEl) {
this.input = this.inputComponent.inputEl.value;
}
}

private persistDraft() {
if (this.didSubmit) {
this.draftStore.clear(this.draftKey);
return;
}

if (!this.didChange || this.input === this.initialValue) return;

if (!this.input.trim()) {
this.draftStore.clear(this.draftKey);
return;
}

this.draftStore.set(this.draftKey, this.input);
}

private removeInputListener() {
this.inputComponent.inputEl.removeEventListener(
"keydown",
Expand All @@ -186,6 +230,10 @@ export default class GenericWideInputPrompt extends Modal {

onClose() {
super.onClose();
if (!this.didSubmit) {
this.syncInputFromEl();
}
this.persistDraft();
this.resolveInput();
this.removeInputListener();
Comment thread
chhoumann marked this conversation as resolved.
}
Expand Down
4 changes: 2 additions & 2 deletions src/gui/VDateInputPrompt/VDateInputPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ export default class VDateInputPrompt extends GenericInputPrompt {
.setPlaceholder(placeholder ?? "")
.setValue(value ?? "")
.onChange((newValue) => {
this.onInputChanged(newValue);
this.currentInput = newValue;
this.input = newValue; // Keep parent's input in sync
this.updatePreviewDebounced();
})
.inputEl.addEventListener("keydown", this.submitEnterCallback);
Expand Down Expand Up @@ -168,4 +168,4 @@ export default class VDateInputPrompt extends GenericInputPrompt {

super.onClose();
}
}
}
73 changes: 73 additions & 0 deletions src/utils/InputPromptDraftStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
export type InputPromptDraftKind = "single" | "multi";

export interface InputPromptDraftKey {
kind: InputPromptDraftKind;
header: string;
placeholder?: string;
linkSourcePath?: string;
}

interface DraftEntry {
value: string;
timestamp: number;
}

export class InputPromptDraftStore {
private static instance: InputPromptDraftStore;
private drafts: Map<string, DraftEntry> = new Map();
private readonly MAX_ENTRIES = 100;

static getInstance(): InputPromptDraftStore {
if (!InputPromptDraftStore.instance) {
InputPromptDraftStore.instance = new InputPromptDraftStore();
}
return InputPromptDraftStore.instance;
}

private constructor() {
// Session-only store
}

makeKey(key: InputPromptDraftKey): string {
return JSON.stringify({
v: 1,
kind: key.kind,
header: key.header,
placeholder: key.placeholder ?? "",
linkSourcePath: key.linkSourcePath ?? "",
});
}

get(key: string): string | undefined {
return this.drafts.get(key)?.value;
}

set(key: string, value: string): void {
if (this.drafts.size >= this.MAX_ENTRIES && !this.drafts.has(key)) {
this.evictOldest(1);
}

this.drafts.set(key, {
value,
timestamp: Date.now(),
});
}

clear(key: string): void {
this.drafts.delete(key);
}

clearAll(): void {
this.drafts.clear();
}

private evictOldest(count: number): void {
const entries = Array.from(this.drafts.entries())
.sort(([, a], [, b]) => a.timestamp - b.timestamp)
.slice(0, count);

for (const [key] of entries) {
this.drafts.delete(key);
}
}
}