Files
obsidian-jekyll-publish/src/PublishModal.ts
Claude fb5d11dd6a
All checks were successful
CI / test (push) Successful in 16s
feat: unified, editable frontmatter editor in publish modal
- Preset fields (from settings) are now seeded as editable/deletable rows in
  the modal alongside custom ones, using one consistent row style.
- The note's own merged frontmatter is shown as read-only (disabled) rows at
  the top of the list (only when 'Merge document frontmatter' is on, and only
  for keys not overridden by an editable row).
- Drops the separate read-only 'resolved' text preview; the rows are the
  WYSIWYG result.

Adds pure, unit-tested composeFrontmatter()/docFrontmatterToPairs() and removes
the superseded resolveFrontmatter(). Editable rows are authoritative: on a key
collision they override the merged document field. Presets are deep-copied so
editing rows never mutates saved settings.

Co-Authored-By: Claude
2026-06-19 03:21:04 +00:00

149 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, now: new 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))
);
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();
}
}