diff --git a/.eslintrc b/.eslintrc.json
similarity index 52%
rename from .eslintrc
rename to .eslintrc.json
index 0807290..2f79cdf 100644
--- a/.eslintrc
+++ b/.eslintrc.json
@@ -8,16 +8,22 @@
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
- "plugin:@typescript-eslint/recommended"
+ "plugin:@typescript-eslint/recommended-type-checked"
],
"parserOptions": {
- "sourceType": "module"
+ "sourceType": "module",
+ "project": true
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
- "@typescript-eslint/no-empty-function": "off"
+ "@typescript-eslint/no-unsafe-assignment": "off",
+ "@typescript-eslint/no-unsafe-member-access": "off",
+ "@typescript-eslint/no-unsafe-argument": "off",
+ "@typescript-eslint/no-empty-function": "off",
+ "@typescript-eslint/no-floating-promises": "off",
+ "@typescript-eslint/no-misused-promises": "off"
}
}
\ No newline at end of file
diff --git a/README.md b/README.md
index de9a553..1b4adab 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,12 @@
# Image Context Menus
This plugin provides the following context menus for images in [Obsidian](https://obsidian.md/):
-- Copy Image
+- Copy image to clipboard
- Open image in default app
- Show in system explorer
- Reveal file in navigation
+- Open in new tab
+ - also available through middle mouse button click
It also has an `Open PDF externally` context menu for PDFs.
@@ -40,8 +42,9 @@ Contributions are welcome.
Original plugin by [NomarCub](https://github.com/NomarCub).
If you like this plugin you can sponsor me here on GitHub: [![Sponsor NomarCub](https://img.shields.io/static/v1?label=Sponsor%20NomarCub&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/NomarCub), on Ko-fi here: , or on PayPal here: [![Paypal](https://img.shields.io/badge/paypal-nomarcub-yellow?style=social&logo=paypal)](https://paypal.me/nomarcub).
-[Copying](https://github.com/NomarCub/obsidian-copy-url-in-preview/pull/2) [images](https://github.com/NomarCub/obsidian-copy-url-in-preview/pull/3) developed by [luckman212](https://github.com/luckman212).
-[Android image sharing](https://github.com/NomarCub/obsidian-copy-url-in-preview/issues/5) developed by [mnaoumov](https://github.com/mnaoumov).
-[Open PDF externally](https://github.com/NomarCub/obsidian-copy-url-in-preview/issues/9) feature developed by [mnaoumov](https://github.com/mnaoumov).
+- [Open in new tab](https://github.com/NomarCub/obsidian-copy-url-in-preview/pull/37) developed by [waterproofsodium](https://github.com/waterproofsodium)
+- [Copying](https://github.com/NomarCub/obsidian-copy-url-in-preview/pull/2) [images](https://github.com/NomarCub/obsidian-copy-url-in-preview/pull/3) developed by [luckman212](https://github.com/luckman212).
+- [Android image sharing](https://github.com/NomarCub/obsidian-copy-url-in-preview/issues/5) developed by [mnaoumov](https://github.com/mnaoumov).
+- [Open PDF externally](https://github.com/NomarCub/obsidian-copy-url-in-preview/issues/9) feature developed by [mnaoumov](https://github.com/mnaoumov).
Thank you to the makers of the [Tag Wrangler plugin](https://github.com/pjeby/tag-wrangler), as it was a great starting point for working with context menus in Obsidian.
diff --git a/manifest.json b/manifest.json
index bffcdce..2a9a9a0 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,7 +1,7 @@
{
"id": "copy-url-in-preview",
"name": "Image Context Menus",
- "version": "1.6.0",
+ "version": "1.7.0",
"minAppVersion": "1.5.7",
"description": "Copy, open in default app, show in system explorer, reveal in navigation context menu for images. Also Open PDF externally context menu.",
"author": "NomarCub",
diff --git a/package-lock.json b/package-lock.json
index 5c6a5ea..315f183 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "copy-url-in-preview",
- "version": "1.6.0",
+ "version": "1.7.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "copy-url-in-preview",
- "version": "1.6.0",
+ "version": "1.7.0",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",
@@ -16,7 +16,7 @@
"esbuild": "0.17.3",
"eslint": "8.46.0",
"obsidian": "~1.5.7-1",
- "obsidian-typings": "^1.0.6",
+ "obsidian-typings": "^1.1.2",
"tslib": "2.4.0",
"typescript": "4.7.4"
}
@@ -1632,9 +1632,9 @@
}
},
"node_modules/obsidian-typings": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/obsidian-typings/-/obsidian-typings-1.0.6.tgz",
- "integrity": "sha512-pgQeeIa7Lj6qYjNFAewv8ZcguJsTRTXl9e3cboCGTSmvicmyBATH58UNbC+ioUjvm8uBA4sMDUUbTeDpGxRH4Q==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/obsidian-typings/-/obsidian-typings-1.1.2.tgz",
+ "integrity": "sha512-i9a4OI40owbyq4V27B6bQ7+KqizM5zwu15Gw81CGpbCJuonC07W7r72q+czkv93F9zUW9jf2El9Ryyyym+YtpQ==",
"dev": true,
"dependencies": {
"obsidian": "^1.4.11"
diff --git a/package.json b/package.json
index 495ca53..0ad898b 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,11 @@
{
"name": "copy-url-in-preview",
- "version": "1.6.0",
+ "version": "1.7.0",
"description": "Copy Image, Copy URL and Open PDF externally context menu in reading view (formerly preview mode) for Obsidian (https://obsidian.md)",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
+ "lint": "eslint src",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
@@ -19,7 +20,7 @@
"esbuild": "0.17.3",
"eslint": "8.46.0",
"obsidian": "~1.5.7-1",
- "obsidian-typings": "^1.0.6",
+ "obsidian-typings": "^1.1.2",
"tslib": "2.4.0",
"typescript": "4.7.4"
}
diff --git a/src/helpers.ts b/src/helpers.ts
index a33bb7b..f5cdc8e 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -1,4 +1,4 @@
-import { FileSystemAdapter } from "obsidian";
+import { App, FileSystemAdapter, View } from "obsidian";
const loadImageBlobTimeout = 5_000;
@@ -15,60 +15,100 @@ export interface Listener {
}
export function withTimeout(ms: number, promise: Promise): Promise {
- const timeout = new Promise((resolve, reject) => {
+ const timeout = new Promise((_resolve, reject) => {
const id = setTimeout(() => {
clearTimeout(id);
reject(`timed out after ${ms} ms`)
}, ms)
- })
+ }) as unknown as Promise;
return Promise.race([
promise,
timeout
- ]) as Promise
+ ]);
}
// https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image
// option?: https://www.npmjs.com/package/html-to-image
export async function loadImageBlob(imgSrc: string): Promise {
- const loadImageBlobCore = () => {
- return new Promise((resolve, reject) => {
- const image = new Image();
- image.crossOrigin = "anonymous";
- image.onload = () => {
- const canvas = document.createElement("canvas");
- canvas.width = image.width;
- canvas.height = image.height;
- const ctx = canvas.getContext("2d")!;
- ctx.drawImage(image, 0, 0);
- canvas.toBlob((blob: Blob) => {
- resolve(blob);
- });
- };
- image.onerror = async () => {
- try {
- await fetch(image.src, { "mode": "no-cors" });
+ const loadImageBlobCore = () => new Promise((resolve, reject) => {
+ const image = new Image();
+ image.crossOrigin = "anonymous";
+ image.onload = () => {
+ const canvas = document.createElement("canvas");
+ canvas.width = image.width;
+ canvas.height = image.height;
+ const ctx = canvas.getContext("2d")!;
+ ctx.drawImage(image, 0, 0);
+ canvas.toBlob((blob: Blob) => {
+ resolve(blob);
+ });
+ };
+ image.onerror = async () => {
+ try {
+ await fetch(image.src, { "mode": "no-cors" });
- // console.log("possible CORS violation, falling back to allOrigins proxy");
- // https://github.com/gnuns/allOrigins
- const blob = await loadImageBlob(`https://api.allorigins.win/raw?url=${encodeURIComponent(imgSrc)}`);
- resolve(blob);
- } catch {
- reject();
- }
+ // console.log("possible CORS violation, falling back to allOrigins proxy");
+ // https://github.com/gnuns/allOrigins
+ const blob = await loadImageBlob(`https://api.allorigins.win/raw?url=${encodeURIComponent(imgSrc)}`);
+ resolve(blob);
+ } catch {
+ reject();
}
- image.src = imgSrc;
- });
- };
+ }
+ image.src = imgSrc;
+ });
return withTimeout(loadImageBlobTimeout, loadImageBlobCore())
}
export function onElement(
- el: Document,
- event: keyof HTMLElementEventMap,
- selector: string,
+ el: Document, event: keyof HTMLElementEventMap, selector: string,
listener: Listener,
options?: { capture?: boolean; }
) {
el.on(event, selector, listener, options);
return () => el.off(event, selector, listener, options);
}
+
+export function imageElementFromMouseEvent(event: MouseEvent): HTMLImageElement | undefined {
+ const imageElement = event.target;
+ if (!(imageElement instanceof HTMLImageElement)) {
+ console.error("imageElement is supposed to be a HTMLImageElement. imageElement:");
+ console.error(imageElement);
+ }
+ else {
+ return imageElement;
+ }
+}
+
+export function getRelativePath(url: URL, app: App): string | undefined {
+ // getResourcePath("") also works for root path
+ const baseFileUrl = app.vault.adapter.getFilePath("");
+ const basePath = baseFileUrl.replace("file://", "");
+
+ const urlPathName: string = url.pathname;
+ if (urlPathName.startsWith(basePath)) {
+ const relativePath = urlPathName.substring(basePath.length + 1);
+ return decodeURI(relativePath);
+ }
+}
+
+export function openImageFromMouseEvent(event: MouseEvent, app: App) {
+ const image = imageElementFromMouseEvent(event);
+ if (!image) return;
+
+ const leaf = app.workspace.getLeaf(true);
+ app.workspace.setActiveLeaf(leaf, { focus: true });
+
+ const relativePath = getRelativePath(new URL(image.currentSrc), app);
+ if (relativePath) {
+ const titleContainerEl = (leaf.view as View & { titleContainerEl: Node }).titleContainerEl;
+ titleContainerEl.empty();
+ titleContainerEl.createEl("div", { text: relativePath })
+ }
+
+ const contentEl = (leaf.view as View & { contentEl: Node }).contentEl;
+ contentEl.empty();
+ const div = contentEl.createEl("div", {});
+ const img = div.appendChild(document.createElement("img"));
+ img.src = image.currentSrc;
+}
\ No newline at end of file
diff --git a/src/main.ts b/src/main.ts
index f1666d7..b23e962 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,7 +1,8 @@
import { Menu, Plugin, Notice, MenuItem, Platform, TFile, MarkdownView } from "obsidian";
import {
- loadImageBlob, onElement,
- ElectronWindow, FileSystemAdapterWithInternalApi
+ loadImageBlob, onElement, openImageFromMouseEvent,
+ ElectronWindow, FileSystemAdapterWithInternalApi,
+ imageElementFromMouseEvent, getRelativePath
} from "./helpers"
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import * as internal from 'obsidian-typings';
@@ -25,7 +26,7 @@ export default class CopyUrlInPreview extends Plugin {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
- this.saveData(this.settings);
+ await this.saveData(this.settings);
}
async onload() {
@@ -33,7 +34,7 @@ export default class CopyUrlInPreview extends Plugin {
this.addSettingTab(new CopyUrlInPreviewSettingTab(this.app, this));
this.registerDocument(document);
this.app.workspace.on("window-open",
- (workspaceWindow, window) => {
+ (_workspaceWindow, window) => {
this.registerDocument(window.document);
});
}
@@ -41,18 +42,14 @@ export default class CopyUrlInPreview extends Plugin {
registerDocument(document: Document) {
this.register(
onElement(
- document,
- "mouseover",
- ".pdf-embed iframe, .pdf-embed div.pdf-container, .workspace-leaf-content[data-type=pdf]",
+ document, "mouseover", ".pdf-embed iframe, .pdf-embed div.pdf-container, .workspace-leaf-content[data-type=pdf]",
this.showOpenPdfMenu.bind(this)
)
)
this.register(
onElement(
- document,
- "mousemove",
- ".pdf-canvas",
+ document, "mousemove", ".pdf-canvas",
this.showOpenPdfMenu.bind(this)
)
)
@@ -60,54 +57,49 @@ export default class CopyUrlInPreview extends Plugin {
if (Platform.isDesktop) {
this.register(
onElement(
- document,
- "contextmenu",
- "img",
- this.onClickImage.bind(this)
+ document, "contextmenu", "img",
+ this.onImageContextMenu.bind(this)
)
- )
+ );
this.register(
onElement(
- document,
- "mouseover",
- ".cm-link, .cm-hmd-internal-link",
+ document, "mouseup", "img",
+ this.onImageMouseUp.bind(this)
+ )
+ );
+
+ this.register(
+ onElement(
+ document, "mouseover", ".cm-link, .cm-hmd-internal-link",
this.storeLastHoveredLinkInEditor.bind(this)
)
);
this.register(
onElement(
- document,
- "mouseover",
- "a.internal-link",
+ document, "mouseover", "a.internal-link",
this.storeLastHoveredLinkInPreview.bind(this)
)
);
} else {
this.register(
onElement(
- document,
- "touchstart",
- "img",
+ document, "touchstart", "img",
this.startWaitingForLongTap.bind(this)
)
);
this.register(
onElement(
- document,
- "touchend",
- "img",
+ document, "touchend", "img",
this.stopWaitingForLongTap.bind(this)
)
);
this.register(
onElement(
- document,
- "touchmove",
- "img",
+ document, "touchmove", "img",
this.stopWaitingForLongTap.bind(this)
)
);
@@ -127,7 +119,7 @@ export default class CopyUrlInPreview extends Plugin {
this.lastHoveredLinkTarget = token.text;
}
- storeLastHoveredLinkInPreview(event: MouseEvent, link: HTMLAnchorElement) {
+ storeLastHoveredLinkInPreview(_event: MouseEvent, link: HTMLAnchorElement) {
this.lastHoveredLinkTarget = link.getAttribute("data-href")!;
}
@@ -158,7 +150,7 @@ export default class CopyUrlInPreview extends Plugin {
pdfLink = pdfLink?.replace(/#page=\d+$/, '');
const currentNotePath = this.app.workspace.getActiveFile()!.path;
- pdfFile = this.app.metadataCache.getFirstLinkpathDest(pdfLink!, currentNotePath!)!;
+ pdfFile = this.app.metadataCache.getFirstLinkpathDest(pdfLink!, currentNotePath)!;
} else {
pdfFile = this.app.workspace.getActiveFile()!;
}
@@ -189,9 +181,7 @@ export default class CopyUrlInPreview extends Plugin {
registerEscapeButton(menu: Menu, document: Document = activeDocument) {
menu.register(
onElement(
- document,
- "keydown",
- "*",
+ document, "keydown", "*",
(e: KeyboardEvent) => {
if (e.key === "Escape") {
e.preventDefault();
@@ -267,17 +257,13 @@ export default class CopyUrlInPreview extends Plugin {
// Positions are not accurate from PointerEvent.
// There's also TouchEvent
// The event has target, path, toEvent (null on Android) for finding the link
- onClickImage(event: MouseEvent) {
- const imgElement = event.target;
- if (!(imgElement instanceof HTMLImageElement)) {
- console.error("imgElement is supposed to be a HTMLImageElement. imgElement:");
- console.error(imgElement);
- return;
- }
+ onImageContextMenu(event: MouseEvent) {
+ const imageElement = imageElementFromMouseEvent(event);
+ if (!imageElement) return;
event.preventDefault();
const menu = new Menu();
- const image = imgElement.currentSrc;
+ const image = imageElement.currentSrc;
const url = new URL(image);
const protocol = url.protocol;
switch (protocol) {
@@ -300,14 +286,15 @@ export default class CopyUrlInPreview extends Plugin {
})
);
if (protocol === "app:" && Platform.isDesktop) {
- // getResourcePath("") also works for root path
- const baseFilePath = this.app.vault.adapter.getFilePath("");
- const baseFilePathName: string = baseFilePath.replace("file://", "");
- const urlPathName: string = url.pathname;
- if (urlPathName.startsWith(baseFilePathName)) {
- let relativePath = urlPathName.replace(baseFilePathName, "");
- relativePath = decodeURI(relativePath);
-
+ const relativePath = getRelativePath(url, this.app);
+ if (relativePath) {
+ menu.addItem((item: MenuItem) => item
+ .setIcon("arrow-up-right")
+ .setTitle("Open in new tab")
+ .onClick(() => {
+ openImageFromMouseEvent(event, this.app);
+ })
+ );
menu.addItem((item: MenuItem) => item
.setIcon("arrow-up-right")
.setTitle("Open in default app")
@@ -324,7 +311,7 @@ export default class CopyUrlInPreview extends Plugin {
.setIcon("folder")
.setTitle("Reveal file in navigation")
.onClick(() => {
- const file = this.app.vault.getFileByPath(relativePath.substring(1));
+ const file = this.app.vault.getFileByPath(relativePath);
if (!file) {
console.warn(`getFileByPath returned null for ${relativePath}`)
return;
@@ -344,4 +331,11 @@ export default class CopyUrlInPreview extends Plugin {
menu.showAtPosition({ x: event.pageX, y: event.pageY });
this.app.workspace.trigger("copy-url-in-preview:contextmenu", menu);
}
+
+ onImageMouseUp(event: MouseEvent) {
+ const middleButtonNumber = 1;
+ if (event.button == middleButtonNumber && this.settings.middleClickNewTab) {
+ openImageFromMouseEvent(event, this.app);
+ }
+ }
}
diff --git a/src/settings.ts b/src/settings.ts
index 56a0c0b..b390142 100644
--- a/src/settings.ts
+++ b/src/settings.ts
@@ -3,10 +3,12 @@ import { App, PluginSettingTab, Setting } from "obsidian";
export interface CopyUrlInPreviewSettings {
pdfMenu: boolean;
+ middleClickNewTab: boolean;
}
export const DEFAULT_SETTINGS: CopyUrlInPreviewSettings = {
- pdfMenu: true
+ pdfMenu: true,
+ middleClickNewTab: true
}
export class CopyUrlInPreviewSettingTab extends PluginSettingTab {
@@ -27,5 +29,13 @@ export class CopyUrlInPreviewSettingTab extends PluginSettingTab {
this.plugin.saveSettings();
})
})
+ new Setting(containerEl)
+ .setName("Middle mouse click on image link to open in new tab")
+ .addToggle((toggle) => {
+ toggle.setValue(this.plugin.settings.middleClickNewTab).onChange((value) => {
+ this.plugin.settings.middleClickNewTab = value;
+ this.plugin.saveSettings();
+ });
+ })
}
}
\ No newline at end of file