Skip to main content

Overview

The MarkdownPreviewView class represents the preview/reading mode for Markdown files in Obsidian. It extends MarkdownRenderer and implements both MarkdownSubView and MarkdownPreviewEvents interfaces, providing a rendered view of Markdown content.

Properties

containerEl

The HTML element containing the rendered preview.
containerEl
HTMLElement
The container element for the preview
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view && view.getMode() === 'preview') {
  const container = view.previewMode.containerEl;
  console.log('Preview container:', container);
}

file

Gets the file being previewed.
file
TFile
The file being rendered in preview mode
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view) {
  const file = view.previewMode.file;
  console.log('Previewing file:', file.path);
}

Methods

get()

Gets the current content of the preview.
returns
string
The current Markdown content being previewed
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view && view.getMode() === 'preview') {
  const content = view.previewMode.get();
  console.log('Preview content:', content);
}

set()

Sets the content of the preview.
data
string
required
The Markdown content to render
clear
boolean
required
Whether to clear the view before setting new content
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view && view.getMode() === 'preview') {
  view.previewMode.set('# New Content\n\nUpdated preview.', true);
}

clear()

Clears the preview content.
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view && view.getMode() === 'preview') {
  view.previewMode.clear();
}

rerender()

Rerenders the preview.
full
boolean
Whether to do a full rerender (optional)
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view && view.getMode() === 'preview') {
  // Partial rerender
  view.previewMode.rerender();
  
  // Full rerender
  view.previewMode.rerender(true);
}

getScroll()

Gets the current scroll position.
returns
number
The current scroll position
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view && view.getMode() === 'preview') {
  const scroll = view.previewMode.getScroll();
  console.log('Scroll position:', scroll);
}

applyScroll()

Applies a scroll position to the preview.
scroll
number
required
The scroll position to apply
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view && view.getMode() === 'preview') {
  // Scroll to top
  view.previewMode.applyScroll(0);
  
  // Scroll to specific position
  view.previewMode.applyScroll(500);
}

Examples

Accessing Preview Mode

const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view) {
  // Check if in preview mode
  if (view.getMode() === 'preview') {
    const previewMode = view.previewMode;
    console.log('Currently in preview mode');
  }
}

Working with Preview Content

const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view && view.getMode() === 'preview') {
  const previewMode = view.previewMode;
  
  // Get current content
  const content = previewMode.get();
  
  // Modify and update
  const modified = content.replace(/TODO/g, 'DONE');
  previewMode.set(modified, true);
}

Manipulating Preview DOM

const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view && view.getMode() === 'preview') {
  const container = view.previewMode.containerEl;
  
  // Find all headings
  const headings = container.querySelectorAll('h1, h2, h3, h4, h5, h6');
  
  headings.forEach((heading, index) => {
    console.log(`Heading ${index + 1}: ${heading.textContent}`);
  });
}

Adding Custom Elements to Preview

const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view && view.getMode() === 'preview') {
  const container = view.previewMode.containerEl;
  
  // Add a custom banner at the top
  const banner = container.createDiv({
    cls: 'custom-banner'
  });
  banner.setText('This is a custom banner');
  banner.style.backgroundColor = 'var(--interactive-accent)';
  banner.style.padding = '10px';
  banner.style.marginBottom = '10px';
  
  // Insert at the beginning
  container.prepend(banner);
}

Saving and Restoring Scroll Position

class ScrollPositionManager {
  private scrollPositions: Map<string, number> = new Map();

  saveScrollPosition(file: TFile, view: MarkdownView) {
    if (view.getMode() === 'preview') {
      const scroll = view.previewMode.getScroll();
      this.scrollPositions.set(file.path, scroll);
    }
  }

  restoreScrollPosition(file: TFile, view: MarkdownView) {
    if (view.getMode() === 'preview') {
      const savedScroll = this.scrollPositions.get(file.path);
      if (savedScroll !== undefined) {
        view.previewMode.applyScroll(savedScroll);
      }
    }
  }
}

