mirror of
https://github.com/sbuffkin/hexmaker.git
synced 2026-07-22 14:30:24 +00:00
456 lines
19 KiB
JavaScript
456 lines
19 KiB
JavaScript
import {App, MarkdownView, Plugin, PluginSettingTab, Setting, TFile, WorkspaceWindow, View, Notice} from 'obsidian';
|
|
import { Util, HandleZoomParams } from "./src/util";
|
|
|
|
|
|
interface MouseWheelZoomSettings {
|
|
initialSize: number;
|
|
modifierKey: ModifierKey;
|
|
stepSize: number;
|
|
resizeInCanvas: boolean;
|
|
}
|
|
|
|
enum ModifierKey {
|
|
ALT = "AltLeft",
|
|
CTRL = "ControlLeft",
|
|
SHIFT = "ShiftLeft",
|
|
ALT_RIGHT = "AltRight",
|
|
CTRL_RIGHT = "ControlRight",
|
|
SHIFT_RIGHT = "ShiftRight"
|
|
}
|
|
|
|
const DEFAULT_SETTINGS: MouseWheelZoomSettings = {
|
|
modifierKey: ModifierKey.ALT,
|
|
stepSize: 25,
|
|
initialSize: 500,
|
|
resizeInCanvas: true,
|
|
}
|
|
|
|
const CtrlCanvasConflictWarning = "Warning: Using Ctrl as the modifier key conflicts with default canvas zooming behavior when 'Resize in canvas' is enabled. Consider using another modifier key or disabling 'Resize in canvas'.";
|
|
|
|
|
|
export default class MouseWheelZoomPlugin extends Plugin {
|
|
settings: MouseWheelZoomSettings;
|
|
isKeyHeldDown = false;
|
|
|
|
async onload() {
|
|
await this.loadSettings();
|
|
this.registerEvent(
|
|
this.app.workspace.on("window-open", (newWindow: WorkspaceWindow) => this.registerEvents(newWindow.win))
|
|
);
|
|
this.registerEvents(window);
|
|
|
|
this.addSettingTab(new MouseWheelZoomSettingsTab(this.app, this));
|
|
|
|
console.log("Loaded: Mousewheel image zoom")
|
|
|
|
this.checkExistingUserConflict();
|
|
}
|
|
|
|
checkExistingUserConflict() {
|
|
const noticeShownKey = 'mousewheel-zoom-ctrl-warning-shown'; // Key for localStorage flag
|
|
const isCtrl = this.settings.modifierKey === ModifierKey.CTRL || this.settings.modifierKey === ModifierKey.CTRL_RIGHT;
|
|
|
|
|
|
// Only show the notice if the conflict exists AND the user hasn't dismissed it before (using localStorage flag)
|
|
if (isCtrl && this.settings.resizeInCanvas && !localStorage.getItem(noticeShownKey)) {
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
const titleEl = document.createElement('strong');
|
|
titleEl.textContent = "Mousewheel Image Zoom";
|
|
fragment.appendChild(titleEl);
|
|
|
|
fragment.appendChild(document.createElement('br'));
|
|
|
|
const messageEl = document.createElement('span');
|
|
messageEl.textContent = CtrlCanvasConflictWarning;
|
|
fragment.appendChild(messageEl);
|
|
|
|
fragment.appendChild(document.createElement('br'));
|
|
|
|
const settingsButton = document.createElement('button');
|
|
settingsButton.textContent = "Open Settings";
|
|
settingsButton.style.marginTop = "5px";
|
|
settingsButton.onclick = () => {
|
|
// settings is a private property of the app object, so we need to cast it to any to access it
|
|
// See https://forum.obsidian.md/t/open-settings-for-my-plugin-community-plugin-settings-deeplink/61563/4
|
|
const setting = (this.app as any).setting;
|
|
setting.open();
|
|
setting.openTabById(this.manifest.id);
|
|
};
|
|
fragment.appendChild(settingsButton);
|
|
|
|
|
|
const notice = new Notice(fragment, 0);
|
|
|
|
// Set the flag in localStorage so the notice doesn't appear again
|
|
// unless the user clears their localStorage or the key changes.
|
|
localStorage.setItem(noticeShownKey, 'true');
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* When the config key is released, we enable the scroll again and reset the key held down flag.
|
|
*/
|
|
onConfigKeyUp(currentWindow: Window) {
|
|
this.isKeyHeldDown = false;
|
|
this.enableScroll(currentWindow);
|
|
}
|
|
|
|
onunload(currentWindow: Window = window) {
|
|
// Re-enable the normal scrolling behavior when the plugin unloads
|
|
this.enableScroll(currentWindow);
|
|
}
|
|
|
|
/**
|
|
* Registers image resizing events for the specified window
|
|
* @param currentWindow window in which to register events
|
|
* @private
|
|
*/
|
|
private registerEvents(currentWindow: Window) {
|
|
const doc: Document = currentWindow.document;
|
|
this.registerDomEvent(doc, "keydown", (evt) => {
|
|
if (evt.code === this.settings.modifierKey.toString()) {
|
|
// When canvas mode is enabled we just ignore the keydown event if the canvas is active
|
|
const isActiveViewCanvas = this.app.workspace.getActiveViewOfType(View)?.getViewType() === "canvas";
|
|
if (isActiveViewCanvas && !this.settings.resizeInCanvas) {
|
|
return;
|
|
}
|
|
|
|
this.isKeyHeldDown = true;
|
|
|
|
if (this.settings.modifierKey !== ModifierKey.SHIFT && this.settings.modifierKey !== ModifierKey.SHIFT_RIGHT) { // Ignore shift to allow horizontal scrolling
|
|
// Disable the normal scrolling behavior when the key is held down
|
|
this.disableScroll(currentWindow);
|
|
}
|
|
}
|
|
});
|
|
this.registerDomEvent(doc, "keyup", (evt) => {
|
|
if (evt.code === this.settings.modifierKey.toString()) {
|
|
this.onConfigKeyUp(currentWindow);
|
|
}
|
|
});
|
|
this.registerDomEvent(doc, "wheel", (evt) => {
|
|
if (this.isKeyHeldDown) {
|
|
// When for example using Alt + Tab to switch between windows, the key is still recognized as held down.
|
|
// We check if the key is really held down by checking if the key is still pressed in the event when the
|
|
// wheel event is triggered.
|
|
if (!this.isConfiguredKeyDown(evt)) {
|
|
this.onConfigKeyUp(currentWindow);
|
|
return;
|
|
}
|
|
|
|
const eventTarget = evt.target as Element;
|
|
|
|
const targetIsCanvas: boolean = eventTarget.hasClass("canvas-node-content-blocker")
|
|
const targetIsCanvasNode: boolean = eventTarget.closest(".canvas-node-content") !== null;
|
|
const targetIsImage: boolean = eventTarget.nodeName === "IMG";
|
|
|
|
if (targetIsCanvas || targetIsCanvasNode || targetIsImage) {
|
|
this.disableScroll(currentWindow);
|
|
}
|
|
|
|
if (targetIsCanvas && this.settings.resizeInCanvas){
|
|
// seems we're trying to zoom on some canvas node.
|
|
this.handleZoomForCanvas(evt, eventTarget);
|
|
}
|
|
else if (targetIsCanvasNode) {
|
|
// we trying to resize focused canvas node.
|
|
// i think here can be implementation of zoom images in embded markdown files on canvas.
|
|
}
|
|
else if (targetIsImage) {
|
|
// Handle the zooming of the image
|
|
this.handleZoom(evt, eventTarget);
|
|
}
|
|
}
|
|
});
|
|
this.registerDomEvent(currentWindow, "blur", () => {
|
|
// When the window loses focus, ensure scrolling is re-enabled for this window
|
|
// and reset the key held state defensively, although the keyup should ideally handle it.
|
|
this.isKeyHeldDown = false;
|
|
this.enableScroll(currentWindow);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handles zooming with the mousewheel on canvas node
|
|
* @param evt wheel event
|
|
* @param eventTarget targeted canvas node element
|
|
* @private
|
|
*/
|
|
handleZoomForCanvas(evt: WheelEvent, eventTarget: Element) {
|
|
// get active canvas
|
|
const isCanvas: boolean = this.app.workspace.getActiveViewOfType(View).getViewType() === "canvas";
|
|
if (!isCanvas) {
|
|
throw new Error("Can't find canvas");
|
|
};
|
|
// Unfortunately the current type definitions don't include any canvas functionality...
|
|
const canvas = (this.app.workspace.getActiveViewOfType(View) as any).canvas;
|
|
|
|
// get triggered canvasNode
|
|
const canvasNode =
|
|
Array.from(canvas.nodes.values())
|
|
.find(node => (node as any).contentBlockerEl == eventTarget) as any;
|
|
|
|
// Adjust delta based on the direction of the resize
|
|
let delta = evt.deltaY > 0 ? this.settings.stepSize : this.settings.stepSize * -1;
|
|
|
|
// Calculate new dimensions directly using the delta and aspectRatio
|
|
const aspectRatio = canvasNode.width / canvasNode.height;
|
|
const newWidth = canvasNode.width + delta;
|
|
const newHeight = newWidth / aspectRatio;
|
|
|
|
// Resize the canvas node using the new dimensions
|
|
canvasNode.resize({width: newWidth, height: newHeight});
|
|
}
|
|
|
|
|
|
/**
|
|
* Handles zooming with the mousewheel on an image
|
|
* @param evt wheel event
|
|
* @param eventTarget targeted image element
|
|
* @private
|
|
*/
|
|
private async handleZoom(evt: WheelEvent, eventTarget: Element) {
|
|
const imageUri = eventTarget.attributes.getNamedItem("src").textContent;
|
|
|
|
const activeFile: TFile = await this.getActivePaneWithImage(eventTarget);
|
|
|
|
await this.app.vault.process(activeFile, (fileText) => {
|
|
let frontmatter = "";
|
|
let body = fileText;
|
|
const frontmatterRegex = /^---\s*([\s\S]*?)\s*---\n*/;
|
|
const match = fileText.match(frontmatterRegex);
|
|
|
|
if (match) {
|
|
frontmatter = match[0]; // Keep the full matched frontmatter block including delimiters and trailing newline
|
|
body = fileText.slice(frontmatter.length); // The rest is the body
|
|
}
|
|
|
|
|
|
const zoomParams: HandleZoomParams = this.getZoomParams(imageUri, body, eventTarget);
|
|
|
|
// Perform replacements ONLY on the body
|
|
let modifiedBody = body;
|
|
const sizeMatches = body.match(zoomParams.sizeMatchRegExp);
|
|
|
|
// Element already has a size entry in the body
|
|
if (sizeMatches !== null) {
|
|
const oldSize: number = parseInt(sizeMatches[1]);
|
|
let newSize: number = oldSize;
|
|
if (evt.deltaY < 0) {
|
|
newSize += this.settings.stepSize;
|
|
} else if (evt.deltaY > 0 && newSize > this.settings.stepSize) {
|
|
newSize -= this.settings.stepSize;
|
|
}
|
|
// Replace within the body
|
|
modifiedBody = body.replace(zoomParams.replaceSizeExist.getReplaceFromString(oldSize), zoomParams.replaceSizeExist.getReplaceWithString(newSize));
|
|
} else { // Element has no size entry in the body -> give it an initial size
|
|
const initialSize = this.settings.initialSize;
|
|
const image = new Image();
|
|
image.src = imageUri;
|
|
const width = image.naturalWidth || initialSize;
|
|
const minWidth = Math.min(width, initialSize);
|
|
|
|
// Replace within the body
|
|
modifiedBody = body.replace(zoomParams.replaceSizeNotExist.getReplaceFromString(0), zoomParams.replaceSizeNotExist.getReplaceWithString(minWidth));
|
|
}
|
|
|
|
// Combine original frontmatter with the modified body
|
|
return frontmatter + modifiedBody;
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Loop through all panes and get the pane that hosts a markdown file with the image to zoom
|
|
* @param imageElement The HTML Element of the image
|
|
* @private
|
|
*/
|
|
private async getActivePaneWithImage(imageElement: Element): Promise<TFile> {
|
|
return new Promise(((resolve, reject) => {
|
|
this.app.workspace.iterateAllLeaves(leaf => {
|
|
if (leaf.view.containerEl.contains(imageElement) && leaf.view instanceof MarkdownView) {
|
|
resolve(leaf.view.file);
|
|
}
|
|
})
|
|
|
|
reject(new Error("No file belonging to the image found"))
|
|
}))
|
|
}
|
|
|
|
|
|
private getZoomParams(imageUri: string, fileText: string, target: Element) {
|
|
if (imageUri.contains("http")) {
|
|
return Util.getRemoteImageZoomParams(imageUri, fileText)
|
|
} else if (target.classList.value.match("excalidraw-svg.*")) {
|
|
const src = target.attributes.getNamedItem("filesource").textContent;
|
|
// remove ".md" from the end of the src
|
|
const imageName = src.substring(0, src.length - 3);
|
|
// Only get text after "/"
|
|
const imageNameAfterSlash = imageName.substring(imageName.lastIndexOf("/") + 1);
|
|
return Util.getLocalImageZoomParams(imageNameAfterSlash, fileText)
|
|
} else if (imageUri.contains("app://")) {
|
|
const imageName = Util.getLocalImageNameFromUri(imageUri);
|
|
return Util.getLocalImageZoomParams(imageName, fileText)
|
|
} else if (imageUri.contains("data:image/")) { // for image generated by PDF++ extension
|
|
// example: data:image/png;base64,iVB...
|
|
const imageName = Util.getLocalImageNameFromUri(target.parentElement.getAttribute("src"));
|
|
return Util.getLocalImageZoomParams(imageName, fileText)
|
|
}
|
|
|
|
throw new Error("Image is not zoomable")
|
|
}
|
|
|
|
async loadSettings() {
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
|
}
|
|
|
|
async saveSettings() {
|
|
await this.saveData(this.settings);
|
|
}
|
|
|
|
// Utilities to disable and enable scrolling //
|
|
|
|
|
|
preventDefault(ev: WheelEvent) {
|
|
ev.preventDefault();
|
|
}
|
|
|
|
wheelOpt: AddEventListenerOptions = {passive: false, capture: true }
|
|
wheelEvent = 'wheel' as keyof WindowEventMap;
|
|
|
|
/**
|
|
* Disables the normal scroll event
|
|
*/
|
|
disableScroll(currentWindow: Window) {
|
|
currentWindow.addEventListener(this.wheelEvent, this.preventDefault, this.wheelOpt);
|
|
}
|
|
|
|
/**
|
|
* Enables the normal scroll event
|
|
*/
|
|
enableScroll(currentWindow: Window) {
|
|
currentWindow.removeEventListener(this.wheelEvent, this.preventDefault, this.wheelOpt);
|
|
}
|
|
|
|
private isConfiguredKeyDown(evt: WheelEvent): boolean {
|
|
switch (this.settings.modifierKey) {
|
|
case ModifierKey.ALT:
|
|
case ModifierKey.ALT_RIGHT:
|
|
return evt.altKey;
|
|
case ModifierKey.CTRL:
|
|
case ModifierKey.CTRL_RIGHT:
|
|
return evt.ctrlKey;
|
|
case ModifierKey.SHIFT:
|
|
case ModifierKey.SHIFT_RIGHT:
|
|
return evt.shiftKey;
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
class MouseWheelZoomSettingsTab extends PluginSettingTab {
|
|
plugin: MouseWheelZoomPlugin;
|
|
warningEl: HTMLDivElement;
|
|
|
|
constructor(app: App, plugin: MouseWheelZoomPlugin) {
|
|
super(app, plugin);
|
|
this.plugin = plugin;
|
|
}
|
|
|
|
// Helper function to update the warning message
|
|
updateWarningMessage(modifierKey: ModifierKey, resizeInCanvas: boolean): void {
|
|
if (!this.warningEl) return;
|
|
|
|
const isCtrl = modifierKey === ModifierKey.CTRL || modifierKey === ModifierKey.CTRL_RIGHT;
|
|
const conflict = isCtrl && resizeInCanvas;
|
|
|
|
if (conflict) {
|
|
this.warningEl.setText(CtrlCanvasConflictWarning);
|
|
this.warningEl.style.display = 'block';
|
|
this.warningEl.style.color = 'var(--text-warning)';
|
|
this.warningEl.style.marginTop = '10px';
|
|
} else {
|
|
this.warningEl.setText("");
|
|
this.warningEl.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
display(): void {
|
|
let {containerEl} = this;
|
|
|
|
containerEl.empty();
|
|
|
|
containerEl.createEl('h2', {text: 'Settings for mousewheel zoom'});
|
|
|
|
new Setting(containerEl)
|
|
.setName('Trigger Key')
|
|
.setDesc('Key that needs to be pressed down for mousewheel zoom to work.')
|
|
.addDropdown(dropdown => dropdown
|
|
.addOption(ModifierKey.CTRL, "Ctrl")
|
|
.addOption(ModifierKey.ALT, "Alt")
|
|
.addOption(ModifierKey.SHIFT, "Shift")
|
|
.addOption(ModifierKey.CTRL_RIGHT, "Right Ctrl")
|
|
.addOption(ModifierKey.ALT_RIGHT, "Right Alt")
|
|
.addOption(ModifierKey.SHIFT_RIGHT, "Right Shift")
|
|
.setValue(this.plugin.settings.modifierKey)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.modifierKey = value as ModifierKey;
|
|
this.updateWarningMessage(this.plugin.settings.modifierKey , this.plugin.settings.resizeInCanvas);
|
|
await this.plugin.saveSettings()
|
|
})
|
|
);
|
|
|
|
new Setting(containerEl)
|
|
.setName('Step size')
|
|
.setDesc('Step value by which the size of the image should be increased/decreased')
|
|
.addSlider(slider => {
|
|
slider
|
|
.setValue(25)
|
|
.setLimits(0, 100, 1)
|
|
.setDynamicTooltip()
|
|
.setValue(this.plugin.settings.stepSize)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.stepSize = value
|
|
await this.plugin.saveSettings()
|
|
})
|
|
})
|
|
|
|
new Setting(containerEl)
|
|
.setName('Initial Size')
|
|
.setDesc('Initial image size if no size was defined beforehand')
|
|
.addSlider(slider => {
|
|
slider
|
|
.setValue(500)
|
|
.setLimits(0, 1000, 25)
|
|
.setDynamicTooltip()
|
|
.setValue(this.plugin.settings.initialSize)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.initialSize = value
|
|
await this.plugin.saveSettings()
|
|
})
|
|
})
|
|
|
|
new Setting(containerEl)
|
|
.setName('Resize in canvas')
|
|
.setDesc('When enabled, all nodes on the Obsidian canvas can also be resized using the Modifier key')
|
|
.addToggle((toggle) => {
|
|
toggle.setValue(this.plugin.settings.resizeInCanvas)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.resizeInCanvas = value;
|
|
this.updateWarningMessage(this.plugin.settings.modifierKey, value);
|
|
await this.plugin.saveSettings();
|
|
});
|
|
});
|
|
|
|
this.warningEl = containerEl.createDiv({ cls: 'mousewheel-zoom-warning' });
|
|
this.warningEl.style.display = 'none';
|
|
this.updateWarningMessage(this.plugin.settings.modifierKey, this.plugin.settings.resizeInCanvas);
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|