moved Drop handlers to DropManager, added await to page.getViewport as there seems to be a race condition impacting page.render()

This commit is contained in:
zsviczian
2024-12-20 19:32:37 +01:00
parent 682307b51d
commit be383f2b48
5 changed files with 649 additions and 577 deletions

View File

@@ -5,6 +5,7 @@ import { terser } from "rollup-plugin-terser";
import copy from "rollup-plugin-copy";
import typescript2 from "rollup-plugin-typescript2";
import fs from 'fs';
import path from 'path';
import LZString from 'lz-string';
import postprocess from '@zsviczian/rollup-plugin-postprocess';
import cssnano from 'cssnano';
@@ -16,6 +17,8 @@ import dotenv from 'dotenv';
dotenv.config();
const DIST_FOLDER = 'dist';
const absolutePath = path.resolve(DIST_FOLDER);
fs.mkdirSync(absolutePath, { recursive: true });
const isProd = (process.env.NODE_ENV === "production");
const isLib = (process.env.NODE_ENV === "lib");
console.log(`Running: ${process.env.NODE_ENV}; isProd: ${isProd}; isLib: ${isLib}`);
@@ -145,7 +148,7 @@ const BUILD_CONFIG = {
tsconfig: isProd ? "tsconfig.json" : "tsconfig.dev.json",
sourcemap: !isProd,
clean: true,
verbosity: isProd ? 1 : 2,
//verbosity: isProd ? 1 : 2,
},
...(isProd ? [
terser({

View File

@@ -800,7 +800,7 @@ export class EmbeddedFilesLoader {
// Get page
const page = await pdfDoc.getPage(num);
// Set scale
const viewport = page.getViewport({ scale });
const viewport = await page.getViewport({ scale });
height = canvas.height = viewport.height;
width = canvas.width = viewport.width;

View File

@@ -4,18 +4,7 @@ import { WorkspaceLeaf } from "obsidian";
import { FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ObsidianCanvasNode } from "../view/managers/CanvasNodeFactory";
export interface DropData {
files?: File[];
text?: string;
html?: string;
uri?: string;
}
export interface DropContext {
event: DragEvent;
position: {x: number; y: number};
modifierAction: string;
}
export type Position = { x: number; y: number };
export interface SelectedElementWithLink {
id: string | null;

View File

@@ -40,7 +40,6 @@ import {
TEXT_DISPLAY_RAW_ICON_NAME,
TEXT_DISPLAY_PARSED_ICON_NAME,
IMAGE_TYPES,
REG_LINKINDEX_INVALIDCHARS,
KEYCODE,
FRONTMATTER_KEYS,
DEVICE,
@@ -81,7 +80,6 @@ import {
download,
getDataURLFromURL,
getIMGFilename,
getInternalLinkOrFileURLLink,
getMimeType,
getNewUniqueFilepath,
getURLImageExtension,
@@ -100,7 +98,6 @@ import {
scaleLoadedImage,
svgToBase64,
hyperlinkIsImage,
hyperlinkIsYouTubeLink,
getYouTubeThumbnailLink,
isContainer,
fragWithHTML,
@@ -128,9 +125,8 @@ import { getTextElementAtPointer, getImageElementAtPointer, getElementWithLinkAt
import { excalidrawSword, ICONS, LogoWrapper, Rank, saveIcon, SwordColors } from "../constants/actionIcons";
import { ExportDialog } from "../shared/Dialogs/ExportDialog";
import { getEA } from "src/core"
import { anyModifierKeysPressed, emulateKeysForLinkClick, webbrowserDragModifierType, internalDragModifierType, isWinALTorMacOPT, isWinCTRLorMacCMD, isWinMETAorMacCTRL, isSHIFT, linkClickModifierType, localFileDragModifierType, ModifierKeys, modifierKeyTooltipMessages } from "../utils/modifierkeyHelper";
import { anyModifierKeysPressed, emulateKeysForLinkClick, isWinALTorMacOPT, isWinCTRLorMacCMD, isWinMETAorMacCTRL, isSHIFT, linkClickModifierType, localFileDragModifierType, ModifierKeys, modifierKeyTooltipMessages } from "../utils/modifierkeyHelper";
import { setDynamicStyle } from "../utils/dynamicStyling";
import { InsertPDFModal } from "../shared/Dialogs/InsertPDFModal";
import { CustomEmbeddable, renderWebView } from "./components/CustomEmbeddable";
import { addBackOfTheNoteCard, getExcalidrawFileForwardLinks, getFrameBasedOnFrameNameOrId, getLinkTextFromLink, insertEmbeddableToView, insertImageToView, isTextImageTransclusion, openExternalLink, parseObsidianLink, renderContextMenuAction, tmpBruteForceCleanup } from "../utils/excalidrawViewUtils";
import { imageCache } from "../shared/ImageCache";
@@ -149,7 +145,8 @@ import React from "react";
import { diagramToHTML } from "../utils/matic";
import { IS_WORKER_SUPPORTED } from "../shared/Workers/compression-worker";
import { getPDFCropRect } from "../utils/PDFUtils";
import { ViewSemaphores } from "../types/excalidrawViewTypes";
import { Position, ViewSemaphores } from "../types/excalidrawViewTypes";
import { DropManager } from "./managers/DropManager";
const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000;
const PREVENT_RELOAD_TIMEOUT = 2000;
@@ -278,6 +275,7 @@ type ActionButtons = "save" | "isParsed" | "isRaw" | "link" | "scriptInstall";
let windowMigratedDisableZoomOnce = false;
export default class ExcalidrawView extends TextFileView implements HoverParent{
private dropManager: DropManager;
public hoverPopover: HoverPopover;
private freedrawLastActiveTimestamp: number = 0;
public exportDialog: ExportDialog;
@@ -296,9 +294,8 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
private lastLoadedFile: TFile = null;
//store key state for view mode link resolution
private modifierKeyDown: ModifierKeys = {shiftKey:false, metaKey: false, ctrlKey: false, altKey: false}
public currentPosition: {x:number,y:number} = { x: 0, y: 0 }; //these are scene coord thus would be more apt to call them sceneX and sceneY, however due to scrits already using x and y, I will keep it as is
public currentPosition: Position = { x: 0, y: 0 }; //these are scene coord thus would be more apt to call them sceneX and sceneY, however due to scrits already using x and y, I will keep it as is
//Obsidian 0.15.0
private draginfoDiv: HTMLDivElement;
public canvasNodeFactory: CanvasNodeFactory;
private embeddableRefs = new Map<ExcalidrawElement["id"], HTMLIFrameElement | HTMLWebViewElement>();
private embeddableLeafRefs = new Map<ExcalidrawElement["id"], any>();
@@ -363,6 +360,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
this.excalidrawData = new ExcalidrawData(plugin);
this.canvasNodeFactory = new CanvasNodeFactory(this);
this.setHookServer();
this.dropManager = new DropManager(this);
}
get hookServer (): ExcalidrawAutomate {
@@ -390,7 +388,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
}
}
private getHookServer () {
public getHookServer () {
return this.hookServer ?? this.plugin.ea;
}
@@ -1900,10 +1898,10 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
this.clearEmbeddableNodeIsEditingTimer();
this.plugin.scriptEngine?.removeViewEAs(this);
this.excalidrawAPI = null;
if(this.draginfoDiv) {
this.ownerDocument.body.removeChild(this.draginfoDiv);
delete this.draginfoDiv;
}
this.dropManager.destroy();
this.dropManager = null;
if(this.canvasNodeFactory) {
this.canvasNodeFactory.destroy();
}
@@ -3538,34 +3536,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
}
};
private dropAction(transfer: DataTransfer) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.dropAction, "ExcalidrawView.dropAction");
// Return a 'copy' or 'link' action according to the content types, or undefined if no recognized type
const files = (this.app as any).dragManager.draggable?.files;
if (files) {
if (files[0] == this.file) {
files.shift();
(
this.app as any
).dragManager.draggable.title = `${files.length} files`;
}
}
if (
["file", "files"].includes(
(this.app as any).dragManager.draggable?.type,
)
) {
return "link";
}
if (
transfer.types?.includes("text/html") ||
transfer.types?.includes("text/plain") ||
transfer.types?.includes("Files")
) {
return "copy";
}
};
/**
* identify which element to navigate to on click
* @returns
@@ -3782,50 +3752,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
this.clearHoverPreview();
}
private onDragOver(e: any) {
const action = this.dropAction(e.dataTransfer);
if (action) {
if(!this.draginfoDiv) {
this.draginfoDiv = createDiv({cls:"excalidraw-draginfo"});
this.ownerDocument.body.appendChild(this.draginfoDiv);
}
let msg: string = "";
if((this.app as any).dragManager.draggable) {
//drag from Obsidian file manager
msg = modifierKeyTooltipMessages().InternalDragAction[internalDragModifierType(e)];
} else if(e.dataTransfer.types.length === 1 && e.dataTransfer.types.includes("Files")) {
//drag from OS file manager
msg = modifierKeyTooltipMessages().LocalFileDragAction[localFileDragModifierType(e)];
if(DEVICE.isMacOS && isWinCTRLorMacCMD(e)) {
msg = "CMD is reserved by MacOS for file system drag actions.\nCan't use it in Obsidian.\nUse a combination of SHIFT, CTRL, OPT instead."
}
} else {
//drag from Internet
msg = modifierKeyTooltipMessages().WebBrowserDragAction[webbrowserDragModifierType(e)];
}
if(!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
msg += DEVICE.isMacOS || DEVICE.isIOS
? "\nTry SHIFT, OPT, CTRL combinations for other drop actions"
: "\nTry SHIFT, CTRL, ALT, Meta combinations for other drop actions";
}
if(this.draginfoDiv.innerText !== msg) this.draginfoDiv.innerText = msg;
const top = `${e.clientY-parseFloat(getComputedStyle(this.draginfoDiv).fontSize)*8}px`;
const left = `${e.clientX-this.draginfoDiv.clientWidth/2}px`;
if(this.draginfoDiv.style.top !== top) this.draginfoDiv.style.top = top;
if(this.draginfoDiv.style.left !== left) this.draginfoDiv.style.left = left;
e.dataTransfer.dropEffect = action;
e.preventDefault();
return false;
}
}
private onDragLeave() {
if(this.draginfoDiv) {
this.ownerDocument.body.removeChild(this.draginfoDiv);
delete this.draginfoDiv;
}
}
private onPointerUpdate(p: {
pointer: { x: number; y: number; tool: "pointer" | "laser" };
button: "down" | "up";
@@ -4177,480 +4103,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
window.setTimeout(()=>setDynamicStyle(this.plugin.ea,this,this.previousBackgroundColor,this.plugin.settings.dynamicStyling));
}
private onDrop (event: React.DragEvent<HTMLDivElement>): boolean {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onDrop, "ExcalidrawView.onDrop", event);
if(this.draginfoDiv) {
this.ownerDocument.body.removeChild(this.draginfoDiv);
delete this.draginfoDiv;
}
const api = this.excalidrawAPI;
if (!api) {
return false;
}
const st: AppState = api.getAppState();
this.currentPosition = viewportCoordsToSceneCoords(
{ clientX: event.clientX, clientY: event.clientY },
st,
);
const draggable = (this.app as any).dragManager.draggable;
const internalDragAction = internalDragModifierType(event);
const externalDragAction = webbrowserDragModifierType(event);
const localFileDragAction = localFileDragModifierType(event);
//Call Excalidraw Automate onDropHook
const onDropHook = (
type: "file" | "text" | "unknown",
files: TFile[],
text: string,
): boolean => {
if (this.getHookServer().onDropHook) {
try {
return this.getHookServer().onDropHook({
ea: this.getHookServer(), //the ExcalidrawAutomate object
event, //React.DragEvent<HTMLDivElement>
draggable, //Obsidian draggable object
type, //"file"|"text"
payload: {
files, //TFile[] array of dropped files
text, //string
},
excalidrawFile: this.file, //the file receiving the drop event
view: this, //the excalidraw view receiving the drop
pointerPosition: this.currentPosition, //the pointer position on canvas at the time of drop
});
} catch (e) {
new Notice("on drop hook error. See console log for details");
errorlog({ where: "ExcalidrawView.onDrop", error: e });
return false;
}
} else {
return false;
}
};
//---------------------------------------------------------------------------------
// Obsidian internal drag event
//---------------------------------------------------------------------------------
switch (draggable?.type) {
case "file":
if (!onDropHook("file", [draggable.file], null)) {
const file:TFile = draggable.file;
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/422
if (file.path.match(REG_LINKINDEX_INVALIDCHARS)) {
new Notice(t("FILENAME_INVALID_CHARS"), 4000);
return false;
}
if (
["image", "image-fullsize"].contains(internalDragAction) &&
(IMAGE_TYPES.contains(file.extension) ||
file.extension === "md" ||
file.extension.toLowerCase() === "pdf" )
) {
if(file.extension.toLowerCase() === "pdf") {
const insertPDFModal = new InsertPDFModal(this.plugin, this);
insertPDFModal.open(file);
} else {
(async () => {
const ea: ExcalidrawAutomate = getEA(this);
ea.selectElementsInView([
await insertImageToView(
ea,
this.currentPosition,
file,
!(internalDragAction==="image-fullsize")
)
]);
ea.destroy();
})();
}
return false;
}
if (internalDragAction === "embeddable") {
(async () => {
const ea: ExcalidrawAutomate = getEA(this);
ea.selectElementsInView([
await insertEmbeddableToView(
ea,
this.currentPosition,
file,
)
]);
ea.destroy();
})();
return false;
}
//internalDragAction === "link"
this.addText(
`[[${this.app.metadataCache.fileToLinktext(
draggable.file,
this.file.path,
true,
)}]]`,
);
}
return false;
case "files":
if (!onDropHook("file", draggable.files, null)) {
(async () => {
if (["image", "image-fullsize"].contains(internalDragAction)) {
const ea:ExcalidrawAutomate = getEA(this);
ea.canvas.theme = api.getAppState().theme;
let counter:number = 0;
const ids:string[] = [];
for (const f of draggable.files) {
if ((IMAGE_TYPES.contains(f.extension) || f.extension === "md")) {
ids.push(await ea.addImage(
this.currentPosition.x + counter*50,
this.currentPosition.y + counter*50,
f,
!(internalDragAction==="image-fullsize"),
));
counter++;
await ea.addElementsToView(false, false, true);
ea.selectElementsInView(ids);
}
if (f.extension.toLowerCase() === "pdf") {
const insertPDFModal = new InsertPDFModal(this.plugin, this);
insertPDFModal.open(f);
}
}
ea.destroy();
return;
}
if (internalDragAction === "embeddable") {
const ea:ExcalidrawAutomate = getEA(this);
let column:number = 0;
let row:number = 0;
const ids:string[] = [];
for (const f of draggable.files) {
ids.push(await insertEmbeddableToView(
ea,
{
x:this.currentPosition.x + column*500,
y:this.currentPosition.y + row*550
},
f,
));
column = (column + 1) % 3;
if(column === 0) {
row++;
}
}
ea.destroy();
return false;
}
//internalDragAction === "link"
for (const f of draggable.files) {
await this.addText(
`[[${this.app.metadataCache.fileToLinktext(
f,
this.file.path,
true,
)}]]`, undefined,false
);
this.currentPosition.y += st.currentItemFontSize * 2;
}
this.save(false);
})();
}
return false;
}
//---------------------------------------------------------------------------------
// externalDragAction
//---------------------------------------------------------------------------------
if (event.dataTransfer.types.includes("Files")) {
if (event.dataTransfer.types.includes("text/plain")) {
const text: string = event.dataTransfer.getData("text");
if (text && onDropHook("text", null, text)) {
return false;
}
if(text && (externalDragAction === "image-url") && hyperlinkIsImage(text)) {
this.addImageWithURL(text);
return false;
}
if(text && (externalDragAction === "link")) {
if (
this.plugin.settings.iframelyAllowed &&
text.match(/^https?:\/\/\S*$/)
) {
this.addTextWithIframely(text);
return false;
} else {
this.addText(text);
return false;
}
}
if(text && (externalDragAction === "embeddable")) {
const ea = getEA(this) as ExcalidrawAutomate;
insertEmbeddableToView(
ea,
this.currentPosition,
undefined,
text,
).then(()=>ea.destroy());
return false;
}
}
if(event.dataTransfer.types.includes("text/html")) {
const html = event.dataTransfer.getData("text/html");
const src = html.match(/src=["']([^"']*)["']/)
if(src && (externalDragAction === "image-url") && hyperlinkIsImage(src[1])) {
this.addImageWithURL(src[1]);
return false;
}
if(src && (externalDragAction === "link")) {
if (
this.plugin.settings.iframelyAllowed &&
src[1].match(/^https?:\/\/\S*$/)
) {
this.addTextWithIframely(src[1]);
return false;
} else {
this.addText(src[1]);
return false;
}
}
if(src && (externalDragAction === "embeddable")) {
const ea = getEA(this) as ExcalidrawAutomate;
insertEmbeddableToView(
ea,
this.currentPosition,
undefined,
src[1],
).then(ea.destroy);
return false;
}
}
if (event.dataTransfer.types.length >= 1 && ["image-url","image-import","embeddable"].contains(localFileDragAction)) {
const files = Array.from(event.dataTransfer.files || []);
for(let i = 0; i < files.length; i++) {
// Try multiple ways to get file path
const file = files[i];
let path = file?.path
if(!path && file && DEVICE.isDesktop) {
//https://www.electronjs.org/docs/latest/breaking-changes#removed-filepath
const { webUtils } = require('electron');
if(webUtils && webUtils.getPathForFile) {
path = webUtils.getPathForFile(file);
}
}
if(!path) {
new Notice(t("ERROR_CANT_READ_FILEPATH"),6000);
return true; //excalidarw to continue processing
}
const link = getInternalLinkOrFileURLLink(path, this.plugin, event.dataTransfer.files[i].name, this.file);
const {x,y} = this.currentPosition;
const pos = {x:x+i*300, y:y+i*300};
if(link.isInternal) {
if(localFileDragAction === "embeddable") {
const ea = getEA(this) as ExcalidrawAutomate;
insertEmbeddableToView(ea, pos, link.file).then(()=>ea.destroy());
} else {
if(link.file.extension === "pdf") {
const insertPDFModal = new InsertPDFModal(this.plugin, this);
insertPDFModal.open(link.file);
}
const ea = getEA(this) as ExcalidrawAutomate;
insertImageToView(ea, pos, link.file).then(()=>ea.destroy()) ;
}
} else {
const extension = getURLImageExtension(link.url);
if(localFileDragAction === "image-import") {
if (IMAGE_TYPES.contains(extension)) {
(async () => {
const droppedFilename = event.dataTransfer.files[i].name;
const fileToImport = await event.dataTransfer.files[i].arrayBuffer();
let {folder:_, filepath} = await getAttachmentsFolderAndFilePath(this.app, this.file.path, droppedFilename);
const maybeFile = this.app.vault.getAbstractFileByPath(filepath);
if(maybeFile && maybeFile instanceof TFile) {
const action = await ScriptEngine.suggester(
this.app,[
"Use the file already in the Vault instead of importing",
"Overwrite existing file in the Vault",
"Import the file with a new name",
],[
"Use",
"Overwrite",
"Import",
],
"A file with the same name/path already exists in the Vault",
);
switch(action) {
case "Import":
const {folderpath,filename,basename:_,extension:__} = splitFolderAndFilename(filepath);
filepath = getNewUniqueFilepath(this.app.vault, filename, folderpath);
break;
case "Overwrite":
await this.app.vault.modifyBinary(maybeFile, fileToImport);
// there is deliberately no break here
case "Use":
default:
const ea = getEA(this) as ExcalidrawAutomate;
await insertImageToView(ea, pos, maybeFile);
ea.destroy();
return false;
}
}
const file = await this.app.vault.createBinary(filepath, fileToImport)
const ea = getEA(this) as ExcalidrawAutomate;
await insertImageToView(ea, pos, file);
ea.destroy();
})();
} else if(extension === "excalidraw") {
return true; //excalidarw to continue processing
} else {
(async () => {
const {folder:_, filepath} = await getAttachmentsFolderAndFilePath(this.app, this.file.path,event.dataTransfer.files[i].name);
const file = await this.app.vault.createBinary(filepath, await event.dataTransfer.files[i].arrayBuffer());
const modal = new UniversalInsertFileModal(this.plugin, this);
modal.open(file, pos);
})();
}
}
else if(localFileDragAction === "embeddable" || !IMAGE_TYPES.contains(extension)) {
const ea = getEA(this) as ExcalidrawAutomate;
insertEmbeddableToView(ea, pos, null, link.url).then(()=>ea.destroy());
if(localFileDragAction !== "embeddable") {
new Notice("Not imported to Vault. Embedded with local URI");
}
} else {
const ea = getEA(this) as ExcalidrawAutomate;
insertImageToView(ea, pos, link.url).then(()=>ea.destroy());
}
}
};
return false;
}
if(event.dataTransfer.types.length >= 1 && localFileDragAction === "link") {
const ea = getEA(this) as ExcalidrawAutomate;
for(let i=0;i<event.dataTransfer.files.length;i++) {
const file = event.dataTransfer.files[i];
let path = file?.path;
const name = file?.name;
if(!path && file && DEVICE.isDesktop) {
//https://www.electronjs.org/docs/latest/breaking-changes#removed-filepath
const { webUtils } = require('electron');
if(webUtils && webUtils.getPathForFile) {
path = webUtils.getPathForFile(file);
}
}
if(!path || !name) {
new Notice(t("ERROR_CANT_READ_FILEPATH"),6000);
ea.destroy();
return true; //excalidarw to continue processing
}
const link = getInternalLinkOrFileURLLink(path, this.plugin, name, this.file);
const id = ea.addText(
this.currentPosition.x+i*40,
this.currentPosition.y+i*20,
link.isInternal ? link.link :`📂 ${name}`);
if(!link.isInternal) {
ea.getElement(id).link = link.link;
}
}
ea.addElementsToView().then(()=>ea.destroy());
return false;
}
return true;
}
if (event.dataTransfer.types.includes("text/plain") || event.dataTransfer.types.includes("text/uri-list") || event.dataTransfer.types.includes("text/html")) {
const html = event.dataTransfer.getData("text/html");
const src = html.match(/src=["']([^"']*)["']/);
const htmlText = src ? src[1] : "";
const textText = event.dataTransfer.getData("text");
const uriText = event.dataTransfer.getData("text/uri-list");
let text: string = src ? htmlText : textText;
if (!text || text === "") {
text = uriText
}
if (!text || text === "") {
return true;
}
if (!onDropHook("text", null, text)) {
if(text && (externalDragAction==="embeddable") && /^(blob:)?(http|https):\/\/[^\s/$.?#].[^\s]*$/.test(text)) {
return true;
}
if(text && (externalDragAction==="image-url") && hyperlinkIsYouTubeLink(text)) {
this.addYouTubeThumbnail(text);
return false;
}
if(uriText && (externalDragAction==="image-url") && hyperlinkIsYouTubeLink(uriText)) {
this.addYouTubeThumbnail(uriText);
return false;
}
if(text && (externalDragAction==="image-url") && hyperlinkIsImage(text)) {
this.addImageWithURL(text);
return false;
}
if(uriText && (externalDragAction==="image-url") && hyperlinkIsImage(uriText)) {
this.addImageWithURL(uriText);
return false;
}
if(text && (externalDragAction==="image-import") && hyperlinkIsImage(text)) {
this.addImageSaveToVault(text);
return false;
}
if(uriText && (externalDragAction==="image-import") && hyperlinkIsImage(uriText)) {
this.addImageSaveToVault(uriText);
return false;
}
if (
this.plugin.settings.iframelyAllowed &&
text.match(/^https?:\/\/\S*$/)
) {
this.addTextWithIframely(text);
return false;
}
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/599
if(text.startsWith("obsidian://open?vault=")) {
const html = event.dataTransfer.getData("text/html");
if(html) {
const path = html.match(/href="app:\/\/obsidian\.md\/(.*?)"/);
if(path.length === 2) {
const link = decodeURIComponent(path[1]).split("#");
const f = this.app.vault.getAbstractFileByPath(link[0]);
if(f && f instanceof TFile) {
const path = this.app.metadataCache.fileToLinktext(f,this.file.path);
this.addText(`[[${
path +
(link.length>1 ? "#" + link[1] + "|" + path : "")
}]]`);
return;
}
this.addText(`[[${decodeURIComponent(path[1])}]]`);
return false;
}
}
const path = text.split("file=");
if(path.length === 2) {
this.addText(`[[${decodeURIComponent(path[1])}]]`);
return false;
}
}
this.addText(text.replace(/(!\[\[.*#[^\]]*\]\])/g, "$1{40}"));
}
return false;
}
if (onDropHook("unknown", null, null)) {
return false;
}
return true;
}
//returns the raw text of the element which is the original text without parsing
//in compatibility mode, returns the original text, and for backward compatibility the text if originalText is not available
private onBeforeTextEdit (textElement: ExcalidrawTextElement, isExistingElement: boolean): string {
@@ -5869,8 +5321,8 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
onPointerDown: this.onPointerDown.bind(this),
onMouseMove: this.onMouseMove.bind(this),
onMouseOver: this.onMouseOver.bind(this),
onDragOver : this.onDragOver.bind(this),
onDragLeave: this.onDragLeave.bind(this),
onDragOver : this.dropManager.onDragOver.bind(this.dropManager),
onDragLeave: this.dropManager.onDragLeave.bind(this.dropManager),
},
React.createElement(
Excalidraw,
@@ -5904,7 +5356,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
renderEmbeddableMenu: this.renderEmbeddableMenu.bind(this),
onPaste: this.onPaste.bind(this),
onThemeChange: this.onThemeChange.bind(this),
onDrop: this.onDrop.bind(this),
onDrop: this.dropManager.onDrop.bind(this.dropManager),
onBeforeTextEdit: this.onBeforeTextEdit.bind(this),
onBeforeTextSubmit: this.onBeforeTextSubmit.bind(this),
onLinkOpen: this.onLinkOpen.bind(this),
@@ -6425,4 +5877,4 @@ export function getTextMode(data: string): TextMode {
data.search("excalidraw-plugin: parsed\n") > -1 ||
data.search("excalidraw-plugin: locked\n") > -1; //locked for backward compatibility
return parsed ? TextMode.parsed : TextMode.raw;
}
}

View File

@@ -0,0 +1,628 @@
import { DEBUGGING, debug } from "src/utils/debugHelper";
import ExcalidrawView from "../ExcalidrawView";
import { App, Notice, TFile } from "obsidian";
import { AppState, ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { DEVICE, IMAGE_TYPES, REG_LINKINDEX_INVALIDCHARS, viewportCoordsToSceneCoords } from "src/constants/constants";
import { internalDragModifierType, isWinCTRLorMacCMD, localFileDragModifierType, modifierKeyTooltipMessages, webbrowserDragModifierType } from "src/utils/modifierkeyHelper";
import { errorlog, hyperlinkIsImage, hyperlinkIsYouTubeLink } from "src/utils/utils";
import { InsertPDFModal } from "src/shared/Dialogs/InsertPDFModal";
import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
import { getEA } from "src/core";
import { insertEmbeddableToView, insertImageToView } from "src/utils/excalidrawViewUtils";
import { t } from "src/lang/helpers";
import ExcalidrawPlugin from "src/core/main";
import { getInternalLinkOrFileURLLink, getNewUniqueFilepath, getURLImageExtension, splitFolderAndFilename } from "src/utils/fileUtils";
import { getAttachmentsFolderAndFilePath } from "src/utils/obsidianUtils";
import { ScriptEngine } from "src/shared/Scripts";
import { UniversalInsertFileModal } from "src/shared/Dialogs/UniversalInsertFileModal";
import { Position } from "src/types/excalidrawViewTypes";
/*
static getDropAction(event: DragEvent): string {
// Get modifier action
}
static parseDropData(event: DragEvent): DropData {
// Parse drop data into clean format
}
static handleInternalDrop(data: DropData, context: DropContext): boolean {
// Handle Obsidian internal file drops
}
static handleExternalFileDrop(data: DropData, context: DropContext): boolean {
// Handle external file drops
}
static handleTextDrop(data: DropData, context: DropContext): boolean {
// Handle text/url drops
}*/
export class DropManager {
private view: ExcalidrawView;
private app: App;
private draginfoDiv: HTMLDivElement;
constructor(view: ExcalidrawView) {
this.view = view;
this.app = this.view.app;
}
public destroy() {
if(this.draginfoDiv) {
this.ownerDocument.body.removeChild(this.draginfoDiv);
delete this.draginfoDiv;
}
}
get ownerDocument(): Document {
return this.view.ownerDocument;
}
get currentPosition(): Position {
return this.view.currentPosition;
}
set currentPosition(pos: Position) {
this.view.currentPosition = pos;
}
get excalidrawAPI():ExcalidrawImperativeAPI {
return this.view.excalidrawAPI;
}
get plugin(): ExcalidrawPlugin {
return this.view.plugin;
}
get file(): TFile {
return this.view.file;
}
public onDrop (event: React.DragEvent<HTMLDivElement>): boolean {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onDrop, "ExcalidrawView.onDrop", event);
if(this.draginfoDiv) {
this.ownerDocument.body.removeChild(this.draginfoDiv);
delete this.draginfoDiv;
}
const api = this.excalidrawAPI;
if (!api) {
return false;
}
const st: AppState = api.getAppState();
this.currentPosition = viewportCoordsToSceneCoords(
{ clientX: event.clientX, clientY: event.clientY },
st,
);
const draggable = (this.app as any).dragManager.draggable;
const internalDragAction = internalDragModifierType(event);
const externalDragAction = webbrowserDragModifierType(event);
const localFileDragAction = localFileDragModifierType(event);
//Call Excalidraw Automate onDropHook
const onDropHook = (
type: "file" | "text" | "unknown",
files: TFile[],
text: string,
): boolean => {
if (this.view.getHookServer().onDropHook) {
try {
return this.view.getHookServer().onDropHook({
ea: this.view.getHookServer(), //the ExcalidrawAutomate object
event, //React.DragEvent<HTMLDivElement>
draggable, //Obsidian draggable object
type, //"file"|"text"
payload: {
files, //TFile[] array of dropped files
text, //string
},
excalidrawFile: this.file, //the file receiving the drop event
view: this.view, //the excalidraw view receiving the drop
pointerPosition: this.currentPosition, //the pointer position on canvas at the time of drop
});
} catch (e) {
new Notice("on drop hook error. See console log for details");
errorlog({ where: "ExcalidrawView.onDrop", error: e });
return false;
}
} else {
return false;
}
};
//---------------------------------------------------------------------------------
// Obsidian internal drag event
//---------------------------------------------------------------------------------
switch (draggable?.type) {
case "file":
if (!onDropHook("file", [draggable.file], null)) {
const file:TFile = draggable.file;
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/422
if (file.path.match(REG_LINKINDEX_INVALIDCHARS)) {
new Notice(t("FILENAME_INVALID_CHARS"), 4000);
return false;
}
if (
["image", "image-fullsize"].contains(internalDragAction) &&
(IMAGE_TYPES.contains(file.extension) ||
file.extension === "md" ||
file.extension.toLowerCase() === "pdf" )
) {
if(file.extension.toLowerCase() === "pdf") {
const insertPDFModal = new InsertPDFModal(this.plugin, this.view);
insertPDFModal.open(file);
} else {
(async () => {
const ea: ExcalidrawAutomate = getEA(this.view);
ea.selectElementsInView([
await insertImageToView(
ea,
this.currentPosition,
file,
!(internalDragAction==="image-fullsize")
)
]);
ea.destroy();
})();
}
return false;
}
if (internalDragAction === "embeddable") {
(async () => {
const ea: ExcalidrawAutomate = getEA(this.view);
ea.selectElementsInView([
await insertEmbeddableToView(
ea,
this.currentPosition,
file,
)
]);
ea.destroy();
})();
return false;
}
//internalDragAction === "link"
this.view.addText(
`[[${this.app.metadataCache.fileToLinktext(
draggable.file,
this.file.path,
true,
)}]]`,
);
}
return false;
case "files":
if (!onDropHook("file", draggable.files, null)) {
(async () => {
if (["image", "image-fullsize"].contains(internalDragAction)) {
const ea:ExcalidrawAutomate = getEA(this.view);
ea.canvas.theme = api.getAppState().theme;
let counter:number = 0;
const ids:string[] = [];
for (const f of draggable.files) {
if ((IMAGE_TYPES.contains(f.extension) || f.extension === "md")) {
ids.push(await ea.addImage(
this.currentPosition.x + counter*50,
this.currentPosition.y + counter*50,
f,
!(internalDragAction==="image-fullsize"),
));
counter++;
await ea.addElementsToView(false, false, true);
ea.selectElementsInView(ids);
}
if (f.extension.toLowerCase() === "pdf") {
const insertPDFModal = new InsertPDFModal(this.plugin, this.view);
insertPDFModal.open(f);
}
}
ea.destroy();
return;
}
if (internalDragAction === "embeddable") {
const ea:ExcalidrawAutomate = getEA(this.view);
let column:number = 0;
let row:number = 0;
const ids:string[] = [];
for (const f of draggable.files) {
ids.push(await insertEmbeddableToView(
ea,
{
x:this.currentPosition.x + column*500,
y:this.currentPosition.y + row*550
},
f,
));
column = (column + 1) % 3;
if(column === 0) {
row++;
}
}
ea.destroy();
return false;
}
//internalDragAction === "link"
for (const f of draggable.files) {
await this.view.addText(
`[[${this.app.metadataCache.fileToLinktext(
f,
this.file.path,
true,
)}]]`, undefined,false
);
this.currentPosition.y += st.currentItemFontSize * 2;
}
this.view.save(false);
})();
}
return false;
}
//---------------------------------------------------------------------------------
// externalDragAction
//---------------------------------------------------------------------------------
if (event.dataTransfer.types.includes("Files")) {
if (event.dataTransfer.types.includes("text/plain")) {
const text: string = event.dataTransfer.getData("text");
if (text && onDropHook("text", null, text)) {
return false;
}
if(text && (externalDragAction === "image-url") && hyperlinkIsImage(text)) {
this.view.addImageWithURL(text);
return false;
}
if(text && (externalDragAction === "link")) {
if (
this.plugin.settings.iframelyAllowed &&
text.match(/^https?:\/\/\S*$/)
) {
this.view.addTextWithIframely(text);
return false;
} else {
this.view.addText(text);
return false;
}
}
if(text && (externalDragAction === "embeddable")) {
const ea = getEA(this.view) as ExcalidrawAutomate;
insertEmbeddableToView(
ea,
this.currentPosition,
undefined,
text,
).then(()=>ea.destroy());
return false;
}
}
if(event.dataTransfer.types.includes("text/html")) {
const html = event.dataTransfer.getData("text/html");
const src = html.match(/src=["']([^"']*)["']/)
if(src && (externalDragAction === "image-url") && hyperlinkIsImage(src[1])) {
this.view.addImageWithURL(src[1]);
return false;
}
if(src && (externalDragAction === "link")) {
if (
this.plugin.settings.iframelyAllowed &&
src[1].match(/^https?:\/\/\S*$/)
) {
this.view.addTextWithIframely(src[1]);
return false;
} else {
this.view.addText(src[1]);
return false;
}
}
if(src && (externalDragAction === "embeddable")) {
const ea = getEA(this.view) as ExcalidrawAutomate;
insertEmbeddableToView(
ea,
this.currentPosition,
undefined,
src[1],
).then(ea.destroy);
return false;
}
}
if (event.dataTransfer.types.length >= 1 && ["image-url","image-import","embeddable"].contains(localFileDragAction)) {
const files = Array.from(event.dataTransfer.files || []);
for(let i = 0; i < files.length; i++) {
// Try multiple ways to get file path
const file = files[i];
let path = file?.path
if(!path && file && DEVICE.isDesktop) {
//https://www.electronjs.org/docs/latest/breaking-changes#removed-filepath
const { webUtils } = require('electron');
if(webUtils && webUtils.getPathForFile) {
path = webUtils.getPathForFile(file);
}
}
if(!path) {
new Notice(t("ERROR_CANT_READ_FILEPATH"),6000);
return true; //excalidarw to continue processing
}
const link = getInternalLinkOrFileURLLink(path, this.plugin, event.dataTransfer.files[i].name, this.file);
const {x,y} = this.currentPosition;
const pos = {x:x+i*300, y:y+i*300};
if(link.isInternal) {
if(localFileDragAction === "embeddable") {
const ea = getEA(this.view) as ExcalidrawAutomate;
insertEmbeddableToView(ea, pos, link.file).then(()=>ea.destroy());
} else {
if(link.file.extension === "pdf") {
const insertPDFModal = new InsertPDFModal(this.plugin, this.view);
insertPDFModal.open(link.file);
}
const ea = getEA(this.view) as ExcalidrawAutomate;
insertImageToView(ea, pos, link.file).then(()=>ea.destroy()) ;
}
} else {
const extension = getURLImageExtension(link.url);
if(localFileDragAction === "image-import") {
if (IMAGE_TYPES.contains(extension)) {
(async () => {
const droppedFilename = event.dataTransfer.files[i].name;
const fileToImport = await event.dataTransfer.files[i].arrayBuffer();
let {folder:_, filepath} = await getAttachmentsFolderAndFilePath(this.app, this.file.path, droppedFilename);
const maybeFile = this.app.vault.getAbstractFileByPath(filepath);
if(maybeFile && maybeFile instanceof TFile) {
const action = await ScriptEngine.suggester(
this.app,[
"Use the file already in the Vault instead of importing",
"Overwrite existing file in the Vault",
"Import the file with a new name",
],[
"Use",
"Overwrite",
"Import",
],
"A file with the same name/path already exists in the Vault",
);
switch(action) {
case "Import":
const {folderpath,filename,basename:_,extension:__} = splitFolderAndFilename(filepath);
filepath = getNewUniqueFilepath(this.app.vault, filename, folderpath);
break;
case "Overwrite":
await this.app.vault.modifyBinary(maybeFile, fileToImport);
// there is deliberately no break here
case "Use":
default:
const ea = getEA(this.view) as ExcalidrawAutomate;
await insertImageToView(ea, pos, maybeFile);
ea.destroy();
return false;
}
}
const file = await this.app.vault.createBinary(filepath, fileToImport)
const ea = getEA(this.view) as ExcalidrawAutomate;
await insertImageToView(ea, pos, file);
ea.destroy();
})();
} else if(extension === "excalidraw") {
return true; //excalidarw to continue processing
} else {
(async () => {
const {folder:_, filepath} = await getAttachmentsFolderAndFilePath(this.app, this.file.path,event.dataTransfer.files[i].name);
const file = await this.app.vault.createBinary(filepath, await event.dataTransfer.files[i].arrayBuffer());
const modal = new UniversalInsertFileModal(this.plugin, this.view);
modal.open(file, pos);
})();
}
}
else if(localFileDragAction === "embeddable" || !IMAGE_TYPES.contains(extension)) {
const ea = getEA(this.view) as ExcalidrawAutomate;
insertEmbeddableToView(ea, pos, null, link.url).then(()=>ea.destroy());
if(localFileDragAction !== "embeddable") {
new Notice("Not imported to Vault. Embedded with local URI");
}
} else {
const ea = getEA(this.view) as ExcalidrawAutomate;
insertImageToView(ea, pos, link.url).then(()=>ea.destroy());
}
}
};
return false;
}
if(event.dataTransfer.types.length >= 1 && localFileDragAction === "link") {
const ea = getEA(this.view) as ExcalidrawAutomate;
for(let i=0;i<event.dataTransfer.files.length;i++) {
const file = event.dataTransfer.files[i];
let path = file?.path;
const name = file?.name;
if(!path && file && DEVICE.isDesktop) {
//https://www.electronjs.org/docs/latest/breaking-changes#removed-filepath
const { webUtils } = require('electron');
if(webUtils && webUtils.getPathForFile) {
path = webUtils.getPathForFile(file);
}
}
if(!path || !name) {
new Notice(t("ERROR_CANT_READ_FILEPATH"),6000);
ea.destroy();
return true; //excalidarw to continue processing
}
const link = getInternalLinkOrFileURLLink(path, this.plugin, name, this.file);
const id = ea.addText(
this.currentPosition.x+i*40,
this.currentPosition.y+i*20,
link.isInternal ? link.link :`📂 ${name}`);
if(!link.isInternal) {
ea.getElement(id).link = link.link;
}
}
ea.addElementsToView().then(()=>ea.destroy());
return false;
}
return true;
}
if (event.dataTransfer.types.includes("text/plain") || event.dataTransfer.types.includes("text/uri-list") || event.dataTransfer.types.includes("text/html")) {
const html = event.dataTransfer.getData("text/html");
const src = html.match(/src=["']([^"']*)["']/);
const htmlText = src ? src[1] : "";
const textText = event.dataTransfer.getData("text");
const uriText = event.dataTransfer.getData("text/uri-list");
let text: string = src ? htmlText : textText;
if (!text || text === "") {
text = uriText
}
if (!text || text === "") {
return true;
}
if (!onDropHook("text", null, text)) {
if(text && (externalDragAction==="embeddable") && /^(blob:)?(http|https):\/\/[^\s/$.?#].[^\s]*$/.test(text)) {
return true;
}
if(text && (externalDragAction==="image-url") && hyperlinkIsYouTubeLink(text)) {
this.view.addYouTubeThumbnail(text);
return false;
}
if(uriText && (externalDragAction==="image-url") && hyperlinkIsYouTubeLink(uriText)) {
this.view.addYouTubeThumbnail(uriText);
return false;
}
if(text && (externalDragAction==="image-url") && hyperlinkIsImage(text)) {
this.view.addImageWithURL(text);
return false;
}
if(uriText && (externalDragAction==="image-url") && hyperlinkIsImage(uriText)) {
this.view.addImageWithURL(uriText);
return false;
}
if(text && (externalDragAction==="image-import") && hyperlinkIsImage(text)) {
this.view.addImageSaveToVault(text);
return false;
}
if(uriText && (externalDragAction==="image-import") && hyperlinkIsImage(uriText)) {
this.view.addImageSaveToVault(uriText);
return false;
}
if (
this.plugin.settings.iframelyAllowed &&
text.match(/^https?:\/\/\S*$/)
) {
this.view.addTextWithIframely(text);
return false;
}
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/599
if(text.startsWith("obsidian://open?vault=")) {
const html = event.dataTransfer.getData("text/html");
if(html) {
const path = html.match(/href="app:\/\/obsidian\.md\/(.*?)"/);
if(path.length === 2) {
const link = decodeURIComponent(path[1]).split("#");
const f = this.app.vault.getAbstractFileByPath(link[0]);
if(f && f instanceof TFile) {
const path = this.app.metadataCache.fileToLinktext(f,this.file.path);
this.view.addText(`[[${
path +
(link.length>1 ? "#" + link[1] + "|" + path : "")
}]]`);
return;
}
this.view.addText(`[[${decodeURIComponent(path[1])}]]`);
return false;
}
}
const path = text.split("file=");
if(path.length === 2) {
this.view.addText(`[[${decodeURIComponent(path[1])}]]`);
return false;
}
}
this.view.addText(text.replace(/(!\[\[.*#[^\]]*\]\])/g, "$1{40}"));
}
return false;
}
if (onDropHook("unknown", null, null)) {
return false;
}
return true;
}
public onDragOver(e: any) {
const action = this.dropAction(e.dataTransfer);
if (action) {
if(!this.draginfoDiv) {
this.draginfoDiv = createDiv({cls:"excalidraw-draginfo"});
this.ownerDocument.body.appendChild(this.draginfoDiv);
}
let msg: string = "";
if((this.app as any).dragManager.draggable) {
//drag from Obsidian file manager
msg = modifierKeyTooltipMessages().InternalDragAction[internalDragModifierType(e)];
} else if(e.dataTransfer.types.length === 1 && e.dataTransfer.types.includes("Files")) {
//drag from OS file manager
msg = modifierKeyTooltipMessages().LocalFileDragAction[localFileDragModifierType(e)];
if(DEVICE.isMacOS && isWinCTRLorMacCMD(e)) {
msg = "CMD is reserved by MacOS for file system drag actions.\nCan't use it in Obsidian.\nUse a combination of SHIFT, CTRL, OPT instead."
}
} else {
//drag from Internet
msg = modifierKeyTooltipMessages().WebBrowserDragAction[webbrowserDragModifierType(e)];
}
if(!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
msg += DEVICE.isMacOS || DEVICE.isIOS
? "\nTry SHIFT, OPT, CTRL combinations for other drop actions"
: "\nTry SHIFT, CTRL, ALT, Meta combinations for other drop actions";
}
if(this.draginfoDiv.innerText !== msg) this.draginfoDiv.innerText = msg;
const top = `${e.clientY-parseFloat(getComputedStyle(this.draginfoDiv).fontSize)*8}px`;
const left = `${e.clientX-this.draginfoDiv.clientWidth/2}px`;
if(this.draginfoDiv.style.top !== top) this.draginfoDiv.style.top = top;
if(this.draginfoDiv.style.left !== left) this.draginfoDiv.style.left = left;
e.dataTransfer.dropEffect = action;
e.preventDefault();
return false;
}
}
public onDragLeave() {
if(this.draginfoDiv) {
this.ownerDocument.body.removeChild(this.draginfoDiv);
delete this.draginfoDiv;
}
}
private dropAction(transfer: DataTransfer) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.dropAction, "ExcalidrawView.dropAction");
// Return a 'copy' or 'link' action according to the content types, or undefined if no recognized type
const files = (this.app as any).dragManager.draggable?.files;
if (files) {
if (files[0] == this.file) {
files.shift();
(
this.app as any
).dragManager.draggable.title = `${files.length} files`;
}
}
if (
["file", "files"].includes(
(this.app as any).dragManager.draggable?.type,
)
) {
return "link";
}
if (
transfer.types?.includes("text/html") ||
transfer.types?.includes("text/plain") ||
transfer.types?.includes("Files")
) {
return "copy";
}
};
}