Skip to content

Commit

Permalink
feat(web): link click patch
Browse files Browse the repository at this point in the history
  • Loading branch information
aidenlx committed Dec 16, 2023
1 parent 0e8e574 commit c49798c
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 39 deletions.
9 changes: 5 additions & 4 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
},
"devDependencies": {
"@mx/config": "workspace:*",
"@types/eslint": "~8.4.6",
"@types/react": "~17.0.8",
"@types/react-dom": "~17.0.11",
"autoprefixer": "^10.4.16",
"builtin-modules": "^3.3.0",
"cross-env": "^7.0.3",
Expand All @@ -23,13 +26,11 @@
"release-it": "^17.0.1",
"semver": "^7.5.4",
"tailwindcss": "^3.3.6",
"typescript": "~5.1.6",
"@types/react-dom": "~17.0.11",
"@types/react": "~17.0.8",
"@types/eslint": "~8.4.6"
"typescript": "~5.1.6"
},
"dependencies": {
"clsx": "^1.2.1",
"monkey-around": "^2.3.0",
"react": "npm:@preact/compat@~17.1.2",
"react-dom": "npm:@preact/compat@~17.1.2"
}
Expand Down
10 changes: 0 additions & 10 deletions apps/app/src/comp.tsx

This file was deleted.

27 changes: 2 additions & 25 deletions apps/app/src/mx-main.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,8 @@
import "./style.css";
import { App, Modal, Plugin } from "obsidian";
import ReactDOM from "react-dom";
import { render } from "./comp";
import { Plugin } from "obsidian";

export default class MxPlugin extends Plugin {
async onload() {
// This adds a simple command that can be triggered anywhere
this.addCommand({
id: "open-sample-modal-simple",
name: "Open sample modal (simple)",
callback: () => {
new SampleModal(this.app).open();
},
});
}
async onload() {}

onunload() {}
}

class SampleModal extends Modal {
constructor(app: App) {
super(app);
}

onOpen() {
const { contentEl } = this;
contentEl.addClass("mx");
this.onClose = render(contentEl);
}
}
9 changes: 9 additions & 0 deletions apps/app/src/patch/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface LinkEvent {
onExternalLinkClick(url: string, newLeaf: boolean, fallback: () => void): any;
onInternalLinkClick(
linktext: string,
sourcePath: string,
newLeaf: boolean,
fallback: () => void
): any;
}
12 changes: 12 additions & 0 deletions apps/app/src/patch/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { LinkEvent } from "./event";
import type MediaExtended from "@/mx-main";
import patchEditorClick from "./link.editor";
import patchPreviewClick from "./link.preview";

export default function patchClickAction(
this: MediaExtended,
events: Partial<LinkEvent>
) {
patchEditorClick(this, events);
patchPreviewClick(this, events);
}
53 changes: 53 additions & 0 deletions apps/app/src/patch/link.editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import "obsidian";

import type MediaExtended from "@/mx-main";
import { around } from "monkey-around";
import { MarkdownEditView } from "obsidian";
import { getInstancePrototype, getMarkdownViewInstance } from "./utils";
import { LinkEvent } from "./event";

declare module "obsidian" {
interface MarkdownEditView {
triggerClickableToken(
token: { type: string; text: string; start: number; end: number },
newLeaf: boolean
): void;
}
interface MarkdownView {
// for safe access
editMode?: MarkdownEditView;
}
}

export default function patchEditorClick(
plugin: MediaExtended,
{ onExternalLinkClick, onInternalLinkClick }: Partial<LinkEvent>
) {
return getMarkdownViewInstance(plugin).then((view) => {
if (!view.editMode) {
console.error(
"MarkdownView.editMode is not available, cannot patch editor click"
);
return;
}
plugin.register(
around(getInstancePrototype(view.editMode), {
triggerClickableToken: (next) =>
function (this: MarkdownEditView, token, newLeaf, ...args) {
const fallback = () => next.call(this, token, newLeaf, ...args);
if ("internal-link" === token.type && onInternalLinkClick) {
onInternalLinkClick(
token.text,
this.file.path,
newLeaf,
fallback
);
} else if ("external-link" === token.type && onExternalLinkClick) {
onExternalLinkClick(token.text, newLeaf, fallback);
} else fallback();
},
})
);
console.debug("editor click patched");
});
}
107 changes: 107 additions & 0 deletions apps/app/src/patch/link.preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type MediaExtended from "@/mx-main";
import { around } from "monkey-around";
import { Keymap, type PreviewEventHanlder } from "obsidian";
import { MarkdownPreviewRenderer } from "obsidian";
import { LinkEvent } from "./event";
import { getInstancePrototype } from "./utils";

