150 lines
5.4 KiB
TypeScript
150 lines
5.4 KiB
TypeScript
import { App, Modal, Setting } from "obsidian";
|
||
import { Pair, composeFrontmatter, docFrontmatterToPairs, parseNote } from "./frontmatter";
|
||
import { Strategy } from "./images";
|
||
import { JekyllPublishSettings } from "./settings";
|
||
import { deriveDate, deriveSlug } from "./slug";
|
||
|
||
export interface ModalResult {
|
||
frontmatterPairs: Pair[];
|
||
slug: string;
|
||
date?: string;
|
||
strategy: Strategy;
|
||
commitMessage: string;
|
||
}
|
||
|
||
export class PublishModal extends Modal {
|
||
private result: ModalResult;
|
||
private mergeDoc = true;
|
||
/** Editable rows = preset fields (seeded from settings) + custom additions. */
|
||
private editableRows: Pair[];
|
||
/** The note's own frontmatter, shown read-only when merge is on. */
|
||
private readonly docPairs: Pair[];
|
||
private fmEl!: HTMLElement;
|
||
|
||
constructor(
|
||
app: App,
|
||
settings: JekyllPublishSettings,
|
||
private noteText: string,
|
||
filename: string,
|
||
private onSubmit: (r: ModalResult) => void
|
||
) {
|
||
super(app);
|
||
const { frontmatter } = parseNote(noteText);
|
||
const title = typeof frontmatter.title === "string" ? frontmatter.title : "";
|
||
this.docPairs = docFrontmatterToPairs(frontmatter);
|
||
// Deep-copy the presets so editing/deleting rows here never mutates the
|
||
// saved settings.
|
||
this.editableRows = settings.presetFrontmatter.map((p) => ({ key: p.key, value: p.value }));
|
||
this.result = {
|
||
frontmatterPairs: [],
|
||
slug: deriveSlug({ title, filename }),
|
||
date: deriveDate({ frontmatterDate: frontmatter.date }),
|
||
strategy: settings.defaultImageStrategy,
|
||
commitMessage: settings.commitMessageTemplate.replace("{{title}}", title || filename),
|
||
};
|
||
}
|
||
|
||
private recompute() {
|
||
this.result.frontmatterPairs = composeFrontmatter(this.docPairs, this.editableRows, this.mergeDoc);
|
||
}
|
||
|
||
onOpen() {
|
||
this.recompute();
|
||
const { contentEl } = this;
|
||
contentEl.createEl("h2", { text: "Publish to Jekyll" });
|
||
|
||
new Setting(contentEl).setName("Slug").addText((t) =>
|
||
t.setValue(this.result.slug).onChange((v) => (this.result.slug = v))
|
||
);
|
||
new Setting(contentEl).setName("Date").addText((t) =>
|
||
t.setValue(this.result.date ?? "").onChange((v) => (this.result.date = v))
|
||
).setDesc("The date to prepend to the filename. Useful for blog posts.");
|
||
|
||
new Setting(contentEl).setName("Image strategy").addDropdown((d) =>
|
||
d.addOption("flat-slug", "Flat, renamed to slug")
|
||
.addOption("per-post-folder", "Per-post subfolder")
|
||
.setValue(this.result.strategy)
|
||
.onChange((v) => (this.result.strategy = v as Strategy))
|
||
);
|
||
new Setting(contentEl)
|
||
.setName("Merge document frontmatter")
|
||
.setDesc("Include the note's own frontmatter (shown read-only below).")
|
||
.addToggle((t) =>
|
||
t.setValue(this.mergeDoc).onChange((v) => {
|
||
this.mergeDoc = v;
|
||
this.renderFrontmatter();
|
||
})
|
||
);
|
||
|
||
contentEl.createEl("h3", { text: "Frontmatter" });
|
||
this.fmEl = contentEl.createDiv();
|
||
this.renderFrontmatter();
|
||
|
||
new Setting(contentEl).addButton((b) =>
|
||
b.setButtonText("Add property").onClick(() => {
|
||
this.editableRows = [...this.editableRows, { key: "", value: "" }];
|
||
this.renderFrontmatter();
|
||
})
|
||
);
|
||
new Setting(contentEl).setName("Commit message").addText((t) =>
|
||
t.setValue(this.result.commitMessage).onChange((v) => (this.result.commitMessage = v))
|
||
);
|
||
new Setting(contentEl).addButton((b) =>
|
||
b.setButtonText("Publish").setCta().onClick(() => {
|
||
this.recompute();
|
||
this.close();
|
||
this.onSubmit(this.result);
|
||
})
|
||
);
|
||
}
|
||
|
||
private renderFrontmatter() {
|
||
this.recompute();
|
||
this.fmEl.empty();
|
||
|
||
const editableKeys = new Set(
|
||
this.editableRows.filter((r) => r.key.trim() !== "").map((r) => r.key)
|
||
);
|
||
|
||
// Document fields (read-only) first — only those not overridden by an
|
||
// editable row, so the modal never shows a key twice.
|
||
if (this.mergeDoc) {
|
||
const docRows = this.docPairs.filter((p) => !editableKeys.has(p.key));
|
||
for (const p of docRows) {
|
||
const row = this.fmEl.createDiv({ cls: "jekyll-publish-row" });
|
||
const k = row.createEl("input", { cls: "jekyll-publish-doc", value: p.key });
|
||
const v = row.createEl("input", { cls: "jekyll-publish-doc", value: p.value });
|
||
k.disabled = true;
|
||
v.disabled = true;
|
||
k.title = "From the note's frontmatter (edit the note to change)";
|
||
v.title = k.title;
|
||
}
|
||
}
|
||
|
||
// Editable rows (presets + custom) — same style, all editable and deletable.
|
||
this.editableRows.forEach((row, i) => {
|
||
const div = this.fmEl.createDiv({ cls: "jekyll-publish-row" });
|
||
const k = div.createEl("input", { value: row.key, placeholder: "key" });
|
||
const v = div.createEl("input", { value: row.value, placeholder: "value" });
|
||
k.oninput = () => {
|
||
this.editableRows = this.editableRows.map((r, j) => (j === i ? { ...r, key: k.value } : r));
|
||
this.recompute();
|
||
};
|
||
v.oninput = () => {
|
||
this.editableRows = this.editableRows.map((r, j) => (j === i ? { ...r, value: v.value } : r));
|
||
this.recompute();
|
||
};
|
||
const del = div.createEl("button", { cls: "jekyll-publish-del", text: "×" });
|
||
del.setAttr("aria-label", "Delete property");
|
||
del.onclick = () => {
|
||
this.editableRows = this.editableRows.filter((_, j) => j !== i);
|
||
this.renderFrontmatter();
|
||
};
|
||
});
|
||
}
|
||
|
||
onClose() {
|
||
this.contentEl.empty();
|
||
}
|
||
}
|