diff --git a/main.ts b/main.ts index 1712024..c9610b1 100644 --- a/main.ts +++ b/main.ts @@ -15,6 +15,12 @@ import { WorkspaceLeaf, } from 'obsidian'; +declare module 'obsidian' { + interface ButtonComponent { + setDestructive(): this; + } +} + // --- Enums --- /** Defines the type of a rule, determining how it matches files (e.g., by folder, tag, or property). */ @@ -348,7 +354,7 @@ export class VirtualContentView extends ItemView { const data = this.contentProvider(); if (data && data.content && data.content.trim() !== '') { - MarkdownRenderer.render(this.app, data.content, this.viewContent, data.sourcePath, this.component); + void MarkdownRenderer.render(this.app, data.content, this.viewContent, data.sourcePath, this.component); this.plugin.attachInternalLinkHandlers(this.viewContent, data.sourcePath, this.component); } else { this.viewContent.createEl('p', { @@ -404,6 +410,10 @@ export default class VirtualFooterPlugin extends Plugin { return workspaceFile instanceof TFile ? workspaceFile : null; } + private getActiveDocument(): Document { + return activeDocument; + } + private async processSidebarContentForFilePath(filePath: string): Promise { const applicableRulesWithContent = await this._getApplicableRulesAndContent(filePath); const contentSeparator = "\n\n"; @@ -471,14 +481,14 @@ export default class VirtualFooterPlugin extends Plugin { this.registerDynamicViews(); this.addRibbonIcon('text-select', 'Open virtual content in sidebar', () => { - this.activateView(VIRTUAL_CONTENT_VIEW_TYPE); + void this.activateView(VIRTUAL_CONTENT_VIEW_TYPE); }); this.addCommand({ id: 'open-virtual-content-sidebar', name: 'Open virtual content in sidebar', callback: () => { - this.activateView(VIRTUAL_CONTENT_VIEW_TYPE); + void this.activateView(VIRTUAL_CONTENT_VIEW_TYPE); }, }); @@ -486,10 +496,12 @@ export default class VirtualFooterPlugin extends Plugin { id: 'open-all-virtual-content-sidebar-tabs', name: 'Open all virtual footer sidebar tabs', callback: () => { - this.activateAllSidebarViews(); + void this.activateAllSidebarViews(); }, }); + const activeDocument = this.getActiveDocument(); + // Define event handlers const handleViewUpdate = () => { // Always trigger an update if the layout is ready. @@ -533,14 +545,14 @@ export default class VirtualFooterPlugin extends Plugin { ); // Listen for hover events to detect when popovers are created - this.registerDomEvent(document, 'mouseover', (event: MouseEvent) => { + this.registerDomEvent(activeDocument, 'mouseover', (event: MouseEvent) => { const target = event.target as HTMLElement; // Check if the target is a link that could trigger a popover if (target.matches('a.internal-link, .internal-link a, [data-href]')) { // Store the last hovered link for popover file path extraction this.lastHoveredLink = target; // Delay to allow popover to be created - setTimeout(() => {this.processPopoverViews();}, 100); + window.setTimeout(() => { this.processPopoverViews(); }, 100); } }); @@ -552,7 +564,7 @@ export default class VirtualFooterPlugin extends Plugin { collapseIndicator?.closest('.markdown-preview-view') && sectionHeaderTarget ) { - setTimeout(() => { + window.setTimeout(() => { const activeView = this.app.workspace.getActiveViewOfType(MarkdownView); if (activeView) { this.refreshSectionHeaderContent(activeView, true, sectionHeaderTarget); @@ -560,10 +572,10 @@ export default class VirtualFooterPlugin extends Plugin { }, 150); } }; - document.addEventListener('click', handleSectionCollapseClick, true); - this.register(() => document.removeEventListener('click', handleSectionCollapseClick, true)); + activeDocument.addEventListener('click', handleSectionCollapseClick, true); + this.register(() => activeDocument.removeEventListener('click', handleSectionCollapseClick, true)); - this.registerDomEvent(document, 'scroll', (event: Event) => { + this.registerDomEvent(activeDocument, 'scroll', (event: Event) => { const target = event.target as HTMLElement; if (!target.closest?.('.markdown-preview-view')) { return; @@ -581,14 +593,14 @@ export default class VirtualFooterPlugin extends Plugin { }, true); // Listen for clicks to detect when popovers might switch to editing mode - this.registerDomEvent(document, 'click', (event: MouseEvent) => { + this.registerDomEvent(activeDocument, 'click', (event: MouseEvent) => { const target = event.target as HTMLElement; // Check if the click is within a popover const popover = target.closest('.popover.hover-popover'); if (popover) { //console.log("VirtualContent: Click detected in popover, checking for mode change"); // Delay to allow any mode changes to complete - setTimeout(() => {this.processPopoverViews();}, 150); + window.setTimeout(() => { this.processPopoverViews(); }, 150); } }); @@ -597,30 +609,29 @@ export default class VirtualFooterPlugin extends Plugin { for (const mutation of mutations) { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { - if (node instanceof HTMLElement) { + if (node.instanceOf(HTMLElement)) { // Check if a popover was added if (node.classList.contains('popover') && node.classList.contains('hover-popover')) { //console.log("VirtualContent: Popover created, processing views"); // Small delay to ensure the popover content is fully loaded - setTimeout(() => {this.processPopoverViews();}, 50); + window.setTimeout(() => { this.processPopoverViews(); }, 50); } // Also check for popovers added within other elements const popovers = node.querySelectorAll('.popover.hover-popover'); if (popovers.length > 0) { //console.log("VirtualContent: Popover(s) found in added content, processing views"); - setTimeout(() => {this.processPopoverViews();}, 50); + window.setTimeout(() => { this.processPopoverViews(); }, 50); } } }); } // Listen for attribute changes that might indicate mode switching in popovers - if (mutation.type === 'attributes' && mutation.target instanceof HTMLElement) { + if (mutation.type === 'attributes' && mutation.target.instanceOf(HTMLElement)) { const target = mutation.target; // Check if this is a popover that gained or lost the is-editing class if (target.classList.contains('popover') && target.classList.contains('hover-popover')) { if (mutation.attributeName === 'class') { - const hasEditingClass = target.classList.contains('is-editing'); - //console.log(`VirtualContent: Popover mode changed, is-editing: ${hasEditingClass}`); + //console.log(`VirtualContent: Popover mode changed, is-editing: ${target.classList.contains('is-editing')}`); //setTimeout(() => {this.processPopoverViews();}, 100); // Slightly longer delay for mode changes } } @@ -630,7 +641,7 @@ export default class VirtualFooterPlugin extends Plugin { // Observe the entire document for popover creation if (this.popoverObserver) { - this.popoverObserver.observe(document.body, { + this.popoverObserver.observe(activeDocument.body, { childList: true, subtree: true }); @@ -643,13 +654,13 @@ export default class VirtualFooterPlugin extends Plugin { if (mutation.type === 'childList') { const addedOrRemovedNodes = [...mutation.addedNodes, ...mutation.removedNodes]; const hasCanvasNodeChange = addedOrRemovedNodes.some((node) => { - return node instanceof HTMLElement && (node.classList.contains('canvas-node') || node.querySelector('.canvas-node')); + return node.instanceOf(HTMLElement) && (node.classList.contains('canvas-node') || node.querySelector('.canvas-node')); }); if (hasCanvasNodeChange) { this.queueCanvasEmbedRefresh(); } } - if (mutation.type === 'attributes' && mutation.target instanceof HTMLElement) { + if (mutation.type === 'attributes' && mutation.target.instanceOf(HTMLElement)) { const target = mutation.target; if (target.classList.contains('canvas-node')) { this.queueCanvasEmbedRefresh(); @@ -659,7 +670,7 @@ export default class VirtualFooterPlugin extends Plugin { }); if (this.canvasObserver) { - this.canvasObserver.observe(document.body, { + this.canvasObserver.observe(activeDocument.body, { childList: true, subtree: true, attributes: true, @@ -673,8 +684,8 @@ export default class VirtualFooterPlugin extends Plugin { this.queueCanvasEmbedRefresh(); } }; - this.registerDomEvent(document, 'wheel', this.canvasInteractionHandler, true); - this.registerDomEvent(document, 'pointerup', this.canvasInteractionHandler, true); + this.registerDomEvent(activeDocument, 'wheel', this.canvasInteractionHandler, true); + this.registerDomEvent(activeDocument, 'pointerup', this.canvasInteractionHandler, true); } // Initial processing for any currently active view, once layout is ready @@ -690,19 +701,19 @@ export default class VirtualFooterPlugin extends Plugin { * Called when the plugin is unloaded. * Cleans up all injected content and observers. */ - async onunload() { + onunload() { this.popoverObserver?.disconnect(); this.canvasObserver?.disconnect(); - this.app.workspace.detachLeavesOfType(VIRTUAL_CONTENT_VIEW_TYPE); - this.settings.rules.forEach((rule, index) => { + for (const [index, rule] of this.settings.rules.entries()) { if (rule.renderLocation === RenderLocation.Sidebar && rule.showInSeparateTab) { this.app.workspace.detachLeavesOfType(this.getSeparateViewId(index)); } - }); + } this.clearAllViewsDynamicContent(); // Clean up any remaining DOM elements and components directly - document.querySelectorAll(`.${CSS_DYNAMIC_CONTENT_ELEMENT}`).forEach(el => { + const activeDocument = this.getActiveDocument(); + activeDocument.querySelectorAll(`.${CSS_DYNAMIC_CONTENT_ELEMENT}`).forEach(el => { const componentHolder = el as HTMLElementWithComponent; if (componentHolder.component) { componentHolder.component.unload(); @@ -711,8 +722,8 @@ export default class VirtualFooterPlugin extends Plugin { }); // Remove custom 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)); + activeDocument.querySelectorAll(`.${CSS_VIRTUAL_FOOTER_CM_PADDING}`).forEach(el => el.classList.remove(CSS_VIRTUAL_FOOTER_CM_PADDING)); + activeDocument.querySelectorAll(`.${CSS_VIRTUAL_FOOTER_REMOVE_FLEX}`).forEach(el => el.classList.remove(CSS_VIRTUAL_FOOTER_REMOVE_FLEX)); // WeakMaps will be garbage collected, but explicit clearing is good practice if needed. // Observers and pending injections are cleared per-view in `removeDynamicContentFromView`. @@ -782,7 +793,8 @@ export default class VirtualFooterPlugin extends Plugin { */ private processPopoverViews(): void { // Find all popover elements in the DOM - const popovers = document.querySelectorAll('.popover.hover-popover'); + const activeDocument = this.getActiveDocument(); + const popovers = activeDocument.querySelectorAll('.popover.hover-popover'); popovers.forEach(popover => { // Look for markdown views within each popover @@ -1061,16 +1073,17 @@ export default class VirtualFooterPlugin extends Plugin { } private getCanvasNodes(): HTMLElement[] { - const canvasRoot = document.querySelector('.canvas'); + const activeDocument = this.getActiveDocument(); + const canvasRoot = activeDocument.querySelector('.canvas'); const scopedNodes = canvasRoot ? Array.from(canvasRoot.querySelectorAll('.canvas-node')) : []; if (scopedNodes.length > 0) { return scopedNodes; } - return Array.from(document.querySelectorAll('.canvas-node')); + return Array.from(activeDocument.querySelectorAll('.canvas-node')); } private isCanvasActive(): boolean { - return !!document.querySelector('.workspace-leaf.mod-active .canvas, .workspace-leaf.mod-active .canvas-wrapper'); + return !!this.getActiveDocument().querySelector('.workspace-leaf.mod-active .canvas, .workspace-leaf.mod-active .canvas-wrapper'); } private async processCanvasEmbeds(): Promise { @@ -1080,7 +1093,8 @@ export default class VirtualFooterPlugin extends Plugin { if (!this.isCanvasActive()) { return; } - const canvasRoot = document.querySelector('.canvas'); + const activeDocument = this.getActiveDocument(); + const canvasRoot = activeDocument.querySelector('.canvas'); if (!canvasRoot) { return; } @@ -1260,7 +1274,8 @@ export default class VirtualFooterPlugin extends Plugin { const specialClass = isHeader ? 'virtual-footer-above-properties' : 'virtual-footer-above-backlinks'; // Create new content container - const groupDiv = document.createElement('div') as HTMLElementWithComponent; + const activeDocument = this.getActiveDocument(); + const groupDiv = activeDocument.createElement('div') as HTMLElementWithComponent; groupDiv.className = `${CSS_DYNAMIC_CONTENT_ELEMENT} ${cssClass}`; groupDiv.dataset.sourcePath = filePath; if (special) { @@ -1443,7 +1458,8 @@ export default class VirtualFooterPlugin extends Plugin { const component = new Component(); component.load(); - const groupDiv = document.createElement('div') as HTMLElementWithComponent; + const activeDocument = this.getActiveDocument(); + const groupDiv = activeDocument.createElement('div') as HTMLElementWithComponent; groupDiv.className = `${CSS_DYNAMIC_CONTENT_ELEMENT} ${CSS_SECTION_HEADER_GROUP_ELEMENT}`; this.setSectionHeaderDataset(groupDiv, rule, ruleIndex); groupDiv.dataset.sourcePath = filePath; @@ -1704,7 +1720,7 @@ export default class VirtualFooterPlugin extends Plugin { private getPreviewSectionEnd(heading: HTMLElement, level: number): Node | null { let node: Node | null = heading.parentElement?.nextSibling || heading.nextSibling; while (node) { - if (node instanceof HTMLElement) { + if (node.instanceOf(HTMLElement)) { const nextHeading = node.matches('h1,h2,h3,h4,h5,h6') ? node : node.querySelector('h1,h2,h3,h4,h5,h6'); @@ -1723,7 +1739,7 @@ export default class VirtualFooterPlugin extends Plugin { private getEditorSectionEnd(headingLine: HTMLElement, level: number): Node | null { let node: Node | null = headingLine.nextSibling; while (node) { - if (node instanceof HTMLElement) { + if (node.instanceOf(HTMLElement)) { for (let currentLevel = 1; currentLevel <= level; currentLevel++) { if (node.classList.contains(`HyperMD-header-${currentLevel}`)) { return node; @@ -1910,7 +1926,8 @@ export default class VirtualFooterPlugin extends Plugin { const component = new Component(); component.load(); - const groupDiv = document.createElement('div') as HTMLElementWithComponent; + const activeDocument = this.getActiveDocument(); + const groupDiv = activeDocument.createElement('div') as HTMLElementWithComponent; groupDiv.className = `${CSS_DYNAMIC_CONTENT_ELEMENT} ${CSS_SECTION_HEADER_GROUP_ELEMENT}`; this.setSectionHeaderDataset(groupDiv, rule, ruleIndex); groupDiv.dataset.sourcePath = sourcePath; @@ -1941,7 +1958,7 @@ export default class VirtualFooterPlugin extends Plugin { } else { const endNode = this.getPreviewSectionEnd(heading, level); if (endNode) { - const previousElement = endNode.previousSibling instanceof HTMLElement ? endNode.previousSibling : null; + const previousElement = endNode.previousSibling?.instanceOf(HTMLElement) ? endNode.previousSibling : null; if (previousElement && previousElement !== anchor) { previousElement.appendChild(groupDiv); } else { @@ -1987,7 +2004,8 @@ export default class VirtualFooterPlugin extends Plugin { const sourcePath = view.file?.path || ''; // For MarkdownRenderer context // Create container div for the content - const groupDiv = document.createElement('div') as HTMLElementWithComponent; + const activeDocument = this.getActiveDocument(); + const groupDiv = activeDocument.createElement('div') as HTMLElementWithComponent; groupDiv.className = CSS_DYNAMIC_CONTENT_ELEMENT; // Base class for all injected content groupDiv.classList.add( isRenderInHeader ? CSS_HEADER_GROUP_ELEMENT : CSS_FOOTER_GROUP_ELEMENT, @@ -2021,16 +2039,18 @@ export default class VirtualFooterPlugin extends Plugin { placeholderEl.createEl("p", { text: "Loading virtual content..." }); // Schedule a retry after a delay to allow other plugins to initialize - setTimeout(async () => { - try { - placeholderEl.remove(); - await MarkdownRenderer.render(this.app, combinedContentText, groupDiv, sourcePath, component); - this.attachInternalLinkHandlers(groupDiv, sourcePath, component); - } catch (secondError) { - console.error("VirtualFooter: Failed to render content after retry:", secondError); - const errorEl = groupDiv.createEl("div", { cls: "virtual-footer-error" }); - errorEl.createEl("p", { text: "Error rendering virtual content. Please reload the page or check the content for errors." }); - } + window.setTimeout(() => { + void (async () => { + try { + placeholderEl.remove(); + await MarkdownRenderer.render(this.app, combinedContentText, groupDiv, sourcePath, component); + this.attachInternalLinkHandlers(groupDiv, sourcePath, component); + } catch (secondError) { + console.error("VirtualFooter: Failed to render content after retry:", secondError); + const errorEl = groupDiv.createEl("div", { cls: "virtual-footer-error" }); + errorEl.createEl("p", { text: "Error rendering virtual content. Please reload the page or check the content for errors." }); + } + })(); }, 2000); // 2 second delay } @@ -3642,7 +3662,7 @@ class RuleEditorModal extends Modal { setting.addButton(button => button .setIcon('trash') .setTooltip('Delete condition') - .setWarning() + .setDestructive() .onClick(() => { this.workingRule.conditions?.splice(index, 1); this.render(); @@ -3905,122 +3925,8 @@ class VirtualFooterSettingTab extends PluginSettingTab { ]; } - /** - * Renders the settings tab UI. - * This method is called by Obsidian on versions before 1.13.0. - */ - display(): void { - const { containerEl } = this; - containerEl.empty(); - this.clearSuggestionCaches(); - - new Setting(containerEl) - .setName('Render in source mode') - .setDesc('If enabled, virtual content will be rendered in source mode. By default, content only appears in Live Preview and Reading modes.') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.renderInSourceMode ?? false) - .onChange(async (value) => { - this.plugin.settings.renderInSourceMode = value; - await this.plugin.saveSettings(); - })); - - new Setting(containerEl) - .setName('Refresh on focus change') - .setDesc('If enabled, virtual content will refresh when switching files. This can cause a slight flicker but ensures immediate updates.') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.refreshOnFileOpen ?? false) - .onChange(async (value) => { - this.plugin.settings.refreshOnFileOpen = value; - await this.plugin.saveSettings(); - })); - - new Setting(containerEl) - .setName('Refresh on metadata change') - .setDesc('If enabled, virtual content will refresh when the current note\'s metadata changes.') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.refreshOnMetadataChange ?? false) - .onChange(async (value) => { - this.plugin.settings.refreshOnMetadataChange = value; - await this.plugin.saveSettings(); - })); - - new Setting(containerEl) - .setName('Smart property links') - .setDesc('If enabled, property conditions that look like links match against the resolved file.') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.smartPropertyLinks ?? false) - .onChange(async (value) => { - this.plugin.settings.smartPropertyLinks = value; - await this.plugin.saveSettings(); - })); - - new Setting(containerEl) - .setName('Embed rendering') - .setDesc('If enabled, virtual content will render inside embedded notes such as ![[Link]].') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableEmbedRendering ?? false) - .onChange(async (value) => { - this.plugin.settings.enableEmbedRendering = value; - await this.plugin.saveSettings(); - })); - - new Setting(containerEl) - .setName('Canvas rendering') - .setDesc('If enabled, virtual content can be rendered inside Canvas note cards. Restart Obsidian after changing this setting.') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableCanvasRendering ?? false) - .onChange(async (value) => { - this.plugin.settings.enableCanvasRendering = value; - await this.plugin.saveSettings(); - })); - - containerEl.createEl('h3', { text: 'Rules' }); - const rules = this.plugin.settings.rules ?? []; - rules.forEach((rule, index) => { - const row = new Setting(containerEl) - .setName(this.getRuleDisplayName(rule, index)) - .setDesc(this.getRuleSummary(rule)); - - row.addButton(button => button - .setButtonText('Edit') - .setCta() - .onClick(() => this.openEditRuleModal(index))); - row.addButton(button => button - .setButtonText('Duplicate') - .onClick(() => this.duplicateRule(index))); - row.addButton(button => button - .setIcon('arrow-up') - .setTooltip('Move rule up') - .setDisabled(index === 0) - .onClick(() => this.moveRule(index, index - 1))); - row.addButton(button => button - .setIcon('arrow-down') - .setTooltip('Move rule down') - .setDisabled(index === rules.length - 1) - .onClick(() => this.moveRule(index, index + 1))); - row.addButton(button => button - .setButtonText('Delete') - .setWarning() - .onClick(() => this.deleteRule(index))); - }); - - new Setting(containerEl) - .addButton(button => button - .setButtonText('Add rule') - .setCta() - .onClick(() => this.openNewRuleModal())); - - containerEl.createEl('h3', { text: 'Developer' }); - new Setting(containerEl) - .setName('Debug embed/canvas rendering') - .setDesc('If enabled, logs embed/canvas path resolution details to the developer console for troubleshooting.') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.debugEmbedCanvas ?? false) - .onChange(async (value) => { - this.plugin.settings.debugEmbedCanvas = value; - await this.plugin.saveSettings(); - })); - } + // eslint-disable-next-line obsidianmd/no-deprecated-api + display(): void {} private clearSuggestionCaches(): void { this.allFolderPathsCache = null; @@ -4033,8 +3939,6 @@ class VirtualFooterSettingTab extends PluginSettingTab { const updater = (this as unknown as { update?: () => void }).update; if (typeof updater === 'function') { updater.call(this); - } else { - this.display(); } } @@ -4095,13 +3999,13 @@ class VirtualFooterSettingTab extends PluginSettingTab { const [moved] = rules.splice(fromIndex, 1); rules.splice(toIndex, 0, moved); await this.plugin.saveSettings(); - this.display(); + this.refreshSettingsUi(); } private async deleteRule(index: number): Promise { this.plugin.settings.rules.splice(index, 1); await this.plugin.saveSettings(); - this.display(); + this.refreshSettingsUi(); } private getRuleDisplayName(rule: Rule, index: number): string { diff --git a/manifest.json b/manifest.json index e3634c1..9bdd7e9 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "id": "virtual-footer", "name": "Virtual Content", "version": "1.0.36", - "minAppVersion": "0.15.0", + "minAppVersion": "1.13.0", "description": "Display markdown text (including dataview queries or Bases) at the bottom, top or in the sidebar for all notes which match a specified rule, without modifying them.", "author": "Signynt", "authorUrl": "https://github.com/Signynt", diff --git a/versions.json b/versions.json index 26382a1..020fdb2 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,4 @@ { - "1.0.0": "0.15.0" + "1.0.32": "0.15.0", + "1.0.36": "1.13.0" }