diff --git a/src/embed-manager.js b/src/embed-manager.js index 192c8f5..91c0141 100644 --- a/src/embed-manager.js +++ b/src/embed-manager.js @@ -1,4 +1,4 @@ -const { Component, WorkspaceLeaf, MarkdownView } = require('obsidian'); +const { Component, WorkspaceLeaf, MarkdownView, setIcon } = require('obsidian'); const ViewportController = require('./viewport-controller'); const DynamicPaths = require('./dynamic-paths'); @@ -230,6 +230,12 @@ class EmbedManager { const placeholder = embedContainer.createDiv('sync-embed-placeholder'); placeholder.setText(`Loading ${placeholderText}...`); + const renderAsCallout = options.callout !== undefined ? options.callout : this.plugin.settings.renderAsCallout; + + if (renderAsCallout) { + embedContainer.addClass('is-callout-style'); + } + // Aggressive lazy loading to prevent scrollbar jumps const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { @@ -268,7 +274,8 @@ class EmbedManager { alias, component, leaf, - customOptions + customOptions, + sourcePath: ctx.sourcePath }; // Register cleanup @@ -323,6 +330,16 @@ class EmbedManager { embedContainer.style.setProperty('--sync-max-height', customOptions.maxHeight); } + // Handle collapse option + if (customOptions.collapse === true) { + embedContainer.addClass('is-collapsed'); + } + + const renderAsCallout = customOptions.callout !== undefined ? customOptions.callout : this.plugin.settings.renderAsCallout; + + // Construct the display title: prefer alias, then "Note > Section", then just Note + const headerTitle = alias || (section ? `${file.basename} > ${section}` : file.basename); + // For section embeds, validate section exists then set up if (section) { const content = view.editor.getValue(); @@ -345,12 +362,12 @@ class EmbedManager { await this.viewportController.setupSectionViewport(embedData); // If there's an alias, show it instead of the actual header - if (alias) { - this.setupAliasDisplay(embedData, alias, true); + if (alias || renderAsCallout) { + this.setupAliasDisplay(embedData, headerTitle, true); } - } else if (alias) { - // For whole note embeds with alias, show alias as title - this.setupAliasDisplay(embedData, alias, false); + } else if (alias || renderAsCallout) { + // For whole note embeds with alias or callout style, show title + this.setupAliasDisplay(embedData, headerTitle, false); } // Handle properties collapse @@ -363,13 +380,17 @@ class EmbedManager { ? customOptions.title : this.plugin.settings.showInlineTitle; - if (!showTitle || section) { + // Hide title if disabled, if it's a section, OR if we're rendering as a callout (since callout has its own title) + if (!showTitle || section || renderAsCallout) { this.hideInlineTitle(embedData); } // Remove placeholder and show actual content - placeholder.remove(); - embedContainer.appendChild(view.containerEl); + // placeholder.remove(); + // embedContainer.appendChild(view.containerEl); + // Using replaceWith instead of remove + appendChild + placeholder.replaceWith(view.containerEl); + embedContainer.removeClass('sync-embed-loading'); ctx.addChild(component); @@ -382,7 +403,10 @@ class EmbedManager { } setupAliasDisplay(embedData, displayAlias, isSection) { - const { view } = embedData; + const { view, file, section } = embedData; + const renderAsCallout = embedData.customOptions.callout !== undefined + ? embedData.customOptions.callout + : this.plugin.settings.renderAsCallout; requestAnimationFrame(() => { setTimeout(() => { @@ -393,22 +417,48 @@ class EmbedManager { } // For section embeds, also hide the section header - if (isSection) { + if (isSection && embedData.sectionInfo) { const cmContent = view.containerEl.querySelector('.cm-content'); if (cmContent) { - if (embedData.sectionInfo) { - const headerLineNumber = embedData.sectionInfo.startLine + 1; - const firstLine = cmContent.querySelector(`.cm-line:nth-child(${headerLineNumber})`); - if (firstLine) { - firstLine.style.display = 'none'; - } + const headerLineNumber = embedData.sectionInfo.startLine + 1; + const firstLine = cmContent.querySelector(`.cm-line:nth-child(${headerLineNumber})`); + if (firstLine) { + firstLine.style.display = 'none'; } } } // Create and insert alias header with consistent styling const aliasHeader = view.containerEl.createDiv('sync-embed-alias-header'); - aliasHeader.textContent = displayAlias; + + if (renderAsCallout) { + aliasHeader.addClass('is-sticky'); + + // Add fold button + const foldBtn = aliasHeader.createDiv('sync-embed-fold'); + setIcon(foldBtn, 'chevron-down'); + + // Create a clickable link + const linkPath = section ? `${file.path}#${section}` : file.path; + aliasHeader.createEl('a', { + cls: 'internal-link', + text: displayAlias, + attr: { + 'href': linkPath, + 'data-href': linkPath + } + }); + + // Handle toggle (only if clicking the header background, not the link) + aliasHeader.addEventListener('click', (e) => { + if (e.target.closest('a')) return; + e.stopPropagation(); + e.preventDefault(); + embedData.containerEl.classList.toggle('is-collapsed'); + }); + } else { + aliasHeader.textContent = displayAlias; + } const viewContent = view.containerEl.querySelector('.view-content'); if (viewContent) { diff --git a/src/main.js b/src/main.js index 5eba910..0d548b0 100644 --- a/src/main.js +++ b/src/main.js @@ -9,6 +9,7 @@ const DEFAULT_SETTINGS = { maxEmbedHeight: 'none', collapsePropertiesByDefault: true, showInlineTitle: true, + renderAsCallout: false, enableCommandInterception: true, gapBetweenEmbeds: '16px', lazyLoadThreshold: '100px', diff --git a/src/settings.js b/src/settings.js index 89eca3b..0ca5d86 100644 --- a/src/settings.js +++ b/src/settings.js @@ -158,6 +158,16 @@ class SyncEmbedsSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); })); + new Setting(containerEl) + .setName('Render as callout') + .setDesc('Render embeds as callouts with sticky headers and collapse functionality') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.renderAsCallout) + .onChange(async (value) => { + this.plugin.settings.renderAsCallout = value; + await this.plugin.saveSettings(); + })); + // === HEADER MANAGEMENT SECTION === containerEl.createEl('h3', { text: 'Header Management' }); @@ -231,6 +241,8 @@ class SyncEmbedsSettingTab extends PluginSettingTab {
![[Note|Alias{height:500px}]] - Custom height for this embed![[Note|Alias{maxHeight:600px}]] - Custom max height![[Note|Alias{title:false}]] - Hide title for this embed![[Note|Alias{collapse:true}]] - Start embed in collapsed state (requires callout style)![[Note|Alias{callout:true}]] - Force callout style for this embed![[Note|Alias{height:400px,title:false}]] - Multiple optionsNote: Options go inside curly braces before the closing ]]
@@ -277,6 +289,7 @@ class SyncEmbedsSettingTab extends PluginSettingTab {|) with dynamic patterns for better display{height:400px,title:false}collapse:true to hide embed content by default (requires callout style)