Obsidean_VM/.obsidian/plugins/mousewheel-image-zoom/main.js

524 lines
42 KiB
JavaScript
Raw Normal View History

2025-02-18 05:37:27 -03:00
/*
THIS IS A GENERATED/BUNDLED FILE BY ROLLUP
if you want to view the source visit the plugins github repository
*/
'use strict';
var obsidian = require('obsidian');
/*! *****************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
/**
* ReplaceTerm enables us to store the parameters for a replacement to add a new size parameter.
*/
class ReplaceTerm {
constructor(replaceFrom, replaceWith) {
this.replaceFrom = replaceFrom;
this.replaceWith = replaceWith;
}
// Generate a string that can be used in a string.replace() call as the string to replace
getReplaceFromString(oldSize) {
return this.replaceFrom(oldSize);
}
// Generate a string that can be used in a string.replace() call as the replacement string
getReplaceWithString(newSize) {
return this.replaceWith(newSize);
}
}
class Util {
/**
* For a given file content decide if a string is inside a table
* @param searchString string
* @param fileValue file content
* @private
*/
static isInTable(searchString, fileValue) {
return fileValue.search(new RegExp(`^\\|.+${searchString}.+\\|$`, "m")) !== -1;
}
/**
* Get the image name from a given src uri of a local image
* (URI like app://local/C:/.../image.png?1677337704730)
* @param imageUri uri of the image
* @private
*/
static getLocalImageNameFromUri(imageUri) {
imageUri = decodeURI(imageUri);
const imageNameMatch = imageUri.match(/([^\/?\\]+)(\?.*?|)$/);
const imageName = imageNameMatch ? imageNameMatch[1] : "";
// Handle linux not correctly decoding the %2F before the Filename to a \
const hasLinuxDecodingIssue = imageName.startsWith("2F");
return hasLinuxDecodingIssue ? imageName.slice(2) : imageName;
}
/**
* Get the parameters needed to handle the zoom for a local image.
* Source can be either a obsidian link like [[image.png]] or a markdown link like [image.png](image.png)
* @param imageName Name of the image
* @param fileText content of the current file
* @returns parameters to handle the zoom
*/
static getLocalImageZoomParams(imageName, fileText) {
imageName = this.determineImageName(imageName, fileText);
// Get the folder name if the image is located in a folder
const folderName = this.getFolderNameIfExist(imageName, fileText);
imageName = `${folderName}${imageName}`;
const isInTable = Util.isInTable(imageName, fileText);
// Separator to use for the replacement
const sizeSeparator = isInTable ? "\\|" : "|";
// Separator to use for the regex: isInTable ? \\\| : \|
const regexSeparator = isInTable ? "\\\\\\|" : "\\|";
const imageAttributes = this.getImageAttributes(imageName, fileText);
imageName = `${imageName}${imageAttributes}`;
// check character before the imageName to check if markdown link or obsidian link
const imageNamePosition = fileText.indexOf(imageName);
const isObsidianLink = fileText.charAt(imageNamePosition - 1) === "[";
if (isObsidianLink) {
return Util.generateReplaceTermForObsidianSyntax(imageName, regexSeparator, sizeSeparator);
}
else {
return Util.generateReplaceTermForMarkdownSyntax(imageName, regexSeparator, sizeSeparator, fileText);
}
}
/**
* When using markdown link syntax the image name can be encoded. This function checks if the image name is encoded and if not encodes it.
*
* @param origImageName Image name
* @param fileText File content
* @returns image name with the correct encoding
*/
static determineImageName(origImageName, fileText) {
const encodedImageName = encodeURI(origImageName);
const spaceEncodedImageName = origImageName.replace(/ /g, "%20");
// Try matching original, full URI encoded, and space encoded
const imageNameVariants = [origImageName, encodedImageName, spaceEncodedImageName];
for (const variant of imageNameVariants) {
if (fileText.includes(variant)) {
return variant;
}
}
throw new Error("Image not found in file");
}
/**
* Extracts the folder name from the given image name by looking for the first "[" or "(" character
* that appears before the image name in the file text.
* @param imageName The name of the image.
* @param fileText The text of the file that contains the image.
* @returns The name of the folder that contains the image, or an empty string if no folder is found.
*/
static getFolderNameIfExist(imageName, fileText) {
const index = fileText.indexOf(imageName);
if (index === -1) {
throw new Error("Image not found in file");
}
const stringBeforeFileName = fileText.substring(0, index);
const lastOpeningBracket = stringBeforeFileName.lastIndexOf("["); // Obsidian link
const lastOpeningParenthesis = stringBeforeFileName.lastIndexOf("("); // Markdown link
const lastOpeningBracketOrParenthesis = Math.max(lastOpeningBracket, lastOpeningParenthesis);
const folderName = stringBeforeFileName.substring(lastOpeningBracketOrParenthesis + 1);
return folderName;
}
/**
* Extracts any image attributes like |ctr for ITS Theme that appear after the given image name in the file.
* @param imageName - The name of the image to search for.
* @param fileText - The content of the file to search in.
* @returns A string containing any image attributes that appear after the image name.
*/
static getImageAttributes(imageName, fileText) {
const index = fileText.indexOf(imageName);
const stringAfterFileName = fileText.substring(index + imageName.length);
const regExpMatchArray = stringAfterFileName.match(/([^\]]*?)\\?\|\d+]]|([^\]]*?)]]|/);
if (regExpMatchArray) {
if (!!regExpMatchArray[1]) {
return regExpMatchArray[1];
}
else if (!!regExpMatchArray[2]) {
return regExpMatchArray[2];
}
}
return "";
}
/**
* Get the parameters needed to handle the zoom for images in markdown format.
* Example: ![image.png](image.png)
* @param imageName Name of the image
* @param fileText content of the current file
* @returns parameters to handle the zoom
* @private
*
*/
static generateReplaceTermForMarkdownSyntax(imageName, regexSeparator, sizeSeparator, fileText) {
const sizeMatchRegExp = new RegExp(`${regexSeparator}(\\d+)]${escapeRegex("(" + imageName + ")")}`);
const replaceSizeExistFrom = (oldSize) => `${sizeSeparator}${oldSize}](${imageName})`;
const replaceSizeExistWith = (newSize) => `${sizeSeparator}${newSize}](${imageName})`;
const replaceSizeNotExistsFrom = (oldSize) => `](${imageName})`;
const replaceSizeNotExistsWith = (newSize) => `${sizeSeparator}${newSize}](${imageName})`;
const replaceSizeExist = new ReplaceTerm(replaceSizeExistFrom, replaceSizeExistWith);
const replaceSizeNotExist = new ReplaceTerm(replaceSizeNotExistsFrom, replaceSizeNotExistsWith);
return {
sizeMatchRegExp: sizeMatchRegExp,
replaceSizeExist: replaceSizeExist,
replaceSizeNotExist: replaceSizeNotExist,
};
}
/**
* Get the parameters needed to handle the zoom for images in markdown format.
* Example: ![[image.png]]
* @param imageName Name of the image
* @param fileText content of the current file
* @returns parameters to handle the zoom
* @private
*
*/
static generateReplaceTermForObsidianSyntax(imageName, regexSeparator, sizeSeparator) {
const sizeMatchRegExp = new RegExp(`${escapeRegex(imageName)}${regexSeparator}(\\d+)`);
const replaceSizeExistFrom = (oldSize) => `${imageName}${sizeSeparator}${oldSize}`;
const replaceSizeExistWith = (newSize) => `${imageName}${sizeSeparator}${newSize}`;
const replaceSizeNotExistsFrom = (oldSize) => `${imageName}`;
const replaceSizeNotExistsWith = (newSize) => `${imageName}${sizeSeparator}${newSize}`;
const replaceSizeExist = new ReplaceTerm(replaceSizeExistFrom, replaceSizeExistWith);
const replaceSizeNotExist = new ReplaceTerm(replaceSizeNotExistsFrom, replaceSizeNotExistsWith);
return {
sizeMatchRegExp: sizeMatchRegExp,
replaceSizeExist: replaceSizeExist,
replaceSizeNotExist: replaceSizeNotExist,
};
}
/**
* Get the parameters needed to handle the zoom for a remote image.
* Format: https://www.example.com/image.png
* @param imageUri URI of the image
* @param fileText content of the current file
* @returns parameters to handle the zoom
*/
static getRemoteImageZoomParams(imageUri, fileText) {
const isInTable = Util.isInTable(imageUri, fileText);
// Separator to use for the replacement
const sizeSeparator = isInTable ? "\\|" : "|";
// Separator to use for the regex: isInTable ? \\\| : \|
const regexSeparator = isInTable ? "\\\\\\|" : "\\|";
return Util.generateReplaceTermForMarkdownSyntax(imageUri, regexSeparator, sizeSeparator, fileText);
}
}
/**
* Function to escape a string into a valid searchable string for a regex
* @param string string to escape
* @returns escaped string
*/
function escapeRegex(string) {
return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}
var ModifierKey;
(function (ModifierKey) {
ModifierKey["ALT"] = "AltLeft";
ModifierKey["CTRL"] = "ControlLeft";
ModifierKey["SHIFT"] = "ShiftLeft";
ModifierKey["ALT_RIGHT"] = "AltRight";
ModifierKey["CTRL_RIGHT"] = "ControlRight";
ModifierKey["SHIFT_RIGHT"] = "ShiftRight";
})(ModifierKey || (ModifierKey = {}));
const DEFAULT_SETTINGS = {
modifierKey: ModifierKey.ALT,
stepSize: 25,
initialSize: 500
};
class MouseWheelZoomPlugin extends obsidian.Plugin {
constructor() {
super(...arguments);
this.isKeyHeldDown = false;
this.wheelOpt = { passive: false, capture: true };
this.wheelEvent = 'wheel';
}
onload() {
return __awaiter(this, void 0, void 0, function* () {
yield this.loadSettings();
this.registerEvent(this.app.workspace.on("window-open", (newWindow) => this.registerEvents(newWindow.win)));
this.registerEvents(window);
this.addSettingTab(new MouseWheelZoomSettingsTab(this.app, this));
console.log("Loaded: Mousewheel image zoom");
});
}
/**
* When the config key is released, we enable the scroll again and reset the key held down flag.
*/
onConfigKeyUp(currentWindow) {
this.isKeyHeldDown = false;
this.enableScroll(currentWindow);
}
onunload(currentWindow = 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
*/
registerEvents(currentWindow) {
const doc = currentWindow.document;
this.registerDomEvent(doc, "keydown", (evt) => {
if (evt.code === this.settings.modifierKey.toString()) {
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;
const targetIsCanvas = eventTarget.hasClass("canvas-node-content-blocker");
const targetIsCanvasNode = eventTarget.closest(".canvas-node-content") !== null;
const targetIsImage = eventTarget.nodeName === "IMG";
if (targetIsCanvas || targetIsCanvasNode || targetIsImage) {
this.disableScroll(currentWindow);
}
if (targetIsCanvas) {
// seems we're trying to zoom on some canvas node.
this.handleZoomForCanvas(evt, eventTarget);
}
else if (targetIsCanvasNode) ;
else if (targetIsImage) {
// Handle the zooming of the image
this.handleZoom(evt, eventTarget);
}
}
});
}
/**
* Handles zooming with the mousewheel on canvas node
* @param evt wheel event
* @param eventTarget targeted canvas node element
* @private
*/
handleZoomForCanvas(evt, eventTarget) {
// get active canvas
const isCanvas = this.app.workspace.getActiveViewOfType(obsidian.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(obsidian.View).canvas;
// get triggered canvasNode
const canvasNode = Array.from(canvas.nodes.values())
.find(node => node.contentBlockerEl == eventTarget);
// 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
*/
handleZoom(evt, eventTarget) {
return __awaiter(this, void 0, void 0, function* () {
const imageUri = eventTarget.attributes.getNamedItem("src").textContent;
const activeFile = yield this.getActivePaneWithImage(eventTarget);
let fileText = yield this.app.vault.read(activeFile);
const originalFileText = fileText;
// Get parameters like the regex or the replacement terms based on the fact if the image is locally stored or not.
const zoomParams = this.getZoomParams(imageUri, fileText, eventTarget);
// Check if there is already a size parameter for this image.
const sizeMatches = fileText.match(zoomParams.sizeMatchRegExp);
// Element already has a size entry
if (sizeMatches !== null) {
const oldSize = parseInt(sizeMatches[1]);
let newSize = oldSize;
if (evt.deltaY < 0) {
newSize += this.settings.stepSize;
}
else if (evt.deltaY > 0 && newSize > this.settings.stepSize) {
newSize -= this.settings.stepSize;
}
fileText = fileText.replace(zoomParams.replaceSizeExist.getReplaceFromString(oldSize), zoomParams.replaceSizeExist.getReplaceWithString(newSize));
}
else { // Element has no size entry -> give it an initial size
const initialSize = this.settings.initialSize;
var image = new Image();
image.src = imageUri;
var width = image.naturalWidth;
var minWidth = Math.min(width, initialSize);
fileText = fileText.replace(zoomParams.replaceSizeNotExist.getReplaceFromString(0), zoomParams.replaceSizeNotExist.getReplaceWithString(minWidth));
}
// Save changed size
if (fileText !== originalFileText) {
yield this.app.vault.modify(activeFile, fileText);
}
});
}
/**
* 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
*/
getActivePaneWithImage(imageElement) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise(((resolve, reject) => {
this.app.workspace.iterateAllLeaves(leaf => {
if (leaf.view.containerEl.contains(imageElement) && leaf.view instanceof obsidian.MarkdownView) {
resolve(leaf.view.file);
}
});
reject(new Error("No file belonging to the image found"));
}));
});
}
getZoomParams(imageUri, fileText, target) {
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);
}
throw new Error("Image is not zoomable");
}
loadSettings() {
return __awaiter(this, void 0, void 0, function* () {
this.settings = Object.assign({}, DEFAULT_SETTINGS, yield this.loadData());
});
}
saveSettings() {
return __awaiter(this, void 0, void 0, function* () {
yield this.saveData(this.settings);
});
}
// Utilities to disable and enable scrolling //
preventDefault(ev) {
ev.preventDefault();
}
/**
* Disables the normal scroll event
*/
disableScroll(currentWindow) {
currentWindow.addEventListener(this.wheelEvent, this.preventDefault, this.wheelOpt);
}
/**
* Enables the normal scroll event
*/
enableScroll(currentWindow) {
currentWindow.removeEventListener(this.wheelEvent, this.preventDefault, this.wheelOpt);
}
isConfiguredKeyDown(evt) {
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 obsidian.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
let { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: 'Settings for mousewheel zoom' });
new obsidian.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((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.modifierKey = value;
yield this.plugin.saveSettings();
})));
new obsidian.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((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.stepSize = value;
yield this.plugin.saveSettings();
}));
});
new obsidian.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((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.initialSize = value;
yield this.plugin.saveSettings();
}));
});
}
}
module.exports = MouseWheelZoomPlugin;
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFpbi5qcyIsInNvdXJjZXMiOlsibm9kZV9tb2R1bGVzL3RzbGliL3RzbGliLmVzNi5qcyIsInNyYy91dGlsLnRzIiwibWFpbi50cyJdLCJzb3VyY2VzQ29udGVudCI6bnVsbCwibmFtZXMiOlsiUGx1Z2luIiwiVmlldyIsIk1hcmtkb3duVmlldyIsIlBsdWdpblNldHRpbmdUYWIiLCJTZXR0aW5nIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBdURBO0FBQ08sU0FBUyxTQUFTLENBQUMsT0FBTyxFQUFFLFVBQVUsRUFBRSxDQUFDLEVBQUUsU0FBUyxFQUFFO0FBQzdELElBQUksU0FBUyxLQUFLLENBQUMsS0FBSyxFQUFFLEVBQUUsT0FBTyxLQUFLLFlBQVksQ0FBQyxHQUFHLEtBQUssR0FBRyxJQUFJLENBQUMsQ0FBQyxVQUFVLE9BQU8sRUFBRSxFQUFFLE9BQU8sQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxFQUFFO0FBQ2hILElBQUksT0FBTyxLQUFLLENBQUMsS0FBSyxDQUFDLEdBQUcsT0FBTyxDQUFDLEVBQUUsVUFBVSxPQUFPLEVBQUUsTUFBTSxFQUFFO0FBQy9ELFFBQVEsU0FBUyxTQUFTLENBQUMsS0FBSyxFQUFFLEVBQUUsSUFBSSxFQUFFLElBQUksQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLE9BQU8sQ0FBQyxFQUFFLEVBQUUsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRTtBQUNuRyxRQUFRLFNBQVMsUUFBUSxDQUFDLEtBQUssRUFBRSxFQUFFLElBQUksRUFBRSxJQUFJLENBQUMsU0FBUyxDQUFDLE9BQU8sQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLE9BQU8sQ0FBQyxFQUFFLEVBQUUsTUFBTSxDQUFDLENB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