1626 lines
65 KiB
JavaScript
1626 lines
65 KiB
JavaScript
/*
|
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
|
if you want to view the source, please visit the github repository of this plugin
|
|
*/
|
|
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __export = (target, all) => {
|
|
for (var name in all)
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
};
|
|
var __copyProps = (to, from, except, desc) => {
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
for (let key of __getOwnPropNames(from))
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
}
|
|
return to;
|
|
};
|
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
|
|
// src/main.js
|
|
var main_exports = {};
|
|
__export(main_exports, {
|
|
default: () => main_default
|
|
});
|
|
module.exports = __toCommonJS(main_exports);
|
|
var import_obsidian4 = require("obsidian");
|
|
|
|
// src/modals.js
|
|
var import_obsidian = require("obsidian");
|
|
var ReleaseNotesModal = class extends import_obsidian.Modal {
|
|
constructor(app, plugin, version, releaseNotes2) {
|
|
super(app);
|
|
this.plugin = plugin;
|
|
this.version = version;
|
|
this.releaseNotes = releaseNotes2;
|
|
}
|
|
async onOpen() {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
contentEl.createEl("h2", { text: `Welcome to \u{1F9B6} Rich Foot v${this.version}` });
|
|
contentEl.createEl("p", {
|
|
text: "After each update you'll be prompted with the release notes. You can disable this in the plugin settings.",
|
|
cls: "release-notes-instructions"
|
|
});
|
|
const promotionalLinks = contentEl.createEl("div", { cls: "release-notes-promotional-links" });
|
|
const equilllabsLink = promotionalLinks.createEl("a", {
|
|
href: "https://www.equilllabs.com",
|
|
target: "_blank"
|
|
});
|
|
equilllabsLink.createEl("img", {
|
|
attr: {
|
|
height: "36",
|
|
style: "border:0px;height:36px;",
|
|
src: "https://raw.githubusercontent.com/jparkerweb/pixel-banner/refs/heads/main/img/equilllabs.png?raw=true",
|
|
border: "0",
|
|
alt: "eQuill-Labs"
|
|
}
|
|
});
|
|
const discordLink = promotionalLinks.createEl("a", {
|
|
href: "https://discord.gg/sp8AQQhMJ7",
|
|
target: "_blank"
|
|
});
|
|
discordLink.createEl("img", {
|
|
attr: {
|
|
height: "36",
|
|
style: "border:0px;height:36px;",
|
|
src: "https://raw.githubusercontent.com/jparkerweb/pixel-banner/refs/heads/main/img/discord.png?raw=true",
|
|
border: "0",
|
|
alt: "Discord"
|
|
}
|
|
});
|
|
const kofiLink = promotionalLinks.createEl("a", {
|
|
href: "https://ko-fi.com/Z8Z212UMBI",
|
|
target: "_blank"
|
|
});
|
|
kofiLink.createEl("img", {
|
|
attr: {
|
|
height: "36",
|
|
style: "border:0px;height:36px;",
|
|
src: "https://raw.githubusercontent.com/jparkerweb/pixel-banner/refs/heads/main/img/support.png?raw=true",
|
|
border: "0",
|
|
alt: "Buy Me a Coffee at ko-fi.com"
|
|
}
|
|
});
|
|
const notesContainer = contentEl.createDiv("release-notes-container");
|
|
await import_obsidian.MarkdownRenderer.render(
|
|
this.app,
|
|
this.releaseNotes,
|
|
notesContainer,
|
|
"",
|
|
this
|
|
);
|
|
contentEl.createEl("div", { cls: "release-notes-spacer" });
|
|
new import_obsidian.Setting(contentEl).addButton((btn) => btn.setButtonText("Close").onClick(() => this.close()));
|
|
}
|
|
onClose() {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
}
|
|
};
|
|
|
|
// virtual-module:virtual:release-notes
|
|
var releaseNotes = "<h2>\u{1F680} New Feature</h2>\n<h3>v1.13.0</h3>\n<h4>\u2728 Adjustable Footer Width</h4>\n<ul>\n<li>Added a <strong>Footer Width</strong> setting to control how wide the footer is</li>\n<li>Choose <strong>Readable line length</strong> to lock the footer to Obsidian's readable line width, or <strong>Custom width</strong> to set a maximum width in pixels</li>\n<li>The footer now always caps at the width of the note, so it no longer causes horizontal scrolling on narrow screens (notably on mobile)</li>\n<li>Configure via Settings \u2192 Style Settings \u2192 <strong>Footer Width</strong></li>\n</ul>\n<h3>v1.12.0</h3>\n<h4>\u2728 Limit Links Shown</h4>\n<ul>\n<li>Added an option to limit the number of backlinks/outlinks shown in the footer</li>\n<li>Surplus links are hidden behind a <strong>Show More (X)</strong> button that expands them on click (and collapses again with <strong>Show Less</strong>)</li>\n<li>Helpful for notes with a large number of links that previously caused flickering and scroll-reset issues</li>\n<li>Configure via Settings \u2192 <strong>Limit Links Shown</strong> (toggle) and <strong>Links Limit</strong> (default 10)</li>\n</ul>\n<h2>\u{1F680} Code Refactoring</h2>\n<h3>v1.11.1</h3>\n<h4>\u{1F4E6} Update</h4>\n<ul>\n<li>Optimized CSS for rending <code>rich foot</code> element at the bottom of notes</li>\n</ul>\n<h3>v1.11.0</h3>\n<p>This release represents a complete architectural overhaul of the Rich Foot plugin, implementing modern best practices and significant performance improvements.</p>\n<h4>\u2728 Performance Enhancements</h4>\n<ul>\n<li>Implemented <code>requestAnimationFrame</code> for all visual updates to eliminate page jitter</li>\n<li>Optimized MutationObserver usage with RAF-debounced callbacks</li>\n<li>Added CSS <code>contain</code> and <code>will-change</code> properties for better rendering performance</li>\n<li>Reduced layout thrashing through batched DOM operations</li>\n<li>Smart update detection to skip unnecessary re-renders</li>\n</ul>\n<h4>\u{1F3D7}\uFE0F Architecture Improvements</h4>\n<ul>\n<li>Complete code reorganization with separation of concerns</li>\n<li>New modular structure:<ul>\n<li><strong>RichFootDataManager</strong>: Handles all data fetching and parsing</li>\n<li><strong>RichFootRenderer</strong>: Pure rendering logic with optimal DOM operations</li>\n<li><strong>RichFootViewManager</strong>: View lifecycle and observer management</li>\n</ul>\n</li>\n<li>Eliminated code duplication across date parsing and link creation</li>\n<li>Cleaner, more maintainable codebase with JSDoc documentation</li>\n</ul>\n<h4>\u{1F9F9} Cleanup & Stability</h4>\n<ul>\n<li>Proper resource cleanup using Obsidian's <code>registerEvent</code> exclusively</li>\n<li>Improved observer management with automatic disconnection</li>\n<li>Data attributes for better element tracking</li>\n<li>No more manual event cleanup in <code>onunload</code> (automatic via registration)</li>\n<li>Fixed potential memory leaks from orphaned observers</li>\n</ul>\n<h4>\u{1F527} Compatibility</h4>\n<ul>\n<li>Enhanced native hover preview integration (works in all modes)</li>\n<li>Respects view lifecycle changes more accurately</li>\n</ul>\n<h4>\u{1F4CA} Code Quality</h4>\n<ul>\n<li>Comprehensive error handling with try-catch blocks</li>\n<li>Modern ES6+ patterns throughout</li>\n<li>Clear naming conventions and documentation</li>\n</ul>\n<h4>\u{1F3A8} CSS Optimizations</h4>\n<ul>\n<li>Added GPU-accelerated transforms for animations</li>\n<li>Optimized transitions with <code>will-change</code> hints</li>\n<li>Layout containment for better performance</li>\n<li>Smoother fade-in animations</li>\n</ul>\n<p>This update maintains 100% backwards compatibility with all existing settings and configurations while providing a more robust, performant foundation for future enhancements.</p>\n";
|
|
|
|
// src/settings.js
|
|
var import_obsidian2 = require("obsidian");
|
|
|
|
// src/utils.js
|
|
function rgbToHex(color) {
|
|
if (color.startsWith("hsl")) {
|
|
const temp = document.createElement("div");
|
|
temp.style.color = color;
|
|
document.body.appendChild(temp);
|
|
color = getComputedStyle(temp).color;
|
|
document.body.removeChild(temp);
|
|
}
|
|
const rgb = color.match(/\d+/g);
|
|
if (!rgb || rgb.length < 3) return "#000000";
|
|
const [r, g, b] = rgb.slice(0, 3).map((x) => {
|
|
const val = Math.min(255, Math.max(0, Math.round(parseFloat(x))));
|
|
return val.toString(16).padStart(2, "0");
|
|
});
|
|
return `#${r}${g}${b}`;
|
|
}
|
|
function resolveCssVar(cssValue, property = "color") {
|
|
const temp = document.createElement("div");
|
|
temp.style[property] = cssValue;
|
|
document.body.appendChild(temp);
|
|
const computed = getComputedStyle(temp)[property];
|
|
document.body.removeChild(temp);
|
|
return rgbToHex(computed);
|
|
}
|
|
|
|
// src/settings.js
|
|
var DEFAULT_SETTINGS = {
|
|
borderWidth: 1,
|
|
borderStyle: "dashed",
|
|
borderOpacity: 1,
|
|
borderRadius: 15,
|
|
datesOpacity: 1,
|
|
linksOpacity: 1,
|
|
showReleaseNotes: true,
|
|
excludedFolders: [],
|
|
dateColor: "var(--text-accent)",
|
|
borderColor: "var(--text-accent)",
|
|
linkColor: "var(--link-color)",
|
|
linkBackgroundColor: "var(--tag-background)",
|
|
linkBorderColor: "rgba(255, 255, 255, 0.204)",
|
|
customCreatedDateProp: "",
|
|
customModifiedDateProp: "",
|
|
dateDisplayFormat: "mmmm dd, yyyy",
|
|
showBacklinks: true,
|
|
showOutlinks: true,
|
|
showDates: true,
|
|
combineLinks: false,
|
|
limitLinks: false,
|
|
linksLimit: 10,
|
|
footerWidth: "default",
|
|
footerMaxWidth: 700,
|
|
updateDelay: 3e3,
|
|
excludedParentSelectors: [],
|
|
frontmatterExclusionField: ""
|
|
};
|
|
var RichFootSettingTab = class extends import_obsidian2.PluginSettingTab {
|
|
constructor(app, plugin) {
|
|
super(app, plugin);
|
|
this.plugin = plugin;
|
|
this.excludedParentSelectors = [];
|
|
this.frontmatterExclusionField = "";
|
|
}
|
|
display() {
|
|
var _a, _b;
|
|
const { containerEl } = this;
|
|
containerEl.empty();
|
|
containerEl.addClass("rich-foot-settings");
|
|
containerEl.createEl("div", { cls: "rich-foot-info", text: "\u{1F9B6} Rich Foot adds a footer to your notes with useful information such as backlinks, creation date, and last modified date. Use the settings below to customize the appearance." });
|
|
containerEl.createEl("h3", { text: "Visibility Settings" });
|
|
new import_obsidian2.Setting(containerEl).setName("Show Backlinks").setDesc("Show backlinks in the footer").addToggle((toggle) => toggle.setValue(this.plugin.settings.showBacklinks).onChange(async (value) => {
|
|
this.plugin.settings.showBacklinks = value;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
}));
|
|
new import_obsidian2.Setting(containerEl).setName("Show Outlinks").setDesc("Show outgoing links in the footer").addToggle((toggle) => toggle.setValue(this.plugin.settings.showOutlinks).onChange(async (value) => {
|
|
this.plugin.settings.showOutlinks = value;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
}));
|
|
new import_obsidian2.Setting(containerEl).setName("Show Combine Links").setDesc("Show backlinks and outlinks in a single combined section (overrides show backlinks and show outlinks settings)").addToggle((toggle) => toggle.setValue(this.plugin.settings.combineLinks).onChange(async (value) => {
|
|
this.plugin.settings.combineLinks = value;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
}));
|
|
new import_obsidian2.Setting(containerEl).setName("Limit Links Shown").setDesc('Limit the number of backlinks/outlinks shown in the footer. Surplus links are hidden behind a "Show More" button that expands them on click.').addToggle((toggle) => toggle.setValue(this.plugin.settings.limitLinks).onChange(async (value) => {
|
|
this.plugin.settings.limitLinks = value;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
}));
|
|
let linksLimitInput;
|
|
new import_obsidian2.Setting(containerEl).setName("Links Limit").setDesc('Maximum number of links to show before the "Show More" button (only applies when "Limit Links Shown" is enabled)').addText((text) => {
|
|
linksLimitInput = text;
|
|
text.setPlaceholder("10").setValue(String(this.plugin.settings.linksLimit)).onChange(async (value) => {
|
|
const numValue = Math.floor(Number(value));
|
|
if (!isNaN(numValue) && numValue > 0) {
|
|
this.plugin.settings.linksLimit = numValue;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
}
|
|
});
|
|
}).addButton((button) => button.setButtonText("Reset").onClick(async () => {
|
|
this.plugin.settings.linksLimit = DEFAULT_SETTINGS.linksLimit;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
if (linksLimitInput) {
|
|
linksLimitInput.setValue(String(DEFAULT_SETTINGS.linksLimit));
|
|
}
|
|
}));
|
|
let updateDelayInput;
|
|
new import_obsidian2.Setting(containerEl).setName("Rich-foot update delay").setDesc("Delay in milliseconds before updating the rich-foot in edit mode (lower values may impact performance)").addText((text) => {
|
|
updateDelayInput = text;
|
|
text.setPlaceholder("3000").setValue(String(this.plugin.settings.updateDelay)).onChange(async (value) => {
|
|
const numValue = Math.floor(Number(value));
|
|
if (!isNaN(numValue) && numValue > 0) {
|
|
this.plugin.settings.updateDelay = numValue;
|
|
await this.plugin.saveSettings();
|
|
}
|
|
});
|
|
}).addButton((button) => button.setButtonText("Reset").onClick(async () => {
|
|
this.plugin.settings.updateDelay = DEFAULT_SETTINGS.updateDelay;
|
|
await this.plugin.saveSettings();
|
|
if (updateDelayInput) {
|
|
updateDelayInput.setValue(String(DEFAULT_SETTINGS.updateDelay));
|
|
}
|
|
}));
|
|
containerEl.createEl("hr");
|
|
containerEl.createEl("h3", { text: "Date Settings" });
|
|
new import_obsidian2.Setting(containerEl).setName("Show Dates").setDesc("Show creation and modification dates in the footer").addToggle((toggle) => toggle.setValue(this.plugin.settings.showDates).onChange(async (value) => {
|
|
this.plugin.settings.showDates = value;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
}));
|
|
new import_obsidian2.Setting(containerEl).setName("Date Display Format").setDesc("Choose how dates should be displayed in the footer").addDropdown((dropdown) => {
|
|
const today = /* @__PURE__ */ new Date();
|
|
const formats = [
|
|
"mm/dd/yyyy",
|
|
"dd/mm/yyyy",
|
|
"yyyy-mm-dd",
|
|
"mmm dd, yyyy",
|
|
"dd mmm yyyy",
|
|
"mmmm dd, yyyy",
|
|
"ddd, mmm dd, yyyy",
|
|
"dddd, mmmm dd, yyyy",
|
|
"mm/dd/yy",
|
|
"dd/mm/yy",
|
|
"yy-mm-dd",
|
|
"m/d/yy"
|
|
];
|
|
formats.forEach((format) => {
|
|
const example = this.plugin.dataManager.formatDate(today, format);
|
|
dropdown.addOption(format, `${format} (${example})`);
|
|
});
|
|
dropdown.setValue(this.plugin.settings.dateDisplayFormat).onChange(async (value) => {
|
|
this.plugin.settings.dateDisplayFormat = value;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
});
|
|
});
|
|
new import_obsidian2.Setting(containerEl).setName("Custom Created Date Property").setDesc("Specify a frontmatter property to use for creation date (leave empty to use file creation date)").addText((text) => {
|
|
text.setValue(this.plugin.settings.customCreatedDateProp).onChange(async (value) => {
|
|
this.plugin.settings.customCreatedDateProp = value;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
});
|
|
this.createdDateInput = text;
|
|
return text;
|
|
}).addButton((button) => button.setButtonText("Reset").onClick(async () => {
|
|
this.plugin.settings.customCreatedDateProp = "";
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
this.createdDateInput.setValue("");
|
|
}));
|
|
new import_obsidian2.Setting(containerEl).setName("Custom Modified Date Property").setDesc("Specify a frontmatter property to use for modification date (leave empty to use file modification date)").addText((text) => {
|
|
text.setValue(this.plugin.settings.customModifiedDateProp).onChange(async (value) => {
|
|
this.plugin.settings.customModifiedDateProp = value;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
});
|
|
this.modifiedDateInput = text;
|
|
return text;
|
|
}).addButton((button) => button.setButtonText("Reset").onClick(async () => {
|
|
this.plugin.settings.customModifiedDateProp = "";
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
this.modifiedDateInput.setValue("");
|
|
}));
|
|
containerEl.createEl("hr");
|
|
containerEl.createEl("h3", { text: "Style Settings" });
|
|
let customWidthSetting;
|
|
let footerMaxWidthSlider;
|
|
const toggleCustomWidthVisibility = (value) => {
|
|
if (customWidthSetting) {
|
|
customWidthSetting.settingEl.style.display = value === "custom" ? "" : "none";
|
|
}
|
|
};
|
|
new import_obsidian2.Setting(containerEl).setName("Footer Width").setDesc(`Control how wide the footer is. "Readable line length" locks the footer to Obsidian's readable line width (recommended when your notes use Readable Line Length), and "Custom" lets you set a maximum width in pixels. The footer never grows wider than the note, so it won't cause horizontal scrolling on narrow screens.`).addDropdown((dropdown) => {
|
|
dropdown.addOptions({
|
|
"default": "Default (match content width)",
|
|
"readable": "Readable line length",
|
|
"custom": "Custom width (px)"
|
|
}).setValue(this.plugin.settings.footerWidth).onChange(async (value) => {
|
|
this.plugin.settings.footerWidth = value;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
toggleCustomWidthVisibility(value);
|
|
});
|
|
});
|
|
let footerMaxWidthDebounce;
|
|
customWidthSetting = new import_obsidian2.Setting(containerEl).setName("Custom Footer Width").setDesc("Maximum width of the footer in pixels (200-1200px)").addSlider((slider) => {
|
|
footerMaxWidthSlider = slider;
|
|
slider.setLimits(200, 1200, 10).setValue(this.plugin.settings.footerMaxWidth).setDynamicTooltip().onChange((value) => {
|
|
this.plugin.settings.footerMaxWidth = value;
|
|
document.documentElement.style.setProperty("--rich-foot-content-max-width", value + "px");
|
|
if (footerMaxWidthDebounce) clearTimeout(footerMaxWidthDebounce);
|
|
footerMaxWidthDebounce = setTimeout(async () => {
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
}, 300);
|
|
});
|
|
}).addButton((button) => button.setButtonText("Reset").onClick(async () => {
|
|
this.plugin.settings.footerMaxWidth = DEFAULT_SETTINGS.footerMaxWidth;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
footerMaxWidthSlider.setValue(DEFAULT_SETTINGS.footerMaxWidth);
|
|
}));
|
|
toggleCustomWidthVisibility(this.plugin.settings.footerWidth);
|
|
let borderWidthSlider;
|
|
new import_obsidian2.Setting(containerEl).setName("Border Width").setDesc("Adjust the width of the footer border (1-10px)").addSlider((slider) => {
|
|
borderWidthSlider = slider;
|
|
slider.setLimits(1, 10, 1).setValue(this.plugin.settings.borderWidth).setDynamicTooltip().onChange(async (value) => {
|
|
this.plugin.settings.borderWidth = value;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
});
|
|
}).addButton((button) => button.setButtonText("Reset").onClick(async () => {
|
|
this.plugin.settings.borderWidth = DEFAULT_SETTINGS.borderWidth;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
borderWidthSlider.setValue(DEFAULT_SETTINGS.borderWidth);
|
|
}));
|
|
let borderStyleDropdown;
|
|
new import_obsidian2.Setting(containerEl).setName("Border Style").setDesc("Choose the style of the footer border").addDropdown((dropdown) => {
|
|
borderStyleDropdown = dropdown;
|
|
dropdown.addOptions({
|
|
"solid": "Solid",
|
|
"dashed": "Dashed",
|
|
"dotted": "Dotted",
|
|
"double": "Double",
|
|
"groove": "Groove",
|
|
"ridge": "Ridge",
|
|
"inset": "Inset",
|
|
"outset": "Outset"
|
|
}).setValue(this.plugin.settings.borderStyle).onChange(async (value) => {
|
|
this.plugin.settings.borderStyle = value;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
});
|
|
}).addButton((button) => button.setButtonText("Reset").onClick(async () => {
|
|
this.plugin.settings.borderStyle = DEFAULT_SETTINGS.borderStyle;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
borderStyleDropdown.setValue(DEFAULT_SETTINGS.borderStyle);
|
|
}));
|
|
let borderOpacitySlider;
|
|
new import_obsidian2.Setting(containerEl).setName("Border Opacity").setDesc("Adjust the opacity of the footer border (0-1)").addSlider((slider) => {
|
|
borderOpacitySlider = slider;
|
|
slider.setLimits(0, 1, 0.1).setValue(this.plugin.settings.borderOpacity).setDynamicTooltip().onChange(async (value) => {
|
|
this.plugin.settings.borderOpacity = value;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
});
|
|
}).addButton((button) => button.setButtonText("Reset").onClick(async () => {
|
|
this.plugin.settings.borderOpacity = DEFAULT_SETTINGS.borderOpacity;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
borderOpacitySlider.setValue(DEFAULT_SETTINGS.borderOpacity);
|
|
}));
|
|
let borderColorPicker;
|
|
new import_obsidian2.Setting(containerEl).setName("Border Color").setDesc("Choose the color for the footer border").addColorPicker((color) => {
|
|
borderColorPicker = color;
|
|
color.setValue(this.plugin.settings.borderColor.startsWith("var(--") ? resolveCssVar("var(--text-accent)", "borderColor") : this.plugin.settings.borderColor).onChange(async (value) => {
|
|
this.plugin.settings.borderColor = value;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
});
|
|
}).addButton((button) => button.setButtonText("Reset").onClick(async () => {
|
|
this.plugin.settings.borderColor = DEFAULT_SETTINGS.borderColor;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
borderColorPicker.setValue(resolveCssVar("var(--text-accent)", "borderColor"));
|
|
}));
|
|
let borderRadiusSlider;
|
|
new import_obsidian2.Setting(containerEl).setName("Link Border Radius").setDesc("Adjust the border radius of Backlinks and Outlinks (0-15px)").addSlider((slider) => {
|
|
borderRadiusSlider = slider;
|
|
slider.setLimits(0, 15, 1).setValue(this.plugin.settings.borderRadius).setDynamicTooltip().onChange(async (value) => {
|
|
this.plugin.settings.borderRadius = value;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
});
|
|
}).addButton((button) => button.setButtonText("Reset").onClick(async () => {
|
|
this.plugin.settings.borderRadius = DEFAULT_SETTINGS.borderRadius;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
borderRadiusSlider.setValue(DEFAULT_SETTINGS.borderRadius);
|
|
}));
|
|
let linksOpacitySlider;
|
|
new import_obsidian2.Setting(containerEl).setName("Links Opacity").setDesc("Adjust the opacity of Backlinks and Outlinks (0-1)").addSlider((slider) => {
|
|
linksOpacitySlider = slider;
|
|
slider.setLimits(0, 1, 0.1).setValue(this.plugin.settings.linksOpacity).setDynamicTooltip().onChange(async (value) => {
|
|
this.plugin.settings.linksOpacity = value;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
});
|
|
}).addButton((button) => button.setButtonText("Reset").onClick(async () => {
|
|
this.plugin.settings.linksOpacity = DEFAULT_SETTINGS.linksOpacity;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
linksOpacitySlider.setValue(DEFAULT_SETTINGS.linksOpacity);
|
|
}));
|
|
let linkColorPicker;
|
|
new import_obsidian2.Setting(containerEl).setName("Link Text Color").setDesc("Choose the color for link text").addColorPicker((color) => {
|
|
linkColorPicker = color;
|
|
color.setValue(this.plugin.settings.linkColor.startsWith("var(--") ? resolveCssVar("var(--link-color)", "color") : this.plugin.settings.linkColor).onChange(async (value) => {
|
|
this.plugin.settings.linkColor = value;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
});
|
|
}).addButton((button) => button.setButtonText("Reset").onClick(async () => {
|
|
this.plugin.settings.linkColor = DEFAULT_SETTINGS.linkColor;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
linkColorPicker.setValue(resolveCssVar("var(--link-color)", "color"));
|
|
}));
|
|
let linkBackgroundColorPicker;
|
|
new import_obsidian2.Setting(containerEl).setName("Link Background Color").setDesc("Choose the background color for links").addColorPicker((color) => {
|
|
linkBackgroundColorPicker = color;
|
|
color.setValue(this.plugin.settings.linkBackgroundColor.startsWith("var(--") ? resolveCssVar("var(--tag-background)", "backgroundColor") : this.plugin.settings.linkBackgroundColor).onChange(async (value) => {
|
|
this.plugin.settings.linkBackgroundColor = value;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
});
|
|
}).addButton((button) => button.setButtonText("Reset").onClick(async () => {
|
|
this.plugin.settings.linkBackgroundColor = DEFAULT_SETTINGS.linkBackgroundColor;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
linkBackgroundColorPicker.setValue(resolveCssVar("var(--tag-background)", "backgroundColor"));
|
|
}));
|
|
let linkBorderColorPicker;
|
|
new import_obsidian2.Setting(containerEl).setName("Link Border Color").setDesc("Choose the border color for links").addColorPicker((color) => {
|
|
linkBorderColorPicker = color;
|
|
color.setValue(this.plugin.settings.linkBorderColor.startsWith("rgba(") ? resolveCssVar(this.plugin.settings.linkBorderColor, "borderColor") : this.plugin.settings.linkBorderColor).onChange(async (value) => {
|
|
this.plugin.settings.linkBorderColor = value;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
});
|
|
}).addButton((button) => button.setButtonText("Reset").onClick(async () => {
|
|
this.plugin.settings.linkBorderColor = DEFAULT_SETTINGS.linkBorderColor;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
linkBorderColorPicker.setValue(resolveCssVar(DEFAULT_SETTINGS.linkBorderColor, "borderColor"));
|
|
}));
|
|
let datesOpacitySlider;
|
|
new import_obsidian2.Setting(containerEl).setName("Dates Opacity").setDesc("Adjust the opacity of the Created / Modified Dates (0-1)").addSlider((slider) => {
|
|
datesOpacitySlider = slider;
|
|
slider.setLimits(0, 1, 0.1).setValue(this.plugin.settings.datesOpacity).setDynamicTooltip().onChange(async (value) => {
|
|
this.plugin.settings.datesOpacity = value;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
});
|
|
}).addButton((button) => button.setButtonText("Reset").onClick(async () => {
|
|
this.plugin.settings.datesOpacity = DEFAULT_SETTINGS.datesOpacity;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
datesOpacitySlider.setValue(DEFAULT_SETTINGS.datesOpacity);
|
|
}));
|
|
let dateColorPicker;
|
|
new import_obsidian2.Setting(containerEl).setName("Date Color").setDesc("Choose the color for Created / Modified Dates").addColorPicker((color) => {
|
|
dateColorPicker = color;
|
|
color.setValue(this.plugin.settings.dateColor.startsWith("var(--") ? resolveCssVar("var(--text-accent)", "color") : this.plugin.settings.dateColor).onChange(async (value) => {
|
|
this.plugin.settings.dateColor = value;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
});
|
|
}).addButton((button) => button.setButtonText("Reset").onClick(async () => {
|
|
this.plugin.settings.dateColor = DEFAULT_SETTINGS.dateColor;
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
dateColorPicker.setValue(resolveCssVar("var(--text-accent)", "color"));
|
|
}));
|
|
containerEl.createEl("hr");
|
|
containerEl.createEl("h3", { text: "Exclusion Rules" });
|
|
containerEl.createEl("h3", { text: "Excluded Folders" });
|
|
containerEl.createEl("p", {
|
|
text: "Notes in excluded folders (and their subfolders) will not display the Rich Foot footer. This is useful for system folders or areas where you don't want footer information to appear.",
|
|
cls: "setting-item-description"
|
|
});
|
|
const excludedFoldersContainer = containerEl.createDiv("excluded-folders-container");
|
|
if ((_a = this.plugin.settings) == null ? void 0 : _a.excludedFolders) {
|
|
this.plugin.settings.excludedFolders.forEach((folder, index) => {
|
|
const folderDiv = excludedFoldersContainer.createDiv("excluded-folder-item");
|
|
folderDiv.createSpan({ text: folder });
|
|
const deleteButton = folderDiv.createEl("button", {
|
|
text: "Delete",
|
|
cls: "excluded-folder-delete"
|
|
});
|
|
deleteButton.addEventListener("click", async () => {
|
|
this.plugin.settings.excludedFolders.splice(index, 1);
|
|
await this.plugin.saveSettings();
|
|
this.display();
|
|
});
|
|
});
|
|
}
|
|
const newFolderSetting = new import_obsidian2.Setting(containerEl).setName("Add excluded folder").setDesc("Enter a folder path or browse to select").addText((text) => text.setPlaceholder("folder/subfolder")).addButton((button) => button.setButtonText("Browse").onClick(async () => {
|
|
const folder = await this.browseForFolder();
|
|
if (folder) {
|
|
const textComponent = newFolderSetting.components[0];
|
|
textComponent.setValue(folder);
|
|
}
|
|
})).addButton((button) => button.setButtonText("Add").onClick(async () => {
|
|
const textComponent = newFolderSetting.components[0];
|
|
const newFolder = textComponent.getValue().trim();
|
|
if (newFolder && !this.plugin.settings.excludedFolders.includes(newFolder)) {
|
|
this.plugin.settings.excludedFolders.push(newFolder);
|
|
await this.plugin.saveSettings();
|
|
textComponent.setValue("");
|
|
this.display();
|
|
}
|
|
}));
|
|
containerEl.createEl("h4", { text: "Exclude Rich Foot via Frontmatter" });
|
|
let frontmatterExclusionInput;
|
|
new import_obsidian2.Setting(containerEl).setName("Frontmatter Exclusion Field").setDesc("If this frontmatter field exists and has a truthy value (true, yes, 1, on), Rich Foot will not be shown on that note").addText((text) => {
|
|
frontmatterExclusionInput = text;
|
|
text.setPlaceholder("e.g., exclude-rich-foot").setValue(this.plugin.settings.frontmatterExclusionField).onChange(async (value) => {
|
|
this.plugin.settings.frontmatterExclusionField = value.trim();
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
});
|
|
}).addButton((button) => button.setButtonText("Reset").onClick(async () => {
|
|
this.plugin.settings.frontmatterExclusionField = "";
|
|
await this.plugin.saveSettings();
|
|
await this.plugin.updateRichFoot();
|
|
if (frontmatterExclusionInput) {
|
|
frontmatterExclusionInput.setValue("");
|
|
}
|
|
}));
|
|
containerEl.createEl("h4", { text: "Excluded Parent Selectors" });
|
|
containerEl.createEl("p", {
|
|
text: "Rich Foot will not be added to notes that have any of these parent selectors in their DOM hierarchy. Useful for compatibility with other plugins.",
|
|
cls: "setting-item-description"
|
|
});
|
|
const excludedSelectorsContainer = containerEl.createDiv("excluded-selectors-container");
|
|
if ((_b = this.plugin.settings) == null ? void 0 : _b.excludedParentSelectors) {
|
|
this.plugin.settings.excludedParentSelectors.forEach((selector, index) => {
|
|
const selectorDiv = excludedSelectorsContainer.createDiv("excluded-selector-item");
|
|
selectorDiv.createSpan({ text: selector });
|
|
const deleteButton = selectorDiv.createEl("button", {
|
|
text: "Delete",
|
|
cls: "excluded-selector-delete"
|
|
});
|
|
deleteButton.addEventListener("click", async () => {
|
|
this.plugin.settings.excludedParentSelectors.splice(index, 1);
|
|
await this.plugin.saveSettings();
|
|
this.display();
|
|
});
|
|
});
|
|
}
|
|
const newSelectorSetting = new import_obsidian2.Setting(containerEl).setName("Add excluded parent selector").setDesc('Enter a CSS selector (e.g., .some-class, #some-id, [data-type="special"])').addText((text) => text.setPlaceholder("Enter selector").onChange(() => {
|
|
try {
|
|
document.querySelector(text.getValue());
|
|
text.inputEl.removeClass("rich-foot-input-error");
|
|
} catch (e) {
|
|
text.inputEl.addClass("rich-foot-input-error");
|
|
}
|
|
})).addButton((button) => button.setButtonText("Add").onClick(async () => {
|
|
const textComponent = newSelectorSetting.components[0];
|
|
const newSelector = textComponent.getValue().trim();
|
|
if (!newSelector) return;
|
|
try {
|
|
document.querySelector(newSelector);
|
|
} catch (e) {
|
|
new import_obsidian2.Notice("Invalid CSS selector");
|
|
return;
|
|
}
|
|
if (!this.plugin.settings.excludedParentSelectors.includes(newSelector)) {
|
|
this.plugin.settings.excludedParentSelectors.push(newSelector);
|
|
await this.plugin.saveSettings();
|
|
textComponent.setValue("");
|
|
this.display();
|
|
}
|
|
}));
|
|
containerEl.createEl("h3", { text: "Example Screenshot", cls: "rich-foot-example-title" });
|
|
const exampleDiv = containerEl.createDiv({ cls: "rich-foot-example" });
|
|
exampleDiv.createEl("img", {
|
|
attr: {
|
|
src: "https://raw.githubusercontent.com/jparkerweb/rich-foot/refs/heads/main/rich-foot.jpg",
|
|
alt: "Rich Foot Example"
|
|
}
|
|
});
|
|
new import_obsidian2.Setting(containerEl).setName("Show Release Notes").setDesc("Show release notes after plugin updates").addToggle((toggle) => toggle.setValue(this.plugin.settings.showReleaseNotes).onChange(async (value) => {
|
|
this.plugin.settings.showReleaseNotes = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
new import_obsidian2.Setting(containerEl).setName("View Release Notes").setDesc("View release notes for the current version").addButton((button) => button.setButtonText("View Release Notes").onClick(async () => {
|
|
const notes = await this.plugin.getReleaseNotes(this.plugin.manifest.version);
|
|
new ReleaseNotesModal(this.app, this.plugin, this.plugin.manifest.version, notes).open();
|
|
}));
|
|
}
|
|
async browseForFolder() {
|
|
const folders = this.app.vault.getAllLoadedFiles().filter((file) => file.children).map((folder) => folder.path);
|
|
return new Promise((resolve) => {
|
|
const modal = new FolderSuggestModal(this.app, folders, (result) => {
|
|
resolve(result);
|
|
});
|
|
modal.open();
|
|
});
|
|
}
|
|
};
|
|
var FolderSuggestModal = class extends import_obsidian2.FuzzySuggestModal {
|
|
constructor(app, folders, onChoose) {
|
|
super(app);
|
|
this.folders = folders;
|
|
this.onChoose = onChoose;
|
|
}
|
|
getItems() {
|
|
return this.folders;
|
|
}
|
|
getItemText(item) {
|
|
return item;
|
|
}
|
|
onChooseItem(item, evt) {
|
|
this.onChoose(item);
|
|
}
|
|
};
|
|
|
|
// src/data-manager.js
|
|
var RichFootDataManager = class {
|
|
constructor(app) {
|
|
this.app = app;
|
|
}
|
|
/**
|
|
* Get backlinks for a file
|
|
* @param {TFile} file - The file to get backlinks for
|
|
* @returns {Map} Map of backlink paths to their data
|
|
*/
|
|
getBacklinks(file) {
|
|
const resolvedLinks = this.app.metadataCache.resolvedLinks;
|
|
const backlinks = /* @__PURE__ */ new Map();
|
|
for (const sourcePath in resolvedLinks) {
|
|
const targets = resolvedLinks[sourcePath];
|
|
if (targets[file.path]) {
|
|
backlinks.set(sourcePath, targets[file.path]);
|
|
}
|
|
}
|
|
return backlinks;
|
|
}
|
|
/**
|
|
* Get all outlinks from a file (links, embeds, frontmatter, footnotes)
|
|
* @param {TFile} file - The file to get outlinks from
|
|
* @returns {Promise<Set>} Set of outlink paths
|
|
*/
|
|
async getOutlinks(file) {
|
|
const cache = this.app.metadataCache.getFileCache(file);
|
|
const links = /* @__PURE__ */ new Set();
|
|
if (cache == null ? void 0 : cache.links) {
|
|
for (const link of cache.links) {
|
|
this.addResolvedLink(link.link, file, links);
|
|
}
|
|
}
|
|
if (cache == null ? void 0 : cache.embeds) {
|
|
for (const embed of cache.embeds) {
|
|
this.addResolvedLink(embed.link, file, links);
|
|
}
|
|
}
|
|
if (cache == null ? void 0 : cache.frontmatterLinks) {
|
|
for (const link of cache.frontmatterLinks) {
|
|
this.addResolvedLink(link.link, file, links);
|
|
}
|
|
}
|
|
if (cache == null ? void 0 : cache.blocks) {
|
|
for (const block of Object.values(cache.blocks)) {
|
|
if (block.type === "footnote") {
|
|
this.extractWikiLinks(block.text, file, links);
|
|
}
|
|
}
|
|
}
|
|
const fileContent = await this.app.vault.cachedRead(file);
|
|
this.processFootnotes(fileContent, file, links);
|
|
return links;
|
|
}
|
|
/**
|
|
* Add a resolved link to the set
|
|
* @private
|
|
*/
|
|
addResolvedLink(linkText, sourceFile, linksSet) {
|
|
const linkPath = linkText.split("#")[0];
|
|
const targetFile = this.app.metadataCache.getFirstLinkpathDest(linkPath, sourceFile.path);
|
|
if (targetFile && targetFile.extension === "md") {
|
|
linksSet.add(targetFile.path);
|
|
}
|
|
}
|
|
/**
|
|
* Extract wiki links from text
|
|
* @private
|
|
*/
|
|
extractWikiLinks(text, sourceFile, linksSet) {
|
|
const wikiLinkRegex = /\[\[(.*?)\]\]/g;
|
|
let match;
|
|
while ((match = wikiLinkRegex.exec(text)) !== null) {
|
|
const linkText = match[1].trim();
|
|
this.addResolvedLink(linkText, sourceFile, linksSet);
|
|
}
|
|
}
|
|
/**
|
|
* Process footnotes from file content
|
|
* @private
|
|
*/
|
|
processFootnotes(content, file, links) {
|
|
const inlineFootnoteRegex = /\^\[((?:[^\[\]]|\[(?:[^\[\]]|\[[^\[\]]*\])*\])*)\]/g;
|
|
const refFootnoteRegex = /\[\^[^\]]+\]:\s*((?:[^\[\]]|\[(?:[^\[\]]|\[[^\[\]]*\])*\])*)/g;
|
|
let match;
|
|
while ((match = inlineFootnoteRegex.exec(content)) !== null) {
|
|
const footnoteContent = match[1];
|
|
this.extractWikiLinks(footnoteContent, file, links);
|
|
}
|
|
while ((match = refFootnoteRegex.exec(content)) !== null) {
|
|
const footnoteContent = match[1];
|
|
this.extractWikiLinks(footnoteContent, file, links);
|
|
}
|
|
}
|
|
/**
|
|
* Get dates for a file (created and modified)
|
|
* @param {TFile} file - The file to get dates for
|
|
* @param {Object} settings - Plugin settings
|
|
* @returns {Object} { created, modified } formatted dates
|
|
*/
|
|
getDates(file, settings) {
|
|
const cache = this.app.metadataCache.getFileCache(file);
|
|
const frontmatter = cache == null ? void 0 : cache.frontmatter;
|
|
return {
|
|
created: this.getFormattedDate(
|
|
file,
|
|
frontmatter,
|
|
settings.customCreatedDateProp,
|
|
"ctime",
|
|
settings.dateDisplayFormat
|
|
),
|
|
modified: this.getFormattedDate(
|
|
file,
|
|
frontmatter,
|
|
settings.customModifiedDateProp,
|
|
"mtime",
|
|
settings.dateDisplayFormat
|
|
)
|
|
};
|
|
}
|
|
/**
|
|
* Get a formatted date from frontmatter or file stats
|
|
* @private
|
|
*/
|
|
getFormattedDate(file, frontmatter, customProp, statProp, format) {
|
|
let dateValue;
|
|
if (customProp && frontmatter && frontmatter[customProp]) {
|
|
const parsed = this.parseDate(frontmatter[customProp]);
|
|
if (parsed) {
|
|
dateValue = parsed;
|
|
}
|
|
}
|
|
if (!dateValue) {
|
|
dateValue = new Date(file.stat[statProp]);
|
|
}
|
|
return this.formatDate(dateValue, format);
|
|
}
|
|
/**
|
|
* Parse a date string with multiple format attempts
|
|
* @param {string} value - Date string to parse
|
|
* @returns {Date|null} Parsed date or null if invalid
|
|
*/
|
|
parseDate(value) {
|
|
if (!value) return null;
|
|
let tempDate = String(value);
|
|
if (!isNaN(Date.parse(tempDate))) {
|
|
return this.createDateWithTime(tempDate);
|
|
}
|
|
tempDate = this.replaceNTimes(String(value), /\./g, "-", 2);
|
|
if (!isNaN(Date.parse(tempDate))) {
|
|
return this.createDateWithTime(tempDate);
|
|
}
|
|
tempDate = this.replaceNTimes(String(value), /\//g, "-", 2);
|
|
if (!isNaN(Date.parse(tempDate))) {
|
|
return this.createDateWithTime(tempDate);
|
|
}
|
|
return null;
|
|
}
|
|
/**
|
|
* Replace pattern N times in a string
|
|
* @private
|
|
*/
|
|
replaceNTimes(str, pattern, replacement, times) {
|
|
let count = 0;
|
|
return str.replace(pattern, (match) => {
|
|
count++;
|
|
return count <= times ? replacement : match;
|
|
});
|
|
}
|
|
/**
|
|
* Create date object and add midnight time if no time component exists
|
|
* @private
|
|
*/
|
|
createDateWithTime(dateStr) {
|
|
let tempDate = dateStr;
|
|
if (!tempDate.includes("T") && !tempDate.includes(" ")) {
|
|
tempDate = `${tempDate}T00:00:00`;
|
|
}
|
|
return new Date(tempDate);
|
|
}
|
|
/**
|
|
* Format a date according to the specified format
|
|
* @param {Date} date - Date to format
|
|
* @param {string} format - Format string
|
|
* @returns {string} Formatted date string
|
|
*/
|
|
formatDate(date, format) {
|
|
const d = new Date(date);
|
|
const year = d.getFullYear();
|
|
const month = d.getMonth();
|
|
const day = d.getDate();
|
|
const weekday = d.getDay();
|
|
const months = [
|
|
"January",
|
|
"February",
|
|
"March",
|
|
"April",
|
|
"May",
|
|
"June",
|
|
"July",
|
|
"August",
|
|
"September",
|
|
"October",
|
|
"November",
|
|
"December"
|
|
];
|
|
const monthsShort = months.map((m) => m.slice(0, 3));
|
|
const weekdays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
const weekdaysShort = weekdays.map((w) => w.slice(0, 3));
|
|
const pad = (num) => num.toString().padStart(2, "0");
|
|
const tokens = {
|
|
"dddd": weekdays[weekday],
|
|
"ddd": weekdaysShort[weekday],
|
|
"dd": pad(day),
|
|
"d": day.toString(),
|
|
"mmmm": months[month],
|
|
"mmm": monthsShort[month],
|
|
"mm": pad(month + 1),
|
|
"m": (month + 1).toString(),
|
|
"yyyy": year.toString(),
|
|
"yy": year.toString().slice(-2)
|
|
};
|
|
const sortedTokens = Object.keys(tokens).sort((a, b) => b.length - a.length);
|
|
let result = format.toLowerCase();
|
|
const replacements = /* @__PURE__ */ new Map();
|
|
sortedTokens.forEach((token, index) => {
|
|
const placeholder = `__${index}__`;
|
|
replacements.set(placeholder, tokens[token]);
|
|
result = result.replace(new RegExp(token, "gi"), placeholder);
|
|
});
|
|
replacements.forEach((value, placeholder) => {
|
|
result = result.replace(new RegExp(placeholder, "g"), value);
|
|
});
|
|
return result;
|
|
}
|
|
};
|
|
|
|
// src/renderer.js
|
|
var import_obsidian3 = require("obsidian");
|
|
var showMoreIdCounter = 0;
|
|
var RichFootRenderer = class {
|
|
constructor(plugin) {
|
|
this.plugin = plugin;
|
|
}
|
|
/**
|
|
* Create the complete footer element for a file
|
|
* @param {TFile} file - The file to create footer for
|
|
* @param {Object} data - Pre-fetched data { backlinks, outlinks, dates }
|
|
* @returns {HTMLElement} The footer element
|
|
*/
|
|
createFooter(file, data) {
|
|
const { backlinks, outlinks, dates } = data;
|
|
const { settings } = this.plugin;
|
|
const richFoot = createDiv({ cls: "rich-foot rich-foot--hidden" });
|
|
richFoot.setAttribute("data-rich-foot", "true");
|
|
richFoot.setAttribute("data-file-path", file.path);
|
|
richFoot.createDiv({ cls: "rich-foot--dashed-line" });
|
|
if (settings.combineLinks) {
|
|
this.createCombinedLinksSection(richFoot, file, backlinks, outlinks);
|
|
} else {
|
|
if (settings.showBacklinks) {
|
|
this.createLinksSection(richFoot, file, backlinks, "backlinks");
|
|
}
|
|
if (settings.showOutlinks) {
|
|
this.createLinksSection(richFoot, file, outlinks, "outlinks");
|
|
}
|
|
}
|
|
if (settings.showDates && dates) {
|
|
this.createDatesSection(richFoot, dates);
|
|
}
|
|
return richFoot;
|
|
}
|
|
/**
|
|
* Create combined links section (backlinks + outlinks)
|
|
* @private
|
|
*/
|
|
createCombinedLinksSection(container, file, backlinks, outlinks) {
|
|
if (backlinks.size === 0 && outlinks.size === 0) return;
|
|
const linksDiv = container.createDiv({ cls: "rich-foot--links" });
|
|
const linksUl = linksDiv.createEl("ul");
|
|
const processedLinks = /* @__PURE__ */ new Set();
|
|
for (const [linkPath] of backlinks) {
|
|
if (!linkPath.endsWith(".md")) continue;
|
|
processedLinks.add(linkPath);
|
|
const metadata = {
|
|
isBacklink: true,
|
|
isOutlink: outlinks.has(linkPath)
|
|
};
|
|
const li = linksUl.createEl("li");
|
|
this.createLinkElement(li, file, linkPath, metadata);
|
|
}
|
|
for (const linkPath of outlinks) {
|
|
if (processedLinks.has(linkPath)) continue;
|
|
const metadata = {
|
|
isBacklink: false,
|
|
isOutlink: true
|
|
};
|
|
const li = linksUl.createEl("li");
|
|
this.createLinkElement(li, file, linkPath, metadata);
|
|
}
|
|
if (linksUl.childElementCount === 0) {
|
|
linksDiv.remove();
|
|
return;
|
|
}
|
|
this.applyLinkLimit(linksUl);
|
|
}
|
|
/**
|
|
* Create links section (backlinks or outlinks)
|
|
* @private
|
|
*/
|
|
createLinksSection(container, file, links, type) {
|
|
const linksArray = type === "backlinks" ? Array.from(links.keys()).filter((path) => path.endsWith(".md")) : Array.from(links);
|
|
if (linksArray.length === 0) return;
|
|
const className = `rich-foot--${type}`;
|
|
const linksDiv = container.createDiv({ cls: className });
|
|
const linksUl = linksDiv.createEl("ul");
|
|
for (const linkPath of linksArray) {
|
|
const li = linksUl.createEl("li");
|
|
const metadata = {
|
|
isBacklink: type === "backlinks",
|
|
isOutlink: type === "outlinks"
|
|
};
|
|
this.createLinkElement(li, file, linkPath, metadata);
|
|
}
|
|
if (linksUl.childElementCount === 0) {
|
|
linksDiv.remove();
|
|
return;
|
|
}
|
|
this.applyLinkLimit(linksUl);
|
|
}
|
|
/**
|
|
* Limit the number of visible links, hiding the surplus behind a
|
|
* toggleable "Show More (X)" button.
|
|
* @private
|
|
* @param {HTMLElement} linksUl - The <ul> containing link <li> elements
|
|
*/
|
|
applyLinkLimit(linksUl) {
|
|
const { settings } = this.plugin;
|
|
if (!settings.limitLinks) return;
|
|
const limit = Math.floor(Number(settings.linksLimit));
|
|
if (!Number.isFinite(limit) || limit < 1) return;
|
|
const items = Array.from(linksUl.children);
|
|
if (items.length <= limit) return;
|
|
const surplus = items.length - limit;
|
|
const listId = `rich-foot-links-${++showMoreIdCounter}`;
|
|
linksUl.id = listId;
|
|
for (let i = limit; i < items.length; i++) {
|
|
items[i].addClass("rich-foot--link-hidden");
|
|
}
|
|
const toggleLi = linksUl.createEl("li", { cls: "rich-foot--show-more-li" });
|
|
const toggleBtn = toggleLi.createEl("button", {
|
|
cls: "rich-foot--show-more",
|
|
text: `Show More (${surplus})`,
|
|
attr: {
|
|
type: "button",
|
|
"aria-expanded": "false",
|
|
"aria-controls": listId
|
|
}
|
|
});
|
|
let expanded = false;
|
|
toggleBtn.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
expanded = !expanded;
|
|
for (let i = limit; i < items.length; i++) {
|
|
items[i].toggleClass("rich-foot--link-hidden", !expanded);
|
|
}
|
|
toggleBtn.setText(expanded ? "Show Less" : `Show More (${surplus})`);
|
|
toggleBtn.setAttribute("aria-expanded", String(expanded));
|
|
});
|
|
}
|
|
/**
|
|
* Create a single link element
|
|
* @private
|
|
*/
|
|
createLinkElement(container, file, linkPath, metadata) {
|
|
const displayName = linkPath.split("/").pop().slice(0, -3);
|
|
const isEditMode = this.isEditMode();
|
|
const link = container.createEl("a", {
|
|
href: linkPath,
|
|
text: displayName,
|
|
cls: isEditMode ? "cm-hmd-internal-link cm-underline" : "internal-link"
|
|
});
|
|
link.dataset.href = linkPath;
|
|
link.dataset.sourcePath = file.path;
|
|
if (metadata.isBacklink) link.dataset.isBacklink = "true";
|
|
if (metadata.isOutlink) link.dataset.isOutlink = "true";
|
|
this.setupLinkHandlers(link, linkPath, file);
|
|
return link;
|
|
}
|
|
/**
|
|
* Setup event handlers for a link element
|
|
* @private
|
|
*/
|
|
setupLinkHandlers(link, linkPath, file) {
|
|
link.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
this.plugin.app.workspace.openLinkText(linkPath, file.path);
|
|
});
|
|
if (this.isEditMode()) {
|
|
this.setupHoverPreview(link, linkPath, file);
|
|
}
|
|
}
|
|
/**
|
|
* Setup hover preview for a link
|
|
* @private
|
|
*/
|
|
setupHoverPreview(link, linkPath, file) {
|
|
const pagePreviewPlugin = this.plugin.app.internalPlugins.plugins["page-preview"];
|
|
if (!(pagePreviewPlugin == null ? void 0 : pagePreviewPlugin.enabled)) return;
|
|
link.addEventListener("mouseover", (mouseEvent) => {
|
|
const previewPlugin = pagePreviewPlugin.instance;
|
|
if (previewPlugin == null ? void 0 : previewPlugin.onLinkHover) {
|
|
previewPlugin.onLinkHover(mouseEvent, link, linkPath, file.path);
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Create dates section
|
|
* @private
|
|
*/
|
|
createDatesSection(container, dates) {
|
|
const datesWrapper = container.createDiv({ cls: "rich-foot--dates-wrapper" });
|
|
datesWrapper.createDiv({
|
|
cls: "rich-foot--modified-date",
|
|
text: dates.modified
|
|
});
|
|
datesWrapper.createDiv({
|
|
cls: "rich-foot--created-date",
|
|
text: dates.created
|
|
});
|
|
}
|
|
/**
|
|
* Attach footer to container with fade-in animation using RAF
|
|
* @param {HTMLElement} container - Target container
|
|
* @param {HTMLElement} footer - Footer element
|
|
*/
|
|
attachToContainer(container, footer) {
|
|
container.appendChild(footer);
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
footer.removeClass("rich-foot--hidden");
|
|
});
|
|
});
|
|
}
|
|
/**
|
|
* Check if current view is in edit mode
|
|
* @private
|
|
*/
|
|
isEditMode() {
|
|
var _a, _b;
|
|
const activeView = this.plugin.app.workspace.getActiveViewOfType(import_obsidian3.MarkdownView);
|
|
if (!activeView) return false;
|
|
const mode = (_b = (_a = activeView.getMode) == null ? void 0 : _a.call(activeView)) != null ? _b : activeView.mode;
|
|
return mode === "source";
|
|
}
|
|
};
|
|
|
|
// src/view-manager.js
|
|
var RichFootViewManager = class {
|
|
constructor(plugin) {
|
|
this.plugin = plugin;
|
|
this.observers = /* @__PURE__ */ new Map();
|
|
this.pendingUpdates = /* @__PURE__ */ new Map();
|
|
}
|
|
/**
|
|
* Attach footer to a view
|
|
* @param {MarkdownView} view - The view to attach to
|
|
*/
|
|
async attachToView(view) {
|
|
if (!view || !view.file) return;
|
|
const file = view.file;
|
|
if (this.shouldExclude(view, file)) {
|
|
this.detachFromView(view);
|
|
return;
|
|
}
|
|
const container = this.getTargetContainer(view);
|
|
if (!container) {
|
|
return;
|
|
}
|
|
if (!this.shouldUpdate(container, file)) {
|
|
return;
|
|
}
|
|
this.cancelPendingUpdate(container);
|
|
const data = await this.fetchData(file);
|
|
await this.renderFooter(view, container, file, data);
|
|
}
|
|
/**
|
|
* Fetch all data needed for rendering
|
|
* @private
|
|
*/
|
|
async fetchData(file) {
|
|
const { dataManager, settings } = this.plugin;
|
|
const [backlinks, outlinks, dates] = await Promise.all([
|
|
Promise.resolve(dataManager.getBacklinks(file)),
|
|
dataManager.getOutlinks(file),
|
|
Promise.resolve(dataManager.getDates(file, settings))
|
|
]);
|
|
return { backlinks, outlinks, dates };
|
|
}
|
|
/**
|
|
* Render footer in container
|
|
* @private
|
|
*/
|
|
async renderFooter(view, container, file, data) {
|
|
const rafId = requestAnimationFrame(() => {
|
|
try {
|
|
this.removeFooterFromContainer(container);
|
|
const footer = this.plugin.renderer.createFooter(file, data);
|
|
this.plugin.renderer.attachToContainer(container, footer);
|
|
this.setupObserver(container, view);
|
|
} catch (error) {
|
|
console.error("Rich Foot render error:", error);
|
|
}
|
|
this.pendingUpdates.delete(container);
|
|
});
|
|
this.pendingUpdates.set(container, rafId);
|
|
}
|
|
/**
|
|
* Check if should update footer
|
|
* @private
|
|
*/
|
|
shouldUpdate(container, file) {
|
|
const existingFooter = container.querySelector(".rich-foot[data-rich-foot]");
|
|
if (!existingFooter) return true;
|
|
const currentPath = existingFooter.getAttribute("data-file-path");
|
|
return currentPath !== file.path;
|
|
}
|
|
/**
|
|
* Cancel pending RAF update
|
|
* @private
|
|
*/
|
|
cancelPendingUpdate(container) {
|
|
const rafId = this.pendingUpdates.get(container);
|
|
if (rafId) {
|
|
cancelAnimationFrame(rafId);
|
|
this.pendingUpdates.delete(container);
|
|
}
|
|
}
|
|
/**
|
|
* Detach footer from a view
|
|
* @param {MarkdownView} view - The view to detach from
|
|
*/
|
|
detachFromView(view) {
|
|
if (!view || !view.contentEl) return;
|
|
const footers = view.contentEl.querySelectorAll(".rich-foot[data-rich-foot]");
|
|
footers.forEach((footer) => footer.remove());
|
|
const containers = this.getContainersInView(view);
|
|
containers.forEach((container) => {
|
|
this.disconnectObserver(container);
|
|
});
|
|
}
|
|
/**
|
|
* Get all containers in a view that might have footers
|
|
* @private
|
|
*/
|
|
getContainersInView(view) {
|
|
const containers = [];
|
|
const previewSection = view.contentEl.querySelector(".markdown-preview-section");
|
|
if (previewSection) containers.push(previewSection);
|
|
const cmSizer = view.contentEl.querySelector(".cm-sizer");
|
|
if (cmSizer) containers.push(cmSizer);
|
|
return containers;
|
|
}
|
|
/**
|
|
* Get the target container for footer attachment
|
|
* @param {MarkdownView} view - The view
|
|
* @returns {HTMLElement|null} Container element or null
|
|
*/
|
|
getTargetContainer(view) {
|
|
var _a, _b;
|
|
const mode = (_b = (_a = view.getMode) == null ? void 0 : _a.call(view)) != null ? _b : view.mode;
|
|
if (mode === "preview") {
|
|
const previewSections = view.contentEl.querySelectorAll(".markdown-preview-section");
|
|
for (const section of previewSections) {
|
|
if (!section.closest(".internal-embed")) {
|
|
return section;
|
|
}
|
|
}
|
|
} else if (mode === "source" || mode === "live") {
|
|
return view.contentEl.querySelector(".cm-sizer");
|
|
}
|
|
return null;
|
|
}
|
|
/**
|
|
* Check if file/view should be excluded
|
|
* @param {MarkdownView} view - The view
|
|
* @param {TFile} file - The file
|
|
* @returns {boolean} True if should exclude
|
|
*/
|
|
shouldExclude(view, file) {
|
|
var _a, _b, _c;
|
|
const { settings } = this.plugin;
|
|
if ((_a = settings.excludedFolders) == null ? void 0 : _a.some((folder) => file.path.startsWith(folder))) {
|
|
return true;
|
|
}
|
|
if (settings.frontmatterExclusionField) {
|
|
const cache = this.plugin.app.metadataCache.getFileCache(file);
|
|
const frontmatterValue = (_b = cache == null ? void 0 : cache.frontmatter) == null ? void 0 : _b[settings.frontmatterExclusionField];
|
|
if (this.isTruthy(frontmatterValue)) {
|
|
return true;
|
|
}
|
|
}
|
|
if (((_c = settings.excludedParentSelectors) == null ? void 0 : _c.length) > 0) {
|
|
if (this.hasExcludedParent(view)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* Check if view has an excluded parent element
|
|
* @private
|
|
*/
|
|
hasExcludedParent(view) {
|
|
const { settings } = this.plugin;
|
|
return settings.excludedParentSelectors.some((selector) => {
|
|
var _a, _b;
|
|
try {
|
|
let element = view.containerEl;
|
|
while (element) {
|
|
if ((_a = element.matches) == null ? void 0 : _a.call(element, selector)) {
|
|
return true;
|
|
}
|
|
if ((_b = element.querySelector) == null ? void 0 : _b.call(element, selector)) {
|
|
return true;
|
|
}
|
|
element = element.parentElement;
|
|
}
|
|
return false;
|
|
} catch (e) {
|
|
console.error(`Invalid selector in Rich Foot settings: ${selector}`);
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Check if value is truthy
|
|
* @private
|
|
*/
|
|
isTruthy(value) {
|
|
if (!value) return false;
|
|
const truthyValues = ["true", "yes", "1", "on"];
|
|
return truthyValues.includes(String(value).toLowerCase());
|
|
}
|
|
/**
|
|
* Remove footer from a container
|
|
* @private
|
|
*/
|
|
removeFooterFromContainer(container) {
|
|
const footer = container.querySelector(".rich-foot[data-rich-foot]");
|
|
if (footer) {
|
|
footer.remove();
|
|
}
|
|
}
|
|
/**
|
|
* Setup optimized MutationObserver for a container
|
|
* @private
|
|
*/
|
|
setupObserver(container, view) {
|
|
this.disconnectObserver(container);
|
|
let timeoutId = null;
|
|
let rafId = null;
|
|
let recheckTimeoutId = null;
|
|
const observerCallback = () => {
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
timeoutId = null;
|
|
}
|
|
timeoutId = setTimeout(() => {
|
|
if (rafId) cancelAnimationFrame(rafId);
|
|
rafId = requestAnimationFrame(async () => {
|
|
rafId = null;
|
|
timeoutId = null;
|
|
if (!container.isConnected) {
|
|
return;
|
|
}
|
|
const footer = container.querySelector(".rich-foot[data-rich-foot]");
|
|
if (!footer) {
|
|
recheckTimeoutId = setTimeout(() => {
|
|
recheckTimeoutId = null;
|
|
const footerRecheck = container.querySelector(".rich-foot[data-rich-foot]");
|
|
if (!footerRecheck && container.isConnected) {
|
|
try {
|
|
this.attachToView(view);
|
|
} catch (error) {
|
|
console.error("Rich Foot observer re-attach error:", error);
|
|
}
|
|
}
|
|
}, 100);
|
|
}
|
|
});
|
|
}, 150);
|
|
};
|
|
const observer = new MutationObserver((mutations) => {
|
|
const footerRemoved = mutations.some((mutation) => {
|
|
var _a, _b;
|
|
if (mutation.type !== "childList" || mutation.removedNodes.length === 0) {
|
|
return false;
|
|
}
|
|
for (const node of mutation.removedNodes) {
|
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
if (((_a = node.classList) == null ? void 0 : _a.contains("rich-foot")) || ((_b = node.querySelector) == null ? void 0 : _b.call(node, ".rich-foot[data-rich-foot]"))) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
if (footerRemoved) {
|
|
observerCallback();
|
|
}
|
|
});
|
|
observer.observe(container, {
|
|
childList: true,
|
|
subtree: false
|
|
// Only watch direct children for better performance
|
|
});
|
|
this.observers.set(container, {
|
|
observer,
|
|
cleanup: () => {
|
|
if (timeoutId) clearTimeout(timeoutId);
|
|
if (rafId) cancelAnimationFrame(rafId);
|
|
if (recheckTimeoutId) clearTimeout(recheckTimeoutId);
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Disconnect observer for a container
|
|
* @param {HTMLElement} container - The container
|
|
*/
|
|
disconnectObserver(container) {
|
|
const observerData = this.observers.get(container);
|
|
if (observerData) {
|
|
if (observerData.cleanup) {
|
|
observerData.cleanup();
|
|
}
|
|
if (observerData.observer) {
|
|
observerData.observer.disconnect();
|
|
}
|
|
this.observers.delete(container);
|
|
}
|
|
this.cancelPendingUpdate(container);
|
|
}
|
|
/**
|
|
* Disconnect all observers
|
|
*/
|
|
disconnectAllObservers() {
|
|
this.observers.forEach((observerData, container) => {
|
|
if (observerData.cleanup) {
|
|
observerData.cleanup();
|
|
}
|
|
if (observerData.observer) {
|
|
observerData.observer.disconnect();
|
|
}
|
|
this.cancelPendingUpdate(container);
|
|
});
|
|
this.observers.clear();
|
|
this.pendingUpdates.clear();
|
|
}
|
|
};
|
|
|
|
// src/main.js
|
|
var RICH_FOOT_CSS_VARS = [
|
|
"--rich-foot-border-width",
|
|
"--rich-foot-border-style",
|
|
"--rich-foot-border-opacity",
|
|
"--rich-foot-border-radius",
|
|
"--rich-foot-dates-opacity",
|
|
"--rich-foot-links-opacity",
|
|
"--rich-foot-date-color",
|
|
"--rich-foot-border-color",
|
|
"--rich-foot-link-color",
|
|
"--rich-foot-link-background",
|
|
"--rich-foot-link-border-color",
|
|
"--rich-foot-content-max-width"
|
|
];
|
|
var RichFootPlugin = class extends import_obsidian4.Plugin {
|
|
async onload() {
|
|
await this.loadSettings();
|
|
this.dataManager = new RichFootDataManager(this.app);
|
|
this.renderer = new RichFootRenderer(this);
|
|
this.viewManager = new RichFootViewManager(this);
|
|
this.updateCSSProperties();
|
|
await this.checkVersion();
|
|
this.addSettingTab(new RichFootSettingTab(this.app, this));
|
|
this.debounceTimer = null;
|
|
this.updateRafId = null;
|
|
this.app.workspace.onLayoutReady(() => {
|
|
this.registerWorkspaceEvents();
|
|
this.updateActiveView(true);
|
|
});
|
|
}
|
|
/**
|
|
* Register all workspace events using proper Obsidian registration
|
|
*/
|
|
registerWorkspaceEvents() {
|
|
this.registerEvent(
|
|
this.app.workspace.on("layout-change", () => {
|
|
this.updateActiveView(true);
|
|
})
|
|
);
|
|
this.registerEvent(
|
|
this.app.workspace.on("active-leaf-change", () => {
|
|
const isEditMode = this.isEditMode();
|
|
this.updateActiveView(!isEditMode);
|
|
})
|
|
);
|
|
this.registerEvent(
|
|
this.app.workspace.on("file-open", () => {
|
|
this.updateActiveView(true);
|
|
})
|
|
);
|
|
this.registerEvent(
|
|
this.app.workspace.on("editor-change", () => {
|
|
if (this.debounceTimer) {
|
|
clearTimeout(this.debounceTimer);
|
|
}
|
|
this.debounceTimer = setTimeout(() => {
|
|
this.updateActiveView();
|
|
}, this.settings.updateDelay);
|
|
})
|
|
);
|
|
this.registerEvent(
|
|
this.app.metadataCache.on("changed", (file) => {
|
|
if (this.shouldUpdateForMetadataChange(file)) {
|
|
const isEditMode = this.isEditMode();
|
|
this.updateActiveView(!isEditMode);
|
|
}
|
|
})
|
|
);
|
|
}
|
|
/**
|
|
* Update the active view's footer
|
|
* @param {boolean} immediate - If true, update immediately; otherwise use RAF
|
|
*/
|
|
updateActiveView(immediate = false) {
|
|
if (this.updateRafId) {
|
|
cancelAnimationFrame(this.updateRafId);
|
|
this.updateRafId = null;
|
|
}
|
|
const performUpdate = async () => {
|
|
const views = [];
|
|
this.app.workspace.iterateAllLeaves((leaf) => {
|
|
if (leaf.view instanceof import_obsidian4.MarkdownView && leaf.view.file) {
|
|
views.push(leaf.view);
|
|
}
|
|
});
|
|
for (const view of views) {
|
|
try {
|
|
await this.viewManager.attachToView(view);
|
|
} catch (error) {
|
|
console.error("Rich Foot update error:", error);
|
|
}
|
|
}
|
|
};
|
|
if (immediate) {
|
|
performUpdate();
|
|
} else {
|
|
this.updateRafId = requestAnimationFrame(performUpdate);
|
|
}
|
|
}
|
|
/**
|
|
* Check if should update for metadata change
|
|
* @private
|
|
*/
|
|
shouldUpdateForMetadataChange(file) {
|
|
const activeView = this.app.workspace.getActiveViewOfType(import_obsidian4.MarkdownView);
|
|
if (!activeView || activeView.file !== file) return false;
|
|
const cache = this.app.metadataCache.getFileCache(file);
|
|
if (!(cache == null ? void 0 : cache.frontmatter)) return false;
|
|
const hasCustomCreated = this.settings.customCreatedDateProp && this.settings.customCreatedDateProp in cache.frontmatter;
|
|
const hasCustomModified = this.settings.customModifiedDateProp && this.settings.customModifiedDateProp in cache.frontmatter;
|
|
return hasCustomCreated || hasCustomModified;
|
|
}
|
|
/**
|
|
* Check if current view is in edit mode
|
|
* @private
|
|
*/
|
|
isEditMode() {
|
|
var _a, _b;
|
|
const activeView = this.app.workspace.getActiveViewOfType(import_obsidian4.MarkdownView);
|
|
if (!activeView) return false;
|
|
const mode = (_b = (_a = activeView.getMode) == null ? void 0 : _a.call(activeView)) != null ? _b : activeView.mode;
|
|
return mode === "source";
|
|
}
|
|
/**
|
|
* Update all CSS custom properties
|
|
*/
|
|
updateCSSProperties() {
|
|
const properties = {
|
|
"--rich-foot-border-width": `${this.settings.borderWidth}px`,
|
|
"--rich-foot-border-style": this.settings.borderStyle,
|
|
"--rich-foot-border-opacity": this.settings.borderOpacity,
|
|
"--rich-foot-border-radius": `${this.settings.borderRadius}px`,
|
|
"--rich-foot-dates-opacity": this.settings.datesOpacity,
|
|
"--rich-foot-links-opacity": this.settings.linksOpacity,
|
|
"--rich-foot-date-color": this.settings.dateColor,
|
|
"--rich-foot-border-color": this.settings.borderColor,
|
|
"--rich-foot-link-color": this.settings.linkColor,
|
|
"--rich-foot-link-background": this.settings.linkBackgroundColor,
|
|
"--rich-foot-link-border-color": this.settings.linkBorderColor,
|
|
"--rich-foot-content-max-width": `${this.settings.footerMaxWidth}px`
|
|
};
|
|
Object.entries(properties).forEach(([property, value]) => {
|
|
document.documentElement.style.setProperty(property, value);
|
|
});
|
|
document.body.setAttribute("data-rich-foot-width", this.settings.footerWidth);
|
|
}
|
|
/**
|
|
* Update rich foot (called from settings)
|
|
*/
|
|
async updateRichFoot() {
|
|
this.updateCSSProperties();
|
|
this.updateActiveView(true);
|
|
}
|
|
/**
|
|
* Load plugin settings
|
|
*/
|
|
async loadSettings() {
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
|
if (!Array.isArray(this.settings.excludedFolders)) {
|
|
this.settings.excludedFolders = [];
|
|
}
|
|
if (!Array.isArray(this.settings.excludedParentSelectors)) {
|
|
this.settings.excludedParentSelectors = [];
|
|
}
|
|
}
|
|
/**
|
|
* Save plugin settings
|
|
*/
|
|
async saveSettings() {
|
|
await this.saveData(this.settings);
|
|
}
|
|
/**
|
|
* Check version and show release notes
|
|
*/
|
|
async checkVersion() {
|
|
const currentVersion = this.manifest.version;
|
|
const lastVersion = this.settings.lastVersion;
|
|
const shouldShow = this.settings.showReleaseNotes && (!lastVersion || lastVersion !== currentVersion);
|
|
if (shouldShow) {
|
|
const notes = await this.getReleaseNotes(currentVersion);
|
|
new ReleaseNotesModal(this.app, this, currentVersion, notes).open();
|
|
this.settings.lastVersion = currentVersion;
|
|
await this.saveSettings();
|
|
}
|
|
}
|
|
/**
|
|
* Get release notes
|
|
*/
|
|
async getReleaseNotes(version) {
|
|
return releaseNotes;
|
|
}
|
|
/**
|
|
* Plugin cleanup
|
|
*/
|
|
onunload() {
|
|
if (this.viewManager) {
|
|
this.viewManager.disconnectAllObservers();
|
|
}
|
|
if (this.debounceTimer) {
|
|
clearTimeout(this.debounceTimer);
|
|
this.debounceTimer = null;
|
|
}
|
|
if (this.updateRafId) {
|
|
cancelAnimationFrame(this.updateRafId);
|
|
this.updateRafId = null;
|
|
}
|
|
document.querySelectorAll("[data-rich-foot]").forEach((el) => el.remove());
|
|
RICH_FOOT_CSS_VARS.forEach((property) => {
|
|
document.documentElement.style.removeProperty(property);
|
|
});
|
|
document.body.removeAttribute("data-rich-foot-width");
|
|
}
|
|
};
|
|
var main_default = RichFootPlugin;
|
|
|
|
/* nosourcemap */ |