export default function patchPreviewClick(
plugin: MediaExtended,
events: Partial<LinkEvent>
) {
const unloadPatchHook = around(
MarkdownPreviewRenderer as MDPreviewRendererCtor,
{
registerDomEvents: (next) =>
function (this: MarkdownPreviewRenderer, _el, helper, ...args) {
patchPreviewEventHanlder(helper, events, plugin);
unloadPatchHook();
console.debug("preview click patched");
return next.call(this, _el, helper, ...args);
},
}
);
plugin.register(unloadPatchHook);
}

function patchPreviewEventHanlder(
handler: PreviewEventHanlder,
{ onExternalLinkClick, onInternalLinkClick }: Partial<LinkEvent>,
plugin: MediaExtended
) {
plugin.register(
around(getInstancePrototype(handler), {
onExternalLinkClick: (next) =>
function (this: PreviewEventHanlder, evt, target, link, ...args) {
const fallback = () => next.call(this, evt, target, link, ...args);
if (!onExternalLinkClick) return fallback();
evt.preventDefault();
const paneCreateType = Keymap.isModEvent(evt);
onExternalLinkClick(link, paneCreateType !== false, fallback);
},
onInternalLinkClick: (next) =>
function (this: PreviewEventHanlder, evt, target, linktext, ...args) {
const fallback = () =>
next.call(this, evt, target, linktext, ...args);
if (!onInternalLinkClick) return fallback();
evt.preventDefault();
const paneCreateType = Keymap.isModEvent(evt);
const sourcePath = this.info?.file?.path ?? "";
onInternalLinkClick(
linktext,
sourcePath,
paneCreateType !== false,
fallback
);
},
})
);
}

import "obsidian";

declare module "obsidian" {
class PreviewEventHanlder {
app: App;
onInternalLinkDrag(
evt: MouseEvent,
delegateTarget: HTMLElement,
linktext: string
): void;
onInternalLinkClick(
evt: MouseEvent,
delegateTarget: HTMLElement,
linktext: string
): void;
onInternalLinkRightClick(
evt: MouseEvent,
delegateTarget: HTMLElement,
linktext: string
): void;
onExternalLinkClick(
evt: MouseEvent,
delegateTarget: HTMLElement,
href: string
): void;
onInternalLinkMouseover(
evt: MouseEvent,
delegateTarget: HTMLElement,
href: string
): void;
onTagClick(evt: MouseEvent, delegateTarget: HTMLElement, tag: string): void;
info?: MarkdownView | MarkdownFileInfo;
}
}

type MDPreviewRendererCtor = typeof MarkdownPreviewRenderer & {
registerDomEvents(
el: HTMLElement,
helper: PreviewEventHanlder,
isBelongTo: (el: HTMLElement) => boolean
): void;
belongsToMe(
target: HTMLElement,
el: HTMLElement,
isBelongTo: (el: HTMLElement) => boolean
): boolean;
};
44 changes: 44 additions & 0 deletions apps/app/src/patch/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { MarkdownView, Plugin, Workspace } from "obsidian";

export function getMarkdownViewInstance(plugin: Plugin): Promise<MarkdownView> {
const { app } = plugin;
return new Promise((resolve) => {
function tryGetMarkdownView() {
const view = app.workspace.getLeavesOfType("markdown")[0];
if (view) {
resolve(view.view as MarkdownView);
return true;
}
return false;
}
app.workspace.onLayoutReady(() => {
if (tryGetMarkdownView()) return;
const onLayoutChange = () => {
if (tryGetMarkdownView())
app.workspace.off("layout-change", onLayoutChange);
};
app.workspace.on("layout-change", onLayoutChange);
plugin.register(() => app.workspace.off("layout-change", onLayoutChange));
});
});
}

export function getViewPrototype<T>(ctor: T): T {
return (ctor as any).prototype;
}

export function getInstancePrototype<T>(instance: T): T {
return (instance as any).constructor.prototype;
}

declare module "obsidian" {
interface MarkdownPreviewView {
rerender(full?: boolean): void;
}
}

export function reloadMarkdownPreview(workspace: Workspace) {
workspace.getLeavesOfType("markdown").forEach((leaf) => {
(leaf.view as MarkdownView).previewMode?.rerender(true);
});
}
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit c49798c

Please sign in to comment.