diff --git a/main.ts b/main.ts index 34daf9c..2bdcda7 100644 --- a/main.ts +++ b/main.ts @@ -7,12 +7,20 @@ import { MarkdownRenderer, AbstractInputSuggest, Component, - TFile + TFile, + getAllTags // Ensure getAllTags is imported } from 'obsidian'; // Interfaces +interface Rule { + type: 'folder' | 'tag'; + path?: string; // For folder type + tag?: string; // For tag type + footerText: string; +} + interface VirtualFooterSettings { - rules: { folderPath: string; footerText: string }[]; + rules: Rule[]; renderLocation: 'footer' | 'header'; } @@ -22,7 +30,7 @@ interface HTMLElementWithComponent extends HTMLElement { // Constants const DEFAULT_SETTINGS: VirtualFooterSettings = { - rules: [{ folderPath: '', footerText: '' }], + rules: [{ type: 'folder', path: '', footerText: '' }], // Default to one empty folder rule renderLocation: 'footer' }; @@ -79,118 +87,70 @@ export class MultiSuggest extends AbstractInputSuggest { /** * Main plugin class for VirtualFooter. * This plugin allows users to define custom footer (or header) content - * that is dynamically injected into Markdown views based on folder-specific rules. + * that is dynamically injected into Markdown views based on folder-specific or tag-specific rules. */ export default class VirtualFooterPlugin extends Plugin { - /** - * Stores the plugin's settings, loaded from and saved to Obsidian's data storage. - * Contains rules for matching folders to footer text and other display preferences. - */ settings: VirtualFooterSettings; - /** - * Called when the plugin is first loaded. - * Initializes the plugin by loading settings, adding a settings tab, - * and registering event listeners for file opening and layout changes - * to dynamically update views with virtual footers/headers. - */ async onload() { await this.loadSettings(); this.addSettingTab(new VirtualFooterSettingTab(this.app, this)); - // Register event listeners to update views when files are opened or layout changes this.registerEvent( this.app.workspace.on('file-open', () => this.handleActiveViewChange()) ); this.registerEvent( this.app.workspace.on('layout-change', () => this.handleActiveViewChange()) ); - this.handleActiveViewChange(); // Initial call to process any already open files + this.handleActiveViewChange(); } - /** - * Called when the plugin is unloaded. - * Performs cleanup tasks, including removing all injected content and styles - * from all Markdown views and cleaning up any globally applied CSS classes - * or dynamically created elements. - */ async onunload() { - this.clearAllViews(); // Clean up all individual views + this.clearAllViews(); - // Global cleanup for any elements potentially missed by view-specific cleanup - // This targets dynamically created content elements. document.querySelectorAll(`.${CSS_DYNAMIC_CONTENT_ELEMENT}`).forEach(el => { const componentHolder = el as HTMLElementWithComponent; if (componentHolder.component) { - componentHolder.component.unload(); // Unload associated Obsidian component + componentHolder.component.unload(); } - el.remove(); // Remove the element from the DOM + el.remove(); }); - // Remove global CSS classes applied for styling document.querySelectorAll(`.${CSS_VIRTUAL_FOOTER_CM_PADDING}`).forEach(el => el.classList.remove(CSS_VIRTUAL_FOOTER_CM_PADDING)); document.querySelectorAll(`.${CSS_VIRTUAL_FOOTER_REMOVE_FLEX}`).forEach(el => el.classList.remove(CSS_VIRTUAL_FOOTER_REMOVE_FLEX)); } - // --- Core View Handling --- - - /** - * Handles changes in the active view (e.g., opening a new file, switching tabs). - * It identifies the active Markdown view and triggers processing for it. - */ handleActiveViewChange() { const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); this._processView(activeView); } - /** - * Processes a given Markdown view to apply or remove virtual footer/header content. - * If the view is invalid or has no file, it clears all views. - * Otherwise, it cleans up existing content/styles from the view, - * applies specific styles for Live Preview footer mode if applicable, - * and then renders and injects new content if the view is in Preview or Live Preview editor mode. - * @param view The MarkdownView to process, or null if no Markdown view is active. - */ private async _processView(view: MarkdownView | null) { if (!view || !view.file) { - this.clearAllViews(); // If no valid view, clear everything + this.clearAllViews(); return; } - // Always clean up the current view before applying new content/styles await this.removeStylesAndInjectedContent(view); const state = view.getState(); const isRenderInHeader = this.settings.renderLocation === 'header'; - // Apply specific styles for Live Preview footer mode - // These styles adjust padding to make space for the footer. - if (state.mode === 'source' && !state.source && !isRenderInHeader) { // Live Preview editor mode, footer rendering + if (state.mode === 'source' && !state.source && !isRenderInHeader) { this.applyLivePreviewFooterStyles(view); } - // Inject content if in Preview mode or Live Preview editor mode if (state.mode === 'preview' || (state.mode === 'source' && !state.source)) { await this.renderAndInjectContent(view); } } - // --- Content Rendering & Injection --- - - /** - * Renders the appropriate footer/header content for the given view and injects it into the DOM. - * It determines the content text based on the file path, prepares the HTML element, - * and then injects it into the correct location (header or footer area) depending on the - * view mode (Preview or Live Preview) and plugin settings. - * Also attaches handlers for internal links within the injected content. - * @param view The MarkdownView where the content will be injected. - */ private async renderAndInjectContent(view: MarkdownView) { const filePath = view.file?.path || ''; const contentText = this.getFooterTextForFile(filePath); if (!contentText) { - await this.removeInjectedContentDOM(view); // Ensure removal if content becomes empty + await this.removeInjectedContentDOM(view); return; } @@ -201,7 +161,6 @@ export default class VirtualFooterPlugin extends Plugin { const state = view.getState(); if (state.mode === 'preview') { - // Target the preview mode's header or footer area const targetParent = view.containerEl.querySelector( isRenderInHeader ? SELECTOR_PREVIEW_HEADER_AREA : SELECTOR_PREVIEW_FOOTER_AREA ); @@ -211,14 +170,12 @@ export default class VirtualFooterPlugin extends Plugin { } } else if (state.mode === 'source' && !state.source) { // Live Preview editor mode if (isRenderInHeader) { - // Target the area before the CodeMirror content container for header in Live Preview const cmContentContainer = view.containerEl.querySelector(SELECTOR_LIVE_PREVIEW_CONTENT_CONTAINER); if (cmContentContainer?.parentElement) { cmContentContainer.parentElement.insertBefore(contentDiv, cmContentContainer); injectionSuccessful = true; } - } else { // Footer in Live Preview editor - // Target the sizer element which is a reliable parent for appending footer content + } else { const targetParent = view.containerEl.querySelector(SELECTOR_EDITOR_SIZER); if (targetParent) { targetParent.appendChild(contentDiv); @@ -228,61 +185,32 @@ export default class VirtualFooterPlugin extends Plugin { } if (injectionSuccessful) { - // If content was successfully injected, attach internal link handlers - this.attachInternalLinkHandlers(contentDiv, filePath, component, view); + this.attachInternalLinkHandlers(contentDiv, filePath, component); } else { - // If injection failed (e.g., target element not found), unload the component to free resources. - // console.warn('VirtualFooterPlugin: Target for injection not found. Content not rendered.'); component.unload(); } } - /** - * Creates and prepares an HTML element to hold the rendered Markdown content. - * It sets up a `div` with appropriate CSS classes, creates an Obsidian `Component` - * for lifecycle management of the rendered content (e.g., internal links), - * and then renders the provided Markdown text into this `div`. - * @param contentText The Markdown string to render. - * @param isRenderInHeader True if the content is for the header, false for the footer. - * @param sourcePath The path of the file for which this content is being rendered, used for Markdown rendering context. - * @returns A promise that resolves to an object containing the created HTMLElement and its associated Component. - */ private async prepareContentElement(contentText: string, isRenderInHeader: boolean, sourcePath: string): Promise<{ element: HTMLElement; component: Component }> { const contentDiv = document.createElement('div'); - contentDiv.className = CSS_DYNAMIC_CONTENT_ELEMENT; // Base class for all dynamic content - contentDiv.classList.add(isRenderInHeader ? CSS_HEADER_RENDERED_CONTENT : CSS_FOOTER_RENDERED_CONTENT); // Specific class for header/footer + contentDiv.className = CSS_DYNAMIC_CONTENT_ELEMENT; + contentDiv.classList.add(isRenderInHeader ? CSS_HEADER_RENDERED_CONTENT : CSS_FOOTER_RENDERED_CONTENT); - // Create a new component to manage the lifecycle of the rendered Markdown. - // This is important for Obsidian's internal link handling and other dynamic features. const component = new Component(); - component.load(); // Load the component - (contentDiv as HTMLElementWithComponent).component = component; // Store component reference on the element + component.load(); + (contentDiv as HTMLElementWithComponent).component = component; - // Render the Markdown content into the div. await MarkdownRenderer.render(this.app, contentText, contentDiv, sourcePath, component); return { element: contentDiv, component }; } - // --- DOM Styling & Cleanup --- - - /** - * Applies specific CSS classes to elements within a MarkdownView when rendering - * a footer in Live Preview mode. These classes adjust padding and layout - * to accommodate the injected footer content. - * @param view The MarkdownView to style. - */ private applyLivePreviewFooterStyles(view: MarkdownView): void { const contentEl = view.containerEl.querySelector(SELECTOR_EDITOR_CONTENT_AREA); const containerEl = view.containerEl.querySelector(SELECTOR_EDITOR_CONTENT_CONTAINER_PARENT); - contentEl?.classList.add(CSS_VIRTUAL_FOOTER_CM_PADDING); // Adds bottom padding to CodeMirror content - containerEl?.classList.add(CSS_VIRTUAL_FOOTER_REMOVE_FLEX); // Modifies flex behavior of parent container + contentEl?.classList.add(CSS_VIRTUAL_FOOTER_CM_PADDING); + containerEl?.classList.add(CSS_VIRTUAL_FOOTER_REMOVE_FLEX); } - /** - * Removes the CSS classes previously applied by `applyLivePreviewFooterStyles`. - * This is used to clean up styles when the footer is removed or the view mode changes. - * @param view The MarkdownView from which to remove styles. - */ private removeLivePreviewFooterStyles(view: MarkdownView): void { const contentEl = view.containerEl.querySelector(SELECTOR_EDITOR_CONTENT_AREA); const containerEl = view.containerEl.querySelector(SELECTOR_EDITOR_CONTENT_CONTAINER_PARENT); @@ -290,40 +218,24 @@ export default class VirtualFooterPlugin extends Plugin { containerEl?.classList.remove(CSS_VIRTUAL_FOOTER_REMOVE_FLEX); } - /** - * Removes all dynamically injected content elements (footers/headers) from a specific view. - * It iterates through known parent selectors where content might be injected, - * finds elements with the `CSS_DYNAMIC_CONTENT_ELEMENT` class, unloads their - * associated Obsidian components, and then removes the elements from the DOM. - * @param view The MarkdownView from which to remove injected content. - */ private async removeInjectedContentDOM(view: MarkdownView) { SELECTORS_POTENTIAL_DYNAMIC_CONTENT_PARENTS.forEach(selector => { const parentEl = view.containerEl.querySelector(selector); parentEl?.querySelectorAll(`.${CSS_DYNAMIC_CONTENT_ELEMENT}`).forEach(el => { const componentHolder = el as HTMLElementWithComponent; if (componentHolder.component) { - componentHolder.component.unload(); // Unload the component to free resources + componentHolder.component.unload(); } - el.remove(); // Remove the element from the DOM + el.remove(); }); }); } - /** - * A utility method to remove both applied Live Preview styles and injected DOM content - * from a given Markdown view. - * @param view The MarkdownView to clean up. - */ private async removeStylesAndInjectedContent(view: MarkdownView) { this.removeLivePreviewFooterStyles(view); await this.removeInjectedContentDOM(view); } - /** - * Clears all injected content and styles from all open Markdown views in the workspace. - * Iterates through all 'markdown' type leaves and calls `removeStylesAndInjectedContent` for each. - */ private clearAllViews(): void { this.app.workspace.getLeavesOfType('markdown').forEach(leaf => { if (leaf.view instanceof MarkdownView) { @@ -332,66 +244,141 @@ export default class VirtualFooterPlugin extends Plugin { }); } - // --- Utility Methods --- - - /** - * Determines the appropriate footer/header text for a given file path based on the - * plugin's configured rules. It finds the rule with the longest matching folder path. - * @param filePath The path of the file to find footer text for. - * @returns The footer/header text string if a matching rule is found, otherwise an empty string. - */ private getFooterTextForFile(filePath: string): string { - let bestMatchPath = ''; - let footerText = ''; - // Iterate through rules to find the most specific match (longest folder path) + let bestMatchPath = ""; // Stores the path string of the best match, "" is least specific + let folderRuleText = ""; + + // Check folder rules first for (const rule of this.settings.rules) { - if (filePath.startsWith(rule.folderPath) && rule.folderPath.length >= bestMatchPath.length) { - bestMatchPath = rule.folderPath; - footerText = rule.footerText; + if (rule.type === 'folder' && rule.path !== undefined) { + let isMatch = false; + let currentRuleSpecificity = -1; + + if (rule.path === "") { // Empty string path rule applies to all files + isMatch = true; + currentRuleSpecificity = 0; // Least specific + } else if (rule.path === "/") { // Root folder path rule + const fileForPath = this.app.vault.getAbstractFileByPath(filePath); + if (fileForPath instanceof TFile && fileForPath.parent && fileForPath.parent.isRoot()) { + isMatch = true; + currentRuleSpecificity = 1; // More specific than "" + } + } else { // Regular folder path (e.g., "Meetings/") + if (filePath.startsWith(rule.path)) { + isMatch = true; + currentRuleSpecificity = rule.path.length; + } + } + + if (isMatch) { + const bestMatchSpecificity = (bestMatchPath === "") ? 0 : (bestMatchPath === "/" ? 1 : bestMatchPath.length); + if (currentRuleSpecificity >= bestMatchSpecificity) { + bestMatchPath = rule.path; + folderRuleText = rule.footerText; + } + } } } - return footerText; + + if (folderRuleText) { + return folderRuleText; // Folder rule takes precedence + } + + // If no folder rule matched, check tag rules + const file = this.app.vault.getAbstractFileByPath(filePath); + if (file instanceof TFile) { + const fileCache = this.app.metadataCache.getFileCache(file); + if (fileCache) { + // Use getAllTags to include frontmatter tags. It returns tags with '#'. + const allTagsInFileWithHash = getAllTags(fileCache); + const fileTags = allTagsInFileWithHash ? allTagsInFileWithHash.map(tag => tag.substring(1)) : []; // Remove leading '#' + + for (const rule of this.settings.rules) { + if (rule.type === 'tag' && rule.tag && fileTags.includes(rule.tag)) { + return rule.footerText; // First matching tag rule + } + } + } + } + + return ''; // No rule matched } - /** - * Attaches click event handlers to internal links (``) - * within the injected content container. This ensures that Obsidian's internal link - * navigation (including opening in new panes with Ctrl/Cmd click) works correctly. - * @param container The HTMLElement containing the rendered Markdown content. - * @param sourcePath The path of the file where the content is displayed, used as context for link resolution. - * @param component The Obsidian Component associated with the rendered content, used to register DOM events. - * @param view The MarkdownView instance, currently unused but kept for potential future use. - */ - private attachInternalLinkHandlers(container: HTMLElement, sourcePath: string, component: Component, view: MarkdownView) { - // Register a DOM event listener on the component for click events within the container. + private attachInternalLinkHandlers(container: HTMLElement, sourcePath: string, component: Component) { + // Handle left-clicks and Ctrl/Meta + left-clicks component.registerDomEvent(container, 'click', (event: MouseEvent) => { + // Ensure it's a left click (button 0) + if (event.button !== 0) { + return; + } + const target = event.target as HTMLElement; - const link = target.closest('a.internal-link') as HTMLAnchorElement; // Find the closest internal link ancestor + const link = target.closest('a.internal-link') as HTMLAnchorElement; if (link) { - event.preventDefault(); // Prevent default link navigation - const href = link.dataset.href; // Get the link destination from data-href attribute + event.preventDefault(); // Prevent default navigation for all captured internal links + const href = link.dataset.href; if (href) { - const newPane = event.ctrlKey || event.metaKey; // Check for Ctrl/Cmd key for new pane - this.app.workspace.openLinkText(href, sourcePath, newPane); // Use Obsidian API to open link + const newPane = event.ctrlKey || event.metaKey; // For left click, new pane is only for Ctrl/Meta + this.app.workspace.openLinkText(href, sourcePath, newPane); + } + } + }); + + // Handle middle-clicks (auxiliary clicks) + component.registerDomEvent(container, 'auxclick', (event: MouseEvent) => { + // Ensure it's a middle click (button 1) + if (event.button !== 1) { + return; + } + + const target = event.target as HTMLElement; + const link = target.closest('a.internal-link') as HTMLAnchorElement; + + if (link) { + event.preventDefault(); // Prevent default middle-click behavior (e.g., autoscroll) + const href = link.dataset.href; + if (href) { + // Middle click always opens in a new pane + this.app.workspace.openLinkText(href, sourcePath, true); } } }); } - // --- Settings Persistence --- - - /** - * Loads plugin settings from Obsidian's data storage. - * Merges stored settings with default settings to ensure all properties are present. - */ async loadSettings() { - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + const loadedSettings = await this.loadData(); + if (loadedSettings && loadedSettings.rules) { + // Migration for rules that don't have a 'type' (from older versions) + loadedSettings.rules = loadedSettings.rules.map((rule: any) => { + if (typeof rule.folderPath === 'string' && typeof rule.type === 'undefined') { + return { + type: 'folder', + path: rule.folderPath, // Use 'path' instead of 'folderPath' + footerText: rule.footerText + }; + } + // Ensure new rules have path/tag initialized if not present + if (rule.type === 'folder' && rule.path === undefined) rule.path = ''; + if (rule.type === 'tag' && rule.tag === undefined) rule.tag = ''; + return rule; + }); + } + this.settings = Object.assign({}, DEFAULT_SETTINGS, loadedSettings); + + // Ensure default rule structure if rules array is empty or missing after loading + if (!this.settings.rules || this.settings.rules.length === 0) { + this.settings.rules = [{ type: 'folder', path: '', footerText: '' }]; + } else { + // Final check to ensure all rules have a type and initialized path/tag + this.settings.rules.forEach(rule => { + if (!rule.type) rule.type = 'folder'; // Default to folder if somehow still missing + if (rule.type === 'folder' && rule.path === undefined) rule.path = ''; + if (rule.type === 'tag' && rule.tag === undefined) rule.tag = ''; + }); + } } - /** - * Saves the current plugin settings to Obsidian's data storage. - */ async saveSettings() { await this.saveData(this.settings); } @@ -400,6 +387,7 @@ export default class VirtualFooterPlugin extends Plugin { class VirtualFooterSettingTab extends PluginSettingTab { plugin: VirtualFooterPlugin; private allFilePaths: Set | null = null; + private allTagsCache: Set | null = null; constructor(app: App, plugin: VirtualFooterPlugin) { super(app, plugin); @@ -409,22 +397,45 @@ class VirtualFooterSettingTab extends PluginSettingTab { private generateAllFilePaths(): Set { if (this.allFilePaths) return this.allFilePaths; - const paths = new Set(['/']); + const paths = new Set(['/']); // Add root path this.plugin.app.vault.getAllLoadedFiles().forEach(file => { if (file instanceof TFile && file.parent) { - paths.add(file.parent.path === '/' ? '/' : file.parent.path + '/'); + const parentPath = file.parent.isRoot() ? '/' : (file.parent.path.endsWith('/') ? file.parent.path : file.parent.path + '/'); + if (parentPath !== '/') paths.add(parentPath); } else if ('children' in file && file.path !== '/') { // TFolder - paths.add(file.path + '/'); + const folderPath = file.path.endsWith('/') ? file.path : file.path + '/'; + paths.add(folderPath); } }); this.allFilePaths = paths; return paths; } + private generateAllTagsInVault(): Set { + if (this.allTagsCache) return this.allTagsCache; + + const collectedTags = new Set(); + const markdownFiles = this.plugin.app.vault.getMarkdownFiles(); + + for (const file of markdownFiles) { + const fileCache = this.plugin.app.metadataCache.getFileCache(file); + if (fileCache) { + const tagsInFile = getAllTags(fileCache); + if (tagsInFile) { + tagsInFile.forEach(tag => { + collectedTags.add(tag.startsWith('#') ? tag.substring(1) : tag); + }); + } + } + } + this.allTagsCache = collectedTags; + return collectedTags; + } display(): void { const { containerEl } = this; containerEl.empty(); - this.allFilePaths = null; // Reset cached paths for re-display + this.allFilePaths = null; + this.allTagsCache = null; new Setting(containerEl) .setName('Render location') @@ -445,6 +456,10 @@ class VirtualFooterSettingTab extends PluginSettingTab { if (!this.plugin.settings.rules) { this.plugin.settings.rules = []; } + if (this.plugin.settings.rules.length === 0) { + this.plugin.settings.rules.push({ type: 'folder', path: '', footerText: '' }); + } + this.plugin.settings.rules.forEach((rule, index) => { this.renderRuleControls(rule, index, rulesContainer); @@ -455,48 +470,90 @@ class VirtualFooterSettingTab extends PluginSettingTab { .setButtonText('Add rule') .setClass('virtual-footer-add-button') .onClick(async () => { - this.plugin.settings.rules.push({ folderPath: '', footerText: '' }); + this.plugin.settings.rules.push({ type: 'folder', path: '', footerText: '' }); await this.plugin.saveSettings(); - this.display(); // Re-render the entire settings tab - this.plugin.handleActiveViewChange(); // Update views + this.display(); + this.plugin.handleActiveViewChange(); })); } - private renderRuleControls(rule: { folderPath: string; footerText: string }, index: number, containerEl: HTMLElement) { + private renderRuleControls(rule: Rule, index: number, containerEl: HTMLElement) { const ruleDiv = containerEl.createDiv(); ruleDiv.addClass('rule'); - + new Setting(ruleDiv) - .setName(`Folder path ${index + 1}`) - .setDesc('Path in the vault. Content will apply to notes in this folder and its subfolders. Use "/" for all notes.') - .addText(text => { - text.setPlaceholder('e.g., Meetings/ or /') - .setValue(rule.folderPath) - .onChange(async (value) => { - this.plugin.settings.rules[index].folderPath = value; - await this.plugin.saveSettings(); - this.plugin.handleActiveViewChange(); - }); - new MultiSuggest(text.inputEl, this.generateAllFilePaths(), (selectedPath) => { - this.plugin.settings.rules[index].folderPath = selectedPath; - this.plugin.saveSettings(); // await not strictly needed if not chaining + .setName(`Rule ${index + 1} type`) + .addDropdown(dropdown => dropdown + .addOption('folder', 'Folder') + .addOption('tag', 'Tag') + .setValue(rule.type) + .onChange(async (value: 'folder' | 'tag') => { + rule.type = value; + if (value === 'folder') { + delete rule.tag; + if (rule.path === undefined) rule.path = ''; + } else { + delete rule.path; + if (rule.tag === undefined) rule.tag = ''; + } + await this.plugin.saveSettings(); + this.display(); this.plugin.handleActiveViewChange(); - text.setValue(selectedPath); // Ensure text input updates - }, this.plugin.app); - }); - + })); + + if (rule.type === 'folder') { + new Setting(ruleDiv) + .setName(`Folder path`) + .setDesc('Path for the rule. Use "" for all files, "/" for root folder files, or "FolderName/" for specific folders and their subfolders.') + .addText(text => { + text.setPlaceholder('e.g., Meetings/, /, or leave empty for all') + .setValue(rule.path || '') + .onChange(async (value) => { + rule.path = value; + await this.plugin.saveSettings(); + this.plugin.handleActiveViewChange(); + }); + new MultiSuggest(text.inputEl, this.generateAllFilePaths(), (selectedPath) => { + rule.path = selectedPath; + this.plugin.saveSettings(); + this.plugin.handleActiveViewChange(); + text.setValue(selectedPath); + }, this.plugin.app); + }); + } else if (rule.type === 'tag') { + new Setting(ruleDiv) + .setName(`Tag value`) + .setDesc('Tag to match (without the #).') + .addText(text => { + text.setPlaceholder('e.g., important or project/alpha') + .setValue(rule.tag || '') + .onChange(async (value) => { + rule.tag = value.startsWith('#') ? value.substring(1) : value; + await this.plugin.saveSettings(); + this.plugin.handleActiveViewChange(); + }); + new MultiSuggest(text.inputEl, this.generateAllTagsInVault(), (selectedTag) => { + const normalizedTag = selectedTag.startsWith('#') ? selectedTag.substring(1) : selectedTag; + rule.tag = normalizedTag; + this.plugin.saveSettings(); + this.plugin.handleActiveViewChange(); + text.setValue(normalizedTag); + }, this.plugin.app); + }); + } + new Setting(ruleDiv) - .setName(`Content text ${index + 1}`) + .setName(`Content text`) .setDesc('Markdown text to display.') .addTextArea(text => text .setPlaceholder('Enter your markdown content here...') .setValue(rule.footerText) .onChange(async (value) => { - this.plugin.settings.rules[index].footerText = value; + rule.footerText = value; await this.plugin.saveSettings(); this.plugin.handleActiveViewChange(); })); - + new Setting(ruleDiv) .addButton(button => button .setButtonText('Delete rule') @@ -504,10 +561,10 @@ class VirtualFooterSettingTab extends PluginSettingTab { .onClick(async () => { this.plugin.settings.rules.splice(index, 1); await this.plugin.saveSettings(); - this.display(); // Re-render the entire settings tab - this.plugin.handleActiveViewChange(); // Update views + this.display(); + this.plugin.handleActiveViewChange(); })); - + ruleDiv.createEl('hr'); } }