feat: support full featured modal

This commit is contained in:
quorafind 2025-04-12 16:37:58 +08:00
parent c96c425bdf
commit bea3d11f88
8 changed files with 696 additions and 139 deletions

View file

@ -71,7 +71,8 @@ export class MarkdownRendererComponent extends Component {
constructor(
private app: App,
container: HTMLElement,
sourcePath: string = ""
sourcePath: string = "",
private hideMarks: boolean = true
) {
super();
this.container = container;
@ -131,6 +132,11 @@ export class MarkdownRendererComponent extends Component {
* Split markdown content into blocks based on double line breaks
*/
private splitIntoBlocks(markdown: string): string[] {
if (!this.hideMarks) {
return markdown
.split(/\n\s*\n/)
.filter((block) => block.trim().length > 0);
}
// Split on double newlines (paragraph breaks)
return clearAllMarks(markdown)
.split(/\n\s*\n/)

View file

@ -1,4 +1,12 @@
import { App, Modal, Setting, TFile, Notice } from "obsidian";
import {
App,
Modal,
Setting,
TFile,
Notice,
Platform,
MarkdownRenderer,
} from "obsidian";
import {
createEmbeddableMarkdownEditor,
EmbeddableMarkdownEditor,
@ -7,6 +15,17 @@ import TaskProgressBarPlugin from "../index";
import { saveCapture } from "../utils/fileUtils";
import { FileSuggest } from "../editor-ext/quickCapture";
import { t } from "../translations/helper";
import { MarkdownRendererComponent } from "./MarkdownRenderer";
interface TaskMetadata {
startDate?: Date;
dueDate?: Date;
scheduledDate?: Date;
priority?: number;
project?: string;
context?: string;
recurrence?: string;
}
export class QuickCaptureModal extends Modal {
plugin: TaskProgressBarPlugin;
@ -14,18 +33,41 @@ export class QuickCaptureModal extends Modal {
capturedContent: string = "";
tempTargetFilePath: string = "";
taskMetadata: TaskMetadata = {};
useFullFeaturedMode: boolean = false;
constructor(app: App, plugin: TaskProgressBarPlugin) {
previewContainerEl: HTMLElement | null = null;
markdownRenderer: MarkdownRendererComponent | null = null;
constructor(
app: App,
plugin: TaskProgressBarPlugin,
metadata?: TaskMetadata,
useFullFeaturedMode: boolean = false
) {
super(app);
this.plugin = plugin;
this.tempTargetFilePath = this.plugin.settings.quickCapture.targetFile;
if (metadata) {
this.taskMetadata = metadata;
}
this.useFullFeaturedMode = useFullFeaturedMode && !Platform.isMobile;
}
onOpen() {
const { contentEl } = this;
this.modalEl.toggleClass("quick-capture-modal", true);
if (this.useFullFeaturedMode) {
this.modalEl.toggleClass(["quick-capture-modal", "full"], true);
this.createFullFeaturedModal(contentEl);
} else {
this.createSimpleModal(contentEl);
}
}
createSimpleModal(contentEl: HTMLElement) {
this.titleEl.createDiv({
text: t("Capture to"),
});
@ -44,68 +86,7 @@ export class QuickCaptureModal extends Modal {
cls: "quick-capture-modal-editor",
});
// Create the markdown editor with our EmbeddableMarkdownEditor
setTimeout(() => {
this.markdownEditor = createEmbeddableMarkdownEditor(
this.app,
editorContainer,
{
placeholder: this.plugin.settings.quickCapture.placeholder,
onEnter: (editor, mod, shift) => {
if (mod) {
// Submit on Cmd/Ctrl+Enter
this.handleSubmit();
return true;
}
// Allow normal Enter key behavior
return false;
},
onEscape: (editor) => {
// Close the modal on Escape
this.close();
},
onSubmit: (editor) => {
this.handleSubmit();
},
onChange: (update) => {
// Handle changes if needed
this.capturedContent = this.markdownEditor?.value || "";
},
}
);
this.markdownEditor?.scope.register(
["Alt"],
"c",
(e: KeyboardEvent) => {
e.preventDefault();
if (!this.markdownEditor) return false;
if (this.markdownEditor.value.trim() === "") {
this.close();
return true;
} else {
this.handleSubmit();
}
return true;
}
);
this.markdownEditor?.scope.register(
["Alt"],
"x",
(e: KeyboardEvent) => {
e.preventDefault();
targetFileEl.focus();
return true;
}
);
// Focus the editor when it's created
this.markdownEditor?.editor?.focus();
}, 50);
this.setupMarkdownEditor(editorContainer, targetFileEl);
// Create button container
const buttonContainer = contentEl.createDiv({
@ -137,6 +118,289 @@ export class QuickCaptureModal extends Modal {
);
}
createFullFeaturedModal(contentEl: HTMLElement) {
// Create a layout container with two panels
const layoutContainer = contentEl.createDiv({
cls: "quick-capture-layout",
});
// Create left panel for configuration
const configPanel = layoutContainer.createDiv({
cls: "quick-capture-config-panel",
});
// Create right panel for editor
const editorPanel = layoutContainer.createDiv({
cls: "quick-capture-editor-panel",
});
// Target file selector
const targetFileContainer = configPanel.createDiv({
cls: "quick-capture-target-container",
});
targetFileContainer.createDiv({
text: t("Target File:"),
cls: "quick-capture-section-title",
});
const targetFileEl = targetFileContainer.createEl("div", {
cls: "quick-capture-target",
attr: {
contenteditable: "true",
spellcheck: "false",
},
text: this.tempTargetFilePath,
});
new FileSuggest(
this.app,
targetFileEl,
this.plugin.settings.quickCapture,
(file: TFile) => {
targetFileEl.textContent = file.path;
this.tempTargetFilePath = file.path;
this.markdownEditor?.editor?.focus();
}
);
// Task metadata configuration
configPanel.createDiv({
text: t("Task Properties"),
cls: "quick-capture-section-title",
});
// Start Date
new Setting(configPanel).setName(t("Start Date")).addText((text) => {
text.setPlaceholder("YYYY-MM-DD")
.setValue(
this.taskMetadata.startDate
? this.formatDate(this.taskMetadata.startDate)
: ""
)
.onChange((value) => {
if (value) {
this.taskMetadata.startDate = this.parseDate(value);
} else {
this.taskMetadata.startDate = undefined;
}
this.updatePreview();
});
text.inputEl.type = "date";
});
// Due Date
new Setting(configPanel).setName(t("Due Date")).addText((text) => {
text.setPlaceholder("YYYY-MM-DD")
.setValue(
this.taskMetadata.dueDate
? this.formatDate(this.taskMetadata.dueDate)
: ""
)
.onChange((value) => {
if (value) {
this.taskMetadata.dueDate = this.parseDate(value);
} else {
this.taskMetadata.dueDate = undefined;
}
this.updatePreview();
});
text.inputEl.type = "date";
});
// Scheduled Date
new Setting(configPanel)
.setName(t("Scheduled Date"))
.addText((text) => {
text.setPlaceholder("YYYY-MM-DD")
.setValue(
this.taskMetadata.scheduledDate
? this.formatDate(this.taskMetadata.scheduledDate)
: ""
)
.onChange((value) => {
if (value) {
this.taskMetadata.scheduledDate =
this.parseDate(value);
} else {
this.taskMetadata.scheduledDate = undefined;
}
this.updatePreview();
});
text.inputEl.type = "date";
});
// Priority
new Setting(configPanel)
.setName(t("Priority"))
.addDropdown((dropdown) => {
dropdown
.addOption("", t("None"))
.addOption("5", t("Highest"))
.addOption("4", t("High"))
.addOption("3", t("Medium"))
.addOption("2", t("Low"))
.addOption("1", t("Lowest"))
.setValue(this.taskMetadata.priority?.toString() || "")
.onChange((value) => {
this.taskMetadata.priority = value
? parseInt(value)
: undefined;
this.updatePreview();
});
});
// Project
new Setting(configPanel).setName(t("Project")).addText((text) => {
text.setPlaceholder(t("Project name"))
.setValue(this.taskMetadata.project || "")
.onChange((value) => {
this.taskMetadata.project = value || undefined;
this.updatePreview();
});
});
// Context
new Setting(configPanel).setName(t("Context")).addText((text) => {
text.setPlaceholder(t("Context"))
.setValue(this.taskMetadata.context || "")
.onChange((value) => {
this.taskMetadata.context = value || undefined;
this.updatePreview();
});
});
// Recurrence
new Setting(configPanel).setName(t("Recurrence")).addText((text) => {
text.setPlaceholder(t("e.g., every day, every week"))
.setValue(this.taskMetadata.recurrence || "")
.onChange((value) => {
this.taskMetadata.recurrence = value || undefined;
this.updatePreview();
});
});
// Create editor container in the right panel
const editorContainer = editorPanel.createDiv({
cls: "quick-capture-modal-editor",
});
editorPanel.createDiv({
text: t("Task Content"),
cls: "quick-capture-section-title",
});
this.previewContainerEl = editorPanel.createDiv({
cls: "preview-container",
});
this.markdownRenderer = new MarkdownRendererComponent(
this.app,
this.previewContainerEl,
"",
false
);
this.setupMarkdownEditor(editorContainer);
// Create button container
const buttonContainer = contentEl.createDiv({
cls: "quick-capture-modal-buttons",
});
// Create the buttons
const submitButton = buttonContainer.createEl("button", {
text: t("Capture"),
cls: "mod-cta",
});
submitButton.addEventListener("click", () => this.handleSubmit());
const cancelButton = buttonContainer.createEl("button", {
text: t("Cancel"),
});
cancelButton.addEventListener("click", () => this.close());
}
updatePreview() {
if (this.previewContainerEl) {
this.markdownRenderer?.render(
this.processContentWithMetadata(this.capturedContent)
);
}
}
setupMarkdownEditor(container: HTMLElement, targetFileEl?: HTMLElement) {
// Create the markdown editor with our EmbeddableMarkdownEditor
setTimeout(() => {
this.markdownEditor = createEmbeddableMarkdownEditor(
this.app,
container,
{
placeholder: this.plugin.settings.quickCapture.placeholder,
onEnter: (editor, mod, shift) => {
if (mod) {
// Submit on Cmd/Ctrl+Enter
this.handleSubmit();
return true;
}
// Allow normal Enter key behavior
return false;
},
onEscape: (editor) => {
// Close the modal on Escape
this.close();
},
onSubmit: (editor) => {
this.handleSubmit();
},
onChange: (update) => {
// Handle changes if needed
this.capturedContent = this.markdownEditor?.value || "";
if (this.updatePreview) {
this.updatePreview();
}
},
}
);
this.markdownEditor?.scope.register(
["Alt"],
"c",
(e: KeyboardEvent) => {
e.preventDefault();
if (!this.markdownEditor) return false;
if (this.markdownEditor.value.trim() === "") {
this.close();
return true;
} else {
this.handleSubmit();
}
return true;
}
);
if (targetFileEl) {
this.markdownEditor?.scope.register(
["Alt"],
"x",
(e: KeyboardEvent) => {
e.preventDefault();
targetFileEl.focus();
return true;
}
);
}
// Focus the editor when it's created
this.markdownEditor?.editor?.focus();
}, 50);
}
async handleSubmit() {
const content =
this.capturedContent.trim() ||
@ -149,7 +413,8 @@ export class QuickCaptureModal extends Modal {
}
try {
await saveCapture(this.app, content, {
const processedContent = this.processContentWithMetadata(content);
await saveCapture(this.app, processedContent, {
...this.plugin.settings.quickCapture,
targetFile: this.tempTargetFilePath,
});
@ -160,6 +425,137 @@ export class QuickCaptureModal extends Modal {
}
}
processContentWithMetadata(content: string): string {
// Split content into lines
const lines = content.split("\n");
const processedLines: string[] = [];
const indentationRegex = /^(\s+)/;
for (const line of lines) {
if (!line.trim()) {
processedLines.push(line);
continue;
}
// Check for indentation to identify sub-tasks
const indentMatch = line.match(indentationRegex);
const isSubTask = indentMatch && indentMatch[1].length > 0;
// Check if line is already a task or a list item
const isTaskOrList = line
.trim()
.match(/^(-|\d+\.|\*|\+)(\s+\[[-x ]\])?/);
if (isSubTask) {
// Don't add metadata to sub-tasks
processedLines.push(line);
} else if (isTaskOrList) {
// If it's a task, add metadata
if (line.trim().match(/^(-|\d+\.|\*|\+)\s+\[[-x ]\]/)) {
processedLines.push(this.addMetadataToTask(line));
} else {
// If it's a list item but not a task, convert to task and add metadata
const listPrefix = line
.trim()
.match(/^(-|\d+\.|\*|\+)/)?.[0];
const restOfLine = line
.trim()
.substring(listPrefix?.length || 0)
.trim();
processedLines.push(
this.addMetadataToTask(
`${listPrefix} [ ] ${restOfLine}`
)
);
}
} else {
// Not a list item or task, convert to task and add metadata
processedLines.push(this.addMetadataToTask(`- [ ] ${line}`));
}
}
return processedLines.join("\n");
}
addMetadataToTask(taskLine: string): string {
const metadata = this.generateMetadataString();
if (!metadata) return taskLine;
return `${taskLine} ${metadata}`.trim();
}
generateMetadataString(): string {
const metadata: string[] = [];
// Format dates to strings in YYYY-MM-DD format
if (this.taskMetadata.startDate) {
metadata.push(`🛫 ${this.formatDate(this.taskMetadata.startDate)}`);
}
if (this.taskMetadata.dueDate) {
metadata.push(`📅 ${this.formatDate(this.taskMetadata.dueDate)}`);
}
if (this.taskMetadata.scheduledDate) {
metadata.push(
`${this.formatDate(this.taskMetadata.scheduledDate)}`
);
}
// Add priority if set
if (this.taskMetadata.priority) {
let priorityMarker = "";
switch (this.taskMetadata.priority) {
case 5:
priorityMarker = "🔺";
break; // Highest
case 4:
priorityMarker = "⏫";
break; // High
case 3:
priorityMarker = "🔼";
break; // Medium
case 2:
priorityMarker = "🔽";
break; // Low
case 1:
priorityMarker = "⏬";
break; // Lowest
}
if (priorityMarker) {
metadata.push(priorityMarker);
}
}
// Add project if set
if (this.taskMetadata.project) {
metadata.push(`#project/${this.taskMetadata.project}`);
}
// Add context if set
if (this.taskMetadata.context) {
metadata.push(`@${this.taskMetadata.context}`);
}
// Add recurrence if set
if (this.taskMetadata.recurrence) {
metadata.push(`🔁 ${this.taskMetadata.recurrence}`);
}
return metadata.join(" ");
}
formatDate(date: Date): string {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(
2,
"0"
)}-${String(date.getDate()).padStart(2, "0")}`;
}
parseDate(dateString: string): Date {
return new Date(dateString);
}
onClose() {
const { contentEl } = this;
@ -171,5 +567,10 @@ export class QuickCaptureModal extends Modal {
// Clear the content
contentEl.empty();
if (this.markdownRenderer) {
this.markdownRenderer.unload();
this.markdownRenderer = null;
}
}
}

View file

@ -301,6 +301,83 @@ export default class TaskProgressBarPlugin extends Plugin {
this.activateTaskView();
},
});
// Add a global command for quick capture from anywhere
this.addCommand({
id: "global-quick-capture",
name: t("Quick capture (Global)"),
callback: () => {
// Get the active leaf if available
const activeLeaf =
this.app.workspace.getActiveViewOfType(MarkdownView);
if (activeLeaf && activeLeaf.editor) {
// If we're in a markdown editor, use the editor command
const editorView = activeLeaf.editor.cm as EditorView;
// Import necessary functions dynamically to avoid circular dependencies
try {
// Show the quick capture panel
editorView.dispatch({
effects: toggleQuickCapture.of(true),
});
} catch (e) {
// No quick capture state found, try to add the extension first
// This is a simplified approach and might not work in all cases
this.registerEditorExtension([
quickCaptureExtension(this.app, this),
]);
// Try again after registering the extension
setTimeout(() => {
try {
editorView.dispatch({
effects: toggleQuickCapture.of(true),
});
} catch (e) {
new Notice(
t(
"Could not open quick capture panel in the current editor"
)
);
}
}, 100);
}
} else {
// No active markdown view, show a floating capture window instead
// Create a simple modal with capture functionality
new QuickCaptureModal(this.app, this).open();
}
},
});
// Add command for full-featured task capture
this.addCommand({
id: "full-featured-task-capture",
name: t("Task capture with metadata"),
callback: () => {
// Create a modal with full task metadata options
new QuickCaptureModal(this.app, this, {}, true).open();
},
});
// Add command for toggling task filter
this.addCommand({
id: "toggle-task-filter",
name: t("Toggle task filter panel"),
editorCallback: (editor, ctx) => {
const view = editor.cm as EditorView;
if (view) {
view.dispatch({
effects: toggleTaskFilter.of(
!view.state.field(taskFilterState)
),
});
}
},
});
}
registerCommands() {
@ -473,73 +550,6 @@ export default class TaskProgressBarPlugin extends Plugin {
}
},
});
// Add a global command for quick capture from anywhere
this.addCommand({
id: "global-quick-capture",
name: t("Quick capture (Global)"),
callback: () => {
// Get the active leaf if available
const activeLeaf =
this.app.workspace.getActiveViewOfType(MarkdownView);
if (activeLeaf && activeLeaf.editor) {
// If we're in a markdown editor, use the editor command
const editorView = activeLeaf.editor.cm as EditorView;
// Import necessary functions dynamically to avoid circular dependencies
try {
// Show the quick capture panel
editorView.dispatch({
effects: toggleQuickCapture.of(true),
});
} catch (e) {
// No quick capture state found, try to add the extension first
// This is a simplified approach and might not work in all cases
this.registerEditorExtension([
quickCaptureExtension(this.app, this),
]);
// Try again after registering the extension
setTimeout(() => {
try {
editorView.dispatch({
effects: toggleQuickCapture.of(true),
});
} catch (e) {
new Notice(
t(
"Could not open quick capture panel in the current editor"
)
);
}
}, 100);
}
} else {
// No active markdown view, show a floating capture window instead
// Create a simple modal with capture functionality
new QuickCaptureModal(this.app, this).open();
}
},
});
// Add command for toggling task filter
this.addCommand({
id: "toggle-task-filter",
name: t("Toggle task filter panel"),
editorCallback: (editor, ctx) => {
const view = editor.cm as EditorView;
if (view) {
view.dispatch({
effects: toggleTaskFilter.of(
!view.state.field(taskFilterState)
),
});
}
},
});
}
registerEditorExt() {

View file

@ -102,7 +102,9 @@ export class TaskView extends ItemView {
.onClick(() => {
const modal = new QuickCaptureModal(
this.plugin.app,
this.plugin
this.plugin,
{},
true
);
modal.open();
});
@ -215,7 +217,12 @@ export class TaskView extends ItemView {
this.detailsToggleBtn.toggleClass("panel-toggle-btn", true);
this.addAction("check-square", "capture", () => {
const modal = new QuickCaptureModal(this.plugin.app, this.plugin);
const modal = new QuickCaptureModal(
this.plugin.app,
this.plugin,
{},
true
);
modal.open();
});
}

View file

@ -100,3 +100,72 @@
justify-content: flex-end;
gap: 10px;
}
/* Full-featured modal styles */
.quick-capture-modal.full {
width: 80vw;
max-width: 900px;
}
.quick-capture-layout {
display: flex;
height: 100%;
gap: 16px;
margin-bottom: 16px;
}
.quick-capture-config-panel {
flex: 1;
border-right: 1px solid var(--background-modifier-border);
padding-right: 16px;
overflow-y: auto;
max-width: 40%;
}
.quick-capture-editor-panel {
flex: 1.5;
display: flex;
flex-direction: column;
}
.quick-capture-section-title {
font-weight: bold;
margin-bottom: 8px;
font-size: var(--font-ui-medium);
color: var(--text-normal);
}
.quick-capture-target-container {
margin-bottom: 16px;
}
.quick-capture-modal.full .quick-capture-modal-editor {
min-height: 200px;
flex: 1;
overflow-y: auto;
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-s);
padding: 8px;
margin-top: 8px;
}
/* Mobile optimization */
@media (max-width: 768px) {
.quick-capture-modal.full {
width: 95vw;
}
.quick-capture-layout {
flex-direction: column;
}
.quick-capture-config-panel {
max-width: 100%;
border-right: none;
border-bottom: 1px solid var(--background-modifier-border);
padding-right: 0;
padding-bottom: 16px;
margin-bottom: 16px;
max-height: 40%;
}
}

View file

@ -62,6 +62,10 @@
flex: 1;
overflow-y: auto;
padding: var(--size-4-2);
display: flex;
flex-direction: column;
gap: var(--size-2-1);
}
.tag-list-item {

View file

@ -86,7 +86,7 @@
background-color: var(--background-modifier-active-hover);
padding: var(--size-2-1) var(--size-2-3);
border-radius: var(--radius-s);
opacity: 0.5;
opacity: 0.8;
}
.task-item:hover .task-date {

File diff suppressed because one or more lines are too long