// Usage
const manager = new ScrollPositionManager();

// Save before switching files
const activeFile = this.app.workspace.getActiveFile();
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (activeFile && activeView) {
  manager.saveScrollPosition(activeFile, activeView);
}

// Restore after opening a file
this.registerEvent(
  this.app.workspace.on('file-open', (file) => {
    if (file) {
      const view = this.app.workspace.getActiveViewOfType(MarkdownView);
      if (view) {
        // Wait for preview to load
        setTimeout(() => {
          manager.restoreScrollPosition(file, view);
        }, 100);
      }
    }
  })
);

Forcing Preview Rerender

this.addCommand({
  id: 'force-preview-rerender',
  name: 'Force Preview Rerender',
  checkCallback: (checking: boolean) => {
    const view = this.app.workspace.getActiveViewOfType(MarkdownView);
    
    if (view && view.getMode() === 'preview') {
      if (!checking) {
        view.previewMode.rerender(true);
        new Notice('Preview rerendered');
      }
      return true;
    }
    
    return false;
  }
});
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view && view.getMode() === 'preview') {
  const container = view.previewMode.containerEl;
  
  // Find all internal links
  const internalLinks = container.querySelectorAll('a.internal-link');
  
  const links: string[] = [];
  internalLinks.forEach(link => {
    const href = link.getAttribute('data-href');
    if (href) {
      links.push(href);
    }
  });
  
  console.log('Found internal links:', links);
}

Adding Click Handlers in Preview

const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view && view.getMode() === 'preview') {
  const container = view.previewMode.containerEl;
  
  // Add click handler to all code blocks
  const codeBlocks = container.querySelectorAll('pre > code');
  
  codeBlocks.forEach(codeBlock => {
    const pre = codeBlock.parentElement;
    if (pre) {
      pre.style.cursor = 'pointer';
      pre.addEventListener('click', () => {
        const code = codeBlock.textContent || '';
        navigator.clipboard.writeText(code);
        new Notice('Code copied to clipboard!');
      });
    }
  });
}

Monitoring Preview Changes

this.registerEvent(
  this.app.workspace.on('layout-change', () => {
    const view = this.app.workspace.getActiveViewOfType(MarkdownView);
    
    if (view && view.getMode() === 'preview') {
      const previewMode = view.previewMode;
      const file = previewMode.file;
      
      console.log('Preview mode active for:', file.path);
      
      // Perform custom preview modifications
      const container = previewMode.containerEl;
      // ... custom logic here
    }
  })
);

Scroll to Specific Heading

function scrollToHeading(view: MarkdownView, headingText: string) {
  if (view.getMode() !== 'preview') return;
  
  const container = view.previewMode.containerEl;
  const headings = Array.from(
    container.querySelectorAll('h1, h2, h3, h4, h5, h6')
  );
  
  const targetHeading = headings.find(
    h => h.textContent?.includes(headingText)
  );
  
  if (targetHeading instanceof HTMLElement) {
    targetHeading.scrollIntoView({ behavior: 'smooth' });
  }
}

// Usage
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view) {
  scrollToHeading(view, 'Introduction');
}

MarkdownSubView

Interface implemented by both editor and preview modes.
interface MarkdownSubView {
  getScroll(): number;
  applyScroll(scroll: number): void;
  get(): string;
  set(data: string, clear: boolean): void;
}

Tips

Always check if the view is in preview mode using view.getMode() === 'preview' before accessing preview-specific functionality.
The containerEl provides direct access to the rendered HTML. You can query and manipulate it using standard DOM APIs.
Use rerender(true) to force a complete rerender if the preview doesn’t update automatically after changes.
When adding event listeners to preview content, remember to clean them up when the view changes or closes to avoid memory leaks.

Build docs developers (and LLMs) love