mirror of
https://github.com/jheddings/obsidian-stomp.git
synced 2026-07-22 06:44:59 +00:00
Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
262 lines
8 KiB
TypeScript
262 lines
8 KiB
TypeScript
import { App, Notice, MarkdownView, MarkdownPreviewView } from "obsidian";
|
|
import { StompPluginSettings } from "./config";
|
|
import { ScrollEngine } from "./engine";
|
|
import { Logger } from "obskit";
|
|
import {
|
|
PageScrollerDown,
|
|
PageScrollerUp,
|
|
ScrollStopper,
|
|
SectionScrollerNext,
|
|
SectionScrollerPrev,
|
|
AutoScrollerUp,
|
|
AutoScrollerDown,
|
|
ViewScroller,
|
|
ScrollToggler,
|
|
EdgeScrollerUp,
|
|
EdgeScrollerDown,
|
|
} from "./scroller";
|
|
|
|
/**
|
|
* Represents a scroll command definition.
|
|
*/
|
|
export interface ScrollCommand {
|
|
id: string;
|
|
name: string;
|
|
description?: string;
|
|
}
|
|
|
|
/**
|
|
* List of available scroll commands.
|
|
*/
|
|
export const SCROLL_COMMANDS: ScrollCommand[] = [
|
|
{
|
|
id: "stomp-stop-scroll",
|
|
name: "Stop scrolling",
|
|
description: "Stop any active scroll animation",
|
|
},
|
|
// page scroll commands
|
|
{
|
|
id: "stomp-page-scroll-up",
|
|
name: "Scroll page up",
|
|
description: "Scroll up by a percentage of the viewport height",
|
|
},
|
|
{
|
|
id: "stomp-page-scroll-down",
|
|
name: "Scroll page down",
|
|
description: "Scroll down by a percentage of the viewport height",
|
|
},
|
|
{
|
|
id: "stomp-quick-scroll-up",
|
|
name: "Quick scroll up",
|
|
description: "Fast scroll up with full viewport height",
|
|
},
|
|
{
|
|
id: "stomp-quick-scroll-down",
|
|
name: "Quick scroll down",
|
|
description: "Fast scroll down with full viewport height",
|
|
},
|
|
// section scroll commands
|
|
{
|
|
id: "stomp-section-scroll-next",
|
|
name: "Scroll to next section",
|
|
description: "Scroll to the next heading or section element",
|
|
},
|
|
{
|
|
id: "stomp-section-scroll-prev",
|
|
name: "Scroll to previous section",
|
|
description: "Scroll to the previous heading or section element",
|
|
},
|
|
{
|
|
id: "stomp-edge-scroll-top",
|
|
name: "Scroll to top edge section",
|
|
description: "Scroll to the topmost visible section element",
|
|
},
|
|
{
|
|
id: "stomp-edge-scroll-bottom",
|
|
name: "Scroll to bottom edge section",
|
|
description: "Scroll to the bottommost visible section element",
|
|
},
|
|
// auto scroll commands
|
|
{
|
|
id: "stomp-auto-scroll-up",
|
|
name: "Auto scroll up",
|
|
description: "Continuously scroll up at a set speed",
|
|
},
|
|
{
|
|
id: "stomp-auto-scroll-down",
|
|
name: "Auto scroll down",
|
|
description: "Continuously scroll down at a set speed",
|
|
},
|
|
{
|
|
id: "stomp-toggle-auto-scroll-up",
|
|
name: "Toggle auto scroll up",
|
|
description: "Toggles auto scrolling up",
|
|
},
|
|
{
|
|
id: "stomp-toggle-auto-scroll-down",
|
|
name: "Toggle auto scroll down",
|
|
description: "Toggles auto scrolling down",
|
|
},
|
|
];
|
|
|
|
/**
|
|
* Controls scroll actions and strategies for the plugin.
|
|
*/
|
|
export class ScrollController {
|
|
private scrollStrategies: Map<string, ViewScroller> = new Map();
|
|
|
|
private engine: ScrollEngine = new ScrollEngine();
|
|
private logger: Logger = Logger.getLogger("ScrollController");
|
|
|
|
/**
|
|
* Create a new ScrollController instance.
|
|
*/
|
|
constructor(
|
|
private app: App,
|
|
settings: StompPluginSettings
|
|
) {
|
|
this.scrollStrategies.set("stomp-stop-scroll", new ScrollStopper(this.engine));
|
|
|
|
// page scroll strategies
|
|
this.scrollStrategies.set(
|
|
"stomp-page-scroll-up",
|
|
new PageScrollerUp(this.engine, settings.pageScrollSettings)
|
|
);
|
|
this.scrollStrategies.set(
|
|
"stomp-page-scroll-down",
|
|
new PageScrollerDown(this.engine, settings.pageScrollSettings)
|
|
);
|
|
this.scrollStrategies.set(
|
|
"stomp-quick-scroll-up",
|
|
new PageScrollerUp(this.engine, settings.quickScrollSettings)
|
|
);
|
|
this.scrollStrategies.set(
|
|
"stomp-quick-scroll-down",
|
|
new PageScrollerDown(this.engine, settings.quickScrollSettings)
|
|
);
|
|
|
|
// section scroll strategies
|
|
this.scrollStrategies.set(
|
|
"stomp-section-scroll-next",
|
|
new SectionScrollerNext(this.engine, settings.sectionScrollSettings)
|
|
);
|
|
this.scrollStrategies.set(
|
|
"stomp-section-scroll-prev",
|
|
new SectionScrollerPrev(this.engine, settings.sectionScrollSettings)
|
|
);
|
|
this.scrollStrategies.set(
|
|
"stomp-edge-scroll-top",
|
|
new EdgeScrollerUp(this.engine, settings.sectionScrollSettings)
|
|
);
|
|
this.scrollStrategies.set(
|
|
"stomp-edge-scroll-bottom",
|
|
new EdgeScrollerDown(this.engine, settings.sectionScrollSettings)
|
|
);
|
|
|
|
// auto scroll strategies
|
|
const autoScrollUp = new AutoScrollerUp(this.engine, settings.autoScrollSettings);
|
|
this.scrollStrategies.set("stomp-auto-scroll-up", autoScrollUp);
|
|
this.scrollStrategies.set(
|
|
"stomp-toggle-auto-scroll-up",
|
|
new ScrollToggler(this.engine, autoScrollUp)
|
|
);
|
|
|
|
const autoScrollDown = new AutoScrollerDown(this.engine, settings.autoScrollSettings);
|
|
this.scrollStrategies.set("stomp-auto-scroll-down", autoScrollDown);
|
|
this.scrollStrategies.set(
|
|
"stomp-toggle-auto-scroll-down",
|
|
new ScrollToggler(this.engine, autoScrollDown)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Checks if a command ID is valid.
|
|
*/
|
|
isValidCommand(commandId: string): boolean {
|
|
return this.scrollStrategies.has(commandId);
|
|
}
|
|
|
|
/**
|
|
* Executes a scroll command by ID on the active scrollable element.
|
|
*/
|
|
async executeCommand(commandId: string): Promise<void> {
|
|
const strategy = this.scrollStrategies.get(commandId);
|
|
|
|
if (strategy) {
|
|
this.logger.info(`Executing command: ${commandId}`);
|
|
await this.executeScroll(strategy);
|
|
} else {
|
|
this.logger.warn(`Unknown command: ${commandId}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Executes a specific scroll strategy on the active scrollable element.
|
|
*/
|
|
async executeScroll(scroll: ViewScroller): Promise<void> {
|
|
const element = this.getScrollable();
|
|
if (!element) throw new Error("No scrollable element found");
|
|
|
|
this.engine.activate(element);
|
|
|
|
try {
|
|
await scroll.execute(element);
|
|
} catch (error) {
|
|
this.logger.error("Error during scroll:", error);
|
|
new Notice("Scroll error", 2000);
|
|
}
|
|
|
|
this.engine.deactivate();
|
|
}
|
|
|
|
/**
|
|
* Gets the active scrollable element, or null if none found.
|
|
* @returns The scrollable HTMLElement or null.
|
|
*/
|
|
private getScrollable(): HTMLElement | null {
|
|
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
|
|
|
if (!activeView) {
|
|
this.logger.warn("No active view found");
|
|
return null;
|
|
}
|
|
|
|
const mode = activeView.currentMode;
|
|
|
|
if (!(mode instanceof MarkdownPreviewView)) {
|
|
this.logger.warn(`Unsupported mode: ${mode.constructor.name}`);
|
|
return activeView.containerEl;
|
|
}
|
|
|
|
const containerEl = mode.containerEl;
|
|
|
|
const candidates = [
|
|
".markdown-preview-view",
|
|
".markdown-preview-sizer",
|
|
".view-content",
|
|
".markdown-reading-view",
|
|
".cm-scroller",
|
|
];
|
|
|
|
for (const selector of candidates) {
|
|
const element = containerEl.querySelector<HTMLElement>(selector);
|
|
if (element && this.isScrollable(element)) {
|
|
return element;
|
|
}
|
|
}
|
|
|
|
this.logger.warn("No scrollable found; using fallback");
|
|
|
|
return containerEl;
|
|
}
|
|
|
|
/**
|
|
* Determines if an element is scrollable.
|
|
*/
|
|
private isScrollable(element: HTMLElement): boolean {
|
|
const style = activeWindow.getComputedStyle(element);
|
|
const hasScrollableContent = element.scrollHeight > element.clientHeight;
|
|
const hasScrollableStyle = style.overflowY === "scroll" || style.overflowY === "auto";
|
|
return hasScrollableContent && hasScrollableStyle;
|
|
}
|
|
}
|