Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188// src/plugin/views/item-view.tsimport { View } from './view'import type { WorkspaceLeaf } from '../workspace'import { setIcon } from '../ui/icon'import type { IconName } from '../obsidian-stubs'
/** * @deprecated Use WorkspaceLeaf from '../workspace' instead. * Kept for backwards compatibility with existing code that imports this type. */export interface WorkspaceLeafLike { id: string app?: unknown}
/** * ItemView is a view that can display content and actions. * * It extends View with: * - contentEl: a content container inside containerEl * - addAction(): method to add toolbar buttons * * Non-markdown file views (image viewers, PDF viewers, unsupported-file * views, and any future typed editors / Patchwork-compatible adapters) * MUST extend `ItemView`, NOT `MarkdownView`. The host dispatcher * (`WorkspaceLeaf.openFile`) assigns `view.file = file` before * `onOpen()`; an `ItemView` subclass that declares `file: TFile | null` * participates in the typed-view host contract automatically. * * **Pattern for file-backed ItemViews:** * ```ts * class MyFileView extends ItemView { * file: TFile | null = null // Set by WorkspaceLeaf.openFile * * async onOpen(): Promise<void> { * if (!this.file) return * // Render content based on this.file * } * } * ``` * * See `ImageView` and `UnsupportedFileView` for reference implementations. * * @see {@link TypedViewHostContract} in * `src/plugin/views/typed-view-host-contract.ts` for the full * invariants list (activeEditor, command availability, editor events, * markdown-only plugin APIs) and the Patchwork-integration surface. * * @public * @since 0.9.7 */export abstract class ItemView extends View { /** * Content container inside containerEl. * @public */ contentEl: HTMLElement
/** * Buttons created by `addAction()`. Two writers exist: * - `addAction()` itself pushes new buttons here (plugin-side). * - `onunload()` clears the array (host-side, lifecycle reset). * * The host (SidebarViewContainer) reads this list and reparents the * buttons into the unified chrome row's actions slot for the active * leaf, so Obsidian-compat code that calls `addAction()` gets visible * buttons in the panel header even though the chrome row is now * host-owned. The host also MutationObserves `containerEl` so late * `addAction()` calls (e.g. inside async `onOpen()`) are caught. * * @internal Reserved for host + addAction() use; external code should * never mutate this array directly. Read at your own risk — * mutations from outside `addAction()` won't fire the host's * reconciliation path. */ _actionElements: HTMLElement[] = []
/** * @public */ constructor(leaf: WorkspaceLeaf) { super(leaf) this.contentEl = document.createElement('div') this.contentEl.className = 'view-content-inner' this.containerEl.appendChild(this.contentEl) }
/** * Add an action button to the view header. * Obsidian-compatible API for adding toolbar buttons to views. * * The button is created and parented under `containerEl` (matching the * Obsidian DOM shape so plugin code that walks the DOM still finds it), * but the host reparents it into the unified chrome row's actions slot * when this view is the active leaf in its panel. See * `SidebarViewContainer` for the reparenting effect. * * @param icon - The icon ID to display (see Obsidian icon documentation) * @param title - The tooltip text for the button * @param callback - Function called when the button is clicked * @returns The created button element * @public */ // eslint-disable-next-line @typescript-eslint/no-explicit-any addAction(icon: IconName, title: string, callback: (evt: MouseEvent) => any): HTMLElement { const button = document.createElement('button') button.className = 'view-action' button.setAttribute('aria-label', title) button.title = title setIcon(button, icon) // Register DOM event with automatic cleanup on unload this.registerDomEvent(button, 'click', callback) // Insert before contentEl so actions appear in the header area // (Obsidian DOM-shape compat — the host moves them out for display). this.containerEl.insertBefore(button, this.contentEl) this._actionElements.push(button) return button }
/** * Clear the action-buttons list on unload. Without this, a plugin that * unload()s and load()s its view again (the Component lifecycle pattern) * would re-register the same view object with stale, detached buttons * in `_actionElements` — the host would dutifully try to reparent dead * DOM nodes whose event handlers have already been torn down by the * `registerDomEvent` cleanup above. * * Subclasses overriding `onunload()` should call `super.onunload()`. */ onunload(): void { this._actionElements.length = 0 super.onunload() }}
/** * Interface for ItemViews that provide a React component. * Views implementing this are rendered inline in the React tree * (preserving React context like LayoutContext) instead of having * their containerEl DOM-mounted. * * @internal This is host-internal infrastructure for Reams built-in views. * Built-in sidebar panels (file-tree, backlinks, toc, tags, frontmatter, * outgoing-links) use this to maintain React context access for hooks like * `useLayout()`. External plugins should use the standard `ItemView.containerEl` * rendering path. This interface is not exported from the Obsidian compat layer * and should not be treated as stable plugin-author API. */export interface ReactItemView { getReactComponent(): import('react').ReactNode /** Host-owned sidebar order; equal values retain workspace insertion order. */ readonly sidebarOrder?: number
/** * Optional header actions slot rendered by SidebarViewContainer in the * unified chrome row, to the right of the panel title. Use this for * compact action buttons that belong with the panel (e.g. file-tree's * "+" button). Returning `null`/`undefined` leaves the slot empty. * * Contract (enforced by convention, not by the host): * * - MUST return synchronously. A thenable is rejected with a console * error; the slot is left empty. * - SHOULD return a stable React element. The host memoizes the result * by view identity, so this is called only when the active leaf * changes — but if it does heavy work, it still runs once per * activation. * - MUST NOT throw. Synchronous throws are caught by the host (the * slot is left empty and the error is logged), but a throw still * counts as a render-path bug. * - Plugins are also asked NOT to render their own panel header inside * the leaf body. The host renders the canonical header strip; a * second one stacked below it produces a ~64px-tall header band. * * @internal */ getHeaderActions?(): import('react').ReactNode}
/** * Type guard for ReactItemView. * * @internal Host-internal helper for SidebarViewContainer's dual-rendering logic. */export function isReactItemView(view: unknown): view is ReactItemView { return view != null && typeof (view as ReactItemView).getReactComponent === 'function'}