Tweaks & fixes
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
import { App, TFile } from 'obsidian';
|
||||
import { TFile } from 'obsidian';
|
||||
|
||||
export interface FileSwitcherBarOptions {
|
||||
app: App;
|
||||
app: unknown; // kept for interface compatibility, unused after modal removal
|
||||
currentFile: TFile;
|
||||
taskFiles: TFile[];
|
||||
inboxPath: string;
|
||||
@ -10,8 +10,8 @@ export interface FileSwitcherBarOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a header bar with a file switcher dropdown and a "New task file" button.
|
||||
* Returns the container element to be prepended into the view's root.
|
||||
* Renders a header bar with a file switcher dropdown and an inline
|
||||
* "New task file" affordance. Returns the container element.
|
||||
*/
|
||||
export function buildFileSwitcherBar(opts: FileSwitcherBarOptions): HTMLElement {
|
||||
const { currentFile, taskFiles, inboxPath, onSwitchFile, onCreateFile } = opts;
|
||||
@ -20,39 +20,70 @@ export function buildFileSwitcherBar(opts: FileSwitcherBarOptions): HTMLElement
|
||||
|
||||
// Left side: file selector
|
||||
const selectWrap = bar.createDiv({ cls: 'yaotp-switcher-select-wrap' });
|
||||
|
||||
const select = selectWrap.createEl('select', { cls: 'yaotp-switcher-select' });
|
||||
|
||||
// Always show Inbox first if it exists in the list
|
||||
const inbox = taskFiles.find((f) => f.path === inboxPath);
|
||||
const others = taskFiles.filter((f) => f.path !== inboxPath);
|
||||
const ordered = inbox ? [inbox, ...others] : others;
|
||||
|
||||
for (const file of ordered) {
|
||||
const opt = select.createEl('option', {
|
||||
text: file.basename,
|
||||
value: file.path,
|
||||
});
|
||||
if (file.path === currentFile.path) {
|
||||
opt.selected = true;
|
||||
}
|
||||
for (const file of (inbox ? [inbox, ...others] : others)) {
|
||||
const opt = select.createEl('option', { text: file.basename, value: file.path });
|
||||
if (file.path === currentFile.path) opt.selected = true;
|
||||
}
|
||||
|
||||
select.addEventListener('change', () => {
|
||||
const chosen = taskFiles.find((f) => f.path === select.value);
|
||||
if (chosen) onSwitchFile(chosen);
|
||||
});
|
||||
|
||||
// Right side: new file button
|
||||
// Right side: "+ New list" button / inline create input (mutually exclusive)
|
||||
const newBtn = bar.createEl('button', {
|
||||
text: '+ New list',
|
||||
cls: 'yaotp-switcher-new-btn',
|
||||
});
|
||||
newBtn.addEventListener('click', () => {
|
||||
const name = prompt('New task file name (without .md):');
|
||||
if (name && name.trim()) {
|
||||
onCreateFile(name.trim());
|
||||
|
||||
const createGroup = bar.createDiv({ cls: 'yaotp-switcher-create-group' });
|
||||
createGroup.style.display = 'none';
|
||||
|
||||
const nameInput = createGroup.createEl('input', {
|
||||
type: 'text',
|
||||
cls: 'yaotp-switcher-name-input',
|
||||
attr: { placeholder: 'File name (without .md)' },
|
||||
}) as HTMLInputElement;
|
||||
|
||||
const confirmBtn = createGroup.createEl('button', {
|
||||
text: 'Create',
|
||||
cls: 'mod-cta yaotp-switcher-confirm-btn',
|
||||
});
|
||||
const cancelBtn = createGroup.createEl('button', {
|
||||
text: '✕',
|
||||
cls: 'yaotp-switcher-cancel-btn',
|
||||
});
|
||||
|
||||
const showInput = () => {
|
||||
newBtn.style.display = 'none';
|
||||
createGroup.style.display = 'flex';
|
||||
nameInput.value = '';
|
||||
setTimeout(() => nameInput.focus(), 0);
|
||||
};
|
||||
|
||||
const hideInput = () => {
|
||||
createGroup.style.display = 'none';
|
||||
newBtn.style.display = '';
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
const name = nameInput.value.trim();
|
||||
if (name) {
|
||||
hideInput();
|
||||
onCreateFile(name);
|
||||
}
|
||||
};
|
||||
|
||||
newBtn.addEventListener('click', showInput);
|
||||
confirmBtn.addEventListener('click', submit);
|
||||
cancelBtn.addEventListener('click', hideInput);
|
||||
nameInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') submit();
|
||||
if (e.key === 'Escape') hideInput();
|
||||
});
|
||||
|
||||
return bar;
|
||||
|
||||
@ -9,6 +9,7 @@ export interface TaskEditorResult {
|
||||
}
|
||||
|
||||
type SaveCallback = (result: TaskEditorResult) => void;
|
||||
type DeleteCallback = () => void;
|
||||
|
||||
/**
|
||||
* Modal for editing a task's text and notes together, and optionally
|
||||
@ -19,6 +20,7 @@ export class TaskEditorModal extends Modal {
|
||||
private taskFiles: TFile[];
|
||||
private currentFile: TFile;
|
||||
private onSave: SaveCallback;
|
||||
private onDelete: DeleteCallback;
|
||||
private textarea: HTMLTextAreaElement | null = null;
|
||||
private selectedFile: TFile | null = null;
|
||||
private fileLabel: HTMLSpanElement | null = null;
|
||||
@ -28,13 +30,15 @@ export class TaskEditorModal extends Modal {
|
||||
task: Task,
|
||||
taskFiles: TFile[],
|
||||
currentFile: TFile,
|
||||
onSave: SaveCallback
|
||||
onSave: SaveCallback,
|
||||
onDelete: DeleteCallback
|
||||
) {
|
||||
super(app);
|
||||
this.task = task;
|
||||
this.taskFiles = taskFiles;
|
||||
this.currentFile = currentFile;
|
||||
this.onSave = onSave;
|
||||
this.onDelete = onDelete;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
@ -44,8 +48,11 @@ export class TaskEditorModal extends Modal {
|
||||
// Title
|
||||
contentEl.createEl('h2', { text: 'Edit task' });
|
||||
|
||||
// Textarea: first line = task text, rest = notes
|
||||
const initialValue = [this.task.text, ...this.task.notes].join('\n');
|
||||
// Textarea: first line = task text, blank line, then notes
|
||||
const initialValue =
|
||||
this.task.notes.length > 0
|
||||
? [this.task.text, '', ...this.task.notes].join('\n')
|
||||
: this.task.text;
|
||||
this.textarea = contentEl.createEl('textarea', {
|
||||
cls: 'yaotp-editor-textarea',
|
||||
attr: { rows: '8', placeholder: 'Task title\n\nNotes…' },
|
||||
@ -81,6 +88,18 @@ export class TaskEditorModal extends Modal {
|
||||
|
||||
// Buttons
|
||||
const btnRow = contentEl.createDiv({ cls: 'yaotp-editor-btn-row' });
|
||||
|
||||
const deleteBtn = btnRow.createEl('button', {
|
||||
text: 'Delete',
|
||||
cls: 'yaotp-editor-delete-btn',
|
||||
});
|
||||
deleteBtn.addEventListener('click', () => {
|
||||
new ConfirmModal(this.app, 'Delete this task? This cannot be undone.', () => {
|
||||
this.close();
|
||||
this.onDelete();
|
||||
}).open();
|
||||
});
|
||||
|
||||
const saveBtn = btnRow.createEl('button', {
|
||||
text: 'Save',
|
||||
cls: 'mod-cta yaotp-editor-save-btn',
|
||||
@ -100,7 +119,7 @@ export class TaskEditorModal extends Modal {
|
||||
const lines = this.textarea.value.split('\n');
|
||||
const text = lines[0] ?? '';
|
||||
const notes = lines.slice(1);
|
||||
// Trim leading blank lines from notes
|
||||
// Trim leading blank lines from notes (user may have removed or added the blank separator)
|
||||
while (notes.length > 0 && notes[0].trim() === '') notes.shift();
|
||||
this.onSave({
|
||||
text,
|
||||
@ -111,6 +130,37 @@ export class TaskEditorModal extends Modal {
|
||||
}
|
||||
}
|
||||
|
||||
class ConfirmModal extends Modal {
|
||||
private message: string;
|
||||
private onConfirm: () => void;
|
||||
|
||||
constructor(app: App, message: string, onConfirm: () => void) {
|
||||
super(app);
|
||||
this.message = message;
|
||||
this.onConfirm = onConfirm;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.createEl('p', { text: this.message });
|
||||
const row = contentEl.createDiv({ cls: 'yaotp-editor-btn-row' });
|
||||
const confirmBtn = row.createEl('button', {
|
||||
text: 'Delete',
|
||||
cls: 'mod-warning yaotp-editor-delete-btn',
|
||||
});
|
||||
confirmBtn.addEventListener('click', () => {
|
||||
this.onConfirm();
|
||||
this.close();
|
||||
});
|
||||
const cancelBtn = row.createEl('button', { text: 'Cancel' });
|
||||
cancelBtn.addEventListener('click', () => this.close());
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
class TaskFilePicker extends FuzzySuggestModal<TFile> {
|
||||
private files: TFile[];
|
||||
private onSelect: (file: TFile) => void;
|
||||
|
||||
@ -5,11 +5,13 @@ import { TaskListComponent } from './TaskListComponent';
|
||||
import { buildFileSwitcherBar } from './FileSwitcherBar';
|
||||
import { TaskEditorModal } from './TaskEditorModal';
|
||||
import { VIEW_ICON, VIEW_TYPE } from '../constants';
|
||||
import { allowMarkdownOnce } from '../file-intercept';
|
||||
import type { Task } from '../types';
|
||||
import type { YaotpSettings } from '../settings';
|
||||
|
||||
export class TaskFileView extends TextFileView {
|
||||
private tasks: Task[] = [];
|
||||
private fileHeader: string = '';
|
||||
private taskList: TaskListComponent | null = null;
|
||||
private rootEl: HTMLElement | null = null;
|
||||
private listEl: HTMLElement | null = null;
|
||||
@ -40,16 +42,19 @@ export class TaskFileView extends TextFileView {
|
||||
|
||||
// Called by Obsidian when the file content is loaded or changes externally.
|
||||
setViewData(data: string, _clear: boolean): void {
|
||||
this.tasks = parseTaskFile(data);
|
||||
const { header, tasks } = parseTaskFile(data);
|
||||
this.fileHeader = header;
|
||||
this.tasks = tasks;
|
||||
this.renderView();
|
||||
}
|
||||
|
||||
// Called by Obsidian when it needs to read back the file content.
|
||||
getViewData(): string {
|
||||
return serializeTaskFile(this.tasks);
|
||||
return serializeTaskFile({ header: this.fileHeader, tasks: this.tasks });
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.fileHeader = '';
|
||||
this.tasks = [];
|
||||
this.renderView();
|
||||
}
|
||||
@ -58,6 +63,24 @@ export class TaskFileView extends TextFileView {
|
||||
async onOpen(): Promise<void> {
|
||||
this.contentEl.addClass('yaotp-view');
|
||||
this.rootEl = this.contentEl;
|
||||
|
||||
this.addAction('pencil', 'Edit source', async () => {
|
||||
if (!this.file) return;
|
||||
const filePath = this.file.path;
|
||||
// First call: bypass the intercept to land in markdown (Obsidian may
|
||||
// open in Live Preview regardless of the mode we pass here).
|
||||
allowMarkdownOnce(this.leaf);
|
||||
await this.leaf.setViewState({
|
||||
type: 'markdown',
|
||||
state: { file: filePath },
|
||||
});
|
||||
// Second call: now the leaf is markdown + same file, so the intercept
|
||||
// treats this as a mode change and lets it through.
|
||||
await this.leaf.setViewState({
|
||||
type: 'markdown',
|
||||
state: { file: filePath, mode: 'source' },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async onClose(): Promise<void> {
|
||||
@ -100,7 +123,16 @@ export class TaskFileView extends TextFileView {
|
||||
|
||||
private async persistTasks(): Promise<void> {
|
||||
if (!this.file) return;
|
||||
await this.app.vault.modify(this.file, serializeTaskFile(this.tasks));
|
||||
await this.app.vault.modify(
|
||||
this.file,
|
||||
serializeTaskFile({ header: this.fileHeader, tasks: this.tasks })
|
||||
);
|
||||
}
|
||||
|
||||
private async addTask(text: string): Promise<void> {
|
||||
this.tasks.unshift({ text, notes: [], completed: false });
|
||||
await this.persistTasks();
|
||||
this.renderView();
|
||||
}
|
||||
|
||||
private async completeTask(index: number): Promise<void> {
|
||||
@ -142,18 +174,17 @@ export class TaskFileView extends TextFileView {
|
||||
await this.persistTasks();
|
||||
}
|
||||
|
||||
this.renderView();
|
||||
},
|
||||
async () => {
|
||||
this.tasks.splice(index, 1);
|
||||
await this.persistTasks();
|
||||
this.renderView();
|
||||
}
|
||||
);
|
||||
modal.open();
|
||||
}
|
||||
|
||||
private async addTask(text: string): Promise<void> {
|
||||
this.tasks.unshift({ text, notes: [], completed: false });
|
||||
await this.persistTasks();
|
||||
this.renderView();
|
||||
}
|
||||
|
||||
private reorderTask(oldIndex: number, newIndex: number): void {
|
||||
const moved = this.tasks.splice(oldIndex, 1)[0];
|
||||
this.tasks.splice(newIndex, 0, moved);
|
||||
@ -163,9 +194,9 @@ export class TaskFileView extends TextFileView {
|
||||
|
||||
private async appendTaskToFile(task: Task, file: TFile): Promise<void> {
|
||||
const content = await this.app.vault.read(file);
|
||||
const existing = parseTaskFile(content);
|
||||
existing.push(task);
|
||||
await this.app.vault.modify(file, serializeTaskFile(existing));
|
||||
const data = parseTaskFile(content);
|
||||
data.tasks.push(task);
|
||||
await this.app.vault.modify(file, serializeTaskFile(data));
|
||||
}
|
||||
|
||||
private openFile(file: TFile): void {
|
||||
@ -173,11 +204,8 @@ export class TaskFileView extends TextFileView {
|
||||
}
|
||||
|
||||
private async createAndOpenFile(name: string): Promise<void> {
|
||||
// Derive folder from inboxPath
|
||||
const inboxFolder = this.settings.inboxPath.includes('/')
|
||||
? this.settings.inboxPath.slice(0, this.settings.inboxPath.lastIndexOf('/'))
|
||||
: '';
|
||||
const path = inboxFolder ? `${inboxFolder}/${name}.md` : `${name}.md`;
|
||||
const folder = this.settings.taskFileFolder;
|
||||
const path = folder ? `${folder}/${name}.md` : `${name}.md`;
|
||||
|
||||
let file = this.app.vault.getAbstractFileByPath(path) as TFile | null;
|
||||
if (!file) {
|
||||
|
||||
Reference in New Issue
Block a user