mirror of
https://github.com/youfoundjk/TeXcore.git
synced 2026-07-22 07:33:31 +00:00
feat: Quick Preview external dependency removed
- Added new modules for Quick Preview: - `hoverParent.ts`, `patcher.ts`, `popoverManager.ts`, `types.ts`, and `utils.ts`. - Updated `declarations.d.ts` with reusable `Suggestions` interface for type safety. - Removed `obsidian-quick-preview` dependency from `main.ts`. - Integrated internal Quick Preview patcher for `LinkAutocomplete` and `MathSearchModal`. - Improved item normalization for precise linking via block IDs. - Enhanced modularity and user experience with better hover popover management.
This commit is contained in:
parent
230193aa1d
commit
06ffb81f65
7 changed files with 289 additions and 29 deletions
37
src/declarations.d.ts
vendored
37
src/declarations.d.ts
vendored
|
|
@ -1,7 +1,17 @@
|
|||
import { PaneType, SplitDirection } from "obsidian";
|
||||
import { PaneType, SplitDirection, UserEvent } from "obsidian";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
|
||||
declare module "obsidian" {
|
||||
// 1. DEFINE the complete Suggestions interface here
|
||||
interface Suggestions<T> {
|
||||
selectedItem: number;
|
||||
values: T[];
|
||||
containerEl: HTMLElement;
|
||||
moveUp(event: KeyboardEvent): void;
|
||||
moveDown(event: KeyboardEvent): void;
|
||||
setSelectedItem(index: number, event: UserEvent | null): void;
|
||||
}
|
||||
|
||||
interface App {
|
||||
plugins: {
|
||||
enabledPlugins: Set<string>;
|
||||
|
|
@ -10,33 +20,24 @@ declare module "obsidian" {
|
|||
};
|
||||
getPlugin: (id: string) => Plugin | null;
|
||||
};
|
||||
internalPlugins: {
|
||||
getPluginById(id: string): Plugin & { instance: any };
|
||||
};
|
||||
}
|
||||
interface Editor {
|
||||
cm?: EditorView;
|
||||
}
|
||||
// Reference: https://github.com/tadashi-aikawa/obsidian-various-complements-plugin/blob/be4a12c3f861c31f2be3c0f81809cfc5ab6bb5fd/src/ui/AutoCompleteSuggest.ts#L595-L619
|
||||
|
||||
interface EditorSuggest<T> {
|
||||
scope: Scope;
|
||||
suggestions: {
|
||||
selectedItem: number;
|
||||
values: T[];
|
||||
containerEl: HTMLElement;
|
||||
};
|
||||
// 2. USE the complete interface
|
||||
suggestions: Suggestions<T>;
|
||||
suggestEl: HTMLElement;
|
||||
}
|
||||
|
||||
// Reference: https://github.com/tadashi-aikawa/obsidian-another-quick-switcher/blob/6aa40a46fe817d25c11847a46ec6c765c742d629/src/ui/UnsafeModalInterface.ts#L5
|
||||
interface SuggestModal<T> {
|
||||
chooser: {
|
||||
values: T[] | null;
|
||||
selectedItem: number;
|
||||
setSelectedItem(
|
||||
item: number,
|
||||
event?: KeyboardEvent,
|
||||
): void;
|
||||
useSelectedItem(ev: Partial<KeyboardEvent>): void;
|
||||
suggestions: Element[];
|
||||
};
|
||||
// 3. USE the complete interface here as well
|
||||
chooser: Suggestions<T>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
44
src/features/quick-preview/hoverParent.ts
Normal file
44
src/features/quick-preview/hoverParent.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { HoverParent, HoverPopover } from "obsidian";
|
||||
|
||||
import { PopoverManager } from "./popoverManager";
|
||||
import { PatchedSuggester } from "./types";
|
||||
|
||||
|
||||
export class QuickPreviewHoverParent<T> implements HoverParent {
|
||||
#hoverPopover: HoverPopover | null = null;
|
||||
hidden: boolean;
|
||||
manager: PopoverManager<T>;
|
||||
|
||||
constructor(private suggest: PatchedSuggester<T>) {
|
||||
this.hidden = false;
|
||||
this.manager = this.suggest.popoverManager;
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.hoverPopover?.hide();
|
||||
this.hidden = true;
|
||||
if (this.manager.currentOpenHoverParent === this) {
|
||||
this.manager.currentOpenHoverParent = null;
|
||||
}
|
||||
}
|
||||
|
||||
get hoverPopover() {
|
||||
return this.#hoverPopover;
|
||||
}
|
||||
|
||||
set hoverPopover(hoverPopover: HoverPopover | null) {
|
||||
this.#hoverPopover = hoverPopover;
|
||||
if (this.#hoverPopover) {
|
||||
this.manager.addChild(this.#hoverPopover);
|
||||
this.manager.currentOpenHoverParent?.hide();
|
||||
this.manager.currentOpenHoverParent = this;
|
||||
if (this.hidden) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
this.#hoverPopover.hoverEl.addClass('quick-preview');
|
||||
// is requestAnimationFrame necessary here?
|
||||
this.#hoverPopover!.position(this.#hoverPopover!.shownPos = this.manager.getShownPos());
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/features/quick-preview/patcher.ts
Normal file
32
src/features/quick-preview/patcher.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { around } from 'monkey-around';
|
||||
import LatexReferencer from 'main';
|
||||
import { PopoverManager } from './popoverManager';
|
||||
import { PatchedSuggester, PreviewInfo, Suggester } from './types';
|
||||
|
||||
export function patchSuggesterWithQuickPreview<T>(
|
||||
plugin: LatexReferencer,
|
||||
suggesterClass: new (...args: unknown[]) => Suggester<T>,
|
||||
itemNormalizer: (item: T) => PreviewInfo | null
|
||||
) {
|
||||
const uninstaller = around(suggesterClass.prototype, {
|
||||
open(old) {
|
||||
return function (this: PatchedSuggester<T>) {
|
||||
old.call(this);
|
||||
if (!this.popoverManager) {
|
||||
this.popoverManager = new PopoverManager<T>(plugin, this, itemNormalizer);
|
||||
}
|
||||
this.popoverManager.load();
|
||||
}
|
||||
},
|
||||
close(old) {
|
||||
return function (this: PatchedSuggester<T>) {
|
||||
old.call(this);
|
||||
// close() can be called before open() at startup, so we need the optional chaining (?.)
|
||||
this.popoverManager?.unload();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
plugin.register(uninstaller);
|
||||
return uninstaller;
|
||||
}
|
||||
139
src/features/quick-preview/popoverManager.ts
Normal file
139
src/features/quick-preview/popoverManager.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import { Component, Keymap, KeymapEventHandler, PopoverSuggest, SuggestModal, UserEvent, Suggestions } from "obsidian";
|
||||
|
||||
import LatexReferencer from "main";
|
||||
import { QuickPreviewHoverParent } from "./hoverParent";
|
||||
import { getSelectedItem } from "./utils";
|
||||
import { PatchedSuggester, PreviewInfo } from "./types";
|
||||
|
||||
export class PopoverManager<T> extends Component {
|
||||
suggestions: Suggestions<T>;
|
||||
currentHoverParent: QuickPreviewHoverParent<T> | null = null;
|
||||
currentOpenHoverParent: QuickPreviewHoverParent<T> | null = null;
|
||||
lastEvent: MouseEvent | PointerEvent | null = null;
|
||||
handlers: KeymapEventHandler[] = [];
|
||||
popoverHeight: number | null = null;
|
||||
popoverWidth: number | null = null;
|
||||
|
||||
constructor(private plugin: LatexReferencer, public suggest: PatchedSuggester<T>, private itemNormalizer: (item: T) => PreviewInfo | null) {
|
||||
super();
|
||||
|
||||
if (suggest instanceof PopoverSuggest) {
|
||||
this.suggestions = suggest.suggestions;
|
||||
} else {
|
||||
// The 'as any' cast is no longer needed because the types now match perfectly
|
||||
this.suggestions = (suggest as SuggestModal<T>).chooser;
|
||||
}
|
||||
}
|
||||
|
||||
get doc() {
|
||||
return this.suggestions.containerEl.doc;
|
||||
}
|
||||
|
||||
get win() {
|
||||
return this.doc.win;
|
||||
}
|
||||
|
||||
onload() {
|
||||
const modifier = this.plugin.settings.modifierToJump;
|
||||
|
||||
this.registerDomEvent(this.win, 'keydown', (event) => {
|
||||
if (this.suggest.isOpen && Keymap.isModifier(event, modifier)) {
|
||||
if (this.currentOpenHoverParent) this.hide();
|
||||
else {
|
||||
const item = getSelectedItem(this.suggestions);
|
||||
if (item) this.spawnPreview(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.registerDomEvent(this.win, 'keyup', (event: KeyboardEvent) => {
|
||||
// We check for the specific key, not the modifier state, to avoid conflicts.
|
||||
if (event.key === modifier) this.hide();
|
||||
});
|
||||
|
||||
this.registerDomEvent(this.win, 'mousemove', (event: MouseEvent) => {
|
||||
if (!Keymap.isModifier(event, modifier)) this.hide();
|
||||
});
|
||||
|
||||
if (this.suggest instanceof PopoverSuggest) {
|
||||
this.handlers.push(
|
||||
this.suggest.scope.register([modifier], 'ArrowUp', (event) => {
|
||||
this.suggestions.moveUp(event);
|
||||
return false;
|
||||
}),
|
||||
this.suggest.scope.register([modifier], 'ArrowDown', (event) => {
|
||||
this.suggestions.moveDown(event);
|
||||
return false;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onunload() {
|
||||
this.handlers.forEach((handler) => {
|
||||
this.suggest.scope.unregister(handler);
|
||||
});
|
||||
this.handlers.length = 0;
|
||||
|
||||
this.currentHoverParent?.hide();
|
||||
this.currentHoverParent = null;
|
||||
this.currentOpenHoverParent?.hide();
|
||||
this.currentOpenHoverParent = null;
|
||||
this.lastEvent = null;
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.currentHoverParent?.hide();
|
||||
this.currentHoverParent = null;
|
||||
}
|
||||
|
||||
spawnPreview(item: T) {
|
||||
this.hide();
|
||||
this.currentHoverParent = new QuickPreviewHoverParent(this.suggest);
|
||||
const info = this.itemNormalizer(item);
|
||||
|
||||
if (info) {
|
||||
// @ts-ignore - Using internal API
|
||||
const self = this.plugin.app.internalPlugins.getPluginById('page-preview').instance;
|
||||
// @ts-ignore - Using internal API
|
||||
self.onLinkHover(this.currentHoverParent, this.doc.body, info.linktext, info.sourcePath, { scroll: info.line });
|
||||
}
|
||||
}
|
||||
|
||||
getShownPos(): { x: number, y: number } {
|
||||
return this.getShownPosAuto();
|
||||
}
|
||||
|
||||
getShownPosCorner(position: 'Top left' | 'Top right' | 'Bottom left' | 'Bottom right') {
|
||||
if (position === 'Top left') return { x: 0, y: 0 };
|
||||
if (position === 'Top right') return { x: this.win.innerWidth, y: 0 };
|
||||
if (position === 'Bottom left') return { x: 0, y: this.win.innerHeight };
|
||||
return { x: this.win.innerWidth, y: this.win.innerHeight };
|
||||
}
|
||||
|
||||
getShownPosAuto(): { x: number, y: number } {
|
||||
const el = this.suggestions.containerEl;
|
||||
const { top, bottom, left, right, width } = el.getBoundingClientRect();
|
||||
|
||||
const popover = this.currentHoverParent?.hoverPopover;
|
||||
this.popoverWidth = popover?.hoverEl.offsetWidth ?? this.popoverWidth ?? null;
|
||||
|
||||
if (this.popoverWidth) {
|
||||
const offsetX = width * 0.1;
|
||||
if (right - offsetX + this.popoverWidth < this.win.innerWidth) {
|
||||
return { x: right - offsetX, y: top + 20 };
|
||||
}
|
||||
if (left > this.popoverWidth + offsetX) {
|
||||
return { x: left - this.popoverWidth - offsetX, y: top + 20 };
|
||||
}
|
||||
}
|
||||
|
||||
const x = (left + right) * 0.5;
|
||||
const y = (top + bottom) * 0.5;
|
||||
|
||||
if (x >= this.win.innerWidth * 0.6) {
|
||||
return y >= this.win.innerHeight * 0.5 ? this.getShownPosCorner('Top left') : this.getShownPosCorner('Bottom left');
|
||||
}
|
||||
return y >= this.win.innerHeight * 0.5 ? this.getShownPosCorner('Top right') : this.getShownPosCorner('Bottom right');
|
||||
}
|
||||
}
|
||||
32
src/features/quick-preview/types.ts
Normal file
32
src/features/quick-preview/types.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { PopoverSuggest, SuggestModal } from "obsidian";
|
||||
import { PopoverManager } from "./popoverManager";
|
||||
|
||||
export type Suggester<T> = PopoverSuggest<T> | SuggestModal<T>;
|
||||
export type PatchedSuggester<T> = Suggester<T> & { popoverManager: PopoverManager<T> };
|
||||
|
||||
export interface PreviewInfo {
|
||||
linktext: string;
|
||||
sourcePath: string;
|
||||
line?: number;
|
||||
}
|
||||
|
||||
// This module now augments the global obsidian types
|
||||
declare module "obsidian" {
|
||||
interface PopoverSuggest<T> {
|
||||
suggestions: Suggestions<T>;
|
||||
suggestEl: HTMLElement;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
interface SuggestModal<T> {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
interface HoverPopover {
|
||||
parent: HoverParent;
|
||||
targetEl: HTMLElement | null;
|
||||
shownPos: { x: number, y: number } | null;
|
||||
hide(): void;
|
||||
position(pos: { x: number, y: number } | null): void;
|
||||
}
|
||||
}
|
||||
5
src/features/quick-preview/utils.ts
Normal file
5
src/features/quick-preview/utils.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Suggestions } from "obsidian";
|
||||
|
||||
export function getSelectedItem<T>(suggestions: Suggestions<T>): T | undefined {
|
||||
return suggestions.values[suggestions.selectedItem];
|
||||
}
|
||||
29
src/main.ts
29
src/main.ts
|
|
@ -1,7 +1,8 @@
|
|||
import { MarkdownView, Plugin } from 'obsidian';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
|
||||
import { registerQuickPreview } from 'obsidian-quick-preview';
|
||||
// REMOVED: No longer importing from the external plugin
|
||||
// import { registerQuickPreview } from 'obsidian-quick-preview';
|
||||
|
||||
import { PluginSettings, DEFAULT_SETTINGS } from './features/settings/settings';
|
||||
import { MathSettingTab } from "./features/settings/tab";
|
||||
|
|
@ -18,6 +19,9 @@ import { LinkAutocomplete } from 'features/search/editor-suggest';
|
|||
import { MathSearchModal } from 'features/search/modal';
|
||||
import { EquationBlock } from 'types';
|
||||
|
||||
// ADDED: Import our new internal patcher function
|
||||
import { patchSuggesterWithQuickPreview } from 'features/quick-preview/patcher';
|
||||
|
||||
export default class LatexReferencer extends Plugin {
|
||||
settings: PluginSettings;
|
||||
editorExtensions: Extension[];
|
||||
|
|
@ -28,8 +32,6 @@ export default class LatexReferencer extends Plugin {
|
|||
this.internalProviders.push(new LatexLinkProvider(this));
|
||||
this.addSettingTab(new MathSettingTab(this.app, this));
|
||||
|
||||
|
||||
|
||||
// Commands
|
||||
this.addCommand({
|
||||
id: 'insert-display-math',
|
||||
|
|
@ -52,18 +54,23 @@ export default class LatexReferencer extends Plugin {
|
|||
|
||||
// Link autocompletion
|
||||
this.registerEditorSuggest(new LinkAutocomplete(this));
|
||||
|
||||
// REPLACED: The old external plugin logic is gone.
|
||||
// We now call our internal patcher directly.
|
||||
const itemNormalizer = (item: EquationBlock) => ({
|
||||
linktext: item.$file,
|
||||
sourcePath: '',
|
||||
linktext: item.$file + '#^' + item.$blockId, // Use the block ID for more precise linking
|
||||
sourcePath: item.$file,
|
||||
line: item.$position.start,
|
||||
});
|
||||
registerQuickPreview(this.app, this, LinkAutocomplete, itemNormalizer);
|
||||
registerQuickPreview(this.app, this, MathSearchModal, itemNormalizer);
|
||||
|
||||
// Markdown post processors for Reading View
|
||||
this.registerMarkdownPostProcessor(createEquationNumberProcessor(this));
|
||||
this.registerMarkdownPostProcessor(CustomMathLinksProcessor(this));
|
||||
this.app.workspace.onLayoutReady(() => this.forceRerender());
|
||||
patchSuggesterWithQuickPreview(this, LinkAutocomplete, itemNormalizer);
|
||||
patchSuggesterWithQuickPreview(this, MathSearchModal, itemNormalizer);
|
||||
|
||||
|
||||
// Markdown post processors for Reading View
|
||||
this.registerMarkdownPostProcessor(createEquationNumberProcessor(this));
|
||||
this.registerMarkdownPostProcessor(CustomMathLinksProcessor(this));
|
||||
this.app.workspace.onLayoutReady(() => this.forceRerender());
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue