From eebc428f1bc3be834fcf1f5222ccc54e387de883 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Thu, 19 Dec 2024 22:29:51 +0100 Subject: [PATCH] major file reorganization --- docs/API/ExcalidrawAutomate.d.ts | 8 +- rollup.config.js | 6 +- .../CodeMirrorExtension/EditorHandler.ts | 4 +- src/{ => Core}/CodeMirrorExtension/Fadeout.ts | 0 src/{ => Core}/Managers/CommandManager.ts | 60 +- src/{ => Core}/Managers/EventManager.ts | 16 +- src/{ => Core}/Managers/FileManager.ts | 22 +- .../Managers}/MarkdownPostProcessor.ts | 2084 +-- src/{ => Core}/Managers/ObserverManager.ts | 8 +- src/{ => Core}/Managers/PackageManager.ts | 10 +- src/{ => Core}/index.ts | 26 +- src/{ => Core}/main.ts | 2880 ++-- src/{ => Core}/settings.ts | 49 +- src/OneOffs.ts | 368 - ...ddableMDFileCustomDataSettingsComponent.ts | 4 +- .../Dialogs}/EmbeddableSettings.ts | 18 +- .../Dialogs}/ExportDialog.ts | 12 +- .../Dialogs}/FrameSettings.ts | 4 +- .../Dialogs}/HotkeyEditor.ts | 10 +- .../Dialogs}/ImportSVGDialog.ts | 12 +- .../Dialogs}/InsertCommandDialog.ts | 4 +- .../Dialogs}/InsertImageDialog.ts | 14 +- .../Dialogs}/InsertLinkDialog.ts | 10 +- .../Dialogs}/InsertMDDialog.ts | 10 +- .../Dialogs}/InsertPDFModal.ts | 14 +- src/{dialogs => Shared/Dialogs}/Messages.ts | 0 .../Dialogs}/ModifierKeySettings.ts | 6 +- .../Dialogs}/OpenDrawing.ts | 6 +- .../Dialogs}/PenSettingsModal.ts | 16 +- src/{dialogs => Shared/Dialogs}/Prompt.ts | 27 +- .../Dialogs}/PublishOutOfDateFiles.ts | 6 +- .../Dialogs}/RankMessage.ts | 4 +- .../Dialogs}/ReleaseNotes.ts | 4 +- .../Dialogs}/ScriptInstallPrompt.ts | 6 +- src/{dialogs => Shared/Dialogs}/SelectCard.ts | 12 +- .../Dialogs}/SuggesterInfo.ts | 0 .../Dialogs}/UniversalInsertFileModal.ts | 16 +- src/{ => Shared}/EmbeddedFileLoader.ts | 2302 +-- src/{ => Shared}/ExcalidrawAutomate.ts | 7118 ++++----- src/{ => Shared}/ExcalidrawData.ts | 26 +- src/{ => Shared}/LaTeX.ts | 2 +- src/{ocr => Shared/OCR}/Taskbone.ts | 14 +- src/{ => Shared}/Scripts.ts | 21 +- .../Suggesters/FieldSuggester.ts | 6 +- .../Suggesters/FileSuggestionModal.ts | 8 +- .../Suggesters/FolderSuggestionModal.ts | 0 .../Suggesters/PathSuggestionModal.ts | 0 .../Suggesters/Suggester.ts | 0 .../Suggesters/SuggestionModal.ts | 0 .../Workers}/compression-worker.ts | 0 .../svgToExcalidraw/attributes.ts | 0 .../elements/ExcalidrawElement.ts | 0 .../elements/ExcalidrawScene.ts | 2 +- .../svgToExcalidraw/elements/Group.ts | 0 .../svgToExcalidraw/elements/index.ts | 0 .../svgToExcalidraw/elements/path/index.ts | 0 .../elements/path/utils/bezier.ts | 0 .../elements/path/utils/ellipse.ts | 0 .../elements/path/utils/path-to-points.ts | 0 .../svgToExcalidraw/elements/utils.ts | 0 src/{ => Shared}/svgToExcalidraw/parser.ts | 0 src/{ => Shared}/svgToExcalidraw/readme.md | 0 src/{ => Shared}/svgToExcalidraw/transform.ts | 0 src/{ => Shared}/svgToExcalidraw/types.ts | 0 src/{ => Shared}/svgToExcalidraw/utils.ts | 0 src/{ => Shared}/svgToExcalidraw/walker.ts | 2 +- .../Components/Menu}/ActionButton.tsx | 0 .../Components/Menu}/ActionIcons.tsx | 2 +- .../Menu}/EmbeddableActionsMenu.tsx | 22 +- .../Components/Menu}/ObsidianMenu.tsx | 18 +- .../Components/Menu}/ToolsPanel.tsx | 1478 +- .../Components}/customEmbeddable.tsx | 12 +- src/{dialogs => View}/ExcalidrawLoading.ts | 6 +- src/{ => View}/ExcalidrawView.ts | 12892 ++++++++-------- src/constants/{ => Assets}/startupScript.md | 0 src/constants/constants.ts | 6 +- src/lang/helpers.ts | 4 +- src/lang/locale/en.ts | 6 +- src/lang/locale/ru.ts | 6 +- src/lang/locale/zh-cn.ts | 6 +- src/{ => types}/ExcalidrawLib.d.ts | 0 src/types/ExcalidrawViewTypes.ts | 73 + src/types/types.d.ts | 4 +- src/utils/AIUtils.ts | 3 +- src/utils/CJKLoader.ts | 2 +- src/utils/CanvasNodeFactory.ts | 2 +- src/utils/CarveOut.ts | 4 +- src/utils/CropImage.ts | 8 +- src/utils/CustomEmbeddableUtils.ts | 4 +- src/utils/DynamicStyling.ts | 10 +- src/utils/ExcalidrawConfig.ts | 4 +- src/utils/ExcalidrawViewUtils.ts | 14 +- src/utils/FileUtils.ts | 8 +- src/utils/GetElementAtPointer.ts | 6 +- src/utils/ImageCache.ts | 2 +- src/utils/ModifierkeyHelper.ts | 10 +- src/utils/ObsidianUtils.ts | 6 +- src/utils/Pens.ts | 2 +- src/utils/StylesManager.ts | 2 +- src/utils/Utils.ts | 14 +- src/utils/matic.ts | 2 +- tsconfig.dev.json | 2 +- tsconfig.json | 4 +- 103 files changed, 14783 insertions(+), 15118 deletions(-) rename src/{ => Core}/CodeMirrorExtension/EditorHandler.ts (94%) rename src/{ => Core}/CodeMirrorExtension/Fadeout.ts (100%) rename src/{ => Core}/Managers/CommandManager.ts (97%) rename src/{ => Core}/Managers/EventManager.ts (96%) rename src/{ => Core}/Managers/FileManager.ts (96%) rename src/{ => Core/Managers}/MarkdownPostProcessor.ts (94%) rename src/{ => Core}/Managers/ObserverManager.ts (97%) rename src/{ => Core}/Managers/PackageManager.ts (90%) rename src/{ => Core}/index.ts (90%) rename src/{ => Core}/main.ts (94%) rename src/{ => Core}/settings.ts (98%) delete mode 100644 src/OneOffs.ts rename src/{dialogs => Shared/Dialogs}/EmbeddableMDFileCustomDataSettingsComponent.ts (98%) rename src/{dialogs => Shared/Dialogs}/EmbeddableSettings.ts (94%) rename src/{dialogs => Shared/Dialogs}/ExportDialog.ts (96%) rename src/{dialogs => Shared/Dialogs}/FrameSettings.ts (96%) rename src/{dialogs => Shared/Dialogs}/HotkeyEditor.ts (94%) rename src/{dialogs => Shared/Dialogs}/ImportSVGDialog.ts (83%) rename src/{dialogs => Shared/Dialogs}/InsertCommandDialog.ts (90%) rename src/{dialogs => Shared/Dialogs}/InsertImageDialog.ts (87%) rename src/{dialogs => Shared/Dialogs}/InsertLinkDialog.ts (94%) rename src/{dialogs => Shared/Dialogs}/InsertMDDialog.ts (84%) rename src/{dialogs => Shared/Dialogs}/InsertPDFModal.ts (97%) rename src/{dialogs => Shared/Dialogs}/Messages.ts (100%) rename src/{dialogs => Shared/Dialogs}/ModifierKeySettings.ts (95%) rename src/{dialogs => Shared/Dialogs}/OpenDrawing.ts (93%) rename src/{dialogs => Shared/Dialogs}/PenSettingsModal.ts (98%) rename src/{dialogs => Shared/Dialogs}/Prompt.ts (97%) rename src/{dialogs => Shared/Dialogs}/PublishOutOfDateFiles.ts (97%) rename src/{dialogs => Shared/Dialogs}/RankMessage.ts (98%) rename src/{dialogs => Shared/Dialogs}/ReleaseNotes.ts (93%) rename src/{dialogs => Shared/Dialogs}/ScriptInstallPrompt.ts (97%) rename src/{dialogs => Shared/Dialogs}/SelectCard.ts (82%) rename src/{dialogs => Shared/Dialogs}/SuggesterInfo.ts (100%) rename src/{dialogs => Shared/Dialogs}/UniversalInsertFileModal.ts (95%) rename src/{ => Shared}/EmbeddedFileLoader.ts (95%) rename src/{ => Shared}/ExcalidrawAutomate.ts (95%) rename src/{ => Shared}/ExcalidrawData.ts (99%) rename src/{ => Shared}/LaTeX.ts (97%) rename src/{ocr => Shared/OCR}/Taskbone.ts (93%) rename src/{ => Shared}/Scripts.ts (95%) rename src/{Components => Shared}/Suggesters/FieldSuggester.ts (95%) rename src/{Components => Shared}/Suggesters/FileSuggestionModal.ts (96%) rename src/{Components => Shared}/Suggesters/FolderSuggestionModal.ts (100%) rename src/{Components => Shared}/Suggesters/PathSuggestionModal.ts (100%) rename src/{Components => Shared}/Suggesters/Suggester.ts (100%) rename src/{Components => Shared}/Suggesters/SuggestionModal.ts (100%) rename src/{workers => Shared/Workers}/compression-worker.ts (100%) rename src/{ => Shared}/svgToExcalidraw/attributes.ts (100%) rename src/{ => Shared}/svgToExcalidraw/elements/ExcalidrawElement.ts (100%) rename src/{ => Shared}/svgToExcalidraw/elements/ExcalidrawScene.ts (88%) rename src/{ => Shared}/svgToExcalidraw/elements/Group.ts (100%) rename src/{ => Shared}/svgToExcalidraw/elements/index.ts (100%) rename src/{ => Shared}/svgToExcalidraw/elements/path/index.ts (100%) rename src/{ => Shared}/svgToExcalidraw/elements/path/utils/bezier.ts (100%) rename src/{ => Shared}/svgToExcalidraw/elements/path/utils/ellipse.ts (100%) rename src/{ => Shared}/svgToExcalidraw/elements/path/utils/path-to-points.ts (100%) rename src/{ => Shared}/svgToExcalidraw/elements/utils.ts (100%) rename src/{ => Shared}/svgToExcalidraw/parser.ts (100%) rename src/{ => Shared}/svgToExcalidraw/readme.md (100%) rename src/{ => Shared}/svgToExcalidraw/transform.ts (100%) rename src/{ => Shared}/svgToExcalidraw/types.ts (100%) rename src/{ => Shared}/svgToExcalidraw/utils.ts (100%) rename src/{ => Shared}/svgToExcalidraw/walker.ts (99%) rename src/{menu => View/Components/Menu}/ActionButton.tsx (100%) rename src/{menu => View/Components/Menu}/ActionIcons.tsx (99%) rename src/{menu => View/Components/Menu}/EmbeddableActionsMenu.tsx (94%) rename src/{menu => View/Components/Menu}/ObsidianMenu.tsx (95%) rename src/{menu => View/Components/Menu}/ToolsPanel.tsx (93%) rename src/{ => View/Components}/customEmbeddable.tsx (98%) rename src/{dialogs => View}/ExcalidrawLoading.ts (88%) rename src/{ => View}/ExcalidrawView.ts (95%) rename src/constants/{ => Assets}/startupScript.md (100%) rename src/{ => types}/ExcalidrawLib.d.ts (100%) create mode 100644 src/types/ExcalidrawViewTypes.ts diff --git a/docs/API/ExcalidrawAutomate.d.ts b/docs/API/ExcalidrawAutomate.d.ts index 1138c22..2813b4a 100644 --- a/docs/API/ExcalidrawAutomate.d.ts +++ b/docs/API/ExcalidrawAutomate.d.ts @@ -1,16 +1,16 @@ /// -import ExcalidrawPlugin from "src/main"; +import ExcalidrawPlugin from "src/Core/main"; import { FillStyle, StrokeStyle, ExcalidrawElement, ExcalidrawBindableElement, FileId, NonDeletedExcalidrawElement, ExcalidrawImageElement, StrokeRoundness, RoundnessType } from "@zsviczian/excalidraw/types/excalidraw/element/types"; import { Editor, OpenViewState, TFile, WorkspaceLeaf } from "obsidian"; import * as obsidian_module from "obsidian"; -import ExcalidrawView, { ExportSettings } from "src/ExcalidrawView"; +import ExcalidrawView, { ExportSettings } from "src/View/ExcalidrawView"; import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/excalidraw/types"; import { EmbeddedFilesLoader } from "src/EmbeddedFileLoader"; -import { ConnectionPoint, DeviceType } from "src/types/types"; +import { ConnectionPoint, DeviceType } from "src/Types/Types"; import { ColorMaster } from "colormaster"; import { TInput } from "colormaster/types"; import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard"; -import { PaneTarget } from "src/utils/ModifierkeyHelper"; +import { PaneTarget } from "src/Utils/ModifierkeyHelper"; export declare class ExcalidrawAutomate { /** * Utility function that returns the Obsidian Module object. diff --git a/rollup.config.js b/rollup.config.js index 86967c1..a9d83b5 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -48,7 +48,7 @@ function minifyCode(code) { } function compressLanguageFile(lang) { - const inputDir = "./src/lang/locale"; + const inputDir = "./src/Lang/Locale"; const filePath = `${inputDir}/${lang}.ts`; let content = fs.readFileSync(filePath, "utf-8"); content = trimLastSemicolon(content.split("export default")[1].trim()); @@ -102,7 +102,7 @@ const packageString = isLib 'const PLUGIN_VERSION="' + manifest.version + '";'; const BASE_CONFIG = { - input: 'src/main.ts', + input: 'src/Core/main.ts', external: [ '@codemirror/autocomplete', '@codemirror/collab', @@ -166,7 +166,7 @@ const BUILD_CONFIG = { const LIB_CONFIG = { ...BASE_CONFIG, - input: "src/index.ts", + input: "src/Core/index.ts", output: { dir: "lib", sourcemap: true, diff --git a/src/CodeMirrorExtension/EditorHandler.ts b/src/Core/CodeMirrorExtension/EditorHandler.ts similarity index 94% rename from src/CodeMirrorExtension/EditorHandler.ts rename to src/Core/CodeMirrorExtension/EditorHandler.ts index 15a7e0d..d53af3d 100644 --- a/src/CodeMirrorExtension/EditorHandler.ts +++ b/src/Core/CodeMirrorExtension/EditorHandler.ts @@ -1,7 +1,7 @@ import { Extension } from "@codemirror/state"; -import ExcalidrawPlugin from "src/main"; +import ExcalidrawPlugin from "src/Core/main"; import { HideTextBetweenCommentsExtension } from "./Fadeout"; -import { debug, DEBUGGING } from "src/utils/DebugHelper"; +import { debug, DEBUGGING } from "src/Utils/DebugHelper"; export const EDITOR_FADEOUT = "fadeOutExcalidrawMarkup"; const editorExtensions: {[key:string]:Extension}= { diff --git a/src/CodeMirrorExtension/Fadeout.ts b/src/Core/CodeMirrorExtension/Fadeout.ts similarity index 100% rename from src/CodeMirrorExtension/Fadeout.ts rename to src/Core/CodeMirrorExtension/Fadeout.ts diff --git a/src/Managers/CommandManager.ts b/src/Core/Managers/CommandManager.ts similarity index 97% rename from src/Managers/CommandManager.ts rename to src/Core/Managers/CommandManager.ts index 7fde449..8e0ea44 100644 --- a/src/Managers/CommandManager.ts +++ b/src/Core/Managers/CommandManager.ts @@ -16,25 +16,25 @@ import { IMAGE_TYPES, DEVICE, sceneCoordsToViewportCoords, -} from "../constants/constants"; -import ExcalidrawView, { TextMode } from "../ExcalidrawView"; +} from "../../Constants/Constants"; +import ExcalidrawView, { TextMode } from "../../View/ExcalidrawView"; import { REGEX_LINK, -} from "../ExcalidrawData"; +} from "../../Shared/ExcalidrawData"; import { ExcalidrawSettings } from "../settings"; -import { openDialogAction, OpenFileDialog } from "../dialogs/OpenDrawing"; -import { InsertLinkDialog } from "../dialogs/InsertLinkDialog"; -import { InsertCommandDialog } from "../dialogs/InsertCommandDialog"; -import { InsertImageDialog } from "../dialogs/InsertImageDialog"; -import { ImportSVGDialog } from "../dialogs/ImportSVGDialog"; -import { InsertMDDialog } from "../dialogs/InsertMDDialog"; +import { openDialogAction, OpenFileDialog } from "../../Shared/Dialogs/OpenDrawing"; +import { InsertLinkDialog } from "../../Shared/Dialogs/InsertLinkDialog"; +import { InsertCommandDialog } from "../../Shared/Dialogs/InsertCommandDialog"; +import { InsertImageDialog } from "../../Shared/Dialogs/InsertImageDialog"; +import { ImportSVGDialog } from "../../Shared/Dialogs/ImportSVGDialog"; +import { InsertMDDialog } from "../../Shared/Dialogs/InsertMDDialog"; import { ExcalidrawAutomate, insertLaTeXToView, search, -} from "../ExcalidrawAutomate"; -import { templatePromt } from "../dialogs/Prompt"; -import { t } from "../lang/helpers"; +} from "../../Shared/ExcalidrawAutomate"; +import { templatePromt } from "../../Shared/Dialogs/Prompt"; +import { t } from "../../Lang/Helpers"; import { getAliasWithSize, getAnnotationFileNameAndFolder, @@ -45,32 +45,32 @@ import { getLink, getListOfTemplateFiles, getURLImageExtension, -} from "../utils/FileUtils"; +} from "../../Utils/FileUtils"; import { setLeftHandedMode, sleep, decompress, getImageSize, -} from "../utils/Utils"; -import { extractSVGPNGFileName, getActivePDFPageNumberFromPDFView, getAttachmentsFolderAndFilePath, isObsidianThemeDark, mergeMarkdownFiles, setExcalidrawView } from "../utils/ObsidianUtils"; +} from "../../Utils/Utils"; +import { extractSVGPNGFileName, getActivePDFPageNumberFromPDFView, getAttachmentsFolderAndFilePath, isObsidianThemeDark, mergeMarkdownFiles, setExcalidrawView } from "../../Utils/ObsidianUtils"; import { ExcalidrawElement, ExcalidrawEmbeddableElement, ExcalidrawImageElement, ExcalidrawTextElement } from "@zsviczian/excalidraw/types/excalidraw/element/types"; -import { ReleaseNotes } from "../dialogs/ReleaseNotes"; -import { ScriptInstallPrompt } from "../dialogs/ScriptInstallPrompt"; -import Taskbone from "../ocr/Taskbone"; -import { emulateCTRLClickForLinks, linkClickModifierType, PaneTarget } from "../utils/ModifierkeyHelper"; -import { InsertPDFModal } from "../dialogs/InsertPDFModal"; -import { ExportDialog } from "../dialogs/ExportDialog"; -import { UniversalInsertFileModal } from "../dialogs/UniversalInsertFileModal"; -import { PublishOutOfDateFilesDialog } from "../dialogs/PublishOutOfDateFiles"; -import { EmbeddableSettings } from "../dialogs/EmbeddableSettings"; -import { processLinkText } from "../utils/CustomEmbeddableUtils"; -import { getEA } from "src"; +import { ReleaseNotes } from "../../Shared/Dialogs/ReleaseNotes"; +import { ScriptInstallPrompt } from "../../Shared/Dialogs/ScriptInstallPrompt"; +import Taskbone from "../../Shared/OCR/Taskbone"; +import { emulateCTRLClickForLinks, linkClickModifierType, PaneTarget } from "../../Utils/ModifierkeyHelper"; +import { InsertPDFModal } from "../../Shared/Dialogs/InsertPDFModal"; +import { ExportDialog } from "../../Shared/Dialogs/ExportDialog"; +import { UniversalInsertFileModal } from "../../Shared/Dialogs/UniversalInsertFileModal"; +import { PublishOutOfDateFilesDialog } from "../../Shared/Dialogs/PublishOutOfDateFiles"; +import { EmbeddableSettings } from "../../Shared/Dialogs/EmbeddableSettings"; +import { processLinkText } from "../../Utils/CustomEmbeddableUtils"; +import { getEA } from "src/Core"; import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types"; import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types"; -import { carveOutImage, carveOutPDF, createImageCropperFile } from "../utils/CarveOut"; -import { showFrameSettings } from "../dialogs/FrameSettings"; -import { insertImageToView } from "../utils/ExcalidrawViewUtils"; -import ExcalidrawPlugin from "src/main"; +import { carveOutImage, carveOutPDF, createImageCropperFile } from "../../Utils/CarveOut"; +import { showFrameSettings } from "../../Shared/Dialogs/FrameSettings"; +import { insertImageToView } from "../../Utils/ExcalidrawViewUtils"; +import ExcalidrawPlugin from "src/Core/main"; declare const PLUGIN_VERSION:string; diff --git a/src/Managers/EventManager.ts b/src/Core/Managers/EventManager.ts similarity index 96% rename from src/Managers/EventManager.ts rename to src/Core/Managers/EventManager.ts index 1fb1abd..2582c0f 100644 --- a/src/Managers/EventManager.ts +++ b/src/Core/Managers/EventManager.ts @@ -1,13 +1,13 @@ import { WorkspaceLeaf, TFile, Editor, MarkdownView, MarkdownFileInfo, MetadataCache, App, EventRef, Menu, FileView } from "obsidian"; import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types"; -import { getLink } from "../utils/FileUtils"; -import { editorInsertText, getParentOfClass, setExcalidrawView } from "../utils/ObsidianUtils"; -import ExcalidrawPlugin from "src/main"; -import { DEBUGGING, debug } from "src/utils/DebugHelper"; -import { ExcalidrawAutomate } from "src/ExcalidrawAutomate"; -import { DEVICE, FRONTMATTER_KEYS, ICON_NAME, VIEW_TYPE_EXCALIDRAW } from "src/constants/constants"; -import ExcalidrawView from "src/ExcalidrawView"; -import { t } from "src/lang/helpers"; +import { getLink } from "../../Utils/FileUtils"; +import { editorInsertText, getParentOfClass, setExcalidrawView } from "../../Utils/ObsidianUtils"; +import ExcalidrawPlugin from "src/Core/main"; +import { DEBUGGING, debug } from "src/Utils/DebugHelper"; +import { ExcalidrawAutomate } from "src/Shared/ExcalidrawAutomate"; +import { DEVICE, FRONTMATTER_KEYS, ICON_NAME, VIEW_TYPE_EXCALIDRAW } from "src/Constants/Constants"; +import ExcalidrawView from "src/View/ExcalidrawView"; +import { t } from "src/Lang/Helpers"; /** * Registers event listeners for the plugin diff --git a/src/Managers/FileManager.ts b/src/Core/Managers/FileManager.ts similarity index 96% rename from src/Managers/FileManager.ts rename to src/Core/Managers/FileManager.ts index 4dc2ffb..d9bdf2c 100644 --- a/src/Managers/FileManager.ts +++ b/src/Core/Managers/FileManager.ts @@ -1,15 +1,15 @@ -import { debug } from "src/utils/DebugHelper"; +import { debug } from "src/Utils/DebugHelper"; import { App, FrontMatterCache, MarkdownView, MetadataCache, normalizePath, Notice, TAbstractFile, TFile, WorkspaceLeaf } from "obsidian"; -import { BLANK_DRAWING, DARK_BLANK_DRAWING, DEVICE, EXPORT_TYPES, FRONTMATTER, FRONTMATTER_KEYS, JSON_parse, nanoid, VIEW_TYPE_EXCALIDRAW } from "src/constants/constants"; -import { Prompt, templatePromt } from "src/dialogs/Prompt"; -import { changeThemeOfExcalidrawMD, ExcalidrawData, getMarkdownDrawingSection } from "src/ExcalidrawData"; -import ExcalidrawView, { getTextMode } from "src/ExcalidrawView"; -import ExcalidrawPlugin from "src/main"; -import { DEBUGGING } from "src/utils/DebugHelper"; -import { checkAndCreateFolder, download, getIMGFilename, getLink, getListOfTemplateFiles, getNewUniqueFilepath } from "src/utils/FileUtils"; -import { PaneTarget } from "src/utils/ModifierkeyHelper"; -import { getExcalidrawViews, getNewOrAdjacentLeaf, isObsidianThemeDark, openLeaf } from "src/utils/ObsidianUtils"; -import { errorlog, getExportTheme } from "src/utils/Utils"; +import { BLANK_DRAWING, DARK_BLANK_DRAWING, DEVICE, EXPORT_TYPES, FRONTMATTER, FRONTMATTER_KEYS, JSON_parse, nanoid, VIEW_TYPE_EXCALIDRAW } from "src/Constants/Constants"; +import { Prompt, templatePromt } from "src/Shared/Dialogs/Prompt"; +import { changeThemeOfExcalidrawMD, ExcalidrawData, getMarkdownDrawingSection } from "../../Shared/ExcalidrawData"; +import ExcalidrawView, { getTextMode } from "src/View/ExcalidrawView"; +import ExcalidrawPlugin from "src/Core/main"; +import { DEBUGGING } from "src/Utils/DebugHelper"; +import { checkAndCreateFolder, download, getIMGFilename, getLink, getListOfTemplateFiles, getNewUniqueFilepath } from "src/Utils/FileUtils"; +import { PaneTarget } from "src/Utils/ModifierkeyHelper"; +import { getExcalidrawViews, getNewOrAdjacentLeaf, isObsidianThemeDark, openLeaf } from "src/Utils/ObsidianUtils"; +import { errorlog, getExportTheme } from "src/Utils/Utils"; export class PluginFileManager { private plugin: ExcalidrawPlugin; diff --git a/src/MarkdownPostProcessor.ts b/src/Core/Managers/MarkdownPostProcessor.ts similarity index 94% rename from src/MarkdownPostProcessor.ts rename to src/Core/Managers/MarkdownPostProcessor.ts index ea600ef..4f8bf98 100644 --- a/src/MarkdownPostProcessor.ts +++ b/src/Core/Managers/MarkdownPostProcessor.ts @@ -1,1042 +1,1042 @@ -import { - App, - MarkdownPostProcessorContext, - MetadataCache, - PaneType, - TFile, - Vault, -} from "obsidian"; -import { DEVICE, RERENDER_EVENT } from "./constants/constants"; -import { EmbeddedFilesLoader } from "./EmbeddedFileLoader"; -import { createPNG, createSVG } from "./ExcalidrawAutomate"; -import { ExportSettings } from "./ExcalidrawView"; -import ExcalidrawPlugin from "./main"; -import {getIMGFilename,} from "./utils/FileUtils"; -import { - getEmbeddedFilenameParts, - getExportTheme, - getQuickImagePreview, - getExportPadding, - getWithBackground, - hasExportTheme, - convertSVGStringToElement, - isMaskFile, -} from "./utils/Utils"; -import { getParentOfClass, isObsidianThemeDark, getFileCSSClasses } from "./utils/ObsidianUtils"; -import { linkClickModifierType } from "./utils/ModifierkeyHelper"; -import { ImageKey, imageCache } from "./utils/ImageCache"; -import { FILENAMEPARTS, PreviewImageType } from "./utils/UtilTypes"; -import { CustomMutationObserver, debug, DEBUGGING } from "./utils/DebugHelper"; -import { getExcalidrawFileForwardLinks } from "./utils/ExcalidrawViewUtils"; -import { linkPrompt } from "./dialogs/Prompt"; -import { isHTMLElement } from "./utils/typechecks"; - -interface imgElementAttributes { - file?: TFile; - fname: string; //Excalidraw filename - fwidth: string; //Display width of image - fheight: string; //Display height of image - style: string[]; //css style to apply to IMG element -} - -let plugin: ExcalidrawPlugin; -let app: App; -let vault: Vault; -let metadataCache: MetadataCache; -const DEBUGGING_MPP = false; - - -const getDefaultWidth = (plugin: ExcalidrawPlugin): string => { - const width = parseInt(plugin.settings.width); - if (isNaN(width) || width === 0 || width === null) { - if(getDefaultHeight(plugin)!=="") return ""; - return "400"; - } - return plugin.settings.width; -}; - -const getDefaultHeight = (plugin: ExcalidrawPlugin): string => { - const height = parseInt(plugin.settings.height); - if (isNaN(height) || height === 0 || height === null) { - return ""; - } - return plugin.settings.height; -}; - -export const initializeMarkdownPostProcessor = (p: ExcalidrawPlugin) => { - plugin = p; - app = plugin.app; - vault = app.vault; - metadataCache = app.metadataCache; -}; - -const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader}:{ - imgAttributes: imgElementAttributes, - filenameParts: FILENAMEPARTS, - theme: string, - cacheReady: boolean, - img: HTMLImageElement, - file: TFile, - exportSettings: ExportSettings, - loader: EmbeddedFilesLoader, -}):Promise => { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getPNG, `MarkdownPostProcessor.ts > _getPNG`); - const width = parseInt(imgAttributes.fwidth); - const scale = width >= 2400 - ? 5 - : width >= 1800 - ? 4 - : width >= 1200 - ? 3 - : width >= 600 - ? 2 - : 1; - - const cacheKey = { - ...filenameParts, - isDark: theme==="dark", - previewImageType: PreviewImageType.PNG, - scale, - isTransparent: !exportSettings.withBackground, - inlineFonts: true, //though for PNG this makes no difference, but the key requires it - }; - - if(cacheReady) { - const src = await imageCache.getImageFromCache(cacheKey); - //In case of PNG I cannot change the viewBox to select the area of the element - //being referenced. For PNG only the group reference works - if(src && typeof src === "string") { - img.src = src; - return img; - } - } - - const quickPNG = !(filenameParts.hasGroupref || filenameParts.hasFrameref) - ? await getQuickImagePreview(plugin, file.path, "png") - : undefined; - - const png = - quickPNG ?? - (await createPNG( - (filenameParts.hasGroupref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref) - ? filenameParts.filepath + filenameParts.linkpartReference - : file.path, - scale, - filenameParts.hasClippedFrameref - ? { ...exportSettings, frameRendering: { enabled: true, name: false, outline: false, clip: true}} - : exportSettings, - loader, - theme, - null, - null, - [], - plugin, - 0 - )); - if (!png) { - return null; - } - img.src = URL.createObjectURL(png); - cacheReady && imageCache.addImageToCache(cacheKey, img.src, png); - return img; -} - -const setStyle = ({element,imgAttributes,onCanvas}:{ - element: HTMLElement, - imgAttributes: imgElementAttributes, - onCanvas: boolean, -} -) => { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(setStyle, `MarkdownPostProcessor.ts > setStyle`); - let style = ""; - if(imgAttributes.fwidth) { - style = `max-width:${imgAttributes.fwidth}${imgAttributes.fwidth.match(/\d$/) ? "px":""}; `; //width:100%;`; //removed !important https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/886 - } else { - style = "width: fit-content;" - } - if (imgAttributes.fheight) { - style += `${imgAttributes.fwidth?"min-":"max-"}height:${imgAttributes.fheight}px;`; - } - if(!onCanvas) element.setAttribute("style", style); - element.classList.add(...Array.from(imgAttributes.style)) - if(!element.hasClass("excalidraw-embedded-img")) { - element.addClass("excalidraw-embedded-img"); - } - if( - window?.ExcalidrawAutomate?.plugin?.settings?.canvasImmersiveEmbed && - !element.hasClass("excalidraw-canvas-immersive") - ) { - element.addClass("excalidraw-canvas-immersive"); - } -} - -const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSettings,loader}:{ - filenameParts: FILENAMEPARTS, - theme: string, - cacheReady: boolean, - img: HTMLImageElement, - file: TFile, - exportSettings: ExportSettings, - loader: EmbeddedFilesLoader, -}):Promise => { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getSVGIMG, `MarkdownPostProcessor.ts > _getSVGIMG`); - exportSettings.skipInliningFonts = false; - const cacheKey = { - ...filenameParts, - isDark: theme==="dark", - previewImageType: PreviewImageType.SVGIMG, - scale:1, - isTransparent: !exportSettings.withBackground, - inlineFonts: !exportSettings.skipInliningFonts, - }; - - if(cacheReady) { - const src = await imageCache.getImageFromCache(cacheKey); - if(src && typeof src === "string") { - img.setAttribute("src", src); - return img; - } - } - - if(!(filenameParts.hasBlockref || filenameParts.hasSectionref)) { - const quickSVG = await getQuickImagePreview(plugin, file.path, "svg"); - if (quickSVG) { - const svg = convertSVGStringToElement(quickSVG); - if (svg) { - return addSVGToImgSrc(img, svg, cacheReady, cacheKey); - } - } - } - - const svg = convertSVGStringToElement(( - await createSVG( - filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref - ? filenameParts.filepath + filenameParts.linkpartReference - : file.path, - true, - filenameParts?.hasClippedFrameref - ? { ...exportSettings, frameRendering: { enabled: true, name: false, outline: false, clip: true}} - : exportSettings, - loader, - theme, - null, - null, - [], - plugin, - 0, - getExportPadding(plugin, file), - ) - ).outerHTML); - - if (!svg) { - return null; - } - - //need to remove width and height attributes to support area= embeds - svg.removeAttribute("width"); - svg.removeAttribute("height"); - return addSVGToImgSrc(img, svg, cacheReady, cacheKey); -} - -const _getSVGNative = async ({filenameParts,theme,cacheReady,containerElement,file,exportSettings,loader}:{ - filenameParts: FILENAMEPARTS, - theme: string, - cacheReady: boolean, - containerElement: HTMLDivElement, - file: TFile, - exportSettings: ExportSettings, - loader: EmbeddedFilesLoader, -}):Promise => { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getSVGNative, `MarkdownPostProcessor.ts > _getSVGNative`); - exportSettings.skipInliningFonts = false; - const cacheKey = { - ...filenameParts, - isDark: theme==="dark", - previewImageType: PreviewImageType.SVG, - scale:1, - isTransparent: !exportSettings.withBackground, - inlineFonts: !exportSettings.skipInliningFonts, - }; - let maybeSVG; - if(cacheReady) { - maybeSVG = await imageCache.getImageFromCache(cacheKey); - } - - const svg = (maybeSVG && (maybeSVG instanceof SVGSVGElement)) - ? maybeSVG - : convertSVGStringToElement((await createSVG( - filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref - ? filenameParts.filepath + filenameParts.linkpartReference - : file.path, - false, - filenameParts.hasClippedFrameref - ? { ...exportSettings, frameRendering: { enabled: true, name: false, outline: false, clip: true}} - : exportSettings, - loader, - theme, - null, - null, - [], - plugin, - 0, - getExportPadding(plugin, file), - undefined, - true - )).outerHTML); - - if (!svg) { - return null; - } - - //cache SVG should have the width and height parameters and not the embedded font - if(!Boolean(maybeSVG)) { - cacheReady && imageCache.addImageToCache(cacheKey,"", svg); - } - - svg.removeAttribute("width"); - svg.removeAttribute("height"); - containerElement.append(svg); - return containerElement; -} - -/** - * Generates an IMG or DIV element - * - The IMG element will have the drawing encoded as a base64 SVG or a PNG (depending on settings) - * - The DIV element will have the drawing as an SVG element - * @param parts {imgElementAttributes} - display properties of the image - * @returns {Promise} - the IMG HTML element containing the image - */ -const getIMG = async ( - imgAttributes: imgElementAttributes, - onCanvas: boolean = false, -): Promise => { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(getIMG, `MarkdownPostProcessor.ts > getIMG`, imgAttributes); - let file = imgAttributes.file; - if (!imgAttributes.file) { - const f = vault.getAbstractFileByPath(imgAttributes.fname?.split("#")[0]); - if (!(f && f instanceof TFile)) { - return null; - } - file = f; - } - - const filenameParts = getEmbeddedFilenameParts(imgAttributes.fname); - - // https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/387 - imgAttributes.style = imgAttributes.style.map(s=>s.replaceAll(" ", "-")); - - const forceTheme = hasExportTheme(plugin, file) - ? getExportTheme(plugin, file, "light") - : undefined; - - const exportSettings: ExportSettings = { - withBackground: getWithBackground(plugin, file), - withTheme: forceTheme ? true : plugin.settings.exportWithTheme, - isMask: isMaskFile(plugin, file), - }; - - const theme = - forceTheme ?? - (plugin.settings.previewMatchObsidianTheme - ? isObsidianThemeDark() - ? "dark" - : "light" - : !plugin.settings.exportWithTheme - ? "light" - : undefined); - if (theme) { - exportSettings.withTheme = true; - } - const loader = new EmbeddedFilesLoader( - plugin, - theme ? theme === "dark" : undefined, - ); - - const cacheReady = imageCache.isReady(); - - await plugin.awaitInit(); - switch (plugin.settings.previewImageType) { - case PreviewImageType.PNG: { - const img = createEl("img"); - setStyle({element:img,imgAttributes,onCanvas}); - return await _getPNG({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader}); - } - case PreviewImageType.SVGIMG: { - const img = createEl("img"); - setStyle({element:img,imgAttributes,onCanvas}); - return await _getSVGIMG({filenameParts,theme,cacheReady,img,file,exportSettings,loader}); - } - case PreviewImageType.SVG: { - const img = createEl("div"); - setStyle({element:img,imgAttributes,onCanvas}); - return await _getSVGNative({filenameParts,theme,cacheReady,containerElement: img,file,exportSettings,loader}); - } - } -}; - -const addSVGToImgSrc = (img: HTMLImageElement, svg: SVGSVGElement, cacheReady: boolean, cacheKey: ImageKey):HTMLImageElement => { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(addSVGToImgSrc, `MarkdownPostProcessor.ts > addSVGToImgSrc`); - //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2026 - //const svgString = new XMLSerializer().serializeToString(svg); - const svgString = svg.outerHTML; - const blob = new Blob([svgString], { type: 'image/svg+xml' }); - const blobUrl = URL.createObjectURL(blob); - img.setAttribute("src", blobUrl); - cacheReady && imageCache.addImageToCache(cacheKey, blobUrl, blob); - return img; -} - -const createImgElement = async ( - attr: imgElementAttributes, - onCanvas: boolean = false, -) :Promise => { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(createImgElement, `MarkdownPostProcessor.ts > createImgElement`); - const imgOrDiv = await getIMG(attr,onCanvas); - if(!imgOrDiv) { - return null; - } - imgOrDiv.setAttribute("fileSource", attr.fname); - if (attr.fwidth) { - imgOrDiv.setAttribute("w", attr.fwidth); - } - if (attr.fheight) { - imgOrDiv.setAttribute("h", attr.fheight); - } - imgOrDiv.setAttribute("draggable","false"); - imgOrDiv.setAttribute("onCanvas",onCanvas?"true":"false"); - - let timer:number; - const clickEvent = (ev:PointerEvent) => { - if (!isHTMLElement(ev.target)) { - return; - } - const targetElement = ev.target as HTMLElement; - const containerElement = targetElement.hasClass("excalidraw-embedded-img") - ? ev.target - : getParentOfClass(targetElement, "excalidraw-embedded-img"); - if (!containerElement) { - return; - } - const src = imgOrDiv.getAttribute("fileSource"); - if (src) { - const srcParts = src.match(/([^#]*)(.*)/); - if(!srcParts) return; - const f = vault.getAbstractFileByPath(srcParts[1]) as TFile; - const linkModifier = linkClickModifierType(ev); - if (plugin.isExcalidrawFile(f) && isMaskFile(plugin, f)) { - (async () => { - const linkString = `[[${f.path}${srcParts[2]?"#"+srcParts[2]:""}]] ${getExcalidrawFileForwardLinks(plugin.app, f, new Set())}`; - const result = await linkPrompt(linkString, plugin.app); - if(!result) return; - const [file, linkText, subpath] = result; - if(plugin.isExcalidrawFile(file)) { - plugin.openDrawing(file,linkModifier, true, subpath); - return; - } - let paneType: boolean | PaneType = false; - switch(linkModifier) { - case "active-pane": paneType = false; break; - case "new-pane": paneType = "split"; break; - case "popout-window": paneType = "window"; break; - case "new-tab": paneType = "tab"; break; - case "md-properties": paneType = "tab"; break; - } - plugin.app.workspace.openLinkText(linkText,"",paneType,subpath ? {eState: {subpath}} : {}); - })() - return; - } - plugin.openDrawing(f,linkModifier,true,srcParts[2]); - } //.ctrlKey||ev.metaKey); - }; - //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1003 - let pointerDownEvent:any; - const eventElement = imgOrDiv as HTMLElement; - - /*plugin.settings.previewImageType === PreviewImageType.SVG - ? imgOrDiv.firstElementChild as HTMLElement - : imgOrDiv;*/ - - eventElement.addEventListener("pointermove",(ev)=>{ - if(!timer) return; - if(Math.abs(ev.screenX-pointerDownEvent.screenX)>10 || Math.abs(ev.screenY-pointerDownEvent.screenY)>10) { - window.clearTimeout(timer); - timer = null; - } - }); - eventElement.addEventListener("pointerdown",(ev)=>{ - if(imgOrDiv?.parentElement?.hasClass("canvas-node-content")) return; - //@ts-ignore - const PLUGIN = app.plugins.plugins["obsidian-excalidraw-plugin"] as ExcalidrawPlugin; - const timeoutValue = DEVICE.isDesktop ? PLUGIN.settings.longPressDesktop : PLUGIN.settings.longPressMobile; - timer = window.setTimeout(()=>clickEvent(ev),timeoutValue); - pointerDownEvent = ev; - }); - eventElement.addEventListener("pointerup",()=>{ - if(timer) window.clearTimeout(timer); - timer = null; - }) - eventElement.addEventListener("dblclick",clickEvent); - eventElement.addEventListener(RERENDER_EVENT, async (e) => { - e.stopPropagation(); - const parent = imgOrDiv.parentElement; - const imgMaxWidth = imgOrDiv.style.maxWidth; - const imgMaxHeigth = imgOrDiv.style.maxHeight; - const fileSource = imgOrDiv.getAttribute("fileSource"); - const onCanvas = imgOrDiv.getAttribute("onCanvas") === "true"; - const newImg = await createImgElement({ - fname: fileSource, - fwidth: imgOrDiv.getAttribute("w"), - fheight: imgOrDiv.getAttribute("h"), - style: [...Array.from(imgOrDiv.classList)], - }, onCanvas); - if(!newImg) return; - parent.empty(); - if(!onCanvas) { - newImg.style.maxHeight = imgMaxHeigth; - newImg.style.maxWidth = imgMaxWidth; - } - newImg.setAttribute("fileSource",fileSource); - parent.append(newImg); - }); - const cssClasses = getFileCSSClasses(attr.file); - cssClasses.forEach((cssClass) => { - if(imgOrDiv.hasClass(cssClass)) return; - imgOrDiv.addClass(cssClass); - }); - if(window?.ExcalidrawAutomate?.plugin?.settings?.canvasImmersiveEmbed) { - if(!imgOrDiv.hasClass("excalidraw-canvas-immersive")) { - imgOrDiv.addClass("excalidraw-canvas-immersive"); - } - } else { - if(imgOrDiv.hasClass("excalidraw-canvas-immersive")) { - imgOrDiv.removeClass("excalidraw-canvas-immersive"); - } - } - return imgOrDiv; -} - -const createImageDiv = async ( - attr: imgElementAttributes, - onCanvas: boolean = false -): Promise => { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(createImageDiv, `MarkdownPostProcessor.ts > createImageDiv`); - const img = await createImgElement(attr, onCanvas); - return createDiv(attr.style.join(" "), (el) => el.append(img)); -}; - -const processReadingMode = async ( - embeddedItems: NodeListOf | [HTMLElement], - ctx: MarkdownPostProcessorContext, -) => { - (process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(processReadingMode, `MarkdownPostProcessor.ts > processReadingMode`); - //We are processing a non-excalidraw file in reading mode - //Embedded files will be displayed in an .internal-embed container - - //Iterating all the containers in the file to check which one is an excalidraw drawing - //This is a for loop instead of embeddedItems.forEach() because processInternalEmbed at the end - //is awaited, otherwise excalidraw images would not display in the Kanban plugin - for (const maybeDrawing of embeddedItems) { - //check to see if the file in the src attribute exists - const fname = maybeDrawing.getAttribute("src")?.split("#")[0]; - if(!fname) continue; - - const file = metadataCache.getFirstLinkpathDest(fname, ctx.sourcePath); - - //if the embeddedFile exits and it is an Excalidraw file - //then lets replace the .internal-embed with the generated PNG or SVG image - if (file && file instanceof TFile && plugin.isExcalidrawFile(file)) { - if(isTextOnlyEmbed(maybeDrawing)) { - //legacy reference to a block or section as text - //should be embedded as legacy text - continue; - } - - maybeDrawing.parentElement.replaceChild( - await processInternalEmbed(maybeDrawing,file), - maybeDrawing - ); - } - } -}; - -const processInternalEmbed = async (internalEmbedEl: Element, file: TFile ):Promise => { - (process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(processInternalEmbed, `MarkdownPostProcessor.ts > processInternalEmbed`, internalEmbedEl); - const attr: imgElementAttributes = { - fname: "", - fheight: "", - fwidth: "", - style: [], - }; - - const src = internalEmbedEl.getAttribute("src"); - if(!src) return; - - //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1059 - internalEmbedEl.removeClass("markdown-embed"); - internalEmbedEl.removeClass("inline-embed"); - internalEmbedEl.addClass("media-embed"); - internalEmbedEl.addClass("image-embed"); - - attr.fwidth = internalEmbedEl.getAttribute("width") - ? internalEmbedEl.getAttribute("width") - : getDefaultWidth(plugin); - attr.fheight = internalEmbedEl.getAttribute("height") - ? internalEmbedEl.getAttribute("height") - : getDefaultHeight(plugin); - let alt = internalEmbedEl.getAttribute("alt"); - attr.style = ["excalidraw-svg"]; - processAltText(src.split("#")[0],alt,attr); - const fnameParts = getEmbeddedFilenameParts(src); - attr.fname = file?.path + (fnameParts.hasBlockref||fnameParts.hasSectionref?fnameParts.linkpartReference:""); - attr.file = file; - return await createImageDiv(attr); -} - -function getDimensionsFromAliasString(data: string) { - const dimensionRegex = /^(?\d+%|\d+)(x(?\d+%|\d+))?$/; - const heightOnlyRegex = /^x(?\d+%|\d+)$/; - - const match = data.match(dimensionRegex) || data.match(heightOnlyRegex); - if (match) { - const { width, height } = match.groups; - - // Ensure width and height do not start with '0' - if ((width && width.startsWith('0') && width !== '0') || - (height && height.startsWith('0') && height !== '0')) { - return null; - } - - return { - width: width || undefined, - height: height || undefined, - }; - } - - // If the input starts with a 0 or is a decimal, return null - if (/^0\d|^\d+\.\d+/.test(data)) { - return null; - } - return null; -} - -type AliasParts = { alias?: string, width?: string, height?: string, style?: string }; -function parseAlias(input: string):AliasParts { - const result:AliasParts = {}; - const parts = input.split('|').map(part => part.trim()); - - switch (parts.length) { - case 1: - const singleMatch = getDimensionsFromAliasString(parts[0]); - if (singleMatch) { - return singleMatch; // Return dimensions if valid - } - result.style = parts[0]; // Otherwise, return as style - break; - - case 2: - const firstDim = getDimensionsFromAliasString(parts[0]); - const secondDim = getDimensionsFromAliasString(parts[1]); - - if (secondDim) { - result.alias = parts[0]; - result.width = secondDim.width; - result.height = secondDim.height; - } else if (firstDim) { - result.width = firstDim.width; - result.height = firstDim.height; - result.style = parts[1]; // Second part is style - } else { - result.alias = parts[0]; - result.style = parts[1]; // Assuming second part is style - } - break; - - case 3: - const middleMatch = getDimensionsFromAliasString(parts[1]); - if (middleMatch) { - result.alias = parts[0]; - result.width = middleMatch.width; - result.height = middleMatch.height; - result.style = parts[2]; - } else { - result.alias = parts[0]; - result.style = parts[2]; // Last part is style - } - break; - - default: - const secondValue = getDimensionsFromAliasString(parts[1]); - if (secondValue) { - result.alias = parts[0]; - result.width = secondValue.width; - result.height = secondValue.height; - result.style = parts[parts.length - 1]; // Last part is style - } else { - result.alias = parts[0]; - result.style = parts[parts.length - 1]; // Last part is style - } - break; - } - - // Clean up the result to remove undefined properties - Object.keys(result).forEach((key: keyof AliasParts) => { - if (result[key] === undefined) { - delete result[key]; - } - }); - - return result; -} - -const processAltText = ( - fname: string, - alt:string, - attr: imgElementAttributes -) => { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(processAltText, `MarkdownPostProcessor.ts > processAltText`); - if (alt && !alt.startsWith(fname)) { - const aliasParts = parseAlias(alt); - attr.fwidth = aliasParts.width ?? attr.fwidth; - attr.fheight = aliasParts.height ?? attr.fheight; - if (aliasParts.style && !aliasParts.style.startsWith(fname)) { - attr.style = [`excalidraw-svg${`-${aliasParts.style}`}`]; - } - } -} - -const isTextOnlyEmbed = (internalEmbedEl: Element):boolean => { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(isTextOnlyEmbed, `MarkdownPostProcessor.ts > isTextOnlyEmbed`); - const src = internalEmbedEl.getAttribute("src"); - if(!src) return true; //technically this does not mean this is a text only embed, but still should abort further processing - const fnameParts = getEmbeddedFilenameParts(src); - return !(fnameParts.hasArearef || fnameParts.hasGroupref || fnameParts.hasFrameref || fnameParts.hasClippedFrameref) && - (fnameParts.hasBlockref || fnameParts.hasSectionref) -} - -const tmpObsidianWYSIWYG = async ( - el: HTMLElement, - ctx: MarkdownPostProcessorContext, - isPrinting: boolean, - isMarkdownReadingMode: boolean, - isHoverPopover: boolean, -) => { - (process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(tmpObsidianWYSIWYG, `MarkdownPostProcessor.ts > tmpObsidianWYSIWYG`); - const file = app.vault.getAbstractFileByPath(ctx.sourcePath); - if(!(file instanceof TFile)) return; - if(!plugin.isExcalidrawFile(file)) return; - - //@ts-ignore - if (ctx.remainingNestLevel < 4) { - return; - } - - //internal-embed: Excalidraw is embedded into a markdown document - //markdown-reading-view: we are processing the markdown reading view of an actual Excalidraw file - //markdown-embed: we are processing the hover preview of a markdown file - //alt, width, and height attributes of .internal-embed to size and style the image - - //@ts-ignore - const containerEl = ctx.containerEl; - - if(!plugin.settings.renderImageInMarkdownReadingMode && isMarkdownReadingMode) { // containerEl.parentElement?.parentElement?.hasClass("markdown-reading-view")) { - return; - } - - if(!plugin.settings.renderImageInMarkdownToPDF && isPrinting) { //containerEl.parentElement?.hasClass("print")) { - return; - } - - let internalEmbedDiv: HTMLElement = containerEl; - while ( - !internalEmbedDiv.hasClass("print") && - !internalEmbedDiv.hasClass("dataview") && - !internalEmbedDiv.hasClass("cm-preview-code-block") && - !internalEmbedDiv.hasClass("cm-embed-block") && - !internalEmbedDiv.hasClass("internal-embed") && - !internalEmbedDiv.hasClass("markdown-reading-view") && - !internalEmbedDiv.hasClass("markdown-embed") && - internalEmbedDiv.parentElement - ) { - internalEmbedDiv = internalEmbedDiv.parentElement; - } - - if( - internalEmbedDiv.hasClass("dataview") || - internalEmbedDiv.hasClass("cm-preview-code-block") || - internalEmbedDiv.hasClass("cm-embed-block") - ) { - return; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/835 - } - - - if(!plugin.settings.renderImageInHoverPreviewForMDNotes) { - //const isHoverPopover = internalEmbedDiv.parentElement?.hasClass("hover-popover"); - const shouldOpenMD = Boolean(ctx.frontmatter?.["excalidraw-open-md"]); - if(isHoverPopover && shouldOpenMD) { - return; - } - } - - //const isPrinting = Boolean(internalEmbedDiv.hasClass("print")); - - const attr: imgElementAttributes = { - fname: ctx.sourcePath, - fheight: isPrinting ? "100%" : getDefaultHeight(plugin), - fwidth: isPrinting ? "100%" : getDefaultWidth(plugin), - style: ["excalidraw-svg"], - }; - - attr.file = file; - - const markdownEmbed = internalEmbedDiv.hasClass("markdown-embed"); - const markdownReadingView = isPrinting || isMarkdownReadingMode; //internalEmbedDiv.hasClass("markdown-reading-view") - if (!internalEmbedDiv.hasClass("internal-embed") && (markdownEmbed || markdownReadingView)) { - if(isPrinting) { - internalEmbedDiv = containerEl; - } - //We are processing the markdown preview of an actual Excalidraw file - //the excalidraw file in markdown preview mode - const isFrontmatterDiv = Boolean(el.querySelector(".frontmatter")); - let areaPreview = false; - if(Boolean(ctx.frontmatter)) { - el.empty(); - } else { - const warningEl = el.querySelector("div>h3[data-heading^='Unable to find section #^"); - if(warningEl) { - const ref = warningEl.getAttr("data-heading").match(/Unable to find section (#\^(?:group=|area=|frame=|clippedframe=)[^ ]*)/)?.[1]; - if(ref) { - attr.fname = file.path + ref; - areaPreview = true; - } - } - - } - if(!isFrontmatterDiv && !areaPreview) { - if(el.parentElement === containerEl) containerEl.removeChild(el); - return; - } - internalEmbedDiv.empty(); - const onCanvas = internalEmbedDiv.hasClass("canvas-node-content"); - const imgDiv = await createImageDiv(attr, onCanvas); - if(markdownEmbed) { - //display image on canvas without markdown frame - internalEmbedDiv.removeClass("markdown-embed"); - internalEmbedDiv.removeClass("inline-embed"); - internalEmbedDiv.addClass("media-embed"); - internalEmbedDiv.addClass("image-embed"); - if(!onCanvas && imgDiv.firstChild instanceof HTMLElement) { - imgDiv.firstChild.style.maxHeight = "100%"; - imgDiv.firstChild.style.maxWidth = null; - } - internalEmbedDiv.appendChild(imgDiv.firstChild); - return; - } - internalEmbedDiv.appendChild(imgDiv); - return; - } - - if(isTextOnlyEmbed(internalEmbedDiv)) { - //legacy reference to a block or section as text - //should be embedded as legacy text - return; - } - - el.empty(); - - if(internalEmbedDiv.hasAttribute("ready")) { - return; - } - internalEmbedDiv.setAttribute("ready",""); - - internalEmbedDiv.empty(); - const imgDiv = await processInternalEmbed(internalEmbedDiv,file); - internalEmbedDiv.appendChild(imgDiv); - - //timer to avoid the image flickering when the user is typing - let timer: number = null; - const markdownObserverFn: MutationCallback = (m) => { - if (!["alt", "width", "height"].contains(m[0]?.attributeName)) { - return; - } - if (timer) { - window.clearTimeout(timer); - } - timer = window.setTimeout(async () => { - timer = null; - internalEmbedDiv.empty(); - const imgDiv = await processInternalEmbed(internalEmbedDiv,file); - internalEmbedDiv.appendChild(imgDiv); - }, 500); - } - const observer = DEBUGGING - ? new CustomMutationObserver(markdownObserverFn, "markdowPostProcessorObserverFn") - : new MutationObserver(markdownObserverFn); - observer.observe(internalEmbedDiv, { - attributes: true, //configure it to listen to attribute changes - }); -}; - -const docIDs = new Set(); -/** - * - * @param el - * @param ctx - */ -export const markdownPostProcessor = async ( - el: HTMLElement, - ctx: MarkdownPostProcessorContext, -) => { - await plugin.awaitSettings(); - const isPrinting = Boolean(document.body.querySelectorAll("body > .print").length>0); - //firstElementChild: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1956 - const isFrontmatter = el.hasClass("mod-frontmatter") || - el.firstElementChild?.hasClass("frontmatter") || - el.firstElementChild?.hasClass("block-language-yaml"); - if(isPrinting && isFrontmatter) { - return; - } - - //@ts-ignore - const containerEl = ctx.containerEl; - - (process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(markdownPostProcessor, `MarkdownPostProcessor.ts > markdownPostProcessor`, ctx, el); - - //check to see if we are rendering in editing mode or live preview - //if yes, then there should be no .internal-embed containers - const isMarkdownReadingMode = Boolean(containerEl && getParentOfClass(containerEl, "markdown-reading-view")); - const isHoverPopover = Boolean(containerEl && getParentOfClass(containerEl, "hover-popover")); - const isPreview = (isHoverPopover && Boolean(ctx?.frontmatter?.["excalidraw-open-md"]) && !plugin.settings.renderImageInHoverPreviewForMDNotes); - const embeddedItems = el.querySelectorAll(".internal-embed"); - - if(isPrinting && plugin.settings.renderImageInMarkdownToPDF) { - await tmpObsidianWYSIWYG(el, ctx, isPrinting, isMarkdownReadingMode, isHoverPopover); - return; - } - - if (!isPreview && embeddedItems.length === 0) { - if(isFrontmatter) { - docIDs.add(ctx.docId); - } else { - if(docIDs.has(ctx.docId) && !el.hasChildNodes()) { - docIDs.delete(ctx.docId); - } - const isAreaGroupFrameRef = el.querySelectorAll('[data-heading^="Unable to find"]').length === 1; - if(!isAreaGroupFrameRef) { - return; - } - } - await tmpObsidianWYSIWYG(el, ctx, isPrinting, isMarkdownReadingMode, isHoverPopover); - return; - } - - //If the file being processed is an excalidraw file, - //then I want to hide all embedded items as these will be - //transcluded text element or some other transcluded content inside the Excalidraw file - //in reading mode these elements should be hidden - const excalidrawFile = Boolean(ctx.frontmatter?.hasOwnProperty("excalidraw-plugin")); - if (!(isPreview || isMarkdownReadingMode || isPrinting) && excalidrawFile) { - el.style.display = "none"; - return; - } - - await processReadingMode(embeddedItems, ctx); -}; - -/** - * internal-link quick preview - * @param e - * @returns - */ -export const hoverEvent = (e: any) => { - if (!e.linktext) { - plugin.hover.linkText = null; - return; - } - plugin.hover.linkText = e.linktext; - plugin.hover.sourcePath = e.sourcePath; -}; - -//monitoring for div.popover.hover-popover.file-embed.is-loaded to be added to the DOM tree -const legacyExcalidrawPopoverObserverFn: MutationCallback = async (m) => { - if (m.length === 0) { - return; - } - if (!plugin.hover.linkText) { - return; - } - if (!plugin.hover.linkText.endsWith("excalidraw")) { - return; - } - const file = metadataCache.getFirstLinkpathDest( - plugin.hover.linkText, - plugin.hover.sourcePath ? plugin.hover.sourcePath : "", - ); - if (!file) { - return; - } - if (!(file instanceof TFile)) { - return; - } - if (file.extension !== "excalidraw") { - return; - } - - const svgFileName = getIMGFilename(file.path, "svg"); - const svgFile = vault.getAbstractFileByPath(svgFileName); - if (svgFile && svgFile instanceof TFile) { - return; - } //If auto export SVG or PNG is enabled it will be inserted at the top of the excalidraw file. No need to manually insert hover preview - - const pngFileName = getIMGFilename(file.path, "png"); - const pngFile = vault.getAbstractFileByPath(pngFileName); - if (pngFile && pngFile instanceof TFile) { - return; - } //If auto export SVG or PNG is enabled it will be inserted at the top of the excalidraw file. No need to manually insert hover preview - - if (!plugin.hover.linkText) { - return; - } - if (m.length !== 1) { - return; - } - if (m[0].addedNodes.length !== 1) { - return; - } - if ( - (m[0].addedNodes[0] as HTMLElement).className !== "popover hover-popover" - ) { - return; - } - const node = m[0].addedNodes[0]; - node.empty(); - - //this div will be on top of original DIV. By stopping the propagation of the click - //I prevent the default Obsidian feature of opening the link in the native app - const img = await getIMG({ - file, - fname: file.path, - fwidth: "300", - fheight: null, - style: ["excalidraw-svg"], - }); - const div = createDiv("", async (el) => { - el.appendChild(img); - el.setAttribute("src", file.path); - el.onClickEvent((ev) => { - ev.stopImmediatePropagation(); - const src = el.getAttribute("src"); - if (src) { - plugin.openDrawing( - vault.getAbstractFileByPath(src) as TFile, - linkClickModifierType(ev) - ); - } //.ctrlKey||ev.metaKey); - }); - }); - node.appendChild(div); -}; - -export const legacyExcalidrawPopoverObserver = DEBUGGING - ? new CustomMutationObserver(legacyExcalidrawPopoverObserverFn, "legacyExcalidrawPopoverObserverFn") - : new MutationObserver(legacyExcalidrawPopoverObserverFn); - +import { + App, + MarkdownPostProcessorContext, + MetadataCache, + PaneType, + TFile, + Vault, +} from "obsidian"; +import { DEVICE, RERENDER_EVENT } from "../../Constants/Constants"; +import { EmbeddedFilesLoader } from "../../Shared/EmbeddedFileLoader"; +import { createPNG, createSVG } from "../../Shared/ExcalidrawAutomate"; +import { ExportSettings } from "../../View/ExcalidrawView"; +import ExcalidrawPlugin from "../main"; +import {getIMGFilename,} from "../../Utils/FileUtils"; +import { + getEmbeddedFilenameParts, + getExportTheme, + getQuickImagePreview, + getExportPadding, + getWithBackground, + hasExportTheme, + convertSVGStringToElement, + isMaskFile, +} from "../../Utils/Utils"; +import { getParentOfClass, isObsidianThemeDark, getFileCSSClasses } from "../../Utils/ObsidianUtils"; +import { linkClickModifierType } from "../../Utils/ModifierkeyHelper"; +import { ImageKey, imageCache } from "../../Utils/ImageCache"; +import { FILENAMEPARTS, PreviewImageType } from "../../Utils/UtilTypes"; +import { CustomMutationObserver, debug, DEBUGGING } from "../../Utils/DebugHelper"; +import { getExcalidrawFileForwardLinks } from "../../Utils/ExcalidrawViewUtils"; +import { linkPrompt } from "../../Shared/Dialogs/Prompt"; +import { isHTMLElement } from "../../Utils/Typechecks"; + +interface imgElementAttributes { + file?: TFile; + fname: string; //Excalidraw filename + fwidth: string; //Display width of image + fheight: string; //Display height of image + style: string[]; //css style to apply to IMG element +} + +let plugin: ExcalidrawPlugin; +let app: App; +let vault: Vault; +let metadataCache: MetadataCache; +const DEBUGGING_MPP = false; + + +const getDefaultWidth = (plugin: ExcalidrawPlugin): string => { + const width = parseInt(plugin.settings.width); + if (isNaN(width) || width === 0 || width === null) { + if(getDefaultHeight(plugin)!=="") return ""; + return "400"; + } + return plugin.settings.width; +}; + +const getDefaultHeight = (plugin: ExcalidrawPlugin): string => { + const height = parseInt(plugin.settings.height); + if (isNaN(height) || height === 0 || height === null) { + return ""; + } + return plugin.settings.height; +}; + +export const initializeMarkdownPostProcessor = (p: ExcalidrawPlugin) => { + plugin = p; + app = plugin.app; + vault = app.vault; + metadataCache = app.metadataCache; +}; + +const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader}:{ + imgAttributes: imgElementAttributes, + filenameParts: FILENAMEPARTS, + theme: string, + cacheReady: boolean, + img: HTMLImageElement, + file: TFile, + exportSettings: ExportSettings, + loader: EmbeddedFilesLoader, +}):Promise => { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getPNG, `MarkdownPostProcessor.ts > _getPNG`); + const width = parseInt(imgAttributes.fwidth); + const scale = width >= 2400 + ? 5 + : width >= 1800 + ? 4 + : width >= 1200 + ? 3 + : width >= 600 + ? 2 + : 1; + + const cacheKey = { + ...filenameParts, + isDark: theme==="dark", + previewImageType: PreviewImageType.PNG, + scale, + isTransparent: !exportSettings.withBackground, + inlineFonts: true, //though for PNG this makes no difference, but the key requires it + }; + + if(cacheReady) { + const src = await imageCache.getImageFromCache(cacheKey); + //In case of PNG I cannot change the viewBox to select the area of the element + //being referenced. For PNG only the group reference works + if(src && typeof src === "string") { + img.src = src; + return img; + } + } + + const quickPNG = !(filenameParts.hasGroupref || filenameParts.hasFrameref) + ? await getQuickImagePreview(plugin, file.path, "png") + : undefined; + + const png = + quickPNG ?? + (await createPNG( + (filenameParts.hasGroupref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref) + ? filenameParts.filepath + filenameParts.linkpartReference + : file.path, + scale, + filenameParts.hasClippedFrameref + ? { ...exportSettings, frameRendering: { enabled: true, name: false, outline: false, clip: true}} + : exportSettings, + loader, + theme, + null, + null, + [], + plugin, + 0 + )); + if (!png) { + return null; + } + img.src = URL.createObjectURL(png); + cacheReady && imageCache.addImageToCache(cacheKey, img.src, png); + return img; +} + +const setStyle = ({element,imgAttributes,onCanvas}:{ + element: HTMLElement, + imgAttributes: imgElementAttributes, + onCanvas: boolean, +} +) => { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(setStyle, `MarkdownPostProcessor.ts > setStyle`); + let style = ""; + if(imgAttributes.fwidth) { + style = `max-width:${imgAttributes.fwidth}${imgAttributes.fwidth.match(/\d$/) ? "px":""}; `; //width:100%;`; //removed !important https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/886 + } else { + style = "width: fit-content;" + } + if (imgAttributes.fheight) { + style += `${imgAttributes.fwidth?"min-":"max-"}height:${imgAttributes.fheight}px;`; + } + if(!onCanvas) element.setAttribute("style", style); + element.classList.add(...Array.from(imgAttributes.style)) + if(!element.hasClass("excalidraw-embedded-img")) { + element.addClass("excalidraw-embedded-img"); + } + if( + window?.ExcalidrawAutomate?.plugin?.settings?.canvasImmersiveEmbed && + !element.hasClass("excalidraw-canvas-immersive") + ) { + element.addClass("excalidraw-canvas-immersive"); + } +} + +const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSettings,loader}:{ + filenameParts: FILENAMEPARTS, + theme: string, + cacheReady: boolean, + img: HTMLImageElement, + file: TFile, + exportSettings: ExportSettings, + loader: EmbeddedFilesLoader, +}):Promise => { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getSVGIMG, `MarkdownPostProcessor.ts > _getSVGIMG`); + exportSettings.skipInliningFonts = false; + const cacheKey = { + ...filenameParts, + isDark: theme==="dark", + previewImageType: PreviewImageType.SVGIMG, + scale:1, + isTransparent: !exportSettings.withBackground, + inlineFonts: !exportSettings.skipInliningFonts, + }; + + if(cacheReady) { + const src = await imageCache.getImageFromCache(cacheKey); + if(src && typeof src === "string") { + img.setAttribute("src", src); + return img; + } + } + + if(!(filenameParts.hasBlockref || filenameParts.hasSectionref)) { + const quickSVG = await getQuickImagePreview(plugin, file.path, "svg"); + if (quickSVG) { + const svg = convertSVGStringToElement(quickSVG); + if (svg) { + return addSVGToImgSrc(img, svg, cacheReady, cacheKey); + } + } + } + + const svg = convertSVGStringToElement(( + await createSVG( + filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref + ? filenameParts.filepath + filenameParts.linkpartReference + : file.path, + true, + filenameParts?.hasClippedFrameref + ? { ...exportSettings, frameRendering: { enabled: true, name: false, outline: false, clip: true}} + : exportSettings, + loader, + theme, + null, + null, + [], + plugin, + 0, + getExportPadding(plugin, file), + ) + ).outerHTML); + + if (!svg) { + return null; + } + + //need to remove width and height attributes to support area= embeds + svg.removeAttribute("width"); + svg.removeAttribute("height"); + return addSVGToImgSrc(img, svg, cacheReady, cacheKey); +} + +const _getSVGNative = async ({filenameParts,theme,cacheReady,containerElement,file,exportSettings,loader}:{ + filenameParts: FILENAMEPARTS, + theme: string, + cacheReady: boolean, + containerElement: HTMLDivElement, + file: TFile, + exportSettings: ExportSettings, + loader: EmbeddedFilesLoader, +}):Promise => { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getSVGNative, `MarkdownPostProcessor.ts > _getSVGNative`); + exportSettings.skipInliningFonts = false; + const cacheKey = { + ...filenameParts, + isDark: theme==="dark", + previewImageType: PreviewImageType.SVG, + scale:1, + isTransparent: !exportSettings.withBackground, + inlineFonts: !exportSettings.skipInliningFonts, + }; + let maybeSVG; + if(cacheReady) { + maybeSVG = await imageCache.getImageFromCache(cacheKey); + } + + const svg = (maybeSVG && (maybeSVG instanceof SVGSVGElement)) + ? maybeSVG + : convertSVGStringToElement((await createSVG( + filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref + ? filenameParts.filepath + filenameParts.linkpartReference + : file.path, + false, + filenameParts.hasClippedFrameref + ? { ...exportSettings, frameRendering: { enabled: true, name: false, outline: false, clip: true}} + : exportSettings, + loader, + theme, + null, + null, + [], + plugin, + 0, + getExportPadding(plugin, file), + undefined, + true + )).outerHTML); + + if (!svg) { + return null; + } + + //cache SVG should have the width and height parameters and not the embedded font + if(!Boolean(maybeSVG)) { + cacheReady && imageCache.addImageToCache(cacheKey,"", svg); + } + + svg.removeAttribute("width"); + svg.removeAttribute("height"); + containerElement.append(svg); + return containerElement; +} + +/** + * Generates an IMG or DIV element + * - The IMG element will have the drawing encoded as a base64 SVG or a PNG (depending on settings) + * - The DIV element will have the drawing as an SVG element + * @param parts {imgElementAttributes} - display properties of the image + * @returns {Promise} - the IMG HTML element containing the image + */ +const getIMG = async ( + imgAttributes: imgElementAttributes, + onCanvas: boolean = false, +): Promise => { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(getIMG, `MarkdownPostProcessor.ts > getIMG`, imgAttributes); + let file = imgAttributes.file; + if (!imgAttributes.file) { + const f = vault.getAbstractFileByPath(imgAttributes.fname?.split("#")[0]); + if (!(f && f instanceof TFile)) { + return null; + } + file = f; + } + + const filenameParts = getEmbeddedFilenameParts(imgAttributes.fname); + + // https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/387 + imgAttributes.style = imgAttributes.style.map(s=>s.replaceAll(" ", "-")); + + const forceTheme = hasExportTheme(plugin, file) + ? getExportTheme(plugin, file, "light") + : undefined; + + const exportSettings: ExportSettings = { + withBackground: getWithBackground(plugin, file), + withTheme: forceTheme ? true : plugin.settings.exportWithTheme, + isMask: isMaskFile(plugin, file), + }; + + const theme = + forceTheme ?? + (plugin.settings.previewMatchObsidianTheme + ? isObsidianThemeDark() + ? "dark" + : "light" + : !plugin.settings.exportWithTheme + ? "light" + : undefined); + if (theme) { + exportSettings.withTheme = true; + } + const loader = new EmbeddedFilesLoader( + plugin, + theme ? theme === "dark" : undefined, + ); + + const cacheReady = imageCache.isReady(); + + await plugin.awaitInit(); + switch (plugin.settings.previewImageType) { + case PreviewImageType.PNG: { + const img = createEl("img"); + setStyle({element:img,imgAttributes,onCanvas}); + return await _getPNG({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader}); + } + case PreviewImageType.SVGIMG: { + const img = createEl("img"); + setStyle({element:img,imgAttributes,onCanvas}); + return await _getSVGIMG({filenameParts,theme,cacheReady,img,file,exportSettings,loader}); + } + case PreviewImageType.SVG: { + const img = createEl("div"); + setStyle({element:img,imgAttributes,onCanvas}); + return await _getSVGNative({filenameParts,theme,cacheReady,containerElement: img,file,exportSettings,loader}); + } + } +}; + +const addSVGToImgSrc = (img: HTMLImageElement, svg: SVGSVGElement, cacheReady: boolean, cacheKey: ImageKey):HTMLImageElement => { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(addSVGToImgSrc, `MarkdownPostProcessor.ts > addSVGToImgSrc`); + //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2026 + //const svgString = new XMLSerializer().serializeToString(svg); + const svgString = svg.outerHTML; + const blob = new Blob([svgString], { type: 'image/svg+xml' }); + const blobUrl = URL.createObjectURL(blob); + img.setAttribute("src", blobUrl); + cacheReady && imageCache.addImageToCache(cacheKey, blobUrl, blob); + return img; +} + +const createImgElement = async ( + attr: imgElementAttributes, + onCanvas: boolean = false, +) :Promise => { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(createImgElement, `MarkdownPostProcessor.ts > createImgElement`); + const imgOrDiv = await getIMG(attr,onCanvas); + if(!imgOrDiv) { + return null; + } + imgOrDiv.setAttribute("fileSource", attr.fname); + if (attr.fwidth) { + imgOrDiv.setAttribute("w", attr.fwidth); + } + if (attr.fheight) { + imgOrDiv.setAttribute("h", attr.fheight); + } + imgOrDiv.setAttribute("draggable","false"); + imgOrDiv.setAttribute("onCanvas",onCanvas?"true":"false"); + + let timer:number; + const clickEvent = (ev:PointerEvent) => { + if (!isHTMLElement(ev.target)) { + return; + } + const targetElement = ev.target as HTMLElement; + const containerElement = targetElement.hasClass("excalidraw-embedded-img") + ? ev.target + : getParentOfClass(targetElement, "excalidraw-embedded-img"); + if (!containerElement) { + return; + } + const src = imgOrDiv.getAttribute("fileSource"); + if (src) { + const srcParts = src.match(/([^#]*)(.*)/); + if(!srcParts) return; + const f = vault.getAbstractFileByPath(srcParts[1]) as TFile; + const linkModifier = linkClickModifierType(ev); + if (plugin.isExcalidrawFile(f) && isMaskFile(plugin, f)) { + (async () => { + const linkString = `[[${f.path}${srcParts[2]?"#"+srcParts[2]:""}]] ${getExcalidrawFileForwardLinks(plugin.app, f, new Set())}`; + const result = await linkPrompt(linkString, plugin.app); + if(!result) return; + const [file, linkText, subpath] = result; + if(plugin.isExcalidrawFile(file)) { + plugin.openDrawing(file,linkModifier, true, subpath); + return; + } + let paneType: boolean | PaneType = false; + switch(linkModifier) { + case "active-pane": paneType = false; break; + case "new-pane": paneType = "split"; break; + case "popout-window": paneType = "window"; break; + case "new-tab": paneType = "tab"; break; + case "md-properties": paneType = "tab"; break; + } + plugin.app.workspace.openLinkText(linkText,"",paneType,subpath ? {eState: {subpath}} : {}); + })() + return; + } + plugin.openDrawing(f,linkModifier,true,srcParts[2]); + } //.ctrlKey||ev.metaKey); + }; + //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1003 + let pointerDownEvent:any; + const eventElement = imgOrDiv as HTMLElement; + + /*plugin.settings.previewImageType === PreviewImageType.SVG + ? imgOrDiv.firstElementChild as HTMLElement + : imgOrDiv;*/ + + eventElement.addEventListener("pointermove",(ev)=>{ + if(!timer) return; + if(Math.abs(ev.screenX-pointerDownEvent.screenX)>10 || Math.abs(ev.screenY-pointerDownEvent.screenY)>10) { + window.clearTimeout(timer); + timer = null; + } + }); + eventElement.addEventListener("pointerdown",(ev)=>{ + if(imgOrDiv?.parentElement?.hasClass("canvas-node-content")) return; + //@ts-ignore + const PLUGIN = app.plugins.plugins["obsidian-excalidraw-plugin"] as ExcalidrawPlugin; + const timeoutValue = DEVICE.isDesktop ? PLUGIN.settings.longPressDesktop : PLUGIN.settings.longPressMobile; + timer = window.setTimeout(()=>clickEvent(ev),timeoutValue); + pointerDownEvent = ev; + }); + eventElement.addEventListener("pointerup",()=>{ + if(timer) window.clearTimeout(timer); + timer = null; + }) + eventElement.addEventListener("dblclick",clickEvent); + eventElement.addEventListener(RERENDER_EVENT, async (e) => { + e.stopPropagation(); + const parent = imgOrDiv.parentElement; + const imgMaxWidth = imgOrDiv.style.maxWidth; + const imgMaxHeigth = imgOrDiv.style.maxHeight; + const fileSource = imgOrDiv.getAttribute("fileSource"); + const onCanvas = imgOrDiv.getAttribute("onCanvas") === "true"; + const newImg = await createImgElement({ + fname: fileSource, + fwidth: imgOrDiv.getAttribute("w"), + fheight: imgOrDiv.getAttribute("h"), + style: [...Array.from(imgOrDiv.classList)], + }, onCanvas); + if(!newImg) return; + parent.empty(); + if(!onCanvas) { + newImg.style.maxHeight = imgMaxHeigth; + newImg.style.maxWidth = imgMaxWidth; + } + newImg.setAttribute("fileSource",fileSource); + parent.append(newImg); + }); + const cssClasses = getFileCSSClasses(attr.file); + cssClasses.forEach((cssClass) => { + if(imgOrDiv.hasClass(cssClass)) return; + imgOrDiv.addClass(cssClass); + }); + if(window?.ExcalidrawAutomate?.plugin?.settings?.canvasImmersiveEmbed) { + if(!imgOrDiv.hasClass("excalidraw-canvas-immersive")) { + imgOrDiv.addClass("excalidraw-canvas-immersive"); + } + } else { + if(imgOrDiv.hasClass("excalidraw-canvas-immersive")) { + imgOrDiv.removeClass("excalidraw-canvas-immersive"); + } + } + return imgOrDiv; +} + +const createImageDiv = async ( + attr: imgElementAttributes, + onCanvas: boolean = false +): Promise => { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(createImageDiv, `MarkdownPostProcessor.ts > createImageDiv`); + const img = await createImgElement(attr, onCanvas); + return createDiv(attr.style.join(" "), (el) => el.append(img)); +}; + +const processReadingMode = async ( + embeddedItems: NodeListOf | [HTMLElement], + ctx: MarkdownPostProcessorContext, +) => { + (process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(processReadingMode, `MarkdownPostProcessor.ts > processReadingMode`); + //We are processing a non-excalidraw file in reading mode + //Embedded files will be displayed in an .internal-embed container + + //Iterating all the containers in the file to check which one is an excalidraw drawing + //This is a for loop instead of embeddedItems.forEach() because processInternalEmbed at the end + //is awaited, otherwise excalidraw images would not display in the Kanban plugin + for (const maybeDrawing of embeddedItems) { + //check to see if the file in the src attribute exists + const fname = maybeDrawing.getAttribute("src")?.split("#")[0]; + if(!fname) continue; + + const file = metadataCache.getFirstLinkpathDest(fname, ctx.sourcePath); + + //if the embeddedFile exits and it is an Excalidraw file + //then lets replace the .internal-embed with the generated PNG or SVG image + if (file && file instanceof TFile && plugin.isExcalidrawFile(file)) { + if(isTextOnlyEmbed(maybeDrawing)) { + //legacy reference to a block or section as text + //should be embedded as legacy text + continue; + } + + maybeDrawing.parentElement.replaceChild( + await processInternalEmbed(maybeDrawing,file), + maybeDrawing + ); + } + } +}; + +const processInternalEmbed = async (internalEmbedEl: Element, file: TFile ):Promise => { + (process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(processInternalEmbed, `MarkdownPostProcessor.ts > processInternalEmbed`, internalEmbedEl); + const attr: imgElementAttributes = { + fname: "", + fheight: "", + fwidth: "", + style: [], + }; + + const src = internalEmbedEl.getAttribute("src"); + if(!src) return; + + //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1059 + internalEmbedEl.removeClass("markdown-embed"); + internalEmbedEl.removeClass("inline-embed"); + internalEmbedEl.addClass("media-embed"); + internalEmbedEl.addClass("image-embed"); + + attr.fwidth = internalEmbedEl.getAttribute("width") + ? internalEmbedEl.getAttribute("width") + : getDefaultWidth(plugin); + attr.fheight = internalEmbedEl.getAttribute("height") + ? internalEmbedEl.getAttribute("height") + : getDefaultHeight(plugin); + let alt = internalEmbedEl.getAttribute("alt"); + attr.style = ["excalidraw-svg"]; + processAltText(src.split("#")[0],alt,attr); + const fnameParts = getEmbeddedFilenameParts(src); + attr.fname = file?.path + (fnameParts.hasBlockref||fnameParts.hasSectionref?fnameParts.linkpartReference:""); + attr.file = file; + return await createImageDiv(attr); +} + +function getDimensionsFromAliasString(data: string) { + const dimensionRegex = /^(?\d+%|\d+)(x(?\d+%|\d+))?$/; + const heightOnlyRegex = /^x(?\d+%|\d+)$/; + + const match = data.match(dimensionRegex) || data.match(heightOnlyRegex); + if (match) { + const { width, height } = match.groups; + + // Ensure width and height do not start with '0' + if ((width && width.startsWith('0') && width !== '0') || + (height && height.startsWith('0') && height !== '0')) { + return null; + } + + return { + width: width || undefined, + height: height || undefined, + }; + } + + // If the input starts with a 0 or is a decimal, return null + if (/^0\d|^\d+\.\d+/.test(data)) { + return null; + } + return null; +} + +type AliasParts = { alias?: string, width?: string, height?: string, style?: string }; +function parseAlias(input: string):AliasParts { + const result:AliasParts = {}; + const parts = input.split('|').map(part => part.trim()); + + switch (parts.length) { + case 1: + const singleMatch = getDimensionsFromAliasString(parts[0]); + if (singleMatch) { + return singleMatch; // Return dimensions if valid + } + result.style = parts[0]; // Otherwise, return as style + break; + + case 2: + const firstDim = getDimensionsFromAliasString(parts[0]); + const secondDim = getDimensionsFromAliasString(parts[1]); + + if (secondDim) { + result.alias = parts[0]; + result.width = secondDim.width; + result.height = secondDim.height; + } else if (firstDim) { + result.width = firstDim.width; + result.height = firstDim.height; + result.style = parts[1]; // Second part is style + } else { + result.alias = parts[0]; + result.style = parts[1]; // Assuming second part is style + } + break; + + case 3: + const middleMatch = getDimensionsFromAliasString(parts[1]); + if (middleMatch) { + result.alias = parts[0]; + result.width = middleMatch.width; + result.height = middleMatch.height; + result.style = parts[2]; + } else { + result.alias = parts[0]; + result.style = parts[2]; // Last part is style + } + break; + + default: + const secondValue = getDimensionsFromAliasString(parts[1]); + if (secondValue) { + result.alias = parts[0]; + result.width = secondValue.width; + result.height = secondValue.height; + result.style = parts[parts.length - 1]; // Last part is style + } else { + result.alias = parts[0]; + result.style = parts[parts.length - 1]; // Last part is style + } + break; + } + + // Clean up the result to remove undefined properties + Object.keys(result).forEach((key: keyof AliasParts) => { + if (result[key] === undefined) { + delete result[key]; + } + }); + + return result; +} + +const processAltText = ( + fname: string, + alt:string, + attr: imgElementAttributes +) => { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(processAltText, `MarkdownPostProcessor.ts > processAltText`); + if (alt && !alt.startsWith(fname)) { + const aliasParts = parseAlias(alt); + attr.fwidth = aliasParts.width ?? attr.fwidth; + attr.fheight = aliasParts.height ?? attr.fheight; + if (aliasParts.style && !aliasParts.style.startsWith(fname)) { + attr.style = [`excalidraw-svg${`-${aliasParts.style}`}`]; + } + } +} + +const isTextOnlyEmbed = (internalEmbedEl: Element):boolean => { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(isTextOnlyEmbed, `MarkdownPostProcessor.ts > isTextOnlyEmbed`); + const src = internalEmbedEl.getAttribute("src"); + if(!src) return true; //technically this does not mean this is a text only embed, but still should abort further processing + const fnameParts = getEmbeddedFilenameParts(src); + return !(fnameParts.hasArearef || fnameParts.hasGroupref || fnameParts.hasFrameref || fnameParts.hasClippedFrameref) && + (fnameParts.hasBlockref || fnameParts.hasSectionref) +} + +const tmpObsidianWYSIWYG = async ( + el: HTMLElement, + ctx: MarkdownPostProcessorContext, + isPrinting: boolean, + isMarkdownReadingMode: boolean, + isHoverPopover: boolean, +) => { + (process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(tmpObsidianWYSIWYG, `MarkdownPostProcessor.ts > tmpObsidianWYSIWYG`); + const file = app.vault.getAbstractFileByPath(ctx.sourcePath); + if(!(file instanceof TFile)) return; + if(!plugin.isExcalidrawFile(file)) return; + + //@ts-ignore + if (ctx.remainingNestLevel < 4) { + return; + } + + //internal-embed: Excalidraw is embedded into a markdown document + //markdown-reading-view: we are processing the markdown reading view of an actual Excalidraw file + //markdown-embed: we are processing the hover preview of a markdown file + //alt, width, and height attributes of .internal-embed to size and style the image + + //@ts-ignore + const containerEl = ctx.containerEl; + + if(!plugin.settings.renderImageInMarkdownReadingMode && isMarkdownReadingMode) { // containerEl.parentElement?.parentElement?.hasClass("markdown-reading-view")) { + return; + } + + if(!plugin.settings.renderImageInMarkdownToPDF && isPrinting) { //containerEl.parentElement?.hasClass("print")) { + return; + } + + let internalEmbedDiv: HTMLElement = containerEl; + while ( + !internalEmbedDiv.hasClass("print") && + !internalEmbedDiv.hasClass("dataview") && + !internalEmbedDiv.hasClass("cm-preview-code-block") && + !internalEmbedDiv.hasClass("cm-embed-block") && + !internalEmbedDiv.hasClass("internal-embed") && + !internalEmbedDiv.hasClass("markdown-reading-view") && + !internalEmbedDiv.hasClass("markdown-embed") && + internalEmbedDiv.parentElement + ) { + internalEmbedDiv = internalEmbedDiv.parentElement; + } + + if( + internalEmbedDiv.hasClass("dataview") || + internalEmbedDiv.hasClass("cm-preview-code-block") || + internalEmbedDiv.hasClass("cm-embed-block") + ) { + return; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/835 + } + + + if(!plugin.settings.renderImageInHoverPreviewForMDNotes) { + //const isHoverPopover = internalEmbedDiv.parentElement?.hasClass("hover-popover"); + const shouldOpenMD = Boolean(ctx.frontmatter?.["excalidraw-open-md"]); + if(isHoverPopover && shouldOpenMD) { + return; + } + } + + //const isPrinting = Boolean(internalEmbedDiv.hasClass("print")); + + const attr: imgElementAttributes = { + fname: ctx.sourcePath, + fheight: isPrinting ? "100%" : getDefaultHeight(plugin), + fwidth: isPrinting ? "100%" : getDefaultWidth(plugin), + style: ["excalidraw-svg"], + }; + + attr.file = file; + + const markdownEmbed = internalEmbedDiv.hasClass("markdown-embed"); + const markdownReadingView = isPrinting || isMarkdownReadingMode; //internalEmbedDiv.hasClass("markdown-reading-view") + if (!internalEmbedDiv.hasClass("internal-embed") && (markdownEmbed || markdownReadingView)) { + if(isPrinting) { + internalEmbedDiv = containerEl; + } + //We are processing the markdown preview of an actual Excalidraw file + //the excalidraw file in markdown preview mode + const isFrontmatterDiv = Boolean(el.querySelector(".frontmatter")); + let areaPreview = false; + if(Boolean(ctx.frontmatter)) { + el.empty(); + } else { + const warningEl = el.querySelector("div>h3[data-heading^='Unable to find section #^"); + if(warningEl) { + const ref = warningEl.getAttr("data-heading").match(/Unable to find section (#\^(?:group=|area=|frame=|clippedframe=)[^ ]*)/)?.[1]; + if(ref) { + attr.fname = file.path + ref; + areaPreview = true; + } + } + + } + if(!isFrontmatterDiv && !areaPreview) { + if(el.parentElement === containerEl) containerEl.removeChild(el); + return; + } + internalEmbedDiv.empty(); + const onCanvas = internalEmbedDiv.hasClass("canvas-node-content"); + const imgDiv = await createImageDiv(attr, onCanvas); + if(markdownEmbed) { + //display image on canvas without markdown frame + internalEmbedDiv.removeClass("markdown-embed"); + internalEmbedDiv.removeClass("inline-embed"); + internalEmbedDiv.addClass("media-embed"); + internalEmbedDiv.addClass("image-embed"); + if(!onCanvas && imgDiv.firstChild instanceof HTMLElement) { + imgDiv.firstChild.style.maxHeight = "100%"; + imgDiv.firstChild.style.maxWidth = null; + } + internalEmbedDiv.appendChild(imgDiv.firstChild); + return; + } + internalEmbedDiv.appendChild(imgDiv); + return; + } + + if(isTextOnlyEmbed(internalEmbedDiv)) { + //legacy reference to a block or section as text + //should be embedded as legacy text + return; + } + + el.empty(); + + if(internalEmbedDiv.hasAttribute("ready")) { + return; + } + internalEmbedDiv.setAttribute("ready",""); + + internalEmbedDiv.empty(); + const imgDiv = await processInternalEmbed(internalEmbedDiv,file); + internalEmbedDiv.appendChild(imgDiv); + + //timer to avoid the image flickering when the user is typing + let timer: number = null; + const markdownObserverFn: MutationCallback = (m) => { + if (!["alt", "width", "height"].contains(m[0]?.attributeName)) { + return; + } + if (timer) { + window.clearTimeout(timer); + } + timer = window.setTimeout(async () => { + timer = null; + internalEmbedDiv.empty(); + const imgDiv = await processInternalEmbed(internalEmbedDiv,file); + internalEmbedDiv.appendChild(imgDiv); + }, 500); + } + const observer = DEBUGGING + ? new CustomMutationObserver(markdownObserverFn, "markdowPostProcessorObserverFn") + : new MutationObserver(markdownObserverFn); + observer.observe(internalEmbedDiv, { + attributes: true, //configure it to listen to attribute changes + }); +}; + +const docIDs = new Set(); +/** + * + * @param el + * @param ctx + */ +export const markdownPostProcessor = async ( + el: HTMLElement, + ctx: MarkdownPostProcessorContext, +) => { + await plugin.awaitSettings(); + const isPrinting = Boolean(document.body.querySelectorAll("body > .print").length>0); + //firstElementChild: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1956 + const isFrontmatter = el.hasClass("mod-frontmatter") || + el.firstElementChild?.hasClass("frontmatter") || + el.firstElementChild?.hasClass("block-language-yaml"); + if(isPrinting && isFrontmatter) { + return; + } + + //@ts-ignore + const containerEl = ctx.containerEl; + + (process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(markdownPostProcessor, `MarkdownPostProcessor.ts > markdownPostProcessor`, ctx, el); + + //check to see if we are rendering in editing mode or live preview + //if yes, then there should be no .internal-embed containers + const isMarkdownReadingMode = Boolean(containerEl && getParentOfClass(containerEl, "markdown-reading-view")); + const isHoverPopover = Boolean(containerEl && getParentOfClass(containerEl, "hover-popover")); + const isPreview = (isHoverPopover && Boolean(ctx?.frontmatter?.["excalidraw-open-md"]) && !plugin.settings.renderImageInHoverPreviewForMDNotes); + const embeddedItems = el.querySelectorAll(".internal-embed"); + + if(isPrinting && plugin.settings.renderImageInMarkdownToPDF) { + await tmpObsidianWYSIWYG(el, ctx, isPrinting, isMarkdownReadingMode, isHoverPopover); + return; + } + + if (!isPreview && embeddedItems.length === 0) { + if(isFrontmatter) { + docIDs.add(ctx.docId); + } else { + if(docIDs.has(ctx.docId) && !el.hasChildNodes()) { + docIDs.delete(ctx.docId); + } + const isAreaGroupFrameRef = el.querySelectorAll('[data-heading^="Unable to find"]').length === 1; + if(!isAreaGroupFrameRef) { + return; + } + } + await tmpObsidianWYSIWYG(el, ctx, isPrinting, isMarkdownReadingMode, isHoverPopover); + return; + } + + //If the file being processed is an excalidraw file, + //then I want to hide all embedded items as these will be + //transcluded text element or some other transcluded content inside the Excalidraw file + //in reading mode these elements should be hidden + const excalidrawFile = Boolean(ctx.frontmatter?.hasOwnProperty("excalidraw-plugin")); + if (!(isPreview || isMarkdownReadingMode || isPrinting) && excalidrawFile) { + el.style.display = "none"; + return; + } + + await processReadingMode(embeddedItems, ctx); +}; + +/** + * internal-link quick preview + * @param e + * @returns + */ +export const hoverEvent = (e: any) => { + if (!e.linktext) { + plugin.hover.linkText = null; + return; + } + plugin.hover.linkText = e.linktext; + plugin.hover.sourcePath = e.sourcePath; +}; + +//monitoring for div.popover.hover-popover.file-embed.is-loaded to be added to the DOM tree +const legacyExcalidrawPopoverObserverFn: MutationCallback = async (m) => { + if (m.length === 0) { + return; + } + if (!plugin.hover.linkText) { + return; + } + if (!plugin.hover.linkText.endsWith("excalidraw")) { + return; + } + const file = metadataCache.getFirstLinkpathDest( + plugin.hover.linkText, + plugin.hover.sourcePath ? plugin.hover.sourcePath : "", + ); + if (!file) { + return; + } + if (!(file instanceof TFile)) { + return; + } + if (file.extension !== "excalidraw") { + return; + } + + const svgFileName = getIMGFilename(file.path, "svg"); + const svgFile = vault.getAbstractFileByPath(svgFileName); + if (svgFile && svgFile instanceof TFile) { + return; + } //If auto export SVG or PNG is enabled it will be inserted at the top of the excalidraw file. No need to manually insert hover preview + + const pngFileName = getIMGFilename(file.path, "png"); + const pngFile = vault.getAbstractFileByPath(pngFileName); + if (pngFile && pngFile instanceof TFile) { + return; + } //If auto export SVG or PNG is enabled it will be inserted at the top of the excalidraw file. No need to manually insert hover preview + + if (!plugin.hover.linkText) { + return; + } + if (m.length !== 1) { + return; + } + if (m[0].addedNodes.length !== 1) { + return; + } + if ( + (m[0].addedNodes[0] as HTMLElement).className !== "popover hover-popover" + ) { + return; + } + const node = m[0].addedNodes[0]; + node.empty(); + + //this div will be on top of original DIV. By stopping the propagation of the click + //I prevent the default Obsidian feature of opening the link in the native app + const img = await getIMG({ + file, + fname: file.path, + fwidth: "300", + fheight: null, + style: ["excalidraw-svg"], + }); + const div = createDiv("", async (el) => { + el.appendChild(img); + el.setAttribute("src", file.path); + el.onClickEvent((ev) => { + ev.stopImmediatePropagation(); + const src = el.getAttribute("src"); + if (src) { + plugin.openDrawing( + vault.getAbstractFileByPath(src) as TFile, + linkClickModifierType(ev) + ); + } //.ctrlKey||ev.metaKey); + }); + }); + node.appendChild(div); +}; + +export const legacyExcalidrawPopoverObserver = DEBUGGING + ? new CustomMutationObserver(legacyExcalidrawPopoverObserverFn, "legacyExcalidrawPopoverObserverFn") + : new MutationObserver(legacyExcalidrawPopoverObserverFn); + diff --git a/src/Managers/ObserverManager.ts b/src/Core/Managers/ObserverManager.ts similarity index 97% rename from src/Managers/ObserverManager.ts rename to src/Core/Managers/ObserverManager.ts index 9145115..ae946ca 100644 --- a/src/Managers/ObserverManager.ts +++ b/src/Core/Managers/ObserverManager.ts @@ -1,7 +1,7 @@ -import { debug, DEBUGGING } from "src/utils/DebugHelper"; -import ExcalidrawPlugin from "src/main"; -import { CustomMutationObserver } from "src/utils/DebugHelper"; -import { getExcalidrawViews, isObsidianThemeDark } from "src/utils/ObsidianUtils"; +import { debug, DEBUGGING } from "src/Utils/DebugHelper"; +import ExcalidrawPlugin from "src/Core/main"; +import { CustomMutationObserver } from "src/Utils/DebugHelper"; +import { getExcalidrawViews, isObsidianThemeDark } from "src/Utils/ObsidianUtils"; import { App, Notice, TFile } from "obsidian"; export class ObserverManager { diff --git a/src/Managers/PackageManager.ts b/src/Core/Managers/PackageManager.ts similarity index 90% rename from src/Managers/PackageManager.ts rename to src/Core/Managers/PackageManager.ts index c462693..ff1dccd 100644 --- a/src/Managers/PackageManager.ts +++ b/src/Core/Managers/PackageManager.ts @@ -1,9 +1,9 @@ -import { updateExcalidrawLib } from "src/constants/constants"; -import { ExcalidrawLib } from "../ExcalidrawLib"; -import { Packages } from "../types/types"; -import { debug, DEBUGGING } from "../utils/DebugHelper"; +import { updateExcalidrawLib } from "src/Constants/Constants"; +import { ExcalidrawLib } from "../../Types/ExcalidrawLib"; +import { Packages } from "../../Types/Types"; +import { debug, DEBUGGING } from "../../Utils/DebugHelper"; import { Notice } from "obsidian"; -import ExcalidrawPlugin from "src/main"; +import ExcalidrawPlugin from "src/Core/main"; declare let REACT_PACKAGES:string; declare let react:any; diff --git a/src/index.ts b/src/Core/index.ts similarity index 90% rename from src/index.ts rename to src/Core/index.ts index 38c6ae5..24feded 100644 --- a/src/index.ts +++ b/src/Core/index.ts @@ -1,14 +1,14 @@ -import "obsidian"; -//import { ExcalidrawAutomate } from "./ExcalidrawAutomate"; -//export ExcalidrawAutomate from "./ExcalidrawAutomate"; -//export {ExcalidrawAutomate} from "./ExcaildrawAutomate"; -export type { ExcalidrawBindableElement, ExcalidrawElement, FileId, FillStyle, StrokeRoundness, StrokeStyle } from "@zsviczian/excalidraw/types/excalidraw/element/types"; -export type { Point } from "src/types/types"; -export const getEA = (view?:any): any => { - try { - return window.ExcalidrawAutomate.getAPI(view); - } catch(e) { - console.log({message: "Excalidraw not available", fn: getEA}); - return null; - } +import "obsidian"; +//import { ExcalidrawAutomate } from "./ExcalidrawAutomate"; +//export ExcalidrawAutomate from "./ExcalidrawAutomate"; +//export {ExcalidrawAutomate} from "./ExcaildrawAutomate"; +export type { ExcalidrawBindableElement, ExcalidrawElement, FileId, FillStyle, StrokeRoundness, StrokeStyle } from "@zsviczian/excalidraw/types/excalidraw/element/types"; +export type { Point } from "src/Types/Types"; +export const getEA = (view?:any): any => { + try { + return window.ExcalidrawAutomate.getAPI(view); + } catch(e) { + console.log({message: "Excalidraw not available", fn: getEA}); + return null; + } } \ No newline at end of file diff --git a/src/main.ts b/src/Core/main.ts similarity index 94% rename from src/main.ts rename to src/Core/main.ts index 6bc26fd..0a978ba 100644 --- a/src/main.ts +++ b/src/Core/main.ts @@ -1,1441 +1,1441 @@ -import { - TFile, - Plugin, - WorkspaceLeaf, - addIcon, - App, - PluginManifest, - MarkdownView, - normalizePath, - ViewState, - Notice, - request, - MetadataCache, - Workspace, - TAbstractFile, - FrontMatterCache, -} from "obsidian"; -import { - VIEW_TYPE_EXCALIDRAW, - EXCALIDRAW_ICON, - ICON_NAME, - SCRIPTENGINE_ICON, - SCRIPTENGINE_ICON_NAME, - RERENDER_EVENT, - FRONTMATTER_KEYS, - FRONTMATTER, - JSON_parse, - SCRIPT_INSTALL_CODEBLOCK, - SCRIPT_INSTALL_FOLDER, - EXPORT_TYPES, - EXPORT_IMG_ICON_NAME, - EXPORT_IMG_ICON, - LOCALE, - setExcalidrawPlugin, - DEVICE, - FONTS_STYLE_ID, - CJK_STYLE_ID, - updateExcalidrawLib, - loadMermaid, - setRootElementSize, -} from "./constants/constants"; -import { ExcalidrawSettings, DEFAULT_SETTINGS, ExcalidrawSettingTab } from "./settings"; -import { initExcalidrawAutomate, ExcalidrawAutomate } from "./ExcalidrawAutomate"; -import { around, dedupe } from "monkey-around"; -import { t } from "./lang/helpers"; -import { - checkAndCreateFolder, - fileShouldDefaultAsExcalidraw, - getDrawingFilename, - getIMGFilename, - getNewUniqueFilepath, -} from "./utils/FileUtils"; -import { - getFontDataURL, - errorlog, - setLeftHandedMode, - sleep, - isVersionNewerThanOther, - isCallerFromTemplaterPlugin, - versionUpdateCheckTimer, - getFontMetrics, -} from "./utils/Utils"; -import { foldExcalidrawSection, getExcalidrawViews, setExcalidrawView } from "./utils/ObsidianUtils"; -import { FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types"; -import { ScriptEngine } from "./Scripts"; -import { hoverEvent, initializeMarkdownPostProcessor, markdownPostProcessor, legacyExcalidrawPopoverObserver } from "./MarkdownPostProcessor"; -import { FieldSuggester } from "./Components/Suggesters/FieldSuggester"; -import { ReleaseNotes } from "./dialogs/ReleaseNotes"; -import { Packages } from "./types/types"; -import { PreviewImageType } from "./utils/UtilTypes"; -import { emulateCTRLClickForLinks, linkClickModifierType, PaneTarget } from "./utils/ModifierkeyHelper"; -import { imageCache } from "./utils/ImageCache"; -import { StylesManager } from "./utils/StylesManager"; -import { CustomMutationObserver, debug, log, DEBUGGING, setDebugging, ts } from "./utils/DebugHelper"; -import { ExcalidrawConfig } from "./utils/ExcalidrawConfig"; -import { EditorHandler } from "./CodeMirrorExtension/EditorHandler"; -import { ExcalidrawLib } from "./ExcalidrawLib"; -import { Rank, SwordColors } from "./menu/ActionIcons"; -import { RankMessage } from "./dialogs/RankMessage"; -import { initCompressionWorker, terminateCompressionWorker } from "./workers/compression-worker"; -import { WeakArray } from "./utils/WeakArray"; -import { getCJKDataURLs } from "./utils/CJKLoader"; -import { ExcalidrawLoading, switchToExcalidraw } from "./dialogs/ExcalidrawLoading"; -import { clearMathJaxVariables } from "./LaTeX"; -import { PluginFileManager } from "./Managers/FileManager"; -import { ObserverManager } from "./Managers/ObserverManager"; -import { PackageManager } from "./Managers/PackageManager"; -import ExcalidrawView from "./ExcalidrawView"; -import { CommandManager } from "./Managers/CommandManager"; -import { EventManager } from "./Managers/EventManager"; - -declare const PLUGIN_VERSION:string; -declare const INITIAL_TIMESTAMP: number; - -export default class ExcalidrawPlugin extends Plugin { - private fileManager: PluginFileManager; - private observerManager: ObserverManager; - private packageManager: PackageManager; - private commandManager: CommandManager; - private eventManager: EventManager; - public eaInstances = new WeakArray(); - public fourthFontLoaded: boolean = false; - public excalidrawConfig: ExcalidrawConfig; - public excalidrawFileModes: { [file: string]: string } = {}; - public settings: ExcalidrawSettings; - public activeExcalidrawView: ExcalidrawView = null; - public lastActiveExcalidrawFilePath: string = null; - public hover: { linkText: string; sourcePath: string } = { - linkText: null, - sourcePath: null, - }; - private legacyExcalidrawPopoverObserver: MutationObserver | CustomMutationObserver; - private fileExplorerObserver: MutationObserver | CustomMutationObserver; - public opencount: number = 0; - public ea: ExcalidrawAutomate; - //A master list of fileIds to facilitate copy / paste - public filesMaster: Map = - null; //fileId, path - public equationsMaster: Map = null; //fileId, formula - public mermaidsMaster: Map = null; //fileId, mermaidText - public scriptEngine: ScriptEngine; - private stylesManager:StylesManager; - public editorHandler: EditorHandler; - //if set, the next time this file is opened it will be opened as markdown - public forceToOpenInMarkdownFilepath: string = null; - //private slob:string; - public loadTimestamp:number; - private isLocalCJKFontAvailabe:boolean = undefined - public isReady = false; - private startupAnalytics: string[] = []; - private lastLogTimestamp: number; - private settingsReady: boolean = false; - public wasPenModeActivePreviously: boolean = false; - public popScope: Function = null; - public lastPDFLeafID: string = null; - - constructor(app: App, manifest: PluginManifest) { - super(app, manifest); - this.loadTimestamp = INITIAL_TIMESTAMP; - this.lastLogTimestamp = this.loadTimestamp; - this.filesMaster = new Map< - FileId, - { isHyperLink: boolean; isLocalLink: boolean; path: string; hasSVGwithBitmap: boolean; blockrefData: string; colorMapJSON?: string } - >(); - this.equationsMaster = new Map(); - this.mermaidsMaster = new Map(); - - //isExcalidraw function is used already is already used by MarkdownPostProcessor in onLoad before onLayoutReady - this.fileManager = new PluginFileManager(this); - - setExcalidrawPlugin(this); - /*if((process.env.NODE_ENV === 'development')) { - this.slob = new Array(200 * 1024 * 1024 + 1).join('A'); // Create a 200MB blob - }*/ - } - - public logStartupEvent(message:string) { - const timestamp = Date.now(); - this.startupAnalytics.push(`${message}\nTotal: ${timestamp - this.loadTimestamp}ms Delta: ${timestamp - this.lastLogTimestamp}ms\n`); - this.lastLogTimestamp = timestamp; - } - - public printStarupBreakdown() { - console.log(`Excalidraw ${PLUGIN_VERSION} startup breakdown:\n`+this.startupAnalytics.join("\n")); - } - - get locale() { - return LOCALE; - } - - get window(): Window { - return window; - }; - - get document(): Document { - return document; - }; - - // by adding the wrapper like this, likely in debug mode I am leaking memory because my code removes - // the original event handlers, not the wrapped ones. I will only uncomment this if I need to debug - /*public registerEvent(event: any) { - if (process.env.NODE_ENV !== 'development') { - super.registerEvent(event); - return; - } else { - if(!DEBUGGING) { - super.registerEvent(event); - return; - } - const originalHandler = event.fn; - - // Wrap the original event handler - const wrappedHandler = async (...args: any[]) => { - const startTime = performance.now(); // Get start time - - // Invoke the original event handler - const result = await originalHandler(...args); - - const endTime = performance.now(); // Get end time - const executionTime = endTime - startTime; - - if(executionTime > durationTreshold) { - console.log(`Excalidraw Event '${event.name}' took ${executionTime}ms to execute`); - } - - return result; - } - - // Replace the original event handler with the wrapped one - event.fn = wrappedHandler; - - // Register the modified event - super.registerEvent(event); - }; - }*/ - - /** - * used by Excalidraw to getSharedMermaidInstance - * @returns shared mermaid instance - */ - public async getMermaid() { - return await loadMermaid(); - } - - public isPenMode() { - return this.wasPenModeActivePreviously || - (this.settings.defaultPenMode === "always") || - (this.settings.defaultPenMode === "mobile" && DEVICE.isMobile); - } - - public getCJKFontSettings() { - const assetsFoler = this.settings.fontAssetsPath; - if(typeof this.isLocalCJKFontAvailabe === "undefined") { - this.isLocalCJKFontAvailabe = this.app.vault.getFiles().some(f=>f.path.startsWith(assetsFoler)); - } - if(!this.isLocalCJKFontAvailabe) { - return { c: false, j: false, k: false }; - } - return { - c: this.settings.loadChineseFonts, - j: this.settings.loadJapaneseFonts, - k: this.settings.loadKoreanFonts, - } - } - - public async loadFontFromFile(fontName: string): Promise { - const assetsFoler = this.settings.fontAssetsPath; - - if(!this.isLocalCJKFontAvailabe) { - return; - } - const file = this.app.vault.getAbstractFileByPath(normalizePath(assetsFoler + "/" + fontName)); - if(!file || !(file instanceof TFile)) { - return; - } - return await this.app.vault.readBinary(file); - } - - async onload() { - this.logStartupEvent("Plugin Constructor ready, starting onload()"); - this.registerView( - VIEW_TYPE_EXCALIDRAW, - (leaf: WorkspaceLeaf) => { - if(this.isReady) { - return new ExcalidrawView(leaf, this); - } else { - return new ExcalidrawLoading(leaf, this); - } - }, - ); - //Compatibility mode with .excalidraw files - this.registerExtensions(["excalidraw"], VIEW_TYPE_EXCALIDRAW); - - addIcon(ICON_NAME, EXCALIDRAW_ICON); - addIcon(SCRIPTENGINE_ICON_NAME, SCRIPTENGINE_ICON); - addIcon(EXPORT_IMG_ICON_NAME, EXPORT_IMG_ICON); - this.addRibbonIcon(ICON_NAME, t("CREATE_NEW"), this.actionRibbonClick.bind(this)); - - try { - this.loadSettings({reEnableAutosave:true}) - .then(this.onloadCheckForOnceOffSettingsUpdates.bind(this)); - } catch (e) { - new Notice("Error loading plugin settings", 6000); - console.error("Error loading plugin settings", e); - } - this.logStartupEvent("Settings loaded"); - - try { - // need it her for ExcaliBrain - this.ea = initExcalidrawAutomate(this); - } catch (e) { - new Notice("Error initializing Excalidraw Automate", 6000); - console.error("Error initializing Excalidraw Automate", e); - } - this.logStartupEvent("Excalidraw Automate initialized"); - - try { - //Licat: Are you registering your post processors in onLayoutReady? You should register them in onload instead - this.addMarkdownPostProcessor(); - } catch (e) { - new Notice("Error adding markdown post processor", 6000); - console.error("Error adding markdown post processor", e); - } - this.logStartupEvent("Markdown post processor added"); - - this.app.workspace.onLayoutReady(this.onloadOnLayoutReady.bind(this)); - this.logStartupEvent("Workspace ready event handler added"); - } - - private async onloadCheckForOnceOffSettingsUpdates() { - const updateSettings = !this.settings.onceOffCompressFlagReset || !this.settings.onceOffGPTVersionReset; - if(!this.settings.onceOffCompressFlagReset) { - this.settings.compress = true; - this.settings.onceOffCompressFlagReset = true; - } - if(!this.settings.onceOffGPTVersionReset) { - this.settings.onceOffGPTVersionReset = true; - if(this.settings.openAIDefaultVisionModel === "gpt-4-vision-preview") { - this.settings.openAIDefaultVisionModel = "gpt-4o"; - } - } - if(updateSettings) { - await this.saveSettings(); - } - this.addSettingTab(new ExcalidrawSettingTab(this.app, this)); - this.settingsReady = true; - } - - private async onloadOnLayoutReady() { - this.loadTimestamp = Date.now(); - this.lastLogTimestamp = this.loadTimestamp; - this.logStartupEvent("\n----------------------------------\nWorkspace onLayoutReady event fired (these actions are outside the plugin initialization)"); - await this.awaitSettings(); - this.logStartupEvent("Settings awaited"); - if(!this.settings.overrideObsidianFontSize) { - setRootElementSize(); - } - - this.packageManager = new PackageManager(this); - this.eventManager = new EventManager(this); - this.observerManager = new ObserverManager(this); - this.commandManager = new CommandManager(this); - - try { - initCompressionWorker(); - } catch (e) { - new Notice("Error initializing compression worker", 6000); - console.error("Error initializing compression worker", e); - } - this.logStartupEvent("Compression worker initialized"); - - try { - this.excalidrawConfig = new ExcalidrawConfig(this); - } catch (e) { - new Notice("Error initializing Excalidraw config", 6000); - console.error("Error initializing Excalidraw config", e); - } - this.logStartupEvent("Excalidraw config initialized"); - - this.observerManager.initialize(); - - try { - //inspiration taken from kanban: - //https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/main.ts#L267 - this.registerMonkeyPatches(); - } catch (e) { - new Notice("Error registering monkey patches", 6000); - console.error("Error registering monkey patches", e); - } - this.logStartupEvent("Monkey patches registered"); - - try { - this.stylesManager = new StylesManager(this); - } catch (e) { - new Notice("Error initializing styles manager", 6000); - console.error("Error initializing styles manager", e); - } - this.logStartupEvent("Styles manager initialized"); - - try { - this.scriptEngine = new ScriptEngine(this); - } catch (e) { - new Notice("Error initializing script engine", 6000); - console.error("Error initializing script engine", e); - } - this.logStartupEvent("Script engine initialized"); - - try { - await this.initializeFonts(); - } catch (e) { - new Notice("Error initializing fonts", 6000); - console.error("Error initializing fonts", e); - } - this.logStartupEvent("Fonts initialized"); - - try { - imageCache.initializeDB(this); - } catch (e) { - new Notice("Error initializing image cache", 6000); - console.error("Error initializing image cache", e); - } - this.logStartupEvent("Image cache initialized"); - - try { - this.isReady = true; - switchToExcalidraw(this.app); - this.switchToExcalidarwAfterLoad(); - } catch (e) { - new Notice("Error switching views to Excalidraw", 6000); - console.error("Error switching views to Excalidraw", e); - } - this.logStartupEvent("Switched to Excalidraw views"); - - try { - if (this.settings.showReleaseNotes) { - //I am repurposing imageElementNotice, if the value is true, this means the plugin was just newly installed to Obsidian. - const obsidianJustInstalled = (this.settings.previousRelease === "0.0.0") || !this.settings.previousRelease; - - if (isVersionNewerThanOther(PLUGIN_VERSION, this.settings.previousRelease ?? "0.0.0")) { - new ReleaseNotes( - this.app, - this, - obsidianJustInstalled ? null : PLUGIN_VERSION, - ).open(); - } - } - } catch (e) { - new Notice("Error opening release notes", 6000); - console.error("Error opening release notes", e); - } - this.logStartupEvent("Release notes opened"); - - //--------------------------------------------------------------------- - //initialization that can happen after Excalidraw views are initialized - //--------------------------------------------------------------------- - - this.fileManager.initialize(); //fileManager will preLoad the filecache - this.eventManager.initialize(); //eventManager also adds event listner to filecache - - try { - this.runStartupScript(); - } catch (e) { - new Notice("Error running startup script", 6000); - console.error("Error running startup script", e); - } - this.logStartupEvent("Startup script run"); - - try { - this.editorHandler = new EditorHandler(this); - this.editorHandler.setup(); - } catch (e) { - new Notice("Error setting up editor handler", 6000); - console.error("Error setting up editor handler", e); - } - this.logStartupEvent("Editor handler initialized"); - - try { - this.registerInstallCodeblockProcessor(); - } catch (e) { - new Notice("Error registering script install-codeblock processor", 6000); - console.error("Error registering script install-codeblock processor", e); - } - this.logStartupEvent("Script install-codeblock processor registered"); - - this.commandManager.initialize(); - - try { - this.registerEditorSuggest(new FieldSuggester(this)); - } catch (e) { - new Notice("Error registering editor suggester", 6000); - console.error("Error registering editor suggester", e); - } - this.logStartupEvent("Editor suggester registered"); - - try { - this.setPropertyTypes(); - } catch (e) { - new Notice("Error setting up property types", 6000); - console.error("Error setting up property types", e); - } - this.logStartupEvent("Property types set"); - } - - public async awaitSettings() { - let counter = 0; - while(!this.settingsReady && counter < 150) { - await sleep(20); - } - } - - public async awaitInit() { - let counter = 0; - while(!this.isReady && counter < 150) { - await sleep(50); - } - } - - /** - * Loads the Excalidraw frontmatter tags to Obsidian property suggester so people can more easily find relevant front matter switches - * Must run after the workspace is ready - * @returns - */ - private async setPropertyTypes() { - if(!this.settings.loadPropertySuggestions) return; - const app = this.app; - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setPropertyTypes, `ExcalidrawPlugin.setPropertyTypes`); - Object.keys(FRONTMATTER_KEYS).forEach((key) => { - if(FRONTMATTER_KEYS[key].depricated === true) return; - const {name, type} = FRONTMATTER_KEYS[key]; - app.metadataTypeManager.setType(name,type); - }); - } - - public async initializeFonts() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.initializeFonts, `ExcalidrawPlugin.initializeFonts`); - const cjkFontDataURLs = await getCJKDataURLs(this); - if(typeof cjkFontDataURLs === "boolean" && !cjkFontDataURLs) { - new Notice(t("FONTS_LOAD_ERROR") + this.settings.fontAssetsPath,6000); - } - - if(typeof cjkFontDataURLs === "object") { - const fontDeclarations = cjkFontDataURLs.map(dataURL => - `@font-face { font-family: 'Xiaolai'; src: url("${dataURL}"); font-display: swap; font-weight: 400; }` - ); - for(const ownerDocument of this.getOpenObsidianDocuments()) { - await this.addFonts(fontDeclarations, ownerDocument, CJK_STYLE_ID); - }; - new Notice(t("FONTS_LOADED")); - } - - const font = await getFontDataURL( - this.app, - this.settings.experimantalFourthFont, - "", - "Local Font", - ); - - if(font.dataURL === "") { - this.fourthFontLoaded = true; - return; - } - - const fourthFontDataURL = font.dataURL; - - const f = this.app.metadataCache.getFirstLinkpathDest(this.settings.experimantalFourthFont, ""); - // Call getFontMetrics with the fourthFontDataURL - let fontMetrics = f.extension.startsWith("woff") ? undefined : await getFontMetrics(fourthFontDataURL, "Local Font"); - - if (!fontMetrics) { - console.log("Font Metrics not found, using default"); - fontMetrics = { - unitsPerEm: 1000, - ascender: 750, - descender: -250, - lineHeight: 1.2, - fontName: "Local Font", - } - } - this.packageManager.getPackageMap().forEach(({excalidrawLib}) => { - (excalidrawLib as typeof ExcalidrawLib).registerLocalFont({metrics: fontMetrics as any, icon: null}, fourthFontDataURL); - }); - // Add fonts to open Obsidian documents - for(const ownerDocument of this.getOpenObsidianDocuments()) { - await this.addFonts([ - `@font-face{font-family:'Local Font';src:url("${fourthFontDataURL}");font-display: swap;font-weight: 400;`, - ], ownerDocument); - }; - if(!this.fourthFontLoaded) setTimeout(()=>{this.fourthFontLoaded = true},100); - } - - public async addFonts(declarations: string[],ownerDocument:Document = document, styleId:string = FONTS_STYLE_ID) { - // replace the old local font ` : "" - }${xml}${ - xmlFooter //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/286#issuecomment-982179639 - }${ - fontDef !== "" ? `` : "" - }`; - - //4. - //create document div - this will be the contents of the foreign object - const mdDIV = createDiv(); - mdDIV.setAttribute("xmlns", "http://www.w3.org/1999/xhtml"); - mdDIV.setAttribute("class", "excalidraw-md-host"); - // mdDIV.setAttribute("style",style); - if (fontName !== "") { - mdDIV.style.fontFamily = fontName; - } - mdDIV.style.overflow = "auto"; - mdDIV.style.display = "block"; - mdDIV.style.color = fontColor && fontColor !== "" ? fontColor : "initial"; - - //await MarkdownRenderer.renderMarkdown(text, mdDIV, file.path, plugin); - await MarkdownRenderer.render(this.plugin.app,text,mdDIV,file.path,this.plugin); - - mdDIV - .querySelectorAll(":scope > *[class^='frontmatter']") - .forEach((el) => mdDIV.removeChild(el)); - - await replaceBlobWithBase64(mdDIV); //because image cache returns a blob - const internalEmbeds = Array.from(mdDIV.querySelectorAll("span[class='internal-embed']")) - for(let i=0;i { - const elementStyle = el.style; - const computedStyle = window.getComputedStyle(el); - let style = ""; - for (const prop in elementStyle) { - if (elementStyle.hasOwnProperty(prop)) { - style += `${prop}: ${computedStyle[prop]};`; - } - } - el.setAttribute("style", style); - }); - - const xmlINiframe = new XMLSerializer().serializeToString(stylingDIV); - const xmlFooter = new XMLSerializer().serializeToString(footerDIV); - document.body.removeChild(iframeHost); - - //5.2 - //get SVG size - const parser = new DOMParser(); - const doc = parser.parseFromString( - svg(xmlINiframe, xmlFooter), - "image/svg+xml", - ); - const svgEl = doc.firstElementChild; - const host = createDiv(); - host.appendChild(svgEl); - document.body.appendChild(host); - const footerHeight = svgEl.querySelector( - ".excalidraw-md-footer", - ).scrollHeight; - const height = - svgEl.querySelector(".excalidraw-md-host").scrollHeight + footerHeight; - const svgHeight = height <= linkParts.height ? height : linkParts.height; - document.body.removeChild(host); - - //finalize SVG - svgStyle = ` width="${linkParts.width}px" height="${svgHeight}px"`; - foreignObjectStyle = ` width="${linkParts.width}px" height="${svgHeight}px"`; - mdDIV.style.height = `${svgHeight - footerHeight}px`; - mdDIV.style.overflow = "hidden"; - - const imageList = mdDIV.querySelectorAll( - "img:not([src^='data:image/svg+xml'])", - ); - if (imageList.length > 0) { - hasSVGwithBitmap = true; - } - if (hasSVGwithBitmap && this.isDark) { - imageList.forEach(img => { - if(img instanceof HTMLImageElement) { - img.style.filter = THEME_FILTER; - } - }); - } - - const xml = new XMLSerializer().serializeToString(mdDIV); - const finalSVG = svg(xml, '', style); - plugin.ea.mostRecentMarkdownSVG = parser.parseFromString( - finalSVG, - "image/svg+xml", - ).firstElementChild as SVGSVGElement; - return { - dataURL: svgToBase64(finalSVG) as DataURL, - hasSVGwithBitmap - }; - }; -} - -const getSVGData = async (app: App, file: TFile, colorMap: ColorMap | null): Promise => { - const svgString = replaceSVGColors(await app.vault.read(file), colorMap) as string; - return svgToBase64(svgString) as DataURL; -}; - -export const generateIdFromFile = async (file: ArrayBuffer, key?: string): Promise => { - let id: FileId; - try { - // Convert the file ArrayBuffer to a Uint8Array - const fileArray = new Uint8Array(file); - - // If a key is provided, concatenate it to the file data - let dataToHash: Uint8Array; - if (key) { - const encoder = new TextEncoder(); - const keyArray = encoder.encode(key); - dataToHash = new Uint8Array(fileArray.length + keyArray.length); - dataToHash.set(fileArray); - dataToHash.set(keyArray, fileArray.length); - } else { - dataToHash = fileArray; - } - - // Hash the combined data (file and key, if provided) - const hashBuffer = await window.crypto.subtle.digest("SHA-1", dataToHash); - id = - // Convert buffer to byte array - Array.from(new Uint8Array(hashBuffer)) - // Convert to hex string - .map((byte) => byte.toString(16).padStart(2, "0")) - .join("") as FileId; - } catch (error) { - errorlog({ where: "EmbeddedFileLoader.generateIdFromFile", error }); - id = fileid() as FileId; - } - return id; -}; - -const replaceBlobWithBase64 = async (divElement: HTMLDivElement): Promise => { - const images = divElement.querySelectorAll('img[src^="blob:app://obsidian.md"]'); - - for (let img of images) { - const blobUrl = img.src; - try { - const response = await fetch(blobUrl); - const blob = await response.blob(); - const base64 = await blobToBase64(blob); - img.src = `data:${blob.type};base64,${base64}`; - } catch (error) { - console.error(`Failed to fetch or convert blob: ${blobUrl}`, error); - } - } +//https://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api +//https://img.youtube.com/vi/uZz5MgzWXiM/maxresdefault.jpg + +import { ExcalidrawElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types"; +import { BinaryFileData, DataURL } from "@zsviczian/excalidraw/types/excalidraw/types"; +import { App, MarkdownRenderer, Notice, TFile } from "obsidian"; +import { + DEFAULT_MD_EMBED_CSS, + fileid, + IMAGE_TYPES, + nanoid, + THEME_FILTER, + FRONTMATTER_KEYS, + getCSSFontDefinition, +} from "../Constants/Constants"; +import { createSVG } from "./ExcalidrawAutomate"; +import { ExcalidrawData, getTransclusion } from "./ExcalidrawData"; +import { ExportSettings } from "../View/ExcalidrawView"; +import { t } from "../Lang/Helpers"; +import { tex2dataURL } from "./LaTeX"; +import ExcalidrawPlugin from "../Core/main"; +import { blobToBase64, getDataURLFromURL, getMimeType, getPDFDoc, getURLImageExtension, hasExcalidrawEmbeddedImagesTreeChanged, readLocalFileBinary } from "../Utils/FileUtils"; +import { + errorlog, + getDataURL, + getExportTheme, + getFontDataURL, + getImageSize, + getLinkParts, + getExportPadding, + getWithBackground, + hasExportBackground, + hasExportTheme, + LinkParts, + svgToBase64, + isMaskFile, + getEmbeddedFilenameParts, + cropCanvas, + promiseTry, + PromisePool, +} from "../Utils/Utils"; +import { ValueOf } from "../Types/Types"; +import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "../Utils/MermaidUtils"; +import { mermaidToExcalidraw } from "src/Constants/Constants"; +import { ImageKey, imageCache } from "../Utils/ImageCache"; +import { FILENAMEPARTS, PreviewImageType } from "../Utils/UtilTypes"; + +//An ugly workaround for the following situation. +//File A is a markdown file that has an embedded Excalidraw file B +//Later file A is embedded into file B as a Markdown embed +//Because MarkdownRenderer.renderMarkdown does not take a depth parameter as input +//EmbeddedFileLoader cannot track the recursion depth (as it can when Excalidraw drawings are embedded) +//For this reason, the markdown TFile is added to the Watchdog when rendering starts +//and getObsidianImage is aborted if the file is already in the Watchdog stack +const markdownRendererRecursionWatcthdog = new Set(); + +export const IMAGE_MIME_TYPES = { + svg: "image/svg+xml", + png: "image/png", + jpg: "image/jpeg", + jpeg: "image/jpeg", + gif: "image/gif", + webp: "image/webp", + bmp: "image/bmp", + ico: "image/x-icon", + avif: "image/avif", + jfif: "image/jfif", +} as const; + +type ImgData = { + mimeType: MimeType; + fileId: FileId; + dataURL: DataURL; + created: number; + hasSVGwithBitmap: boolean; + size: { height: number; width: number }; +}; + +export declare type MimeType = ValueOf | "application/octet-stream"; + +export type FileData = BinaryFileData & { + size: Size; + hasSVGwithBitmap: boolean; + shouldScale: boolean; //true if image should maintain its area, false if image should display at 100% its size +}; + +export type Size = { + height: number; + width: number; +}; + +export interface ColorMap { + [color: string]: string; +}; + +/** + * Function takes an SVG and replaces all fill and stroke colors with the ones in the colorMap + * @param svg: SVGSVGElement + * @param colorMap: {[color: string]: string;} | null + * @returns svg with colors replaced + */ +const replaceSVGColors = (svg: SVGSVGElement | string, colorMap: ColorMap | null): SVGSVGElement | string => { + if(!colorMap) { + return svg; + } + + if(typeof svg === 'string') { + // Replace colors in the SVG string + for (const [oldColor, newColor] of Object.entries(colorMap)) { + const fillRegex = new RegExp(`fill="${oldColor}"`, 'gi'); + svg = svg.replaceAll(fillRegex, `fill="${newColor}"`); + const fillStyleRegex = new RegExp(`fill:${oldColor}`, 'gi'); + svg = svg.replaceAll(fillStyleRegex, `fill:${newColor}`); + const strokeRegex = new RegExp(`stroke="${oldColor}"`, 'gi'); + svg = svg.replaceAll(strokeRegex, `stroke="${newColor}"`); + const strokeStyleRegex = new RegExp(`stroke:${oldColor}`, 'gi'); + svg = svg.replaceAll(strokeStyleRegex, `stroke:${newColor}`); + } + return svg; + } + + // Modify the fill and stroke attributes of child nodes + const childNodes = (node: ChildNode) => { + if (node instanceof SVGElement) { + const oldFill = node.getAttribute('fill')?.toLocaleLowerCase(); + const oldStroke = node.getAttribute('stroke')?.toLocaleLowerCase(); + + if (oldFill && colorMap[oldFill]) { + node.setAttribute('fill', colorMap[oldFill]); + } + if (oldStroke && colorMap[oldStroke]) { + node.setAttribute('stroke', colorMap[oldStroke]); + } + } + for(const child of node.childNodes) { + childNodes(child); + } + } + + for (const child of svg.childNodes) { + childNodes(child); + } + + return svg; +} + +export class EmbeddedFile { + public file: TFile = null; + public isSVGwithBitmap: boolean = false; + private img: string = ""; //base64 + private imgInverted: string = ""; //base64 + public mtime: number = 0; //modified time of the image + private plugin: ExcalidrawPlugin; + public mimeType: MimeType = "application/octet-stream"; + public size: Size = { height: 0, width: 0 }; + public linkParts: LinkParts; + public filenameparts: FILENAMEPARTS + private hostPath: string; + public attemptCounter: number = 0; + public isHyperLink: boolean = false; + public isLocalLink: boolean = false; + public hyperlink:DataURL; + public colorMap: ColorMap | null = null; + + constructor(plugin: ExcalidrawPlugin, hostPath: string, imgPath: string, colorMapJSON?: string) { + this.plugin = plugin; + this.resetImage(hostPath, imgPath); + if(this.file && (this.plugin.isExcalidrawFile(this.file) || this.file.extension.toLowerCase() === "svg")) { + try { + this.colorMap = colorMapJSON ? JSON.parse(colorMapJSON.toLocaleLowerCase()) : null; + } catch (error) { + this.colorMap = null; + } + } + } + + public resetImage(hostPath: string, imgPath: string) { + this.imgInverted = this.img = ""; + this.mtime = 0; + + if(imgPath.startsWith("https://") || imgPath.startsWith("http://") || imgPath.startsWith("ftp://") || imgPath.startsWith("ftps://")) { + this.isHyperLink = true; + this.hyperlink = imgPath as DataURL; + return; + }; + + if(imgPath.startsWith("file://")) { + this.isLocalLink = true; + this.hyperlink = imgPath as DataURL; + return; + } + + this.linkParts = getLinkParts(imgPath); + this.hostPath = hostPath; + if (!this.linkParts.path) { + new Notice(`Excalidraw Error\nIncorrect embedded filename: ${imgPath}`); + return; + } + if (!this.linkParts.width) { + this.linkParts.width = this.plugin.settings.mdSVGwidth; + } + if (!this.linkParts.height) { + this.linkParts.height = this.plugin.settings.mdSVGmaxHeight; + } + this.file = this.plugin.app.metadataCache.getFirstLinkpathDest( + this.linkParts.path, + hostPath, + ); + if (!this.file) { + if(this.attemptCounter++ === 0) { + new Notice( + `Excalidraw Warning: could not find image file: ${imgPath}`, + 5000, + ); + } + } else { + this.filenameparts = getEmbeddedFilenameParts(imgPath); + this.filenameparts.filepath = this.file.path; + } + } + + private fileChanged(): boolean { + if(this.isHyperLink || this.isLocalLink) { + return false; + } + if (!this.file) { + this.file = this.plugin.app.metadataCache.getFirstLinkpathDest( + this.linkParts.path, + this.hostPath, + ); // maybe the file has synchronized in the mean time + if(!this.file) { + this.attemptCounter++; + return false; + } + } + return this.mtime !== this.file.stat.mtime; + } + + public setImage( + imgBase64: string, + mimeType: MimeType, + size: Size, + isDark: boolean, + isSVGwithBitmap: boolean, + ) { + if (!this.file && !this.isHyperLink && !this.isLocalLink) { + return; + } + if (this.fileChanged()) { + this.imgInverted = this.img = ""; + } + this.mtime = this.isHyperLink || this.isLocalLink ? 0 : this.file.stat.mtime; + this.size = size; + this.mimeType = mimeType; + switch (isDark && isSVGwithBitmap) { + case true: + this.imgInverted = imgBase64; + break; + case false: + this.img = imgBase64; + break; //bitmaps and SVGs without an embedded bitmap do not need a negative image + } + this.isSVGwithBitmap = isSVGwithBitmap; + } + + public isLoaded(isDark: boolean): boolean { + if(!this.isHyperLink && !this.isLocalLink) { + if (!this.file) { + this.file = this.plugin.app.metadataCache.getFirstLinkpathDest( + this.linkParts.path, + this.hostPath, + ); // maybe the file has synchronized in the mean time + if(!this.file) { + this.attemptCounter++; + return true; + } + } + if (this.fileChanged()) { + return false; + } + } + if (this.isSVGwithBitmap && isDark) { + return this.imgInverted !== ""; + } + return this.img !== ""; + } + + public getImage(isDark: boolean) { + if (!this.file && !this.isHyperLink && !this.isLocalLink) { + return ""; + } + if (isDark && this.isSVGwithBitmap) { + return this.imgInverted; + } + return this.img; //images that are not SVGwithBitmap, only the light string is stored, since inverted and non-inverted are === + } + + /** + * + * @returns true if image should scale such as the updated images has the same area as the previous images, false if the image should be displayed at 100% + */ + public shouldScale() { + return this.isHyperLink || this.isLocalLink || !Boolean(this.linkParts && this.linkParts.original && this.linkParts.original.endsWith("|100%")); + } +} + +export class EmbeddedFilesLoader { + private pdfDocsMap: Map = new Map(); + private plugin: ExcalidrawPlugin; + private isDark: boolean; + public terminate = false; + public uid: string; + + constructor(plugin: ExcalidrawPlugin, isDark?: boolean) { + this.plugin = plugin; + this.isDark = isDark; + this.uid = nanoid(); + } + + public emptyPDFDocsMap() { + this.pdfDocsMap.forEach((pdfDoc) => pdfDoc.destroy()); + this.pdfDocsMap.clear(); + } + + public async getObsidianImage(inFile: TFile | EmbeddedFile, depth: number): Promise<{ + mimeType: MimeType; + fileId: FileId; + dataURL: DataURL; + created: number; + hasSVGwithBitmap: boolean; + size: { height: number; width: number }; + }> { + const result = await this._getObsidianImage(inFile, depth); + this.emptyPDFDocsMap(); + return result; + } + + private async getExcalidrawSVG ({ + isDark, + file, + depth, + inFile, + hasSVGwithBitmap, + elements = [], + }: { + isDark: boolean; + file: TFile; + depth: number; + inFile: TFile | EmbeddedFile; + hasSVGwithBitmap: boolean; + elements?: ExcalidrawElement[]; + }) : Promise<{dataURL: DataURL, hasSVGwithBitmap:boolean}> { + //debug({where:"EmbeddedFileLoader.getExcalidrawSVG",uid:this.uid,file:file.name}); + const isMask = isMaskFile(this.plugin, file); + const forceTheme = hasExportTheme(this.plugin, file) + ? getExportTheme(this.plugin, file, "light") + : undefined; + const exportSettings: ExportSettings = { + withBackground: hasExportBackground(this.plugin, file) + ? getWithBackground(this.plugin, file) + : false, + withTheme: !!forceTheme, + isMask, + skipInliningFonts: false, + }; + + const hasColorMap = Boolean(inFile instanceof EmbeddedFile ? inFile.colorMap : null); + const shouldUseCache = !hasColorMap && this.plugin.settings.allowImageCacheInScene && file && imageCache.isReady(); + const hasFilenameParts = Boolean((inFile instanceof EmbeddedFile) && inFile.filenameparts); + const filenameParts = hasFilenameParts ? (inFile as EmbeddedFile).filenameparts : null; + const cacheKey:ImageKey = { + ...hasFilenameParts? { + ...filenameParts, + inlineFonts: !exportSettings.skipInliningFonts, + }: { + filepath: file.path, + hasBlockref: false, + hasGroupref: false, + hasTaskbone: false, + hasArearef: false, + hasFrameref: false, + hasClippedFrameref: false, + hasSectionref: false, + inlineFonts: !exportSettings.skipInliningFonts, + blockref: null, + sectionref: null, + linkpartReference: null, + linkpartAlias: null, + }, + isDark, + previewImageType: PreviewImageType.SVG, + scale: 1, + isTransparent: !exportSettings.withBackground, + } + + const maybeSVG = shouldUseCache + ? await imageCache.getImageFromCache(cacheKey) + : undefined; + + const svg = (maybeSVG && (maybeSVG instanceof SVGSVGElement)) + ? maybeSVG + : replaceSVGColors( + await createSVG( + hasFilenameParts + ? (filenameParts.hasGroupref || filenameParts.hasBlockref || + filenameParts.hasSectionref || filenameParts.hasFrameref || + filenameParts.hasClippedFrameref + ? filenameParts.filepath + filenameParts.linkpartReference + : file.path) + : file?.path, + false, //false + hasFilenameParts && filenameParts.hasClippedFrameref + ? {...exportSettings, frameRendering: {enabled: true, name: false, outline: false, clip: true}} + : exportSettings, + this, + forceTheme, + null, + null, + elements, + this.plugin, + depth+1, + getExportPadding(this.plugin, file), + ), + inFile instanceof EmbeddedFile ? inFile.colorMap : null + ) as SVGSVGElement; + + //https://stackoverflow.com/questions/51154171/remove-css-filter-on-child-elements + const imageList = svg.querySelectorAll( + "image:not([href^='data:image/svg'])", + ); + if (imageList.length > 0) { + hasSVGwithBitmap = true; + } + + if (hasSVGwithBitmap && isDark && !Boolean(maybeSVG)) { + imageList.forEach((i) => { + const id = i.parentElement?.id; + svg.querySelectorAll(`use[href='#${id}']`).forEach((u) => { + u.setAttribute("filter", THEME_FILTER); + }); + }); + } + if (!hasSVGwithBitmap && svg.getAttribute("hasbitmap")) { + hasSVGwithBitmap = true; + } + if(shouldUseCache && !Boolean(maybeSVG)) { + //cache SVG should have the width and height parameters and not the embedded font + //see svgWithFont below + imageCache.addImageToCache(cacheKey,"", svg); + } + + if(!svg.hasAttribute("width") && svg.hasAttribute("viewBox")){ + //2024.06.09 + //this addresses backward compatibility issues where the cache does not have the width and height attributes + //this should be removed in the future + const vb = svg.getAttr("viewBox").split(" "); + Boolean(vb[2]) && svg.setAttribute("width", vb[2]); + Boolean(vb[3]) && svg.setAttribute("height", vb[3]); + } + const dURL = svgToBase64(svg.outerHTML) as DataURL; + return {dataURL: dURL as DataURL, hasSVGwithBitmap}; + }; + + //this is a fix for backward compatibility - I messed up with generating the local link + private getLocalPath(path: string) { + const localPath = path.split("file://")[1] + if(localPath.startsWith("/")) { + return localPath.substring(1); + } + return localPath; + } + + private async _getObsidianImage(inFile: TFile | EmbeddedFile, depth: number): Promise { + if (!this.plugin || !inFile) { + return null; + } + + const app = this.plugin.app; + + const isHyperLink = inFile instanceof EmbeddedFile ? inFile.isHyperLink : false; + const isLocalLink = inFile instanceof EmbeddedFile ? inFile.isLocalLink : false; + const hyperlink = inFile instanceof EmbeddedFile ? inFile.hyperlink : ""; + const file: TFile = inFile instanceof EmbeddedFile ? inFile.file : inFile; + if(file && markdownRendererRecursionWatcthdog.has(file)) { + new Notice(`Loading of ${file.path}. Please check if there is an inifinite loop of one file embedded in the other.`); + return null; + } + + const linkParts = + isHyperLink + ? null + : inFile instanceof EmbeddedFile + ? inFile.linkParts + : { + original: file.path, + path: file.path, + isBlockRef: false, + ref: null, + width: this.plugin.settings.mdSVGwidth, + height: this.plugin.settings.mdSVGmaxHeight, + page: null, + }; + + let hasSVGwithBitmap = false; + const isExcalidrawFile = !isHyperLink && !isLocalLink && this.plugin.isExcalidrawFile(file); + const isPDF = !isHyperLink && !isLocalLink && file.extension.toLowerCase() === "pdf"; + + if ( + !isHyperLink && !isPDF && !isLocalLink && + !( + IMAGE_TYPES.contains(file.extension) || + isExcalidrawFile || + file.extension === "md" + ) + ) { + return null; + } + const ab = isHyperLink || isPDF + ? null + : isLocalLink + ? await readLocalFileBinary(this.getLocalPath((inFile as EmbeddedFile).hyperlink)) + : await app.vault.readBinary(file); + + let dURL: DataURL = null; + if (isExcalidrawFile) { + const res = await this.getExcalidrawSVG({ + isDark: this.isDark, + file, + depth, + inFile, + hasSVGwithBitmap, + }); + dURL = res.dataURL; + hasSVGwithBitmap = res.hasSVGwithBitmap; + } + + const excalidrawSVG = isExcalidrawFile ? dURL : null; + + const [pdfDataURL, pdfSize] = isPDF + ? await this.pdfToDataURL(file,linkParts) + : [null, null]; + + let mimeType: MimeType = isPDF + ? "image/png" + : "image/svg+xml"; + + const extension = isHyperLink || isLocalLink + ? getURLImageExtension(hyperlink) + : file.extension; + if (!isExcalidrawFile && !isPDF) { + mimeType = getMimeType(extension); + } + + let dataURL = + isHyperLink + ? ( + inFile instanceof EmbeddedFile + ? await getDataURLFromURL(inFile.hyperlink, mimeType) + : null + ) + : excalidrawSVG ?? pdfDataURL ?? + (file?.extension === "svg" + ? await getSVGData(app, file, inFile instanceof EmbeddedFile ? inFile.colorMap : null) + : file?.extension === "md" + ? null + : await getDataURL(ab, mimeType)); + + if(!isHyperLink && !dataURL && !isLocalLink) { + markdownRendererRecursionWatcthdog.add(file); + const result = await this.convertMarkdownToSVG(this.plugin, file, linkParts, depth); + markdownRendererRecursionWatcthdog.delete(file); + dataURL = result.dataURL; + hasSVGwithBitmap = result.hasSVGwithBitmap; + } + try{ + const size = isPDF ? pdfSize : await getImageSize(dataURL); + return { + mimeType, + fileId: await generateIdFromFile( + isHyperLink || isPDF ? (new TextEncoder()).encode(dataURL as string) : ab, + inFile instanceof EmbeddedFile ? inFile.filenameparts?.linkpartReference : undefined + ), + dataURL, + created: isHyperLink || isLocalLink ? 0 : file.stat.mtime, + hasSVGwithBitmap, + size, + }; + } catch(e) { + return null; + } + } + + public async loadSceneFiles( + excalidrawData: ExcalidrawData, + addFiles: (files: FileData[], isDark: boolean, final?: boolean) => void, + depth:number, + isThemeChange:boolean = false, + fileIDWhiteList?: Set, + ) { + + if(depth > 7) { + new Notice(t("INFINITE_LOOP_WARNING")+depth.toString(), 6000); + return; + } + const entries = excalidrawData.getFileEntries(); + //debug({where:"EmbeddedFileLoader.loadSceneFiles",uid:this.uid,isDark:this.isDark,sceneTheme:excalidrawData.scene.appState.theme}); + if (this.isDark === undefined) { + this.isDark = excalidrawData?.scene?.appState?.theme === "dark"; + } + let entry: IteratorResult<[FileId, EmbeddedFile]>; + const files: FileData[][] = []; + files.push([]); + let batch = 0; + + function* loadIterator():Generator> { + while (!(entry = entries.next()).done) { + if(fileIDWhiteList && !fileIDWhiteList.has(entry.value[0])) continue; + const embeddedFile: EmbeddedFile = entry.value[1]; + const id = entry.value[0]; + yield promiseTry(async () => { + if(this.terminate) { + return; + } + if (!embeddedFile.isLoaded(this.isDark)) { + //debug({where:"EmbeddedFileLoader.loadSceneFiles",uid:this.uid,status:"embedded Files are not loaded"}); + const data = await this._getObsidianImage(embeddedFile, depth); + if (data) { + const fileData: FileData = { + mimeType: data.mimeType, + id: id, + dataURL: data.dataURL, + created: data.created, + size: data.size, + hasSVGwithBitmap: data.hasSVGwithBitmap, + shouldScale: embeddedFile.shouldScale() + }; + files[batch].push(fileData); + } + } else if (embeddedFile.isSVGwithBitmap && (depth !== 0 || isThemeChange)) { + //this will reload the image in light/dark mode when switching themes + const fileData = { + mimeType: embeddedFile.mimeType, + id: id, + dataURL: embeddedFile.getImage(this.isDark) as DataURL, + created: embeddedFile.mtime, + size: embeddedFile.size, + hasSVGwithBitmap: embeddedFile.isSVGwithBitmap, + shouldScale: embeddedFile.shouldScale() + }; + files[batch].push(fileData); + } + }); + } + + let equationItem; + const equations = excalidrawData.getEquationEntries(); + while (!(equationItem = equations.next()).done) { + if(fileIDWhiteList && !fileIDWhiteList.has(equationItem.value[0])) continue; + const equation = equationItem.value[1]; + const id = equationItem.value[0]; + yield promiseTry(async () => { + if (this.terminate) { + return; + } + if (!excalidrawData.getEquation(id).isLoaded) { + const latex = equation.latex; + const data = await tex2dataURL(latex, 4, this.plugin.app); + if (data) { + const fileData = { + mimeType: data.mimeType, + id: id, + dataURL: data.dataURL, + created: data.created, + size: data.size, + hasSVGwithBitmap: false, + shouldScale: true + }; + files[batch].push(fileData); + } + } + }); + } + + if(shouldRenderMermaid()) { + const mermaidElements = getMermaidImageElements(excalidrawData.scene.elements); + for(const element of mermaidElements) { + yield promiseTry(async () => { + if(this.terminate) { + return; + } + const data = getMermaidText(element); + const result = await mermaidToExcalidraw( + data, + { themeVariables: { fontSize: "20" } }, + true + ); + if(!result) { + return; + } + if(result?.files) { + for (const key in result.files) { + const fileData = { + ...result.files[key], + id: element.fileId, + created: Date.now(), + hasSVGwithBitmap: false, + shouldScale: true, + size: await getImageSize(result.files[key].dataURL), + }; + files[batch].push(fileData); + } + return; + } + if(result?.elements) { + //handle case that mermaidToExcalidraw has implemented this type of diagram in the mean time + if (this.terminate) { + return; + } + const res = await this.getExcalidrawSVG({ + isDark: this.isDark, + file: null, + depth, + inFile: null, + hasSVGwithBitmap: false, + elements: result.elements + }); + if(res?.dataURL) { + const size = await getImageSize(res.dataURL); + const fileData:FileData = { + mimeType: "image/svg+xml", + id: element.fileId, + dataURL: res.dataURL, + created: Date.now(), + hasSVGwithBitmap: res.hasSVGwithBitmap, + size, + shouldScale: true, + }; + files[batch].push(fileData); + } + return; + } + }); + } + }; + } + + const addFilesTimer = setInterval(() => { + if(files[batch].length === 0) { + return; + } + try { + addFiles(files[batch], this.isDark, false); + } + catch(e) { + errorlog({ where: "EmbeddedFileLoader.loadSceneFiles", error: e }); + } + files.push([]); + batch++; + }, 1200); + + const iterator = loadIterator.bind(this)(); + const concurency = 3; + await new PromisePool(iterator, concurency).all(); + + clearInterval(addFilesTimer); + + this.emptyPDFDocsMap(); + if (this.terminate) { + return; + } + //debug({where:"EmbeddedFileLoader.loadSceneFiles",uid:this.uid,status:"add Files"}); + try { + //in try block because by the time files are loaded the user may have closed the view + addFiles(files[batch], this.isDark, true); + } catch (e) { + errorlog({ where: "EmbeddedFileLoader.loadSceneFiles", error: e }); + } + } + + private async pdfToDataURL( + file: TFile, + linkParts: LinkParts, + ): Promise<[DataURL,{width:number, height:number}]> { + try { + let width = 0, height = 0; + const pdfDoc = this.pdfDocsMap.get(file.path) ?? await getPDFDoc(file); + if(!this.pdfDocsMap.has(file.path)) { + this.pdfDocsMap.set(file.path, pdfDoc); + } + const pageNum = isNaN(linkParts.page) ? 1 : (linkParts.page??1); + const scale = this.plugin.settings.pdfScale; + const cropRect = linkParts.ref.split("rect=")[1]?.split(",").map(x=>parseInt(x)); + const validRect = cropRect && cropRect.length === 4 && cropRect.every(x=>!isNaN(x)); + + // Render the page + const renderPage = async (num:number) => { + const canvas = createEl("canvas"); + const ctx = canvas.getContext('2d'); + + // Get page + const page = await pdfDoc.getPage(num); + // Set scale + const viewport = page.getViewport({ scale }); + height = canvas.height = viewport.height; + width = canvas.width = viewport.width; + + const renderCtx = { + canvasContext: ctx, + background: 'rgba(0,0,0,0)', + viewport + }; + + await page.render(renderCtx).promise; + if(validRect) { + const [left, bottom, _, top] = page.view; + + const pageHeight = top - bottom; + width = (cropRect[2] - cropRect[0]) * scale; + height = (cropRect[3] - cropRect[1]) * scale; + + const crop = validRect ? { + left: (cropRect[0] - left) * scale, + top: (bottom + pageHeight - cropRect[3]) * scale, + width, + height, + } : undefined; + if(crop) { + return cropCanvas(canvas, crop); + } + } + return canvas; + }; + + const canvas = await renderPage(pageNum); + if(canvas) { + const result: [DataURL,{width:number, height:number}] = [`data:image/png;base64,${await new Promise((resolve, reject) => { + canvas.toBlob(async (blob) => { + const dataURL = await blobToBase64(blob); + resolve(dataURL); + }); + })}` as DataURL, {width, height}]; + canvas.width = 0; //free memory iOS bug + canvas.height = 0; + return result; + } + } catch(e) { + console.log(e); + return [null,null]; + } + } + + private async convertMarkdownToSVG( + plugin: ExcalidrawPlugin, + file: TFile, + linkParts: LinkParts, + depth: number, + ): Promise<{dataURL: DataURL, hasSVGwithBitmap:boolean}> { + //1. + //get the markdown text + let hasSVGwithBitmap = false; + const transclusion = await getTransclusion(linkParts, plugin.app, file); + let text = (transclusion.leadingHashes??"") + transclusion.contents; + if (text === "") { + text = + "# Empty markdown file\nCTRL+Click here to open the file for editing in the current active pane, or CTRL+SHIFT+Click to open it in an adjacent pane."; + } + + //2. + //get styles + const fileCache = plugin.app.metadataCache.getFileCache(file); + let fontDef: string; + let fontName = plugin.settings.mdFont; + if ( + fileCache?.frontmatter && + Boolean(fileCache.frontmatter[FRONTMATTER_KEYS["font"].name]) + ) { + fontName = fileCache.frontmatter[FRONTMATTER_KEYS["font"].name]; + } + switch (fontName) { + case "Virgil": + fontDef = await getCSSFontDefinition(1); + break; + case "Cascadia": + fontDef = await getCSSFontDefinition(3); + break; + case "Assistant": + case "Helvetica": + fontDef = await getCSSFontDefinition(2); + break; + case "Excalifont": + fontDef = await getCSSFontDefinition(5); + break; + case "Nunito": + fontDef = await getCSSFontDefinition(6); + break; + case "Lilita One": + fontDef = await getCSSFontDefinition(7); + break; + case "Comic Shanns": + fontDef = await getCSSFontDefinition(8); + break; + case "Liberation Sans": + fontDef = await getCSSFontDefinition(9); + break; + case "": + fontDef = ""; + break; + default: + const font = await getFontDataURL(plugin.app, fontName, file.path); + fontDef = font.fontDef; + fontName = font.fontName; + } + + if ( + fileCache?.frontmatter && + fileCache.frontmatter["banner"] !== null + ) { + text = text.replace(/banner:\s*.*/,""); //patch https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/814 + } + + const fontColor = fileCache?.frontmatter + ? fileCache.frontmatter[FRONTMATTER_KEYS["font-color"].name] ?? + plugin.settings.mdFontColor + : plugin.settings.mdFontColor; + + let style = fileCache?.frontmatter + ? fileCache.frontmatter[FRONTMATTER_KEYS["md-css"].name] ?? "" + : ""; + let frontmatterCSSisAfile = false; + if (style && style !== "") { + const f = plugin.app.metadataCache.getFirstLinkpathDest(style, file.path); + if (f) { + style = await plugin.app.vault.read(f); + frontmatterCSSisAfile = true; + } + } + if (!frontmatterCSSisAfile) { + if (plugin.settings.mdCSS && plugin.settings.mdCSS !== "") { + const f = plugin.app.metadataCache.getFirstLinkpathDest( + plugin.settings.mdCSS, + file.path, + ); + style += f ? `\n${await plugin.app.vault.read(f)}` : DEFAULT_MD_EMBED_CSS; + } else { + style += DEFAULT_MD_EMBED_CSS; + } + } + + const borderColor = fileCache?.frontmatter + ? fileCache.frontmatter[FRONTMATTER_KEYS["border-color"].name] ?? + plugin.settings.mdBorderColor + : plugin.settings.mdBorderColor; + + if (borderColor && borderColor !== "" && !style.match(/svg/i)) { + style += `svg{border:2px solid;color:${borderColor};transform:scale(.95)}`; + } + + //3. + //SVG helper functions + //the SVG will first have ~infinite height. After sizing this will be reduced + let svgStyle = ` width="${linkParts.width}px" height="100000"`; + let foreignObjectStyle = ` width="${linkParts.width}px" height="100%"`; + + const svg = (xml: string, xmlFooter: string, style?: string) => + `${ + style ? `` : "" + }${xml}${ + xmlFooter //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/286#issuecomment-982179639 + }${ + fontDef !== "" ? `` : "" + }`; + + //4. + //create document div - this will be the contents of the foreign object + const mdDIV = createDiv(); + mdDIV.setAttribute("xmlns", "http://www.w3.org/1999/xhtml"); + mdDIV.setAttribute("class", "excalidraw-md-host"); + // mdDIV.setAttribute("style",style); + if (fontName !== "") { + mdDIV.style.fontFamily = fontName; + } + mdDIV.style.overflow = "auto"; + mdDIV.style.display = "block"; + mdDIV.style.color = fontColor && fontColor !== "" ? fontColor : "initial"; + + //await MarkdownRenderer.renderMarkdown(text, mdDIV, file.path, plugin); + await MarkdownRenderer.render(this.plugin.app,text,mdDIV,file.path,this.plugin); + + mdDIV + .querySelectorAll(":scope > *[class^='frontmatter']") + .forEach((el) => mdDIV.removeChild(el)); + + await replaceBlobWithBase64(mdDIV); //because image cache returns a blob + const internalEmbeds = Array.from(mdDIV.querySelectorAll("span[class='internal-embed']")) + for(let i=0;i { + const elementStyle = el.style; + const computedStyle = window.getComputedStyle(el); + let style = ""; + for (const prop in elementStyle) { + if (elementStyle.hasOwnProperty(prop)) { + style += `${prop}: ${computedStyle[prop]};`; + } + } + el.setAttribute("style", style); + }); + + const xmlINiframe = new XMLSerializer().serializeToString(stylingDIV); + const xmlFooter = new XMLSerializer().serializeToString(footerDIV); + document.body.removeChild(iframeHost); + + //5.2 + //get SVG size + const parser = new DOMParser(); + const doc = parser.parseFromString( + svg(xmlINiframe, xmlFooter), + "image/svg+xml", + ); + const svgEl = doc.firstElementChild; + const host = createDiv(); + host.appendChild(svgEl); + document.body.appendChild(host); + const footerHeight = svgEl.querySelector( + ".excalidraw-md-footer", + ).scrollHeight; + const height = + svgEl.querySelector(".excalidraw-md-host").scrollHeight + footerHeight; + const svgHeight = height <= linkParts.height ? height : linkParts.height; + document.body.removeChild(host); + + //finalize SVG + svgStyle = ` width="${linkParts.width}px" height="${svgHeight}px"`; + foreignObjectStyle = ` width="${linkParts.width}px" height="${svgHeight}px"`; + mdDIV.style.height = `${svgHeight - footerHeight}px`; + mdDIV.style.overflow = "hidden"; + + const imageList = mdDIV.querySelectorAll( + "img:not([src^='data:image/svg+xml'])", + ); + if (imageList.length > 0) { + hasSVGwithBitmap = true; + } + if (hasSVGwithBitmap && this.isDark) { + imageList.forEach(img => { + if(img instanceof HTMLImageElement) { + img.style.filter = THEME_FILTER; + } + }); + } + + const xml = new XMLSerializer().serializeToString(mdDIV); + const finalSVG = svg(xml, '', style); + plugin.ea.mostRecentMarkdownSVG = parser.parseFromString( + finalSVG, + "image/svg+xml", + ).firstElementChild as SVGSVGElement; + return { + dataURL: svgToBase64(finalSVG) as DataURL, + hasSVGwithBitmap + }; + }; +} + +const getSVGData = async (app: App, file: TFile, colorMap: ColorMap | null): Promise => { + const svgString = replaceSVGColors(await app.vault.read(file), colorMap) as string; + return svgToBase64(svgString) as DataURL; +}; + +export const generateIdFromFile = async (file: ArrayBuffer, key?: string): Promise => { + let id: FileId; + try { + // Convert the file ArrayBuffer to a Uint8Array + const fileArray = new Uint8Array(file); + + // If a key is provided, concatenate it to the file data + let dataToHash: Uint8Array; + if (key) { + const encoder = new TextEncoder(); + const keyArray = encoder.encode(key); + dataToHash = new Uint8Array(fileArray.length + keyArray.length); + dataToHash.set(fileArray); + dataToHash.set(keyArray, fileArray.length); + } else { + dataToHash = fileArray; + } + + // Hash the combined data (file and key, if provided) + const hashBuffer = await window.crypto.subtle.digest("SHA-1", dataToHash); + id = + // Convert buffer to byte array + Array.from(new Uint8Array(hashBuffer)) + // Convert to hex string + .map((byte) => byte.toString(16).padStart(2, "0")) + .join("") as FileId; + } catch (error) { + errorlog({ where: "EmbeddedFileLoader.generateIdFromFile", error }); + id = fileid() as FileId; + } + return id; +}; + +const replaceBlobWithBase64 = async (divElement: HTMLDivElement): Promise => { + const images = divElement.querySelectorAll('img[src^="blob:app://obsidian.md"]'); + + for (let img of images) { + const blobUrl = img.src; + try { + const response = await fetch(blobUrl); + const blob = await response.blob(); + const base64 = await blobToBase64(blob); + img.src = `data:${blob.type};base64,${base64}`; + } catch (error) { + console.error(`Failed to fetch or convert blob: ${blobUrl}`, error); + } + } }; \ No newline at end of file diff --git a/src/ExcalidrawAutomate.ts b/src/Shared/ExcalidrawAutomate.ts similarity index 95% rename from src/ExcalidrawAutomate.ts rename to src/Shared/ExcalidrawAutomate.ts index 5ab745e..2ec8cc2 100644 --- a/src/ExcalidrawAutomate.ts +++ b/src/Shared/ExcalidrawAutomate.ts @@ -1,3560 +1,3560 @@ -import ExcalidrawPlugin from "src/main"; -import { - FillStyle, - StrokeStyle, - ExcalidrawElement, - ExcalidrawBindableElement, - FileId, - NonDeletedExcalidrawElement, - ExcalidrawImageElement, - ExcalidrawTextElement, - StrokeRoundness, - RoundnessType, - ExcalidrawFrameElement, - ExcalidrawTextContainer, -} from "@zsviczian/excalidraw/types/excalidraw/element/types"; -import { MimeType } from "./EmbeddedFileLoader"; -import { Editor, normalizePath, Notice, OpenViewState, RequestUrlResponse, TFile, TFolder, WorkspaceLeaf } from "obsidian"; -import * as obsidian_module from "obsidian"; -import ExcalidrawView, { ExportSettings, TextMode, getTextMode } from "src/ExcalidrawView"; -import { ExcalidrawData, getExcalidrawMarkdownHeaderSection, getMarkdownDrawingSection, REGEX_LINK } from "src/ExcalidrawData"; -import { - FRONTMATTER, - nanoid, - MAX_IMAGE_SIZE, - COLOR_NAMES, - fileid, - GITHUB_RELEASES, - determineFocusDistance, - getCommonBoundingBox, - getLineHeight, - getMaximumGroups, - intersectElementWithLine, - measureText, - DEVICE, - restore, - REG_LINKINDEX_INVALIDCHARS, - THEME_FILTER, - mermaidToExcalidraw, - refreshTextDimensions, - getFontFamilyString, - EXCALIDRAW_PLUGIN, -} from "src/constants/constants"; -import { blobToBase64, checkAndCreateFolder, getDrawingFilename, getExcalidrawEmbeddedFilesFiletree, getListOfTemplateFiles, getNewUniqueFilepath, hasExcalidrawEmbeddedImagesTreeChanged, } from "src/utils/FileUtils"; -import { - //debug, - errorlog, - getEmbeddedFilenameParts, - getImageSize, - getLinkParts, - getPNG, - getSVG, - isMaskFile, - isVersionNewerThanOther, - scaleLoadedImage, - wrapTextAtCharLength, - arrayToMap, -} from "src/utils/Utils"; -import { getAttachmentsFolderAndFilePath, getExcalidrawViews, getLeaf, getNewOrAdjacentLeaf, isObsidianThemeDark, mergeMarkdownFiles, openLeaf } from "src/utils/ObsidianUtils"; -import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types"; -import { EmbeddedFile, EmbeddedFilesLoader, FileData } from "src/EmbeddedFileLoader"; -import { tex2dataURL } from "src/LaTeX"; -import { GenericInputPrompt, NewFileActions } from "src/dialogs/Prompt"; -import { t } from "src/lang/helpers"; -import { ScriptEngine } from "src/Scripts"; -import { ConnectionPoint, DeviceType, Point } from "src/types/types"; -import CM, { ColorMaster, extendPlugins } from "@zsviczian/colormaster"; -import HarmonyPlugin from "@zsviczian/colormaster/plugins/harmony"; -import MixPlugin from "@zsviczian/colormaster/plugins/mix" -import A11yPlugin from "@zsviczian/colormaster/plugins/accessibility" -import NamePlugin from "@zsviczian/colormaster/plugins/name" -import LCHPlugin from "@zsviczian/colormaster/plugins/lch"; -import LUVPlugin from "@zsviczian/colormaster/plugins/luv"; -import LABPlugin from "@zsviczian/colormaster/plugins/lab"; -import UVWPlugin from "@zsviczian/colormaster/plugins/uvw"; -import XYZPlugin from "@zsviczian/colormaster/plugins/xyz"; -import HWBPlugin from "@zsviczian/colormaster/plugins/hwb"; -import HSVPlugin from "@zsviczian/colormaster/plugins/hsv"; -import RYBPlugin from "@zsviczian/colormaster/plugins/ryb"; -import CMYKPlugin from "@zsviczian/colormaster/plugins/cmyk"; -import { TInput } from "@zsviczian/colormaster/types"; -import {ConversionResult, svgToExcalidraw} from "src/svgToExcalidraw/parser" -import { ROUNDNESS } from "src/constants/constants"; -import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard"; -import { emulateKeysForLinkClick, PaneTarget } from "src/utils/ModifierkeyHelper"; -import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types"; -import PolyBool from "polybooljs"; -import { EmbeddableMDCustomProps } from "./dialogs/EmbeddableSettings"; -import { - AIRequest, - postOpenAI as _postOpenAI, - extractCodeBlocks as _extractCodeBlocks, -} from "./utils/AIUtils"; -import { EXCALIDRAW_AUTOMATE_INFO, EXCALIDRAW_SCRIPTENGINE_INFO } from "./dialogs/SuggesterInfo"; -import { addBackOfTheNoteCard, getFrameBasedOnFrameNameOrId } from "./utils/ExcalidrawViewUtils"; -import { log } from "./utils/DebugHelper"; -import { ExcalidrawLib } from "./ExcalidrawLib"; -import { GlobalPoint } from "@zsviczian/excalidraw/types/math/types"; - -extendPlugins([ - HarmonyPlugin, - MixPlugin, - A11yPlugin, - NamePlugin, - LCHPlugin, - LUVPlugin, - LABPlugin, - UVWPlugin, - XYZPlugin, - HWBPlugin, - HSVPlugin, - RYBPlugin, - CMYKPlugin -]); - -declare const PLUGIN_VERSION:string; -declare var LZString: any; -declare const excalidrawLib: typeof ExcalidrawLib; - -const GAP = 4; - -export class ExcalidrawAutomate { - /** - * Utility function that returns the Obsidian Module object. - */ - get obsidian() { - return obsidian_module; - }; - - get LASERPOINTER() { - return this.plugin.settings.laserSettings; - } - - get DEVICE():DeviceType { - return DEVICE; - } - - public printStartupBreakdown() { - this.plugin.printStarupBreakdown(); - } - - public help(target: Function | string) { - if (!target) { - log("Usage: ea.help(ea.functionName) or ea.help('propertyName') or ea.help('utils.functionName') - notice property name and utils function name is in quotes"); - return; - } - - let funcInfo; - - if (typeof target === 'function') { - funcInfo = EXCALIDRAW_AUTOMATE_INFO.find((info) => info.field === target.name); - } else if (typeof target === 'string') { - let stringTarget:string = target; - stringTarget = stringTarget.startsWith("utils.") ? stringTarget.substring(6) : stringTarget; - stringTarget = stringTarget.startsWith("ea.") ? stringTarget.substring(3) : stringTarget; - funcInfo = EXCALIDRAW_AUTOMATE_INFO.find((info) => info.field === stringTarget); - if(!funcInfo) { - funcInfo = EXCALIDRAW_SCRIPTENGINE_INFO.find((info) => info.field === stringTarget); - } - } - - if(!funcInfo) { - log("Usage: ea.help(ea.functionName) or ea.help('propertyName') or ea.help('utils.functionName') - notice property name and utils function name is in quotes"); - return; - } - - let isMissing = true; - if (funcInfo.code) { - isMissing = false; - log(`Declaration: ${funcInfo.code}`); - } - if (funcInfo.desc) { - isMissing = false; - const formattedDesc = funcInfo.desc - .replaceAll("
", "\n") - .replace(/(.*?)<\/code>/g, '%c\u200b$1%c') // Zero-width space - .replace(/(.*?)<\/b>/g, '%c\u200b$1%c') // Zero-width space - .replace(/(.*?)<\/a>/g, (_, href, text) => `%c\u200b${text}%c\u200b (link: ${href})`); // Zero-width non-joiner - - const styles = Array.from({ length: (formattedDesc.match(/%c/g) || []).length }, (_, i) => i % 2 === 0 ? 'color: #007bff;' : ''); - log(`Description: ${formattedDesc}`, ...styles); - } - if (isMissing) { - log("Description not available for this function."); - } - } - - /** - * Post's an AI request to the OpenAI API and returns the response. - * @param request - * @returns - */ - public async postOpenAI (request: AIRequest): Promise { - return await _postOpenAI(request); - } - - /** - * Grabs the codeblock contents from the supplied markdown string. - * @param markdown - * @param codeblockType - * @returns an array of dictionaries with the codeblock contents and type - */ - public extractCodeBlocks(markdown: string): { data: string, type: string }[] { - return _extractCodeBlocks(markdown); - } - - /** - * converts a string to a DataURL - * @param htmlString - * @returns dataURL - */ - public async convertStringToDataURL (data:string, type: string = "text/html"):Promise { - // Create a blob from the HTML string - const blob = new Blob([data], { type }); - - // Read the blob as Data URL - const base64String = await new Promise((resolve) => { - const reader = new FileReader(); - reader.onload = () => { - if(typeof reader.result === "string") { - const base64String = reader.result.split(',')[1]; - resolve(base64String); - } else { - resolve(null); - } - }; - reader.readAsDataURL(blob); - }); - if(base64String) { - return `data:${type};base64,${base64String}`; - } - return "about:blank"; - } - - /** - * Checks if the folder exists, if not, creates it. - * @param folderpath - * @returns - */ - public async checkAndCreateFolder(folderpath: string): Promise { - return await checkAndCreateFolder(folderpath); - } - - /** - * Checks if the filepath already exists, if so, returns a new filepath with a number appended to the filename. - * @param filename - * @param folderpath - * @returns - */ - public getNewUniqueFilepath(filename: string, folderpath: string): string { - return getNewUniqueFilepath(this.plugin.app.vault, filename, folderpath); - } - - /** - * - * @returns the Excalidraw Template files or null. - */ - public getListOfTemplateFiles(): TFile[] | null { - return getListOfTemplateFiles(this.plugin); - } - - /** - * Retruns the embedded images in the scene recursively. If excalidrawFile is not provided, - * the function will use ea.targetView.file - * @param excalidrawFile - * @returns TFile[] of all nested images and Excalidraw drawings recursively - */ - public getEmbeddedImagesFiletree(excalidrawFile?: TFile): TFile[] { - if(!excalidrawFile && this.targetView && this.targetView.file) { - excalidrawFile = this.targetView.file; - } - if(!excalidrawFile) { - return []; - } - return getExcalidrawEmbeddedFilesFiletree(excalidrawFile, this.plugin); - } - - public async getAttachmentFilepath(filename: string): Promise { - if (!this.targetView || !this.targetView?.file) { - errorMessage("targetView not set", "getAttachmentFolderAndFilePath()"); - return null; - } - const folderAndPath = await getAttachmentsFolderAndFilePath(this.plugin.app,this.targetView.file.path, filename); - return getNewUniqueFilepath(this.plugin.app.vault, filename, folderAndPath.folder); - } - - public compressToBase64(str:string): string { - return LZString.compressToBase64(str); - } - - public decompressFromBase64(data:string): string { - if (!data) throw new Error("No input string provided for decompression."); - let cleanedData = ''; - const length = data.length; - for (let i = 0; i < length; i++) { - const char = data[i]; - if (char !== '\\n' && char !== '\\r') { - cleanedData += char; - } - } - return LZString.decompressFromBase64(cleanedData); - } - - /** - * Prompts the user with a dialog to select new file action. - * - create markdown file - * - create excalidraw file - * - cancel action - * The new file will be relative to this.targetView.file.path, unless parentFile is provided. - * If shouldOpenNewFile is true, the new file will be opened in a workspace leaf. - * targetPane control which leaf will be used for the new file. - * Returns the TFile for the new file or null if the user cancelled the action. - * @param newFileNameOrPath - * @param shouldOpenNewFile - * @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties"; - * @param parentFile - * @returns - */ - public async newFilePrompt( - newFileNameOrPath: string, - shouldOpenNewFile: boolean, - targetPane?: PaneTarget, - parentFile?: TFile, - ): Promise { - if (!this.targetView || !this.targetView?.file) { - errorMessage("targetView not set", "newFileActions()"); - return null; - } - const modifierKeys = emulateKeysForLinkClick(targetPane); - const newFilePrompt = new NewFileActions({ - plugin: this.plugin, - path: newFileNameOrPath, - keys: modifierKeys, - view: this.targetView, - openNewFile: shouldOpenNewFile, - parentFile: parentFile - }) - newFilePrompt.open(); - return await newFilePrompt.waitForClose; - } - - /** - * Generates a new Obsidian Leaf following Excalidraw plugin settings such as open in Main Workspace or not, open in adjacent pane if available, etc. - * @param origo // the currently active leaf, the origin of the new leaf - * @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties"; - * @returns - */ - public getLeaf ( - origo: WorkspaceLeaf, - targetPane?: PaneTarget, - ): WorkspaceLeaf { - const modifierKeys = emulateKeysForLinkClick(targetPane??"new-tab"); - return getLeaf(this.plugin,origo,modifierKeys); - } - - /** - * Returns the editor or leaf.view of the currently active embedded obsidian file. - * If view is not provided, ea.targetView is used. - * If the embedded file is a markdown document the function will return - * {file:TFile, editor:Editor} otherwise it will return {view:any}. You can check view type with view.getViewType(); - * @param view - * @returns - */ - public getActiveEmbeddableViewOrEditor (view?:ExcalidrawView): {view:any}|{file:TFile, editor:Editor}|null { - if (!this.targetView && !view) { - return null; - } - view = view ?? this.targetView; - const leafOrNode = view.getActiveEmbeddable(); - if(leafOrNode) { - if(leafOrNode.node && leafOrNode.node.isEditing) { - return {file: leafOrNode.node.file, editor: leafOrNode.node.child.editor}; - } - if(leafOrNode.leaf && leafOrNode.leaf.view) { - return {view: leafOrNode.leaf.view}; - } - } - return null; - } - - public isExcalidrawMaskFile(file?:TFile): boolean { - if(file) { - return this.isExcalidrawFile(file) && isMaskFile(this.plugin, file); - } - if (!this.targetView || !this.targetView?.file) { - errorMessage("targetView not set", "isMaskFile()"); - return null; - } - return isMaskFile(this.plugin, this.targetView.file); - } - - plugin: ExcalidrawPlugin; - elementsDict: {[key:string]:any}; //contains the ExcalidrawElements currently edited in Automate indexed by el.id - imagesDict: {[key: FileId]: any}; //the images files including DataURL, indexed by fileId - mostRecentMarkdownSVG:SVGSVGElement = null; //Markdown renderer will drop a copy of the most recent SVG here for debugging purposes - style: { - strokeColor: string; //https://www.w3schools.com/colors/default.asp - backgroundColor: string; - angle: number; //radian - fillStyle: FillStyle; //type FillStyle = "hachure" | "cross-hatch" | "solid" - strokeWidth: number; - strokeStyle: StrokeStyle; //type StrokeStyle = "solid" | "dashed" | "dotted" - roughness: number; - opacity: number; - strokeSharpness?: StrokeRoundness; //defaults to undefined, use strokeRoundess and roundess instead. Only kept for legacy script compatibility type StrokeRoundness = "round" | "sharp" - roundness: null | { type: RoundnessType; value?: number }; - fontFamily: number; //1: Virgil, 2:Helvetica, 3:Cascadia, 4:Local Font - fontSize: number; - textAlign: string; //"left"|"right"|"center" - verticalAlign: string; //"top"|"bottom"|"middle" :for future use, has no effect currently - startArrowHead: string; //"arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null - endArrowHead: string; - }; - canvas: { - theme: string; //"dark"|"light" - viewBackgroundColor: string; - gridSize: number; - }; - colorPalette: {}; - - constructor(plugin: ExcalidrawPlugin, view?: ExcalidrawView) { - this.plugin = plugin; - this.reset(); - this.targetView = view; - } - - /** - * - * @returns the last recorded pointer position on the Excalidraw canvas - */ - public getViewLastPointerPosition(): {x:number, y:number} { - //@ts-ignore - if (!this.targetView || !this.targetView?._loaded) { - errorMessage("targetView not set", "getExcalidrawAPI()"); - return null; - } - return this.targetView.currentPosition; - } - - /** - * - * @returns - */ - public getAPI(view?:ExcalidrawView):ExcalidrawAutomate { - const ea = new ExcalidrawAutomate(this.plugin, view); - this.plugin.eaInstances.push(ea); - return ea; - } - - /** - * @param val //0:"hachure", 1:"cross-hatch" 2:"solid" - * @returns - */ - setFillStyle(val: number) { - switch (val) { - case 0: - this.style.fillStyle = "hachure"; - return "hachure"; - case 1: - this.style.fillStyle = "cross-hatch"; - return "cross-hatch"; - default: - this.style.fillStyle = "solid"; - return "solid"; - } - }; - - /** - * @param val //0:"solid", 1:"dashed", 2:"dotted" - * @returns - */ - setStrokeStyle(val: number) { - switch (val) { - case 0: - this.style.strokeStyle = "solid"; - return "solid"; - case 1: - this.style.strokeStyle = "dashed"; - return "dashed"; - default: - this.style.strokeStyle = "dotted"; - return "dotted"; - } - }; - - /** - * @param val //0:"round", 1:"sharp" - * @returns - */ - setStrokeSharpness(val: number) { - switch (val) { - case 0: - this.style.roundness = { - type: ROUNDNESS.LEGACY - } - return "round"; - default: - this.style.roundness = null; //sharp - return "sharp"; - } - }; - - /** - * @param val //1: Virgil, 2:Helvetica, 3:Cascadia - * @returns - */ - setFontFamily(val: number) { - switch (val) { - case 1: - this.style.fontFamily = 4; - return getFontFamily(4); - case 2: - this.style.fontFamily = 2; - return getFontFamily(2); - case 3: - this.style.fontFamily = 3; - return getFontFamily(3); - default: - this.style.fontFamily = 1; - return getFontFamily(1); - } - }; - - /** - * @param val //0:"light", 1:"dark" - * @returns - */ - setTheme(val: number) { - switch (val) { - case 0: - this.canvas.theme = "light"; - return "light"; - default: - this.canvas.theme = "dark"; - return "dark"; - } - }; - - /** - * @param objectIds - * @returns - */ - addToGroup(objectIds: string[]): string { - const id = nanoid(); - objectIds.forEach((objectId) => { - this.elementsDict[objectId]?.groupIds?.push(id); - }); - return id; - }; - - /** - * @param templatePath - */ - async toClipboard(templatePath?: string) { - const template = templatePath - ? await getTemplate( - this.plugin, - templatePath, - false, - new EmbeddedFilesLoader(this.plugin), - 0 - ) - : null; - let elements = template ? template.elements : []; - elements = elements.concat(this.getElements()); - navigator.clipboard.writeText( - JSON.stringify({ - type: "excalidraw/clipboard", - elements, - }), - ); - }; - - /** - * @param file: TFile - * @returns ExcalidrawScene - */ - async getSceneFromFile(file: TFile): Promise<{elements: ExcalidrawElement[]; appState: AppState;}> { - if(!file) { - errorMessage("file not found", "getScene()"); - return null; - } - if(!this.isExcalidrawFile(file)) { - errorMessage("file is not an Excalidraw file", "getScene()"); - return null; - } - const template = await getTemplate(this.plugin,file.path,false,new EmbeddedFilesLoader(this.plugin),0); - return { - elements: template.elements, - appState: template.appState - } - } - - /** - * get all elements from ExcalidrawAutomate elementsDict - * @returns elements from elementsDict - */ - getElements(): Mutable[] { - const elements = []; - const elementIds = Object.keys(this.elementsDict); - for (let i = 0; i < elementIds.length; i++) { - elements.push(this.elementsDict[elementIds[i]]); - } - return elements; - }; - - /** - * get single element from ExcalidrawAutomate elementsDict - * @param id - * @returns - */ - getElement(id: string): Mutable { - return this.elementsDict[id]; - }; - - /** - * create a drawing and save it to filename - * @param params - * filename: if null, default filename as defined in Excalidraw settings - * foldername: if null, default folder as defined in Excalidraw settings - * @returns - */ - async create(params?: { - filename?: string; - foldername?: string; - templatePath?: string; - onNewPane?: boolean; - silent?: boolean; - frontmatterKeys?: { - "excalidraw-plugin"?: "raw" | "parsed"; - "excalidraw-link-prefix"?: string; - "excalidraw-link-brackets"?: boolean; - "excalidraw-url-prefix"?: string; - "excalidraw-export-transparent"?: boolean; - "excalidraw-export-dark"?: boolean; - "excalidraw-export-padding"?: number; - "excalidraw-export-pngscale"?: number; - "excalidraw-export-embed-scene"?: boolean; - "excalidraw-default-mode"?: "view" | "zen"; - "excalidraw-onload-script"?: string; - "excalidraw-linkbutton-opacity"?: number; - "excalidraw-autoexport"?: boolean; - "excalidraw-mask"?: boolean; - "excalidraw-open-md"?: boolean; - "cssclasses"?: string; - }; - plaintext?: string; //text to insert above the `# Text Elements` section - }): Promise { - - const template = params?.templatePath - ? await getTemplate( - this.plugin, - params.templatePath, - true, - new EmbeddedFilesLoader(this.plugin), - 0 - ) - : null; - if (template?.plaintext) { - if(params.plaintext) { - params.plaintext = params.plaintext + "\n\n" + template.plaintext; - } else { - params.plaintext = template.plaintext; - } - } - let elements = template ? template.elements : []; - elements = elements.concat(this.getElements()); - let frontmatter: string; - if (params?.frontmatterKeys) { - const keys = Object.keys(params.frontmatterKeys); - if (!keys.includes("excalidraw-plugin")) { - params.frontmatterKeys["excalidraw-plugin"] = "parsed"; - } - frontmatter = "---\n\n"; - for (const key of Object.keys(params.frontmatterKeys)) { - frontmatter += `${key}: ${ - //@ts-ignore - params.frontmatterKeys[key] === "" - ? '""' - : //@ts-ignore - params.frontmatterKeys[key] - }\n`; - } - frontmatter += "\n---\n"; - } else { - frontmatter = template?.frontmatter - ? template.frontmatter - : FRONTMATTER; - } - - frontmatter += params.plaintext - ? (params.plaintext.endsWith("\n\n") - ? params.plaintext - : (params.plaintext.endsWith("\n") - ? params.plaintext + "\n" - : params.plaintext + "\n\n")) - : ""; - if(template?.frontmatter && params?.frontmatterKeys) { - //the frontmatter tags supplyed to create take priority - frontmatter = mergeMarkdownFiles(template.frontmatter,frontmatter); - } - - const scene = { - type: "excalidraw", - version: 2, - source: GITHUB_RELEASES+PLUGIN_VERSION, - elements, - appState: { - theme: template?.appState?.theme ?? this.canvas.theme, - viewBackgroundColor: - template?.appState?.viewBackgroundColor ?? - this.canvas.viewBackgroundColor, - currentItemStrokeColor: - template?.appState?.currentItemStrokeColor ?? - this.style.strokeColor, - currentItemBackgroundColor: - template?.appState?.currentItemBackgroundColor ?? - this.style.backgroundColor, - currentItemFillStyle: - template?.appState?.currentItemFillStyle ?? this.style.fillStyle, - currentItemStrokeWidth: - template?.appState?.currentItemStrokeWidth ?? - this.style.strokeWidth, - currentItemStrokeStyle: - template?.appState?.currentItemStrokeStyle ?? - this.style.strokeStyle, - currentItemRoughness: - template?.appState?.currentItemRoughness ?? this.style.roughness, - currentItemOpacity: - template?.appState?.currentItemOpacity ?? this.style.opacity, - currentItemFontFamily: - template?.appState?.currentItemFontFamily ?? this.style.fontFamily, - currentItemFontSize: - template?.appState?.currentItemFontSize ?? this.style.fontSize, - currentItemTextAlign: - template?.appState?.currentItemTextAlign ?? this.style.textAlign, - currentItemStartArrowhead: - template?.appState?.currentItemStartArrowhead ?? - this.style.startArrowHead, - currentItemEndArrowhead: - template?.appState?.currentItemEndArrowhead ?? - this.style.endArrowHead, - currentItemRoundness: //type StrokeRoundness = "round" | "sharp" - template?.appState?.currentItemLinearStrokeSharpness ?? //legacy compatibility - template?.appState?.currentItemStrokeSharpness ?? //legacy compatibility - template?.appState?.currentItemRoundness ?? - this.style.roundness ? "round":"sharp", - gridSize: template?.appState?.gridSize ?? this.canvas.gridSize, - colorPalette: template?.appState?.colorPalette ?? this.colorPalette, - ...template?.appState?.frameRendering - ? {frameRendering: template.appState.frameRendering} - : {}, - ...template?.appState?.objectsSnapModeEnabled - ? {objectsSnapModeEnabled: template.appState.objectsSnapModeEnabled} - : {}, - }, - files: template?.files ?? {}, - }; - - const generateMD = ():string => { - const textElements = this.getElements().filter(el => el.type === "text") as ExcalidrawTextElement[]; - let outString = `# Excalidraw Data\n## Text Elements\n`; - textElements.forEach(te=> { - outString += `${te.rawText ?? (te.originalText ?? te.text)} ^${te.id}\n\n`; - }); - - const elementsWithLinks = this.getElements().filter( el => el.type !== "text" && el.link) - elementsWithLinks.forEach(el=>{ - outString += `${el.link} ^${el.id}\n\n`; - }) - - outString += Object.keys(this.imagesDict).length > 0 - ? `\n## Embedded Files\n` - : "\n"; - - Object.keys(this.imagesDict).forEach((key: FileId)=> { - const item = this.imagesDict[key]; - if(item.latex) { - outString += `${key}: $$${item.latex}$$\n\n`; - } else { - if(item.file) { - if(item.file instanceof TFile) { - outString += `${key}: [[${item.file.path}]]\n\n`; - } else { - outString += `${key}: [[${item.file}]]\n\n`; - } - } else { - const hyperlinkSplit = item.hyperlink.split("#"); - const file = this.plugin.app.vault.getAbstractFileByPath(hyperlinkSplit[0]); - if(file && file instanceof TFile) { - const hasFileRef = hyperlinkSplit.length === 2 - outString += hasFileRef - ? `${key}: [[${file.path}#${hyperlinkSplit[1]}]]\n\n` - : `${key}: [[${file.path}]]\n\n`; - } else { - outString += `${key}: ${item.hyperlink}\n\n`; - } - } - } - }) - return outString + "%%\n"; - } - - const filename = params?.filename - ? params.filename + (params.filename.endsWith(".md") ? "": ".excalidraw.md") - : getDrawingFilename(this.plugin.settings); - const foldername = params?.foldername ? params.foldername : this.plugin.settings.folder; - const initData = this.plugin.settings.compatibilityMode - ? JSON.stringify(scene, null, "\t") - : frontmatter + generateMD() + - getMarkdownDrawingSection(JSON.stringify(scene, null, "\t"),this.plugin.settings.compress) - - if(params.silent) { - return (await this.plugin.createDrawing(filename,foldername,initData)).path; - } else { - return this.plugin.createAndOpenDrawing( - filename, - (params?.onNewPane ? params.onNewPane : false)?"new-pane":"active-pane", - foldername, - initData - ); - } - }; - - /** - * - * @param templatePath - * @param embedFont - * @param exportSettings use ExcalidrawAutomate.getExportSettings(boolean,boolean) - * @param loader use ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) - * @param theme - * @returns - */ - async createSVG( - templatePath?: string, - embedFont: boolean = false, - exportSettings?: ExportSettings, - loader?: EmbeddedFilesLoader, - theme?: string, - padding?: number, - ): Promise { - if (!theme) { - theme = this.plugin.settings.previewMatchObsidianTheme - ? isObsidianThemeDark() - ? "dark" - : "light" - : !this.plugin.settings.exportWithTheme - ? "light" - : undefined; - } - if (theme && !exportSettings) { - exportSettings = { - withBackground: this.plugin.settings.exportWithBackground, - withTheme: true, - isMask: false, - }; - } - if (!loader) { - loader = new EmbeddedFilesLoader( - this.plugin, - theme ? theme === "dark" : undefined, - ); - } - - return await createSVG( - templatePath, - embedFont, - exportSettings, - loader, - theme, - this.canvas.theme, - this.canvas.viewBackgroundColor, - this.getElements(), - this.plugin, - 0, - padding, - this.imagesDict - ); - }; - - - /** - * - * @param templatePath - * @param scale - * @param exportSettings use ExcalidrawAutomate.getExportSettings(boolean,boolean) - * @param loader use ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) - * @param theme - * @returns - */ - async createPNG( - templatePath?: string, - scale: number = 1, - exportSettings?: ExportSettings, - loader?: EmbeddedFilesLoader, - theme?: string, - padding?: number, - ): Promise { - if (!theme) { - theme = this.plugin.settings.previewMatchObsidianTheme - ? isObsidianThemeDark() - ? "dark" - : "light" - : !this.plugin.settings.exportWithTheme - ? "light" - : undefined; - } - if (theme && !exportSettings) { - exportSettings = { - withBackground: this.plugin.settings.exportWithBackground, - withTheme: true, - isMask: false, - }; - } - if (!loader) { - loader = new EmbeddedFilesLoader( - this.plugin, - theme ? theme === "dark" : undefined, - ); - } - - return await createPNG( - templatePath, - scale, - exportSettings, - loader, - theme, - this.canvas.theme, - this.canvas.viewBackgroundColor, - this.getElements(), - this.plugin, - 0, - padding, - this.imagesDict, - ); - }; - - /** - * Wrapper for createPNG() that returns a base64 encoded string - * @param templatePath - * @param scale - * @param exportSettings - * @param loader - * @param theme - * @param padding - * @returns - */ - async createPNGBase64( - templatePath?: string, - scale: number = 1, - exportSettings?: ExportSettings, - loader?: EmbeddedFilesLoader, - theme?: string, - padding?: number, - ): Promise { - const png = await this.createPNG(templatePath,scale,exportSettings,loader,theme,padding); - return `data:image/png;base64,${await blobToBase64(png)}` - } - - /** - * - * @param text - * @param lineLen - * @returns - */ - wrapText(text: string, lineLen: number): string { - return wrapTextAtCharLength(text, lineLen, this.plugin.settings.forceWrap); - }; - - private boxedElement( - id: string, - eltype: any, - x: number, - y: number, - w: number, - h: number, - link: string | null = null, - scale?: [number, number], - ) { - return { - id, - type: eltype, - x, - y, - width: w, - height: h, - angle: this.style.angle, - strokeColor: this.style.strokeColor, - backgroundColor: this.style.backgroundColor, - fillStyle: this.style.fillStyle, - strokeWidth: this.style.strokeWidth, - strokeStyle: this.style.strokeStyle, - roughness: this.style.roughness, - opacity: this.style.opacity, - roundness: this.style.strokeSharpness - ? (this.style.strokeSharpness === "round" - ? {type: ROUNDNESS.ADAPTIVE_RADIUS} - : null) - : this.style.roundness, - seed: Math.floor(Math.random() * 100000), - version: 1, - versionNonce: Math.floor(Math.random() * 1000000000), - updated: Date.now(), - isDeleted: false, - groupIds: [] as any, - boundElements: [] as any, - link, - locked: false, - ...scale ? {scale} : {}, - }; - } - - //retained for backward compatibility - addIFrame(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string { - return this.addEmbeddable(topX, topY, width, height, url, file); - } - /** - * - * @param topX - * @param topY - * @param width - * @param height - * @returns - */ - public addEmbeddable( - topX: number, - topY: number, - width: number, - height: number, - url?: string, - file?: TFile, - embeddableCustomData?: EmbeddableMDCustomProps, - ): string { - //@ts-ignore - if (!this.targetView || !this.targetView?._loaded) { - errorMessage("targetView not set", "addEmbeddable()"); - return null; - } - - if (!url && !file) { - errorMessage("Either the url or the file must be set. If both are provided the URL takes precedence", "addEmbeddable()"); - return null; - } - - const id = nanoid(); - this.elementsDict[id] = this.boxedElement( - id, - "embeddable", - topX, - topY, - width, - height, - url ? url : file ? `[[${ - this.plugin.app.metadataCache.fileToLinktext( - file, - this.targetView.file.path, - false, //file.extension === "md", //changed this to false because embedable link navigation in ExcaliBrain - ) - }]]` : "", - [1,1], - ); - this.elementsDict[id].customData = {mdProps: embeddableCustomData ?? this.plugin.settings.embeddableMarkdownDefaults}; - return id; - }; - - /** - * Add elements to frame - * @param frameId - * @param elementIDs to add - * @returns void - */ - addElementsToFrame(frameId: string, elementIDs: string[]):void { - if(!this.getElement(frameId)) return; - elementIDs.forEach(elID => { - const el = this.getElement(elID); - if(el) { - el.frameId = frameId; - } - }); - } - - /** - * - * @param topX - * @param topY - * @param width - * @param height - * @param name: the display name of the frame - * @returns - */ - addFrame(topX: number, topY: number, width: number, height: number, name?: string): string { - const id = this.addRect(topX, topY, width, height); - const frame = this.getElement(id) as Mutable; - frame.type = "frame"; - frame.backgroundColor = "transparent"; - frame.strokeColor = "#000"; - frame.strokeStyle = "solid"; - frame.strokeWidth = 2; - frame.roughness = 0; - frame.roundness = null; - if(name) frame.name = name; - return id; - } - - /** - * - * @param topX - * @param topY - * @param width - * @param height - * @returns - */ - addRect(topX: number, topY: number, width: number, height: number, id?: string): string { - if(!id) id = nanoid(); - this.elementsDict[id] = this.boxedElement( - id, - "rectangle", - topX, - topY, - width, - height, - ); - return id; - }; - - /** - * - * @param topX - * @param topY - * @param width - * @param height - * @returns - */ - addDiamond( - topX: number, - topY: number, - width: number, - height: number, - id?: string, - ): string { - if(!id) id = nanoid(); - this.elementsDict[id] = this.boxedElement( - id, - "diamond", - topX, - topY, - width, - height, - ); - return id; - }; - - /** - * - * @param topX - * @param topY - * @param width - * @param height - * @returns - */ - addEllipse( - topX: number, - topY: number, - width: number, - height: number, - id?: string, - ): string { - if(!id) id = nanoid(); - this.elementsDict[id] = this.boxedElement( - id, - "ellipse", - topX, - topY, - width, - height, - ); - return id; - }; - - /** - * - * @param topX - * @param topY - * @param width - * @param height - * @returns - */ - addBlob(topX: number, topY: number, width: number, height: number, id?: string): string { - const b = height * 0.5; //minor axis of the ellipsis - const a = width * 0.5; //major axis of the ellipsis - const sx = a / 9; - const sy = b * 0.8; - const step = 6; - const p: any = []; - const pushPoint = (i: number, dir: number) => { - const x = i + Math.random() * sx - sx / 2; - p.push([ - x + Math.random() * sx - sx / 2 + ((i % 2) * sx) / 6 + topX, - dir * Math.sqrt(b * b * (1 - (x * x) / (a * a))) + - Math.random() * sy - - sy / 2 + - ((i % 2) * sy) / 6 + - topY, - ]); - }; - let i: number; - for (i = -a + sx / 2; i <= a - sx / 2; i += a / step) { - pushPoint(i, 1); - } - for (i = a - sx / 2; i >= -a + sx / 2; i -= a / step) { - pushPoint(i, -1); - } - p.push(p[0]); - const scale = (p: [[x: number, y: number]]): [[x: number, y: number]] => { - const box = getLineBox(p); - const scaleX = width / box.w; - const scaleY = height / box.h; - let i; - for (i = 0; i < p.length; i++) { - let [x, y] = p[i]; - x = (x - box.x) * scaleX + box.x; - y = (y - box.y) * scaleY + box.y; - p[i] = [x, y]; - } - return p; - }; - id = this.addLine(scale(p), id); - this.elementsDict[id] = repositionElementsToCursor( - [this.getElement(id)], - { x: topX, y: topY }, - false, - )[0]; - return id; - }; - - /** - * Refresh the size of a text element to fit its contents - * @param id - the id of the text element - */ - public refreshTextElementSize(id: string) { - const element = this.getElement(id); - if (element.type !== "text") { - return; - } - const { w, h } = _measureText( - element.text, - element.fontSize, - element.fontFamily, - getLineHeight(element.fontFamily) - ); - element.width = w; - element.height = h; - } - - - /** - * - * @param topX - * @param topY - * @param text - * @param formatting - * box: if !null, text will be boxed - * @param id - * @returns - */ - addText( - topX: number, - topY: number, - text: string, - formatting?: { - autoResize?: boolean; //Default is true. Setting this to false will wrap the text in the text element without the need for the containser. If set to false, you must set a width value as well. - wrapAt?: number; //wrapAt is ignored if autoResize is set to false (and width is provided) - width?: number; - height?: number; - textAlign?: "left" | "center" | "right"; - box?: boolean | "box" | "blob" | "ellipse" | "diamond"; - boxPadding?: number; - boxStrokeColor?: string; - textVerticalAlign?: "top" | "middle" | "bottom"; - }, - id?: string, - ): string { - id = id ?? nanoid(); - const originalText = text; - const autoresize = ((typeof formatting?.width === "undefined") || formatting?.box) - ? true - : (formatting?.autoResize ?? true) - text = (formatting?.wrapAt && autoresize) ? this.wrapText(text, formatting.wrapAt) : text; - - const { w, h } = _measureText( - text, - this.style.fontSize, - this.style.fontFamily, - getLineHeight(this.style.fontFamily) - ); - const width = formatting?.width ? formatting.width : w; - const height = formatting?.height ? formatting.height : h; - - let boxId: string = null; - const strokeColor = this.style.strokeColor; - this.style.strokeColor = formatting?.boxStrokeColor ?? strokeColor; - const boxPadding = formatting?.boxPadding ?? 30; - if (formatting?.box) { - switch (formatting.box) { - case "ellipse": - boxId = this.addEllipse( - topX - boxPadding, - topY - boxPadding, - width + 2 * boxPadding, - height + 2 * boxPadding, - ); - break; - case "diamond": - boxId = this.addDiamond( - topX - boxPadding, - topY - boxPadding, - width + 2 * boxPadding, - height + 2 * boxPadding, - ); - break; - case "blob": - boxId = this.addBlob( - topX - boxPadding, - topY - boxPadding, - width + 2 * boxPadding, - height + 2 * boxPadding, - ); - break; - default: - boxId = this.addRect( - topX - boxPadding, - topY - boxPadding, - width + 2 * boxPadding, - height + 2 * boxPadding, - ); - } - } - this.style.strokeColor = strokeColor; - const isContainerBound = boxId && formatting.box !== "blob"; - this.elementsDict[id] = { - text, - fontSize: this.style.fontSize, - fontFamily: this.style.fontFamily, - textAlign: formatting?.textAlign - ? formatting.textAlign - : this.style.textAlign ?? "left", - verticalAlign: formatting?.textVerticalAlign ?? this.style.verticalAlign, - ...this.boxedElement(id, "text", topX, topY, width, height), - containerId: isContainerBound ? boxId : null, - originalText: isContainerBound ? originalText : text, - rawText: isContainerBound ? originalText : text, - lineHeight: getLineHeight(this.style.fontFamily), - autoResize: formatting?.box ? true : (formatting?.autoResize ?? true), - }; - if (boxId && formatting?.box === "blob") { - this.addToGroup([id, boxId]); - } - if (isContainerBound) { - const box = this.elementsDict[boxId]; - if (!box.boundElements) { - box.boundElements = []; - } - box.boundElements.push({ type: "text", id }); - } - const textElement = this.getElement(id) as Mutable; - const container = (boxId && formatting.box !== "blob") ? this.getElement(boxId) as Mutable: undefined; - const dimensions = refreshTextDimensions( - textElement, - container, - arrayToMap(this.getElements()), - originalText, - ); - if(dimensions) { - textElement.width = dimensions.width; - textElement.height = dimensions.height; - textElement.x = dimensions.x; - textElement.y = dimensions.y; - textElement.text = dimensions.text; - if(container) { - container.width = dimensions.width + 2 * boxPadding; - container.height = dimensions.height + 2 * boxPadding; - } - } - return boxId ?? id; - }; - - /** - * - * @param points - * @returns - */ - addLine(points: [[x: number, y: number]], id?: string): string { - const box = getLineBox(points); - id = id ?? nanoid(); - this.elementsDict[id] = { - points: normalizeLinePoints(points), - lastCommittedPoint: null, - startBinding: null, - endBinding: null, - startArrowhead: null, - endArrowhead: null, - ...this.boxedElement(id, "line", points[0][0], points[0][1], box.w, box.h), - }; - return id; - }; - - /** - * - * @param points - * @param formatting - * @returns - */ - addArrow( - points: [x: number, y: number][], - formatting?: { - startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null; - endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null; - startObjectId?: string; - endObjectId?: string; - }, - id?: string, - ): string { - const box = getLineBox(points); - id = id ?? nanoid(); - const startPoint = points[0] as GlobalPoint; - const endPoint = points[points.length - 1] as GlobalPoint; - this.elementsDict[id] = { - points: normalizeLinePoints(points), - lastCommittedPoint: null, - startBinding: { - elementId: formatting?.startObjectId, - focus: formatting?.startObjectId - ? determineFocusDistance( - this.getElement(formatting?.startObjectId) as ExcalidrawBindableElement, - endPoint, - startPoint, - ) - : 0.1, - gap: GAP, - }, - endBinding: { - elementId: formatting?.endObjectId, - focus: formatting?.endObjectId - ? determineFocusDistance( - this.getElement(formatting?.endObjectId) as ExcalidrawBindableElement, - startPoint, - endPoint, - ) - : 0.1, - gap: GAP, - }, - //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/388 - startArrowhead: - typeof formatting?.startArrowHead !== "undefined" - ? formatting.startArrowHead - : this.style.startArrowHead, - endArrowhead: - typeof formatting?.endArrowHead !== "undefined" - ? formatting.endArrowHead - : this.style.endArrowHead, - ...this.boxedElement(id, "arrow", points[0][0], points[0][1], box.w, box.h), - }; - if (formatting?.startObjectId) { - if (!this.elementsDict[formatting.startObjectId].boundElements) { - this.elementsDict[formatting.startObjectId].boundElements = []; - } - this.elementsDict[formatting.startObjectId].boundElements.push({ - type: "arrow", - id, - }); - } - if (formatting?.endObjectId) { - if (!this.elementsDict[formatting.endObjectId].boundElements) { - this.elementsDict[formatting.endObjectId].boundElements = []; - } - this.elementsDict[formatting.endObjectId].boundElements.push({ - type: "arrow", - id, - }); - } - return id; - }; - - /** - * Adds a mermaid diagram to ExcalidrawAutomate elements - * @param diagram string containing the mermaid diagram - * @param groupElements default is trud. If true, the elements will be grouped - * @returns the ids of the elements that were created or null if there was an error - */ - async addMermaid( - diagram: string, - groupElements: boolean = true, - ): Promise { - const result = await mermaidToExcalidraw( - diagram, { - themeVariables: {fontSize: `${this.style.fontSize}`}, - flowchart: {curve: this.style.roundness===null ? "linear" : "basis"}, - } - ); - const ids:string[] = []; - if(!result) return null; - if(result?.error) return result.error; - - if(result?.elements) { - result.elements.forEach(el=>{ - ids.push(el.id); - this.elementsDict[el.id] = el; - }) - } - - if(result?.files) { - for (const key in result.files) { - this.imagesDict[key as FileId] = { - ...result.files[key], - created: Date.now(), - isHyperLink: false, - hyperlink: null, - file: null, - hasSVGwithBitmap: false, - latex: null, - } - } - } - - if(groupElements && result?.elements && ids.length > 1) { - this.addToGroup(ids); - } - return ids; - } - - /** - * - * @param topX - * @param topY - * @param imageFile - * @returns - */ - async addImage( - topX: number, - topY: number, - imageFile: TFile | string, //string may also be an Obsidian filepath with a reference such as folder/path/my.pdf#page=2 - scale: boolean = true, //default is true which will scale the image to MAX_IMAGE_SIZE, false will insert image at 100% of its size - anchor: boolean = true, //only has effect if scale is false. If anchor is true the image path will include |100%, if false the image will be inserted at 100%, but if resized by the user it won't pop back to 100% the next time Excalidraw is opened. - ): Promise { - const id = nanoid(); - const loader = new EmbeddedFilesLoader( - this.plugin, - this.canvas.theme === "dark", - ); - const image = (typeof imageFile === "string") - ? await loader.getObsidianImage(new EmbeddedFile(this.plugin, "", imageFile),0) - : await loader.getObsidianImage(imageFile,0); - - if (!image) { - return null; - } - const fileId = typeof imageFile === "string" - ? image.fileId - : imageFile.extension === "md" || imageFile.extension.toLowerCase() === "pdf" ? fileid() as FileId : image.fileId; - this.imagesDict[fileId] = { - mimeType: image.mimeType, - id: fileId, - dataURL: image.dataURL, - created: image.created, - isHyperLink: typeof imageFile === "string", - hyperlink: typeof imageFile === "string" - ? imageFile - : null, - file: typeof imageFile === "string" - ? null - : imageFile.path + (scale || !anchor ? "":"|100%"), - hasSVGwithBitmap: image.hasSVGwithBitmap, - latex: null, - size: { //must have the natural size here (e.g. for PDF cropping) - height: image.size.height, - width: image.size.width, - }, - }; - if (scale && (Math.max(image.size.width, image.size.height) > MAX_IMAGE_SIZE)) { - const scale = - MAX_IMAGE_SIZE / Math.max(image.size.width, image.size.height); - image.size.width = scale * image.size.width; - image.size.height = scale * image.size.height; - } - this.elementsDict[id] = this.boxedElement( - id, - "image", - topX, - topY, - image.size.width, - image.size.height, - ); - this.elementsDict[id].fileId = fileId; - this.elementsDict[id].scale = [1, 1]; - if(!scale && anchor) { - this.elementsDict[id].customData = {isAnchored: true} - }; - return id; - }; - - /** - * - * @param topX - * @param topY - * @param tex - * @returns - */ - async addLaTex(topX: number, topY: number, tex: string): Promise { - const id = nanoid(); - const image = await tex2dataURL(tex, 4, this.plugin.app); - if (!image) { - return null; - } - this.imagesDict[image.fileId] = { - mimeType: image.mimeType, - id: image.fileId, - dataURL: image.dataURL, - created: image.created, - file: null, - hasSVGwithBitmap: false, - latex: tex, - }; - this.elementsDict[id] = this.boxedElement( - id, - "image", - topX, - topY, - image.size.width, - image.size.height, - ); - this.elementsDict[id].fileId = image.fileId; - this.elementsDict[id].scale = [1, 1]; - return id; - }; - - /** - * returns the base64 dataURL of the LaTeX equation rendered as an SVG - * @param tex The LaTeX equation as string - * @param scale of the image, default value is 4 - * @returns - */ - //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1930 - async tex2dataURL( - tex: string, - scale: number = 4 // Default scale value, adjust as needed - ): Promise<{ - mimeType: MimeType; - fileId: FileId; - dataURL: DataURL; - created: number; - size: { height: number; width: number }; - }> { - return await tex2dataURL(tex,scale, this.plugin.app); - }; - - /** - * - * @param objectA - * @param connectionA type ConnectionPoint = "top" | "bottom" | "left" | "right" | null - * @param objectB - * @param connectionB when passed null, Excalidraw will automatically decide - * @param formatting - * numberOfPoints: points on the line. Default is 0 ie. line will only have a start and end point - * startArrowHead: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null - * endArrowHead: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null - * padding: - * @returns - */ - connectObjects( - objectA: string, - connectionA: ConnectionPoint | null, - objectB: string, - connectionB: ConnectionPoint | null, - formatting?: { - numberOfPoints?: number; - startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null; - endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null; - padding?: number; - }, - ): string { - if (!(this.elementsDict[objectA] && this.elementsDict[objectB])) { - return; - } - - if ( - ["line", "arrow", "freedraw"].includes( - this.elementsDict[objectA].type, - ) || - ["line", "arrow", "freedraw"].includes(this.elementsDict[objectB].type) - ) { - return; - } - - const padding = formatting?.padding ? formatting.padding : 10; - const numberOfPoints = formatting?.numberOfPoints - ? formatting.numberOfPoints - : 0; - const getSidePoints = (side: string, el: any) => { - switch (side) { - case "bottom": - return [(el.x + (el.x + el.width)) / 2, el.y + el.height + padding]; - case "left": - return [el.x - padding, (el.y + (el.y + el.height)) / 2]; - case "right": - return [el.x + el.width + padding, (el.y + (el.y + el.height)) / 2]; - default: - //"top" - return [(el.x + (el.x + el.width)) / 2, el.y - padding]; - } - }; - let aX; - let aY; - let bX; - let bY; - const elA = this.elementsDict[objectA]; - const elB = this.elementsDict[objectB]; - if (!connectionA || !connectionB) { - const aCenterX = elA.x + elA.width / 2; - const bCenterX = elB.x + elB.width / 2; - const aCenterY = elA.y + elA.height / 2; - const bCenterY = elB.y + elB.height / 2; - if (!connectionA) { - const intersect = intersectElementWithLine( - elA, - [bCenterX, bCenterY] as GlobalPoint, - [aCenterX, aCenterY] as GlobalPoint, - GAP, - ); - if (intersect.length === 0) { - [aX, aY] = [aCenterX, aCenterY]; - } else { - [aX, aY] = intersect[0]; - } - } - - if (!connectionB) { - const intersect = intersectElementWithLine( - elB, - [aCenterX, aCenterY] as GlobalPoint, - [bCenterX, bCenterY] as GlobalPoint, - GAP, - ); - if (intersect.length === 0) { - [bX, bY] = [bCenterX, bCenterY]; - } else { - [bX, bY] = intersect[0]; - } - } - } - if (connectionA) { - [aX, aY] = getSidePoints(connectionA, this.elementsDict[objectA]); - } - if (connectionB) { - [bX, bY] = getSidePoints(connectionB, this.elementsDict[objectB]); - } - const numAP = numberOfPoints + 2; //number of break points plus the beginning and the end - const points:[x:number, y:number][] = []; - for (let i = 0; i < numAP; i++) { - points.push([ - aX + (i * (bX - aX)) / (numAP - 1), - aY + (i * (bY - aY)) / (numAP - 1), - ]); - } - return this.addArrow(points, { - startArrowHead: formatting?.startArrowHead, - endArrowHead: formatting?.endArrowHead, - startObjectId: objectA, - endObjectId: objectB, - }); - }; - - /** - * Adds a text label to a line or arrow. Currently only works with a straight (2 point - start & end - line) - * @param lineId id of the line or arrow object in elementsDict - * @param label the label text - * @returns undefined (if unsuccessful) or the id of the new text element - */ - addLabelToLine(lineId: string, label: string): string { - const line = this.elementsDict[lineId]; - if(!line || !["arrow","line"].includes(line.type) || line.points.length !== 2) { - return; - } - - let angle = Math.atan2(line.points[1][1],line.points[1][0]); - - const size = this.measureText(label); - //let delta = size.height/6; - - if(angle < 0) { - if(angle < -Math.PI/2) { - angle+= Math.PI; - } /*else { - delta = -delta; - } */ - } else { - if(angle > Math.PI/2) { - angle-= Math.PI; - //delta = -delta; - } - } - this.style.angle = angle; - const id = this.addText( - line.x+line.points[1][0]/2-size.width/2,//+delta, - line.y+line.points[1][1]/2-size.height,//-5*size.height/6, - label - ); - this.style.angle = 0; - return id; - } - - /** - * clear elementsDict and imagesDict only - */ - clear() { - this.elementsDict = {}; - this.imagesDict = {}; - }; - - /** - * clear() + reset all style values to default - */ - reset() { - this.clear(); - this.activeScript = null; - this.style = { - strokeColor: "#000000", - backgroundColor: "transparent", - angle: 0, - fillStyle: "hachure", - strokeWidth: 1, - strokeStyle: "solid", - roughness: 1, - opacity: 100, - roundness: null, - fontFamily: 1, - fontSize: 20, - textAlign: "left", - verticalAlign: "top", - startArrowHead: null, - endArrowHead: "arrow" - }; - this.canvas = { - theme: "light", - viewBackgroundColor: "#FFFFFF", - gridSize: 0 - }; - }; - - /** - * returns true if MD file is an Excalidraw file - * @param f - * @returns - */ - isExcalidrawFile(f: TFile): boolean { - return this.plugin.isExcalidrawFile(f); - }; - targetView: ExcalidrawView = null; //the view currently edited - /** - * sets the target view for EA. All the view operations and the access to Excalidraw API will be performend on this view - * if view is null or undefined, the function will first try setView("active"), then setView("first"). - * @param view - * @returns targetView - */ - setView(view?: ExcalidrawView | "first" | "active"): ExcalidrawView { - if(!view) { - const v = this.plugin.app.workspace.getActiveViewOfType(ExcalidrawView); - if (v instanceof ExcalidrawView) { - this.targetView = v; - } - else { - this.targetView = getExcalidrawViews(this.plugin.app)[0]; - } - } - if (view == "active") { - const v = this.plugin.app.workspace.getActiveViewOfType(ExcalidrawView); - if (!(v instanceof ExcalidrawView)) { - return; - } - this.targetView = v; - } - if (view == "first") { - this.targetView = getExcalidrawViews(this.plugin.app)[0]; - } - if (view instanceof ExcalidrawView) { - this.targetView = view; - } - return this.targetView; - }; - - /** - * - * @returns https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw#ref - */ - getExcalidrawAPI(): any { - //@ts-ignore - if (!this.targetView || !this.targetView?._loaded) { - errorMessage("targetView not set", "getExcalidrawAPI()"); - return null; - } - return (this.targetView as ExcalidrawView).excalidrawAPI; - }; - - /** - * get elements in View - * @returns - */ - getViewElements(): ExcalidrawElement[] { - //@ts-ignore - if (!this.targetView || !this.targetView?._loaded) { - errorMessage("targetView not set", "getViewElements()"); - return []; - } - return this.targetView.getViewElements(); - }; - - /** - * - * @param elToDelete - * @returns - */ - deleteViewElements(elToDelete: ExcalidrawElement[]): boolean { - //@ts-ignore - if (!this.targetView || !this.targetView?._loaded) { - errorMessage("targetView not set", "deleteViewElements()"); - return false; - } - const api = this.targetView?.excalidrawAPI as ExcalidrawImperativeAPI; - if (!api) { - return false; - } - const el: ExcalidrawElement[] = api.getSceneElements() as ExcalidrawElement[]; - const st: AppState = api.getAppState(); - this.targetView.updateScene({ - elements: el.filter((e: ExcalidrawElement) => !elToDelete.includes(e)), - appState: st, - storeAction: "capture", - }); - //this.targetView.save(); - return true; - }; - - /** - * Adds a back of the note card to the current active view - * @param sectionTitle: string - * @param activate:boolean = true; if true, the new Embedded Element will be activated after creation - * @param sectionBody?: string; - * @param embeddableCustomData?: EmbeddableMDCustomProps; formatting of the embeddable element - * @returns embeddable element id - */ - async addBackOfTheCardNoteToView(sectionTitle: string, activate: boolean = false, sectionBody?: string, embeddableCustomData?: EmbeddableMDCustomProps): Promise { - //@ts-ignore - if (!this.targetView || !this.targetView?._loaded) { - errorMessage("targetView not set", "addBackOfTheCardNoteToView()"); - return null; - } - await this.targetView.forceSave(true); - return addBackOfTheNoteCard(this.targetView, sectionTitle, activate, sectionBody, embeddableCustomData); - } - - /** - * get the selected element in the view, if more are selected, get the first - * @returns - */ - getViewSelectedElement(): any { - const elements = this.getViewSelectedElements(); - return elements ? elements[0] : null; - }; - - /** - * - * @param includeFrameChildren - * @returns - */ - getViewSelectedElements(includeFrameChildren:boolean = true): any[] { - //@ts-ignore - if (!this.targetView || !this.targetView?._loaded) { - errorMessage("targetView not set", "getViewSelectedElements()"); - return []; - } - return this.targetView.getViewSelectedElements(includeFrameChildren); - }; - - /** - * - * @param el - * @returns TFile file handle for the image element - */ - getViewFileForImageElement(el: ExcalidrawElement): TFile | null { - //@ts-ignore - if (!this.targetView || !this.targetView?._loaded) { - errorMessage("targetView not set", "getViewFileForImageElement()"); - return null; - } - if (!el || el.type !== "image") { - errorMessage( - "Must provide an image element as input", - "getViewFileForImageElement()", - ); - return null; - } - return (this.targetView as ExcalidrawView)?.excalidrawData?.getFile( - el.fileId, - )?.file; - }; - - /** - * copies elements from view to elementsDict for editing - * @param elements - */ - copyViewElementsToEAforEditing(elements: ExcalidrawElement[], copyImages: boolean = false): void { - if(copyImages && elements.some(el=>el.type === "image")) { - //@ts-ignore - if (!this.targetView || !this.targetView?._loaded) { - errorMessage("targetView not set", "copyViewElementsToEAforEditing()"); - return; - } - const sceneFiles = this.targetView.getScene().files; - elements.forEach((el) => { - this.elementsDict[el.id] = cloneElement(el); - if(el.type === "image") { - const ef = this.targetView.excalidrawData.getFile(el.fileId); - const imageWithRef = ef && ef.file && ef.linkParts && ef.linkParts.ref; - const equation = this.targetView.excalidrawData.getEquation(el.fileId); - const sceneFile = sceneFiles?.[el.fileId]; - this.imagesDict[el.fileId] = { - mimeType: sceneFile.mimeType, - id: el.fileId, - dataURL: sceneFile.dataURL, - created: sceneFile.created, - ...ef ? { - isHyperLink: ef.isHyperLink || imageWithRef, - hyperlink: imageWithRef ? `${ef.file.path}#${ef.linkParts.ref}` : ef.hyperlink, - file: imageWithRef ? null : ef.file, - hasSVGwithBitmap: ef.isSVGwithBitmap, - latex: null, - } : {}, - ...equation ? { - file: null, - isHyperLink: false, - hyperlink: null, - hasSVGwithBitmap: false, - latex: equation.latex, - } : {}, - }; - } - }); - } else { - elements.forEach((el) => { - this.elementsDict[el.id] = cloneElement(el); - }); - } - }; - - /** - * - * @param forceViewMode - * @returns - */ - viewToggleFullScreen(forceViewMode: boolean = false): void { - //@ts-ignore - if (!this.targetView || !this.targetView?._loaded) { - errorMessage("targetView not set", "viewToggleFullScreen()"); - return; - } - const view = this.targetView as ExcalidrawView; - const isFullscreen = view.isFullscreen(); - if (forceViewMode) { - view.updateScene({ - //elements: ref.getSceneElements(), - appState: { - viewModeEnabled: !isFullscreen, - }, - storeAction: "update", - }); - this.targetView.toolsPanelRef?.current?.setExcalidrawViewMode(!isFullscreen); - } - - if (isFullscreen) { - view.exitFullscreen(); - } else { - view.gotoFullscreen(); - } - }; - - setViewModeEnabled(enabled: boolean): void { - //@ts-ignore - if (!this.targetView || !this.targetView?._loaded) { - errorMessage("targetView not set", "viewToggleFullScreen()"); - return; - } - const view = this.targetView as ExcalidrawView; - view.updateScene({appState:{viewModeEnabled: enabled}, storeAction: "update"}); - view.toolsPanelRef?.current?.setExcalidrawViewMode(enabled); - } - - /** - * This function gives you a more hands on access to Excalidraw. - * @param scene - The scene you want to load to Excalidraw - * @param restore - Use this if the scene includes legacy excalidraw file elements that need to be converted to the latest excalidraw data format (not a typical usecase) - * @returns - */ - viewUpdateScene ( - scene: { - elements?: ExcalidrawElement[], - appState?: AppState, - files?: BinaryFileData, - commitToHistory?: boolean, - storeAction?: "capture" | "none" | "update", - }, - restore: boolean = false, - ):void { - //@ts-ignore - if (!this.targetView || !this.targetView?._loaded) { - errorMessage("targetView not set", "viewToggleFullScreen()"); - return; - } - if (!Boolean(scene.storeAction)) { - scene.storeAction = scene.commitToHistory ? "capture" : "update"; - } - - this.targetView.updateScene({ - elements: scene.elements, - appState: scene.appState, - files: scene.files, - storeAction: scene.storeAction, - },restore); - } - - /** - * connect an object to the selected element in the view - * @param objectA ID of the element - * @param connectionA - * @param connectionB - * @param formatting - * @returns - */ - connectObjectWithViewSelectedElement( - objectA: string, - connectionA: ConnectionPoint | null, - connectionB: ConnectionPoint | null, - formatting?: { - numberOfPoints?: number; - startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null; - endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null; - padding?: number; - }, - ): boolean { - const el = this.getViewSelectedElement(); - if (!el) { - return false; - } - const id = el.id; - this.elementsDict[id] = el; - this.connectObjects(objectA, connectionA, id, connectionB, formatting); - delete this.elementsDict[id]; - return true; - }; - - /** - * zoom tarteView to fit elements provided as input - * elements === [] will zoom to fit the entire scene - * selectElements toggles whether the elements should be in a selected state at the end of the operation - * @param selectElements - * @param elements - */ - viewZoomToElements( - selectElements: boolean, - elements: ExcalidrawElement[] - ):void { - //@ts-ignore - if (!this.targetView || !this.targetView?._loaded) { - errorMessage("targetView not set", "viewToggleFullScreen()"); - return; - } - this.targetView.zoomToElements(selectElements,elements); - } - - /** - * Adds elements from elementsDict to the current view - * @param repositionToCursor default is false - * @param save default is true - * @param newElementsOnTop controls whether elements created with ExcalidrawAutomate - * are added at the bottom of the stack or the top of the stack of elements already in the view - * Note that elements copied to the view with copyViewElementsToEAforEditing retain their - * position in the stack of elements in the view even if modified using EA - * default is false, i.e. the new elements get to the bottom of the stack - * @param shouldRestoreElements - restore elements - auto-corrects broken, incomplete or old elements included in the update - * @returns - */ - async addElementsToView( - repositionToCursor: boolean = false, - save: boolean = true, - newElementsOnTop: boolean = false, - shouldRestoreElements: boolean = false, - ): Promise { - //@ts-ignore - if (!this.targetView || !this.targetView?._loaded) { - errorMessage("targetView not set", "addElementsToView()"); - return false; - } - const elements = this.getElements(); - return await this.targetView.addElements( - elements, - repositionToCursor, - save, - this.imagesDict, - newElementsOnTop, - shouldRestoreElements, - ); - }; - - /** - * Register instance of EA to use for hooks with TargetView - * By default ExcalidrawViews will check window.ExcalidrawAutomate for event hooks. - * Using this event you can set a different instance of Excalidraw Automate for hooks - * @returns true if successful - */ - registerThisAsViewEA():boolean { - //@ts-ignore - if (!this.targetView || !this.targetView?._loaded) { - errorMessage("targetView not set", "addElementsToView()"); - return false; - } - this.targetView.setHookServer(this); - return true; - } - - /** - * Sets the targetView EA to window.ExcalidrawAutomate - * @returns true if successful - */ - deregisterThisAsViewEA():boolean { - //@ts-ignore - if (!this.targetView || !this.targetView?._loaded) { - errorMessage("targetView not set", "addElementsToView()"); - return false; - } - this.targetView.setHookServer(this); - return true; - } - - /** - * If set, this callback is triggered when the user closes an Excalidraw view. - */ - onViewUnloadHook: (view: ExcalidrawView) => void = null; - - /** - * If set, this callback is triggered, when the user changes the view mode. - * You can use this callback in case you want to do something additional when the user switches to view mode and back. - */ - onViewModeChangeHook: (isViewModeEnabled:boolean, view: ExcalidrawView, ea: ExcalidrawAutomate) => void = null; - - /** - * If set, this callback is triggered, when the user hovers a link in the scene. - * You can use this callback in case you want to do something additional when the onLinkHover event occurs. - * This callback must return a boolean value. - * In case you want to prevent the excalidraw onLinkHover action you must return false, it will stop the native excalidraw onLinkHover management flow. - */ - onLinkHoverHook: ( - element: NonDeletedExcalidrawElement, - linkText: string, - view: ExcalidrawView, - ea: ExcalidrawAutomate - ) => boolean = null; - - /** - * If set, this callback is triggered, when the user clicks a link in the scene. - * You can use this callback in case you want to do something additional when the onLinkClick event occurs. - * This callback must return a boolean value. - * In case you want to prevent the excalidraw onLinkClick action you must return false, it will stop the native excalidraw onLinkClick management flow. - */ - onLinkClickHook:( - element: ExcalidrawElement, - linkText: string, - event: MouseEvent, - view: ExcalidrawView, - ea: ExcalidrawAutomate - ) => boolean = null; - - /** - * If set, this callback is triggered, when Excalidraw receives an onDrop event. - * You can use this callback in case you want to do something additional when the onDrop event occurs. - * This callback must return a boolean value. - * In case you want to prevent the excalidraw onDrop action you must return false, it will stop the native excalidraw onDrop management flow. - */ - onDropHook: (data: { - ea: ExcalidrawAutomate; - event: React.DragEvent; - draggable: any; //Obsidian draggable object - type: "file" | "text" | "unknown"; - payload: { - files: TFile[]; //TFile[] array of dropped files - text: string; //string - }; - excalidrawFile: TFile; //the file receiving the drop event - view: ExcalidrawView; //the excalidraw view receiving the drop - pointerPosition: { x: number; y: number }; //the pointer position on canvas at the time of drop - }) => boolean = null; - - /** - * If set, this callback is triggered, when Excalidraw receives an onPaste event. - * You can use this callback in case you want to do something additional when the - * onPaste event occurs. - * This callback must return a boolean value. - * In case you want to prevent the excalidraw onPaste action you must return false, - * it will stop the native excalidraw onPaste management flow. - */ - onPasteHook: (data: { - ea: ExcalidrawAutomate; - payload: ClipboardData; - event: ClipboardEvent; - excalidrawFile: TFile; //the file receiving the paste event - view: ExcalidrawView; //the excalidraw view receiving the paste - pointerPosition: { x: number; y: number }; //the pointer position on canvas - }) => boolean = null; - - /** - * if set, this callback is triggered, when an Excalidraw file is opened - * You can use this callback in case you want to do something additional when the file is opened. - * This will run before the file level script defined in the `excalidraw-onload-script` frontmatter. - */ - onFileOpenHook: (data: { - ea: ExcalidrawAutomate; - excalidrawFile: TFile; //the file being loaded - view: ExcalidrawView; - }) => Promise; - - - /** - * if set, this callback is triggered, when an Excalidraw file is created - * see also: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1124 - */ - onFileCreateHook: (data: { - ea: ExcalidrawAutomate; - excalidrawFile: TFile; //the file being created - view: ExcalidrawView; - }) => Promise; - - - /** - * If set, this callback is triggered whenever the active canvas color changes - */ - onCanvasColorChangeHook: ( - ea: ExcalidrawAutomate, - view: ExcalidrawView, //the excalidraw view - color: string, - ) => void = null; - - /** - * If set, this callback is triggered whenever a drawing is exported to SVG. - * The string returned will replace the link in the exported SVG. - * The hook is only executed if the link is to a file internal to Obsidian - * see: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1605 - */ - onUpdateElementLinkForExportHook: (data: { - originalLink: string, - obsidianLink: string, - linkedFile: TFile | null, - hostFile: TFile, - }) => string = null; - - /** - * utility function to generate EmbeddedFilesLoader object - * @param isDark - * @returns - */ - getEmbeddedFilesLoader(isDark?: boolean): EmbeddedFilesLoader { - return new EmbeddedFilesLoader(this.plugin, isDark); - }; - - /** - * utility function to generate ExportSettings object - * @param withBackground - * @param withTheme - * @returns - */ - getExportSettings( - withBackground: boolean, - withTheme: boolean, - isMask: boolean = false, - ): ExportSettings { - return { withBackground, withTheme, isMask }; - }; - - /** - * get bounding box of elements - * bounding box is the box encapsulating all of the elements completely - * @param elements - * @returns - */ - getBoundingBox(elements: ExcalidrawElement[]): { - topX: number; - topY: number; - width: number; - height: number; - } { - const bb = getCommonBoundingBox(elements); - return { - topX: bb.minX, - topY: bb.minY, - width: bb.maxX - bb.minX, - height: bb.maxY - bb.minY, - }; - }; - - /** - * elements grouped by the highest level groups - * @param elements - * @returns - */ - getMaximumGroups(elements: ExcalidrawElement[]): ExcalidrawElement[][] { - return getMaximumGroups(elements, arrayToMap(elements)); - }; - - /** - * gets the largest element from a group. useful when a text element is grouped with a box, and you want to connect an arrow to the box - * @param elements - * @returns - */ - getLargestElement(elements: ExcalidrawElement[]): ExcalidrawElement { - if (!elements || elements.length === 0) { - return null; - } - let largestElement = elements[0]; - const getSize = (el: ExcalidrawElement): Number => { - return el.height * el.width; - }; - let largetstSize = getSize(elements[0]); - for (let i = 1; i < elements.length; i++) { - const size = getSize(elements[i]); - if (size > largetstSize) { - largetstSize = size; - largestElement = elements[i]; - } - } - return largestElement; - }; - - /** - * @param element - * @param a - * @param b - * @param gap - * @returns 2 or 0 intersection points between line going through `a` and `b` - * and the `element`, in ascending order of distance from `a`. - */ - intersectElementWithLine( - element: ExcalidrawBindableElement, - a: readonly [number, number], - b: readonly [number, number], - gap?: number, - ): Point[] { - return intersectElementWithLine( - element, - a as GlobalPoint, - b as GlobalPoint, - gap - ); - }; - - /** - * Gets the groupId for the group that contains all the elements, or null if such a group does not exist - * @param elements - * @returns null or the groupId - */ - getCommonGroupForElements(elements: ExcalidrawElement[]): string { - const groupId = elements.map(el=>el.groupIds).reduce((prev,cur)=>cur.filter(v=>prev.includes(v))); - return groupId.length > 0 ? groupId[0] : null; - } - - /** - * Gets all the elements from elements[] that share one or more groupIds with element. - * @param element - * @param elements - typically all the non-deleted elements in the scene - * @returns - */ - getElementsInTheSameGroupWithElement( - element: ExcalidrawElement, - elements: ExcalidrawElement[], - includeFrameElements: boolean = false, - ): ExcalidrawElement[] { - if(!element || !elements) return []; - const container = (element.type === "text" && element.containerId) - ? elements.filter(el=>el.id === element.containerId) - : []; - if(element.groupIds.length === 0) { - if(includeFrameElements && element.type === "frame") { - return this.getElementsInFrame(element,elements,true); - } - if(container.length === 1) return [element,container[0]]; - return [element]; - } - - const conditionFN = container.length === 1 - ? (el: ExcalidrawElement) => el.groupIds.some(id=>element.groupIds.includes(id)) || el === container[0] - : (el: ExcalidrawElement) => el.groupIds.some(id=>element.groupIds.includes(id)); - - if(!includeFrameElements) { - return elements.filter(el=>conditionFN(el)); - } else { - //I use the set and the filter at the end to preserve scene layer seqeuence - //adding frames could potentially mess up the sequence otherwise - const elementIDs = new Set(); - elements - .filter(el=>conditionFN(el)) - .forEach(el=>{ - if(el.type === "frame") { - this.getElementsInFrame(el,elements,true).forEach(el=>elementIDs.add(el.id)) - } else { - elementIDs.add(el.id); - } - }); - return elements.filter(el=>elementIDs.has(el.id)); - } - } - - /** - * Gets all the elements from elements[] that are contained in the frame. - * @param frameElement - the frame element for which to get the elements - * @param elements - typically all the non-deleted elements in the scene - * @param shouldIncludeFrame - if true, the frame element will be included in the returned array - * this is useful when generating an image in which you want the frame to be clipped - * @returns - */ - getElementsInFrame( - frameElement: ExcalidrawElement, - elements: ExcalidrawElement[], - shouldIncludeFrame: boolean = false, - ): ExcalidrawElement[] { - if(!frameElement || !elements || frameElement.type !== "frame") return []; - return elements.filter(el=>(el.frameId === frameElement.id) || (shouldIncludeFrame && el.id === frameElement.id)); - } - - /** - * See OCR plugin for example on how to use scriptSettings - * Set by the ScriptEngine - */ - activeScript: string = null; - - /** - * - * @returns script settings. Saves settings in plugin settings, under the activeScript key - */ - getScriptSettings(): {} { - if (!this.activeScript) { - return null; - } - return this.plugin.settings.scriptEngineSettings[this.activeScript] ?? {}; - }; - - /** - * sets script settings. - * @param settings - * @returns - */ - async setScriptSettings(settings: any): Promise { - if (!this.activeScript) { - return null; - } - this.plugin.settings.scriptEngineSettings[this.activeScript] = settings; - await this.plugin.saveSettings(); - }; - - /** - * Open a file in a new workspaceleaf or reuse an existing adjacent leaf depending on Excalidraw Plugin Settings - * @param file - * @param openState - if not provided {active: true} will be used - * @returns - */ - openFileInNewOrAdjacentLeaf(file: TFile, openState?: OpenViewState): WorkspaceLeaf { - if (!file || !(file instanceof TFile)) { - return null; - } - if (!this.targetView) { - return null; - } - - const {leaf, promise} = openLeaf({ - plugin: this.plugin, - fnGetLeaf: () => getNewOrAdjacentLeaf(this.plugin, this.targetView.leaf), - file, - openState: openState ?? {active: true} - }); - return leaf; - }; - - /** - * measure text size based on current style settings - * @param text - * @returns - */ - measureText(text: string): { width: number; height: number } { - const size = _measureText( - text, - this.style.fontSize, - this.style.fontFamily, - getLineHeight(this.style.fontFamily), - ); - return { width: size.w ?? 0, height: size.h ?? 0 }; - }; - - /** - * Returns the size of the image element at 100% (i.e. the original size), or undefined if the data URL is not available - * @param imageElement an image element from the active scene on targetView - * @param shouldWaitForImage if true, the function will wait for the image to load before returning the size - */ - async getOriginalImageSize(imageElement: ExcalidrawImageElement, shouldWaitForImage: boolean=false): Promise<{width: number; height: number}> { - //@ts-ignore - if (!this.targetView || !this.targetView?._loaded) { - errorMessage("targetView not set", "getOriginalImageSize()"); - return null; - } - if(!imageElement || imageElement.type !== "image") { - errorMessage("Please provide a single image element as input", "getOriginalImageSize()"); - return null; - } - const ef = this.targetView.excalidrawData.getFile(imageElement.fileId); - if(!ef) { - errorMessage("Please provide a single image element as input", "getOriginalImageSize()"); - return null; - } - const isDark = this.getExcalidrawAPI().getAppState().theme === "dark"; - let dataURL = ef.getImage(isDark); - if(!dataURL && !shouldWaitForImage) return; - if(!dataURL) { - let watchdog = 0; - while(!dataURL && watchdog < 50) { - await sleep(100); - dataURL = ef.getImage(isDark); - watchdog++; - } - if(!dataURL) return; - } - return await getImageSize(dataURL); - } - - /** - * Resets the image to its original aspect ratio. - * If the image is resized then the function returns true. - * If the image element is not in EA (only in the view), then if image is resized, the element is copied to EA for Editing using copyViewElementsToEAforEditing([imgEl]). - * Note you need to run await ea.addElementsToView(false); to add the modified image to the view. - * @param imageElement - the EA image element to be resized - * returns true if image was changed, false if image was not changed - */ - async resetImageAspectRatio(imgEl: ExcalidrawImageElement): Promise { - //@ts-ignore - if (!this.targetView || !this.targetView?._loaded) { - errorMessage("targetView not set", "resetImageAspectRatio()"); - return null; - } - - let originalArea, originalAspectRatio; - if(imgEl.crop) { - originalArea = imgEl.width * imgEl.height; - originalAspectRatio = imgEl.crop.width / imgEl.crop.height; - } else { - const size = await this.getOriginalImageSize(imgEl, true); - if (!size) { return false; } - originalArea = imgEl.width * imgEl.height; - originalAspectRatio = size.width / size.height; - } - let newWidth = Math.sqrt(originalArea * originalAspectRatio); - let newHeight = Math.sqrt(originalArea / originalAspectRatio); - const centerX = imgEl.x + imgEl.width / 2; - const centerY = imgEl.y + imgEl.height / 2; - - if (newWidth !== imgEl.width || newHeight !== imgEl.height) { - if(!this.getElement(imgEl.id)) { - this.copyViewElementsToEAforEditing([imgEl]); - } - const eaEl = this.getElement(imgEl.id); - eaEl.width = newWidth; - eaEl.height = newHeight; - eaEl.x = centerX - newWidth / 2; - eaEl.y = centerY - newHeight / 2; - return true; - } - return false; - } - - /** - * verifyMinimumPluginVersion returns true if plugin version is >= than required - * recommended use: - * if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.5.20")) {new Notice("message");return;} - * @param requiredVersion - * @returns - */ - verifyMinimumPluginVersion(requiredVersion: string): boolean { - return verifyMinimumPluginVersion(requiredVersion); - }; - - /** - * Check if view is instance of ExcalidrawView - * @param view - * @returns - */ - isExcalidrawView(view: any): boolean { - return view instanceof ExcalidrawView; - } - - /** - * sets selection in view - * @param elements - * @returns - */ - selectElementsInView(elements: ExcalidrawElement[] | string[]): void { - //@ts-ignore - if (!this.targetView || !this.targetView?._loaded) { - errorMessage("targetView not set", "selectElementsInView()"); - return; - } - if (!elements || elements.length === 0) { - return; - } - const API: ExcalidrawImperativeAPI = this.getExcalidrawAPI(); - if(typeof elements[0] === "string") { - const els = this.getViewElements().filter(el=>(elements as string[]).includes(el.id)); - API.selectElements(els); - } else { - API.selectElements(elements as ExcalidrawElement[]); - } - }; - - /** - * @returns an 8 character long random id - */ - generateElementId(): string { - return nanoid(); - }; - - /** - * @param element - * @returns a clone of the element with a new id - */ - cloneElement(element: ExcalidrawElement): ExcalidrawElement { - const newEl = JSON.parse(JSON.stringify(element)); - newEl.id = nanoid(); - return newEl; - }; - - /** - * Moves the element to a specific position in the z-index - */ - moveViewElementToZIndex(elementId: number, newZIndex: number): void { - //@ts-ignore - if (!this.targetView || !this.targetView?._loaded) { - errorMessage("targetView not set", "moveViewElementToZIndex()"); - return; - } - const API = this.getExcalidrawAPI(); - const elements = this.getViewElements(); - const elementToMove = elements.filter((el: any) => el.id === elementId); - if (elementToMove.length === 0) { - errorMessage( - `Element (id: ${elementId}) not found`, - "moveViewElementToZIndex", - ); - return; - } - if (newZIndex >= elements.length) { - API.bringToFront(elementToMove); - return; - } - if (newZIndex < 0) { - API.sendToBack(elementToMove); - return; - } - - const oldZIndex = elements.indexOf(elementToMove[0]); - elements.splice(newZIndex, 0, elements.splice(oldZIndex, 1)[0]); - this.targetView.updateScene({ - elements, - storeAction: "capture", - }); - }; - - /** - * Deprecated. Use getCM / ColorMaster instead - * @param color - * @returns - */ - hexStringToRgb(color: string): number[] { - const res = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color); - return [parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16)]; - }; - - /** - * Deprecated. Use getCM / ColorMaster instead - * @param color - * @returns - */ - rgbToHexString(color: number[]): string { - const cm = CM({r:color[0], g:color[1], b:color[2]}); - return cm.stringHEX({alpha: false}); - }; - - /** - * Deprecated. Use getCM / ColorMaster instead - * @param color - * @returns - */ - hslToRgb(color: number[]): number[] { - const cm = CM({h:color[0], s:color[1], l:color[2]}); - return [cm.red, cm.green, cm.blue]; - }; - - /** - * Deprecated. Use getCM / ColorMaster instead - * @param color - * @returns - */ - rgbToHsl(color: number[]): number[] { - const cm = CM({r:color[0], g:color[1], b:color[2]}); - return [cm.hue, cm.saturation, cm.lightness]; - }; - - /** - * - * @param color - * @returns - */ - colorNameToHex(color: string): string { - if (COLOR_NAMES.has(color.toLowerCase().trim())) { - return COLOR_NAMES.get(color.toLowerCase().trim()); - } - return color.trim(); - }; - - /** - * https://github.com/lbragile/ColorMaster - * @param color - * @returns - */ - getCM(color:TInput): ColorMaster { - if(!color) { - log("Creates a CM object. Visit https://github.com/lbragile/ColorMaster for documentation."); - return; - } - if(typeof color === "string") { - color = this.colorNameToHex(color); - } - - return CM(color); - } - - /** - * Gets the class PolyBool from https://github.com/velipso/polybooljs - * @returns - */ - getPolyBool() { - const defaultEpsilon = 0.0000000001; - PolyBool.epsilon(defaultEpsilon); - return PolyBool; - } - - importSVG(svgString:string):boolean { - const res:ConversionResult = svgToExcalidraw(svgString); - if(res.hasErrors) { - new Notice (`There were errors while parsing the given SVG:\n${res.errors}`); - return false; - } - this.copyViewElementsToEAforEditing(res.content); - return true; - } - - destroy(): void { - this.targetView = null; - this.plugin = null; - this.elementsDict = {}; - this.imagesDict = {}; - this.mostRecentMarkdownSVG = null; - this.activeScript = null; - //@ts-ignore - this.style = {}; - //@ts-ignore - this.canvas = {}; - this.colorPalette = {}; - } -}; - -export function initExcalidrawAutomate( - plugin: ExcalidrawPlugin, -): ExcalidrawAutomate { - const ea = new ExcalidrawAutomate(plugin); - //@ts-ignore - window.ExcalidrawAutomate = ea; - return ea; -} - -function normalizeLinePoints( - points: [x: number, y: number][], - //box: { x: number; y: number; w: number; h: number }, -): number[][] { - const p = []; - const [x, y] = points[0]; - for (let i = 0; i < points.length; i++) { - p.push([points[i][0] - x, points[i][1] - y]); - } - return p; -} - -function getLineBox( - points: [x: number, y: number][] -):{x:number, y:number, w: number, h:number} { - const [x1, y1, x2, y2] = estimateLineBound(points); - return { - x: x1, - y: y1, - w: x2 - x1, //Math.abs(points[points.length-1][0]-points[0][0]), - h: y2 - y1, //Math.abs(points[points.length-1][1]-points[0][1]) - }; -} - -function getFontFamily(id: number):string { - return getFontFamilyString({fontFamily:id}) -} - -export function _measureText( - newText: string, - fontSize: number, - fontFamily: number, - lineHeight: number, -): {w: number, h:number} { - //following odd error with mindmap on iPad while synchornizing with desktop. - if (!fontSize) { - fontSize = 20; - } - if (!fontFamily) { - fontFamily = 1; - lineHeight = getLineHeight(fontFamily); - } - const metrics = measureText( - newText, - `${fontSize.toString()}px ${getFontFamily(fontFamily)}` as any, - lineHeight - ); - return { w: metrics.width, h: metrics.height }; -} - -async function getTemplate( - plugin: ExcalidrawPlugin, - fileWithPath: string, - loadFiles: boolean = false, - loader: EmbeddedFilesLoader, - depth: number, - convertMarkdownLinksToObsidianURLs: boolean = false, -): Promise<{ - elements: any; - appState: any; - frontmatter: string; - files: any; - hasSVGwithBitmap: boolean; - plaintext: string; //markdown data above Excalidraw data and below YAML frontmatter -}> { - const app = plugin.app; - const vault = app.vault; - const filenameParts = getEmbeddedFilenameParts(fileWithPath); - const templatePath = normalizePath(filenameParts.filepath); - const file = app.metadataCache.getFirstLinkpathDest(templatePath, ""); - let hasSVGwithBitmap = false; - if (file && file instanceof TFile) { - const data = (await vault.read(file)) - .replaceAll("\r\n", "\n") - .replaceAll("\r", "\n"); - const excalidrawData: ExcalidrawData = new ExcalidrawData(plugin); - - if (file.extension === "excalidraw") { - await excalidrawData.loadLegacyData(data, file); - return { - elements: convertMarkdownLinksToObsidianURLs - ? updateElementLinksToObsidianLinks({ - elements: excalidrawData.scene.elements, - hostFile: file, - }) : excalidrawData.scene.elements, - appState: excalidrawData.scene.appState, - frontmatter: "", - files: excalidrawData.scene.files, - hasSVGwithBitmap, - plaintext: "", - }; - } - - const textMode = getTextMode(data); - await excalidrawData.loadData( - data, - file, - textMode, - ); - - let trimLocation = data.search(/^##? Text Elements$/m); - if (trimLocation == -1) { - trimLocation = data.search(/##? Drawing\n/); - } - - let scene = excalidrawData.scene; - - let groupElements:ExcalidrawElement[] = scene.elements; - if(filenameParts.hasGroupref) { - const el = filenameParts.hasSectionref - ? getTextElementsMatchingQuery(scene.elements,["# "+filenameParts.sectionref],true) - : scene.elements.filter((el: ExcalidrawElement)=>el.id===filenameParts.blockref); - if(el.length > 0) { - groupElements = plugin.ea.getElementsInTheSameGroupWithElement(el[0],scene.elements,true) - } - } - if(filenameParts.hasFrameref || filenameParts.hasClippedFrameref) { - const el = getFrameBasedOnFrameNameOrId(filenameParts.blockref,scene.elements); - - if(el) { - groupElements = plugin.ea.getElementsInFrame(el,scene.elements, filenameParts.hasClippedFrameref); - } - } - - if(filenameParts.hasTaskbone) { - groupElements = groupElements.filter( el => - el.type==="freedraw" || - ( el.type==="image" && - !plugin.isExcalidrawFile(excalidrawData.getFile(el.fileId)?.file) - )); - } - - let fileIDWhiteList:Set; - - if(groupElements.length < scene.elements.length) { - fileIDWhiteList = new Set(); - groupElements.filter(el=>el.type==="image").forEach((el:ExcalidrawImageElement)=>fileIDWhiteList.add(el.fileId)); - } - - if (loadFiles) { - //debug({where:"getTemplate",template:file.name,loader:loader.uid}); - await loader.loadSceneFiles(excalidrawData, (fileArray: FileData[]) => { - //, isDark: boolean) => { - if (!fileArray || fileArray.length === 0) { - return; - } - for (const f of fileArray) { - if (f.hasSVGwithBitmap) { - hasSVGwithBitmap = true; - } - excalidrawData.scene.files[f.id] = { - mimeType: f.mimeType, - id: f.id, - dataURL: f.dataURL, - created: f.created, - }; - } - scene = scaleLoadedImage(excalidrawData.scene, fileArray).scene; - }, depth, false, fileIDWhiteList); - } - - excalidrawData.destroy(); - const filehead = getExcalidrawMarkdownHeaderSection(data); // data.substring(0, trimLocation); - let files:any = {}; - const sceneFilesSize = Object.values(scene.files).length; - if (sceneFilesSize > 0) { - if(fileIDWhiteList && (sceneFilesSize > fileIDWhiteList.size)) { - Object.values(scene.files).filter((f: any) => fileIDWhiteList.has(f.id)).forEach((f: any) => { - files[f.id] = f; - }); - } else { - files = scene.files; - } - } - - const frontmatter = filehead.match(/^---\n.*\n---\n/ms)?.[0] ?? filehead; - return { - elements: convertMarkdownLinksToObsidianURLs - ? updateElementLinksToObsidianLinks({ - elements: groupElements, - hostFile: file, - }) : groupElements, - appState: scene.appState, - frontmatter, - plaintext: frontmatter !== filehead - ? (filehead.split(/^---\n.*\n---\n/ms)?.[1] ?? "") - : "", - files, - hasSVGwithBitmap, - }; - } - return { - elements: [], - appState: {}, - frontmatter: null, - files: [], - hasSVGwithBitmap, - plaintext: "", - }; -} - -export const generatePlaceholderDataURL = (width: number, height: number): DataURL => { - const svgString = `Placeholder`; - return `data:image/svg+xml;base64,${btoa(svgString)}` as DataURL; -}; - -export async function createPNG( - templatePath: string = undefined, - scale: number = 1, - exportSettings: ExportSettings, - loader: EmbeddedFilesLoader, - forceTheme: string = undefined, - canvasTheme: string = undefined, - canvasBackgroundColor: string = undefined, - automateElements: ExcalidrawElement[] = [], - plugin: ExcalidrawPlugin, - depth: number, - padding?: number, - imagesDict?: any, -): Promise { - if (!loader) { - loader = new EmbeddedFilesLoader(plugin); - } - padding = padding ?? plugin.settings.exportPaddingSVG; - const template = templatePath - ? await getTemplate(plugin, templatePath, true, loader, depth) - : null; - let elements = template?.elements ?? []; - elements = elements.concat(automateElements); - const files = imagesDict ?? {}; - if(template?.files) { - Object.values(template.files).forEach((f:any)=>{ - if(!f.dataURL.startsWith("http")) { - files[f.id]=f; - }; - }); - } - - return await getPNG( - { - type: "excalidraw", - version: 2, - source: GITHUB_RELEASES+PLUGIN_VERSION, - elements, - appState: { - theme: forceTheme ?? template?.appState?.theme ?? canvasTheme, - viewBackgroundColor: - template?.appState?.viewBackgroundColor ?? canvasBackgroundColor, - ...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {}, - }, - files, - }, - { - withBackground: - exportSettings?.withBackground ?? plugin.settings.exportWithBackground, - withTheme: exportSettings?.withTheme ?? plugin.settings.exportWithTheme, - isMask: exportSettings?.isMask ?? false, - }, - padding, - scale, - ); -} - -export const updateElementLinksToObsidianLinks = ({elements, hostFile}:{ - elements: ExcalidrawElement[]; - hostFile: TFile; -}): ExcalidrawElement[] => { - return elements.map((el)=>{ - if(el.link && el.link.startsWith("[")) { - const partsArray = REGEX_LINK.getResList(el.link)[0]; - if(!partsArray?.value) return el; - let linkText = REGEX_LINK.getLink(partsArray); - if (linkText.search("#") > -1) { - const linkParts = getLinkParts(linkText, hostFile); - linkText = linkParts.path; - } - if (linkText.match(REG_LINKINDEX_INVALIDCHARS)) { - return el; - } - const file = EXCALIDRAW_PLUGIN.app.metadataCache.getFirstLinkpathDest( - linkText, - hostFile.path, - ); - if(!file) { - return el; - } - let link = EXCALIDRAW_PLUGIN.app.getObsidianUrl(file); - if(window.ExcalidrawAutomate?.onUpdateElementLinkForExportHook) { - link = window.ExcalidrawAutomate.onUpdateElementLinkForExportHook({ - originalLink: el.link, - obsidianLink: link, - linkedFile: file, - hostFile: hostFile - }); - } - const newElement: Mutable = cloneElement(el); - newElement.link = link; - return newElement; - } - return el; - }) -} - -function addFilterToForeignObjects(svg:SVGSVGElement):void { - const foreignObjects = svg.querySelectorAll("foreignObject"); - foreignObjects.forEach((foreignObject) => { - foreignObject.setAttribute("filter", THEME_FILTER); - }); -} - -export async function createSVG( - templatePath: string = undefined, - embedFont: boolean = false, - exportSettings: ExportSettings, - loader: EmbeddedFilesLoader, - forceTheme: string = undefined, - canvasTheme: string = undefined, - canvasBackgroundColor: string = undefined, - automateElements: ExcalidrawElement[] = [], - plugin: ExcalidrawPlugin, - depth: number, - padding?: number, - imagesDict?: any, - convertMarkdownLinksToObsidianURLs: boolean = false, -): Promise { - if (!loader) { - loader = new EmbeddedFilesLoader(plugin); - } - if(typeof exportSettings.skipInliningFonts === "undefined") { - exportSettings.skipInliningFonts = !embedFont; - } - const template = templatePath - ? await getTemplate(plugin, templatePath, true, loader, depth, convertMarkdownLinksToObsidianURLs) - : null; - let elements = template?.elements ?? []; - elements = elements.concat(automateElements); - padding = padding ?? plugin.settings.exportPaddingSVG; - const files = imagesDict ?? {}; - if(template?.files) { - Object.values(template.files).forEach((f:any)=>{ - files[f.id]=f; - }); - } - - const theme = forceTheme ?? template?.appState?.theme ?? canvasTheme; - const withTheme = exportSettings?.withTheme ?? plugin.settings.exportWithTheme; - - const filenameParts = getEmbeddedFilenameParts(templatePath); - const svg = await getSVG( - { - //createAndOpenDrawing - type: "excalidraw", - version: 2, - source: GITHUB_RELEASES+PLUGIN_VERSION, - elements, - appState: { - theme, - viewBackgroundColor: - template?.appState?.viewBackgroundColor ?? canvasBackgroundColor, - ...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {}, - }, - files, - }, - { - withBackground: - exportSettings?.withBackground ?? plugin.settings.exportWithBackground, - withTheme, - isMask: exportSettings?.isMask ?? false, - ...filenameParts?.hasClippedFrameref - ? {frameRendering: {enabled: true, name: false, outline: false, clip: true}} - : {}, - }, - padding, - null, - ); - - if (withTheme && theme === "dark") addFilterToForeignObjects(svg); - - if( - !(filenameParts.hasGroupref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref) && - (filenameParts.hasBlockref || filenameParts.hasSectionref) - ) { - let el = filenameParts.hasSectionref - ? getTextElementsMatchingQuery(elements,["# "+filenameParts.sectionref],true) - : elements.filter((el: ExcalidrawElement)=>el.id===filenameParts.blockref); - if(el.length>0) { - const containerId = el[0].containerId; - if(containerId) { - el = el.concat(elements.filter((el: ExcalidrawElement)=>el.id === containerId)); - } - const elBB = plugin.ea.getBoundingBox(el); - const drawingBB = plugin.ea.getBoundingBox(elements); - svg.viewBox.baseVal.x = elBB.topX - drawingBB.topX; - svg.viewBox.baseVal.y = elBB.topY - drawingBB.topY; - svg.viewBox.baseVal.width = elBB.width + 2*padding; - svg.viewBox.baseVal.height = elBB.height + 2*padding; - } - } - if (template?.hasSVGwithBitmap) { - svg.setAttribute("hasbitmap", "true"); - } - return svg; -} - -function estimateLineBound(points: any): [number, number, number, number] { - let minX = Infinity; - let minY = Infinity; - let maxX = -Infinity; - let maxY = -Infinity; - - for (const [x, y] of points) { - minX = Math.min(minX, x); - minY = Math.min(minY, y); - maxX = Math.max(maxX, x); - maxY = Math.max(maxY, y); - } - - return [minX, minY, maxX, maxY]; -} - -export function estimateBounds( - elements: ExcalidrawElement[], -): [number, number, number, number] { - const bb = getCommonBoundingBox(elements); - return [bb.minX, bb.minY, bb.maxX, bb.maxY]; -} - -export function repositionElementsToCursor( - elements: ExcalidrawElement[], - newPosition: { x: number; y: number }, - center: boolean = false, -): ExcalidrawElement[] { - const [x1, y1, x2, y2] = estimateBounds(elements); - let [offsetX, offsetY] = [0, 0]; - if (center) { - [offsetX, offsetY] = [ - newPosition.x - (x1 + x2) / 2, - newPosition.y - (y1 + y2) / 2, - ]; - } else { - [offsetX, offsetY] = [newPosition.x - x1, newPosition.y - y1]; - } - - elements.forEach((element: any) => { - //using any so I can write read-only propery x & y - element.x = element.x + offsetX; - element.y = element.y + offsetY; - }); - - return restore({elements}, null, null).elements; -} - -function errorMessage(message: string, source: string):void { - switch (message) { - case "targetView not set": - errorlog({ - where: "ExcalidrawAutomate", - source, - message: - "targetView not set, or no longer active. Use setView before calling this function", - }); - break; - case "mobile not supported": - errorlog({ - where: "ExcalidrawAutomate", - source, - message: "this function is not available on Obsidian Mobile", - }); - break; - default: - errorlog({ - where: "ExcalidrawAutomate", - source, - message: message??"unknown error", - }); - } -} - -export const insertLaTeXToView = (view: ExcalidrawView) => { - const app = view.plugin.app; - const ea = view.plugin.ea; - GenericInputPrompt.Prompt( - view, - view.plugin, - app, - t("ENTER_LATEX"), - "\\color{red}\\oint_S {E_n dA = \\frac{1}{{\\varepsilon _0 }}} Q_{inside}", - view.plugin.settings.latexBoilerplate, - undefined, - 3 - ).then(async (formula: string) => { - if (!formula) { - return; - } - ea.reset(); - await ea.addLaTex(0, 0, formula); - ea.setView(view); - ea.addElementsToView(true, false, true); - }); -}; - -export const search = async (view: ExcalidrawView) => { - const ea = view.plugin.ea; - ea.reset(); - ea.setView(view); - const elements = ea.getViewElements().filter((el) => el.type === "text" || el.type === "frame" || el.link || el.type === "image"); - if (elements.length === 0) { - return; - } - let text = await ScriptEngine.inputPrompt( - view, - view.plugin, - view.plugin.app, - "Search for", - "use quotation marks for exact match", - "", - ); - if (!text) { - return; - } - const res = text.matchAll(/"(.*?)"/g); - let query: string[] = []; - let parts; - while (!(parts = res.next()).done) { - query.push(parts.value[1]); - } - text = text.replaceAll(/"(.*?)"/g, ""); - query = query.concat(text.split(" ").filter((s) => s.length !== 0)); - - ea.targetView.selectElementsMatchingQuery(elements, query); -}; - -/** - * - * @param elements - * @param query - * @param exactMatch - when searching for section header exactMatch should be set to true - * @returns the elements matching the query - */ -export const getTextElementsMatchingQuery = ( - elements: ExcalidrawElement[], - query: string[], - exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530 -): ExcalidrawElement[] => { - if (!elements || elements.length === 0 || !query || query.length === 0) { - return []; - } - - return elements.filter((el: any) => - el.type === "text" && - query.some((q) => { - if (exactMatch) { - const text = el.rawText.toLowerCase().split("\n")[0].trim(); - const m = text.match(/^#*(# .*)/); - if (!m || m.length !== 2) { - return false; - } - return m[1] === q.toLowerCase(); - } - const text = el.rawText.toLowerCase().replaceAll("\n", " ").trim(); - return text.match(q.toLowerCase()); //to distinguish between "# frame" and "# frame 1" https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530 - })); -} - -/** - * - * @param elements - * @param query - * @param exactMatch - when searching for section header exactMatch should be set to true - * @returns the elements matching the query - */ -export const getFrameElementsMatchingQuery = ( - elements: ExcalidrawElement[], - query: string[], - exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530 -): ExcalidrawElement[] => { - if (!elements || elements.length === 0 || !query || query.length === 0) { - return []; - } - - return elements.filter((el: any) => - el.type === "frame" && - query.some((q) => { - if (exactMatch) { - const text = el.name?.toLowerCase().split("\n")[0].trim() ?? ""; - const m = text.match(/^#*(# .*)/); - if (!m || m.length !== 2) { - return false; - } - return m[1] === q.toLowerCase(); - } - const text = el.name - ? el.name.toLowerCase().replaceAll("\n", " ").trim() - : ""; - - return text.match(q.toLowerCase()); //to distinguish between "# frame" and "# frame 1" https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530 - })); -} - -/** - * - * @param elements - * @param query - * @param exactMatch - when searching for section header exactMatch should be set to true - * @returns the elements matching the query - */ -export const getElementsWithLinkMatchingQuery = ( - elements: ExcalidrawElement[], - query: string[], - exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530 -): ExcalidrawElement[] => { - if (!elements || elements.length === 0 || !query || query.length === 0) { - return []; - } - - return elements.filter((el: any) => - el.link && - query.some((q) => { - const text = el.link.toLowerCase().trim(); - return exactMatch - ? (text === q.toLowerCase()) - : text.match(q.toLowerCase()); - })); -} - -/** - * - * @param elements - * @param query - * @param exactMatch - when searching for section header exactMatch should be set to true - * @returns the elements matching the query - */ -export const getImagesMatchingQuery = ( - elements: ExcalidrawElement[], - query: string[], - excalidrawData: ExcalidrawData, - exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530 -): ExcalidrawElement[] => { - if (!elements || elements.length === 0 || !query || query.length === 0) { - return []; - } - - return elements.filter((el: ExcalidrawElement) => - el.type === "image" && - query.some((q) => { - const filename = excalidrawData.getFile(el.fileId)?.file?.basename.toLowerCase().trim(); - const equation = excalidrawData.getEquation(el.fileId)?.latex?.toLocaleLowerCase().trim(); - const text = filename ?? equation; - if(!text) return false; - return exactMatch - ? (text === q.toLowerCase()) - : text.match(q.toLowerCase()); - })); - } - -export const cloneElement = (el: ExcalidrawElement):any => { - const newEl = JSON.parse(JSON.stringify(el)); - newEl.version = el.version + 1; - newEl.updated = Date.now(); - newEl.versionNonce = Math.floor(Math.random() * 1000000000); - return newEl; -} - -export const verifyMinimumPluginVersion = (requiredVersion: string): boolean => { - return PLUGIN_VERSION === requiredVersion || isVersionNewerThanOther(PLUGIN_VERSION,requiredVersion); -} - -export const getBoundTextElementId = (container: ExcalidrawElement | null) => { - return container?.boundElements?.length - ? container?.boundElements?.find((ele) => ele.type === "text")?.id || null - : null; +import ExcalidrawPlugin from "src/Core/main"; +import { + FillStyle, + StrokeStyle, + ExcalidrawElement, + ExcalidrawBindableElement, + FileId, + NonDeletedExcalidrawElement, + ExcalidrawImageElement, + ExcalidrawTextElement, + StrokeRoundness, + RoundnessType, + ExcalidrawFrameElement, + ExcalidrawTextContainer, +} from "@zsviczian/excalidraw/types/excalidraw/element/types"; +import { MimeType } from "./EmbeddedFileLoader"; +import { Editor, normalizePath, Notice, OpenViewState, RequestUrlResponse, TFile, TFolder, WorkspaceLeaf } from "obsidian"; +import * as obsidian_module from "obsidian"; +import ExcalidrawView, { ExportSettings, TextMode, getTextMode } from "src/View/ExcalidrawView"; +import { ExcalidrawData, getExcalidrawMarkdownHeaderSection, getMarkdownDrawingSection, REGEX_LINK } from "./ExcalidrawData"; +import { + FRONTMATTER, + nanoid, + MAX_IMAGE_SIZE, + COLOR_NAMES, + fileid, + GITHUB_RELEASES, + determineFocusDistance, + getCommonBoundingBox, + getLineHeight, + getMaximumGroups, + intersectElementWithLine, + measureText, + DEVICE, + restore, + REG_LINKINDEX_INVALIDCHARS, + THEME_FILTER, + mermaidToExcalidraw, + refreshTextDimensions, + getFontFamilyString, + EXCALIDRAW_PLUGIN, +} from "src/Constants/Constants"; +import { blobToBase64, checkAndCreateFolder, getDrawingFilename, getExcalidrawEmbeddedFilesFiletree, getListOfTemplateFiles, getNewUniqueFilepath, hasExcalidrawEmbeddedImagesTreeChanged, } from "src/Utils/FileUtils"; +import { + //debug, + errorlog, + getEmbeddedFilenameParts, + getImageSize, + getLinkParts, + getPNG, + getSVG, + isMaskFile, + isVersionNewerThanOther, + scaleLoadedImage, + wrapTextAtCharLength, + arrayToMap, +} from "src/Utils/Utils"; +import { getAttachmentsFolderAndFilePath, getExcalidrawViews, getLeaf, getNewOrAdjacentLeaf, isObsidianThemeDark, mergeMarkdownFiles, openLeaf } from "src/Utils/ObsidianUtils"; +import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types"; +import { EmbeddedFile, EmbeddedFilesLoader, FileData } from "./EmbeddedFileLoader"; +import { tex2dataURL } from "./LaTeX"; +import { GenericInputPrompt, NewFileActions } from "src/Shared/Dialogs/Prompt"; +import { t } from "src/Lang/Helpers"; +import { ScriptEngine } from "./Scripts"; +import { ConnectionPoint, DeviceType, Point } from "src/Types/Types"; +import CM, { ColorMaster, extendPlugins } from "@zsviczian/colormaster"; +import HarmonyPlugin from "@zsviczian/colormaster/plugins/harmony"; +import MixPlugin from "@zsviczian/colormaster/plugins/mix" +import A11yPlugin from "@zsviczian/colormaster/plugins/accessibility" +import NamePlugin from "@zsviczian/colormaster/plugins/name" +import LCHPlugin from "@zsviczian/colormaster/plugins/lch"; +import LUVPlugin from "@zsviczian/colormaster/plugins/luv"; +import LABPlugin from "@zsviczian/colormaster/plugins/lab"; +import UVWPlugin from "@zsviczian/colormaster/plugins/uvw"; +import XYZPlugin from "@zsviczian/colormaster/plugins/xyz"; +import HWBPlugin from "@zsviczian/colormaster/plugins/hwb"; +import HSVPlugin from "@zsviczian/colormaster/plugins/hsv"; +import RYBPlugin from "@zsviczian/colormaster/plugins/ryb"; +import CMYKPlugin from "@zsviczian/colormaster/plugins/cmyk"; +import { TInput } from "@zsviczian/colormaster/types"; +import {ConversionResult, svgToExcalidraw} from "src/Shared/svgToExcalidraw/parser" +import { ROUNDNESS } from "src/Constants/Constants"; +import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard"; +import { emulateKeysForLinkClick, PaneTarget } from "src/Utils/ModifierkeyHelper"; +import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types"; +import PolyBool from "polybooljs"; +import { EmbeddableMDCustomProps } from "./Dialogs/EmbeddableSettings"; +import { + AIRequest, + postOpenAI as _postOpenAI, + extractCodeBlocks as _extractCodeBlocks, +} from "../Utils/AIUtils"; +import { EXCALIDRAW_AUTOMATE_INFO, EXCALIDRAW_SCRIPTENGINE_INFO } from "./Dialogs/SuggesterInfo"; +import { addBackOfTheNoteCard, getFrameBasedOnFrameNameOrId } from "../Utils/ExcalidrawViewUtils"; +import { log } from "../Utils/DebugHelper"; +import { ExcalidrawLib } from "../Types/ExcalidrawLib"; +import { GlobalPoint } from "@zsviczian/excalidraw/types/math/types"; + +extendPlugins([ + HarmonyPlugin, + MixPlugin, + A11yPlugin, + NamePlugin, + LCHPlugin, + LUVPlugin, + LABPlugin, + UVWPlugin, + XYZPlugin, + HWBPlugin, + HSVPlugin, + RYBPlugin, + CMYKPlugin +]); + +declare const PLUGIN_VERSION:string; +declare var LZString: any; +declare const excalidrawLib: typeof ExcalidrawLib; + +const GAP = 4; + +export class ExcalidrawAutomate { + /** + * Utility function that returns the Obsidian Module object. + */ + get obsidian() { + return obsidian_module; + }; + + get LASERPOINTER() { + return this.plugin.settings.laserSettings; + } + + get DEVICE():DeviceType { + return DEVICE; + } + + public printStartupBreakdown() { + this.plugin.printStarupBreakdown(); + } + + public help(target: Function | string) { + if (!target) { + log("Usage: ea.help(ea.functionName) or ea.help('propertyName') or ea.help('utils.functionName') - notice property name and utils function name is in quotes"); + return; + } + + let funcInfo; + + if (typeof target === 'function') { + funcInfo = EXCALIDRAW_AUTOMATE_INFO.find((info) => info.field === target.name); + } else if (typeof target === 'string') { + let stringTarget:string = target; + stringTarget = stringTarget.startsWith("utils.") ? stringTarget.substring(6) : stringTarget; + stringTarget = stringTarget.startsWith("ea.") ? stringTarget.substring(3) : stringTarget; + funcInfo = EXCALIDRAW_AUTOMATE_INFO.find((info) => info.field === stringTarget); + if(!funcInfo) { + funcInfo = EXCALIDRAW_SCRIPTENGINE_INFO.find((info) => info.field === stringTarget); + } + } + + if(!funcInfo) { + log("Usage: ea.help(ea.functionName) or ea.help('propertyName') or ea.help('utils.functionName') - notice property name and utils function name is in quotes"); + return; + } + + let isMissing = true; + if (funcInfo.code) { + isMissing = false; + log(`Declaration: ${funcInfo.code}`); + } + if (funcInfo.desc) { + isMissing = false; + const formattedDesc = funcInfo.desc + .replaceAll("
", "\n") + .replace(/(.*?)<\/code>/g, '%c\u200b$1%c') // Zero-width space + .replace(/(.*?)<\/b>/g, '%c\u200b$1%c') // Zero-width space + .replace(/
(.*?)<\/a>/g, (_, href, text) => `%c\u200b${text}%c\u200b (link: ${href})`); // Zero-width non-joiner + + const styles = Array.from({ length: (formattedDesc.match(/%c/g) || []).length }, (_, i) => i % 2 === 0 ? 'color: #007bff;' : ''); + log(`Description: ${formattedDesc}`, ...styles); + } + if (isMissing) { + log("Description not available for this function."); + } + } + + /** + * Post's an AI request to the OpenAI API and returns the response. + * @param request + * @returns + */ + public async postOpenAI (request: AIRequest): Promise { + return await _postOpenAI(request); + } + + /** + * Grabs the codeblock contents from the supplied markdown string. + * @param markdown + * @param codeblockType + * @returns an array of dictionaries with the codeblock contents and type + */ + public extractCodeBlocks(markdown: string): { data: string, type: string }[] { + return _extractCodeBlocks(markdown); + } + + /** + * converts a string to a DataURL + * @param htmlString + * @returns dataURL + */ + public async convertStringToDataURL (data:string, type: string = "text/html"):Promise { + // Create a blob from the HTML string + const blob = new Blob([data], { type }); + + // Read the blob as Data URL + const base64String = await new Promise((resolve) => { + const reader = new FileReader(); + reader.onload = () => { + if(typeof reader.result === "string") { + const base64String = reader.result.split(',')[1]; + resolve(base64String); + } else { + resolve(null); + } + }; + reader.readAsDataURL(blob); + }); + if(base64String) { + return `data:${type};base64,${base64String}`; + } + return "about:blank"; + } + + /** + * Checks if the folder exists, if not, creates it. + * @param folderpath + * @returns + */ + public async checkAndCreateFolder(folderpath: string): Promise { + return await checkAndCreateFolder(folderpath); + } + + /** + * Checks if the filepath already exists, if so, returns a new filepath with a number appended to the filename. + * @param filename + * @param folderpath + * @returns + */ + public getNewUniqueFilepath(filename: string, folderpath: string): string { + return getNewUniqueFilepath(this.plugin.app.vault, filename, folderpath); + } + + /** + * + * @returns the Excalidraw Template files or null. + */ + public getListOfTemplateFiles(): TFile[] | null { + return getListOfTemplateFiles(this.plugin); + } + + /** + * Retruns the embedded images in the scene recursively. If excalidrawFile is not provided, + * the function will use ea.targetView.file + * @param excalidrawFile + * @returns TFile[] of all nested images and Excalidraw drawings recursively + */ + public getEmbeddedImagesFiletree(excalidrawFile?: TFile): TFile[] { + if(!excalidrawFile && this.targetView && this.targetView.file) { + excalidrawFile = this.targetView.file; + } + if(!excalidrawFile) { + return []; + } + return getExcalidrawEmbeddedFilesFiletree(excalidrawFile, this.plugin); + } + + public async getAttachmentFilepath(filename: string): Promise { + if (!this.targetView || !this.targetView?.file) { + errorMessage("targetView not set", "getAttachmentFolderAndFilePath()"); + return null; + } + const folderAndPath = await getAttachmentsFolderAndFilePath(this.plugin.app,this.targetView.file.path, filename); + return getNewUniqueFilepath(this.plugin.app.vault, filename, folderAndPath.folder); + } + + public compressToBase64(str:string): string { + return LZString.compressToBase64(str); + } + + public decompressFromBase64(data:string): string { + if (!data) throw new Error("No input string provided for decompression."); + let cleanedData = ''; + const length = data.length; + for (let i = 0; i < length; i++) { + const char = data[i]; + if (char !== '\\n' && char !== '\\r') { + cleanedData += char; + } + } + return LZString.decompressFromBase64(cleanedData); + } + + /** + * Prompts the user with a dialog to select new file action. + * - create markdown file + * - create excalidraw file + * - cancel action + * The new file will be relative to this.targetView.file.path, unless parentFile is provided. + * If shouldOpenNewFile is true, the new file will be opened in a workspace leaf. + * targetPane control which leaf will be used for the new file. + * Returns the TFile for the new file or null if the user cancelled the action. + * @param newFileNameOrPath + * @param shouldOpenNewFile + * @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties"; + * @param parentFile + * @returns + */ + public async newFilePrompt( + newFileNameOrPath: string, + shouldOpenNewFile: boolean, + targetPane?: PaneTarget, + parentFile?: TFile, + ): Promise { + if (!this.targetView || !this.targetView?.file) { + errorMessage("targetView not set", "newFileActions()"); + return null; + } + const modifierKeys = emulateKeysForLinkClick(targetPane); + const newFilePrompt = new NewFileActions({ + plugin: this.plugin, + path: newFileNameOrPath, + keys: modifierKeys, + view: this.targetView, + openNewFile: shouldOpenNewFile, + parentFile: parentFile + }) + newFilePrompt.open(); + return await newFilePrompt.waitForClose; + } + + /** + * Generates a new Obsidian Leaf following Excalidraw plugin settings such as open in Main Workspace or not, open in adjacent pane if available, etc. + * @param origo // the currently active leaf, the origin of the new leaf + * @param targetPane //type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties"; + * @returns + */ + public getLeaf ( + origo: WorkspaceLeaf, + targetPane?: PaneTarget, + ): WorkspaceLeaf { + const modifierKeys = emulateKeysForLinkClick(targetPane??"new-tab"); + return getLeaf(this.plugin,origo,modifierKeys); + } + + /** + * Returns the editor or leaf.view of the currently active embedded obsidian file. + * If view is not provided, ea.targetView is used. + * If the embedded file is a markdown document the function will return + * {file:TFile, editor:Editor} otherwise it will return {view:any}. You can check view type with view.getViewType(); + * @param view + * @returns + */ + public getActiveEmbeddableViewOrEditor (view?:ExcalidrawView): {view:any}|{file:TFile, editor:Editor}|null { + if (!this.targetView && !view) { + return null; + } + view = view ?? this.targetView; + const leafOrNode = view.getActiveEmbeddable(); + if(leafOrNode) { + if(leafOrNode.node && leafOrNode.node.isEditing) { + return {file: leafOrNode.node.file, editor: leafOrNode.node.child.editor}; + } + if(leafOrNode.leaf && leafOrNode.leaf.view) { + return {view: leafOrNode.leaf.view}; + } + } + return null; + } + + public isExcalidrawMaskFile(file?:TFile): boolean { + if(file) { + return this.isExcalidrawFile(file) && isMaskFile(this.plugin, file); + } + if (!this.targetView || !this.targetView?.file) { + errorMessage("targetView not set", "isMaskFile()"); + return null; + } + return isMaskFile(this.plugin, this.targetView.file); + } + + plugin: ExcalidrawPlugin; + elementsDict: {[key:string]:any}; //contains the ExcalidrawElements currently edited in Automate indexed by el.id + imagesDict: {[key: FileId]: any}; //the images files including DataURL, indexed by fileId + mostRecentMarkdownSVG:SVGSVGElement = null; //Markdown renderer will drop a copy of the most recent SVG here for debugging purposes + style: { + strokeColor: string; //https://www.w3schools.com/colors/default.asp + backgroundColor: string; + angle: number; //radian + fillStyle: FillStyle; //type FillStyle = "hachure" | "cross-hatch" | "solid" + strokeWidth: number; + strokeStyle: StrokeStyle; //type StrokeStyle = "solid" | "dashed" | "dotted" + roughness: number; + opacity: number; + strokeSharpness?: StrokeRoundness; //defaults to undefined, use strokeRoundess and roundess instead. Only kept for legacy script compatibility type StrokeRoundness = "round" | "sharp" + roundness: null | { type: RoundnessType; value?: number }; + fontFamily: number; //1: Virgil, 2:Helvetica, 3:Cascadia, 4:Local Font + fontSize: number; + textAlign: string; //"left"|"right"|"center" + verticalAlign: string; //"top"|"bottom"|"middle" :for future use, has no effect currently + startArrowHead: string; //"arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null + endArrowHead: string; + }; + canvas: { + theme: string; //"dark"|"light" + viewBackgroundColor: string; + gridSize: number; + }; + colorPalette: {}; + + constructor(plugin: ExcalidrawPlugin, view?: ExcalidrawView) { + this.plugin = plugin; + this.reset(); + this.targetView = view; + } + + /** + * + * @returns the last recorded pointer position on the Excalidraw canvas + */ + public getViewLastPointerPosition(): {x:number, y:number} { + //@ts-ignore + if (!this.targetView || !this.targetView?._loaded) { + errorMessage("targetView not set", "getExcalidrawAPI()"); + return null; + } + return this.targetView.currentPosition; + } + + /** + * + * @returns + */ + public getAPI(view?:ExcalidrawView):ExcalidrawAutomate { + const ea = new ExcalidrawAutomate(this.plugin, view); + this.plugin.eaInstances.push(ea); + return ea; + } + + /** + * @param val //0:"hachure", 1:"cross-hatch" 2:"solid" + * @returns + */ + setFillStyle(val: number) { + switch (val) { + case 0: + this.style.fillStyle = "hachure"; + return "hachure"; + case 1: + this.style.fillStyle = "cross-hatch"; + return "cross-hatch"; + default: + this.style.fillStyle = "solid"; + return "solid"; + } + }; + + /** + * @param val //0:"solid", 1:"dashed", 2:"dotted" + * @returns + */ + setStrokeStyle(val: number) { + switch (val) { + case 0: + this.style.strokeStyle = "solid"; + return "solid"; + case 1: + this.style.strokeStyle = "dashed"; + return "dashed"; + default: + this.style.strokeStyle = "dotted"; + return "dotted"; + } + }; + + /** + * @param val //0:"round", 1:"sharp" + * @returns + */ + setStrokeSharpness(val: number) { + switch (val) { + case 0: + this.style.roundness = { + type: ROUNDNESS.LEGACY + } + return "round"; + default: + this.style.roundness = null; //sharp + return "sharp"; + } + }; + + /** + * @param val //1: Virgil, 2:Helvetica, 3:Cascadia + * @returns + */ + setFontFamily(val: number) { + switch (val) { + case 1: + this.style.fontFamily = 4; + return getFontFamily(4); + case 2: + this.style.fontFamily = 2; + return getFontFamily(2); + case 3: + this.style.fontFamily = 3; + return getFontFamily(3); + default: + this.style.fontFamily = 1; + return getFontFamily(1); + } + }; + + /** + * @param val //0:"light", 1:"dark" + * @returns + */ + setTheme(val: number) { + switch (val) { + case 0: + this.canvas.theme = "light"; + return "light"; + default: + this.canvas.theme = "dark"; + return "dark"; + } + }; + + /** + * @param objectIds + * @returns + */ + addToGroup(objectIds: string[]): string { + const id = nanoid(); + objectIds.forEach((objectId) => { + this.elementsDict[objectId]?.groupIds?.push(id); + }); + return id; + }; + + /** + * @param templatePath + */ + async toClipboard(templatePath?: string) { + const template = templatePath + ? await getTemplate( + this.plugin, + templatePath, + false, + new EmbeddedFilesLoader(this.plugin), + 0 + ) + : null; + let elements = template ? template.elements : []; + elements = elements.concat(this.getElements()); + navigator.clipboard.writeText( + JSON.stringify({ + type: "excalidraw/clipboard", + elements, + }), + ); + }; + + /** + * @param file: TFile + * @returns ExcalidrawScene + */ + async getSceneFromFile(file: TFile): Promise<{elements: ExcalidrawElement[]; appState: AppState;}> { + if(!file) { + errorMessage("file not found", "getScene()"); + return null; + } + if(!this.isExcalidrawFile(file)) { + errorMessage("file is not an Excalidraw file", "getScene()"); + return null; + } + const template = await getTemplate(this.plugin,file.path,false,new EmbeddedFilesLoader(this.plugin),0); + return { + elements: template.elements, + appState: template.appState + } + } + + /** + * get all elements from ExcalidrawAutomate elementsDict + * @returns elements from elementsDict + */ + getElements(): Mutable[] { + const elements = []; + const elementIds = Object.keys(this.elementsDict); + for (let i = 0; i < elementIds.length; i++) { + elements.push(this.elementsDict[elementIds[i]]); + } + return elements; + }; + + /** + * get single element from ExcalidrawAutomate elementsDict + * @param id + * @returns + */ + getElement(id: string): Mutable { + return this.elementsDict[id]; + }; + + /** + * create a drawing and save it to filename + * @param params + * filename: if null, default filename as defined in Excalidraw settings + * foldername: if null, default folder as defined in Excalidraw settings + * @returns + */ + async create(params?: { + filename?: string; + foldername?: string; + templatePath?: string; + onNewPane?: boolean; + silent?: boolean; + frontmatterKeys?: { + "excalidraw-plugin"?: "raw" | "parsed"; + "excalidraw-link-prefix"?: string; + "excalidraw-link-brackets"?: boolean; + "excalidraw-url-prefix"?: string; + "excalidraw-export-transparent"?: boolean; + "excalidraw-export-dark"?: boolean; + "excalidraw-export-padding"?: number; + "excalidraw-export-pngscale"?: number; + "excalidraw-export-embed-scene"?: boolean; + "excalidraw-default-mode"?: "view" | "zen"; + "excalidraw-onload-script"?: string; + "excalidraw-linkbutton-opacity"?: number; + "excalidraw-autoexport"?: boolean; + "excalidraw-mask"?: boolean; + "excalidraw-open-md"?: boolean; + "cssclasses"?: string; + }; + plaintext?: string; //text to insert above the `# Text Elements` section + }): Promise { + + const template = params?.templatePath + ? await getTemplate( + this.plugin, + params.templatePath, + true, + new EmbeddedFilesLoader(this.plugin), + 0 + ) + : null; + if (template?.plaintext) { + if(params.plaintext) { + params.plaintext = params.plaintext + "\n\n" + template.plaintext; + } else { + params.plaintext = template.plaintext; + } + } + let elements = template ? template.elements : []; + elements = elements.concat(this.getElements()); + let frontmatter: string; + if (params?.frontmatterKeys) { + const keys = Object.keys(params.frontmatterKeys); + if (!keys.includes("excalidraw-plugin")) { + params.frontmatterKeys["excalidraw-plugin"] = "parsed"; + } + frontmatter = "---\n\n"; + for (const key of Object.keys(params.frontmatterKeys)) { + frontmatter += `${key}: ${ + //@ts-ignore + params.frontmatterKeys[key] === "" + ? '""' + : //@ts-ignore + params.frontmatterKeys[key] + }\n`; + } + frontmatter += "\n---\n"; + } else { + frontmatter = template?.frontmatter + ? template.frontmatter + : FRONTMATTER; + } + + frontmatter += params.plaintext + ? (params.plaintext.endsWith("\n\n") + ? params.plaintext + : (params.plaintext.endsWith("\n") + ? params.plaintext + "\n" + : params.plaintext + "\n\n")) + : ""; + if(template?.frontmatter && params?.frontmatterKeys) { + //the frontmatter tags supplyed to create take priority + frontmatter = mergeMarkdownFiles(template.frontmatter,frontmatter); + } + + const scene = { + type: "excalidraw", + version: 2, + source: GITHUB_RELEASES+PLUGIN_VERSION, + elements, + appState: { + theme: template?.appState?.theme ?? this.canvas.theme, + viewBackgroundColor: + template?.appState?.viewBackgroundColor ?? + this.canvas.viewBackgroundColor, + currentItemStrokeColor: + template?.appState?.currentItemStrokeColor ?? + this.style.strokeColor, + currentItemBackgroundColor: + template?.appState?.currentItemBackgroundColor ?? + this.style.backgroundColor, + currentItemFillStyle: + template?.appState?.currentItemFillStyle ?? this.style.fillStyle, + currentItemStrokeWidth: + template?.appState?.currentItemStrokeWidth ?? + this.style.strokeWidth, + currentItemStrokeStyle: + template?.appState?.currentItemStrokeStyle ?? + this.style.strokeStyle, + currentItemRoughness: + template?.appState?.currentItemRoughness ?? this.style.roughness, + currentItemOpacity: + template?.appState?.currentItemOpacity ?? this.style.opacity, + currentItemFontFamily: + template?.appState?.currentItemFontFamily ?? this.style.fontFamily, + currentItemFontSize: + template?.appState?.currentItemFontSize ?? this.style.fontSize, + currentItemTextAlign: + template?.appState?.currentItemTextAlign ?? this.style.textAlign, + currentItemStartArrowhead: + template?.appState?.currentItemStartArrowhead ?? + this.style.startArrowHead, + currentItemEndArrowhead: + template?.appState?.currentItemEndArrowhead ?? + this.style.endArrowHead, + currentItemRoundness: //type StrokeRoundness = "round" | "sharp" + template?.appState?.currentItemLinearStrokeSharpness ?? //legacy compatibility + template?.appState?.currentItemStrokeSharpness ?? //legacy compatibility + template?.appState?.currentItemRoundness ?? + this.style.roundness ? "round":"sharp", + gridSize: template?.appState?.gridSize ?? this.canvas.gridSize, + colorPalette: template?.appState?.colorPalette ?? this.colorPalette, + ...template?.appState?.frameRendering + ? {frameRendering: template.appState.frameRendering} + : {}, + ...template?.appState?.objectsSnapModeEnabled + ? {objectsSnapModeEnabled: template.appState.objectsSnapModeEnabled} + : {}, + }, + files: template?.files ?? {}, + }; + + const generateMD = ():string => { + const textElements = this.getElements().filter(el => el.type === "text") as ExcalidrawTextElement[]; + let outString = `# Excalidraw Data\n## Text Elements\n`; + textElements.forEach(te=> { + outString += `${te.rawText ?? (te.originalText ?? te.text)} ^${te.id}\n\n`; + }); + + const elementsWithLinks = this.getElements().filter( el => el.type !== "text" && el.link) + elementsWithLinks.forEach(el=>{ + outString += `${el.link} ^${el.id}\n\n`; + }) + + outString += Object.keys(this.imagesDict).length > 0 + ? `\n## Embedded Files\n` + : "\n"; + + Object.keys(this.imagesDict).forEach((key: FileId)=> { + const item = this.imagesDict[key]; + if(item.latex) { + outString += `${key}: $$${item.latex}$$\n\n`; + } else { + if(item.file) { + if(item.file instanceof TFile) { + outString += `${key}: [[${item.file.path}]]\n\n`; + } else { + outString += `${key}: [[${item.file}]]\n\n`; + } + } else { + const hyperlinkSplit = item.hyperlink.split("#"); + const file = this.plugin.app.vault.getAbstractFileByPath(hyperlinkSplit[0]); + if(file && file instanceof TFile) { + const hasFileRef = hyperlinkSplit.length === 2 + outString += hasFileRef + ? `${key}: [[${file.path}#${hyperlinkSplit[1]}]]\n\n` + : `${key}: [[${file.path}]]\n\n`; + } else { + outString += `${key}: ${item.hyperlink}\n\n`; + } + } + } + }) + return outString + "%%\n"; + } + + const filename = params?.filename + ? params.filename + (params.filename.endsWith(".md") ? "": ".excalidraw.md") + : getDrawingFilename(this.plugin.settings); + const foldername = params?.foldername ? params.foldername : this.plugin.settings.folder; + const initData = this.plugin.settings.compatibilityMode + ? JSON.stringify(scene, null, "\t") + : frontmatter + generateMD() + + getMarkdownDrawingSection(JSON.stringify(scene, null, "\t"),this.plugin.settings.compress) + + if(params.silent) { + return (await this.plugin.createDrawing(filename,foldername,initData)).path; + } else { + return this.plugin.createAndOpenDrawing( + filename, + (params?.onNewPane ? params.onNewPane : false)?"new-pane":"active-pane", + foldername, + initData + ); + } + }; + + /** + * + * @param templatePath + * @param embedFont + * @param exportSettings use ExcalidrawAutomate.getExportSettings(boolean,boolean) + * @param loader use ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) + * @param theme + * @returns + */ + async createSVG( + templatePath?: string, + embedFont: boolean = false, + exportSettings?: ExportSettings, + loader?: EmbeddedFilesLoader, + theme?: string, + padding?: number, + ): Promise { + if (!theme) { + theme = this.plugin.settings.previewMatchObsidianTheme + ? isObsidianThemeDark() + ? "dark" + : "light" + : !this.plugin.settings.exportWithTheme + ? "light" + : undefined; + } + if (theme && !exportSettings) { + exportSettings = { + withBackground: this.plugin.settings.exportWithBackground, + withTheme: true, + isMask: false, + }; + } + if (!loader) { + loader = new EmbeddedFilesLoader( + this.plugin, + theme ? theme === "dark" : undefined, + ); + } + + return await createSVG( + templatePath, + embedFont, + exportSettings, + loader, + theme, + this.canvas.theme, + this.canvas.viewBackgroundColor, + this.getElements(), + this.plugin, + 0, + padding, + this.imagesDict + ); + }; + + + /** + * + * @param templatePath + * @param scale + * @param exportSettings use ExcalidrawAutomate.getExportSettings(boolean,boolean) + * @param loader use ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) + * @param theme + * @returns + */ + async createPNG( + templatePath?: string, + scale: number = 1, + exportSettings?: ExportSettings, + loader?: EmbeddedFilesLoader, + theme?: string, + padding?: number, + ): Promise { + if (!theme) { + theme = this.plugin.settings.previewMatchObsidianTheme + ? isObsidianThemeDark() + ? "dark" + : "light" + : !this.plugin.settings.exportWithTheme + ? "light" + : undefined; + } + if (theme && !exportSettings) { + exportSettings = { + withBackground: this.plugin.settings.exportWithBackground, + withTheme: true, + isMask: false, + }; + } + if (!loader) { + loader = new EmbeddedFilesLoader( + this.plugin, + theme ? theme === "dark" : undefined, + ); + } + + return await createPNG( + templatePath, + scale, + exportSettings, + loader, + theme, + this.canvas.theme, + this.canvas.viewBackgroundColor, + this.getElements(), + this.plugin, + 0, + padding, + this.imagesDict, + ); + }; + + /** + * Wrapper for createPNG() that returns a base64 encoded string + * @param templatePath + * @param scale + * @param exportSettings + * @param loader + * @param theme + * @param padding + * @returns + */ + async createPNGBase64( + templatePath?: string, + scale: number = 1, + exportSettings?: ExportSettings, + loader?: EmbeddedFilesLoader, + theme?: string, + padding?: number, + ): Promise { + const png = await this.createPNG(templatePath,scale,exportSettings,loader,theme,padding); + return `data:image/png;base64,${await blobToBase64(png)}` + } + + /** + * + * @param text + * @param lineLen + * @returns + */ + wrapText(text: string, lineLen: number): string { + return wrapTextAtCharLength(text, lineLen, this.plugin.settings.forceWrap); + }; + + private boxedElement( + id: string, + eltype: any, + x: number, + y: number, + w: number, + h: number, + link: string | null = null, + scale?: [number, number], + ) { + return { + id, + type: eltype, + x, + y, + width: w, + height: h, + angle: this.style.angle, + strokeColor: this.style.strokeColor, + backgroundColor: this.style.backgroundColor, + fillStyle: this.style.fillStyle, + strokeWidth: this.style.strokeWidth, + strokeStyle: this.style.strokeStyle, + roughness: this.style.roughness, + opacity: this.style.opacity, + roundness: this.style.strokeSharpness + ? (this.style.strokeSharpness === "round" + ? {type: ROUNDNESS.ADAPTIVE_RADIUS} + : null) + : this.style.roundness, + seed: Math.floor(Math.random() * 100000), + version: 1, + versionNonce: Math.floor(Math.random() * 1000000000), + updated: Date.now(), + isDeleted: false, + groupIds: [] as any, + boundElements: [] as any, + link, + locked: false, + ...scale ? {scale} : {}, + }; + } + + //retained for backward compatibility + addIFrame(topX: number, topY: number, width: number, height: number, url?: string, file?: TFile): string { + return this.addEmbeddable(topX, topY, width, height, url, file); + } + /** + * + * @param topX + * @param topY + * @param width + * @param height + * @returns + */ + public addEmbeddable( + topX: number, + topY: number, + width: number, + height: number, + url?: string, + file?: TFile, + embeddableCustomData?: EmbeddableMDCustomProps, + ): string { + //@ts-ignore + if (!this.targetView || !this.targetView?._loaded) { + errorMessage("targetView not set", "addEmbeddable()"); + return null; + } + + if (!url && !file) { + errorMessage("Either the url or the file must be set. If both are provided the URL takes precedence", "addEmbeddable()"); + return null; + } + + const id = nanoid(); + this.elementsDict[id] = this.boxedElement( + id, + "embeddable", + topX, + topY, + width, + height, + url ? url : file ? `[[${ + this.plugin.app.metadataCache.fileToLinktext( + file, + this.targetView.file.path, + false, //file.extension === "md", //changed this to false because embedable link navigation in ExcaliBrain + ) + }]]` : "", + [1,1], + ); + this.elementsDict[id].customData = {mdProps: embeddableCustomData ?? this.plugin.settings.embeddableMarkdownDefaults}; + return id; + }; + + /** + * Add elements to frame + * @param frameId + * @param elementIDs to add + * @returns void + */ + addElementsToFrame(frameId: string, elementIDs: string[]):void { + if(!this.getElement(frameId)) return; + elementIDs.forEach(elID => { + const el = this.getElement(elID); + if(el) { + el.frameId = frameId; + } + }); + } + + /** + * + * @param topX + * @param topY + * @param width + * @param height + * @param name: the display name of the frame + * @returns + */ + addFrame(topX: number, topY: number, width: number, height: number, name?: string): string { + const id = this.addRect(topX, topY, width, height); + const frame = this.getElement(id) as Mutable; + frame.type = "frame"; + frame.backgroundColor = "transparent"; + frame.strokeColor = "#000"; + frame.strokeStyle = "solid"; + frame.strokeWidth = 2; + frame.roughness = 0; + frame.roundness = null; + if(name) frame.name = name; + return id; + } + + /** + * + * @param topX + * @param topY + * @param width + * @param height + * @returns + */ + addRect(topX: number, topY: number, width: number, height: number, id?: string): string { + if(!id) id = nanoid(); + this.elementsDict[id] = this.boxedElement( + id, + "rectangle", + topX, + topY, + width, + height, + ); + return id; + }; + + /** + * + * @param topX + * @param topY + * @param width + * @param height + * @returns + */ + addDiamond( + topX: number, + topY: number, + width: number, + height: number, + id?: string, + ): string { + if(!id) id = nanoid(); + this.elementsDict[id] = this.boxedElement( + id, + "diamond", + topX, + topY, + width, + height, + ); + return id; + }; + + /** + * + * @param topX + * @param topY + * @param width + * @param height + * @returns + */ + addEllipse( + topX: number, + topY: number, + width: number, + height: number, + id?: string, + ): string { + if(!id) id = nanoid(); + this.elementsDict[id] = this.boxedElement( + id, + "ellipse", + topX, + topY, + width, + height, + ); + return id; + }; + + /** + * + * @param topX + * @param topY + * @param width + * @param height + * @returns + */ + addBlob(topX: number, topY: number, width: number, height: number, id?: string): string { + const b = height * 0.5; //minor axis of the ellipsis + const a = width * 0.5; //major axis of the ellipsis + const sx = a / 9; + const sy = b * 0.8; + const step = 6; + const p: any = []; + const pushPoint = (i: number, dir: number) => { + const x = i + Math.random() * sx - sx / 2; + p.push([ + x + Math.random() * sx - sx / 2 + ((i % 2) * sx) / 6 + topX, + dir * Math.sqrt(b * b * (1 - (x * x) / (a * a))) + + Math.random() * sy - + sy / 2 + + ((i % 2) * sy) / 6 + + topY, + ]); + }; + let i: number; + for (i = -a + sx / 2; i <= a - sx / 2; i += a / step) { + pushPoint(i, 1); + } + for (i = a - sx / 2; i >= -a + sx / 2; i -= a / step) { + pushPoint(i, -1); + } + p.push(p[0]); + const scale = (p: [[x: number, y: number]]): [[x: number, y: number]] => { + const box = getLineBox(p); + const scaleX = width / box.w; + const scaleY = height / box.h; + let i; + for (i = 0; i < p.length; i++) { + let [x, y] = p[i]; + x = (x - box.x) * scaleX + box.x; + y = (y - box.y) * scaleY + box.y; + p[i] = [x, y]; + } + return p; + }; + id = this.addLine(scale(p), id); + this.elementsDict[id] = repositionElementsToCursor( + [this.getElement(id)], + { x: topX, y: topY }, + false, + )[0]; + return id; + }; + + /** + * Refresh the size of a text element to fit its contents + * @param id - the id of the text element + */ + public refreshTextElementSize(id: string) { + const element = this.getElement(id); + if (element.type !== "text") { + return; + } + const { w, h } = _measureText( + element.text, + element.fontSize, + element.fontFamily, + getLineHeight(element.fontFamily) + ); + element.width = w; + element.height = h; + } + + + /** + * + * @param topX + * @param topY + * @param text + * @param formatting + * box: if !null, text will be boxed + * @param id + * @returns + */ + addText( + topX: number, + topY: number, + text: string, + formatting?: { + autoResize?: boolean; //Default is true. Setting this to false will wrap the text in the text element without the need for the containser. If set to false, you must set a width value as well. + wrapAt?: number; //wrapAt is ignored if autoResize is set to false (and width is provided) + width?: number; + height?: number; + textAlign?: "left" | "center" | "right"; + box?: boolean | "box" | "blob" | "ellipse" | "diamond"; + boxPadding?: number; + boxStrokeColor?: string; + textVerticalAlign?: "top" | "middle" | "bottom"; + }, + id?: string, + ): string { + id = id ?? nanoid(); + const originalText = text; + const autoresize = ((typeof formatting?.width === "undefined") || formatting?.box) + ? true + : (formatting?.autoResize ?? true) + text = (formatting?.wrapAt && autoresize) ? this.wrapText(text, formatting.wrapAt) : text; + + const { w, h } = _measureText( + text, + this.style.fontSize, + this.style.fontFamily, + getLineHeight(this.style.fontFamily) + ); + const width = formatting?.width ? formatting.width : w; + const height = formatting?.height ? formatting.height : h; + + let boxId: string = null; + const strokeColor = this.style.strokeColor; + this.style.strokeColor = formatting?.boxStrokeColor ?? strokeColor; + const boxPadding = formatting?.boxPadding ?? 30; + if (formatting?.box) { + switch (formatting.box) { + case "ellipse": + boxId = this.addEllipse( + topX - boxPadding, + topY - boxPadding, + width + 2 * boxPadding, + height + 2 * boxPadding, + ); + break; + case "diamond": + boxId = this.addDiamond( + topX - boxPadding, + topY - boxPadding, + width + 2 * boxPadding, + height + 2 * boxPadding, + ); + break; + case "blob": + boxId = this.addBlob( + topX - boxPadding, + topY - boxPadding, + width + 2 * boxPadding, + height + 2 * boxPadding, + ); + break; + default: + boxId = this.addRect( + topX - boxPadding, + topY - boxPadding, + width + 2 * boxPadding, + height + 2 * boxPadding, + ); + } + } + this.style.strokeColor = strokeColor; + const isContainerBound = boxId && formatting.box !== "blob"; + this.elementsDict[id] = { + text, + fontSize: this.style.fontSize, + fontFamily: this.style.fontFamily, + textAlign: formatting?.textAlign + ? formatting.textAlign + : this.style.textAlign ?? "left", + verticalAlign: formatting?.textVerticalAlign ?? this.style.verticalAlign, + ...this.boxedElement(id, "text", topX, topY, width, height), + containerId: isContainerBound ? boxId : null, + originalText: isContainerBound ? originalText : text, + rawText: isContainerBound ? originalText : text, + lineHeight: getLineHeight(this.style.fontFamily), + autoResize: formatting?.box ? true : (formatting?.autoResize ?? true), + }; + if (boxId && formatting?.box === "blob") { + this.addToGroup([id, boxId]); + } + if (isContainerBound) { + const box = this.elementsDict[boxId]; + if (!box.boundElements) { + box.boundElements = []; + } + box.boundElements.push({ type: "text", id }); + } + const textElement = this.getElement(id) as Mutable; + const container = (boxId && formatting.box !== "blob") ? this.getElement(boxId) as Mutable: undefined; + const dimensions = refreshTextDimensions( + textElement, + container, + arrayToMap(this.getElements()), + originalText, + ); + if(dimensions) { + textElement.width = dimensions.width; + textElement.height = dimensions.height; + textElement.x = dimensions.x; + textElement.y = dimensions.y; + textElement.text = dimensions.text; + if(container) { + container.width = dimensions.width + 2 * boxPadding; + container.height = dimensions.height + 2 * boxPadding; + } + } + return boxId ?? id; + }; + + /** + * + * @param points + * @returns + */ + addLine(points: [[x: number, y: number]], id?: string): string { + const box = getLineBox(points); + id = id ?? nanoid(); + this.elementsDict[id] = { + points: normalizeLinePoints(points), + lastCommittedPoint: null, + startBinding: null, + endBinding: null, + startArrowhead: null, + endArrowhead: null, + ...this.boxedElement(id, "line", points[0][0], points[0][1], box.w, box.h), + }; + return id; + }; + + /** + * + * @param points + * @param formatting + * @returns + */ + addArrow( + points: [x: number, y: number][], + formatting?: { + startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null; + endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null; + startObjectId?: string; + endObjectId?: string; + }, + id?: string, + ): string { + const box = getLineBox(points); + id = id ?? nanoid(); + const startPoint = points[0] as GlobalPoint; + const endPoint = points[points.length - 1] as GlobalPoint; + this.elementsDict[id] = { + points: normalizeLinePoints(points), + lastCommittedPoint: null, + startBinding: { + elementId: formatting?.startObjectId, + focus: formatting?.startObjectId + ? determineFocusDistance( + this.getElement(formatting?.startObjectId) as ExcalidrawBindableElement, + endPoint, + startPoint, + ) + : 0.1, + gap: GAP, + }, + endBinding: { + elementId: formatting?.endObjectId, + focus: formatting?.endObjectId + ? determineFocusDistance( + this.getElement(formatting?.endObjectId) as ExcalidrawBindableElement, + startPoint, + endPoint, + ) + : 0.1, + gap: GAP, + }, + //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/388 + startArrowhead: + typeof formatting?.startArrowHead !== "undefined" + ? formatting.startArrowHead + : this.style.startArrowHead, + endArrowhead: + typeof formatting?.endArrowHead !== "undefined" + ? formatting.endArrowHead + : this.style.endArrowHead, + ...this.boxedElement(id, "arrow", points[0][0], points[0][1], box.w, box.h), + }; + if (formatting?.startObjectId) { + if (!this.elementsDict[formatting.startObjectId].boundElements) { + this.elementsDict[formatting.startObjectId].boundElements = []; + } + this.elementsDict[formatting.startObjectId].boundElements.push({ + type: "arrow", + id, + }); + } + if (formatting?.endObjectId) { + if (!this.elementsDict[formatting.endObjectId].boundElements) { + this.elementsDict[formatting.endObjectId].boundElements = []; + } + this.elementsDict[formatting.endObjectId].boundElements.push({ + type: "arrow", + id, + }); + } + return id; + }; + + /** + * Adds a mermaid diagram to ExcalidrawAutomate elements + * @param diagram string containing the mermaid diagram + * @param groupElements default is trud. If true, the elements will be grouped + * @returns the ids of the elements that were created or null if there was an error + */ + async addMermaid( + diagram: string, + groupElements: boolean = true, + ): Promise { + const result = await mermaidToExcalidraw( + diagram, { + themeVariables: {fontSize: `${this.style.fontSize}`}, + flowchart: {curve: this.style.roundness===null ? "linear" : "basis"}, + } + ); + const ids:string[] = []; + if(!result) return null; + if(result?.error) return result.error; + + if(result?.elements) { + result.elements.forEach(el=>{ + ids.push(el.id); + this.elementsDict[el.id] = el; + }) + } + + if(result?.files) { + for (const key in result.files) { + this.imagesDict[key as FileId] = { + ...result.files[key], + created: Date.now(), + isHyperLink: false, + hyperlink: null, + file: null, + hasSVGwithBitmap: false, + latex: null, + } + } + } + + if(groupElements && result?.elements && ids.length > 1) { + this.addToGroup(ids); + } + return ids; + } + + /** + * + * @param topX + * @param topY + * @param imageFile + * @returns + */ + async addImage( + topX: number, + topY: number, + imageFile: TFile | string, //string may also be an Obsidian filepath with a reference such as folder/path/my.pdf#page=2 + scale: boolean = true, //default is true which will scale the image to MAX_IMAGE_SIZE, false will insert image at 100% of its size + anchor: boolean = true, //only has effect if scale is false. If anchor is true the image path will include |100%, if false the image will be inserted at 100%, but if resized by the user it won't pop back to 100% the next time Excalidraw is opened. + ): Promise { + const id = nanoid(); + const loader = new EmbeddedFilesLoader( + this.plugin, + this.canvas.theme === "dark", + ); + const image = (typeof imageFile === "string") + ? await loader.getObsidianImage(new EmbeddedFile(this.plugin, "", imageFile),0) + : await loader.getObsidianImage(imageFile,0); + + if (!image) { + return null; + } + const fileId = typeof imageFile === "string" + ? image.fileId + : imageFile.extension === "md" || imageFile.extension.toLowerCase() === "pdf" ? fileid() as FileId : image.fileId; + this.imagesDict[fileId] = { + mimeType: image.mimeType, + id: fileId, + dataURL: image.dataURL, + created: image.created, + isHyperLink: typeof imageFile === "string", + hyperlink: typeof imageFile === "string" + ? imageFile + : null, + file: typeof imageFile === "string" + ? null + : imageFile.path + (scale || !anchor ? "":"|100%"), + hasSVGwithBitmap: image.hasSVGwithBitmap, + latex: null, + size: { //must have the natural size here (e.g. for PDF cropping) + height: image.size.height, + width: image.size.width, + }, + }; + if (scale && (Math.max(image.size.width, image.size.height) > MAX_IMAGE_SIZE)) { + const scale = + MAX_IMAGE_SIZE / Math.max(image.size.width, image.size.height); + image.size.width = scale * image.size.width; + image.size.height = scale * image.size.height; + } + this.elementsDict[id] = this.boxedElement( + id, + "image", + topX, + topY, + image.size.width, + image.size.height, + ); + this.elementsDict[id].fileId = fileId; + this.elementsDict[id].scale = [1, 1]; + if(!scale && anchor) { + this.elementsDict[id].customData = {isAnchored: true} + }; + return id; + }; + + /** + * + * @param topX + * @param topY + * @param tex + * @returns + */ + async addLaTex(topX: number, topY: number, tex: string): Promise { + const id = nanoid(); + const image = await tex2dataURL(tex, 4, this.plugin.app); + if (!image) { + return null; + } + this.imagesDict[image.fileId] = { + mimeType: image.mimeType, + id: image.fileId, + dataURL: image.dataURL, + created: image.created, + file: null, + hasSVGwithBitmap: false, + latex: tex, + }; + this.elementsDict[id] = this.boxedElement( + id, + "image", + topX, + topY, + image.size.width, + image.size.height, + ); + this.elementsDict[id].fileId = image.fileId; + this.elementsDict[id].scale = [1, 1]; + return id; + }; + + /** + * returns the base64 dataURL of the LaTeX equation rendered as an SVG + * @param tex The LaTeX equation as string + * @param scale of the image, default value is 4 + * @returns + */ + //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1930 + async tex2dataURL( + tex: string, + scale: number = 4 // Default scale value, adjust as needed + ): Promise<{ + mimeType: MimeType; + fileId: FileId; + dataURL: DataURL; + created: number; + size: { height: number; width: number }; + }> { + return await tex2dataURL(tex,scale, this.plugin.app); + }; + + /** + * + * @param objectA + * @param connectionA type ConnectionPoint = "top" | "bottom" | "left" | "right" | null + * @param objectB + * @param connectionB when passed null, Excalidraw will automatically decide + * @param formatting + * numberOfPoints: points on the line. Default is 0 ie. line will only have a start and end point + * startArrowHead: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null + * endArrowHead: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null + * padding: + * @returns + */ + connectObjects( + objectA: string, + connectionA: ConnectionPoint | null, + objectB: string, + connectionB: ConnectionPoint | null, + formatting?: { + numberOfPoints?: number; + startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null; + endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null; + padding?: number; + }, + ): string { + if (!(this.elementsDict[objectA] && this.elementsDict[objectB])) { + return; + } + + if ( + ["line", "arrow", "freedraw"].includes( + this.elementsDict[objectA].type, + ) || + ["line", "arrow", "freedraw"].includes(this.elementsDict[objectB].type) + ) { + return; + } + + const padding = formatting?.padding ? formatting.padding : 10; + const numberOfPoints = formatting?.numberOfPoints + ? formatting.numberOfPoints + : 0; + const getSidePoints = (side: string, el: any) => { + switch (side) { + case "bottom": + return [(el.x + (el.x + el.width)) / 2, el.y + el.height + padding]; + case "left": + return [el.x - padding, (el.y + (el.y + el.height)) / 2]; + case "right": + return [el.x + el.width + padding, (el.y + (el.y + el.height)) / 2]; + default: + //"top" + return [(el.x + (el.x + el.width)) / 2, el.y - padding]; + } + }; + let aX; + let aY; + let bX; + let bY; + const elA = this.elementsDict[objectA]; + const elB = this.elementsDict[objectB]; + if (!connectionA || !connectionB) { + const aCenterX = elA.x + elA.width / 2; + const bCenterX = elB.x + elB.width / 2; + const aCenterY = elA.y + elA.height / 2; + const bCenterY = elB.y + elB.height / 2; + if (!connectionA) { + const intersect = intersectElementWithLine( + elA, + [bCenterX, bCenterY] as GlobalPoint, + [aCenterX, aCenterY] as GlobalPoint, + GAP, + ); + if (intersect.length === 0) { + [aX, aY] = [aCenterX, aCenterY]; + } else { + [aX, aY] = intersect[0]; + } + } + + if (!connectionB) { + const intersect = intersectElementWithLine( + elB, + [aCenterX, aCenterY] as GlobalPoint, + [bCenterX, bCenterY] as GlobalPoint, + GAP, + ); + if (intersect.length === 0) { + [bX, bY] = [bCenterX, bCenterY]; + } else { + [bX, bY] = intersect[0]; + } + } + } + if (connectionA) { + [aX, aY] = getSidePoints(connectionA, this.elementsDict[objectA]); + } + if (connectionB) { + [bX, bY] = getSidePoints(connectionB, this.elementsDict[objectB]); + } + const numAP = numberOfPoints + 2; //number of break points plus the beginning and the end + const points:[x:number, y:number][] = []; + for (let i = 0; i < numAP; i++) { + points.push([ + aX + (i * (bX - aX)) / (numAP - 1), + aY + (i * (bY - aY)) / (numAP - 1), + ]); + } + return this.addArrow(points, { + startArrowHead: formatting?.startArrowHead, + endArrowHead: formatting?.endArrowHead, + startObjectId: objectA, + endObjectId: objectB, + }); + }; + + /** + * Adds a text label to a line or arrow. Currently only works with a straight (2 point - start & end - line) + * @param lineId id of the line or arrow object in elementsDict + * @param label the label text + * @returns undefined (if unsuccessful) or the id of the new text element + */ + addLabelToLine(lineId: string, label: string): string { + const line = this.elementsDict[lineId]; + if(!line || !["arrow","line"].includes(line.type) || line.points.length !== 2) { + return; + } + + let angle = Math.atan2(line.points[1][1],line.points[1][0]); + + const size = this.measureText(label); + //let delta = size.height/6; + + if(angle < 0) { + if(angle < -Math.PI/2) { + angle+= Math.PI; + } /*else { + delta = -delta; + } */ + } else { + if(angle > Math.PI/2) { + angle-= Math.PI; + //delta = -delta; + } + } + this.style.angle = angle; + const id = this.addText( + line.x+line.points[1][0]/2-size.width/2,//+delta, + line.y+line.points[1][1]/2-size.height,//-5*size.height/6, + label + ); + this.style.angle = 0; + return id; + } + + /** + * clear elementsDict and imagesDict only + */ + clear() { + this.elementsDict = {}; + this.imagesDict = {}; + }; + + /** + * clear() + reset all style values to default + */ + reset() { + this.clear(); + this.activeScript = null; + this.style = { + strokeColor: "#000000", + backgroundColor: "transparent", + angle: 0, + fillStyle: "hachure", + strokeWidth: 1, + strokeStyle: "solid", + roughness: 1, + opacity: 100, + roundness: null, + fontFamily: 1, + fontSize: 20, + textAlign: "left", + verticalAlign: "top", + startArrowHead: null, + endArrowHead: "arrow" + }; + this.canvas = { + theme: "light", + viewBackgroundColor: "#FFFFFF", + gridSize: 0 + }; + }; + + /** + * returns true if MD file is an Excalidraw file + * @param f + * @returns + */ + isExcalidrawFile(f: TFile): boolean { + return this.plugin.isExcalidrawFile(f); + }; + targetView: ExcalidrawView = null; //the view currently edited + /** + * sets the target view for EA. All the view operations and the access to Excalidraw API will be performend on this view + * if view is null or undefined, the function will first try setView("active"), then setView("first"). + * @param view + * @returns targetView + */ + setView(view?: ExcalidrawView | "first" | "active"): ExcalidrawView { + if(!view) { + const v = this.plugin.app.workspace.getActiveViewOfType(ExcalidrawView); + if (v instanceof ExcalidrawView) { + this.targetView = v; + } + else { + this.targetView = getExcalidrawViews(this.plugin.app)[0]; + } + } + if (view == "active") { + const v = this.plugin.app.workspace.getActiveViewOfType(ExcalidrawView); + if (!(v instanceof ExcalidrawView)) { + return; + } + this.targetView = v; + } + if (view == "first") { + this.targetView = getExcalidrawViews(this.plugin.app)[0]; + } + if (view instanceof ExcalidrawView) { + this.targetView = view; + } + return this.targetView; + }; + + /** + * + * @returns https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw#ref + */ + getExcalidrawAPI(): any { + //@ts-ignore + if (!this.targetView || !this.targetView?._loaded) { + errorMessage("targetView not set", "getExcalidrawAPI()"); + return null; + } + return (this.targetView as ExcalidrawView).excalidrawAPI; + }; + + /** + * get elements in View + * @returns + */ + getViewElements(): ExcalidrawElement[] { + //@ts-ignore + if (!this.targetView || !this.targetView?._loaded) { + errorMessage("targetView not set", "getViewElements()"); + return []; + } + return this.targetView.getViewElements(); + }; + + /** + * + * @param elToDelete + * @returns + */ + deleteViewElements(elToDelete: ExcalidrawElement[]): boolean { + //@ts-ignore + if (!this.targetView || !this.targetView?._loaded) { + errorMessage("targetView not set", "deleteViewElements()"); + return false; + } + const api = this.targetView?.excalidrawAPI as ExcalidrawImperativeAPI; + if (!api) { + return false; + } + const el: ExcalidrawElement[] = api.getSceneElements() as ExcalidrawElement[]; + const st: AppState = api.getAppState(); + this.targetView.updateScene({ + elements: el.filter((e: ExcalidrawElement) => !elToDelete.includes(e)), + appState: st, + storeAction: "capture", + }); + //this.targetView.save(); + return true; + }; + + /** + * Adds a back of the note card to the current active view + * @param sectionTitle: string + * @param activate:boolean = true; if true, the new Embedded Element will be activated after creation + * @param sectionBody?: string; + * @param embeddableCustomData?: EmbeddableMDCustomProps; formatting of the embeddable element + * @returns embeddable element id + */ + async addBackOfTheCardNoteToView(sectionTitle: string, activate: boolean = false, sectionBody?: string, embeddableCustomData?: EmbeddableMDCustomProps): Promise { + //@ts-ignore + if (!this.targetView || !this.targetView?._loaded) { + errorMessage("targetView not set", "addBackOfTheCardNoteToView()"); + return null; + } + await this.targetView.forceSave(true); + return addBackOfTheNoteCard(this.targetView, sectionTitle, activate, sectionBody, embeddableCustomData); + } + + /** + * get the selected element in the view, if more are selected, get the first + * @returns + */ + getViewSelectedElement(): any { + const elements = this.getViewSelectedElements(); + return elements ? elements[0] : null; + }; + + /** + * + * @param includeFrameChildren + * @returns + */ + getViewSelectedElements(includeFrameChildren:boolean = true): any[] { + //@ts-ignore + if (!this.targetView || !this.targetView?._loaded) { + errorMessage("targetView not set", "getViewSelectedElements()"); + return []; + } + return this.targetView.getViewSelectedElements(includeFrameChildren); + }; + + /** + * + * @param el + * @returns TFile file handle for the image element + */ + getViewFileForImageElement(el: ExcalidrawElement): TFile | null { + //@ts-ignore + if (!this.targetView || !this.targetView?._loaded) { + errorMessage("targetView not set", "getViewFileForImageElement()"); + return null; + } + if (!el || el.type !== "image") { + errorMessage( + "Must provide an image element as input", + "getViewFileForImageElement()", + ); + return null; + } + return (this.targetView as ExcalidrawView)?.excalidrawData?.getFile( + el.fileId, + )?.file; + }; + + /** + * copies elements from view to elementsDict for editing + * @param elements + */ + copyViewElementsToEAforEditing(elements: ExcalidrawElement[], copyImages: boolean = false): void { + if(copyImages && elements.some(el=>el.type === "image")) { + //@ts-ignore + if (!this.targetView || !this.targetView?._loaded) { + errorMessage("targetView not set", "copyViewElementsToEAforEditing()"); + return; + } + const sceneFiles = this.targetView.getScene().files; + elements.forEach((el) => { + this.elementsDict[el.id] = cloneElement(el); + if(el.type === "image") { + const ef = this.targetView.excalidrawData.getFile(el.fileId); + const imageWithRef = ef && ef.file && ef.linkParts && ef.linkParts.ref; + const equation = this.targetView.excalidrawData.getEquation(el.fileId); + const sceneFile = sceneFiles?.[el.fileId]; + this.imagesDict[el.fileId] = { + mimeType: sceneFile.mimeType, + id: el.fileId, + dataURL: sceneFile.dataURL, + created: sceneFile.created, + ...ef ? { + isHyperLink: ef.isHyperLink || imageWithRef, + hyperlink: imageWithRef ? `${ef.file.path}#${ef.linkParts.ref}` : ef.hyperlink, + file: imageWithRef ? null : ef.file, + hasSVGwithBitmap: ef.isSVGwithBitmap, + latex: null, + } : {}, + ...equation ? { + file: null, + isHyperLink: false, + hyperlink: null, + hasSVGwithBitmap: false, + latex: equation.latex, + } : {}, + }; + } + }); + } else { + elements.forEach((el) => { + this.elementsDict[el.id] = cloneElement(el); + }); + } + }; + + /** + * + * @param forceViewMode + * @returns + */ + viewToggleFullScreen(forceViewMode: boolean = false): void { + //@ts-ignore + if (!this.targetView || !this.targetView?._loaded) { + errorMessage("targetView not set", "viewToggleFullScreen()"); + return; + } + const view = this.targetView as ExcalidrawView; + const isFullscreen = view.isFullscreen(); + if (forceViewMode) { + view.updateScene({ + //elements: ref.getSceneElements(), + appState: { + viewModeEnabled: !isFullscreen, + }, + storeAction: "update", + }); + this.targetView.toolsPanelRef?.current?.setExcalidrawViewMode(!isFullscreen); + } + + if (isFullscreen) { + view.exitFullscreen(); + } else { + view.gotoFullscreen(); + } + }; + + setViewModeEnabled(enabled: boolean): void { + //@ts-ignore + if (!this.targetView || !this.targetView?._loaded) { + errorMessage("targetView not set", "viewToggleFullScreen()"); + return; + } + const view = this.targetView as ExcalidrawView; + view.updateScene({appState:{viewModeEnabled: enabled}, storeAction: "update"}); + view.toolsPanelRef?.current?.setExcalidrawViewMode(enabled); + } + + /** + * This function gives you a more hands on access to Excalidraw. + * @param scene - The scene you want to load to Excalidraw + * @param restore - Use this if the scene includes legacy excalidraw file elements that need to be converted to the latest excalidraw data format (not a typical usecase) + * @returns + */ + viewUpdateScene ( + scene: { + elements?: ExcalidrawElement[], + appState?: AppState, + files?: BinaryFileData, + commitToHistory?: boolean, + storeAction?: "capture" | "none" | "update", + }, + restore: boolean = false, + ):void { + //@ts-ignore + if (!this.targetView || !this.targetView?._loaded) { + errorMessage("targetView not set", "viewToggleFullScreen()"); + return; + } + if (!Boolean(scene.storeAction)) { + scene.storeAction = scene.commitToHistory ? "capture" : "update"; + } + + this.targetView.updateScene({ + elements: scene.elements, + appState: scene.appState, + files: scene.files, + storeAction: scene.storeAction, + },restore); + } + + /** + * connect an object to the selected element in the view + * @param objectA ID of the element + * @param connectionA + * @param connectionB + * @param formatting + * @returns + */ + connectObjectWithViewSelectedElement( + objectA: string, + connectionA: ConnectionPoint | null, + connectionB: ConnectionPoint | null, + formatting?: { + numberOfPoints?: number; + startArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null; + endArrowHead?: "arrow"|"bar"|"circle"|"circle_outline"|"triangle"|"triangle_outline"|"diamond"|"diamond_outline"|null; + padding?: number; + }, + ): boolean { + const el = this.getViewSelectedElement(); + if (!el) { + return false; + } + const id = el.id; + this.elementsDict[id] = el; + this.connectObjects(objectA, connectionA, id, connectionB, formatting); + delete this.elementsDict[id]; + return true; + }; + + /** + * zoom tarteView to fit elements provided as input + * elements === [] will zoom to fit the entire scene + * selectElements toggles whether the elements should be in a selected state at the end of the operation + * @param selectElements + * @param elements + */ + viewZoomToElements( + selectElements: boolean, + elements: ExcalidrawElement[] + ):void { + //@ts-ignore + if (!this.targetView || !this.targetView?._loaded) { + errorMessage("targetView not set", "viewToggleFullScreen()"); + return; + } + this.targetView.zoomToElements(selectElements,elements); + } + + /** + * Adds elements from elementsDict to the current view + * @param repositionToCursor default is false + * @param save default is true + * @param newElementsOnTop controls whether elements created with ExcalidrawAutomate + * are added at the bottom of the stack or the top of the stack of elements already in the view + * Note that elements copied to the view with copyViewElementsToEAforEditing retain their + * position in the stack of elements in the view even if modified using EA + * default is false, i.e. the new elements get to the bottom of the stack + * @param shouldRestoreElements - restore elements - auto-corrects broken, incomplete or old elements included in the update + * @returns + */ + async addElementsToView( + repositionToCursor: boolean = false, + save: boolean = true, + newElementsOnTop: boolean = false, + shouldRestoreElements: boolean = false, + ): Promise { + //@ts-ignore + if (!this.targetView || !this.targetView?._loaded) { + errorMessage("targetView not set", "addElementsToView()"); + return false; + } + const elements = this.getElements(); + return await this.targetView.addElements( + elements, + repositionToCursor, + save, + this.imagesDict, + newElementsOnTop, + shouldRestoreElements, + ); + }; + + /** + * Register instance of EA to use for hooks with TargetView + * By default ExcalidrawViews will check window.ExcalidrawAutomate for event hooks. + * Using this event you can set a different instance of Excalidraw Automate for hooks + * @returns true if successful + */ + registerThisAsViewEA():boolean { + //@ts-ignore + if (!this.targetView || !this.targetView?._loaded) { + errorMessage("targetView not set", "addElementsToView()"); + return false; + } + this.targetView.setHookServer(this); + return true; + } + + /** + * Sets the targetView EA to window.ExcalidrawAutomate + * @returns true if successful + */ + deregisterThisAsViewEA():boolean { + //@ts-ignore + if (!this.targetView || !this.targetView?._loaded) { + errorMessage("targetView not set", "addElementsToView()"); + return false; + } + this.targetView.setHookServer(this); + return true; + } + + /** + * If set, this callback is triggered when the user closes an Excalidraw view. + */ + onViewUnloadHook: (view: ExcalidrawView) => void = null; + + /** + * If set, this callback is triggered, when the user changes the view mode. + * You can use this callback in case you want to do something additional when the user switches to view mode and back. + */ + onViewModeChangeHook: (isViewModeEnabled:boolean, view: ExcalidrawView, ea: ExcalidrawAutomate) => void = null; + + /** + * If set, this callback is triggered, when the user hovers a link in the scene. + * You can use this callback in case you want to do something additional when the onLinkHover event occurs. + * This callback must return a boolean value. + * In case you want to prevent the excalidraw onLinkHover action you must return false, it will stop the native excalidraw onLinkHover management flow. + */ + onLinkHoverHook: ( + element: NonDeletedExcalidrawElement, + linkText: string, + view: ExcalidrawView, + ea: ExcalidrawAutomate + ) => boolean = null; + + /** + * If set, this callback is triggered, when the user clicks a link in the scene. + * You can use this callback in case you want to do something additional when the onLinkClick event occurs. + * This callback must return a boolean value. + * In case you want to prevent the excalidraw onLinkClick action you must return false, it will stop the native excalidraw onLinkClick management flow. + */ + onLinkClickHook:( + element: ExcalidrawElement, + linkText: string, + event: MouseEvent, + view: ExcalidrawView, + ea: ExcalidrawAutomate + ) => boolean = null; + + /** + * If set, this callback is triggered, when Excalidraw receives an onDrop event. + * You can use this callback in case you want to do something additional when the onDrop event occurs. + * This callback must return a boolean value. + * In case you want to prevent the excalidraw onDrop action you must return false, it will stop the native excalidraw onDrop management flow. + */ + onDropHook: (data: { + ea: ExcalidrawAutomate; + event: React.DragEvent; + draggable: any; //Obsidian draggable object + type: "file" | "text" | "unknown"; + payload: { + files: TFile[]; //TFile[] array of dropped files + text: string; //string + }; + excalidrawFile: TFile; //the file receiving the drop event + view: ExcalidrawView; //the excalidraw view receiving the drop + pointerPosition: { x: number; y: number }; //the pointer position on canvas at the time of drop + }) => boolean = null; + + /** + * If set, this callback is triggered, when Excalidraw receives an onPaste event. + * You can use this callback in case you want to do something additional when the + * onPaste event occurs. + * This callback must return a boolean value. + * In case you want to prevent the excalidraw onPaste action you must return false, + * it will stop the native excalidraw onPaste management flow. + */ + onPasteHook: (data: { + ea: ExcalidrawAutomate; + payload: ClipboardData; + event: ClipboardEvent; + excalidrawFile: TFile; //the file receiving the paste event + view: ExcalidrawView; //the excalidraw view receiving the paste + pointerPosition: { x: number; y: number }; //the pointer position on canvas + }) => boolean = null; + + /** + * if set, this callback is triggered, when an Excalidraw file is opened + * You can use this callback in case you want to do something additional when the file is opened. + * This will run before the file level script defined in the `excalidraw-onload-script` frontmatter. + */ + onFileOpenHook: (data: { + ea: ExcalidrawAutomate; + excalidrawFile: TFile; //the file being loaded + view: ExcalidrawView; + }) => Promise; + + + /** + * if set, this callback is triggered, when an Excalidraw file is created + * see also: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1124 + */ + onFileCreateHook: (data: { + ea: ExcalidrawAutomate; + excalidrawFile: TFile; //the file being created + view: ExcalidrawView; + }) => Promise; + + + /** + * If set, this callback is triggered whenever the active canvas color changes + */ + onCanvasColorChangeHook: ( + ea: ExcalidrawAutomate, + view: ExcalidrawView, //the excalidraw view + color: string, + ) => void = null; + + /** + * If set, this callback is triggered whenever a drawing is exported to SVG. + * The string returned will replace the link in the exported SVG. + * The hook is only executed if the link is to a file internal to Obsidian + * see: https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1605 + */ + onUpdateElementLinkForExportHook: (data: { + originalLink: string, + obsidianLink: string, + linkedFile: TFile | null, + hostFile: TFile, + }) => string = null; + + /** + * utility function to generate EmbeddedFilesLoader object + * @param isDark + * @returns + */ + getEmbeddedFilesLoader(isDark?: boolean): EmbeddedFilesLoader { + return new EmbeddedFilesLoader(this.plugin, isDark); + }; + + /** + * utility function to generate ExportSettings object + * @param withBackground + * @param withTheme + * @returns + */ + getExportSettings( + withBackground: boolean, + withTheme: boolean, + isMask: boolean = false, + ): ExportSettings { + return { withBackground, withTheme, isMask }; + }; + + /** + * get bounding box of elements + * bounding box is the box encapsulating all of the elements completely + * @param elements + * @returns + */ + getBoundingBox(elements: ExcalidrawElement[]): { + topX: number; + topY: number; + width: number; + height: number; + } { + const bb = getCommonBoundingBox(elements); + return { + topX: bb.minX, + topY: bb.minY, + width: bb.maxX - bb.minX, + height: bb.maxY - bb.minY, + }; + }; + + /** + * elements grouped by the highest level groups + * @param elements + * @returns + */ + getMaximumGroups(elements: ExcalidrawElement[]): ExcalidrawElement[][] { + return getMaximumGroups(elements, arrayToMap(elements)); + }; + + /** + * gets the largest element from a group. useful when a text element is grouped with a box, and you want to connect an arrow to the box + * @param elements + * @returns + */ + getLargestElement(elements: ExcalidrawElement[]): ExcalidrawElement { + if (!elements || elements.length === 0) { + return null; + } + let largestElement = elements[0]; + const getSize = (el: ExcalidrawElement): Number => { + return el.height * el.width; + }; + let largetstSize = getSize(elements[0]); + for (let i = 1; i < elements.length; i++) { + const size = getSize(elements[i]); + if (size > largetstSize) { + largetstSize = size; + largestElement = elements[i]; + } + } + return largestElement; + }; + + /** + * @param element + * @param a + * @param b + * @param gap + * @returns 2 or 0 intersection points between line going through `a` and `b` + * and the `element`, in ascending order of distance from `a`. + */ + intersectElementWithLine( + element: ExcalidrawBindableElement, + a: readonly [number, number], + b: readonly [number, number], + gap?: number, + ): Point[] { + return intersectElementWithLine( + element, + a as GlobalPoint, + b as GlobalPoint, + gap + ); + }; + + /** + * Gets the groupId for the group that contains all the elements, or null if such a group does not exist + * @param elements + * @returns null or the groupId + */ + getCommonGroupForElements(elements: ExcalidrawElement[]): string { + const groupId = elements.map(el=>el.groupIds).reduce((prev,cur)=>cur.filter(v=>prev.includes(v))); + return groupId.length > 0 ? groupId[0] : null; + } + + /** + * Gets all the elements from elements[] that share one or more groupIds with element. + * @param element + * @param elements - typically all the non-deleted elements in the scene + * @returns + */ + getElementsInTheSameGroupWithElement( + element: ExcalidrawElement, + elements: ExcalidrawElement[], + includeFrameElements: boolean = false, + ): ExcalidrawElement[] { + if(!element || !elements) return []; + const container = (element.type === "text" && element.containerId) + ? elements.filter(el=>el.id === element.containerId) + : []; + if(element.groupIds.length === 0) { + if(includeFrameElements && element.type === "frame") { + return this.getElementsInFrame(element,elements,true); + } + if(container.length === 1) return [element,container[0]]; + return [element]; + } + + const conditionFN = container.length === 1 + ? (el: ExcalidrawElement) => el.groupIds.some(id=>element.groupIds.includes(id)) || el === container[0] + : (el: ExcalidrawElement) => el.groupIds.some(id=>element.groupIds.includes(id)); + + if(!includeFrameElements) { + return elements.filter(el=>conditionFN(el)); + } else { + //I use the set and the filter at the end to preserve scene layer seqeuence + //adding frames could potentially mess up the sequence otherwise + const elementIDs = new Set(); + elements + .filter(el=>conditionFN(el)) + .forEach(el=>{ + if(el.type === "frame") { + this.getElementsInFrame(el,elements,true).forEach(el=>elementIDs.add(el.id)) + } else { + elementIDs.add(el.id); + } + }); + return elements.filter(el=>elementIDs.has(el.id)); + } + } + + /** + * Gets all the elements from elements[] that are contained in the frame. + * @param frameElement - the frame element for which to get the elements + * @param elements - typically all the non-deleted elements in the scene + * @param shouldIncludeFrame - if true, the frame element will be included in the returned array + * this is useful when generating an image in which you want the frame to be clipped + * @returns + */ + getElementsInFrame( + frameElement: ExcalidrawElement, + elements: ExcalidrawElement[], + shouldIncludeFrame: boolean = false, + ): ExcalidrawElement[] { + if(!frameElement || !elements || frameElement.type !== "frame") return []; + return elements.filter(el=>(el.frameId === frameElement.id) || (shouldIncludeFrame && el.id === frameElement.id)); + } + + /** + * See OCR plugin for example on how to use scriptSettings + * Set by the ScriptEngine + */ + activeScript: string = null; + + /** + * + * @returns script settings. Saves settings in plugin settings, under the activeScript key + */ + getScriptSettings(): {} { + if (!this.activeScript) { + return null; + } + return this.plugin.settings.scriptEngineSettings[this.activeScript] ?? {}; + }; + + /** + * sets script settings. + * @param settings + * @returns + */ + async setScriptSettings(settings: any): Promise { + if (!this.activeScript) { + return null; + } + this.plugin.settings.scriptEngineSettings[this.activeScript] = settings; + await this.plugin.saveSettings(); + }; + + /** + * Open a file in a new workspaceleaf or reuse an existing adjacent leaf depending on Excalidraw Plugin Settings + * @param file + * @param openState - if not provided {active: true} will be used + * @returns + */ + openFileInNewOrAdjacentLeaf(file: TFile, openState?: OpenViewState): WorkspaceLeaf { + if (!file || !(file instanceof TFile)) { + return null; + } + if (!this.targetView) { + return null; + } + + const {leaf, promise} = openLeaf({ + plugin: this.plugin, + fnGetLeaf: () => getNewOrAdjacentLeaf(this.plugin, this.targetView.leaf), + file, + openState: openState ?? {active: true} + }); + return leaf; + }; + + /** + * measure text size based on current style settings + * @param text + * @returns + */ + measureText(text: string): { width: number; height: number } { + const size = _measureText( + text, + this.style.fontSize, + this.style.fontFamily, + getLineHeight(this.style.fontFamily), + ); + return { width: size.w ?? 0, height: size.h ?? 0 }; + }; + + /** + * Returns the size of the image element at 100% (i.e. the original size), or undefined if the data URL is not available + * @param imageElement an image element from the active scene on targetView + * @param shouldWaitForImage if true, the function will wait for the image to load before returning the size + */ + async getOriginalImageSize(imageElement: ExcalidrawImageElement, shouldWaitForImage: boolean=false): Promise<{width: number; height: number}> { + //@ts-ignore + if (!this.targetView || !this.targetView?._loaded) { + errorMessage("targetView not set", "getOriginalImageSize()"); + return null; + } + if(!imageElement || imageElement.type !== "image") { + errorMessage("Please provide a single image element as input", "getOriginalImageSize()"); + return null; + } + const ef = this.targetView.excalidrawData.getFile(imageElement.fileId); + if(!ef) { + errorMessage("Please provide a single image element as input", "getOriginalImageSize()"); + return null; + } + const isDark = this.getExcalidrawAPI().getAppState().theme === "dark"; + let dataURL = ef.getImage(isDark); + if(!dataURL && !shouldWaitForImage) return; + if(!dataURL) { + let watchdog = 0; + while(!dataURL && watchdog < 50) { + await sleep(100); + dataURL = ef.getImage(isDark); + watchdog++; + } + if(!dataURL) return; + } + return await getImageSize(dataURL); + } + + /** + * Resets the image to its original aspect ratio. + * If the image is resized then the function returns true. + * If the image element is not in EA (only in the view), then if image is resized, the element is copied to EA for Editing using copyViewElementsToEAforEditing([imgEl]). + * Note you need to run await ea.addElementsToView(false); to add the modified image to the view. + * @param imageElement - the EA image element to be resized + * returns true if image was changed, false if image was not changed + */ + async resetImageAspectRatio(imgEl: ExcalidrawImageElement): Promise { + //@ts-ignore + if (!this.targetView || !this.targetView?._loaded) { + errorMessage("targetView not set", "resetImageAspectRatio()"); + return null; + } + + let originalArea, originalAspectRatio; + if(imgEl.crop) { + originalArea = imgEl.width * imgEl.height; + originalAspectRatio = imgEl.crop.width / imgEl.crop.height; + } else { + const size = await this.getOriginalImageSize(imgEl, true); + if (!size) { return false; } + originalArea = imgEl.width * imgEl.height; + originalAspectRatio = size.width / size.height; + } + let newWidth = Math.sqrt(originalArea * originalAspectRatio); + let newHeight = Math.sqrt(originalArea / originalAspectRatio); + const centerX = imgEl.x + imgEl.width / 2; + const centerY = imgEl.y + imgEl.height / 2; + + if (newWidth !== imgEl.width || newHeight !== imgEl.height) { + if(!this.getElement(imgEl.id)) { + this.copyViewElementsToEAforEditing([imgEl]); + } + const eaEl = this.getElement(imgEl.id); + eaEl.width = newWidth; + eaEl.height = newHeight; + eaEl.x = centerX - newWidth / 2; + eaEl.y = centerY - newHeight / 2; + return true; + } + return false; + } + + /** + * verifyMinimumPluginVersion returns true if plugin version is >= than required + * recommended use: + * if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.5.20")) {new Notice("message");return;} + * @param requiredVersion + * @returns + */ + verifyMinimumPluginVersion(requiredVersion: string): boolean { + return verifyMinimumPluginVersion(requiredVersion); + }; + + /** + * Check if view is instance of ExcalidrawView + * @param view + * @returns + */ + isExcalidrawView(view: any): boolean { + return view instanceof ExcalidrawView; + } + + /** + * sets selection in view + * @param elements + * @returns + */ + selectElementsInView(elements: ExcalidrawElement[] | string[]): void { + //@ts-ignore + if (!this.targetView || !this.targetView?._loaded) { + errorMessage("targetView not set", "selectElementsInView()"); + return; + } + if (!elements || elements.length === 0) { + return; + } + const API: ExcalidrawImperativeAPI = this.getExcalidrawAPI(); + if(typeof elements[0] === "string") { + const els = this.getViewElements().filter(el=>(elements as string[]).includes(el.id)); + API.selectElements(els); + } else { + API.selectElements(elements as ExcalidrawElement[]); + } + }; + + /** + * @returns an 8 character long random id + */ + generateElementId(): string { + return nanoid(); + }; + + /** + * @param element + * @returns a clone of the element with a new id + */ + cloneElement(element: ExcalidrawElement): ExcalidrawElement { + const newEl = JSON.parse(JSON.stringify(element)); + newEl.id = nanoid(); + return newEl; + }; + + /** + * Moves the element to a specific position in the z-index + */ + moveViewElementToZIndex(elementId: number, newZIndex: number): void { + //@ts-ignore + if (!this.targetView || !this.targetView?._loaded) { + errorMessage("targetView not set", "moveViewElementToZIndex()"); + return; + } + const API = this.getExcalidrawAPI(); + const elements = this.getViewElements(); + const elementToMove = elements.filter((el: any) => el.id === elementId); + if (elementToMove.length === 0) { + errorMessage( + `Element (id: ${elementId}) not found`, + "moveViewElementToZIndex", + ); + return; + } + if (newZIndex >= elements.length) { + API.bringToFront(elementToMove); + return; + } + if (newZIndex < 0) { + API.sendToBack(elementToMove); + return; + } + + const oldZIndex = elements.indexOf(elementToMove[0]); + elements.splice(newZIndex, 0, elements.splice(oldZIndex, 1)[0]); + this.targetView.updateScene({ + elements, + storeAction: "capture", + }); + }; + + /** + * Deprecated. Use getCM / ColorMaster instead + * @param color + * @returns + */ + hexStringToRgb(color: string): number[] { + const res = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color); + return [parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16)]; + }; + + /** + * Deprecated. Use getCM / ColorMaster instead + * @param color + * @returns + */ + rgbToHexString(color: number[]): string { + const cm = CM({r:color[0], g:color[1], b:color[2]}); + return cm.stringHEX({alpha: false}); + }; + + /** + * Deprecated. Use getCM / ColorMaster instead + * @param color + * @returns + */ + hslToRgb(color: number[]): number[] { + const cm = CM({h:color[0], s:color[1], l:color[2]}); + return [cm.red, cm.green, cm.blue]; + }; + + /** + * Deprecated. Use getCM / ColorMaster instead + * @param color + * @returns + */ + rgbToHsl(color: number[]): number[] { + const cm = CM({r:color[0], g:color[1], b:color[2]}); + return [cm.hue, cm.saturation, cm.lightness]; + }; + + /** + * + * @param color + * @returns + */ + colorNameToHex(color: string): string { + if (COLOR_NAMES.has(color.toLowerCase().trim())) { + return COLOR_NAMES.get(color.toLowerCase().trim()); + } + return color.trim(); + }; + + /** + * https://github.com/lbragile/ColorMaster + * @param color + * @returns + */ + getCM(color:TInput): ColorMaster { + if(!color) { + log("Creates a CM object. Visit https://github.com/lbragile/ColorMaster for documentation."); + return; + } + if(typeof color === "string") { + color = this.colorNameToHex(color); + } + + return CM(color); + } + + /** + * Gets the class PolyBool from https://github.com/velipso/polybooljs + * @returns + */ + getPolyBool() { + const defaultEpsilon = 0.0000000001; + PolyBool.epsilon(defaultEpsilon); + return PolyBool; + } + + importSVG(svgString:string):boolean { + const res:ConversionResult = svgToExcalidraw(svgString); + if(res.hasErrors) { + new Notice (`There were errors while parsing the given SVG:\n${res.errors}`); + return false; + } + this.copyViewElementsToEAforEditing(res.content); + return true; + } + + destroy(): void { + this.targetView = null; + this.plugin = null; + this.elementsDict = {}; + this.imagesDict = {}; + this.mostRecentMarkdownSVG = null; + this.activeScript = null; + //@ts-ignore + this.style = {}; + //@ts-ignore + this.canvas = {}; + this.colorPalette = {}; + } +}; + +export function initExcalidrawAutomate( + plugin: ExcalidrawPlugin, +): ExcalidrawAutomate { + const ea = new ExcalidrawAutomate(plugin); + //@ts-ignore + window.ExcalidrawAutomate = ea; + return ea; +} + +function normalizeLinePoints( + points: [x: number, y: number][], + //box: { x: number; y: number; w: number; h: number }, +): number[][] { + const p = []; + const [x, y] = points[0]; + for (let i = 0; i < points.length; i++) { + p.push([points[i][0] - x, points[i][1] - y]); + } + return p; +} + +function getLineBox( + points: [x: number, y: number][] +):{x:number, y:number, w: number, h:number} { + const [x1, y1, x2, y2] = estimateLineBound(points); + return { + x: x1, + y: y1, + w: x2 - x1, //Math.abs(points[points.length-1][0]-points[0][0]), + h: y2 - y1, //Math.abs(points[points.length-1][1]-points[0][1]) + }; +} + +function getFontFamily(id: number):string { + return getFontFamilyString({fontFamily:id}) +} + +export function _measureText( + newText: string, + fontSize: number, + fontFamily: number, + lineHeight: number, +): {w: number, h:number} { + //following odd error with mindmap on iPad while synchornizing with desktop. + if (!fontSize) { + fontSize = 20; + } + if (!fontFamily) { + fontFamily = 1; + lineHeight = getLineHeight(fontFamily); + } + const metrics = measureText( + newText, + `${fontSize.toString()}px ${getFontFamily(fontFamily)}` as any, + lineHeight + ); + return { w: metrics.width, h: metrics.height }; +} + +async function getTemplate( + plugin: ExcalidrawPlugin, + fileWithPath: string, + loadFiles: boolean = false, + loader: EmbeddedFilesLoader, + depth: number, + convertMarkdownLinksToObsidianURLs: boolean = false, +): Promise<{ + elements: any; + appState: any; + frontmatter: string; + files: any; + hasSVGwithBitmap: boolean; + plaintext: string; //markdown data above Excalidraw data and below YAML frontmatter +}> { + const app = plugin.app; + const vault = app.vault; + const filenameParts = getEmbeddedFilenameParts(fileWithPath); + const templatePath = normalizePath(filenameParts.filepath); + const file = app.metadataCache.getFirstLinkpathDest(templatePath, ""); + let hasSVGwithBitmap = false; + if (file && file instanceof TFile) { + const data = (await vault.read(file)) + .replaceAll("\r\n", "\n") + .replaceAll("\r", "\n"); + const excalidrawData: ExcalidrawData = new ExcalidrawData(plugin); + + if (file.extension === "excalidraw") { + await excalidrawData.loadLegacyData(data, file); + return { + elements: convertMarkdownLinksToObsidianURLs + ? updateElementLinksToObsidianLinks({ + elements: excalidrawData.scene.elements, + hostFile: file, + }) : excalidrawData.scene.elements, + appState: excalidrawData.scene.appState, + frontmatter: "", + files: excalidrawData.scene.files, + hasSVGwithBitmap, + plaintext: "", + }; + } + + const textMode = getTextMode(data); + await excalidrawData.loadData( + data, + file, + textMode, + ); + + let trimLocation = data.search(/^##? Text Elements$/m); + if (trimLocation == -1) { + trimLocation = data.search(/##? Drawing\n/); + } + + let scene = excalidrawData.scene; + + let groupElements:ExcalidrawElement[] = scene.elements; + if(filenameParts.hasGroupref) { + const el = filenameParts.hasSectionref + ? getTextElementsMatchingQuery(scene.elements,["# "+filenameParts.sectionref],true) + : scene.elements.filter((el: ExcalidrawElement)=>el.id===filenameParts.blockref); + if(el.length > 0) { + groupElements = plugin.ea.getElementsInTheSameGroupWithElement(el[0],scene.elements,true) + } + } + if(filenameParts.hasFrameref || filenameParts.hasClippedFrameref) { + const el = getFrameBasedOnFrameNameOrId(filenameParts.blockref,scene.elements); + + if(el) { + groupElements = plugin.ea.getElementsInFrame(el,scene.elements, filenameParts.hasClippedFrameref); + } + } + + if(filenameParts.hasTaskbone) { + groupElements = groupElements.filter( el => + el.type==="freedraw" || + ( el.type==="image" && + !plugin.isExcalidrawFile(excalidrawData.getFile(el.fileId)?.file) + )); + } + + let fileIDWhiteList:Set; + + if(groupElements.length < scene.elements.length) { + fileIDWhiteList = new Set(); + groupElements.filter(el=>el.type==="image").forEach((el:ExcalidrawImageElement)=>fileIDWhiteList.add(el.fileId)); + } + + if (loadFiles) { + //debug({where:"getTemplate",template:file.name,loader:loader.uid}); + await loader.loadSceneFiles(excalidrawData, (fileArray: FileData[]) => { + //, isDark: boolean) => { + if (!fileArray || fileArray.length === 0) { + return; + } + for (const f of fileArray) { + if (f.hasSVGwithBitmap) { + hasSVGwithBitmap = true; + } + excalidrawData.scene.files[f.id] = { + mimeType: f.mimeType, + id: f.id, + dataURL: f.dataURL, + created: f.created, + }; + } + scene = scaleLoadedImage(excalidrawData.scene, fileArray).scene; + }, depth, false, fileIDWhiteList); + } + + excalidrawData.destroy(); + const filehead = getExcalidrawMarkdownHeaderSection(data); // data.substring(0, trimLocation); + let files:any = {}; + const sceneFilesSize = Object.values(scene.files).length; + if (sceneFilesSize > 0) { + if(fileIDWhiteList && (sceneFilesSize > fileIDWhiteList.size)) { + Object.values(scene.files).filter((f: any) => fileIDWhiteList.has(f.id)).forEach((f: any) => { + files[f.id] = f; + }); + } else { + files = scene.files; + } + } + + const frontmatter = filehead.match(/^---\n.*\n---\n/ms)?.[0] ?? filehead; + return { + elements: convertMarkdownLinksToObsidianURLs + ? updateElementLinksToObsidianLinks({ + elements: groupElements, + hostFile: file, + }) : groupElements, + appState: scene.appState, + frontmatter, + plaintext: frontmatter !== filehead + ? (filehead.split(/^---\n.*\n---\n/ms)?.[1] ?? "") + : "", + files, + hasSVGwithBitmap, + }; + } + return { + elements: [], + appState: {}, + frontmatter: null, + files: [], + hasSVGwithBitmap, + plaintext: "", + }; +} + +export const generatePlaceholderDataURL = (width: number, height: number): DataURL => { + const svgString = `Placeholder`; + return `data:image/svg+xml;base64,${btoa(svgString)}` as DataURL; +}; + +export async function createPNG( + templatePath: string = undefined, + scale: number = 1, + exportSettings: ExportSettings, + loader: EmbeddedFilesLoader, + forceTheme: string = undefined, + canvasTheme: string = undefined, + canvasBackgroundColor: string = undefined, + automateElements: ExcalidrawElement[] = [], + plugin: ExcalidrawPlugin, + depth: number, + padding?: number, + imagesDict?: any, +): Promise { + if (!loader) { + loader = new EmbeddedFilesLoader(plugin); + } + padding = padding ?? plugin.settings.exportPaddingSVG; + const template = templatePath + ? await getTemplate(plugin, templatePath, true, loader, depth) + : null; + let elements = template?.elements ?? []; + elements = elements.concat(automateElements); + const files = imagesDict ?? {}; + if(template?.files) { + Object.values(template.files).forEach((f:any)=>{ + if(!f.dataURL.startsWith("http")) { + files[f.id]=f; + }; + }); + } + + return await getPNG( + { + type: "excalidraw", + version: 2, + source: GITHUB_RELEASES+PLUGIN_VERSION, + elements, + appState: { + theme: forceTheme ?? template?.appState?.theme ?? canvasTheme, + viewBackgroundColor: + template?.appState?.viewBackgroundColor ?? canvasBackgroundColor, + ...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {}, + }, + files, + }, + { + withBackground: + exportSettings?.withBackground ?? plugin.settings.exportWithBackground, + withTheme: exportSettings?.withTheme ?? plugin.settings.exportWithTheme, + isMask: exportSettings?.isMask ?? false, + }, + padding, + scale, + ); +} + +export const updateElementLinksToObsidianLinks = ({elements, hostFile}:{ + elements: ExcalidrawElement[]; + hostFile: TFile; +}): ExcalidrawElement[] => { + return elements.map((el)=>{ + if(el.link && el.link.startsWith("[")) { + const partsArray = REGEX_LINK.getResList(el.link)[0]; + if(!partsArray?.value) return el; + let linkText = REGEX_LINK.getLink(partsArray); + if (linkText.search("#") > -1) { + const linkParts = getLinkParts(linkText, hostFile); + linkText = linkParts.path; + } + if (linkText.match(REG_LINKINDEX_INVALIDCHARS)) { + return el; + } + const file = EXCALIDRAW_PLUGIN.app.metadataCache.getFirstLinkpathDest( + linkText, + hostFile.path, + ); + if(!file) { + return el; + } + let link = EXCALIDRAW_PLUGIN.app.getObsidianUrl(file); + if(window.ExcalidrawAutomate?.onUpdateElementLinkForExportHook) { + link = window.ExcalidrawAutomate.onUpdateElementLinkForExportHook({ + originalLink: el.link, + obsidianLink: link, + linkedFile: file, + hostFile: hostFile + }); + } + const newElement: Mutable = cloneElement(el); + newElement.link = link; + return newElement; + } + return el; + }) +} + +function addFilterToForeignObjects(svg:SVGSVGElement):void { + const foreignObjects = svg.querySelectorAll("foreignObject"); + foreignObjects.forEach((foreignObject) => { + foreignObject.setAttribute("filter", THEME_FILTER); + }); +} + +export async function createSVG( + templatePath: string = undefined, + embedFont: boolean = false, + exportSettings: ExportSettings, + loader: EmbeddedFilesLoader, + forceTheme: string = undefined, + canvasTheme: string = undefined, + canvasBackgroundColor: string = undefined, + automateElements: ExcalidrawElement[] = [], + plugin: ExcalidrawPlugin, + depth: number, + padding?: number, + imagesDict?: any, + convertMarkdownLinksToObsidianURLs: boolean = false, +): Promise { + if (!loader) { + loader = new EmbeddedFilesLoader(plugin); + } + if(typeof exportSettings.skipInliningFonts === "undefined") { + exportSettings.skipInliningFonts = !embedFont; + } + const template = templatePath + ? await getTemplate(plugin, templatePath, true, loader, depth, convertMarkdownLinksToObsidianURLs) + : null; + let elements = template?.elements ?? []; + elements = elements.concat(automateElements); + padding = padding ?? plugin.settings.exportPaddingSVG; + const files = imagesDict ?? {}; + if(template?.files) { + Object.values(template.files).forEach((f:any)=>{ + files[f.id]=f; + }); + } + + const theme = forceTheme ?? template?.appState?.theme ?? canvasTheme; + const withTheme = exportSettings?.withTheme ?? plugin.settings.exportWithTheme; + + const filenameParts = getEmbeddedFilenameParts(templatePath); + const svg = await getSVG( + { + //createAndOpenDrawing + type: "excalidraw", + version: 2, + source: GITHUB_RELEASES+PLUGIN_VERSION, + elements, + appState: { + theme, + viewBackgroundColor: + template?.appState?.viewBackgroundColor ?? canvasBackgroundColor, + ...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {}, + }, + files, + }, + { + withBackground: + exportSettings?.withBackground ?? plugin.settings.exportWithBackground, + withTheme, + isMask: exportSettings?.isMask ?? false, + ...filenameParts?.hasClippedFrameref + ? {frameRendering: {enabled: true, name: false, outline: false, clip: true}} + : {}, + }, + padding, + null, + ); + + if (withTheme && theme === "dark") addFilterToForeignObjects(svg); + + if( + !(filenameParts.hasGroupref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref) && + (filenameParts.hasBlockref || filenameParts.hasSectionref) + ) { + let el = filenameParts.hasSectionref + ? getTextElementsMatchingQuery(elements,["# "+filenameParts.sectionref],true) + : elements.filter((el: ExcalidrawElement)=>el.id===filenameParts.blockref); + if(el.length>0) { + const containerId = el[0].containerId; + if(containerId) { + el = el.concat(elements.filter((el: ExcalidrawElement)=>el.id === containerId)); + } + const elBB = plugin.ea.getBoundingBox(el); + const drawingBB = plugin.ea.getBoundingBox(elements); + svg.viewBox.baseVal.x = elBB.topX - drawingBB.topX; + svg.viewBox.baseVal.y = elBB.topY - drawingBB.topY; + svg.viewBox.baseVal.width = elBB.width + 2*padding; + svg.viewBox.baseVal.height = elBB.height + 2*padding; + } + } + if (template?.hasSVGwithBitmap) { + svg.setAttribute("hasbitmap", "true"); + } + return svg; +} + +function estimateLineBound(points: any): [number, number, number, number] { + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + + for (const [x, y] of points) { + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y); + } + + return [minX, minY, maxX, maxY]; +} + +export function estimateBounds( + elements: ExcalidrawElement[], +): [number, number, number, number] { + const bb = getCommonBoundingBox(elements); + return [bb.minX, bb.minY, bb.maxX, bb.maxY]; +} + +export function repositionElementsToCursor( + elements: ExcalidrawElement[], + newPosition: { x: number; y: number }, + center: boolean = false, +): ExcalidrawElement[] { + const [x1, y1, x2, y2] = estimateBounds(elements); + let [offsetX, offsetY] = [0, 0]; + if (center) { + [offsetX, offsetY] = [ + newPosition.x - (x1 + x2) / 2, + newPosition.y - (y1 + y2) / 2, + ]; + } else { + [offsetX, offsetY] = [newPosition.x - x1, newPosition.y - y1]; + } + + elements.forEach((element: any) => { + //using any so I can write read-only propery x & y + element.x = element.x + offsetX; + element.y = element.y + offsetY; + }); + + return restore({elements}, null, null).elements; +} + +function errorMessage(message: string, source: string):void { + switch (message) { + case "targetView not set": + errorlog({ + where: "ExcalidrawAutomate", + source, + message: + "targetView not set, or no longer active. Use setView before calling this function", + }); + break; + case "mobile not supported": + errorlog({ + where: "ExcalidrawAutomate", + source, + message: "this function is not available on Obsidian Mobile", + }); + break; + default: + errorlog({ + where: "ExcalidrawAutomate", + source, + message: message??"unknown error", + }); + } +} + +export const insertLaTeXToView = (view: ExcalidrawView) => { + const app = view.plugin.app; + const ea = view.plugin.ea; + GenericInputPrompt.Prompt( + view, + view.plugin, + app, + t("ENTER_LATEX"), + "\\color{red}\\oint_S {E_n dA = \\frac{1}{{\\varepsilon _0 }}} Q_{inside}", + view.plugin.settings.latexBoilerplate, + undefined, + 3 + ).then(async (formula: string) => { + if (!formula) { + return; + } + ea.reset(); + await ea.addLaTex(0, 0, formula); + ea.setView(view); + ea.addElementsToView(true, false, true); + }); +}; + +export const search = async (view: ExcalidrawView) => { + const ea = view.plugin.ea; + ea.reset(); + ea.setView(view); + const elements = ea.getViewElements().filter((el) => el.type === "text" || el.type === "frame" || el.link || el.type === "image"); + if (elements.length === 0) { + return; + } + let text = await ScriptEngine.inputPrompt( + view, + view.plugin, + view.plugin.app, + "Search for", + "use quotation marks for exact match", + "", + ); + if (!text) { + return; + } + const res = text.matchAll(/"(.*?)"/g); + let query: string[] = []; + let parts; + while (!(parts = res.next()).done) { + query.push(parts.value[1]); + } + text = text.replaceAll(/"(.*?)"/g, ""); + query = query.concat(text.split(" ").filter((s:string) => s.length !== 0)); + + ea.targetView.selectElementsMatchingQuery(elements, query); +}; + +/** + * + * @param elements + * @param query + * @param exactMatch - when searching for section header exactMatch should be set to true + * @returns the elements matching the query + */ +export const getTextElementsMatchingQuery = ( + elements: ExcalidrawElement[], + query: string[], + exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530 +): ExcalidrawElement[] => { + if (!elements || elements.length === 0 || !query || query.length === 0) { + return []; + } + + return elements.filter((el: any) => + el.type === "text" && + query.some((q) => { + if (exactMatch) { + const text = el.rawText.toLowerCase().split("\n")[0].trim(); + const m = text.match(/^#*(# .*)/); + if (!m || m.length !== 2) { + return false; + } + return m[1] === q.toLowerCase(); + } + const text = el.rawText.toLowerCase().replaceAll("\n", " ").trim(); + return text.match(q.toLowerCase()); //to distinguish between "# frame" and "# frame 1" https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530 + })); +} + +/** + * + * @param elements + * @param query + * @param exactMatch - when searching for section header exactMatch should be set to true + * @returns the elements matching the query + */ +export const getFrameElementsMatchingQuery = ( + elements: ExcalidrawElement[], + query: string[], + exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530 +): ExcalidrawElement[] => { + if (!elements || elements.length === 0 || !query || query.length === 0) { + return []; + } + + return elements.filter((el: any) => + el.type === "frame" && + query.some((q) => { + if (exactMatch) { + const text = el.name?.toLowerCase().split("\n")[0].trim() ?? ""; + const m = text.match(/^#*(# .*)/); + if (!m || m.length !== 2) { + return false; + } + return m[1] === q.toLowerCase(); + } + const text = el.name + ? el.name.toLowerCase().replaceAll("\n", " ").trim() + : ""; + + return text.match(q.toLowerCase()); //to distinguish between "# frame" and "# frame 1" https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530 + })); +} + +/** + * + * @param elements + * @param query + * @param exactMatch - when searching for section header exactMatch should be set to true + * @returns the elements matching the query + */ +export const getElementsWithLinkMatchingQuery = ( + elements: ExcalidrawElement[], + query: string[], + exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530 +): ExcalidrawElement[] => { + if (!elements || elements.length === 0 || !query || query.length === 0) { + return []; + } + + return elements.filter((el: any) => + el.link && + query.some((q) => { + const text = el.link.toLowerCase().trim(); + return exactMatch + ? (text === q.toLowerCase()) + : text.match(q.toLowerCase()); + })); +} + +/** + * + * @param elements + * @param query + * @param exactMatch - when searching for section header exactMatch should be set to true + * @returns the elements matching the query + */ +export const getImagesMatchingQuery = ( + elements: ExcalidrawElement[], + query: string[], + excalidrawData: ExcalidrawData, + exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530 +): ExcalidrawElement[] => { + if (!elements || elements.length === 0 || !query || query.length === 0) { + return []; + } + + return elements.filter((el: ExcalidrawElement) => + el.type === "image" && + query.some((q) => { + const filename = excalidrawData.getFile(el.fileId)?.file?.basename.toLowerCase().trim(); + const equation = excalidrawData.getEquation(el.fileId)?.latex?.toLocaleLowerCase().trim(); + const text = filename ?? equation; + if(!text) return false; + return exactMatch + ? (text === q.toLowerCase()) + : text.match(q.toLowerCase()); + })); + } + +export const cloneElement = (el: ExcalidrawElement):any => { + const newEl = JSON.parse(JSON.stringify(el)); + newEl.version = el.version + 1; + newEl.updated = Date.now(); + newEl.versionNonce = Math.floor(Math.random() * 1000000000); + return newEl; +} + +export const verifyMinimumPluginVersion = (requiredVersion: string): boolean => { + return PLUGIN_VERSION === requiredVersion || isVersionNewerThanOther(PLUGIN_VERSION,requiredVersion); +} + +export const getBoundTextElementId = (container: ExcalidrawElement | null) => { + return container?.boundElements?.length + ? container?.boundElements?.find((ele) => ele.type === "text")?.id || null + : null; }; \ No newline at end of file diff --git a/src/ExcalidrawData.ts b/src/Shared/ExcalidrawData.ts similarity index 99% rename from src/ExcalidrawData.ts rename to src/Shared/ExcalidrawData.ts index eaae297..7140f53 100644 --- a/src/ExcalidrawData.ts +++ b/src/Shared/ExcalidrawData.ts @@ -18,9 +18,9 @@ import { refreshTextDimensions, getContainerElement, loadSceneFonts, -} from "./constants/constants"; -import ExcalidrawPlugin from "./main"; -import { TextMode } from "./ExcalidrawView"; +} from "../Constants/Constants"; +import ExcalidrawPlugin from "../Core/main"; +import { TextMode } from "../View/ExcalidrawView"; import { addAppendUpdateCustomData, compress, @@ -37,8 +37,8 @@ import { wrapTextAtCharLength, arrayToMap, compressAsync, -} from "./utils/Utils"; -import { cleanBlockRef, cleanSectionHeading, getAttachmentsFolderAndFilePath, isObsidianThemeDark } from "./utils/ObsidianUtils"; +} from "../Utils/Utils"; +import { cleanBlockRef, cleanSectionHeading, getAttachmentsFolderAndFilePath, isObsidianThemeDark } from "../Utils/ObsidianUtils"; import { ExcalidrawElement, ExcalidrawImageElement, @@ -47,15 +47,15 @@ import { } from "@zsviczian/excalidraw/types/excalidraw/element/types"; import { BinaryFiles, DataURL, SceneData } from "@zsviczian/excalidraw/types/excalidraw/types"; import { EmbeddedFile, MimeType } from "./EmbeddedFileLoader"; -import { ConfirmationPrompt } from "./dialogs/Prompt"; -import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils"; -import { DEBUGGING, debug } from "./utils/DebugHelper"; +import { ConfirmationPrompt } from "./Dialogs/Prompt"; +import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "../Utils/MermaidUtils"; +import { DEBUGGING, debug } from "../Utils/DebugHelper"; import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types"; -import { updateElementIdsInScene } from "./utils/ExcalidrawSceneUtils"; -import { getNewUniqueFilepath } from "./utils/FileUtils"; -import { t } from "./lang/helpers"; -import { displayFontMessage } from "./utils/ExcalidrawViewUtils"; -import { getPDFRect } from "./utils/PDFUtils"; +import { updateElementIdsInScene } from "../Utils/ExcalidrawSceneUtils"; +import { getNewUniqueFilepath } from "../Utils/FileUtils"; +import { t } from "../Lang/Helpers"; +import { displayFontMessage } from "../Utils/ExcalidrawViewUtils"; +import { getPDFRect } from "../Utils/PDFUtils"; type SceneDataWithFiles = SceneData & { files: BinaryFiles }; diff --git a/src/LaTeX.ts b/src/Shared/LaTeX.ts similarity index 97% rename from src/LaTeX.ts rename to src/Shared/LaTeX.ts index 9178c11..3c9c87c 100644 --- a/src/LaTeX.ts +++ b/src/Shared/LaTeX.ts @@ -1,6 +1,6 @@ // LaTeX.ts import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types"; -import ExcalidrawView from "./ExcalidrawView"; +import ExcalidrawView from "../View/ExcalidrawView"; import { FileData, MimeType } from "./EmbeddedFileLoader"; import { FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types"; import { App } from "obsidian"; diff --git a/src/ocr/Taskbone.ts b/src/Shared/OCR/Taskbone.ts similarity index 93% rename from src/ocr/Taskbone.ts rename to src/Shared/OCR/Taskbone.ts index 864ed9b..004af5f 100644 --- a/src/ocr/Taskbone.ts +++ b/src/Shared/OCR/Taskbone.ts @@ -1,13 +1,13 @@ import { ExcalidrawAutomate } from "../ExcalidrawAutomate"; import {Notice, requestUrl} from "obsidian" -import ExcalidrawPlugin from "../main" -import ExcalidrawView, { ExportSettings } from "../ExcalidrawView" -import FrontmatterEditor from "src/utils/Frontmatter"; +import ExcalidrawPlugin from "../../Core/main" +import ExcalidrawView, { ExportSettings } from "../../View/ExcalidrawView" +import FrontmatterEditor from "src/Utils/Frontmatter"; import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types"; -import { EmbeddedFilesLoader } from "src/EmbeddedFileLoader"; -import { blobToBase64 } from "src/utils/FileUtils"; -import { getEA } from "src"; -import { log } from "src/utils/DebugHelper"; +import { EmbeddedFilesLoader } from "../EmbeddedFileLoader"; +import { blobToBase64 } from "src/Utils/FileUtils"; +import { getEA } from "src/Core"; +import { log } from "src/Utils/DebugHelper"; const TASKBONE_URL = "https://api.taskbone.com/"; //"https://excalidraw-preview.onrender.com/"; const TASKBONE_OCR_FN = "execute?id=60f394af-85f6-40bc-9613-5d26dc283cbb"; diff --git a/src/Scripts.ts b/src/Shared/Scripts.ts similarity index 95% rename from src/Scripts.ts rename to src/Shared/Scripts.ts index 4317c3f..fd83ac8 100644 --- a/src/Scripts.ts +++ b/src/Shared/Scripts.ts @@ -4,18 +4,17 @@ import { normalizePath, TAbstractFile, TFile, - WorkspaceLeaf, } from "obsidian"; -import { PLUGIN_ID } from "./constants/constants"; -import ExcalidrawView from "./ExcalidrawView"; -import ExcalidrawPlugin from "./main"; -import { ButtonDefinition, GenericInputPrompt, GenericSuggester } from "./dialogs/Prompt"; -import { getIMGFilename } from "./utils/FileUtils"; -import { splitFolderAndFilename } from "./utils/FileUtils"; -import { getEA } from "src"; -import { ExcalidrawAutomate } from "./ExcalidrawAutomate"; -import { WeakArray } from "./utils/WeakArray"; -import { getExcalidrawViews } from "./utils/ObsidianUtils"; +import { PLUGIN_ID } from "../Constants/Constants"; +import ExcalidrawView from "../View/ExcalidrawView"; +import ExcalidrawPlugin from "../Core/main"; +import { ButtonDefinition, GenericInputPrompt, GenericSuggester } from "./Dialogs/Prompt"; +import { getIMGFilename } from "../Utils/FileUtils"; +import { splitFolderAndFilename } from "../Utils/FileUtils"; +import { getEA } from "src/Core"; +import { ExcalidrawAutomate } from "../Shared/ExcalidrawAutomate"; +import { WeakArray } from "../Utils/WeakArray"; +import { getExcalidrawViews } from "../Utils/ObsidianUtils"; export type ScriptIconMap = { [key: string]: { name: string; group: string; svgString: string }; diff --git a/src/Components/Suggesters/FieldSuggester.ts b/src/Shared/Suggesters/FieldSuggester.ts similarity index 95% rename from src/Components/Suggesters/FieldSuggester.ts rename to src/Shared/Suggesters/FieldSuggester.ts index a3a3eb2..ecdf140 100644 --- a/src/Components/Suggesters/FieldSuggester.ts +++ b/src/Shared/Suggesters/FieldSuggester.ts @@ -6,12 +6,12 @@ import { EditorSuggestTriggerInfo, TFile, } from "obsidian"; -import { FRONTMATTER_KEYS_INFO } from "../../dialogs/SuggesterInfo"; +import { FRONTMATTER_KEYS_INFO } from "../Dialogs/SuggesterInfo"; import { EXCALIDRAW_AUTOMATE_INFO, EXCALIDRAW_SCRIPTENGINE_INFO, -} from "../../dialogs/SuggesterInfo"; -import type ExcalidrawPlugin from "../../main"; +} from "../Dialogs/SuggesterInfo"; +import type ExcalidrawPlugin from "../../Core/main"; /** * The field suggester recommends document properties in source mode, ea and utils function and attribute names. diff --git a/src/Components/Suggesters/FileSuggestionModal.ts b/src/Shared/Suggesters/FileSuggestionModal.ts similarity index 96% rename from src/Components/Suggesters/FileSuggestionModal.ts rename to src/Shared/Suggesters/FileSuggestionModal.ts index a746150..5064d8b 100644 --- a/src/Components/Suggesters/FileSuggestionModal.ts +++ b/src/Shared/Suggesters/FileSuggestionModal.ts @@ -7,10 +7,10 @@ import { setIcon, } from "obsidian"; import { SuggestionModal } from "./SuggestionModal"; -import { t } from "src/lang/helpers"; -import { LinkSuggestion } from "src/types/types"; -import ExcalidrawPlugin from "src/main"; -import { AUDIO_TYPES, CODE_TYPES, ICON_NAME, IMAGE_TYPES, VIDEO_TYPES } from "src/constants/constants"; +import { t } from "src/Lang/Helpers"; +import { LinkSuggestion } from "src/Types/Types"; +import ExcalidrawPlugin from "src/Core/main"; +import { AUDIO_TYPES, CODE_TYPES, ICON_NAME, IMAGE_TYPES, VIDEO_TYPES } from "src/Constants/Constants"; export class FileSuggestionModal extends SuggestionModal { text: TextComponent; diff --git a/src/Components/Suggesters/FolderSuggestionModal.ts b/src/Shared/Suggesters/FolderSuggestionModal.ts similarity index 100% rename from src/Components/Suggesters/FolderSuggestionModal.ts rename to src/Shared/Suggesters/FolderSuggestionModal.ts diff --git a/src/Components/Suggesters/PathSuggestionModal.ts b/src/Shared/Suggesters/PathSuggestionModal.ts similarity index 100% rename from src/Components/Suggesters/PathSuggestionModal.ts rename to src/Shared/Suggesters/PathSuggestionModal.ts diff --git a/src/Components/Suggesters/Suggester.ts b/src/Shared/Suggesters/Suggester.ts similarity index 100% rename from src/Components/Suggesters/Suggester.ts rename to src/Shared/Suggesters/Suggester.ts diff --git a/src/Components/Suggesters/SuggestionModal.ts b/src/Shared/Suggesters/SuggestionModal.ts similarity index 100% rename from src/Components/Suggesters/SuggestionModal.ts rename to src/Shared/Suggesters/SuggestionModal.ts diff --git a/src/workers/compression-worker.ts b/src/Shared/Workers/compression-worker.ts similarity index 100% rename from src/workers/compression-worker.ts rename to src/Shared/Workers/compression-worker.ts diff --git a/src/svgToExcalidraw/attributes.ts b/src/Shared/svgToExcalidraw/attributes.ts similarity index 100% rename from src/svgToExcalidraw/attributes.ts rename to src/Shared/svgToExcalidraw/attributes.ts diff --git a/src/svgToExcalidraw/elements/ExcalidrawElement.ts b/src/Shared/svgToExcalidraw/elements/ExcalidrawElement.ts similarity index 100% rename from src/svgToExcalidraw/elements/ExcalidrawElement.ts rename to src/Shared/svgToExcalidraw/elements/ExcalidrawElement.ts diff --git a/src/svgToExcalidraw/elements/ExcalidrawScene.ts b/src/Shared/svgToExcalidraw/elements/ExcalidrawScene.ts similarity index 88% rename from src/svgToExcalidraw/elements/ExcalidrawScene.ts rename to src/Shared/svgToExcalidraw/elements/ExcalidrawScene.ts index fa78b84..9481d4e 100644 --- a/src/svgToExcalidraw/elements/ExcalidrawScene.ts +++ b/src/Shared/svgToExcalidraw/elements/ExcalidrawScene.ts @@ -1,4 +1,4 @@ -import { GITHUB_RELEASES } from "src/constants/constants"; +import { GITHUB_RELEASES } from "src/Constants/Constants"; import { ExcalidrawGenericElement } from "./ExcalidrawElement"; declare const PLUGIN_VERSION:string; diff --git a/src/svgToExcalidraw/elements/Group.ts b/src/Shared/svgToExcalidraw/elements/Group.ts similarity index 100% rename from src/svgToExcalidraw/elements/Group.ts rename to src/Shared/svgToExcalidraw/elements/Group.ts diff --git a/src/svgToExcalidraw/elements/index.ts b/src/Shared/svgToExcalidraw/elements/index.ts similarity index 100% rename from src/svgToExcalidraw/elements/index.ts rename to src/Shared/svgToExcalidraw/elements/index.ts diff --git a/src/svgToExcalidraw/elements/path/index.ts b/src/Shared/svgToExcalidraw/elements/path/index.ts similarity index 100% rename from src/svgToExcalidraw/elements/path/index.ts rename to src/Shared/svgToExcalidraw/elements/path/index.ts diff --git a/src/svgToExcalidraw/elements/path/utils/bezier.ts b/src/Shared/svgToExcalidraw/elements/path/utils/bezier.ts similarity index 100% rename from src/svgToExcalidraw/elements/path/utils/bezier.ts rename to src/Shared/svgToExcalidraw/elements/path/utils/bezier.ts diff --git a/src/svgToExcalidraw/elements/path/utils/ellipse.ts b/src/Shared/svgToExcalidraw/elements/path/utils/ellipse.ts similarity index 100% rename from src/svgToExcalidraw/elements/path/utils/ellipse.ts rename to src/Shared/svgToExcalidraw/elements/path/utils/ellipse.ts diff --git a/src/svgToExcalidraw/elements/path/utils/path-to-points.ts b/src/Shared/svgToExcalidraw/elements/path/utils/path-to-points.ts similarity index 100% rename from src/svgToExcalidraw/elements/path/utils/path-to-points.ts rename to src/Shared/svgToExcalidraw/elements/path/utils/path-to-points.ts diff --git a/src/svgToExcalidraw/elements/utils.ts b/src/Shared/svgToExcalidraw/elements/utils.ts similarity index 100% rename from src/svgToExcalidraw/elements/utils.ts rename to src/Shared/svgToExcalidraw/elements/utils.ts diff --git a/src/svgToExcalidraw/parser.ts b/src/Shared/svgToExcalidraw/parser.ts similarity index 100% rename from src/svgToExcalidraw/parser.ts rename to src/Shared/svgToExcalidraw/parser.ts diff --git a/src/svgToExcalidraw/readme.md b/src/Shared/svgToExcalidraw/readme.md similarity index 100% rename from src/svgToExcalidraw/readme.md rename to src/Shared/svgToExcalidraw/readme.md diff --git a/src/svgToExcalidraw/transform.ts b/src/Shared/svgToExcalidraw/transform.ts similarity index 100% rename from src/svgToExcalidraw/transform.ts rename to src/Shared/svgToExcalidraw/transform.ts diff --git a/src/svgToExcalidraw/types.ts b/src/Shared/svgToExcalidraw/types.ts similarity index 100% rename from src/svgToExcalidraw/types.ts rename to src/Shared/svgToExcalidraw/types.ts diff --git a/src/svgToExcalidraw/utils.ts b/src/Shared/svgToExcalidraw/utils.ts similarity index 100% rename from src/svgToExcalidraw/utils.ts rename to src/Shared/svgToExcalidraw/utils.ts diff --git a/src/svgToExcalidraw/walker.ts b/src/Shared/svgToExcalidraw/walker.ts similarity index 99% rename from src/svgToExcalidraw/walker.ts rename to src/Shared/svgToExcalidraw/walker.ts index d3458e0..92eb2bc 100644 --- a/src/svgToExcalidraw/walker.ts +++ b/src/Shared/svgToExcalidraw/walker.ts @@ -25,7 +25,7 @@ import { import { getTransformMatrix, transformPoints } from "./transform"; import { pointsOnPath } from "points-on-path"; import { randomId, getWindingOrder } from "./utils"; -import { ROUNDNESS } from "../constants/constants"; +import { ROUNDNESS } from "../../Constants/Constants"; const SUPPORTED_TAGS = [ "svg", diff --git a/src/menu/ActionButton.tsx b/src/View/Components/Menu/ActionButton.tsx similarity index 100% rename from src/menu/ActionButton.tsx rename to src/View/Components/Menu/ActionButton.tsx diff --git a/src/menu/ActionIcons.tsx b/src/View/Components/Menu/ActionIcons.tsx similarity index 99% rename from src/menu/ActionIcons.tsx rename to src/View/Components/Menu/ActionIcons.tsx index 474d1af..a043140 100644 --- a/src/menu/ActionIcons.tsx +++ b/src/View/Components/Menu/ActionIcons.tsx @@ -1,6 +1,6 @@ import { Copy, Crop, Globe, RotateCcw, Scan, Settings, TextSelect } from "lucide-react"; import * as React from "react"; -import { PenStyle } from "src/types/PenTypes"; +import { PenStyle } from "src/Types/PenTypes"; export const ICONS = { ExportImage: ( diff --git a/src/menu/EmbeddableActionsMenu.tsx b/src/View/Components/Menu/EmbeddableActionsMenu.tsx similarity index 94% rename from src/menu/EmbeddableActionsMenu.tsx rename to src/View/Components/Menu/EmbeddableActionsMenu.tsx index 7ce94f2..18cc668 100644 --- a/src/menu/EmbeddableActionsMenu.tsx +++ b/src/View/Components/Menu/EmbeddableActionsMenu.tsx @@ -1,20 +1,20 @@ import { TFile } from "obsidian"; import * as React from "react"; -import ExcalidrawView from "../ExcalidrawView"; +import ExcalidrawView from "../../ExcalidrawView"; import { ExcalidrawElement, ExcalidrawEmbeddableElement } from "@zsviczian/excalidraw/types/excalidraw/element/types"; import { AppState, ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types"; import { ActionButton } from "./ActionButton"; import { ICONS } from "./ActionIcons"; -import { t } from "src/lang/helpers"; -import { ScriptEngine } from "src/Scripts"; -import { MD_EX_SECTIONS, ROOTELEMENTSIZE, mutateElement, nanoid, sceneCoordsToViewportCoords } from "src/constants/constants"; -import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "src/ExcalidrawData"; -import { processLinkText, useDefaultExcalidrawFrame } from "src/utils/CustomEmbeddableUtils"; -import { cleanSectionHeading } from "src/utils/ObsidianUtils"; -import { EmbeddableSettings } from "src/dialogs/EmbeddableSettings"; -import { openExternalLink } from "src/utils/ExcalidrawViewUtils"; -import { getEA } from "src"; -import { ExcalidrawAutomate } from "src/ExcalidrawAutomate"; +import { t } from "src/Lang/Helpers"; +import { ScriptEngine } from "../../../Shared/Scripts"; +import { MD_EX_SECTIONS, ROOTELEMENTSIZE, nanoid, sceneCoordsToViewportCoords } from "src/Constants/Constants"; +import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "../../../Shared/ExcalidrawData"; +import { processLinkText, useDefaultExcalidrawFrame } from "src/Utils/CustomEmbeddableUtils"; +import { cleanSectionHeading } from "src/Utils/ObsidianUtils"; +import { EmbeddableSettings } from "src/Shared/Dialogs/EmbeddableSettings"; +import { openExternalLink } from "src/Utils/ExcalidrawViewUtils"; +import { getEA } from "src/Core"; +import { ExcalidrawAutomate } from "src/Shared/ExcalidrawAutomate"; export class EmbeddableMenu { private menuFadeTimeout: number = 0; diff --git a/src/menu/ObsidianMenu.tsx b/src/View/Components/Menu/ObsidianMenu.tsx similarity index 95% rename from src/menu/ObsidianMenu.tsx rename to src/View/Components/Menu/ObsidianMenu.tsx index 16c075e..bc3a03c 100644 --- a/src/menu/ObsidianMenu.tsx +++ b/src/View/Components/Menu/ObsidianMenu.tsx @@ -2,16 +2,16 @@ import { AppState, ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/e import clsx from "clsx"; import { TFile } from "obsidian"; import * as React from "react"; -import { DEVICE } from "src/constants/constants"; -import { PenSettingsModal } from "src/dialogs/PenSettingsModal"; -import ExcalidrawView from "src/ExcalidrawView"; -import { PenStyle } from "src/types/PenTypes"; -import { PENS } from "src/utils/Pens"; -import ExcalidrawPlugin from "../main"; +import { DEVICE } from "src/Constants/Constants"; +import { PenSettingsModal } from "src/Shared/Dialogs/PenSettingsModal"; +import ExcalidrawView from "src/View/ExcalidrawView"; +import { PenStyle } from "src/Types/PenTypes"; +import { PENS } from "src/Utils/Pens"; +import ExcalidrawPlugin from "../../../Core/main"; import { ICONS, penIcon, stringToSVG } from "./ActionIcons"; -import { UniversalInsertFileModal } from "src/dialogs/UniversalInsertFileModal"; -import { t } from "src/lang/helpers"; -import { getExcalidrawViews } from "src/utils/ObsidianUtils"; +import { UniversalInsertFileModal } from "src/Shared/Dialogs/UniversalInsertFileModal"; +import { t } from "src/Lang/Helpers"; +import { getExcalidrawViews } from "src/Utils/ObsidianUtils"; export function setPen (pen: PenStyle, api: any) { const st = api.getAppState(); diff --git a/src/menu/ToolsPanel.tsx b/src/View/Components/Menu/ToolsPanel.tsx similarity index 93% rename from src/menu/ToolsPanel.tsx rename to src/View/Components/Menu/ToolsPanel.tsx index 2d4e685..7c56676 100644 --- a/src/menu/ToolsPanel.tsx +++ b/src/View/Components/Menu/ToolsPanel.tsx @@ -1,739 +1,739 @@ -import clsx from "clsx"; -import { Notice, TFile } from "obsidian"; -import * as React from "react"; -import { ActionButton } from "./ActionButton"; -import { ICONS, saveIcon, stringToSVG } from "./ActionIcons"; -import { DEVICE, SCRIPT_INSTALL_FOLDER } from "../constants/constants"; -import { insertLaTeXToView, search } from "../ExcalidrawAutomate"; -import ExcalidrawView, { TextMode } from "../ExcalidrawView"; -import { t } from "../lang/helpers"; -import { ReleaseNotes } from "../dialogs/ReleaseNotes"; -import { ScriptIconMap } from "../Scripts"; -import { ScriptInstallPrompt } from "src/dialogs/ScriptInstallPrompt"; -import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types"; -import { isWinALTorMacOPT, isWinCTRLorMacCMD, isSHIFT } from "src/utils/ModifierkeyHelper"; -import { InsertPDFModal } from "src/dialogs/InsertPDFModal"; -import { ExportDialog } from "src/dialogs/ExportDialog"; -import { openExternalLink } from "src/utils/ExcalidrawViewUtils"; -import { UniversalInsertFileModal } from "src/dialogs/UniversalInsertFileModal"; -import { DEBUGGING, debug } from "src/utils/DebugHelper"; -import { REM_VALUE } from "src/utils/StylesManager"; -import { getExcalidrawViews } from "src/utils/ObsidianUtils"; - -declare const PLUGIN_VERSION:string; - -type PanelProps = { - visible: boolean; - view: WeakRef; - centerPointer: Function; - observer: WeakRef; -}; - -export type PanelState = { - visible: boolean; - top: number; - left: number; - theme: "dark" | "light"; - excalidrawViewMode: boolean; - minimized: boolean; - isDirty: boolean; - isFullscreen: boolean; - isPreviewMode: boolean; - scriptIconMap: ScriptIconMap | null; -}; - -const TOOLS_PANEL_WIDTH = () => REM_VALUE * 14.4; - -export class ToolsPanel extends React.Component { - pos1: number = 0; - pos2: number = 0; - pos3: number = 0; - pos4: number = 0; - penDownX: number = 0; - penDownY: number = 0; - previousWidth: number = 0; - previousHeight: number = 0; - onRightEdge: boolean = false; - onBottomEdge: boolean = false; - public containerRef: React.RefObject; - private view: ExcalidrawView; - - componentWillUnmount(): void { - if (this.containerRef.current) { - this.props.observer.deref()?.unobserve(this.containerRef.current); - } - this.setState({ scriptIconMap: null }); - this.containerRef = null; - this.view = null; - } - - constructor(props: PanelProps) { - super(props); - this.view = props.view.deref(); - const react = this.view.packages.react; - - this.containerRef = react.createRef(); - this.state = { - visible: props.visible, - top: 50, - left: 200, - theme: "dark", - excalidrawViewMode: false, - minimized: false, - isDirty: false, - isFullscreen: false, - isPreviewMode: true, - scriptIconMap: {}, - }; - } - - updateScriptIconMap(scriptIconMap: ScriptIconMap) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updateScriptIconMap,"ToolsPanel.updateScriptIconMap()"); - this.setState(() => { - return { scriptIconMap }; - }); - } - - setPreviewMode(isPreviewMode: boolean) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setPreviewMode,"ToolsPanel.setPreviewMode()"); - this.setState(() => { - return { - isPreviewMode, - }; - }); - } - - setFullscreen(isFullscreen: boolean) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setFullscreen,"ToolsPanel.setFullscreen()"); - this.setState(() => { - return { - isFullscreen, - }; - }); - } - - setDirty(isDirty: boolean) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setDirty,"ToolsPanel.setDirty()"); - this.setState(()=> { - return { - isDirty, - }; - }); - } - - setExcalidrawViewMode(isViewModeEnabled: boolean) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setExcalidrawViewMode,"ToolsPanel.setExcalidrawViewMode()"); - this.setState(() => { - return { - excalidrawViewMode: isViewModeEnabled, - }; - }); - } - - toggleVisibility(isMobileOrZen: boolean) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.toggleVisibility,"ToolsPanel.toggleVisibility()"); - this.setTopCenter(isMobileOrZen); - this.setState((prevState: PanelState) => { - return { - visible: !prevState.visible, - }; - }); - } - - setTheme(theme: "dark" | "light") { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setTheme,"ToolsPanel.setTheme()"); - this.setState((prevState: PanelState) => { - return { - theme, - }; - }); - } - - setTopCenter(isMobileOrZen: boolean) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setTopCenter,"ToolsPanel.setTopCenter()"); - this.setState(() => { - return { - left: - (this.containerRef.current.clientWidth - - TOOLS_PANEL_WIDTH() - - (isMobileOrZen ? 0 : TOOLS_PANEL_WIDTH() + 4)) / - 2 + - this.containerRef.current.parentElement.offsetLeft + - (isMobileOrZen ? 0 : TOOLS_PANEL_WIDTH() + 4), - top: 64 + this.containerRef.current.parentElement.offsetTop, - }; - }); - } - - updatePosition(deltaY: number = 0, deltaX: number = 0) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updatePosition,"ToolsPanel.updatePosition()"); - this.setState(() => { - const { - offsetTop, - offsetLeft, - clientWidth: width, - clientHeight: height, - } = this.containerRef.current.firstElementChild as HTMLElement; - - const top = offsetTop - deltaY; - const left = offsetLeft - deltaX; - - const { - clientWidth: parentWidth, - clientHeight: parentHeight, - offsetTop: parentOffsetTop, - offsetLeft: parentOffsetLeft, - } = this.containerRef.current.parentElement; - - this.previousHeight = parentHeight; - this.previousWidth = parentWidth; - this.onBottomEdge = top >= parentHeight - height + parentOffsetTop; - this.onRightEdge = left >= parentWidth - width + parentOffsetLeft; - - return { - top: - top < parentOffsetTop - ? parentOffsetTop - : this.onBottomEdge - ? parentHeight - height + parentOffsetTop - : top, - left: - left < parentOffsetLeft - ? parentOffsetLeft - : this.onRightEdge - ? parentWidth - width + parentOffsetLeft - : left, - }; - }); - } - - actionOpenScriptInstallDialog() { - new ScriptInstallPrompt(this.view.plugin).open(); - } - - actionOpenReleaseNotes() { - new ReleaseNotes( - this.view.app, - this.view.plugin, - PLUGIN_VERSION, - ).open(); - } - - actionConvertExcalidrawToMD() { - this.view.convertExcalidrawToMD(); - } - - actionToggleViewMode() { - if (this.state.isPreviewMode) { - this.view.changeTextMode(TextMode.raw); - } else { - this.view.changeTextMode(TextMode.parsed); - } - } - - actionToggleTrayMode() { - this.view.toggleTrayMode(); - } - - actionToggleFullscreen() { - if (this.state.isFullscreen) { - this.view.exitFullscreen(); - } else { - this.view.gotoFullscreen(); - } - } - - actionSearch() { - search(this.view); - } - - actionOCR(e:React.MouseEvent) { - if(!this.view.plugin.settings.taskboneEnabled) { - new Notice("Taskbone OCR is not enabled. Please go to plugins settings to enable it.",4000); - return; - } - this.view.plugin.taskbone.getTextForView(this.view, {forceReScan: isWinCTRLorMacCMD(e)}); - } - - actionOpenLink(e:React.MouseEvent) { - const event = new MouseEvent("click", { - ctrlKey: e.ctrlKey || !(DEVICE.isIOS || DEVICE.isMacOS), - metaKey: e.metaKey || (DEVICE.isIOS || DEVICE.isMacOS), - shiftKey: e.shiftKey, - altKey: e.altKey, - }); - this.view.handleLinkClick(event, true); - } - - actionOpenLinkProperties() { - const event = new MouseEvent("click", { - ctrlKey: true, - metaKey: true, - shiftKey: false, - altKey: false, - }); - this.view.handleLinkClick(event); - } - - actionForceSave() { - this.view.forceSave(); - } - - actionExportLibrary() { - this.view.plugin.exportLibrary(); - } - - actionExportImage() { - const view = this.view; - if(!view.exportDialog) { - view.exportDialog = new ExportDialog(view.plugin, view,view.file); - view.exportDialog.createForm(); - } - view.exportDialog.open(); - } - - actionOpenAsMarkdown() { - this.view.openAsMarkdown(); - } - - actionLinkToElement(e:React.MouseEvent) { - if(isWinALTorMacOPT(e)) { - openExternalLink("https://youtu.be/yZQoJg2RCKI", this.view.app); - return; - } - this.view.copyLinkToSelectedElementToClipboard( - isWinCTRLorMacCMD(e) ? "group=" : (isSHIFT(e) ? "area=" : "") - ); - } - - actionAddAnyFile() { - this.props.centerPointer(); - const insertFileModal = new UniversalInsertFileModal(this.view.plugin, this.view); - insertFileModal.open(); - } - - actionInsertImage() { - this.props.centerPointer(); - this.view.plugin.insertImageDialog.start( - this.view, - ); - } - - actionInsertPDF() { - this.props.centerPointer(); - const insertPDFModal = new InsertPDFModal(this.view.plugin, this.view); - insertPDFModal.open(); - } - - actionInsertMarkdown() { - this.props.centerPointer(); - this.view.plugin.insertMDDialog.start( - this.view, - ); - } - - actionInsertBackOfNote() { - this.props.centerPointer(); - this.view.insertBackOfTheNoteCard(); - } - - actionInsertLaTeX(e:React.MouseEvent) { - if(isWinALTorMacOPT(e)) { - openExternalLink("https://youtu.be/r08wk-58DPk", this.view.app); - return; - } - this.props.centerPointer(); - insertLaTeXToView(this.view); - } - - actionInsertLink() { - this.props.centerPointer(); - this.view.plugin.insertLinkDialog.start( - this.view.file.path, - (text: string, fontFamily?: 1 | 2 | 3 | 4, save?: boolean) => this.view.addText (text, fontFamily, save), - ); - } - - actionImportSVG(e:React.MouseEvent) { - this.view.plugin.importSVGDialog.start(this.view); - } - - actionCropImage(e:React.MouseEvent) { - // @ts-ignore - this.view.app.commands.executeCommandById("obsidian-excalidraw-plugin:crop-image"); - } - - async actionRunScript(key: string) { - const view = this.view; - const plugin = view.plugin; - const f = plugin.app.vault.getAbstractFileByPath(key); - if (f && f instanceof TFile) { - plugin.scriptEngine.executeScript( - view, - await plugin.app.vault.read(f), - plugin.scriptEngine.getScriptName(f), - f - ); - } - } - - async actionPinScript(key: string, scriptName: string) { - const view = this.view; - const api = view.excalidrawAPI as ExcalidrawImperativeAPI; - const plugin = view.plugin; - await plugin.loadSettings(); - const index = plugin.settings.pinnedScripts.indexOf(key) - if(index > -1) { - plugin.settings.pinnedScripts.splice(index,1); - api?.setToast({message:`Pin removed: ${scriptName}`, duration: 3000, closable: true}); - } else { - plugin.settings.pinnedScripts.push(key); - api?.setToast({message:`Pinned: ${scriptName}`, duration: 3000, closable: true}) - } - await plugin.saveSettings(); - getExcalidrawViews(plugin.app).forEach(excalidrawView=>excalidrawView.updatePinnedScripts()); - } - - private islandOnClick(event: React.MouseEvent) { - event.preventDefault(); - if ( - Math.abs(this.penDownX - this.pos3) > 5 || - Math.abs(this.penDownY - this.pos4) > 5 - ) { - return; - } - this.setState((prevState: PanelState) => { - return { - minimized: !prevState.minimized, - }; - }); - } - - private islandOnPointerDown(event: React.PointerEvent) { - const onDrag = (e: PointerEvent) => { - e.preventDefault(); - this.pos1 = this.pos3 - e.clientX; - this.pos2 = this.pos4 - e.clientY; - this.pos3 = e.clientX; - this.pos4 = e.clientY; - this.updatePosition(this.pos2, this.pos1); - }; - - const onPointerUp = () => { - this.view.ownerDocument?.removeEventListener("pointerup", onPointerUp); - this.view.ownerDocument?.removeEventListener("pointermove", onDrag); - }; - - event.preventDefault(); - this.penDownX = this.pos3 = event.clientX; - this.penDownY = this.pos4 = event.clientY; - this.view.ownerDocument.addEventListener("pointerup", onPointerUp); - this.view.ownerDocument.addEventListener("pointermove", onDrag); - }; - - render() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.render,"ToolsPanel.render()"); - return ( -
-
-
- -
-
-
-
- Utility actions -
- - - {this.state.isPreviewMode === null ? ( - - ) : ( - - )} - - - - - - - -
-
-
- Export actions -
- - - - -
-
-
- Insert actions -
- - - - - - - - - -
-
- {this.renderScriptButtons(false)} - {this.renderScriptButtons(true)} -
-
-
-
- ); - } - - private renderScriptButtons(isDownloaded: boolean) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.renderScriptButtons,"ToolsPanel.renderScriptButtons()"); - if (Object.keys(this.state.scriptIconMap).length === 0) { - return ""; - } - - const downloadedScriptsRoot = `${this.view.plugin.settings.scriptFolderPath}/${SCRIPT_INSTALL_FOLDER}/`; - - const filterCondition = (key: string): boolean => - isDownloaded - ? key.startsWith(downloadedScriptsRoot) - : !key.startsWith(downloadedScriptsRoot); - - if ( - Object.keys(this.state.scriptIconMap).filter((k) => filterCondition(k)) - .length === 0 - ) { - return ""; - } - - const groups = new Set(); - - Object.keys(this.state.scriptIconMap) - .filter((k) => filterCondition(k)) - .forEach(k => groups.add(this.state.scriptIconMap[k].group)) - - const scriptlist = Array.from(groups).sort((a,b)=>a>b?1:-1); - scriptlist.push(scriptlist.shift()); - return ( - <> - {scriptlist.map((group, index) => ( -
- {isDownloaded ? group : (group === "" ? "User" : "User/"+group)} -
- {Object.entries(this.state.scriptIconMap) - .filter(([k,v])=>v.group === group) - .sort() - .map(([key,value])=>( - - ))} -
-
- ))} - - ); - } -} +import clsx from "clsx"; +import { Notice, TFile } from "obsidian"; +import * as React from "react"; +import { ActionButton } from "./ActionButton"; +import { ICONS, saveIcon, stringToSVG } from "./ActionIcons"; +import { DEVICE, SCRIPT_INSTALL_FOLDER } from "../../../Constants/Constants"; +import { insertLaTeXToView, search } from "../../../Shared/ExcalidrawAutomate"; +import ExcalidrawView, { TextMode } from "../../ExcalidrawView"; +import { t } from "../../../Lang/Helpers"; +import { ReleaseNotes } from "../../../Shared/Dialogs/ReleaseNotes"; +import { ScriptIconMap } from "../../../Shared/Scripts"; +import { ScriptInstallPrompt } from "src/Shared/Dialogs/ScriptInstallPrompt"; +import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types"; +import { isWinALTorMacOPT, isWinCTRLorMacCMD, isSHIFT } from "src/Utils/ModifierkeyHelper"; +import { InsertPDFModal } from "src/Shared/Dialogs/InsertPDFModal"; +import { ExportDialog } from "src/Shared/Dialogs/ExportDialog"; +import { openExternalLink } from "src/Utils/ExcalidrawViewUtils"; +import { UniversalInsertFileModal } from "src/Shared/Dialogs/UniversalInsertFileModal"; +import { DEBUGGING, debug } from "src/Utils/DebugHelper"; +import { REM_VALUE } from "src/Utils/StylesManager"; +import { getExcalidrawViews } from "src/Utils/ObsidianUtils"; + +declare const PLUGIN_VERSION:string; + +type PanelProps = { + visible: boolean; + view: WeakRef; + centerPointer: Function; + observer: WeakRef; +}; + +export type PanelState = { + visible: boolean; + top: number; + left: number; + theme: "dark" | "light"; + excalidrawViewMode: boolean; + minimized: boolean; + isDirty: boolean; + isFullscreen: boolean; + isPreviewMode: boolean; + scriptIconMap: ScriptIconMap | null; +}; + +const TOOLS_PANEL_WIDTH = () => REM_VALUE * 14.4; + +export class ToolsPanel extends React.Component { + pos1: number = 0; + pos2: number = 0; + pos3: number = 0; + pos4: number = 0; + penDownX: number = 0; + penDownY: number = 0; + previousWidth: number = 0; + previousHeight: number = 0; + onRightEdge: boolean = false; + onBottomEdge: boolean = false; + public containerRef: React.RefObject; + private view: ExcalidrawView; + + componentWillUnmount(): void { + if (this.containerRef.current) { + this.props.observer.deref()?.unobserve(this.containerRef.current); + } + this.setState({ scriptIconMap: null }); + this.containerRef = null; + this.view = null; + } + + constructor(props: PanelProps) { + super(props); + this.view = props.view.deref(); + const react = this.view.packages.react; + + this.containerRef = react.createRef(); + this.state = { + visible: props.visible, + top: 50, + left: 200, + theme: "dark", + excalidrawViewMode: false, + minimized: false, + isDirty: false, + isFullscreen: false, + isPreviewMode: true, + scriptIconMap: {}, + }; + } + + updateScriptIconMap(scriptIconMap: ScriptIconMap) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updateScriptIconMap,"ToolsPanel.updateScriptIconMap()"); + this.setState(() => { + return { scriptIconMap }; + }); + } + + setPreviewMode(isPreviewMode: boolean) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setPreviewMode,"ToolsPanel.setPreviewMode()"); + this.setState(() => { + return { + isPreviewMode, + }; + }); + } + + setFullscreen(isFullscreen: boolean) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setFullscreen,"ToolsPanel.setFullscreen()"); + this.setState(() => { + return { + isFullscreen, + }; + }); + } + + setDirty(isDirty: boolean) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setDirty,"ToolsPanel.setDirty()"); + this.setState(()=> { + return { + isDirty, + }; + }); + } + + setExcalidrawViewMode(isViewModeEnabled: boolean) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setExcalidrawViewMode,"ToolsPanel.setExcalidrawViewMode()"); + this.setState(() => { + return { + excalidrawViewMode: isViewModeEnabled, + }; + }); + } + + toggleVisibility(isMobileOrZen: boolean) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.toggleVisibility,"ToolsPanel.toggleVisibility()"); + this.setTopCenter(isMobileOrZen); + this.setState((prevState: PanelState) => { + return { + visible: !prevState.visible, + }; + }); + } + + setTheme(theme: "dark" | "light") { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setTheme,"ToolsPanel.setTheme()"); + this.setState((prevState: PanelState) => { + return { + theme, + }; + }); + } + + setTopCenter(isMobileOrZen: boolean) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setTopCenter,"ToolsPanel.setTopCenter()"); + this.setState(() => { + return { + left: + (this.containerRef.current.clientWidth - + TOOLS_PANEL_WIDTH() - + (isMobileOrZen ? 0 : TOOLS_PANEL_WIDTH() + 4)) / + 2 + + this.containerRef.current.parentElement.offsetLeft + + (isMobileOrZen ? 0 : TOOLS_PANEL_WIDTH() + 4), + top: 64 + this.containerRef.current.parentElement.offsetTop, + }; + }); + } + + updatePosition(deltaY: number = 0, deltaX: number = 0) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updatePosition,"ToolsPanel.updatePosition()"); + this.setState(() => { + const { + offsetTop, + offsetLeft, + clientWidth: width, + clientHeight: height, + } = this.containerRef.current.firstElementChild as HTMLElement; + + const top = offsetTop - deltaY; + const left = offsetLeft - deltaX; + + const { + clientWidth: parentWidth, + clientHeight: parentHeight, + offsetTop: parentOffsetTop, + offsetLeft: parentOffsetLeft, + } = this.containerRef.current.parentElement; + + this.previousHeight = parentHeight; + this.previousWidth = parentWidth; + this.onBottomEdge = top >= parentHeight - height + parentOffsetTop; + this.onRightEdge = left >= parentWidth - width + parentOffsetLeft; + + return { + top: + top < parentOffsetTop + ? parentOffsetTop + : this.onBottomEdge + ? parentHeight - height + parentOffsetTop + : top, + left: + left < parentOffsetLeft + ? parentOffsetLeft + : this.onRightEdge + ? parentWidth - width + parentOffsetLeft + : left, + }; + }); + } + + actionOpenScriptInstallDialog() { + new ScriptInstallPrompt(this.view.plugin).open(); + } + + actionOpenReleaseNotes() { + new ReleaseNotes( + this.view.app, + this.view.plugin, + PLUGIN_VERSION, + ).open(); + } + + actionConvertExcalidrawToMD() { + this.view.convertExcalidrawToMD(); + } + + actionToggleViewMode() { + if (this.state.isPreviewMode) { + this.view.changeTextMode(TextMode.raw); + } else { + this.view.changeTextMode(TextMode.parsed); + } + } + + actionToggleTrayMode() { + this.view.toggleTrayMode(); + } + + actionToggleFullscreen() { + if (this.state.isFullscreen) { + this.view.exitFullscreen(); + } else { + this.view.gotoFullscreen(); + } + } + + actionSearch() { + search(this.view); + } + + actionOCR(e:React.MouseEvent) { + if(!this.view.plugin.settings.taskboneEnabled) { + new Notice("Taskbone OCR is not enabled. Please go to plugins settings to enable it.",4000); + return; + } + this.view.plugin.taskbone.getTextForView(this.view, {forceReScan: isWinCTRLorMacCMD(e)}); + } + + actionOpenLink(e:React.MouseEvent) { + const event = new MouseEvent("click", { + ctrlKey: e.ctrlKey || !(DEVICE.isIOS || DEVICE.isMacOS), + metaKey: e.metaKey || (DEVICE.isIOS || DEVICE.isMacOS), + shiftKey: e.shiftKey, + altKey: e.altKey, + }); + this.view.handleLinkClick(event, true); + } + + actionOpenLinkProperties() { + const event = new MouseEvent("click", { + ctrlKey: true, + metaKey: true, + shiftKey: false, + altKey: false, + }); + this.view.handleLinkClick(event); + } + + actionForceSave() { + this.view.forceSave(); + } + + actionExportLibrary() { + this.view.plugin.exportLibrary(); + } + + actionExportImage() { + const view = this.view; + if(!view.exportDialog) { + view.exportDialog = new ExportDialog(view.plugin, view,view.file); + view.exportDialog.createForm(); + } + view.exportDialog.open(); + } + + actionOpenAsMarkdown() { + this.view.openAsMarkdown(); + } + + actionLinkToElement(e:React.MouseEvent) { + if(isWinALTorMacOPT(e)) { + openExternalLink("https://youtu.be/yZQoJg2RCKI", this.view.app); + return; + } + this.view.copyLinkToSelectedElementToClipboard( + isWinCTRLorMacCMD(e) ? "group=" : (isSHIFT(e) ? "area=" : "") + ); + } + + actionAddAnyFile() { + this.props.centerPointer(); + const insertFileModal = new UniversalInsertFileModal(this.view.plugin, this.view); + insertFileModal.open(); + } + + actionInsertImage() { + this.props.centerPointer(); + this.view.plugin.insertImageDialog.start( + this.view, + ); + } + + actionInsertPDF() { + this.props.centerPointer(); + const insertPDFModal = new InsertPDFModal(this.view.plugin, this.view); + insertPDFModal.open(); + } + + actionInsertMarkdown() { + this.props.centerPointer(); + this.view.plugin.insertMDDialog.start( + this.view, + ); + } + + actionInsertBackOfNote() { + this.props.centerPointer(); + this.view.insertBackOfTheNoteCard(); + } + + actionInsertLaTeX(e:React.MouseEvent) { + if(isWinALTorMacOPT(e)) { + openExternalLink("https://youtu.be/r08wk-58DPk", this.view.app); + return; + } + this.props.centerPointer(); + insertLaTeXToView(this.view); + } + + actionInsertLink() { + this.props.centerPointer(); + this.view.plugin.insertLinkDialog.start( + this.view.file.path, + (text: string, fontFamily?: 1 | 2 | 3 | 4, save?: boolean) => this.view.addText (text, fontFamily, save), + ); + } + + actionImportSVG(e:React.MouseEvent) { + this.view.plugin.importSVGDialog.start(this.view); + } + + actionCropImage(e:React.MouseEvent) { + // @ts-ignore + this.view.app.commands.executeCommandById("obsidian-excalidraw-plugin:crop-image"); + } + + async actionRunScript(key: string) { + const view = this.view; + const plugin = view.plugin; + const f = plugin.app.vault.getAbstractFileByPath(key); + if (f && f instanceof TFile) { + plugin.scriptEngine.executeScript( + view, + await plugin.app.vault.read(f), + plugin.scriptEngine.getScriptName(f), + f + ); + } + } + + async actionPinScript(key: string, scriptName: string) { + const view = this.view; + const api = view.excalidrawAPI as ExcalidrawImperativeAPI; + const plugin = view.plugin; + await plugin.loadSettings(); + const index = plugin.settings.pinnedScripts.indexOf(key) + if(index > -1) { + plugin.settings.pinnedScripts.splice(index,1); + api?.setToast({message:`Pin removed: ${scriptName}`, duration: 3000, closable: true}); + } else { + plugin.settings.pinnedScripts.push(key); + api?.setToast({message:`Pinned: ${scriptName}`, duration: 3000, closable: true}) + } + await plugin.saveSettings(); + getExcalidrawViews(plugin.app).forEach(excalidrawView=>excalidrawView.updatePinnedScripts()); + } + + private islandOnClick(event: React.MouseEvent) { + event.preventDefault(); + if ( + Math.abs(this.penDownX - this.pos3) > 5 || + Math.abs(this.penDownY - this.pos4) > 5 + ) { + return; + } + this.setState((prevState: PanelState) => { + return { + minimized: !prevState.minimized, + }; + }); + } + + private islandOnPointerDown(event: React.PointerEvent) { + const onDrag = (e: PointerEvent) => { + e.preventDefault(); + this.pos1 = this.pos3 - e.clientX; + this.pos2 = this.pos4 - e.clientY; + this.pos3 = e.clientX; + this.pos4 = e.clientY; + this.updatePosition(this.pos2, this.pos1); + }; + + const onPointerUp = () => { + this.view.ownerDocument?.removeEventListener("pointerup", onPointerUp); + this.view.ownerDocument?.removeEventListener("pointermove", onDrag); + }; + + event.preventDefault(); + this.penDownX = this.pos3 = event.clientX; + this.penDownY = this.pos4 = event.clientY; + this.view.ownerDocument.addEventListener("pointerup", onPointerUp); + this.view.ownerDocument.addEventListener("pointermove", onDrag); + }; + + render() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.render,"ToolsPanel.render()"); + return ( +
+
+
+ +
+
+
+
+ Utility actions +
+ + + {this.state.isPreviewMode === null ? ( + + ) : ( + + )} + + + + + + + +
+
+
+ Export actions +
+ + + + +
+
+
+ Insert actions +
+ + + + + + + + + +
+
+ {this.renderScriptButtons(false)} + {this.renderScriptButtons(true)} +
+
+
+
+ ); + } + + private renderScriptButtons(isDownloaded: boolean) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.renderScriptButtons,"ToolsPanel.renderScriptButtons()"); + if (Object.keys(this.state.scriptIconMap).length === 0) { + return ""; + } + + const downloadedScriptsRoot = `${this.view.plugin.settings.scriptFolderPath}/${SCRIPT_INSTALL_FOLDER}/`; + + const filterCondition = (key: string): boolean => + isDownloaded + ? key.startsWith(downloadedScriptsRoot) + : !key.startsWith(downloadedScriptsRoot); + + if ( + Object.keys(this.state.scriptIconMap).filter((k) => filterCondition(k)) + .length === 0 + ) { + return ""; + } + + const groups = new Set(); + + Object.keys(this.state.scriptIconMap) + .filter((k) => filterCondition(k)) + .forEach(k => groups.add(this.state.scriptIconMap[k].group)) + + const scriptlist = Array.from(groups).sort((a,b)=>a>b?1:-1); + scriptlist.push(scriptlist.shift()); + return ( + <> + {scriptlist.map((group, index) => ( +
+ {isDownloaded ? group : (group === "" ? "User" : "User/"+group)} +
+ {Object.entries(this.state.scriptIconMap) + .filter(([k,v])=>v.group === group) + .sort() + .map(([key,value])=>( + + ))} +
+
+ ))} + + ); + } +} diff --git a/src/customEmbeddable.tsx b/src/View/Components/customEmbeddable.tsx similarity index 98% rename from src/customEmbeddable.tsx rename to src/View/Components/customEmbeddable.tsx index bc56587..a89c273 100644 --- a/src/customEmbeddable.tsx +++ b/src/View/Components/customEmbeddable.tsx @@ -1,13 +1,13 @@ import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types"; -import ExcalidrawView from "./ExcalidrawView"; +import ExcalidrawView from "src/View/ExcalidrawView"; import { Notice, WorkspaceLeaf, WorkspaceSplit } from "obsidian"; import * as React from "react"; -import { ConstructableWorkspaceSplit, getContainerForDocument, isObsidianThemeDark } from "./utils/ObsidianUtils"; -import { DEVICE, EXTENDED_EVENT_TYPES, KEYBOARD_EVENT_TYPES } from "./constants/constants"; +import { ConstructableWorkspaceSplit, getContainerForDocument, isObsidianThemeDark } from "src/Utils/ObsidianUtils"; +import { DEVICE, EXTENDED_EVENT_TYPES, KEYBOARD_EVENT_TYPES } from "src/Constants/Constants"; import { ExcalidrawImperativeAPI, UIAppState } from "@zsviczian/excalidraw/types/excalidraw/types"; -import { ObsidianCanvasNode } from "./utils/CanvasNodeFactory"; -import { processLinkText, patchMobileView } from "./utils/CustomEmbeddableUtils"; -import { EmbeddableMDCustomProps } from "./dialogs/EmbeddableSettings"; +import { ObsidianCanvasNode } from "src/Utils/CanvasNodeFactory"; +import { processLinkText, patchMobileView } from "src/Utils/CustomEmbeddableUtils"; +import { EmbeddableMDCustomProps } from "src/Shared/Dialogs/EmbeddableSettings"; declare module "obsidian" { interface Workspace { diff --git a/src/dialogs/ExcalidrawLoading.ts b/src/View/ExcalidrawLoading.ts similarity index 88% rename from src/dialogs/ExcalidrawLoading.ts rename to src/View/ExcalidrawLoading.ts index 62e5b62..895ad6a 100644 --- a/src/dialogs/ExcalidrawLoading.ts +++ b/src/View/ExcalidrawLoading.ts @@ -1,7 +1,7 @@ import { App, FileView, WorkspaceLeaf } from "obsidian"; -import { VIEW_TYPE_EXCALIDRAW_LOADING } from "src/constants/constants"; -import ExcalidrawPlugin from "src/main"; -import { setExcalidrawView } from "src/utils/ObsidianUtils"; +import { VIEW_TYPE_EXCALIDRAW_LOADING } from "src/Constants/Constants"; +import ExcalidrawPlugin from "src/Core/main"; +import { setExcalidrawView } from "src/Utils/ObsidianUtils"; export function switchToExcalidraw(app: App) { const leaves = app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW_LOADING).filter(l=>l.view instanceof ExcalidrawLoading); diff --git a/src/ExcalidrawView.ts b/src/View/ExcalidrawView.ts similarity index 95% rename from src/ExcalidrawView.ts rename to src/View/ExcalidrawView.ts index d2fef9a..258ea5b 100644 --- a/src/ExcalidrawView.ts +++ b/src/View/ExcalidrawView.ts @@ -1,6464 +1,6428 @@ -import { - TextFileView, - WorkspaceLeaf, - normalizePath, - TFile, - WorkspaceItem, - Notice, - Menu, - MarkdownView, - request, - requireApiVersion, - HoverParent, - HoverPopover, -} from "obsidian"; -//import * as React from "react"; -//import * as ReactDOM from "react-dom"; -//import Excalidraw from "@zsviczian/excalidraw"; -import { - ExcalidrawElement, - ExcalidrawImageElement, - ExcalidrawMagicFrameElement, - ExcalidrawTextElement, - FileId, - NonDeletedExcalidrawElement, -} from "@zsviczian/excalidraw/types/excalidraw/element/types"; -import { - AppState, - BinaryFileData, - DataURL, - ExcalidrawImperativeAPI, - Gesture, - LibraryItems, - UIAppState, -} from "@zsviczian/excalidraw/types/excalidraw/types"; -import { - VIEW_TYPE_EXCALIDRAW, - ICON_NAME, - DISK_ICON_NAME, - SCRIPTENGINE_ICON_NAME, - TEXT_DISPLAY_RAW_ICON_NAME, - TEXT_DISPLAY_PARSED_ICON_NAME, - IMAGE_TYPES, - REG_LINKINDEX_INVALIDCHARS, - KEYCODE, - FRONTMATTER_KEYS, - DEVICE, - GITHUB_RELEASES, - EXPORT_IMG_ICON_NAME, - viewportCoordsToSceneCoords, - ERROR_IFRAME_CONVERSION_CANCELED, - restore, - obsidianToExcalidrawMap, - MAX_IMAGE_SIZE, - fileid, - sceneCoordsToViewportCoords, - MD_EX_SECTIONS, - refreshTextDimensions, - getContainerElement, -} from "./constants/constants"; -import ExcalidrawPlugin from "./main"; -import { - repositionElementsToCursor, - ExcalidrawAutomate, - getTextElementsMatchingQuery, - cloneElement, - getFrameElementsMatchingQuery, - getElementsWithLinkMatchingQuery, - getImagesMatchingQuery, - getBoundTextElementId -} from "./ExcalidrawAutomate"; -import { t } from "./lang/helpers"; -import { - ExcalidrawData, - REG_LINKINDEX_HYPERLINK, - REGEX_LINK, - AutoexportPreference, - getExcalidrawMarkdownHeaderSection, -} from "./ExcalidrawData"; -import { - checkAndCreateFolder, - download, - getDataURLFromURL, - getIMGFilename, - getInternalLinkOrFileURLLink, - getMimeType, - getNewUniqueFilepath, - getURLImageExtension, -} from "./utils/FileUtils"; -import { - checkExcalidrawVersion, - errorlog, - getEmbeddedFilenameParts, - getExportTheme, - getPNG, - getPNGScale, - getSVG, - getExportPadding, - getWithBackground, - hasExportTheme, - scaleLoadedImage, - svgToBase64, - hyperlinkIsImage, - hyperlinkIsYouTubeLink, - getYouTubeThumbnailLink, - isContainer, - fragWithHTML, - isMaskFile, - shouldEmbedScene, - _getContainerElement, - arrayToMap, -} from "./utils/Utils"; -import { cleanBlockRef, cleanSectionHeading, closeLeafView, getAttachmentsFolderAndFilePath, getLeaf, getParentOfClass, obsidianPDFQuoteWithRef, openLeaf, setExcalidrawView } from "./utils/ObsidianUtils"; -import { splitFolderAndFilename } from "./utils/FileUtils"; -import { ConfirmationPrompt, GenericInputPrompt, NewFileActions, Prompt, linkPrompt } from "./dialogs/Prompt"; -import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard"; -import { updateEquation } from "./LaTeX"; -import { - EmbeddedFile, - EmbeddedFilesLoader, - FileData, - generateIdFromFile, -} from "./EmbeddedFileLoader"; -import { ScriptInstallPrompt } from "./dialogs/ScriptInstallPrompt"; -import { ObsidianMenu } from "./menu/ObsidianMenu"; -import { ToolsPanel } from "./menu/ToolsPanel"; -import { ScriptEngine } from "./Scripts"; -import { getTextElementAtPointer, getImageElementAtPointer, getElementWithLinkAtPointer } from "./utils/GetElementAtPointer"; -import { excalidrawSword, ICONS, LogoWrapper, Rank, saveIcon, SwordColors } from "./menu/ActionIcons"; -import { ExportDialog } from "./dialogs/ExportDialog"; -import { getEA } from "src" -import { anyModifierKeysPressed, emulateKeysForLinkClick, webbrowserDragModifierType, internalDragModifierType, isWinALTorMacOPT, isWinCTRLorMacCMD, isWinMETAorMacCTRL, isSHIFT, linkClickModifierType, localFileDragModifierType, ModifierKeys, modifierKeyTooltipMessages } from "./utils/ModifierkeyHelper"; -import { setDynamicStyle } from "./utils/DynamicStyling"; -import { InsertPDFModal } from "./dialogs/InsertPDFModal"; -import { CustomEmbeddable, renderWebView } from "./customEmbeddable"; -import { addBackOfTheNoteCard, getExcalidrawFileForwardLinks, getFrameBasedOnFrameNameOrId, getLinkTextFromLink, insertEmbeddableToView, insertImageToView, isTextImageTransclusion, openExternalLink, parseObsidianLink, renderContextMenuAction, tmpBruteForceCleanup } from "./utils/ExcalidrawViewUtils"; -import { imageCache } from "./utils/ImageCache"; -import { CanvasNodeFactory, ObsidianCanvasNode } from "./utils/CanvasNodeFactory"; -import { EmbeddableMenu } from "./menu/EmbeddableActionsMenu"; -import { useDefaultExcalidrawFrame } from "./utils/CustomEmbeddableUtils"; -import { UniversalInsertFileModal } from "./dialogs/UniversalInsertFileModal"; -import { getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils"; -import { nanoid } from "nanoid"; -import { CustomMutationObserver, DEBUGGING, debug, log} from "./utils/DebugHelper"; -import { errorHTML, extractCodeBlocks, postOpenAI } from "./utils/AIUtils"; -import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types"; -import { SelectCard } from "./dialogs/SelectCard"; -import { Packages } from "./types/types"; -import React from "react"; -import { diagramToHTML } from "./utils/matic"; -import { IS_WORKER_SUPPORTED } from "./workers/compression-worker"; -import { getPDFCropRect } from "./utils/PDFUtils"; - -const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000; -const PREVENT_RELOAD_TIMEOUT = 2000; -const RE_TAIL = /^## Drawing\n.*```\n%%$(.*)/ms; - -declare const PLUGIN_VERSION:string; - -declare module "obsidian" { - interface Workspace { - floatingSplit: any; - } - - interface WorkspaceSplit { - containerEl: HTMLDivElement; - } -} - -type SelectedElementWithLink = { id: string; text: string }; -type SelectedImage = { id: string; fileId: FileId }; - -export enum TextMode { - parsed = "parsed", - raw = "raw", -} - -interface WorkspaceItemExt extends WorkspaceItem { - containerEl: HTMLElement; -} - -export interface ExportSettings { - withBackground: boolean; - withTheme: boolean; - isMask: boolean; - frameRendering?: { //optional, overrides relevant appState settings for rendering the frame - enabled: boolean; - name: boolean; - outline: boolean; - clip: boolean; - }; - skipInliningFonts?: boolean; -} - -const HIDE = "excalidraw-hidden"; -const SHOW = "excalidraw-visible"; - -export const addFiles = async ( - files: FileData[], - view: ExcalidrawView, - isDark?: boolean, -) => { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(addFiles, "ExcalidrawView.addFiles", files, view, isDark); - if (!files || files.length === 0 || !view) { - return; - } - const api = view.excalidrawAPI; - if (!api) { - return; - } - - //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/544 - files = files.filter( - (f) => f && f.size && f.size.height > 0 && f.size.width > 0, - ); //height will be zero when file does not exisig in case of broken embedded file links - if (files.length === 0) { - return; - } - const s = scaleLoadedImage(view.getScene(), files); - if (isDark === undefined) { - isDark = s.scene.appState.theme; - } - // update element.crop naturalWidth and naturalHeight in case scale of PDF loading has changed - // update crop.x crop.y, crop.width, crop.height according to the new scale - files - .filter((f:FileData) => view.excalidrawData.getFile(f.id)?.file?.extension === "pdf") - .forEach((f:FileData) => { - s.scene.elements - .filter((el:ExcalidrawElement)=>el.type === "image" && el.fileId === f.id && el.crop && el.crop.naturalWidth !== f.size.width) - .forEach((el:Mutable) => { - s.dirty = true; - const scale = f.size.width / el.crop.naturalWidth; - el.crop = { - x: el.crop.x * scale, - y: el.crop.y * scale, - width: el.crop.width * scale, - height: el.crop.height * scale, - naturalWidth: f.size.width, - naturalHeight: f.size.height, - }; - }); - }); - - if (s.dirty) { - //debug({where:"ExcalidrawView.addFiles",file:view.file.name,dataTheme:view.excalidrawData.scene.appState.theme,before:"updateScene",state:scene.appState}) - view.updateScene({ - elements: s.scene.elements, - appState: s.scene.appState, - storeAction: "update", - }); - } - for (const f of files) { - if (view.excalidrawData.hasFile(f.id)) { - const embeddedFile = view.excalidrawData.getFile(f.id); - - embeddedFile.setImage( - f.dataURL, - f.mimeType, - f.size, - isDark, - f.hasSVGwithBitmap, - ); - } - if (view.excalidrawData.hasEquation(f.id)) { - const latex = view.excalidrawData.getEquation(f.id).latex; - view.excalidrawData.setEquation(f.id, { latex, isLoaded: true }); - } - } - api.addFiles(files); -}; - -const warningUnknowSeriousError = () => { - new Notice(t("WARNING_SERIOUS_ERROR"),60000); -}; - -type ActionButtons = "save" | "isParsed" | "isRaw" | "link" | "scriptInstall"; - -let windowMigratedDisableZoomOnce = false; - -export default class ExcalidrawView extends TextFileView implements HoverParent{ - public hoverPopover: HoverPopover; - private freedrawLastActiveTimestamp: number = 0; - public exportDialog: ExportDialog; - public excalidrawData: ExcalidrawData; - //public excalidrawRef: React.MutableRefObject = null; - public excalidrawRoot: any; - public excalidrawAPI:any = null; - public excalidrawWrapperRef: React.MutableRefObject = null; - public toolsPanelRef: React.MutableRefObject = null; - public embeddableMenuRef: React.MutableRefObject = null; - private parentMoveObserver: MutationObserver | CustomMutationObserver; - public linksAlwaysOpenInANewPane: boolean = false; //override the need for SHIFT+CTRL+click (used by ExcaliBrain) - public allowFrameButtonsInViewMode: boolean = false; //override for ExcaliBrain - private _hookServer: ExcalidrawAutomate; - public lastSaveTimestamp: number = 0; //used to validate if incoming file should sync with open file - 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 - //Obsidian 0.15.0 - private draginfoDiv: HTMLDivElement; - public canvasNodeFactory: CanvasNodeFactory; - private embeddableRefs = new Map(); - private embeddableLeafRefs = new Map(); - - public semaphores: { - warnAboutLinearElementLinkClick: boolean; - //flag to prevent overwriting the changes the user makes in an embeddable view editing the back side of the drawing - embeddableIsEditingSelf: boolean; - popoutUnload: boolean; //the unloaded Excalidraw view was the last leaf in the popout window - viewunload: boolean; - //first time initialization of the view - scriptsReady: boolean; - - //The role of justLoaded is to capture the Excalidraw.onChange event that fires right after the canvas was loaded for the first time to - //- prevent the first onChange event to mark the file as dirty and to consequently cause a save right after load, causing sync issues in turn - //- trigger autozoom (in conjunction with preventAutozoomOnLoad) - justLoaded: boolean; - - //the modifyEventHandler in main.ts will fire when an Excalidraw file has changed (e.g. due to sync) - //when a drawing that is currently open in a view receives a sync update, excalidraw reload() is triggered - //the preventAutozoomOnLoad flag will prevent the open drawing from autozooming when it is reloaded - preventAutozoom: boolean; - - autosaving: boolean; //flags that autosaving is in progress. Autosave is an async timer, the flag prevents collision with force save - forceSaving: boolean; //flags that forcesaving is in progress. The flag prevents collision with autosaving - dirty: string; //null if there are no changes to be saved, the path of the file if the drawing has unsaved changes - - //reload() is triggered by modifyEventHandler in main.ts. preventReload is a one time flag to abort reloading - //to avoid interrupting the flow of drawing by the user. - preventReload: boolean; - - isEditingText: boolean; //https://stackoverflow.com/questions/27132796/is-there-any-javascript-event-fired-when-the-on-screen-keyboard-on-mobile-safari - - //Save is triggered by multiple threads when an Excalidraw pane is terminated - //- by the view itself - //- by the activeLeafChangeEventHandler change event handler - //- by monkeypatches on detach(next) - //This semaphore helps avoid collision of saves - saving: boolean; - hoverSleep: boolean; //flag with timer to prevent hover preview from being triggered dozens of times - wheelTimeout:number; //used to avoid hover preview while zooming - } | null = { - warnAboutLinearElementLinkClick: true, - embeddableIsEditingSelf: false, - popoutUnload: false, - viewunload: false, - scriptsReady: false, - justLoaded: false, - preventAutozoom: false, - autosaving: false, - dirty: null, - preventReload: false, - isEditingText: false, - saving: false, - forceSaving: false, - hoverSleep: false, - wheelTimeout: null, - }; - - public _plugin: ExcalidrawPlugin; - public autosaveTimer: any = null; - public textMode: TextMode = TextMode.raw; - private actionButtons: Record = {} as Record; - public compatibilityMode: boolean = false; - private obsidianMenu: ObsidianMenu; - private embeddableMenu: EmbeddableMenu; - private destroyers: Function[] = []; - - //https://stackoverflow.com/questions/27132796/is-there-any-javascript-event-fired-when-the-on-screen-keyboard-on-mobile-safari - private isEditingTextResetTimer: number = null; - private preventReloadResetTimer: number = null; - private editingSelfResetTimer: number = null; - private colorChangeTimer:number = null; - private previousSceneVersion = 0; - public previousBackgroundColor = ""; - public previousTheme = ""; - - //variables used to handle click events in view mode - private selectedTextElement: SelectedElementWithLink = null; - private selectedImageElement: SelectedImage = null; - private selectedElementWithLink: SelectedElementWithLink = null; - private blockOnMouseButtonDown = false; - private doubleClickTimestamp = Date.now(); - - private hoverPoint = { x: 0, y: 0 }; - private hoverPreviewTarget: EventTarget = null; - private viewModeEnabled:boolean = false; - private lastMouseEvent: any = null; - private editingTextElementId: string = null; //storing to handle on-screen keyboard hide events -/* private lastSceneSnapshot: any = null; - private lastViewDataSnapshot: any = null;*/ - - id: string = (this.leaf as any).id; - public packages: Packages = {react: null, reactDOM: null, excalidrawLib: null}; - - constructor(leaf: WorkspaceLeaf, plugin: ExcalidrawPlugin) { - super(leaf); - this._plugin = plugin; - this.excalidrawData = new ExcalidrawData(plugin); - this.canvasNodeFactory = new CanvasNodeFactory(this); - this.setHookServer(); - } - - get hookServer (): ExcalidrawAutomate { - return this._hookServer; - } - get plugin(): ExcalidrawPlugin { - return this._plugin; - } - get excalidrawContainer(): HTMLDivElement { - return this.excalidrawWrapperRef?.current?.firstElementChild; - } - get ownerDocument(): Document { - return DEVICE.isMobile?document:this.containerEl.ownerDocument; - } - get ownerWindow(): Window { - return this.ownerDocument.defaultView; - } - - setHookServer(ea?:ExcalidrawAutomate) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setHookServer, "ExcalidrawView.setHookServer", ea); - if(ea) { - this._hookServer = ea; - } else { - this._hookServer = this._plugin.ea; - } - } - - private getHookServer () { - return this.hookServer ?? this.plugin.ea; - } - - preventAutozoom() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.preventAutozoom, "ExcalidrawView.preventAutozoom"); - this.semaphores.preventAutozoom = true; - window.setTimeout(() => { - if(!this.semaphores) return; - this.semaphores.preventAutozoom = false; - }, 1500); - } - - public saveExcalidraw(scene?: any) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.saveExcalidraw, "ExcalidrawView.saveExcalidraw", scene); - if (!scene) { - if(!this.excalidrawAPI) { - return; - } - scene = this.getScene(); - } - const filepath = `${this.file.path.substring( - 0, - this.file.path.lastIndexOf(".md"), - )}.excalidraw`; - const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath)); - if (file && file instanceof TFile) { - this.app.vault.modify(file, JSON.stringify(scene, null, "\t")); - } else { - this.app.vault.create(filepath, JSON.stringify(scene, null, "\t")); - } - } - - public async exportExcalidraw(selectedOnly?: boolean) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.exportExcalidraw, "ExcalidrawView.exportExcalidraw", selectedOnly); - if (!this.excalidrawAPI || !this.file) { - return; - } - if (DEVICE.isMobile) { - const prompt = new Prompt( - this.app, - t("EXPORT_FILENAME_PROMPT"), - this.file.basename, - t("EXPORT_FILENAME_PROMPT_PLACEHOLDER"), - ); - prompt.openAndGetValue(async (filename: string) => { - if (!filename) { - return; - } - filename = `${filename}.excalidraw`; - const folderpath = splitFolderAndFilename(this.file.path).folderpath; - await checkAndCreateFolder(folderpath); //create folder if it does not exist - const fname = getNewUniqueFilepath( - this.app.vault, - filename, - folderpath, - ); - this.app.vault.create( - fname, - JSON.stringify(this.getScene(), null, "\t"), - ); - new Notice(`Exported to ${fname}`, 6000); - }); - return; - } - download( - "data:text/plain;charset=utf-8", - encodeURIComponent(JSON.stringify(this.getScene(selectedOnly), null, "\t")), - `${this.file.basename}.excalidraw`, - ); - } - - public async svg(scene: any, theme?:string, embedScene?: boolean, embedFont: boolean = false): Promise { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.svg, "ExcalidrawView.svg", scene, theme, embedScene); - const ed = this.exportDialog; - - const exportSettings: ExportSettings = { - withBackground: ed ? !ed.transparent : getWithBackground(this.plugin, this.file), - withTheme: true, - isMask: isMaskFile(this.plugin, this.file), - skipInliningFonts: !embedFont, - }; - - if(typeof embedScene === "undefined") { - embedScene = shouldEmbedScene(this.plugin, this.file); - } - - return await getSVG( - { - ...scene, - ...{ - appState: { - ...scene.appState, - theme: theme ?? (ed ? ed.theme : getExportTheme(this.plugin, this.file, scene.appState.theme)), - exportEmbedScene: typeof embedScene === "undefined" - ? (ed ? ed.embedScene : false) - : embedScene, - }, - }, - }, - exportSettings, - ed ? ed.padding : getExportPadding(this.plugin, this.file), - this.file, - ); - } - - public async saveSVG(scene?: any, embedScene?: boolean) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.saveSVG, "ExcalidrawView.saveSVG", scene, embedScene); - if (!scene) { - if (!this.excalidrawAPI) { - return false; - } - scene = this.getScene(); - } - - const exportImage = async (filepath:string, theme?:string) => { - const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath)); - - const svg = await this.svg(scene,theme, embedScene, true); - if (!svg) { - return; - } - //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2026 - const svgString = svg.outerHTML; - if (file && file instanceof TFile) { - await this.app.vault.modify(file, svgString); - } else { - await this.app.vault.create(filepath, svgString); - } - } - - if(this.plugin.settings.autoExportLightAndDark) { - await exportImage(getIMGFilename(this.file.path, "dark.svg"),"dark"); - await exportImage(getIMGFilename(this.file.path, "light.svg"),"light"); - } else { - await exportImage(getIMGFilename(this.file.path, "svg")); - } - } - - public async exportSVG(embedScene?: boolean, selectedOnly?: boolean):Promise { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.exportSVG, "ExcalidrawView.exportSVG", embedScene, selectedOnly); - if (!this.excalidrawAPI || !this.file) { - return; - } - - const svg = await this.svg(this.getScene(selectedOnly),undefined,embedScene, true); - if (!svg) { - return; - } - download( - null, - svgToBase64(svg.outerHTML), - `${this.file.basename}.svg`, - ); - } - - public async png(scene: any, theme?:string, embedScene?: boolean): Promise { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.png, "ExcalidrawView.png", scene, theme, embedScene); - const ed = this.exportDialog; - - const exportSettings: ExportSettings = { - withBackground: ed ? !ed.transparent : getWithBackground(this.plugin, this.file), - withTheme: true, - isMask: isMaskFile(this.plugin, this.file), - }; - - if(typeof embedScene === "undefined") { - embedScene = shouldEmbedScene(this.plugin, this.file); - } - - return await getPNG( - { - ...scene, - ...{ - appState: { - ...scene.appState, - theme: theme ?? (ed ? ed.theme : getExportTheme(this.plugin, this.file, scene.appState.theme)), - exportEmbedScene: typeof embedScene === "undefined" - ? (ed ? ed.embedScene : false) - : embedScene, - }, - }, - }, - exportSettings, - ed ? ed.padding : getExportPadding(this.plugin, this.file), - ed ? ed.scale : getPNGScale(this.plugin, this.file), - ); - } - - public async savePNG(scene?: any, embedScene?: boolean) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.savePNG, "ExcalidrawView.savePNG", scene, embedScene); - if (!scene) { - if (!this.excalidrawAPI) { - return false; - } - scene = this.getScene(); - } - - const exportImage = async (filepath:string, theme?:string) => { - const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath)); - - const png = await this.png(scene, theme, embedScene); - if (!png) { - return; - } - if (file && file instanceof TFile) { - await this.app.vault.modifyBinary(file, await png.arrayBuffer()); - } else { - await this.app.vault.createBinary(filepath, await png.arrayBuffer()); - } - } - - if(this.plugin.settings.autoExportLightAndDark) { - await exportImage(getIMGFilename(this.file.path, "dark.png"),"dark"); - await exportImage(getIMGFilename(this.file.path, "light.png"),"light"); - } else { - await exportImage(getIMGFilename(this.file.path, "png")); - } - } - - public async exportPNGToClipboard(embedScene?:boolean, selectedOnly?: boolean) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.exportPNGToClipboard, "ExcalidrawView.exportPNGToClipboard", embedScene, selectedOnly); - if (!this.excalidrawAPI || !this.file) { - return; - } - - const png = await this.png(this.getScene(selectedOnly), undefined, embedScene); - if (!png) { - return; - } - - // in Safari so far we need to construct the ClipboardItem synchronously - // (i.e. in the same tick) otherwise browser will complain for lack of - // user intent. Using a Promise ClipboardItem constructor solves this. - // https://bugs.webkit.org/show_bug.cgi?id=222262 - // - // not await so that we can detect whether the thrown error likely relates - // to a lack of support for the Promise ClipboardItem constructor - await navigator.clipboard.write([ - new window.ClipboardItem({ - "image/png": png, - }), - ]); - } - - public async exportPNG(embedScene?:boolean, selectedOnly?: boolean):Promise { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.exportPNG, "ExcalidrawView.exportPNG", embedScene, selectedOnly); - if (!this.excalidrawAPI || !this.file) { - return; - } - - const png = await this.png(this.getScene(selectedOnly), undefined, embedScene); - if (!png) { - return; - } - const reader = new FileReader(); - reader.readAsDataURL(png); - reader.onloadend = () => { - const base64data = reader.result; - download(null, base64data, `${this.file.basename}.png`); - }; - } - - public setPreventReload() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setPreventReload, "ExcalidrawView.setPreventReload"); - this.semaphores.preventReload = true; - this.preventReloadResetTimer = window.setTimeout(()=>this.semaphores.preventReload = false,PREVENT_RELOAD_TIMEOUT); - } - - public clearPreventReloadTimer() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearPreventReloadTimer, "ExcalidrawView.clearPreventReloadTimer"); - if(this.preventReloadResetTimer) { - window.clearTimeout(this.preventReloadResetTimer); - this.preventReloadResetTimer = null; - } - } - - public async setEmbeddableNodeIsEditing() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setEmbeddableNodeIsEditing, "ExcalidrawView.setEmbeddableNodeIsEditing"); - this.clearEmbeddableNodeIsEditingTimer(); - await this.forceSave(true); - this.semaphores.embeddableIsEditingSelf = true; - } - - public clearEmbeddableNodeIsEditingTimer () { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearEmbeddableNodeIsEditingTimer, "ExcalidrawView.clearEmbeddableNodeIsEditingTimer"); - if(this.editingSelfResetTimer) { - window.clearTimeout(this.editingSelfResetTimer); - this.editingSelfResetTimer = null; - } - } - - public clearEmbeddableNodeIsEditing() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearEmbeddableNodeIsEditing, "ExcalidrawView.clearEmbeddableNodeIsEditing"); - this.clearEmbeddableNodeIsEditingTimer(); - this.editingSelfResetTimer = window.setTimeout(()=>this.semaphores.embeddableIsEditingSelf = false,EMBEDDABLE_SEMAPHORE_TIMEOUT); - } - - async save(preventReload: boolean = true, forcesave: boolean = false, overrideEmbeddableIsEditingSelfDebounce: boolean = false) { - if ((process.env.NODE_ENV === 'development')) { - if (DEBUGGING) { - debug(this.save, "ExcalidrawView.save, enter", preventReload, forcesave); - console.trace(); - } - } - /*if(this.semaphores.viewunload && (this.ownerWindow !== window)) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.save, `ExcalidrawView.save, view is unloading, aborting save`); - return; - }*/ - - if(!this.isLoaded) { - return; - } - if (!overrideEmbeddableIsEditingSelfDebounce && this.semaphores.embeddableIsEditingSelf) { - return; - } - //console.log("saving - embeddable not editing") - //debug({where:"save", preventReload, forcesave, semaphores:this.semaphores}); - if (this.semaphores.saving) { - return; - } - this.semaphores.saving = true; - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.save, `ExcalidrawView.save, saving, dirty:${this.isDirty()}, preventReload:${preventReload}, forcesave:${forcesave}`); - - //if there were no changes to the file super save will not save - //and consequently main.ts modifyEventHandler will not fire - //this.reload will not be called - //triggerReload is used to flag if there were no changes but file should be reloaded anyway - let triggerReload:boolean = false; - - if ( - !this.excalidrawAPI || - !this.isLoaded || - !this.file || - !this.app.vault.getAbstractFileByPath(this.file.path) //file was recently deleted - ) { - this.semaphores.saving = false; - return; - } - - const allowSave = this.isDirty() || forcesave; //removed this.semaphores.autosaving - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.save, `ExcalidrawView.save, try saving, allowSave:${allowSave}, isDirty:${this.isDirty()}, isAutosaving:${this.semaphores.autosaving}, isForceSaving:${forcesave}`); - try { - if (allowSave) { - const scene = this.getScene(); - - if (this.compatibilityMode) { - await this.excalidrawData.syncElements(scene); - } else if ( - await this.excalidrawData.syncElements(scene, this.excalidrawAPI.getAppState().selectedElementIds) - && !this.semaphores.popoutUnload //Obsidian going black after REACT 18 migration when closing last leaf on popout - ) { - await this.loadDrawing( - false, - this.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted) - ); - } - - //reload() is triggered indirectly when saving by the modifyEventHandler in main.ts - //prevent reload is set here to override reload when not wanted: typically when the user is editing - //and we do not want to interrupt the flow by reloading the drawing into the canvas. - this.clearDirty(); - this.clearPreventReloadTimer(); - - this.semaphores.preventReload = preventReload; - await this.prepareGetViewData(); - - //added this to avoid Electron crash when terminating a popout window and saving the drawing, need to check back - //can likely be removed once this is resolved: https://github.com/electron/electron/issues/40607 - if(this.semaphores?.viewunload) { - await this.prepareGetViewData(); - const d = this.getViewData(); - const plugin = this.plugin; - const file = this.file; - window.setTimeout(async ()=>{ - await plugin.app.vault.modify(file,d); - await imageCache.addBAKToCache(file.path,d); - },200) - this.semaphores.saving = false; - return; - } - - await super.save(); - if (process.env.NODE_ENV === 'development') { - if (DEBUGGING) { - debug(this.save, `ExcalidrawView.save, super.save finished`, this.file); - console.trace(); - } - } - //saving to backup with a delay in case application closes in the meantime, I want to avoid both save and backup corrupted. - const path = this.file.path; - const data = this.lastSavedData; - window.setTimeout(()=>imageCache.addBAKToCache(path,data),50); - triggerReload = (this.lastSaveTimestamp === this.file.stat.mtime) && - !preventReload && forcesave; - this.lastSaveTimestamp = this.file.stat.mtime; - //this.clearDirty(); //moved to right after allow save, to avoid autosave collision with load drawing - - //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/629 - //there were odd cases when preventReload semaphore did not get cleared and consequently a synchronized image - //did not update the open drawing - if(preventReload) { - this.setPreventReload(); - } - } - - // !triggerReload means file has not changed. No need to re-export - //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1209 (added popout unload to the condition) - if (!triggerReload && !this.semaphores.autosaving && (!this.semaphores.viewunload || this.semaphores.popoutUnload)) { - const autoexportPreference = this.excalidrawData.autoexportPreference; - if ( - (autoexportPreference === AutoexportPreference.inherit && this.plugin.settings.autoexportSVG) || - autoexportPreference === AutoexportPreference.both || autoexportPreference === AutoexportPreference.svg - ) { - this.saveSVG(); - } - if ( - (autoexportPreference === AutoexportPreference.inherit && this.plugin.settings.autoexportPNG) || - autoexportPreference === AutoexportPreference.both || autoexportPreference === AutoexportPreference.png - ) { - this.savePNG(); - } - if ( - !this.compatibilityMode && - this.plugin.settings.autoexportExcalidraw - ) { - this.saveExcalidraw(); - } - } - } catch (e) { - errorlog({ - where: "ExcalidrawView.save", - fn: this.save, - error: e, - }); - warningUnknowSeriousError(); - } - this.semaphores.saving = false; - if(triggerReload) { - this.reload(true, this.file); - } - this.resetAutosaveTimer(); //next autosave period starts after save - } - - // get the new file content - // if drawing is in Text Element Edit Lock, then everything should be parsed and in sync - // if drawing is in Text Element Edit Unlock, then everything is raw and parse and so an async function is not required here - /** - * I moved the logic from getViewData to prepareGetViewData because getViewData is Sync and prepareGetViewData is async - * prepareGetViewData is async because of moving compression to a worker thread in 2.4.0 - */ - private viewSaveData: string = ""; - - async prepareGetViewData(): Promise { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.prepareGetViewData, "ExcalidrawView.prepareGetViewData"); - if (!this.excalidrawAPI || !this.excalidrawData.loaded) { - this.viewSaveData = this.data; - return; - } - - const scene = this.getScene(); - if(!scene) { - this.viewSaveData = this.data; - return; - } - - //include deleted elements in save in case saving in markdown mode - //deleted elements are only used if sync modifies files while Excalidraw is open - //otherwise deleted elements are discarded when loading the scene - if (!this.compatibilityMode) { - - const keys:[string,string][] = this.exportDialog?.dirty && this.exportDialog?.saveSettings - ? [ - [FRONTMATTER_KEYS["export-padding"].name, this.exportDialog.padding.toString()], - [FRONTMATTER_KEYS["export-pngscale"].name, this.exportDialog.scale.toString()], - [FRONTMATTER_KEYS["export-dark"].name, this.exportDialog.theme === "dark" ? "true" : "false"], - [FRONTMATTER_KEYS["export-transparent"].name, this.exportDialog.transparent ? "true" : "false"], - [FRONTMATTER_KEYS["plugin"].name, this.textMode === TextMode.raw ? "raw" : "parsed"], - [FRONTMATTER_KEYS["export-embed-scene"].name, this.exportDialog.embedScene ? "true" : "false"], - ] - : [ - [FRONTMATTER_KEYS["plugin"].name, this.textMode === TextMode.raw ? "raw" : "parsed"] - ]; - - if(this.exportDialog?.dirty) { - this.exportDialog.dirty = false; - } - - const header = getExcalidrawMarkdownHeaderSection(this.data, keys); - const tail = this.plugin.settings.zoteroCompatibility ? (RE_TAIL.exec(this.data)?.[1] ?? "") : ""; - - if (!this.excalidrawData.disableCompression) { - this.excalidrawData.disableCompression = this.plugin.settings.decompressForMDView && - this.isEditedAsMarkdownInOtherView(); - } - const result = IS_WORKER_SUPPORTED - ? (header + (await this.excalidrawData.generateMDAsync( - this.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted) //will be concatenated to scene.elements - )) + tail) - : (header + (this.excalidrawData.generateMDSync( - this.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted) //will be concatenated to scene.elements - )) + tail) - - this.excalidrawData.disableCompression = false; - this.viewSaveData = result; - return; - } - if (this.compatibilityMode) { - this.viewSaveData = JSON.stringify(scene, null, "\t"); - return; - } - - this.viewSaveData = this.data; - return; - } - - getViewData() { - return this.viewSaveData ?? this.data; - } - - private hiddenMobileLeaves:[WorkspaceLeaf,string][] = []; - - restoreMobileLeaves() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.restoreMobileLeaves, "ExcalidrawView.restoreMobileLeaves"); - if(this.hiddenMobileLeaves.length>0) { - this.hiddenMobileLeaves.forEach((x:[WorkspaceLeaf,string])=>{ - x[0].containerEl.style.display = x[1]; - }) - this.hiddenMobileLeaves = []; - } - } - - async openLaTeXEditor(eqId: string) { - if(await this.excalidrawData.syncElements(this.getScene())) { - //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1994 - await this.forceSave(true); - } - const el = this.getViewElements().find((el:ExcalidrawElement)=>el.id === eqId && el.type==="image") as ExcalidrawImageElement; - if(!el) { - return; - } - - const fileId = el.fileId; - - let equation = this.excalidrawData.getEquation(fileId)?.latex; - if(!equation) { - await this.save(false); - equation = this.excalidrawData.getEquation(fileId)?.latex; - if(!equation) return; - } - - GenericInputPrompt.Prompt(this,this.plugin,this.app,t("ENTER_LATEX"),undefined,equation, undefined, 3).then(async (formula: string) => { - if (!formula || formula === equation) { - return; - } - this.excalidrawData.setEquation(fileId, { - latex: formula, - isLoaded: false, - }); - await this.save(false); - await updateEquation( - formula, - fileId, - this, - addFiles, - ); - this.setDirty(1); - }); - } - - async openEmbeddedLinkEditor(imgId:string) { - const el = this.getViewElements().find((el:ExcalidrawElement)=>el.id === imgId && el.type==="image") as ExcalidrawImageElement; - if(!el) { - return; - } - const fileId = el.fileId; - const ef = this.excalidrawData.getFile(fileId); - if(!ef) { - return - } - if (!ef.isHyperLink && !ef.isLocalLink && ef.file) { - const handler = async (link:string) => { - if (!link || ef.linkParts.original === link) { - return; - } - ef.resetImage(this.file.path, link); - this.excalidrawData.setFile(fileId, ef); - this.setDirty(2); - await this.save(false); - await sleep(100); - if(!this.plugin.isExcalidrawFile(ef.file) && !link.endsWith("|100%")) { - const ea = getEA(this) as ExcalidrawAutomate; - let imgEl = this.getViewElements().find((x:ExcalidrawElement)=>x.id === el.id) as ExcalidrawImageElement; - if(!imgEl) { - ea.destroy(); - return; - } - if(imgEl && await ea.resetImageAspectRatio(imgEl)) { - await ea.addElementsToView(false); - } - ea.destroy(); - } - } - GenericInputPrompt.Prompt( - this, - this.plugin, - this.app, - t("MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE"), - undefined, - ef.linkParts.original, - [{caption: "✅", action: (x:string)=>{x.replaceAll("\n","").trim()}}], - 3, - false, - (container) => container.createEl("p",{text: fragWithHTML(t("MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT"))}), - false - ).then(handler.bind(this),()=>{}); - return; - } - } - - toggleDisableBinding() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.toggleDisableBinding, "ExcalidrawView.toggleDisableBinding"); - const newState = !this.excalidrawAPI.getAppState().invertBindingBehaviour; - this.updateScene({appState: {invertBindingBehaviour:newState}, storeAction: "update"}); - new Notice(newState ? t("ARROW_BINDING_INVERSE_MODE") : t("ARROW_BINDING_NORMAL_MODE")); - } - - toggleFrameRendering() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.toggleFrameRendering, "ExcalidrawView.toggleFrameRendering"); - const frameRenderingSt = (this.excalidrawAPI as ExcalidrawImperativeAPI).getAppState().frameRendering; - this.updateScene({appState: {frameRendering: {...frameRenderingSt, enabled: !frameRenderingSt.enabled}}, storeAction: "update"}); - new Notice(frameRenderingSt.enabled ? t("FRAME_CLIPPING_ENABLED") : t("FRAME_CLIPPING_DISABLED")); - } - - toggleFrameClipping() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.toggleFrameClipping, "ExcalidrawView.toggleFrameClipping"); - const frameRenderingSt = (this.excalidrawAPI as ExcalidrawImperativeAPI).getAppState().frameRendering; - this.updateScene({appState: {frameRendering: {...frameRenderingSt, clip: !frameRenderingSt.clip}}, storeAction: "update"}); - new Notice(frameRenderingSt.clip ? "Frame Clipping: Enabled" : "Frame Clipping: Disabled"); - } - - gotoFullscreen() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.gotoFullscreen, "ExcalidrawView.gotoFullscreen"); - if(this.plugin.leafChangeTimeout) { - window.clearTimeout(this.plugin.leafChangeTimeout); //leafChangeTimeout is created on window in main.ts!!! - this.plugin.clearLeafChangeTimeout(); - } - if (!this.excalidrawWrapperRef) { - return; - } - if (this.toolsPanelRef && this.toolsPanelRef.current) { - this.toolsPanelRef.current.setFullscreen(true); - } - - const hide = (el:HTMLElement) => { - let tmpEl = el; - while(tmpEl && !tmpEl.hasClass("workspace-split")) { - el.addClass(SHOW); - el = tmpEl; - tmpEl = el.parentElement; - } - if(el) { - el.addClass(SHOW); - el.querySelectorAll(`div.workspace-split:not(.${SHOW})`).forEach(node=>{ - if(node !== el) node.addClass(SHOW); - }); - el.querySelector(`div.workspace-leaf-content.${SHOW} > .view-header`).addClass(SHOW); - el.querySelectorAll(`div.workspace-tab-container.${SHOW} > div.workspace-leaf:not(.${SHOW})`).forEach(node=>node.addClass(SHOW)); - el.querySelectorAll(`div.workspace-tabs.${SHOW} > div.workspace-tab-header-container`).forEach(node=>node.addClass(SHOW)); - el.querySelectorAll(`div.workspace-split.${SHOW} > div.workspace-tabs:not(.${SHOW})`).forEach(node=>node.addClass(SHOW)); - } - const doc = this.ownerDocument; - doc.body.querySelectorAll(`div.workspace-split:not(.${SHOW})`).forEach(node=>{ - if(node !== tmpEl) { - node.addClass(HIDE); - } else { - node.addClass(SHOW); - } - }); - doc.body.querySelector(`div.workspace-leaf-content.${SHOW} > .view-header`).addClass(HIDE); - doc.body.querySelectorAll(`div.workspace-tab-container.${SHOW} > div.workspace-leaf:not(.${SHOW})`).forEach(node=>node.addClass(HIDE)); - doc.body.querySelectorAll(`div.workspace-tabs.${SHOW} > div.workspace-tab-header-container`).forEach(node=>node.addClass(HIDE)); - doc.body.querySelectorAll(`div.workspace-split.${SHOW} > div.workspace-tabs:not(.${SHOW})`).forEach(node=>node.addClass(HIDE)); - doc.body.querySelectorAll(`div.workspace-ribbon`).forEach(node=>node.addClass(HIDE)); - doc.body.querySelectorAll(`div.mobile-navbar`).forEach(node=>node.addClass(HIDE)); - doc.body.querySelectorAll(`div.status-bar`).forEach(node=>node.addClass(HIDE)); - } - - hide(this.contentEl); - } - - - isFullscreen(): boolean { - //(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.isFullscreen, "ExcalidrawView.isFullscreen"); - return Boolean(document.body.querySelector(".excalidraw-hidden")); - } - - exitFullscreen() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.exitFullscreen, "ExcalidrawView.exitFullscreen"); - if (this.toolsPanelRef && this.toolsPanelRef.current) { - this.toolsPanelRef.current.setFullscreen(false); - } - const doc = this.ownerDocument; - doc.querySelectorAll(".excalidraw-hidden").forEach(el=>el.removeClass(HIDE)); - doc.querySelectorAll(".excalidraw-visible").forEach(el=>el.removeClass(SHOW)); - } - - removeLinkTooltip() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.removeLinkTooltip, "ExcalidrawView.removeLinkTooltip"); - //.classList.remove("excalidraw-tooltip--visible");document.querySelector(".excalidraw-tooltip",); - const tooltip = this.ownerDocument.body.querySelector( - "body>div.excalidraw-tooltip,div.excalidraw-tooltip--visible", - ); - if (tooltip) { - tooltip.classList.remove("excalidraw-tooltip--visible") - //this.ownerDocument.body.removeChild(tooltip); - } - } - - handleLinkHookCall(element:ExcalidrawElement,link:string, event:any):boolean { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.handleLinkHookCall, "ExcalidrawView.handleLinkHookCall", element, link, event); - if(this.getHookServer().onLinkClickHook) { - try { - if(!this.getHookServer().onLinkClickHook( - element, - link, - event, - this, - this.getHookServer() - )) { - return true; - } - } catch (e) { - errorlog({where: "ExcalidrawView.onLinkOpen", fn: this.getHookServer().onLinkClickHook, error: e}); - } - } - return false; - } - - private getLinkTextForElement( - selectedText:SelectedElementWithLink, - selectedElementWithLink?:SelectedElementWithLink, - allowLinearElementClick: boolean = false, - ): { - linkText: string, - selectedElement: ExcalidrawElement, - isLinearElement: boolean, - } { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getLinkTextForElement, "ExcalidrawView.getLinkTextForElement", selectedText, selectedElementWithLink); - if (selectedText?.id || selectedElementWithLink?.id) { - let selectedTextElement: ExcalidrawTextElement = selectedText.id - ? this.excalidrawAPI.getSceneElements().find((el:ExcalidrawElement)=>el.id === selectedText.id) - : null; - - let selectedElement = selectedElementWithLink.id - ? this.excalidrawAPI.getSceneElements().find((el:ExcalidrawElement)=> - el.id === selectedElementWithLink.id) - : null; - - //if the user clicked on the label of an arrow then the label will be captured in selectedElement, because - //Excalidraw returns the container as the selected element. But in this case we want this to be treated as the - //text element, as the assumption is, if the user wants to invoke the linear element editor for an arrow that has - //a label with a link, then he/she should rather CTRL+click on the arrow line, not the label. CTRL+Click on - //the label is an indication of wanting to navigate. - if (!Boolean(selectedTextElement) && selectedElement?.type === "text") { - const container = getContainerElement(selectedElement, arrayToMap(this.excalidrawAPI.getSceneElements())); - if(container?.type === "arrow") { - const x = getTextElementAtPointer(this.currentPosition,this); - if(x?.id === selectedElement.id) { - selectedTextElement = selectedElement; - selectedElement = null; - } - } - } - - //CTRL click on a linear element with a link will navigate instead of line editor - if(!allowLinearElementClick && ["arrow", "line"].includes(selectedElement?.type)) { - return {linkText: selectedElement.link, selectedElement: selectedElement, isLinearElement: true}; - } - - if (!selectedTextElement && selectedElement?.type === "text") { - if(!allowLinearElementClick) { - //CTRL click on a linear element with a link will navigate instead of line editor - const container = getContainerElement(selectedElement, arrayToMap(this.excalidrawAPI.getSceneElements())); - if(container?.type !== "arrow") { - selectedTextElement = selectedElement as ExcalidrawTextElement; - selectedElement = null; - } else { - const x = this.processLinkText(selectedElement.rawText, selectedElement as ExcalidrawTextElement, container, false); - return {linkText: x.linkText, selectedElement: container, isLinearElement: true}; - } - } else { - selectedTextElement = selectedElement as ExcalidrawTextElement; - selectedElement = null; - } - } - - let linkText = - selectedElementWithLink?.text ?? - (this.textMode === TextMode.parsed - ? this.excalidrawData.getRawText(selectedText.id) - : selectedText.text); - - return {...this.processLinkText(linkText, selectedTextElement, selectedElement), isLinearElement: false}; - } - return {linkText: null, selectedElement: null, isLinearElement: false}; - } - - - processLinkText(linkText: string, selectedTextElement: ExcalidrawTextElement, selectedElement: ExcalidrawElement, shouldOpenLink: boolean = true) { - if(!linkText) { - return {linkText: null, selectedElement: null}; - } - - if(linkText.startsWith("#")) { - return {linkText, selectedElement: selectedTextElement ?? selectedElement}; - } - - const maybeObsidianLink = parseObsidianLink(linkText, this.app, shouldOpenLink); - if(typeof maybeObsidianLink === "string") { - linkText = maybeObsidianLink; - } - - const partsArray = REGEX_LINK.getResList(linkText); - if (!linkText || partsArray.length === 0) { - //the container link takes precedence over the text link - if(selectedTextElement?.containerId) { - const container = _getContainerElement(selectedTextElement, {elements: this.excalidrawAPI.getSceneElements()}); - if(container) { - linkText = container.link; - - if(linkText?.startsWith("#")) { - return {linkText, selectedElement: selectedTextElement ?? selectedElement}; - } - - const maybeObsidianLink = parseObsidianLink(linkText, this.app, shouldOpenLink); - if(typeof maybeObsidianLink === "string") { - linkText = maybeObsidianLink; - } - } - } - if(!linkText || partsArray.length === 0) { - linkText = selectedTextElement?.link; - } - } - return {linkText, selectedElement: selectedTextElement ?? selectedElement}; - } - - async linkClick( - ev: MouseEvent | null, - selectedText: SelectedElementWithLink, - selectedImage: SelectedImage, - selectedElementWithLink: SelectedElementWithLink, - keys?: ModifierKeys, - allowLinearElementClick: boolean = false, - ) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.linkClick, "ExcalidrawView.linkClick", ev, selectedText, selectedImage, selectedElementWithLink, keys); - if(!selectedText) selectedText = {id:null, text: null}; - if(!selectedImage) selectedImage = {id:null, fileId: null}; - if(!selectedElementWithLink) selectedElementWithLink = {id:null, text:null}; - if(!ev && !keys) keys = emulateKeysForLinkClick("new-tab"); - if( ev && !keys) keys = {shiftKey: ev.shiftKey, ctrlKey: ev.ctrlKey, metaKey: ev.metaKey, altKey: ev.altKey}; - - const linkClickType = linkClickModifierType(keys); - - let file = null; - let subpath: string = null; - let {linkText, selectedElement, isLinearElement} = this.getLinkTextForElement(selectedText, selectedElementWithLink, allowLinearElementClick); - - //if (selectedText?.id || selectedElementWithLink?.id) { - if (selectedElement) { - if (!allowLinearElementClick && linkText && isLinearElement) { - if(this.semaphores.warnAboutLinearElementLinkClick) { - new Notice(t("LINEAR_ELEMENT_LINK_CLICK_ERROR"), 20000); - this.semaphores.warnAboutLinearElementLinkClick = false; - } - return; - } - if (!linkText) { - return; - } - linkText = linkText.replaceAll("\n", ""); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187 - - if(this.handleLinkHookCall(selectedElement,linkText,ev)) return; - if(openExternalLink(linkText, this.app)) return; - - const maybeObsidianLink = parseObsidianLink(linkText,this.app); - if (typeof maybeObsidianLink === "boolean" && maybeObsidianLink) return; - if (typeof maybeObsidianLink === "string") { - linkText = maybeObsidianLink; - } - - const result = await linkPrompt(linkText, this.app, this); - if(!result) return; - [file, linkText, subpath] = result; - } - if (selectedImage?.id) { - const imageElement = this.getScene().elements.find((el:ExcalidrawElement)=>el.id === selectedImage.id) as ExcalidrawImageElement; - if (this.excalidrawData.hasEquation(selectedImage.fileId)) { - this.updateScene({appState: {contextMenu: null}}); - this.openLaTeXEditor(selectedImage.id); - return; - } - if (this.excalidrawData.hasMermaid(selectedImage.fileId) || getMermaidText(imageElement)) { - if(shouldRenderMermaid) { - const api = this.excalidrawAPI as ExcalidrawImperativeAPI; - api.updateScene({appState: {openDialog: { name: "ttd", tab: "mermaid" }}, storeAction: "update"}) - } - return; - } - - await this.save(false); //in case pasted images haven't been saved yet - if (this.excalidrawData.hasFile(selectedImage.fileId)) { - const fileId = selectedImage.fileId; - const ef = this.excalidrawData.getFile(fileId); - if (!ef.isHyperLink && !ef.isLocalLink && ef.file && linkClickType === "md-properties") { - this.updateScene({appState: {contextMenu: null}}); - this.openEmbeddedLinkEditor(selectedImage.id); - return; - } - let secondOrderLinks: string = " "; - - const backlinks = this.app.metadataCache?.getBacklinksForFile(ef.file)?.data; - const secondOrderLinksSet = new Set(); - if(backlinks && this.plugin.settings.showSecondOrderLinks) { - const linkPaths = Object.keys(backlinks) - .filter(path => (path !== this.file.path) && (path !== ef.file.path)) - .map(path => { - const filepathParts = splitFolderAndFilename(path); - if(secondOrderLinksSet.has(path)) return ""; - secondOrderLinksSet.add(path); - return `[[${path}|${t("LINKLIST_SECOND_ORDER_LINK")}: ${filepathParts.basename}]]`; - }); - secondOrderLinks += linkPaths.join(" "); - } - - if(this.plugin.settings.showSecondOrderLinks && this.plugin.isExcalidrawFile(ef.file)) { - secondOrderLinks += getExcalidrawFileForwardLinks(this.app, ef.file, secondOrderLinksSet); - } - - const linkString = (ef.isHyperLink || ef.isLocalLink - ? `[](${ef.hyperlink}) ` - : `[[${ef.linkParts.original}]] ` - ) + (imageElement.link - ? (imageElement.link.match(/$cmd:\/\/.*/) || imageElement.link.match(REG_LINKINDEX_HYPERLINK)) - ? `[](${imageElement.link})` - : imageElement.link - : ""); - - const result = await linkPrompt(linkString + secondOrderLinks, this.app, this); - if(!result) return; - [file, linkText, subpath] = result; - } - } - - if (!linkText) { - if(allowLinearElementClick) { - return; - } - new Notice(t("LINK_BUTTON_CLICK_NO_TEXT"), 20000); - return; - } - - const id = selectedImage.id??selectedText.id??selectedElementWithLink.id; - const el = this.excalidrawAPI.getSceneElements().filter((el:ExcalidrawElement)=>el.id === id)[0]; - if(this.handleLinkHookCall(el,linkText,ev)) return; - - try { - if (linkClickType !== "active-pane" && this.isFullscreen()) { - this.exitFullscreen(); - } - if (!file) { - new NewFileActions({ - plugin: this.plugin, - path: linkText, - keys, - view: this, - sourceElement: el - }).open(); - return; - } - if(this.linksAlwaysOpenInANewPane && !anyModifierKeysPressed(keys)) { - keys = emulateKeysForLinkClick("new-pane"); - } - - try { - const drawIO = this.app.plugins.plugins["drawio-obsidian"]; - if(drawIO && drawIO._loaded) { - if(file.extension === "svg") { - const svg = await this.app.vault.cachedRead(file); - if(/(<|\<)(mxfile|mxgraph)/i.test(svg)) { - const leaf = getLeaf(this.plugin,this.leaf,keys); - leaf.setViewState({ - type: "diagram-edit", - state: { - file: file.path - } - }); - return; - } - } - } - } catch(e) { - console.error(e); - } - - //if link will open in the same pane I want to save the drawing before opening the link - await this.forceSaveIfRequired(); - const { promise } = openLeaf({ - plugin: this.plugin, - fnGetLeaf: () => getLeaf(this.plugin,this.leaf,keys), - file, - openState: { - active: !this.linksAlwaysOpenInANewPane, - ...subpath ? { eState: { subpath } } : {} - } - }); //if file exists open file and jump to reference - await promise; - //view.app.workspace.setActiveLeaf(leaf, true, true); //0.15.4 ExcaliBrain focus issue - } catch (e) { - new Notice(e, 4000); - } - } - - async handleLinkClick(ev: MouseEvent | ModifierKeys, allowLinearElementClick: boolean = false) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.handleLinkClick, "ExcalidrawView.handleLinkClick", ev); - this.removeLinkTooltip(); - - const selectedText = this.getSelectedTextElement(); - const selectedImage = selectedText?.id - ? null - : this.getSelectedImageElement(); - const selectedElementWithLink = - (selectedImage?.id || selectedText?.id) - ? null - : this.getSelectedElementWithLink(); - this.linkClick( - ev instanceof MouseEvent ? ev : null, - selectedText, - selectedImage, - selectedElementWithLink, - ev instanceof MouseEvent ? null : ev, - allowLinearElementClick, - ); - } - - onResize() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onResize, "ExcalidrawView.onResize"); - super.onResize(); - if(this.plugin.leafChangeTimeout) return; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/723 - const api = this.excalidrawAPI; - if ( - !this.plugin.settings.zoomToFitOnResize || - !this.excalidrawAPI || - this.semaphores.isEditingText || - !api - ) { - return; - } - - //final fallback to prevent resizing when text element is in edit mode - //this is to prevent jumping text due to on-screen keyboard popup - if (api.getAppState()?.editingTextElement) { - return; - } - this.zoomToFit(false); - } - - excalidrawGetSceneVersion: (elements: ExcalidrawElement[]) => number; - getSceneVersion (elements: ExcalidrawElement[]):number { - if(!this.excalidrawGetSceneVersion) { - this.excalidrawGetSceneVersion = this.packages.excalidrawLib.getSceneVersion; - } - return this.excalidrawGetSceneVersion(elements.filter(el=>!el.isDeleted)); - } - - public async forceSave(silent:boolean=false) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.forceSave, "ExcalidrawView.forceSave"); - if (this.semaphores.autosaving || this.semaphores.saving) { - if(!silent) new Notice(t("FORCE_SAVE_ABORTED")) - return; - } - if(this.preventReloadResetTimer) { - window.clearTimeout(this.preventReloadResetTimer); - this.preventReloadResetTimer = null; - } - this.semaphores.preventReload = false; - this.semaphores.forceSaving = true; - await this.save(false, true, true); - this.plugin.triggerEmbedUpdates(); - this.loadSceneFiles(); - this.semaphores.forceSaving = false; - if(!silent) new Notice("Save successful", 1000); - } - - onload() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onload, "ExcalidrawView.onload"); - if(this.plugin.settings.overrideObsidianFontSize) { - document.documentElement.style.fontSize = ""; - } - - const apiMissing = Boolean(typeof this.containerEl.onWindowMigrated === "undefined") - this.packages = this.plugin.getPackage(this.ownerWindow); - - if(DEVICE.isDesktop && !apiMissing) { - this.destroyers.push( - //this.containerEl.onWindowMigrated(this.leaf.rebuildView.bind(this)) - this.containerEl.onWindowMigrated(async() => { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onload, "ExcalidrawView.onWindowMigrated"); - const f = this.file; - const l = this.leaf; - await closeLeafView(l); - windowMigratedDisableZoomOnce = true; - l.setViewState({ - type: VIEW_TYPE_EXCALIDRAW, - state: { - file: f.path, - } - }); - }) - ); - } - - this.semaphores.scriptsReady = true; - - const wheelEvent = (ev:WheelEvent) => { - if(this.semaphores.wheelTimeout) window.clearTimeout(this.semaphores.wheelTimeout); - if(this.semaphores.hoverSleep && this.excalidrawAPI) this.clearHoverPreview(); - this.semaphores.wheelTimeout = window.setTimeout(()=>{ - window.clearTimeout(this.semaphores.wheelTimeout); - this.semaphores.wheelTimeout = null; - },1000); - } - - this.registerDomEvent(this.containerEl,"wheel",wheelEvent, {passive: false}); - - this.actionButtons['scriptInstall'] = this.addAction(SCRIPTENGINE_ICON_NAME, t("INSTALL_SCRIPT_BUTTON"), () => { - new ScriptInstallPrompt(this.plugin).open(); - }); - - this.actionButtons['save'] = this.addAction( - DISK_ICON_NAME, - t("FORCE_SAVE"), - async () => this.forceSave(), - ); - - this.actionButtons['isRaw'] = this.addAction( - TEXT_DISPLAY_RAW_ICON_NAME, - t("RAW"), - () => this.changeTextMode(TextMode.parsed), - ); - this.actionButtons['isParsed'] = this.addAction( - TEXT_DISPLAY_PARSED_ICON_NAME, - t("PARSED"), - () => this.changeTextMode(TextMode.raw), - ); - - this.actionButtons['link'] = this.addAction("link", t("OPEN_LINK"), (ev) => - this.handleLinkClick(ev), - ); - - this.registerDomEvent(this.ownerWindow, "resize", this.onExcalidrawResize.bind(this)); - - this.app.workspace.onLayoutReady(async () => { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onload,`ExcalidrawView.onload > app.workspace.onLayoutReady, file:${this.file?.name}, isActiveLeaf:${this?.app?.workspace?.activeLeaf === this.leaf}, is activeExcalidrawView set:${Boolean(this?.plugin?.activeExcalidrawView)}`); - //Leaf was moved to new window and ExcalidrawView was destructed. - //Happens during Obsidian startup if View opens in new window. - if(!this.plugin) { - return; - } - await this.plugin.awaitInit(); - //implemented to overcome issue that activeLeafChangeEventHandler is not called when view is initialized from a saved workspace, since Obsidian 1.6.0 - let counter = 0; - while(counter++<50 && (!Boolean(this?.plugin?.activeLeafChangeEventHandler) || !Boolean(this.canvasNodeFactory))) { - await(sleep(50)); - if(!this?.plugin) return; - } - if(!Boolean(this?.plugin?.activeLeafChangeEventHandler)) return; - if (Boolean(this.plugin.activeLeafChangeEventHandler) && (this?.app?.workspace?.activeLeaf === this.leaf)) { - this.plugin.activeLeafChangeEventHandler(this.leaf); - } - this.canvasNodeFactory.initialize(); - this.contentEl.addClass("excalidraw-view"); - //https://github.com/zsviczian/excalibrain/issues/28 - await this.addSlidingPanesListner(); //awaiting this because when using workspaces, onLayoutReady comes too early - this.addParentMoveObserver(); - - const onKeyUp = (e: KeyboardEvent) => { - this.modifierKeyDown = { - shiftKey: e.shiftKey, - ctrlKey: e.ctrlKey, - altKey: e.altKey, - metaKey: e.metaKey - } - }; - - const onKeyDown = (e: KeyboardEvent) => { - this.modifierKeyDown = { - shiftKey: e.shiftKey, - ctrlKey: e.ctrlKey, - altKey: e.altKey, - metaKey: e.metaKey - } - }; - - const onBlurOrLeave = () => { - if(!this.excalidrawAPI || !this.excalidrawData.loaded || !this.isDirty()) { - return; - } - if((this.excalidrawAPI as ExcalidrawImperativeAPI).getAppState().activeTool.type !== "image") { - this.forceSave(true); - } - }; - - this.registerDomEvent(this.ownerWindow, "keydown", onKeyDown, false); - this.registerDomEvent(this.ownerWindow, "keyup", onKeyUp, false); - //this.registerDomEvent(this.contentEl, "mouseleave", onBlurOrLeave, false); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2004 - this.registerDomEvent(this.ownerWindow, "blur", onBlurOrLeave, false); - }); - - this.setupAutosaveTimer(); - super.onload(); - } - - //this is to solve sliding panes bug - //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/9 - private slidingPanesListner: ()=>void; - private async addSlidingPanesListner() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addSlidingPanesListner, "ExcalidrawView.addSlidingPanesListner"); - if(!this.plugin.settings.slidingPanesSupport) { - return; - } - - this.slidingPanesListner = () => { - if (this.excalidrawAPI) { - this.refreshCanvasOffset(); - } - }; - let rootSplit = this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt; - while(!rootSplit) { - await sleep(50); - rootSplit = this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt; - } - this.registerDomEvent(rootSplit.containerEl,"scroll",this.slidingPanesListner); - } - - private removeSlidingPanesListner() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.removeSlidingPanesListner, "ExcalidrawView.removeSlidingPanesListner"); - if (this.slidingPanesListner) { - ( - this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt - ).containerEl?.removeEventListener("scroll", this.slidingPanesListner); - this.slidingPanesListner = null; - } - } - - //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/572 - private offsetLeft: number = 0; - private offsetTop: number = 0; - private addParentMoveObserver() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addParentMoveObserver, "ExcalidrawView.addParentMoveObserver"); - - const parent = - getParentOfClass(this.containerEl, "popover") ?? - getParentOfClass(this.containerEl, "workspace-leaf"); - if (!parent) { - return; - } - - const inHoverEditorLeaf = parent.classList.contains("popover"); - - this.offsetLeft = parent.offsetLeft; - this.offsetTop = parent.offsetTop; - - //triggers when the leaf is moved in the workspace - const observerFn = async (m: MutationRecord[]) => { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(observerFn, `ExcalidrawView.parentMoveObserver, file:${this.file?.name}`); - const target = m[0].target; - if (!(target instanceof HTMLElement)) { - return; - } - const { offsetLeft, offsetTop } = target; - if (offsetLeft !== this.offsetLeft || offsetTop !== this.offsetTop) { - if (this.excalidrawAPI) { - this.refreshCanvasOffset(); - } - this.offsetLeft = offsetLeft; - this.offsetTop = offsetTop; - } - }; - this.parentMoveObserver = DEBUGGING - ? new CustomMutationObserver(observerFn, "parentMoveObserver") - : new MutationObserver(observerFn) - - this.parentMoveObserver.observe(parent, { - attributeOldValue: true, - attributeFilter: inHoverEditorLeaf - ? ["data-x", "data-y"] - : ["class", "style"], - }); - } - - private removeParentMoveObserver() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.removeParentMoveObserver, "ExcalidrawView.removeParentMoveObserver"); - if (this.parentMoveObserver) { - this.parentMoveObserver.disconnect(); - this.parentMoveObserver = null; - } - } - - public setTheme(theme: string) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setTheme, "ExcalidrawView.setTheme", theme); - const api = this.excalidrawAPI; - if (!api) { - return; - } - if (this.file) { - //if there is an export theme set, override the theme change - if (hasExportTheme(this.plugin, this.file)) { - return; - } - } - const st: AppState = api.getAppState(); - this.excalidrawData.scene.theme = theme; - //debug({where:"ExcalidrawView.setTheme",file:this.file.name,dataTheme:this.excalidrawData.scene.appState.theme,before:"updateScene"}); - this.updateScene({ - appState: { - ...st, - theme, - }, - storeAction: "update", - }); - } - - private prevTextMode: TextMode; - private blockTextModeChange: boolean = false; - public async changeTextMode(textMode: TextMode, reload: boolean = true) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.changeTextMode, "ExcalidrawView.changeTextMode", textMode, reload); - if(this.compatibilityMode) return; - if(this.blockTextModeChange) return; - this.blockTextModeChange = true; - this.textMode = textMode; - if (textMode === TextMode.parsed) { - this.actionButtons['isRaw'].hide(); - this.actionButtons['isParsed'].show(); - } else { - this.actionButtons['isRaw'].show(); - this.actionButtons['isParsed'].hide(); - } - if (this.toolsPanelRef && this.toolsPanelRef.current) { - this.toolsPanelRef.current.setPreviewMode(textMode === TextMode.parsed); - } - const api = this.excalidrawAPI; - if (api && reload) { - await this.save(); - this.preventAutozoom(); - await this.excalidrawData.loadData(this.data, this.file, this.textMode); - this.excalidrawData.scene.appState.theme = api.getAppState().theme; - await this.loadDrawing(false); - api.history.clear(); //to avoid undo replacing links with parsed text - } - this.prevTextMode = this.textMode; - this.blockTextModeChange = false; - } - - public autosaveFunction: Function; - get autosaveInterval() { - return DEVICE.isMobile ? this.plugin.settings.autosaveIntervalMobile : this.plugin.settings.autosaveIntervalDesktop; - } - - public setupAutosaveTimer() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setupAutosaveTimer, "ExcalidrawView.setupAutosaveTimer"); - - const timer = async () => { - if(!this.isLoaded) { - this.autosaveTimer = window.setTimeout( - timer, - this.autosaveInterval, - ); - return; - } - - const api = this.excalidrawAPI; - if (!api) { - warningUnknowSeriousError(); - return; - } - const st = api.getAppState() as AppState; - const isFreedrawActive = (st.activeTool?.type === "freedraw") && (this.freedrawLastActiveTimestamp > (Date.now()-2000)); - const isEditingText = st.editingTextElement !== null; - const isEditingNewElement = st.newElement !== null; - //this will reset positioning of the cursor in case due to the popup keyboard, - //or the command palette, or some other unexpected reason the onResize would not fire... - this.refreshCanvasOffset(); - if ( - this.isDirty() && - this.plugin.settings.autosave && - !this.semaphores.forceSaving && - !this.semaphores.autosaving && - !this.semaphores.embeddableIsEditingSelf && - !isFreedrawActive && - !isEditingText && - !isEditingNewElement //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/630 - ) { - //console.log("autosave"); - this.autosaveTimer = null; - if (this.excalidrawAPI) { - this.semaphores.autosaving = true; - //changed from await to then to avoid lag during saving of large file - this.save().then(()=>this.semaphores.autosaving = false); - } - this.autosaveTimer = window.setTimeout( - timer, - this.autosaveInterval, - ); - } else { - this.autosaveTimer = window.setTimeout( - timer, - this.plugin.activeExcalidrawView === this && - this.semaphores.dirty && - this.plugin.settings.autosave - ? 1000 //try again in 1 second - : this.autosaveInterval, - ); - } - }; - - this.autosaveFunction = timer; - this.resetAutosaveTimer(); - } - - - private resetAutosaveTimer() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.resetAutosaveTimer, "ExcalidrawView.resetAutosaveTimer"); - if(!this.autosaveFunction) return; - - if (this.autosaveTimer) { - window.clearTimeout(this.autosaveTimer); - this.autosaveTimer = null; - } // clear previous timer if one exists - this.autosaveTimer = window.setTimeout( - this.autosaveFunction, - this.autosaveInterval, - ); - } - - unload(): void { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.unload,`ExcalidrawView.unload, file:${this.file?.name}`); - super.unload(); - } - - async onUnloadFile(file: TFile): Promise { - //deliberately not calling super.onUnloadFile() to avoid autosave (saved in unload) - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onUnloadFile,`ExcalidrawView.onUnloadFile, file:${this.file?.name}`); - let counter = 0; - while (this.semaphores.saving && (counter++ < 200)) { - await sleep(50); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1988 - if(counter++ === 15) { - new Notice(t("SAVE_IS_TAKING_LONG")); - } - if(counter === 80) { - new Notice(t("SAVE_IS_TAKING_VERY_LONG")); - } - } - if(counter >= 200) { - new Notice("Unknown error, save is taking too long"); - return; - } - await this.forceSaveIfRequired(); - } - - private async forceSaveIfRequired():Promise { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.forceSaveIfRequired,`ExcalidrawView.forceSaveIfRequired`); - let watchdog = 0; - let dirty = false; - //if saving was already in progress - //the function awaits the save to finish. - while (this.semaphores.saving && watchdog++ < 200) { - dirty = true; - await sleep(40); - } - if(this.excalidrawAPI) { - this.checkSceneVersion(this.excalidrawAPI.getSceneElements()); - if(this.isDirty()) { - const path = this.file?.path; - const plugin = this.plugin; - window.setTimeout(() => { - plugin.triggerEmbedUpdates(path) - },400); - dirty = true; - await this.save(true,true,true); - } - } - return dirty; - } - - //onClose happens after onunload - protected async onClose(): Promise { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onClose,`ExcalidrawView.onClose, file:${this.file?.name}`); - this.exitFullscreen(); - await this.forceSaveIfRequired(); - if (this.excalidrawRoot) { - this.excalidrawRoot.unmount(); - this.excalidrawRoot = null; - } - - this.clearPreventReloadTimer(); - this.clearEmbeddableNodeIsEditingTimer(); - this.plugin.scriptEngine?.removeViewEAs(this); - this.excalidrawAPI = null; - if(this.draginfoDiv) { - this.ownerDocument.body.removeChild(this.draginfoDiv); - delete this.draginfoDiv; - } - if(this.canvasNodeFactory) { - this.canvasNodeFactory.destroy(); - } - this.canvasNodeFactory = null; - this.embeddableLeafRefs.clear(); - this.embeddableRefs.clear(); - Object.values(this.actionButtons).forEach((el) => el.remove()); - this.actionButtons = {} as Record; - if (this.excalidrawData) { - this.excalidrawData.destroy(); - this.excalidrawData = null; - }; - if(this.exportDialog) { - this.exportDialog.destroy(); - this.exportDialog = null; - } - this.hoverPreviewTarget = null; - if(this.plugin.ea?.targetView === this) { - this.plugin.ea.targetView = null; - } - if(this._hookServer?.targetView === this) { - this._hookServer.targetView = null; - } - this._hookServer = null; - this.containerEl.onWindowMigrated = null; - this.packages = {react:null, reactDOM:null, excalidrawLib:null}; - - let leafcount = 0; - this.app.workspace.iterateAllLeaves(l=>{ - if(l === this.leaf) return; - - if(l.containerEl?.ownerDocument.defaultView === this.ownerWindow) { - leafcount++; - } - }) - if(leafcount === 0) { - this.plugin.deletePackage(this.ownerWindow); - } - - this.lastMouseEvent = null; - this.requestSave = null; - this.leaf.tabHeaderInnerTitleEl.style.color = ""; - - //super.onClose will unmount Excalidraw, need to save before that - await super.onClose(); - tmpBruteForceCleanup(this); - } - - //onunload is called first - onunload() { - super.onunload(); - this.destroyers.forEach((destroyer) => destroyer()); - this.restoreMobileLeaves(); - this.semaphores.viewunload = true; - this.semaphores.popoutUnload = (this.ownerDocument !== document) && (this.ownerDocument.body.querySelectorAll(".workspace-tab-header").length === 0); - - if(this.getHookServer().onViewUnloadHook) { - try { - this.getHookServer().onViewUnloadHook(this); - } catch(e) { - errorlog({where: "ExcalidrawView.onunload", fn: this.getHookServer().onViewUnloadHook, error: e}); - } - } - const tooltip = this.containerEl?.ownerDocument?.body.querySelector( - "body>div.excalidraw-tooltip,div.excalidraw-tooltip--visible", - ); - if (tooltip) { - this.containerEl?.ownerDocument?.body.removeChild(tooltip); - } - this.removeParentMoveObserver(); - this.removeSlidingPanesListner(); - if (this.autosaveTimer) { - window.clearInterval(this.autosaveTimer); - this.autosaveTimer = null; - } - this.autosaveFunction = null; - - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onunload,`ExcalidrawView.onunload, completed`); - } - - /** - * reload is triggered by the modifyEventHandler in main.ts whenever an excalidraw drawing that is currently open - * in a workspace leaf is modified. There can be two reasons for the file change: - * - The user saves the drawing in the active view (either force-save or autosave) - * - The file is modified by some other process, typically as a result of background sync, or because the drawing is open - * side by side, e.g. the canvas in one view and markdown view in the other. - * @param fullreload - * @param file - * @returns - */ - public async reload(fullreload: boolean = false, file?: TFile) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.reload,`ExcalidrawView.reload, file:${this.file?.name}, fullreload:${fullreload}, file:${file?.name}`); - const loadOnModifyTrigger = file && file === this.file; - - //once you've finished editing the embeddable, the first time the file - //reloads will be because of the embeddable changed the file, - //there is a 2000 ms time window allowed for this, but typically this will - //happen within 100 ms. When this happens the timer is cleared and the - //next time reload triggers the file will be reloaded as normal. - if (this.semaphores.embeddableIsEditingSelf) { - //console.log("reload - embeddable is editing") - if(this.editingSelfResetTimer) { - this.clearEmbeddableNodeIsEditingTimer(); - this.semaphores.embeddableIsEditingSelf = false; - } - if(loadOnModifyTrigger) { - this.data = await this.app.vault.read(this.file); - } - return; - } - //console.log("reload - embeddable is not editing") - - if (this.semaphores.preventReload) { - this.semaphores.preventReload = false; - return; - } - if (this.semaphores.saving) return; - this.lastLoadedFile = null; - this.actionButtons['save'].querySelector("svg").removeClass("excalidraw-dirty"); - if (this.compatibilityMode) { - this.clearDirty(); - return; - } - const api = this.excalidrawAPI; - if (!this.file || !api) { - return; - } - - if (loadOnModifyTrigger) { - this.data = await this.app.vault.read(file); - this.preventAutozoom(); - } - if (fullreload) { - await this.excalidrawData.loadData(this.data, this.file, this.textMode); - } else { - await this.excalidrawData.setTextMode(this.textMode); - } - this.excalidrawData.scene.appState.theme = api.getAppState().theme; - await this.loadDrawing(loadOnModifyTrigger); - this.clearDirty(); - } - - async zoomToElementId(id: string, hasGroupref:boolean) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.zoomToElementId, "ExcalidrawView.zoomToElementId", id, hasGroupref); - let counter = 0; - while (!this.excalidrawAPI && counter++<100) await sleep(50); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/734 - const api = this.excalidrawAPI; - if (!api) { - return; - } - const sceneElements = api.getSceneElements(); - - let elements = sceneElements.filter((el: ExcalidrawElement) => el.id === id); - if(elements.length === 0) { - const frame = getFrameBasedOnFrameNameOrId(id, sceneElements); - if (frame) { - elements = [frame]; - } else { - return; - } - } - if(hasGroupref) { - const groupElements = this.plugin.ea.getElementsInTheSameGroupWithElement(elements[0],sceneElements) - if(groupElements.length>0) { - elements = groupElements; - } - } - - this.preventAutozoom(); - this.zoomToElements(!api.getAppState().viewModeEnabled, elements); - } - - setEphemeralState(state: any): void { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setEphemeralState, "ExcalidrawView.setEphemeralState", state); - if (!state) { - return; - } - - if (state.rename === "all") { - this.app.fileManager.promptForFileRename(this.file); - return; - } - - let query: string[] = null; - - if ( - state.match && - state.match.content && - state.match.matches && - state.match.matches.length >= 1 && - state.match.matches[0].length === 2 - ) { - query = [ - state.match.content.substring( - state.match.matches[0][0], - state.match.matches[0][1], - ), - ]; - } - - const waitForExcalidraw = async () => { - let counter = 0; - while ( - (this.semaphores.justLoaded || - !this.isLoaded || - !this.excalidrawAPI || - this.excalidrawAPI?.getAppState()?.isLoading) && - counter++<100 - ) await sleep(50); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/734 - } - - const filenameParts = getEmbeddedFilenameParts( - (state.subpath && state.subpath.startsWith("#^group") && !state.subpath.startsWith("#^group=")) - ? "#^group=" + state.subpath.substring(7) - : (state.subpath && state.subpath.startsWith("#^area") && !state.subpath.startsWith("#^area=")) - ? "#^area=" + state.subpath.substring(6) - : state.subpath - ); - if(filenameParts.hasBlockref) { - window.setTimeout(async () => { - await waitForExcalidraw(); - if(filenameParts.blockref && !filenameParts.hasGroupref) { - if(!this.getScene()?.elements.find((el:ExcalidrawElement)=>el.id === filenameParts.blockref)) { - const cleanQuery = cleanSectionHeading(filenameParts.blockref).replaceAll(" ",""); - const blocks = await this.getBackOfTheNoteBlocks(); - if(blocks.includes(cleanQuery)) { - this.setMarkdownView(state); - return; - } - } - } - window.setTimeout(()=>this.zoomToElementId(filenameParts.blockref, filenameParts.hasGroupref)); - }); - } - - if(filenameParts.hasSectionref) { - query = [`# ${filenameParts.sectionref}`] - } else if (state.line && state.line > 0) { - query = [this.data.split("\n")[state.line - 1]]; - } - - if (query) { - window.setTimeout(async () => { - await waitForExcalidraw(); - - const api = this.excalidrawAPI; - if (!api) return; - if (api.getAppState().isLoading) return; - - const elements = api.getSceneElements() as ExcalidrawElement[]; - - if(query.length === 1 && query[0].startsWith("[")) { - const partsArray = REGEX_LINK.getResList(query[0]); - let parts = partsArray[0]; - if(parts) { - const linkText = REGEX_LINK.getLink(parts); - if(linkText) { - const file = this.plugin.app.metadataCache.getFirstLinkpathDest(linkText, this.file.path); - if(file) { - let fileId:FileId[] = []; - this.excalidrawData.files.forEach((ef,fileID) => { - if(ef.file?.path === file.path) fileId.push(fileID); - }); - if(fileId.length>0) { - const images = elements.filter(el=>el.type === "image" && fileId.includes(el.fileId)); - if(images.length>0) { - this.preventAutozoom(); - window.setTimeout(()=>this.zoomToElements(!api.getAppState().viewModeEnabled, images)); - return; - } - } - } - } - } - } - - if(!this.selectElementsMatchingQuery( - elements, - query, - !api.getAppState().viewModeEnabled, - filenameParts.hasSectionref, - filenameParts.hasGroupref - )) { - const cleanQuery = cleanSectionHeading(query[0]); - const sections = await this.getBackOfTheNoteSections(); - if(sections.includes(cleanQuery) || this.data.includes(query[0])) { - this.setMarkdownView(state); - return; - } - } - }); - } - - //super.setEphemeralState(state); - } - - // clear the view content - clear() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clear, "ExcalidrawView.clear"); - this.semaphores.warnAboutLinearElementLinkClick = true; - this.viewSaveData = ""; - this.canvasNodeFactory.purgeNodes(); - this.embeddableRefs.clear(); - this.embeddableLeafRefs.clear(); - - delete this.exportDialog; - const api = this.excalidrawAPI; - if (!api) { - return; - } - if (this.activeLoader) { - this.activeLoader.terminate = true; - this.activeLoader = null; - } - this.nextLoader = null; - api.resetScene(); - this.previousSceneVersion = 0; - } - - public isLoaded: boolean = false; - async setViewData(data: string, clear: boolean = false) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setViewData, "ExcalidrawView.setViewData", data, clear); - //I am using last loaded file to control when the view reloads. - //It seems text file view gets the modified file event after sync before the modifyEventHandler in main.ts - //reload can only be triggered via reload() - await this.plugin.awaitInit(); - if(this.lastLoadedFile === this.file) return; - this.isLoaded = false; - if(!this.file) return; - if(this.plugin.settings.showNewVersionNotification) checkExcalidrawVersion(); - if(isMaskFile(this.plugin,this.file)) { - const notice = new Notice(t("MASK_FILE_NOTICE"), 5000); - //add click and hold event listner to the notice - let noticeTimeout:number; - this.registerDomEvent(notice.noticeEl,"pointerdown", (ev:MouseEvent) => { - noticeTimeout = window.setTimeout(()=>{ - window.open("https://youtu.be/uHFd0XoHRxE"); - },1000); - }) - this.registerDomEvent(notice.noticeEl,"pointerup", (ev:MouseEvent) => { - window.clearTimeout(noticeTimeout); - }) - } - if (clear) { - this.clear(); - } - this.lastSaveTimestamp = this.file.stat.mtime; - this.lastLoadedFile = this.file; - data = this.data = data.replaceAll("\r\n", "\n").replaceAll("\r", "\n"); - this.app.workspace.onLayoutReady(async () => { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setViewData, `ExcalidrawView.setViewData > app.workspace.onLayoutReady, file:${this.file?.name}, isActiveLeaf:${this?.app?.workspace?.activeLeaf === this.leaf}`); - //the leaf moved to a window and ExcalidrawView was destructed - //Happens during Obsidian startup if View opens in new window. - if(!this?.app) { - return; - } - await this.plugin.awaitInit(); - let counter = 0; - while ((!this.file || !this.plugin.fourthFontLoaded) && counter++<50) await sleep(50); - if(!this.file) return; - this.compatibilityMode = this.file.extension === "excalidraw"; - await this.plugin.loadSettings(); - if (this.compatibilityMode) { - this.plugin.enableLegacyFilePopoverObserver(); - this.actionButtons['isRaw'].hide(); - this.actionButtons['isParsed'].hide(); - this.actionButtons['link'].hide(); - this.textMode = TextMode.raw; - await this.excalidrawData.loadLegacyData(data, this.file); - if (!this.plugin.settings.compatibilityMode) { - new Notice(t("COMPATIBILITY_MODE"), 4000); - } - this.excalidrawData.disableCompression = true; - } else { - this.actionButtons['link'].show(); - this.excalidrawData.disableCompression = false; - const textMode = getTextMode(data); - this.changeTextMode(textMode, false); - try { - if ( - !(await this.excalidrawData.loadData( - data, - this.file, - this.textMode, - )) - ) { - return; - } - } catch (e) { - errorlog({ where: "ExcalidrawView.setViewData", error: e }); - if(e.message === ERROR_IFRAME_CONVERSION_CANCELED) { - this.setMarkdownView(); - return; - } - const file = this.file; - const plugin = this.plugin; - const leaf = this.leaf; - (async () => { - let confirmation:boolean = true; - let counter = 0; - const timestamp = Date.now(); - while (!imageCache.isReady() && confirmation) { - const message = `You've been now waiting for ${Math.round((Date.now()-timestamp)/1000)} seconds. ` - imageCache.initializationNotice = true; - const confirmationPrompt = new ConfirmationPrompt(plugin, - `${counter>0 - ? counter%4 === 0 - ? message + "The CACHE is still loading.

" - : counter%4 === 1 - ? message + "Watch the top right corner for the notification.

" - : counter%4 === 2 - ? message + "I really, really hope the backup will work for you!

" - : message + "I am sorry, it is taking a while, there is not much I can do...

" - : ""}${t("CACHE_NOT_READY")}`); - confirmation = await confirmationPrompt.waitForClose - counter++; - } - - const drawingBAK = await imageCache.getBAKFromCache(file.path); - if (!drawingBAK) { - new Notice( - `Error loading drawing:\n${e.message}${ - e.message === "Cannot read property 'index' of undefined" - ? "\n'# Drawing' section is likely missing" - : "" - }\n\nTry manually fixing the file or restoring an earlier version from sync history.`, - 10000, - ); - return; - } - const confirmationPrompt = new ConfirmationPrompt(plugin,t("BACKUP_AVAILABLE")); - confirmationPrompt.waitForClose.then(async (confirmed) => { - if (confirmed) { - await this.app.vault.modify(file, drawingBAK); - plugin.excalidrawFileModes[leaf.id || file.path] = VIEW_TYPE_EXCALIDRAW; - setExcalidrawView(leaf); - } - }); - - - })(); - this.setMarkdownView(); - return; - } - } - await this.loadDrawing(true); - - if(this.plugin.ea.onFileOpenHook) { - const tempEA = getEA(this); - try { - await this.plugin.ea.onFileOpenHook({ - ea: tempEA, - excalidrawFile: this.file, - view: this, - }); - } catch(e) { - errorlog({ where: "ExcalidrawView.setViewData.onFileOpenHook", error: e }); - } finally { - tempEA.destroy(); - } - } - - const script = this.excalidrawData.getOnLoadScript(); - if(script) { - const scriptname = this.file.basename+ "-onlaod-script"; - const runScript = () => { - if(!this.excalidrawAPI) { //need to wait for Excalidraw to initialize - window.setTimeout(runScript.bind(this),200); - return; - } - this.plugin.scriptEngine.executeScript(this,script,scriptname,this.file); - } - runScript(); - } - this.isLoaded = true; - }); - } - - private getGridColor(bgColor: string, st: AppState): { Bold: string, Regular: string } { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getGridColor, "ExcalidrawView.getGridColor", bgColor, st); - - const cm = this.plugin.ea.getCM(bgColor); - const isDark = cm.isDark(); - - let Regular: string; - let Bold: string; - const opacity = this.plugin.settings.gridSettings.OPACITY/100; - - if (this.plugin.settings.gridSettings.DYNAMIC_COLOR) { - // Dynamic color: concatenate opacity to the HEX string - Regular = (isDark ? cm.lighterBy(10) : cm.darkerBy(10)).alphaTo(opacity).stringRGB({ alpha: true }); - Bold = (isDark ? cm.lighterBy(5) : cm.darkerBy(5)).alphaTo(opacity).stringRGB({ alpha: true }); - } else { - // Custom color handling - const customCM = this.plugin.ea.getCM(this.plugin.settings.gridSettings.COLOR); - const customIsDark = customCM.isDark(); - - // Regular uses the custom color directly - Regular = customCM.alphaTo(opacity).stringRGB({ alpha: true }); - - // Bold is 7 shades lighter or darker based on the custom color's darkness - Bold = (customIsDark ? customCM.lighterBy(10) : customCM.darkerBy(10)).alphaTo(opacity).stringRGB({ alpha: true }); - } - - return { Bold, Regular }; - } - - - public activeLoader: EmbeddedFilesLoader = null; - private nextLoader: EmbeddedFilesLoader = null; - public async loadSceneFiles(isThemeChange: boolean = false) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.loadSceneFiles, "ExcalidrawView.loadSceneFiles", isThemeChange); - if (!this.excalidrawAPI) { - return; - } - const loader = new EmbeddedFilesLoader(this.plugin); - - const runLoader = (l: EmbeddedFilesLoader) => { - this.nextLoader = null; - this.activeLoader = l; - l.loadSceneFiles( - this.excalidrawData, - (files: FileData[], isDark: boolean, final:boolean = true) => { - if (!files) { - return; - } - addFiles(files, this, isDark); - if(!final) return; - this.activeLoader = null; - if (this.nextLoader) { - runLoader(this.nextLoader); - } else { - //in case one or more files have not loaded retry later hoping that sync has delivered the file in the mean time. - this.excalidrawData.getFiles().some(ef=>{ - if(ef && !ef.file && ef.attemptCounter<30) { - const currentFile = this.file.path; - window.setTimeout(async ()=>{ - if(this && this.excalidrawAPI && currentFile === this.file.path) { - this.loadSceneFiles(); - } - },2000) - return true; - } - return false; - }) - } - },0,isThemeChange, - ); - }; - if (!this.activeLoader) { - runLoader(loader); - } else { - this.nextLoader = loader; - } - } - - public async synchronizeWithData(inData: ExcalidrawData) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.synchronizeWithData, "ExcalidrawView.synchronizeWithData", inData); - if(this.semaphores.embeddableIsEditingSelf) { - return; - } - //console.log("synchronizeWithData - embeddable is not editing"); - //check if saving, wait until not - let counter = 0; - while(this.semaphores.saving && counter++<30) { - await sleep(100); - } - if(counter>=30) { - errorlog({ - where:"ExcalidrawView.synchronizeWithData", - message:`Aborting sync with received file (${this.file.path}) because semaphores.saving remained true for ower 3 seconds`, - "fn": this.synchronizeWithData - }); - return; - } - this.semaphores.saving = true; - let reloadFiles = false; - - try { - const deletedIds = inData.deletedElements.map(el=>el.id); - const sceneElements = this.excalidrawAPI.getSceneElementsIncludingDeleted() - //remove deleted elements - .filter((el: ExcalidrawElement)=>!deletedIds.contains(el.id)); - const sceneElementIds = sceneElements.map((el:ExcalidrawElement)=>el.id); - - const manageMapChanges = (incomingElement: ExcalidrawElement ) => { - switch(incomingElement.type) { - case "text": - this.excalidrawData.textElements.set( - incomingElement.id, - inData.textElements.get(incomingElement.id) - ); - break; - case "image": - if(inData.getFile(incomingElement.fileId)) { - this.excalidrawData.setFile( - incomingElement.fileId, - inData.getFile(incomingElement.fileId) - ); - reloadFiles = true; - } else if (inData.getEquation(incomingElement.fileId)) { - this.excalidrawData.setEquation( - incomingElement.fileId, - inData.getEquation(incomingElement.fileId) - ) - reloadFiles = true; - } - break; - } - - if(inData.elementLinks.has(incomingElement.id)) { - this.excalidrawData.elementLinks.set( - incomingElement.id, - inData.elementLinks.get(incomingElement.id) - ) - } - - } - - //update items with higher version number then in scene - inData.scene.elements.forEach(( - incomingElement:ExcalidrawElement, - idx: number, - inElements: ExcalidrawElement[] - )=>{ - const sceneElement:ExcalidrawElement = sceneElements.filter( - (element:ExcalidrawElement)=>element.id === incomingElement.id - )[0]; - if( - sceneElement && - (sceneElement.version < incomingElement.version || - //in case of competing versions of the truth, the incoming version will be honored - (sceneElement.version === incomingElement.version && - JSON.stringify(sceneElement) !== JSON.stringify(incomingElement)) - ) - ) { - manageMapChanges(incomingElement); - //place into correct element layer sequence - const currentLayer = sceneElementIds.indexOf(incomingElement.id); - //remove current element from scene - const elToMove = sceneElements.splice(currentLayer,1); - if(idx === 0) { - sceneElements.splice(0,0,incomingElement); - if(currentLayer!== 0) { - sceneElementIds.splice(currentLayer,1); - sceneElementIds.splice(0,0,incomingElement.id); - } - } else { - const prevId = inElements[idx-1].id; - const parentLayer = sceneElementIds.indexOf(prevId); - sceneElements.splice(parentLayer+1,0,incomingElement); - if(parentLayer!==currentLayer-1) { - sceneElementIds.splice(currentLayer,1) - sceneElementIds.splice(parentLayer+1,0,incomingElement.id); - } - } - return; - } else if(!sceneElement) { - manageMapChanges(incomingElement); - - if(idx === 0) { - sceneElements.splice(0,0,incomingElement); - sceneElementIds.splice(0,0,incomingElement.id); - } else { - const prevId = inElements[idx-1].id; - const parentLayer = sceneElementIds.indexOf(prevId); - sceneElements.splice(parentLayer+1,0,incomingElement); - sceneElementIds.splice(parentLayer+1,0,incomingElement.id); - } - } else if(sceneElement && incomingElement.type === "image") { //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/632 - const incomingFile = inData.getFile(incomingElement.fileId); - const sceneFile = this.excalidrawData.getFile(incomingElement.fileId); - - const shouldUpdate = Boolean(incomingFile) && ( - ((sceneElement as ExcalidrawImageElement).fileId !== incomingElement.fileId) || - (incomingFile.file && (sceneFile.file !== incomingFile.file)) || - (incomingFile.hyperlink && (sceneFile.hyperlink !== incomingFile.hyperlink)) || - (incomingFile.linkParts?.original && (sceneFile.linkParts?.original !== incomingFile.linkParts?.original)) - ) - if(shouldUpdate) { - this.excalidrawData.setFile( - incomingElement.fileId, - inData.getFile(incomingElement.fileId) - ); - reloadFiles = true; - } - } - }) - this.previousSceneVersion = this.getSceneVersion(sceneElements); - //changing files could result in a race condition for sync. If at the end of sync there are differences - //set dirty will trigger an autosave - if(this.getSceneVersion(inData.scene.elements) !== this.previousSceneVersion) { - this.setDirty(3); - } - this.updateScene({elements: sceneElements, storeAction: "capture"}); - if(reloadFiles) this.loadSceneFiles(); - } catch(e) { - errorlog({ - where:"ExcalidrawView.synchronizeWithData", - message:`Error during sync with received file (${this.file.path})`, - "fn": this.synchronizeWithData, - error: e - }); - } - this.semaphores.saving = false; - } - - /** - * - * @param justloaded - a flag to trigger zoom to fit after the drawing has been loaded - */ - public async loadDrawing(justloaded: boolean, deletedElements?: ExcalidrawElement[]) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.loadDrawing, "ExcalidrawView.loadDrawing", justloaded, deletedElements); - const excalidrawData = this.excalidrawData.scene; - this.semaphores.justLoaded = justloaded; - this.clearDirty(); - const om = this.excalidrawData.getOpenMode(); - this.semaphores.preventReload = false; - const penEnabled = this.plugin.isPenMode(); - const api = this.excalidrawAPI; - if (api) { - //isLoaded flags that a new file is being loaded, isLoaded will be true after loadDrawing completes - const viewModeEnabled = !this.isLoaded - ? (excalidrawData.elements.length > 0 ? om.viewModeEnabled : false) - : api.getAppState().viewModeEnabled; - const zenModeEnabled = !this.isLoaded - ? om.zenModeEnabled - : api.getAppState().zenModeEnabled; - //debug({where:"ExcalidrawView.loadDrawing",file:this.file.name,dataTheme:excalidrawData.appState.theme,before:"updateScene"}) - //api.setLocalFont(this.plugin.settings.experimentalEnableFourthFont); - - this.updateScene( - { - elements: excalidrawData.elements.concat(deletedElements??[]), //need to preserve deleted elements during autosave if images, links, etc. are updated - files: excalidrawData.files, - storeAction: justloaded ? "update" : "update", //was none, but I think based on a false understanding of none - }, - justloaded - ); - this.updateScene( - { - //elements: excalidrawData.elements.concat(deletedElements??[]), //need to preserve deleted elements during autosave if images, links, etc. are updated - appState: { - ...excalidrawData.appState, - ...this.excalidrawData.selectedElementIds //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/609 - ? this.excalidrawData.selectedElementIds - : {}, - zenModeEnabled, - viewModeEnabled, - linkOpacity: this.excalidrawData.getLinkOpacity(), - trayModeEnabled: this.plugin.settings.defaultTrayMode, - penMode: penEnabled, - penDetected: penEnabled, - allowPinchZoom: this.plugin.settings.allowPinchZoom, - allowWheelZoom: this.plugin.settings.allowWheelZoom, - pinnedScripts: this.plugin.settings.pinnedScripts, - customPens: this.plugin.settings.customPens.slice(0,this.plugin.settings.numberOfCustomPens), - }, - storeAction: justloaded ? "update" : "update", //was none, but I think based on a false understanding of none - }, - ); - if ( - this.app.workspace.getActiveViewOfType(ExcalidrawView) === this.leaf.view && - this.excalidrawWrapperRef - ) { - //.firstElmentChild solves this issue: https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/346 - this.excalidrawWrapperRef.current?.firstElementChild?.focus(); - } - //debug({where:"ExcalidrawView.loadDrawing",file:this.file.name,before:"this.loadSceneFiles"}); - this.onAfterLoadScene(justloaded); - } else { - this.instantiateExcalidraw({ - elements: excalidrawData.elements, - appState: { - ...excalidrawData.appState, - zenModeEnabled: om.zenModeEnabled, - viewModeEnabled: excalidrawData.elements.length > 0 ? om.viewModeEnabled : false, - linkOpacity: this.excalidrawData.getLinkOpacity(), - trayModeEnabled: this.plugin.settings.defaultTrayMode, - penMode: penEnabled, - penDetected: penEnabled, - allowPinchZoom: this.plugin.settings.allowPinchZoom, - allowWheelZoom: this.plugin.settings.allowWheelZoom, - pinnedScripts: this.plugin.settings.pinnedScripts, - customPens: this.plugin.settings.customPens.slice(0,this.plugin.settings.numberOfCustomPens), - }, - files: excalidrawData.files, - libraryItems: await this.getLibrary(), - }); - //files are loaded when excalidrawAPI is mounted - } - const isCompressed = this.data.match(/```compressed\-json\n/gm) !== null; - - if ( - !this.compatibilityMode && - this.plugin.settings.compress !== isCompressed && - !this.isEditedAsMarkdownInOtherView() - ) { - this.setDirty(4); - } - } - - isEditedAsMarkdownInOtherView(): boolean { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.isEditedAsMarkdownInOtherView, "ExcalidrawView.isEditedAsMarkdownInOtherView"); - //if the user is editing the same file in markdown mode, do not compress it - const leaves = this.app.workspace.getLeavesOfType("markdown"); - return ( - leaves.filter((leaf) => (leaf.view as MarkdownView).file === this.file) - .length > 0 - ); - } - - private onAfterLoadScene(justloaded: boolean) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onAfterLoadScene, "ExcalidrawView.onAfterLoadScene"); - this.loadSceneFiles(); - this.updateContainerSize(null, true, justloaded); - this.initializeToolsIconPanelAfterLoading(); - } - - public setDirty(location?:number) { - if(this.semaphores.saving) return; //do not set dirty if saving - if(!this.isDirty()) { - //the autosave timer should start when the first stroke was made... thus avoiding an immediate impact by saving right then - this.resetAutosaveTimer(); - } - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setDirty,`ExcalidrawView.setDirty, location:${location}`); - this.semaphores.dirty = this.file?.path; - this.actionButtons['save'].querySelector("svg").addClass("excalidraw-dirty"); - if(!this.semaphores.viewunload && this.toolsPanelRef?.current) { - this.toolsPanelRef.current.setDirty(true); - } - if(!DEVICE.isMobile) { - if(requireApiVersion("0.16.0")) { - this.leaf.tabHeaderInnerIconEl.style.color="var(--color-accent)" - this.leaf.tabHeaderInnerTitleEl.style.color="var(--color-accent)" - } - } - } - - public isDirty() { - return Boolean(this.semaphores?.dirty) && (this.semaphores.dirty === this.file?.path); - } - - public clearDirty() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearDirty,`ExcalidrawView.clearDirty`); - if(this.semaphores.viewunload) return; - const api = this.excalidrawAPI; - if (!api) { - return; - } - this.semaphores.dirty = null; - if(this.toolsPanelRef?.current) { - this.toolsPanelRef.current.setDirty(false); - } - const el = api.getSceneElements(); - if (el) { - this.previousSceneVersion = this.getSceneVersion(el); - } - this.actionButtons['save'].querySelector("svg").removeClass("excalidraw-dirty"); - if(!DEVICE.isMobile) { - if(requireApiVersion("0.16.0")) { - this.leaf.tabHeaderInnerIconEl.style.color="" - this.leaf.tabHeaderInnerTitleEl.style.color="" - } - } - } - - public async initializeToolsIconPanelAfterLoading() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.initializeToolsIconPanelAfterLoading,`ExcalidrawView.initializeToolsIconPanelAfterLoading`); - if(this.semaphores.viewunload) return; - const api = this.excalidrawAPI; - if (!api) { - return; - } - const st = api.getAppState(); - //since Obsidian 1.6.0 onLayoutReady calls happen asynchronously compared to starting Excalidraw view - //these validations are just to make sure that initialization is complete - let counter = 0; - while(!this.plugin.scriptEngine && counter++<50) { - sleep(50); - } - - const panel = this.toolsPanelRef?.current; - if (!panel || !this.plugin.scriptEngine) { - return; - } - - panel.setTheme(st.theme); - panel.setExcalidrawViewMode(st.viewModeEnabled); - panel.setPreviewMode( - this.compatibilityMode ? null : this.textMode === TextMode.parsed, - ); - panel.updateScriptIconMap(this.plugin.scriptEngine.scriptIconMap); - } - - //Compatibility mode with .excalidraw files - canAcceptExtension(extension: string) { - return extension === "excalidraw"; //["excalidraw","md"].includes(extension); - } - - // gets the title of the document - getDisplayText() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getDisplayText, "ExcalidrawView.getDisplayText", this.file?.basename ?? "NOFILE"); - if (this.file) { - return this.file.basename; - } - return t("NOFILE"); - } - - // the view type name - getViewType() { - return VIEW_TYPE_EXCALIDRAW; - } - - // icon for the view - getIcon() { - return ICON_NAME; - } - - async setMarkdownView(eState?: any) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setMarkdownView, "ExcalidrawView.setMarkdownView", eState); - //save before switching to markdown view. - //this would also happen onClose, but it does not hurt to save it here - //this way isDirty() will return false in onClose, thuse - //saving here will not result in double save - //there was a race condition when clicking a link with a section or block reference to the back-of-the-note - //that resulted in a call to save after the view has been destroyed - //The sleep is required for metadata cache to be updated with the location of the block or section - await this.forceSaveIfRequired(); - await sleep(200); //dirty hack to wait for Obsidian metadata to be updated, note that save may have been triggered elsewhere already - this.plugin.excalidrawFileModes[this.id || this.file.path] = "markdown"; - this.plugin.setMarkdownView(this.leaf, eState); - } - - public async openAsMarkdown(eState?: any) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.openAsMarkdown, "ExcalidrawView.openAsMarkdown", eState); - if (this.plugin.settings.compress && this.plugin.settings.decompressForMDView) { - this.excalidrawData.disableCompression = true; - await this.save(true, true, true); - } else if (this.isDirty()) { - await this.save(true, true, true); - } - this.setMarkdownView(eState); - } - - public async convertExcalidrawToMD() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.convertExcalidrawToMD, "ExcalidrawView.convertExcalidrawToMD"); - await this.save(); - const file = await this.plugin.convertSingleExcalidrawToMD(this.file); - await sleep(250); //dirty hack to wait for Obsidian metadata to be updated - this.plugin.openDrawing( - file, - "active-pane", - true - ); - } - - public convertTextElementToMarkdown(textElement: ExcalidrawTextElement, containerElement: ExcalidrawElement) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.convertTextElementToMarkdown, "ExcalidrawView.convertTextElementToMarkdown", textElement, containerElement); - if(!textElement) return; - const prompt = new Prompt( - this.app, - "Filename", - "", - "Leave blank to cancel this action", - ); - prompt.openAndGetValue(async (filename: string) => { - if (!filename) { - return; - } - filename = `${filename}.md`; - const folderpath = splitFolderAndFilename(this.file.path).folderpath; - await checkAndCreateFolder(folderpath); //create folder if it does not exist - const fname = getNewUniqueFilepath( - this.app.vault, - filename, - folderpath, - ); - const text:string[] = []; - if(containerElement && containerElement.link) text.push(containerElement.link); - text.push(textElement.rawText); - const f = await this.app.vault.create( - fname, - text.join("\n"), - ); - if(f) { - const ea:ExcalidrawAutomate = getEA(this); - const elements = containerElement ? [textElement,containerElement] : [textElement]; - ea.copyViewElementsToEAforEditing(elements); - ea.getElements().forEach(el=>el.isDeleted = true); - const [x,y,w,h] = containerElement - ? [containerElement.x,containerElement.y,containerElement.width,containerElement.height] - : [textElement.x, textElement.y, MAX_IMAGE_SIZE,MAX_IMAGE_SIZE]; - const id = ea.addEmbeddable(x,y,w,h, undefined,f); - if(containerElement) { - const props:(keyof ExcalidrawElement)[] = ["backgroundColor", "fillStyle","roughness","roundness","strokeColor","strokeStyle","strokeWidth"]; - props.forEach((prop)=>{ - const element = ea.getElement(id); - if (prop in element) { - (element as any)[prop] = containerElement[prop]; - } - }); - } - ea.getElement(id) - await ea.addElementsToView(); - ea.destroy(); - } - }); - } - - async addYouTubeThumbnail(link:string) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addYouTubeThumbnail, "ExcalidrawView.addYouTubeThumbnail", link); - const thumbnailLink = await getYouTubeThumbnailLink(link); - const ea = getEA(this) as ExcalidrawAutomate; - const id = await ea.addImage(0,0,thumbnailLink); - ea.getElement(id).link = link; - await ea.addElementsToView(true,true,true) - ea.destroy(); - - } - - async addImageWithURL(link:string) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addImageWithURL, "ExcalidrawView.addImageWithURL", link); - const ea = getEA(this) as ExcalidrawAutomate; - await ea.addImage(0,0,link); - await ea.addElementsToView(true,true,true); - ea.destroy(); - } - - async addImageSaveToVault(link:string) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addImageSaveToVault, "ExcalidrawView.addImageSaveToVault", link); - const ea = getEA(this) as ExcalidrawAutomate; - const mimeType = getMimeType(getURLImageExtension(link)); - const dataURL = await getDataURLFromURL(link,mimeType,3000); - const fileId = await generateIdFromFile((new TextEncoder()).encode(dataURL as string)) - const file = await this.excalidrawData.saveDataURLtoVault(dataURL,mimeType,fileId); - if(!file) { - new Notice(t("ERROR_SAVING_IMAGE")); - ea.destroy(); - return; - } - await ea.addImage(0,0,file); - await ea.addElementsToView(true,true,true); - ea.destroy(); - } - - async addTextWithIframely(text:string) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addTextWithIframely, "ExcalidrawView.addTextWithIframely", text); - const id = await this.addText(text); - const url = `http://iframely.server.crestify.com/iframely?url=${text}`; - try { - const data = JSON.parse(await request({ url })); - if (!data || data.error || !data.meta?.title) { - return; - } - const ea = getEA(this) as ExcalidrawAutomate; - const el = ea - .getViewElements() - .filter((el) => el.type==="text" && el.id === id); - if (el.length === 1) { - ea.copyViewElementsToEAforEditing(el); - const textElement = ea.getElement(el[0].id) as Mutable; - textElement.text = textElement.originalText = textElement.rawText = - `[${data.meta.title}](${text})`; - await ea.addElementsToView(false, false, false); - ea.destroy(); - } - } catch(e) { - }; - } - - onPaneMenu(menu: Menu, source: string): void { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onPaneMenu, "ExcalidrawView.onPaneMenu", menu, source); - if(this.excalidrawAPI && this.getViewSelectedElements().some(el=>el.type==="text")) { - menu.addItem(item => { - item - .setTitle(t("OPEN_LINK")) - .setIcon("external-link") - .setSection("pane") - .onClick(evt => { - this.handleLinkClick(evt as MouseEvent); - }); - }) - } - // Add a menu item to force the board to markdown view - if (!this.compatibilityMode) { - menu - .addItem((item) => { - item - .setTitle(t("OPEN_AS_MD")) - .setIcon("document") - .onClick(() => { - this.openAsMarkdown(); - }) - .setSection("pane"); - }) - } else { - menu.addItem((item) => { - item - .setTitle(t("CONVERT_FILE")) - .onClick(() => this.convertExcalidrawToMD()) - .setSection("pane"); - }); - } - menu - .addItem((item) => { - item - .setTitle(t("EXPORT_IMAGE")) - .setIcon(EXPORT_IMG_ICON_NAME) - .setSection("pane") - .onClick(async (ev) => { - if (!this.excalidrawAPI || !this.file) { - return; - } - if(!this.exportDialog) { - this.exportDialog = new ExportDialog(this.plugin, this,this.file); - this.exportDialog.createForm(); - } - this.exportDialog.open(); - }) - .setSection("pane"); - }) - .addItem(item => { - item - .setTitle(t("INSTALL_SCRIPT_BUTTON")) - .setIcon(SCRIPTENGINE_ICON_NAME) - .setSection("pane") - .onClick(()=>{ - new ScriptInstallPrompt(this.plugin).open(); - }) - }) - super.onPaneMenu(menu, source); - } - - async getLibrary() { - const data: any = this.plugin.getStencilLibrary(); - return data?.library ? data.library : data?.libraryItems ?? []; - } - - public setCurrentPositionToCenter(){ - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setCurrentPositionToCenter, "ExcalidrawView.setCurrentPositionToCenter"); - const api = this.excalidrawAPI as ExcalidrawImperativeAPI; - if (!api) { - return; - } - const st = api.getAppState(); - const { width, height, offsetLeft, offsetTop } = st; - this.currentPosition = viewportCoordsToSceneCoords( - { - clientX: width / 2 + offsetLeft, - clientY: height / 2 + offsetTop, - }, - st, - ); - }; - - private getSelectedTextElement(): SelectedElementWithLink{ - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getSelectedTextElement, "ExcalidrawView.getSelectedTextElement"); - const api = this.excalidrawAPI; - if (!api) { - return { id: null, text: null }; - } - if (api.getAppState().viewModeEnabled) { - if (this.selectedTextElement) { - const retval = this.selectedTextElement; - this.selectedTextElement = null; - return retval; - } - //return { id: null, text: null }; - } - const selectedElement = api - .getSceneElements() - .filter( - (el: ExcalidrawElement) => - el.id === Object.keys(api.getAppState().selectedElementIds)[0], - ); - if (selectedElement.length === 0) { - return { id: null, text: null }; - } - - if (selectedElement[0].type === "text") { - return { id: selectedElement[0].id, text: selectedElement[0].text }; - } //a text element was selected. Return text - - if (["image","arrow"].contains(selectedElement[0].type)) { - return { id: null, text: null }; - } - - const boundTextElements = selectedElement[0].boundElements?.filter( - (be: any) => be.type === "text", - ); - if (boundTextElements?.length > 0) { - const textElement = api - .getSceneElements() - .filter( - (el: ExcalidrawElement) => el.id === boundTextElements[0].id, - ); - if (textElement.length > 0) { - return { id: textElement[0].id, text: textElement[0].text }; - } - } //is a text container selected? - - if (selectedElement[0].groupIds.length === 0) { - return { id: null, text: null }; - } //is the selected element part of a group? - - const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of - const textElement = api - .getSceneElements() - .filter((el: any) => el.groupIds?.includes(group)) - .filter((el: any) => el.type === "text"); //filter for text elements of the group - if (textElement.length === 0) { - return { id: null, text: null }; - } //the group had no text element member - - return { id: selectedElement[0].id, text: selectedElement[0].text }; //return text element text - }; - - private getSelectedImageElement(): SelectedImage { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getSelectedImageElement, "ExcalidrawView.getSelectedImageElement"); - const api = this.excalidrawAPI; - if (!api) { - return { id: null, fileId: null }; - } - if (api.getAppState().viewModeEnabled) { - if (this.selectedImageElement) { - const retval = this.selectedImageElement; - this.selectedImageElement = null; - return retval; - } - //return { id: null, fileId: null }; - } - const selectedElement = api - .getSceneElements() - .filter( - (el: any) => - el.id == Object.keys(api.getAppState().selectedElementIds)[0], - ); - if (selectedElement.length === 0) { - return { id: null, fileId: null }; - } - if (selectedElement[0].type == "image") { - return { - id: selectedElement[0].id, - fileId: selectedElement[0].fileId, - }; - } //an image element was selected. Return fileId - - if (selectedElement[0].type === "text") { - return { id: null, fileId: null }; - } - - if (selectedElement[0].groupIds.length === 0) { - return { id: null, fileId: null }; - } //is the selected element part of a group? - const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of - const imageElement = api - .getSceneElements() - .filter((el: any) => el.groupIds?.includes(group)) - .filter((el: any) => el.type == "image"); //filter for Image elements of the group - if (imageElement.length === 0) { - return { id: null, fileId: null }; - } //the group had no image element member - return { id: imageElement[0].id, fileId: imageElement[0].fileId }; //return image element fileId - }; - - private getSelectedElementWithLink(): { id: string; text: string } { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getSelectedElementWithLink, "ExcalidrawView.getSelectedElementWithLink"); - const api = this.excalidrawAPI; - if (!api) { - return { id: null, text: null }; - } - if (api.getAppState().viewModeEnabled) { - if (this.selectedElementWithLink) { - const retval = this.selectedElementWithLink; - this.selectedElementWithLink = null; - return retval; - } - //return { id: null, text: null }; - } - const selectedElement = api - .getSceneElements() - .filter( - (el: any) => - el.id == Object.keys(api.getAppState().selectedElementIds)[0], - ); - if (selectedElement.length === 0) { - return { id: null, text: null }; - } - if (selectedElement[0].link) { - return { - id: selectedElement[0].id, - text: selectedElement[0].link, - }; - } - - const textId = getBoundTextElementId(selectedElement[0]); - if (textId) { - const textElement = api - .getSceneElements() - .filter((el: any) => el.id === textId && el.link); - if (textElement.length > 0) { - return { id: textElement[0].id, text: textElement[0].text }; - } - } - - if (selectedElement[0].groupIds.length === 0) { - return { id: null, text: null }; - } //is the selected element part of a group? - const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of - const elementsWithLink = api - .getSceneElements() - .filter((el: any) => el.groupIds?.includes(group)) - .filter((el: any) => el.link); //filter for elements of the group that have a link - if (elementsWithLink.length === 0) { - return { id: null, text: null }; - } //the group had no image element member - return { id: elementsWithLink[0].id, text: elementsWithLink[0].link }; //return image element fileId - }; - - public async addLink( - markdownlink: string, - path: string, - alias: string, - originalLink?: string, - ) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addLink, "ExcalidrawView.addLink", markdownlink, path, alias); - const api = this.excalidrawAPI as ExcalidrawImperativeAPI; - const st = api.getAppState(); - if( - !st.selectedElementIds || - (st.selectedElementIds && Object.keys(st.selectedElementIds).length !== 1) - ) { - this.addText(markdownlink); - return; - } - const selectedElementId = Object.keys(api.getAppState().selectedElementIds)[0]; - const selectedElement = api.getSceneElements().find(el=>el.id === selectedElementId); - if(!selectedElement || (!Boolean(originalLink) && (selectedElement && selectedElement.link !== null) )) { - if(selectedElement) new Notice("Selected element already has a link. Inserting link as text."); - this.addText(markdownlink); - return; - } - const ea = getEA(this) as ExcalidrawAutomate; - ea.copyViewElementsToEAforEditing([selectedElement]); - if(originalLink?.match(/\[\[(.*?)\]\]/)?.[1]) { - markdownlink = originalLink.replace(/(\[\[.*?\]\])/,markdownlink); - } - ea.getElement(selectedElementId).link = markdownlink; - await ea.addElementsToView(false, true); - ea.destroy(); - if(Boolean(originalLink)) { - this.updateScene({ - appState: { - showHyperlinkPopup: { - newValue : "info", oldValue : "editor" - } - } - }); - } - } - - public async addText ( - text: string, - fontFamily?: 1 | 2 | 3 | 4, - save: boolean = true - ): Promise { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addText, "ExcalidrawView.addText", text, fontFamily, save); - const api = this.excalidrawAPI as ExcalidrawImperativeAPI; - if (!api) { - return; - } - const st: AppState = api.getAppState(); - const ea = getEA(this); - ea.style.strokeColor = st.currentItemStrokeColor ?? "black"; - ea.style.opacity = st.currentItemOpacity ?? 1; - ea.style.fontFamily = fontFamily ?? st.currentItemFontFamily ?? 1; - ea.style.fontSize = st.currentItemFontSize ?? 20; - ea.style.textAlign = st.currentItemTextAlign ?? "left"; - - const { width, height } = st; - - const top = viewportCoordsToSceneCoords( - { - clientX: 0, - clientY: 0, - }, - st, - ); - const bottom = viewportCoordsToSceneCoords( - { - clientX: width, - clientY: height, - }, - st, - ); - const isPointerOutsideVisibleArea = top.x>this.currentPosition.x || bottom.xthis.currentPosition.y || bottom.y { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addElements, "ExcalidrawView.addElements", newElements, repositionToCursor, save, images, newElementsOnTop, shouldRestoreElements); - const api = this.excalidrawAPI as ExcalidrawImperativeAPI; - if (!api) { - return false; - } - const elementsMap = arrayToMap(api.getSceneElements()); - const textElements = newElements.filter((el) => el.type == "text"); - for (let i = 0; i < textElements.length; i++) { - const textElement = textElements[i] as Mutable; - const {parseResult, link} = - await this.excalidrawData.addTextElement( - textElement.id, - textElement.text, - textElement.rawText, //TODO: implement originalText support in ExcalidrawAutomate - ); - if (link) { - textElement.link = link; - } - if (this.textMode === TextMode.parsed && !textElement?.isDeleted) { - const {text, x, y, width, height} = refreshTextDimensions( - textElement,null,elementsMap,parseResult - ); - textElement.text = text; - textElement.originalText = parseResult; - textElement.x = x; - textElement.y = y; - textElement.width = width; - textElement.height = height; - } - } - - if (repositionToCursor) { - newElements = repositionElementsToCursor( - newElements, - this.currentPosition, - true, - ); - } - - const newIds = newElements.map((e) => e.id); - const el: ExcalidrawElement[] = api.getSceneElements() as ExcalidrawElement[]; - const removeList: string[] = []; - - //need to update elements in scene.elements to maintain sequence of layers - for (let i = 0; i < el.length; i++) { - const id = el[i].id; - if (newIds.includes(id)) { - el[i] = newElements.filter((ne) => ne.id === id)[0]; - removeList.push(id); - } - } - - const elements = newElementsOnTop - ? el.concat(newElements.filter((e) => !removeList.includes(e.id))) - : newElements.filter((e) => !removeList.includes(e.id)).concat(el); - - this.updateScene( - { - elements, - storeAction: "capture", - }, - shouldRestoreElements, - ); - - if (images && Object.keys(images).length >0) { - const files: BinaryFileData[] = []; - Object.keys(images).forEach((k) => { - files.push({ - mimeType: images[k].mimeType, - id: images[k].id, - dataURL: images[k].dataURL, - created: images[k].created, - }); - if (images[k].file || images[k].isHyperLink || images[k].isLocalLink) { - const embeddedFile = new EmbeddedFile( - this.plugin, - this.file.path, - images[k].isHyperLink && !images[k].isLocalLink - ? images[k].hyperlink - : images[k].file, - ); - const st: AppState = api.getAppState(); - embeddedFile.setImage( - images[k].dataURL, - images[k].mimeType, - images[k].size, - st.theme === "dark", - images[k].hasSVGwithBitmap, - ); - this.excalidrawData.setFile(images[k].id, embeddedFile); - } - if (images[k].latex) { - this.excalidrawData.setEquation(images[k].id, { - latex: images[k].latex, - isLoaded: true, - }); - } - }); - api.addFiles(files); - } - api.updateContainerSize(api.getSceneElements().filter(el => newIds.includes(el.id)).filter(isContainer)); - if (save) { - await this.save(false); //preventReload=false will ensure that markdown links are paresed and displayed correctly - } else { - this.setDirty(5); - } - return true; - }; - - public getScene (selectedOnly?: boolean) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getScene, "ExcalidrawView.getScene", selectedOnly); -/* if (this.lastSceneSnapshot) { - return this.lastSceneSnapshot; - }*/ - const api = this.excalidrawAPI; - if (!api) { - return null; - } - const el: ExcalidrawElement[] = selectedOnly ? this.getViewSelectedElements() : api.getSceneElements(); - const st: AppState = api.getAppState(); - const files = {...api.getFiles()}; - - if (files) { - const imgIds = el - .filter((e) => e.type === "image") - .map((e: any) => e.fileId); - const toDelete = Object.keys(files).filter( - (k) => !imgIds.contains(k), - ); - toDelete.forEach((k) => delete files[k]); - } - - const activeTool = {...st.activeTool}; - if(!["freedraw","hand"].includes(activeTool.type)) { - activeTool.type = "selection"; - } - activeTool.customType = null; - activeTool.lastActiveTool = null; - - return { - type: "excalidraw", - version: 2, - source: GITHUB_RELEASES+PLUGIN_VERSION, - elements: el, - //see also ExcalidrawAutomate async create( - appState: { - theme: st.theme, - viewBackgroundColor: st.viewBackgroundColor, - currentItemStrokeColor: st.currentItemStrokeColor, - currentItemBackgroundColor: st.currentItemBackgroundColor, - currentItemFillStyle: st.currentItemFillStyle, - currentItemStrokeWidth: st.currentItemStrokeWidth, - currentItemStrokeStyle: st.currentItemStrokeStyle, - currentItemRoughness: st.currentItemRoughness, - currentItemOpacity: st.currentItemOpacity, - currentItemFontFamily: st.currentItemFontFamily, - currentItemFontSize: st.currentItemFontSize, - currentItemTextAlign: st.currentItemTextAlign, - currentItemStartArrowhead: st.currentItemStartArrowhead, - currentItemEndArrowhead: st.currentItemEndArrowhead, - currentItemArrowType: st.currentItemArrowType, - scrollX: st.scrollX, - scrollY: st.scrollY, - zoom: st.zoom, - currentItemRoundness: st.currentItemRoundness, - gridSize: st.gridSize, - gridStep: st.gridStep, - gridModeEnabled: st.gridModeEnabled, - gridColor: st.gridColor, - colorPalette: st.colorPalette, - currentStrokeOptions: st.currentStrokeOptions, - frameRendering: st.frameRendering, - objectsSnapModeEnabled: st.objectsSnapModeEnabled, - activeTool, - }, - prevTextMode: this.prevTextMode, - files, - }; - }; - - /** - * ExcalidrawAPI refreshes canvas offsets - * @returns - */ - private refreshCanvasOffset() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.refreshCanvasOffset, "ExcalidrawView.refreshCanvasOffset"); - if(this.contentEl.clientWidth === 0 || this.contentEl.clientHeight === 0) return; - const api = this.excalidrawAPI; - if (!api) { - return; - } - api.refresh(); - }; - - // depricated. kept for backward compatibility. e.g. used by the Slideshow plugin - // 2024.05.03 - public refresh() { - this.refreshCanvasOffset(); - } - - private clearHoverPreview() { - const hoverContainerEl = this.hoverPopover?.containerEl; - //don't auto hide hover-editor - if (this.hoverPopover && !hoverContainerEl?.parentElement?.hasClass("hover-editor")) { - this.hoverPreviewTarget = null; - //@ts-ignore - if(this.hoverPopover.embed?.editor) { - return; - } - this.hoverPopover?.hide(); - } else if (this.hoverPreviewTarget) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearHoverPreview, "ExcalidrawView.clearHoverPreview", this); - const event = new MouseEvent("click", { - view: this.ownerWindow, - bubbles: true, - cancelable: true, - }); - this.hoverPreviewTarget.dispatchEvent(event); - this.hoverPreviewTarget = null; - } - }; - - 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 - */ - private identifyElementClicked () { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.identifyElementClicked, "ExcalidrawView.identifyElementClicked"); - this.selectedTextElement = getTextElementAtPointer(this.currentPosition, this); - if (this.selectedTextElement && this.selectedTextElement.id) { - const event = new MouseEvent("click", { - ctrlKey: !(DEVICE.isIOS || DEVICE.isMacOS) || this.modifierKeyDown.ctrlKey, - metaKey: (DEVICE.isIOS || DEVICE.isMacOS) || this.modifierKeyDown.metaKey, - shiftKey: this.modifierKeyDown.shiftKey, - altKey: this.modifierKeyDown.altKey, - }); - this.handleLinkClick(event); - this.selectedTextElement = null; - return; - } - this.selectedImageElement = getImageElementAtPointer(this.currentPosition, this); - if (this.selectedImageElement && this.selectedImageElement.id) { - const event = new MouseEvent("click", { - ctrlKey: !(DEVICE.isIOS || DEVICE.isMacOS) || this.modifierKeyDown.ctrlKey, - metaKey: (DEVICE.isIOS || DEVICE.isMacOS) || this.modifierKeyDown.metaKey, - shiftKey: this.modifierKeyDown.shiftKey, - altKey: this.modifierKeyDown.altKey, - }); - this.handleLinkClick(event); - this.selectedImageElement = null; - return; - } - - this.selectedElementWithLink = getElementWithLinkAtPointer(this.currentPosition, this); - if (this.selectedElementWithLink && this.selectedElementWithLink.id) { - const event = new MouseEvent("click", { - ctrlKey: !(DEVICE.isIOS || DEVICE.isMacOS) || this.modifierKeyDown.ctrlKey, - metaKey: (DEVICE.isIOS || DEVICE.isMacOS) || this.modifierKeyDown.metaKey, - shiftKey: this.modifierKeyDown.shiftKey, - altKey: this.modifierKeyDown.altKey, - }); - this.handleLinkClick(event); - this.selectedElementWithLink = null; - return; - } - }; - - private showHoverPreview(linktext?: string, element?: ExcalidrawElement) { - //(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.showHoverPreview, "ExcalidrawView.showHoverPreview", linktext, element); - if(!this.lastMouseEvent) return; - const st = this.excalidrawAPI?.getAppState(); - if(st?.editingTextElement || st?.newElement) return; //should not activate hover preview when element is being edited or dragged - if(this.semaphores.wheelTimeout) return; - //if link text is not provided, try to get it from the element - if (!linktext) { - if(!this.currentPosition) return; - linktext = ""; - const selectedEl = getTextElementAtPointer(this.currentPosition, this); - if (!selectedEl || !selectedEl.text) { - const selectedImgElement = - getImageElementAtPointer(this.currentPosition, this); - const selectedElementWithLink = (selectedImgElement?.id || selectedImgElement?.id) - ? null - : getElementWithLinkAtPointer(this.currentPosition, this); - element = this.excalidrawAPI.getSceneElements().find((el:ExcalidrawElement)=>el.id === selectedImgElement.id); - if ((!selectedImgElement || !selectedImgElement.fileId) && !selectedElementWithLink?.id) { - return; - } - if (selectedImgElement?.id) { - if (!this.excalidrawData.hasFile(selectedImgElement.fileId)) { - return; - } - const ef = this.excalidrawData.getFile(selectedImgElement.fileId); - if ( - (ef.isHyperLink || ef.isLocalLink) || //web images don't have a preview - (IMAGE_TYPES.contains(ef.file.extension)) || //images don't have a preview - (ef.file.extension.toLowerCase() === "pdf") || //pdfs don't have a preview - (this.plugin.ea.isExcalidrawFile(ef.file)) - ) {//excalidraw files don't have a preview - linktext = getLinkTextFromLink(element.link); - if(!linktext) return; - } else { - const ref = ef.linkParts.ref - ? `#${ef.linkParts.isBlockRef ? "^" : ""}${ef.linkParts.ref}` - : ""; - linktext = - ef.file.path + ref; - } - } - if (selectedElementWithLink?.id) { - linktext = getLinkTextFromLink(selectedElementWithLink.text); - if(!linktext) return; - if(this.app.metadataCache.getFirstLinkpathDest(linktext.split("#")[0],this.file.path) === this.file) return; - } - } else { - const {linkText, selectedElement} = this.getLinkTextForElement(selectedEl, selectedEl); - element = selectedElement; - /*this.excalidrawAPI.getSceneElements().filter((el:ExcalidrawElement)=>el.id === selectedElement.id)[0]; - const text: string = - this.textMode === TextMode.parsed - ? this.excalidrawData.getRawText(selectedElement.id) - : selectedElement.text;*/ - - linktext = getLinkTextFromLink(linkText); - if(!linktext) return; - } - } - - if(this.getHookServer().onLinkHoverHook) { - try { - if(!this.getHookServer().onLinkHoverHook( - element, - linktext, - this, - this.getHookServer() - )) { - return; - } - } catch (e) { - errorlog({where: "ExcalidrawView.showHoverPreview", fn: this.getHookServer().onLinkHoverHook, error: e}); - } - } - - if (this.semaphores.hoverSleep) { - return; - } - - const f = this.app.metadataCache.getFirstLinkpathDest( - linktext.split("#")[0], - this.file.path, - ); - if (!f) { - return; - } - - if ( - this.ownerDocument.querySelector(`div.popover-title[data-path="${f.path}"]`) - ) { - return; - } - - this.semaphores.hoverSleep = true; - window.setTimeout(() => (this.semaphores.hoverSleep = false), 500); - this.plugin.hover.linkText = linktext; - this.plugin.hover.sourcePath = this.file.path; - this.hoverPreviewTarget = this.contentEl; //e.target; - this.app.workspace.trigger("hover-link", { - event: this.lastMouseEvent, - source: VIEW_TYPE_EXCALIDRAW, - hoverParent: this, - targetEl: this.hoverPreviewTarget, //null //0.15.0 hover editor!! - linktext: this.plugin.hover.linkText, - sourcePath: this.plugin.hover.sourcePath, - }); - this.hoverPoint = this.currentPosition; - if (this.isFullscreen()) { - window.setTimeout(() => { - const popover = - this.ownerDocument.querySelector(`div.popover-title[data-path="${f.path}"]`) - ?.parentElement?.parentElement?.parentElement ?? - this.ownerDocument.body.querySelector("div.popover"); - if (popover) { - this.contentEl.append(popover); - } - }, 400); - } - }; - - private isLinkSelected():boolean { - return Boolean ( - this.getSelectedTextElement().id || - this.getSelectedImageElement().id || - this.getSelectedElementWithLink().id - ) - }; - - private excalidrawDIVonKeyDown(event: KeyboardEvent) { - //(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.excalidrawDIVonKeyDown, "ExcalidrawView.excalidrawDIVonKeyDown", event); - if (this.semaphores?.viewunload) return; - if (event.target === this.excalidrawWrapperRef.current) { - return; - } //event should originate from the canvas - if (this.isFullscreen() && event.keyCode === KEYCODE.ESC) { - this.exitFullscreen(); - } - if (isWinCTRLorMacCMD(event) && !isSHIFT(event) && !isWinALTorMacOPT(event)) { - this.showHoverPreview(); - } - }; - - private onPointerDown(e: PointerEvent) { - if (!(isWinCTRLorMacCMD(e)||isWinMETAorMacCTRL(e))) { - return; - } - if (!this.plugin.settings.allowCtrlClick && !isWinMETAorMacCTRL(e)) { - return; - } - if (Boolean((this.excalidrawAPI as ExcalidrawImperativeAPI)?.getAppState().contextMenu)) { - return; - } - //added setTimeout when I changed onClick(e: MouseEvent) to onPointerDown() in 1.7.9. - //Timeout is required for Excalidraw to first complete the selection action before execution - //of the link click continues - window.setTimeout(()=>{ - if (!this.isLinkSelected()) return; - this.handleLinkClick(e); - }); - } - - private onMouseMove(e: MouseEvent) { - //@ts-ignore - this.lastMouseEvent = e.nativeEvent; - } - - private onMouseOver() { - 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"; - pointersMap: Gesture["pointers"]; - }) { - this.currentPosition = p.pointer; - if ( - this.hoverPreviewTarget && - (Math.abs(this.hoverPoint.x - p.pointer.x) > 50 || - Math.abs(this.hoverPoint.y - p.pointer.y) > 50) - ) { - this.clearHoverPreview(); - } - if (!this.viewModeEnabled) { - return; - } - - const buttonDown = !this.blockOnMouseButtonDown && p.button === "down"; - if (buttonDown) { - this.blockOnMouseButtonDown = true; - - //ctrl click - if (isWinCTRLorMacCMD(this.modifierKeyDown) || isWinMETAorMacCTRL(this.modifierKeyDown)) { - this.identifyElementClicked(); - return; - } - - if(this.plugin.settings.doubleClickLinkOpenViewMode) { - //dobule click - const now = Date.now(); - if ((now - this.doubleClickTimestamp) < 600 && (now - this.doubleClickTimestamp) > 40) { - this.identifyElementClicked(); - } - this.doubleClickTimestamp = now; - } - return; - } - if (p.button === "up") { - this.blockOnMouseButtonDown = false; - } - if (isWinCTRLorMacCMD(this.modifierKeyDown) || - (this.excalidrawAPI.getAppState().isViewModeEnabled && - this.plugin.settings.hoverPreviewWithoutCTRL)) { - - this.showHoverPreview(); - } - } - - public updateGridColor(canvasColor?: string, st?: any) { - if(!canvasColor) { - st = (this.excalidrawAPI as ExcalidrawImperativeAPI).getAppState(); - canvasColor = canvasColor ?? st.viewBackgroundColor === "transparent" ? "white" : st.viewBackgroundColor; - } - window.setTimeout(()=>this.updateScene({appState:{gridColor: this.getGridColor(canvasColor, st)}, storeAction: "update"})); - } - - private canvasColorChangeHook(st: AppState) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.canvasColorChangeHook, "ExcalidrawView.canvasColorChangeHook", st); - const canvasColor = st.viewBackgroundColor === "transparent" ? "white" : st.viewBackgroundColor; - this.updateGridColor(canvasColor,st); - setDynamicStyle(this.plugin.ea,this,canvasColor,this.plugin.settings.dynamicStyling); - if(this.plugin.ea.onCanvasColorChangeHook) { - try { - this.plugin.ea.onCanvasColorChangeHook( - this.plugin.ea, - this, - st.viewBackgroundColor - ) - } catch (e) { - errorlog({ - where: this.canvasColorChangeHook, - source: this.plugin.ea.onCanvasColorChangeHook, - error: e, - message: "ea.onCanvasColorChangeHook exception" - }) - } - } - } - - private checkSceneVersion(et: ExcalidrawElement[]) { - const sceneVersion = this.getSceneVersion(et); - if ( - ((sceneVersion > 0 || - (sceneVersion === 0 && et.length > 0)) && //Addressing the rare case when the last element is deleted from the scene - sceneVersion !== this.previousSceneVersion) - ) { - this.previousSceneVersion = sceneVersion; - this.setDirty(6.1); - } - } - - private onChange (et: ExcalidrawElement[], st: AppState) { - if(st.newElement?.type === "freedraw") { - this.freedrawLastActiveTimestamp = Date.now(); - } - if (st.newElement || st.editingTextElement || st.editingLinearElement) { - this.plugin.wasPenModeActivePreviously = st.penMode; - } - this.viewModeEnabled = st.viewModeEnabled; - if (this.semaphores.justLoaded) { - const elcount = this.excalidrawData?.scene?.elements?.length ?? 0; - if( elcount>0 && et.length===0 ) return; - this.semaphores.justLoaded = false; - if (!this.semaphores.preventAutozoom && this.plugin.settings.zoomToFitOnOpen) { - this.zoomToFit(false,true); - } - this.previousSceneVersion = this.getSceneVersion(et); - this.previousBackgroundColor = st.viewBackgroundColor; - this.previousTheme = st.theme; - this.canvasColorChangeHook(st); - return; - } - if(st.theme !== this.previousTheme && this.file === this.excalidrawData.file) { - this.previousTheme = st.theme; - this.setDirty(5.1); - } - if(st.viewBackgroundColor !== this.previousBackgroundColor && this.file === this.excalidrawData.file) { - this.previousBackgroundColor = st.viewBackgroundColor; - this.setDirty(6); - if(this.colorChangeTimer) { - window.clearTimeout(this.colorChangeTimer); - } - this.colorChangeTimer = window.setTimeout(()=>{ - this.canvasColorChangeHook(st); - this.colorChangeTimer = null; - },50); //just enough time if the user is playing with color picker, the change is not too frequent. - } - if (this.semaphores.dirty) { - return; - } - if ( - st.editingTextElement === null && - //Removed because of - //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/565 - /*st.resizingElement === null && - st.newElement === null && - st.editingGroupId === null &&*/ - st.editingLinearElement === null - ) { - this.checkSceneVersion(et); - } - } - - private onLibraryChange(items: LibraryItems) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onLibraryChange, "ExcalidrawView.onLibraryChange", items); - (async () => { - const lib = { - type: "excalidrawlib", - version: 2, - source: GITHUB_RELEASES+PLUGIN_VERSION, - libraryItems: items, - }; - this.plugin.setStencilLibrary(lib); - await this.plugin.saveSettings(); - })(); - } - - private onPaste (data: ClipboardData, event: ClipboardEvent | null) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onPaste, "ExcalidrawView.onPaste", data, event); - const api = this.excalidrawAPI as ExcalidrawImperativeAPI; - const ea = this.getHookServer(); - if(data?.elements) { - data.elements - .filter(el=>el.type==="text" && !el.hasOwnProperty("rawText")) - .forEach(el=>(el as Mutable).rawText = (el as ExcalidrawTextElement).originalText); - }; - if(data && ea.onPasteHook) { - const res = ea.onPasteHook({ - ea, - payload: data, - event, - excalidrawFile: this.file, - view: this, - pointerPosition: this.currentPosition, - }); - if(typeof res === "boolean" && res === false) return false; - } - - // Disables Middle Mouse Button Paste Functionality on Linux - if( - !this.modifierKeyDown.ctrlKey - && typeof event !== "undefined" - && event !== null - && DEVICE.isLinux - ) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onPaste,`ExcalidrawView.onPaste, Prevented what is likely middle mouse button paste.`); - return false; - }; - - if(data && data.text && hyperlinkIsImage(data.text)) { - this.addImageWithURL(data.text); - return false; - } - if(data && data.text && !this.modifierKeyDown.shiftKey) { - const isCodeblock = Boolean(data.text.replaceAll("\r\n", "\n").replaceAll("\r", "\n").match(/^`{3}[^\n]*\n.+\n`{3}\s*$/ms)); - if(isCodeblock) { - const clipboardText = data.text; - window.setTimeout(()=>this.pasteCodeBlock(clipboardText)); - return false; - } - - if(isTextImageTransclusion(data.text,this, async (link, file)=>{ - const ea = getEA(this) as ExcalidrawAutomate; - if(IMAGE_TYPES.contains(file.extension)) { - ea.selectElementsInView([await insertImageToView (ea, this.currentPosition, file)]); - ea.destroy(); - } else if(file.extension !== "pdf") { - ea.selectElementsInView([await insertEmbeddableToView (ea, this.currentPosition, file, link)]); - ea.destroy(); - } else { - if(link.match(/^[^#]*#page=\d*(&\w*=[^&]+){0,}&rect=\d*,\d*,\d*,\d*/g)) { - const ea = getEA(this) as ExcalidrawAutomate; - const imgID = await ea.addImage(this.currentPosition.x, this.currentPosition.y,link.split("&rect=")[0]); - const el = ea.getElement(imgID) as Mutable; - const fd = ea.imagesDict[el.fileId] as FileData; - el.crop = getPDFCropRect({ - scale: this.plugin.settings.pdfScale, - link, - naturalHeight: fd.size.height, - naturalWidth: fd.size.width, - }); - if(el.crop) { - el.width = el.crop.width/this.plugin.settings.pdfScale; - el.height = el.crop.height/this.plugin.settings.pdfScale; - } - el.link = `[[${link}]]`; - ea.addElementsToView(false,false).then(()=>ea.destroy()); - } else { - const modal = new UniversalInsertFileModal(this.plugin, this); - modal.open(file, this.currentPosition); - } - } - this.setDirty(9); - })) { - return false; - } - - const quoteWithRef = obsidianPDFQuoteWithRef(data.text); - if(quoteWithRef) { - const ea = getEA(this) as ExcalidrawAutomate; - const st = api.getAppState(); - const strokeC = st.currentItemStrokeColor; - const viewC = st.viewBackgroundColor; - ea.style.strokeColor = strokeC === "transparent" - ? ea.getCM(viewC === "transparent" ? "white" : viewC) - .invert() - .stringHEX({alpha: false}) - : strokeC; - ea.style.fontFamily = st.currentItemFontFamily; - ea.style.fontSize = st.currentItemFontSize; - const textDims = ea.measureText(quoteWithRef.quote); - const textWidth = textDims.width + 2*30; //default padding - const id = ea.addText(this.currentPosition.x, this.currentPosition.y, quoteWithRef.quote, { - box: true, - boxStrokeColor: "transparent", - width: Math.min(500,textWidth), - height: textDims.height + 2*30, - }) - ea.elementsDict[id].link = `[[${quoteWithRef.link}]]`; - ea.addElementsToView(false,false).then(()=>ea.destroy()); - - return false; - } - } - if (data.elements) { - window.setTimeout(() => this.save(), 30); //removed prevent reload = false, as reload was triggered when pasted containers were processed and there was a conflict with the new elements - } - - //process pasted text after it was processed into elements by Excalidraw - //I let Excalidraw handle the paste first, e.g. to split text by lines - //Only process text if it includes links or embeds that need to be parsed - if(data && data.text && data.text.match(/(\[\[[^\]]*]])|(\[[^\]]*]\([^)]*\))/gm)) { - const prevElements = api.getSceneElements().filter(el=>el.type === "text").map(el=>el.id); - - window.setTimeout(async ()=>{ - const sceneElements = api.getSceneElementsIncludingDeleted() as Mutable[]; - const newElements = sceneElements.filter(el=>el.type === "text" && !el.isDeleted && !prevElements.includes(el.id)) as ExcalidrawTextElement[]; - - //collect would-be image elements and their corresponding files and links - const imageElementsMap = new Map(); - let element: ExcalidrawTextElement; - const callback = (link: string, file: TFile) => { - imageElementsMap.set(element, [link, file]); - } - newElements.forEach((el:ExcalidrawTextElement)=>{ - element = el; - isTextImageTransclusion(el.originalText,this,callback); - }); - - //if there are no image elements, save and return - //Save will ensure links and embeds are parsed - if(imageElementsMap.size === 0) { - this.save(false); //saving because there still may be text transclusions - return; - }; - - //if there are image elements - //first delete corresponding "old" text elements - for(const [el, [link, file]] of imageElementsMap) { - const clone = cloneElement(el); - clone.isDeleted = true; - this.excalidrawData.deleteTextElement(clone.id); - sceneElements[sceneElements.indexOf(el)] = clone; - } - this.updateScene({elements: sceneElements, storeAction: "update"}); - - //then insert images and embeds - //shift text elements down to make space for images and embeds - const ea:ExcalidrawAutomate = getEA(this); - let offset = 0; - for(const el of newElements) { - const topleft = {x: el.x, y: el.y+offset}; - if(imageElementsMap.has(el)) { - const [link, file] = imageElementsMap.get(el); - if(IMAGE_TYPES.contains(file.extension)) { - const id = await insertImageToView (ea, topleft, file, undefined, false); - offset += ea.getElement(id).height - el.height; - } else if(file.extension !== "pdf") { - //isTextImageTransclusion will not return text only markdowns, this is here - //for the future when we may want to support other embeddables - const id = await insertEmbeddableToView (ea, topleft, file, link, false); - offset += ea.getElement(id).height - el.height; - } else { - const modal = new UniversalInsertFileModal(this.plugin, this); - modal.open(file, topleft); - } - } else { - if(offset !== 0) { - ea.copyViewElementsToEAforEditing([el]); - ea.getElement(el.id).y = topleft.y; - } - } - } - await ea.addElementsToView(false,true); - ea.selectElementsInView(newElements.map(el=>el.id)); - ea.destroy(); - },200) //parse transclusion and links after paste - } - return true; - } - - private async onThemeChange (newTheme: string) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onThemeChange, "ExcalidrawView.onThemeChange", newTheme); - //debug({where:"ExcalidrawView.onThemeChange",file:this.file.name,before:"this.loadSceneFiles",newTheme}); - this.excalidrawData.scene.appState.theme = newTheme; - this.loadSceneFiles(true); - this.toolsPanelRef?.current?.setTheme(newTheme); - //Timeout is to allow appState to update - window.setTimeout(()=>setDynamicStyle(this.plugin.ea,this,this.previousBackgroundColor,this.plugin.settings.dynamicStyling)); - } - - private onDrop (event: React.DragEvent): 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 - 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;iea.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 { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onBeforeTextEdit, "ExcalidrawView.onBeforeTextEdit", textElement); - /*const api = this.excalidrawAPI as ExcalidrawImperativeAPI; - const st = api.getAppState(); - setDynamicStyle( - this.plugin.ea, - this, - st.viewBackgroundColor === "transparent" ? "white" : st.viewBackgroundColor, - this.plugin.settings.dynamicStyling, - api.getColorAtScenePoint({sceneX: this.currentPosition.x, sceneY: this.currentPosition.y}) - );*/ - if(!isExistingElement) { - return; - } - window.clearTimeout(this.isEditingTextResetTimer); - this.isEditingTextResetTimer = null; - this.semaphores.isEditingText = true; //to prevent autoresize on mobile when keyboard pops up - if(this.compatibilityMode) { - return textElement.originalText ?? textElement.text; - } - const raw = this.excalidrawData.getRawText(textElement.id); - if (!raw) { - return textElement.rawText; - } - return raw; - } - - - private onBeforeTextSubmit ( - textElement: ExcalidrawTextElement, - nextText: string, - nextOriginalText: string, - isDeleted: boolean, - ): {updatedNextOriginalText: string, nextLink: string} { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onBeforeTextSubmit, "ExcalidrawView.onBeforeTextSubmit", textElement, nextText, nextOriginalText, isDeleted); - const api = this.excalidrawAPI; - if (!api) { - return {updatedNextOriginalText: null, nextLink: textElement?.link ?? null}; - } - - // 1. Set the isEditingText flag to true to prevent autoresize on mobile - // 1500ms is an empirical number, the on-screen keyboard usually disappears in 1-2 seconds - this.semaphores.isEditingText = true; - if(this.isEditingTextResetTimer) { - window.clearTimeout(this.isEditingTextResetTimer); - } - this.isEditingTextResetTimer = window.setTimeout(() => { - if(typeof this.semaphores?.isEditingText !== "undefined") { - this.semaphores.isEditingText = false; - } - this.isEditingTextResetTimer = null; - }, 1500); - - // 2. If the text element is deleted, remove it from ExcalidrawData - // parsed textElements cache - if (isDeleted) { - this.excalidrawData.deleteTextElement(textElement.id); - this.setDirty(7); - return {updatedNextOriginalText: null, nextLink: null}; - } - - // 3. Check if the user accidently pasted Excalidraw data from the clipboard - // as text. If so, update the parsed link in ExcalidrawData - // textElements cache and update the text element in the scene with a warning. - const FORBIDDEN_TEXT = `{"type":"excalidraw/clipboard","elements":[{"`; - const WARNING = t("WARNING_PASTING_ELEMENT_AS_TEXT"); - if(nextOriginalText.startsWith(FORBIDDEN_TEXT)) { - window.setTimeout(()=>{ - const elements = this.excalidrawAPI.getSceneElements(); - const el = elements.filter((el:ExcalidrawElement)=>el.id === textElement.id); - if(el.length === 1) { - const clone = cloneElement(el[0]); - clone.rawText = WARNING; - elements[elements.indexOf(el[0])] = clone; - this.excalidrawData.setTextElement(clone.id,WARNING,()=>{}); - this.updateScene({elements, storeAction: "update"}); - api.history.clear(); - } - }); - return {updatedNextOriginalText:WARNING, nextLink:null}; - } - - const containerId = textElement.containerId; - - // 4. Check if the text matches the transclusion pattern and if so, - // check if the link in the transclusion can be resolved to a file in the vault. - // If the link is an image or a PDF file, replace the text element with the image or the PDF. - // If the link is an embedded markdown file, then display a message, but otherwise transclude the text step 5. - // 1 2 - if(isTextImageTransclusion(nextOriginalText, this, (link, file)=>{ - window.setTimeout(async ()=>{ - const elements = this.excalidrawAPI.getSceneElements(); - const el = elements.filter((el:ExcalidrawElement)=>el.id === textElement.id) as ExcalidrawTextElement[]; - if(el.length === 1) { - const center = {x: el[0].x, y: el[0].y }; - const clone = cloneElement(el[0]); - clone.isDeleted = true; - this.excalidrawData.deleteTextElement(clone.id); - elements[elements.indexOf(el[0])] = clone; - this.updateScene({elements, storeAction: "update"}); - const ea:ExcalidrawAutomate = getEA(this); - if(IMAGE_TYPES.contains(file.extension)) { - ea.selectElementsInView([await insertImageToView (ea, center, file)]); - ea.destroy(); - } else if(file.extension !== "pdf") { - ea.selectElementsInView([await insertEmbeddableToView (ea, center, file, link)]); - ea.destroy(); - } else { - const modal = new UniversalInsertFileModal(this.plugin, this); - modal.open(file, center); - } - this.setDirty(9); - } - }); - })) { - return {updatedNextOriginalText: null, nextLink: textElement.link}; - } - - // 5. Check if the user made changes to the text, or - // the text is missing from ExcalidrawData textElements cache (recently copy/pasted) - if ( - nextOriginalText !== textElement.originalText || - !this.excalidrawData.getRawText(textElement.id) - ) { - //the user made changes to the text or the text is missing from Excalidraw Data (recently copy/pasted) - //setTextElement will attempt a quick parse (without processing transclusions) - this.setDirty(8); - - // setTextElement will invoke this callback function in case quick parse was not possible, the parsed text contains transclusions - // in this case I need to update the scene asynchronously when parsing is complete - const callback = async (parsedText:string) => { - //this callback function will only be invoked if quick parse fails, i.e. there is a transclusion in the raw text - if(this.textMode === TextMode.raw) return; - - const elements = this.excalidrawAPI.getSceneElements(); - const elementsMap = arrayToMap(elements); - const el = elements.filter((el:ExcalidrawElement)=>el.id === textElement.id); - if(el.length === 1) { - const container = getContainerElement(el[0],elementsMap); - const clone = cloneElement(el[0]); - if(!el[0]?.isDeleted) { - const {text, x, y, width, height} = refreshTextDimensions(el[0], container, elementsMap, parsedText); - - clone.x = x; - clone.y = y; - clone.width = width; - clone.height = height; - clone.originalText = parsedText; - clone.text = text; - } - - elements[elements.indexOf(el[0])] = clone; - this.updateScene({elements, storeAction: "update"}); - if(clone.containerId) this.updateContainerSize(clone.containerId); - this.setDirty(8.1); - } - api.history.clear(); - }; - - const [parseResultOriginal, link] = - this.excalidrawData.setTextElement( - textElement.id, - nextOriginalText, - callback, - ); - - // if quick parse was successful, - // - check if textElement is in a container and update the container size, - // because the parsed text will have a different size than the raw text had - // - depending on the textMode, return the text with markdown markup or the parsed text - // if quick parse was not successful return [null, null, null] to indicate that the no changes were made to the text element - if (parseResultOriginal) { - //there were no transclusions in the raw text, quick parse was successful - if (containerId) { - this.updateContainerSize(containerId, true); - } - if (this.textMode === TextMode.raw) { - return {updatedNextOriginalText: nextOriginalText, nextLink: link}; - } //text is displayed in raw, no need to clear the history, undo will not create problems - if (nextOriginalText === parseResultOriginal) { - if (link) { - //don't forget the case: link-prefix:"" && link-brackets:true - return {updatedNextOriginalText: parseResultOriginal, nextLink: link}; - } - return {updatedNextOriginalText: null, nextLink: textElement.link}; - } //There were no links to parse, raw text and parsed text are equivalent - api.history.clear(); - return {updatedNextOriginalText: parseResultOriginal, nextLink:link}; - } - return {updatedNextOriginalText: null, nextLink: textElement.link}; - } - // even if the text did not change, container sizes might need to be updated - if (containerId) { - this.updateContainerSize(containerId, true); - } - if (this.textMode === TextMode.parsed) { - const parseResultOriginal = this.excalidrawData.getParsedText(textElement.id); - return {updatedNextOriginalText: parseResultOriginal, nextLink: textElement.link}; - } - return {updatedNextOriginalText: null, nextLink: textElement.link}; - } - - private async onLinkOpen(element: ExcalidrawElement, e: any): Promise { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onLinkOpen, "ExcalidrawView.onLinkOpen", element, e); - e.preventDefault(); - if (!element) { - return; - } - let link = element.link; - if (!link || link === "") { - return; - } - window.setTimeout(()=>this.removeLinkTooltip(),500); - - let event = e?.detail?.nativeEvent; - if(this.handleLinkHookCall(element,element.link,event)) return; - //if(openExternalLink(element.link, this.app, !isSHIFT(event) && !isWinCTRLorMacCMD(event) && !isWinMETAorMacCTRL(event) && !isWinALTorMacOPT(event) ? element : undefined)) return; - if(openExternalLink(element.link, this.app)) return; - - //if element is type text and element has multiple links, then submit the element text to linkClick to trigger link suggester - if(element.type === "text") { - const linkText = element.rawText.replaceAll("\n", ""); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187 - const partsArray = REGEX_LINK.getResList(linkText); - if(partsArray.filter(p=>Boolean(p.value)).length > 1) { - link = linkText; - } - } - - if (!event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) { - event = emulateKeysForLinkClick("new-tab"); - } - - this.linkClick( - event, - null, - null, - {id: element.id, text: link}, - event, - true, - ); - return; - } - - private onLinkHover(element: NonDeletedExcalidrawElement, event: React.PointerEvent): void { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onLinkHover, "ExcalidrawView.onLinkHover", element, event); - if ( - element && - (this.plugin.settings.hoverPreviewWithoutCTRL || - isWinCTRLorMacCMD(event)) - ) { - this.lastMouseEvent = event; - this.lastMouseEvent.ctrlKey = !(DEVICE.isIOS || DEVICE.isMacOS) || this.lastMouseEvent.ctrlKey; - this.lastMouseEvent.metaKey = (DEVICE.isIOS || DEVICE.isMacOS) || this.lastMouseEvent.metaKey; - const link = element.link; - if (!link || link === "") { - return; - } - if (link.startsWith("[[")) { - const linkMatch = link.match(/\[\[(?.*?)\]\]/); - if (!linkMatch) { - return; - } - let linkText = linkMatch.groups.link; - this.showHoverPreview(linkText, element); - } - } - } - - private onViewModeChange(isViewModeEnabled: boolean) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onViewModeChange, "ExcalidrawView.onViewModeChange", isViewModeEnabled); - if(!this.semaphores.viewunload) { - this.toolsPanelRef?.current?.setExcalidrawViewMode( - isViewModeEnabled, - ); - } - if(this.getHookServer().onViewModeChangeHook) { - try { - this.getHookServer().onViewModeChangeHook(isViewModeEnabled,this,this.getHookServer()); - } catch(e) { - errorlog({where: "ExcalidrawView.onViewModeChange", fn: this.getHookServer().onViewModeChangeHook, error: e}); - } - - } - } - - private async getBackOfTheNoteSections() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getBackOfTheNoteSections, "ExcalidrawView.getBackOfTheNoteSections"); - return (await this.app.metadataCache.blockCache.getForFile({ isCancelled: () => false },this.file)) - .blocks.filter((b: any) => b.display && b.node?.type === "heading") - .filter((b: any) => !MD_EX_SECTIONS.includes(b.display)) - .map((b: any) => cleanSectionHeading(b.display)); - } - - private async getBackOfTheNoteBlocks() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getBackOfTheNoteBlocks, "ExcalidrawView.getBackOfTheNoteBlocks"); - return (await this.app.metadataCache.blockCache.getForFile({ isCancelled: () => false },this.file)) - .blocks.filter((b:any) => b.display && b.node && b.node.hasOwnProperty("type") && b.node.hasOwnProperty("id")) - .map((b:any) => cleanBlockRef(b.node.id)); - } - - public getSingleSelectedImage(): {imageEl: ExcalidrawImageElement, embeddedFile: EmbeddedFile} { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getSingleSelectedImage, "ExcalidrawView.getSingleSelectedImage"); - if(!this.excalidrawAPI) return null; - const els = this.getViewSelectedElements().filter(el=>el.type==="image"); - if(els.length !== 1) { - return null; - } - const el = els[0] as ExcalidrawImageElement; - const imageFile = this.excalidrawData.getFile(el.fileId); - return {imageEl: el, embeddedFile: imageFile}; - } - - public async insertBackOfTheNoteCard() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.insertBackOfTheNoteCard, "ExcalidrawView.insertBackOfTheNoteCard"); - const sections = await this.getBackOfTheNoteSections(); - const selectCardDialog = new SelectCard(this.app,this,sections); - selectCardDialog.start(); - } - - public async moveBackOfTheNoteCardToFile(id?: string) { - id = id ?? this.getViewSelectedElements().filter(el=>el.type==="embeddable")[0]?.id; - const embeddableData = this.getEmbeddableLeafElementById(id); - const child = embeddableData?.node?.child; - if(!child || (child.file !== this.file)) return; - - if(child.lastSavedData !== this.data) { - await this.forceSave(true); - if(child.lastSavedData !== this.data) { - new Notice(t("ERROR_TRY_AGAIN")); - return; - } - } - const {folder, filepath:_} = await getAttachmentsFolderAndFilePath( - this.app, - this.file.path, - "dummy", - ); - const filepath = getNewUniqueFilepath( - this.app.vault, - child.subpath.replaceAll("#",""), - folder, - ); - let path = await ScriptEngine.inputPrompt( - this, - this.plugin, - this.app, - "Set filename", - "Enter filename", - filepath, - undefined, - 3, - ); - if(!path) return; - if(!path.endsWith(".md")) { - path += ".md"; - } - const {folderpath, filename} = splitFolderAndFilename(path); - path = getNewUniqueFilepath(this.app.vault, filename, folderpath); - try { - const newFile = await this.app.vault.create(path, child.text); - if(!newFile) { - new Notice("Unexpected error"); - return; - } - const ea = getEA(this) as ExcalidrawAutomate; - ea.copyViewElementsToEAforEditing([this.getViewElements().find(el=>el.id === id)]); - ea.getElement(id).link = `[[${newFile.path}]]`; - this.data = this.data.split(child.heading+child.text).join(""); - await ea.addElementsToView(false); - ea.destroy(); - await this.forceSave(true); - } catch(e) { - new Notice(`Unexpected error: ${e.message}`); - return; - } - } - - public async pasteCodeBlock(data: string) { - try { - data = data.replaceAll("\r\n", "\n").replaceAll("\r", "\n").trim(); - const isCodeblock = Boolean(data.match(/^`{3}[^\n]*\n.+\n`{3}\s*$/ms)); - if(!isCodeblock) { - const codeblockType = await GenericInputPrompt.Prompt(this,this.plugin,this.app,"type codeblock type","javascript, html, python, etc.",""); - data = "```"+codeblockType.trim()+"\n"+data+"\n```"; - } - let title = (await GenericInputPrompt.Prompt(this,this.plugin,this.app,"Code Block Title","Enter title or leave empty for automatic title","")).trim(); - if (title === "") {title = "Code Block";}; - const sections = await this.getBackOfTheNoteSections(); - if (sections.includes(title)) { - let i=0; - while (sections.includes(`${title} ${++i}`)) {}; - title = `${title} ${i}`; - } - addBackOfTheNoteCard(this, title, false, data); - } catch (e) { - } - } - - public async convertImageElWithURLToLocalFile(data: {imageEl: ExcalidrawImageElement, embeddedFile: EmbeddedFile}) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.convertImageElWithURLToLocalFile, "ExcalidrawView.convertImageElWithURLToLocalFile", data); - const {imageEl, embeddedFile} = data; - const imageDataURL = embeddedFile.getImage(false); - if(!imageDataURL && !imageDataURL.startsWith("data:")) { - new Notice("Image not found"); - return false; - } - const ea = getEA(this) as ExcalidrawAutomate; - ea.copyViewElementsToEAforEditing([imageEl]); - const eaEl = ea.getElement(imageEl.id) as Mutable; - eaEl.fileId = fileid() as FileId; - if(!eaEl.link) {eaEl.link = embeddedFile.hyperlink}; - let dataURL = embeddedFile.getImage(false); - if(!dataURL.startsWith("data:")) { - new Notice("Attempting to download image from URL. This may take a long while. The operation will time out after max 1 minute"); - dataURL = await getDataURLFromURL(dataURL, embeddedFile.mimeType, 30000); - if(!dataURL.startsWith("data:")) { - new Notice("Failed. Could not download image!"); - return false; - } - } - const files: BinaryFileData[] = []; - files.push({ - mimeType: embeddedFile.mimeType, - id: eaEl.fileId, - dataURL: dataURL as DataURL, - created: embeddedFile.mtime, - }); - const api = this.excalidrawAPI as ExcalidrawImperativeAPI; - api.addFiles(files); - await ea.addElementsToView(false,true); - ea.destroy(); - new Notice("Image successfully converted to local file"); - } - - private insertLinkAction(linkVal: string) { - let link = linkVal.match(/\[\[(.*?)\]\]/)?.[1]; - if(!link) { - link = linkVal.replaceAll("[","").replaceAll("]",""); - link = link.split("|")[0].trim(); - } - this.plugin.insertLinkDialog.start(this.file.path, (markdownlink: string, path:string, alias:string) => this.addLink(markdownlink, path, alias, linkVal), link); - } - - private onContextMenu(elements: readonly ExcalidrawElement[], appState: AppState, onClose: (callback?: () => void) => void) { - const React = this.packages.react; - const contextMenuActions = []; - const api = this.excalidrawAPI as ExcalidrawImperativeAPI; - const selectedElementIds = Object.keys(api.getAppState().selectedElementIds); - const areElementsSelected = selectedElementIds.length > 0; - - if(this.isLinkSelected()) { - contextMenuActions.push([ - renderContextMenuAction( - React, - t("OPEN_LINK_CLICK"), - () => { - const event = emulateKeysForLinkClick("new-tab"); - this.handleLinkClick(event, true); - }, - onClose - ), - ]); - } - - if(appState.viewModeEnabled) { - const isLaserOn = appState.activeTool?.type === "laser"; - contextMenuActions.push([ - renderContextMenuAction( - React, - isLaserOn ? t("LASER_OFF") : t("LASER_ON"), - () => { - api.setActiveTool({type: isLaserOn ? "selection" : "laser"}); - }, - onClose - ), - ]); - } - - if(!appState.viewModeEnabled) { - const selectedTextElements = this.getViewSelectedElements().filter(el=>el.type === "text"); - if(selectedTextElements.length===1) { - const selectedTextElement = selectedTextElements[0] as ExcalidrawTextElement; - const containerElement = (this.getViewElements() as ExcalidrawElement[]).find(el=>el.id === selectedTextElement.containerId); - - //if the text element in the container no longer has a link associated with it... - if( - containerElement && - selectedTextElement.link && - this.excalidrawData.getParsedText(selectedTextElement.id) === selectedTextElement.rawText - ) { - contextMenuActions.push([ - renderContextMenuAction( - React, - t("REMOVE_LINK"), - async () => { - const ea = getEA(this) as ExcalidrawAutomate; - ea.copyViewElementsToEAforEditing([selectedTextElement]); - const el = ea.getElement(selectedTextElement.id) as Mutable; - el.link = null; - await ea.addElementsToView(false); - ea.destroy(); - }, - onClose - ), - ]); - } - - if(containerElement) { - contextMenuActions.push([ - renderContextMenuAction( - React, - t("SELECT_TEXTELEMENT_ONLY"), - () => { - window.setTimeout(()=> - (this.excalidrawAPI as ExcalidrawImperativeAPI).selectElements([selectedTextElement]) - ); - }, - onClose - ), - ]); - } - - if(!containerElement || (containerElement && containerElement.type !== "arrow")) { - contextMenuActions.push([ - renderContextMenuAction( - React, - t("CONVERT_TO_MARKDOWN"), - () => { - this.convertTextElementToMarkdown(selectedTextElement, containerElement); - }, - onClose - ), - ]); - } - } - - const img = this.getSingleSelectedImage(); - if(img && img.embeddedFile?.isHyperLink) { - contextMenuActions.push([ - renderContextMenuAction( - React, - t("CONVERT_URL_TO_FILE"), - () => { - window.setTimeout(()=>this.convertImageElWithURLToLocalFile(img)); - }, - onClose - ), - ]); - } - - if( - img && img.embeddedFile && img.embeddedFile.mimeType === "image/svg+xml" && - (!img.embeddedFile.file || (img.embeddedFile.file && !this.plugin.isExcalidrawFile(img.embeddedFile.file))) - ) { - contextMenuActions.push([ - renderContextMenuAction( - React, - t("IMPORT_SVG_CONTEXTMENU"), - async () => { - const base64Content = img.embeddedFile.getImage(false).split(',')[1]; - // Decoding the base64 content - const svg = atob(base64Content); - if(!svg || svg === "") return; - const ea = getEA(this) as ExcalidrawAutomate; - ea.importSVG(svg); - ea.addToGroup(ea.getElements().map(el=>el.id)); - await ea.addElementsToView(true, true, true,true); - ea.destroy(); - }, - onClose - ), - ]); - } - - if(areElementsSelected) { - contextMenuActions.push([ - renderContextMenuAction( - React, - t("COPY_ELEMENT_LINK"), - () => { - this.copyLinkToSelectedElementToClipboard(""); - }, - onClose - ), - ]); - } else { - contextMenuActions.push([ - renderContextMenuAction( - React, - t("COPY_DRAWING_LINK"), - () => { - const path = this.file.path.match(/(.*)(\.md)$/)?.[1]; - navigator.clipboard.writeText(`![[${path ?? this.file.path}]]`); - }, - onClose - ), - ]); - } - - if(this.getViewSelectedElements().filter(el=>el.type==="embeddable").length === 1) { - const embeddableData = this.getEmbeddableLeafElementById( - this.getViewSelectedElements().filter(el=>el.type==="embeddable")[0].id - ); - const child = embeddableData?.node?.child; - if(child && (child.file === this.file)) { - contextMenuActions.push([ - renderContextMenuAction( - React, - t("CONVERT_CARD_TO_FILE"), - () => { - this.moveBackOfTheNoteCardToFile(); - }, - onClose - ), - ]); - } - } - - contextMenuActions.push([ - renderContextMenuAction( - React, - t("INSERT_CARD"), - () => { - this.insertBackOfTheNoteCard(); - }, - onClose - ), - ]); - contextMenuActions.push([ - renderContextMenuAction( - React, - t("UNIVERSAL_ADD_FILE"), - () => { - const insertFileModal = new UniversalInsertFileModal(this.plugin, this); - insertFileModal.open(); - }, - onClose - ), - ]); - contextMenuActions.push([ - renderContextMenuAction( - React, - t("INSERT_LINK"), - () => { - this.plugin.insertLinkDialog.start(this.file.path, (markdownlink: string, path:string, alias:string) => this.addLink(markdownlink, path, alias)); - }, - onClose - ), - // Add more context menu actions here if needed - ]); - contextMenuActions.push([ - renderContextMenuAction( - React, - t("PASTE_CODEBLOCK"), - async () => { - const data = await navigator.clipboard?.readText(); - if(!data || data.trim() === "") return; - this.pasteCodeBlock(data); - }, - onClose - ), - ]) - } - - if(contextMenuActions.length === 0) return; - return React.createElement ( - "div", - {}, - ...contextMenuActions, - React.createElement( - "hr", - { - key: nanoid(), - className: "context-menu-item-separator", - }, - ) - ); - } - - private actionOpenScriptInstallPrompt() { - new ScriptInstallPrompt(this.plugin).open(); - } - - private actionOpenExportImageDialog() { - if(!this.exportDialog) { - this.exportDialog = new ExportDialog(this.plugin, this,this.file); - this.exportDialog.createForm(); - } - this.exportDialog.open(); - } - - private setExcalidrawAPI (api: ExcalidrawImperativeAPI) { - this.excalidrawAPI = api; - //api.setLocalFont(this.plugin.settings.experimentalEnableFourthFont); - window.setTimeout(() => { - this.onAfterLoadScene(true); - this.excalidrawContainer?.focus(); - }); - }; - - private ttdDialog() { - return this.packages.react.createElement( - this.packages.excalidrawLib.TTDDialog, - { - onTextSubmit: async (input:string) => { - try { - const response = await postOpenAI({ - systemPrompt: "The user will provide you with a text prompt. Your task is to generate a mermaid diagram based on the prompt. Use the graph, sequenceDiagram, flowchart or classDiagram types based on what best fits the request. Return a single message containing only the mermaid diagram in a codeblock. Avoid the use of `()` parenthesis in the mermaid script.", - text: input, - instruction: "Return a single message containing only the mermaid diagram in a codeblock.", - }) - - if(!response) { - return { - error: new Error("Request failed"), - }; - } - - const json = response.json; - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.ttdDialog, `ExcalidrawView.ttdDialog > onTextSubmit, openAI response`, response); - - if (json?.error) { - log(response); - return { - error: new Error(json.error.message), - }; - } - - if(!json?.choices?.[0]?.message?.content) { - log(response); - return { - error: new Error("Generation failed... see console log for details"), - }; - } - - let generatedResponse = extractCodeBlocks(json.choices[0]?.message?.content)[0]?.data; - - if(!generatedResponse) { - log(response); - return { - error: new Error("Generation failed... see console log for details"), - }; - } - - if(generatedResponse.startsWith("mermaid")) { - generatedResponse = generatedResponse.replace(/^mermaid/,"").trim(); - } - - return { generatedResponse, rateLimit:100, rateLimitRemaining:100 }; - } catch (err: any) { - throw new Error("Request failed"); - } - }, - } - ); - }; - - private diagramToCode() { - return this.packages.react.createElement( - this.packages.excalidrawLib.DiagramToCodePlugin, - { - generate: async ({ frame, children }: - {frame: ExcalidrawMagicFrameElement, children: readonly ExcalidrawElement[]}) => { - const appState = this.excalidrawAPI.getAppState(); - try { - const blob = await this.packages.excalidrawLib.exportToBlob({ - elements: children, - appState: { - ...appState, - exportBackground: true, - viewBackgroundColor: appState.viewBackgroundColor, - }, - exportingFrame: frame, - files: this.excalidrawAPI.getFiles(), - mimeType: "image/jpeg", - }); - - const dataURL = await this.packages.excalidrawLib.getDataURL(blob); - const textFromFrameChildren = this.packages.excalidrawLib.getTextFromElements(children); - - const response = await diagramToHTML ({ - image:dataURL, - apiKey: this.plugin.settings.openAIAPIToken, - text: textFromFrameChildren, - theme: appState.theme, - }); - - if (!response.ok) { - const json = await response.json(); - const text = json.error?.message || "Unknown error during generation"; - return { - html: errorHTML(text), - }; - } - - const json = await response.json(); - if(json.choices[0].message.content == null) { - return { - html: errorHTML("Nothing generated"), - }; - } - - const message = json.choices[0].message.content; - - const html = message.slice( - message.indexOf(""), - message.indexOf("") + "".length, - ); - - return { html }; - } catch (err: any) { - return { - html: errorHTML("Request failed"), - }; - } - }, - } - ); - } - - - private ttdDialogTrigger() { - return this.packages.react.createElement( - this.packages.excalidrawLib.TTDDialogTrigger, - {}, - ); - } - - private renderWelcomeScreen() { - if (!this.plugin.settings.showSplashscreen) return null; - const React = this.packages.react; - const { WelcomeScreen } = this.packages.excalidrawLib; - const filecount = this.app.vault.getFiles().filter(f => this.plugin.isExcalidrawFile(f)).length; - const rank = filecount < 200 ? "Bronze" : filecount < 750 ? "Silver" : filecount < 2000 ? "Gold" : "Platinum"; - const nextRankDelta = filecount < 200 ? 200 - filecount : filecount < 750 ? 750 - filecount : filecount < 2000 ? 2000 - filecount : 0; - const { decoration, title } = SwordColors[rank as Rank]; - return React.createElement( - WelcomeScreen, - {}, - React.createElement( - WelcomeScreen.Center, - {}, - React.createElement( - WelcomeScreen.Center.Logo, - {}, - React.createElement( - LogoWrapper, - {}, - excalidrawSword(rank as Rank), - ), - ), - React.createElement( - WelcomeScreen.Center.Heading, - { - color: decoration, - message: nextRankDelta > 0 - ? `${rank}: ${nextRankDelta} ${t("WELCOME_RANK_NEXT")}` - : `${rank}: ${t("WELCOME_RANK_LEGENDARY")}`, - }, - title, - ), - React.createElement( - WelcomeScreen.Center.Heading, - {}, - t("WELCOME_COMMAND_PALETTE"), - React.createElement("br"), - t("WELCOME_OBSIDIAN_MENU"), - React.createElement("br"), - t("WELCOME_SCRIPT_LIBRARY"), - React.createElement("br"), - t("WELCOME_HELP_MENU"), - ), - React.createElement( - WelcomeScreen.Center.Menu, - {}, - React.createElement( - WelcomeScreen.Center.MenuItemLink, - { - icon: ICONS.YouTube, - href: "https://www.youtube.com/@VisualPKM", - shortcut: null, - "aria-label": t("WELCOME_YOUTUBE_ARIA"), - }, - t("WELCOME_YOUTUBE_LINK") - ), - React.createElement( - WelcomeScreen.Center.MenuItemLink, - { - icon: ICONS.Discord, - href: "https://discord.gg/DyfAXFwUHc", - shortcut: null, - "aria-label": t("WELCOME_DISCORD_ARIA"), - }, - t("WELCOME_DISCORD_LINK") - ), - React.createElement( - WelcomeScreen.Center.MenuItemLink, - { - icon: ICONS.twitter, - href: "https://twitter.com/zsviczian", - shortcut: null, - "aria-label": t("WELCOME_TWITTER_ARIA"), - }, - t("WELCOME_TWITTER_LINK") - ), - React.createElement( - WelcomeScreen.Center.MenuItemLink, - { - icon: ICONS.Learn, - href: "https://visual-thinking-workshop.com", - shortcut: null, - "aria-label": t("WELCOME_LEARN_ARIA"), - }, - t("WELCOME_LEARN_LINK") - ), - React.createElement( - WelcomeScreen.Center.MenuItemLink, - { - icon: ICONS.heart, - href: "https://ko-fi.com/zsolt", - shortcut: null, - "aria-label": t("WELCOME_DONATE_ARIA"), - }, - t("WELCOME_DONATE_LINK") - ), - ) - ) - ); - } - - private renderCustomActionsMenu () { - const React = this.packages.react; - const {MainMenu} = this.packages.excalidrawLib; - - return React.createElement( - MainMenu, - {}, - React.createElement(MainMenu.DefaultItems.ChangeCanvasBackground), - React.createElement(MainMenu.DefaultItems.ToggleTheme), - React.createElement(MainMenu.Separator), - !DEVICE.isPhone ? React.createElement( - MainMenu.Item, - { - icon: ICONS.trayMode, - "aria-label": t("ARIA_LABEL_TRAY_MODE"), - onSelect: ()=> this.toggleTrayMode(), - }, - "Toggle tray-mode" - ) : null, - React.createElement( - MainMenu.Item, - { - icon: saveIcon(false), - "aria-label": t("FORCE_SAVE"), - onSelect: ()=> this.forceSave(), - }, - "Save" - ), - React.createElement( - MainMenu.Item, - { - icon: ICONS.scriptEngine, - "aria-label": "Explore the Excalidraw Script Library", - onSelect: ()=> this.actionOpenScriptInstallPrompt(), - }, - "Script Library" - ), - React.createElement( - MainMenu.Item, - { - icon: ICONS.ExportImage, - "aria-label": "Export image as PNG, SVG, or Excalidraw file", - onSelect: ()=> this.actionOpenExportImageDialog(), - }, - "Export Image..." - ), - React.createElement( - MainMenu.Item, - { - icon: ICONS.switchToMarkdown, - "aria-label": "Switch to markdown view", - onSelect: ()=> this.openAsMarkdown(), - }, - "Open as Markdown" - ), - React.createElement(MainMenu.Separator), - React.createElement(MainMenu.DefaultItems.Help), - React.createElement(MainMenu.DefaultItems.ClearCanvas), - ); - } - - private renderEmbeddable (element: NonDeletedExcalidrawElement, appState: UIAppState) { - const React = this.packages.react; - try { - const useExcalidrawFrame = useDefaultExcalidrawFrame(element); - - if(!this.file || !element || !element.link || element.link.length === 0 || useExcalidrawFrame) { - return null; - } - - if(element.link.match(REG_LINKINDEX_HYPERLINK) || element.link.startsWith("data:")) { - if(!useExcalidrawFrame) { - return renderWebView(element.link, this, element.id, appState); - } else { - return null; - } - } - - const res = REGEX_LINK.getRes(element.link).next(); - if(!res || (!res.value && res.done)) { - return null; - } - - let linkText = REGEX_LINK.getLink(res); - - if(linkText.match(REG_LINKINDEX_HYPERLINK)) { - if(!useExcalidrawFrame) { - return renderWebView(linkText, this, element.id, appState); - } else { - return null; - } - } - - return React.createElement(CustomEmbeddable, {element,view:this, appState, linkText}); - } catch(e) { - return null; - } - } - - private renderEmbeddableMenu(appState: AppState) { - return this.embeddableMenu?.renderButtons(appState); - } - - private renderToolsPanel(observer: any) { - const React = this.packages.react; - - return React.createElement( - ToolsPanel, - { - ref: this.toolsPanelRef, - visible: false, - view: new WeakRef(this), - centerPointer: ()=>this.setCurrentPositionToCenter(), - observer: new WeakRef(observer.current), - } - ); - } - - private renderTopRightUI (isMobile: boolean, appState: AppState) { - return this.obsidianMenu?.renderButton (isMobile, appState); - } - - private onExcalidrawResize () { - try { - const api = this.excalidrawAPI as ExcalidrawImperativeAPI; - if(!api) return; - const width = this.contentEl.clientWidth; - const height = this.contentEl.clientHeight; - if(width === 0 || height === 0) return; - - //this is an aweful hack to prevent the on-screen keyboard pushing the canvas out of view. - //The issue is that contrary to Excalidraw.com where the page is simply pushed up, in - //Obsidian the leaf has a fixed top. As a consequence the top of excalidrawWrapperDiv does not get pushed out of view - //but shirnks. But the text area is positioned relative to excalidrawWrapperDiv and consequently does not fit, which - //the distorts the whole layout. - //I hope to grow up one day and clean up this mess of a workaround, that resets the top of excalidrawWrapperDiv - //to a negative value, and manually scrolls back elements that were scrolled off screen - //I tried updating setDimensions with the value for top... but setting top and height using setDimensions did not do the trick - //I found that adding and removing this style solves the issue. - //...again, just aweful, but works. - const st = api.getAppState(); - //isEventOnSameElement attempts to solve https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1729 - //the issue is that when the user hides the keyboard with the keyboard hide button and not tapping on the screen, then editingTextElement is not null - const isEventOnSameElement = this.editingTextElementId === st.editingTextElement?.id; - const isKeyboardOutEvent:Boolean = st.editingTextElement && !isEventOnSameElement; - const isKeyboardBackEvent:Boolean = (this.semaphores.isEditingText || isEventOnSameElement) && !isKeyboardOutEvent; - this.editingTextElementId = isKeyboardOutEvent ? st.editingTextElement.id : null; - if(isKeyboardOutEvent) { - const appToolHeight = (this.contentEl.querySelector(".Island.App-toolbar") as HTMLElement)?.clientHeight ?? 0; - const editingElViewY = sceneCoordsToViewportCoords({sceneX:0, sceneY:st.editingTextElement.y}, st).y; - const scrollViewY = sceneCoordsToViewportCoords({sceneX:0, sceneY:-st.scrollY}, st).y; - const delta = editingElViewY - scrollViewY; - const isElementAboveKeyboard = height > (delta + appToolHeight*2) - const excalidrawWrapper = this.excalidrawWrapperRef.current; - //console.log({isElementAboveKeyboard}); - if(excalidrawWrapper && !isElementAboveKeyboard) { - excalidrawWrapper.style.top = `${-(st.height - height)}px`; - excalidrawWrapper.style.height = `${st.height}px`; - this.excalidrawContainer?.querySelector(".App-bottom-bar")?.scrollIntoView(); - this.headerEl?.scrollIntoView(); - } - } - if(isKeyboardBackEvent) { - const excalidrawWrapper = this.excalidrawWrapperRef.current; - const appButtonBar = this.excalidrawContainer?.querySelector(".App-bottom-bar"); - const headerEl = this.headerEl; - if(excalidrawWrapper) { - excalidrawWrapper.style.top = ""; - excalidrawWrapper.style.height = ""; - appButtonBar?.scrollIntoView(); - headerEl?.scrollIntoView(); - } - } - //end of aweful hack - - if (this.toolsPanelRef && this.toolsPanelRef.current) { - this.toolsPanelRef.current.updatePosition(); - } - if(this.ownerDocument !== document) { - this.refreshCanvasOffset(); //because resizeobserver in Excalidraw does not seem to work when in Obsidian Window - } - } catch (err) { - errorlog({ - where: "Excalidraw React-Wrapper, onResize", - error: err, - }); - } - }; - - private excalidrawRootElement( - initdata: { - elements: any, - appState: any, - files: any, - libraryItems: any - }, - ) { - const React = this.packages.react; - const {Excalidraw} = this.packages.excalidrawLib; - - const excalidrawWrapperRef = React.useRef(null); - const toolsPanelRef = React.useRef(null); - const embeddableMenuRef = React.useRef(null); - this.toolsPanelRef = toolsPanelRef; - // const [dimensions, setDimensions] = React.useState({ - // width: undefined, - // height: undefined, - // }); - - React.useEffect(() => { - this.embeddableMenuRef = embeddableMenuRef; - this.obsidianMenu = new ObsidianMenu(this.plugin, toolsPanelRef, this); - this.embeddableMenu = new EmbeddableMenu(this, embeddableMenuRef); - this.excalidrawWrapperRef = excalidrawWrapperRef; - return () => { - this.obsidianMenu.destroy(); - this.obsidianMenu = null; - this.embeddableMenu.destroy(); - this.embeddableMenu = null; - this.toolsPanelRef.current = null; - this.embeddableMenuRef.current = null; - this.excalidrawWrapperRef.current = null; - } - }, []); - - //React.useEffect(() => { - // setDimensions({ - // width: this.contentEl.clientWidth, - // height: this.contentEl.clientHeight, - // }); - - // const onResize = () => { - // const width = this.contentEl.clientWidth; - // const height = this.contentEl.clientHeight; - // setDimensions({ width, height }); - // }; - - // this.ownerWindow.addEventListener("resize", onResize); - // return () => { - // this.ownerWindow.removeEventListener("resize", onResize); - // }; - // }, [excalidrawWrapperRef]); - - const observer = React.useRef( - new ResizeObserver((entries) => { - if(!toolsPanelRef || !toolsPanelRef.current) return; - const { width, height } = entries[0].contentRect; - if(width===0 || height ===0) return; - const dx = toolsPanelRef.current.onRightEdge - ? toolsPanelRef.current.previousWidth - width - : 0; - const dy = toolsPanelRef.current.onBottomEdge - ? toolsPanelRef.current.previousHeight - height - : 0; - toolsPanelRef.current.updatePosition(dy, dx); - }), - ); - - React.useEffect(() => { - if (toolsPanelRef?.current) { - observer.current.observe(toolsPanelRef.current.containerRef.current); - } - return () => { - //unobserve is done in ToolsPanel componentWillUnmount - }; - }, [toolsPanelRef, observer]); - - //--------------------------------------------------------------------------------- - //--------------------------------------------------------------------------------- - // Render Excalidraw DIV - //--------------------------------------------------------------------------------- - //--------------------------------------------------------------------------------- - return React.createElement( - React.Fragment, - null, - React.createElement( - "div", - { - className: "excalidraw-wrapper", - ref: excalidrawWrapperRef, - key: "abc", - tabIndex: 0, - onKeyDown: this.excalidrawDIVonKeyDown.bind(this), - 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), - }, - React.createElement( - Excalidraw, - { - excalidrawAPI: (this.setExcalidrawAPI.bind(this)), - width: "100%", //dimensions.width, - height: "100%", //dimensions.height, - UIOptions: - { - canvasActions: - { - loadScene: false, - saveScene: false, - saveAsScene: false, - export: false, - saveAsImage: false, - saveToActiveFile: false, - }, - }, - initState: initdata?.appState, - initialData: initdata, - detectScroll: true, - onPointerUpdate: this.onPointerUpdate.bind(this), - libraryReturnUrl: "app://obsidian.md", - autoFocus: true, - langCode: obsidianToExcalidrawMap[this.plugin.locale]??"en-EN", - aiEnabled: true, - onChange: this.onChange.bind(this), - onLibraryChange: this.onLibraryChange.bind(this), - renderTopRightUI: this.renderTopRightUI.bind(this), //(isMobile: boolean, appState: AppState) => this.obsidianMenu.renderButton (isMobile, appState), - renderEmbeddableMenu: this.renderEmbeddableMenu.bind(this), - onPaste: this.onPaste.bind(this), - onThemeChange: this.onThemeChange.bind(this), - onDrop: this.onDrop.bind(this), - onBeforeTextEdit: this.onBeforeTextEdit.bind(this), - onBeforeTextSubmit: this.onBeforeTextSubmit.bind(this), - onLinkOpen: this.onLinkOpen.bind(this), - onLinkHover: this.onLinkHover.bind(this), - onContextMenu: this.onContextMenu.bind(this), - onViewModeChange: this.onViewModeChange.bind(this), - validateEmbeddable: true, - renderWebview: DEVICE.isDesktop, - renderEmbeddable: this.renderEmbeddable.bind(this), - renderMermaid: shouldRenderMermaid, - showDeprecatedFonts: true, - insertLinkAction: this.insertLinkAction.bind(this), - }, - this.renderCustomActionsMenu(), - this.renderWelcomeScreen(), - this.ttdDialog(), - this.diagramToCode(), - this.ttdDialogTrigger(), - ), - this.renderToolsPanel(observer), - ), - ); - } - - private async instantiateExcalidraw( - initdata: { - elements: any, - appState: any, - files: any, - libraryItems: any - } - ) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.instantiateExcalidraw, "ExcalidrawView.instantiateExcalidraw", initdata); - await this.plugin.awaitInit(); - while(!this.semaphores.scriptsReady) { - await sleep(50); - } - const React = this.packages.react; - const ReactDOM = this.packages.reactDOM; - //console.log("ExcalidrawView.instantiateExcalidraw()"); - this.clearDirty(); - - this.excalidrawRoot = ReactDOM.createRoot(this.contentEl); - this.excalidrawRoot.render(React.createElement(this.excalidrawRootElement.bind(this,initdata))); - } - - private updateContainerSize(containerId?: string, delay: boolean = false, justloaded: boolean = false) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updateContainerSize, "ExcalidrawView.updateContainerSize", containerId, delay); - const api = this.excalidrawAPI; - if (!api) { - return; - } - const update = () => { - const containers = containerId - ? api - .getSceneElements() - .filter((el: ExcalidrawElement) => el.id === containerId && el.type!=="arrow") - : api - .getSceneElements() - .filter(isContainer); - if (containers.length > 0) { - if (justloaded) { - //updateContainerSize will bump scene version which will trigger a false autosave - //after load, which will lead to a ping-pong between two synchronizing devices - this.semaphores.justLoaded = true; - } - api.updateContainerSize(containers); - } - }; - if (delay) { - window.setTimeout(() => update(), 50); - } else { - update(); - } - } - - public zoomToFit(delay: boolean = true, justLoaded: boolean = false) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.zoomToFit, "ExcalidrawView.zoomToFit", delay, justLoaded); - //view is closing via onWindowMigrated - if(this.semaphores?.viewunload) { - return; - } - const modalContainer = document.body.querySelector("div.modal-container"); - if(modalContainer) return; //do not autozoom when the command palette or other modal container is envoked on iPad - const api = this.excalidrawAPI; - if (!api || this.semaphores.isEditingText || this.semaphores.preventAutozoom) { - return; - } - if (windowMigratedDisableZoomOnce) { - windowMigratedDisableZoomOnce = false; - return; - } - const maxZoom = this.plugin.settings.zoomToFitMaxLevel; - const elements = api.getSceneElements().filter((el:ExcalidrawElement)=>el.width<10000 && el.height<10000); - if((DEVICE.isMobile && elements.length>1000) || elements.length>2500) { - if(justLoaded) api.scrollToContent(); - return; - } - if (delay) { - //time for the DOM to render, I am sure there is a more elegant solution - window.setTimeout( - () => api.zoomToFit(elements, maxZoom, this.isFullscreen() ? 0 : 0.05), - 100, - ); - } else { - api.zoomToFit(elements, maxZoom, this.isFullscreen() ? 0 : 0.05); - } - } - - public updatePinnedScripts() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updatePinnedScripts, "ExcalidrawView.updatePinnedScripts"); - const api = this.excalidrawAPI as ExcalidrawImperativeAPI; - if (!api) { - return false; - } - api.updateScene({ - appState: { pinnedScripts: this.plugin.settings.pinnedScripts }, - storeAction: "update", - }); - } - - public updatePinnedCustomPens() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updatePinnedCustomPens, "ExcalidrawView.updatePinnedCustomPens"); - const api = this.excalidrawAPI as ExcalidrawImperativeAPI; - if (!api) { - return false; - } - api.updateScene({ - appState: { - customPens: this.plugin.settings.customPens.slice(0,this.plugin.settings.numberOfCustomPens), - }, - storeAction: "update", - }); - } - - public updatePinchZoom() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updatePinchZoom, "ExcalidrawView.updatePinchZoom"); - const api = this.excalidrawAPI as ExcalidrawImperativeAPI; - if (!api) { - return false; - } - api.updateScene({ - appState: { allowPinchZoom: this.plugin.settings.allowPinchZoom }, - storeAction: "update", - }); - } - - public updateWheelZoom() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updateWheelZoom, "ExcalidrawView.updateWheelZoom"); - const api = this.excalidrawAPI as ExcalidrawImperativeAPI; - if (!api) { - return false; - } - api.updateScene({ - appState: { allowWheelZoom: this.plugin.settings.allowWheelZoom }, - storeAction: "update", - }); - } - - public async toggleTrayMode() { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.toggleTrayMode, "ExcalidrawView.toggleTrayMode"); - const api = this.excalidrawAPI as ExcalidrawImperativeAPI; - if (!api) { - return false; - } - const st = api.getAppState(); - api.updateScene({ - appState: { trayModeEnabled: !st.trayModeEnabled }, - storeAction: "update", - }); - - //just in case settings were updated via Obsidian sync - await this.plugin.loadSettings(); - this.plugin.settings.defaultTrayMode = !st.trayModeEnabled; - this.plugin.saveSettings(); - } - - /** - * - * @param elements - * @param query - * @param selectResult - * @param exactMatch - * @param selectGroup - * @returns true if element found, false if no element is found. - */ - - public selectElementsMatchingQuery( - elements: ExcalidrawElement[], - query: string[], - selectResult: boolean = true, - exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530 - selectGroup: boolean = false, - ):boolean { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.selectElementsMatchingQuery, "ExcalidrawView.selectElementsMatchingQuery", query, selectResult, exactMatch, selectGroup); - let match = getTextElementsMatchingQuery( - elements.filter((el: ExcalidrawElement) => el.type === "text"), - query, - exactMatch, - ).concat(getFrameElementsMatchingQuery( - elements.filter((el: ExcalidrawElement) => el.type === "frame"), - query, - exactMatch, - )).concat(getElementsWithLinkMatchingQuery( - elements.filter((el: ExcalidrawElement) => el.link), - query, - exactMatch, - )).concat(getImagesMatchingQuery( - elements, - query, - this.excalidrawData, - exactMatch, - )); - - if (match.length === 0) { - new Notice(t("NO_SEARCH_RESULT")); - return false; - } - - if(selectGroup) { - const groupElements = this.plugin.ea.getElementsInTheSameGroupWithElement(match[0],elements) - if(groupElements.length>0) { - match = groupElements; - } - } - - this.zoomToElements(selectResult,match); - return true; - } - - public zoomToElements( - selectResult: boolean, - elements: ExcalidrawElement[] - ) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.zoomToElements, "ExcalidrawView.zoomToElements", selectResult, elements); - const api = this.excalidrawAPI; - if (!api) return; - - const zoomLevel = this.plugin.settings.zoomToFitMaxLevel; - if (selectResult) { - api.selectElements(elements, true); - } - api.zoomToFit(elements, zoomLevel, 0.05); - } - - public getViewElements(): ExcalidrawElement[] { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getViewElements, "ExcalidrawView.getViewElements"); - const api = this.excalidrawAPI; - if (!api) { - return []; - } - return api.getSceneElements(); - } - - /** - * - * @param deepSelect: if set to true, child elements of the selected frame will also be selected - * @returns - */ - public getViewSelectedElements(includFrameChildren: boolean = true): ExcalidrawElement[] { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getViewSelectedElements, "ExcalidrawView.getViewSelectedElements"); - const api = this.excalidrawAPI as ExcalidrawImperativeAPI; - if (!api) { - return []; - } - const selectedElements = api.getAppState()?.selectedElementIds; - if (!selectedElements) { - return []; - } - const selectedElementsKeys = Object.keys(selectedElements); - if (!selectedElementsKeys) { - return []; - } - - const elementIDs = new Set(); - - const elements: ExcalidrawElement[] = api - .getSceneElements() - .filter((e: any) => selectedElementsKeys.includes(e.id)); - - const containerBoundTextElmenetsReferencedInElements = elements - .filter( - (el) => - el.boundElements && - el.boundElements.filter((be) => be.type === "text").length > 0, - ) - .map( - (el) => - el.boundElements - .filter((be) => be.type === "text") - .map((be) => be.id)[0], - ); - - if(includFrameChildren && elements.some(el=>el.type === "frame")) { - elements.filter(el=>el.type === "frame").forEach(frameEl => { - api.getSceneElements() - .filter(el=>el.frameId === frameEl.id) - .forEach(el=>elementIDs.add(el.id)) - }) - } - - elements.forEach(el=>elementIDs.add(el.id)); - containerBoundTextElmenetsReferencedInElements.forEach(id=>elementIDs.add(id)); - - return api - .getSceneElements() - .filter((el: ExcalidrawElement) => elementIDs.has(el.id)); - } - - /** - * - * @param prefix - defines the default button. - * @returns - */ - public async copyLinkToSelectedElementToClipboard(prefix:string) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.copyLinkToSelectedElementToClipboard, "ExcalidrawView.copyLinkToSelectedElementToClipboard", prefix); - const elements = this.getViewSelectedElements(); - if (elements.length < 1) { - new Notice(t("INSERT_LINK_TO_ELEMENT_ERROR")); - return; - } - - let elementId:string = undefined; - - if(elements.length === 2) { - const textEl = elements.filter(el=>el.type==="text"); - if(textEl.length===1 && (textEl[0] as ExcalidrawTextElement).containerId) { - const container = elements.filter(el=>el.boundElements.some(be=>be.type==="text")) - if(container.length===1) { - elementId = textEl[0].id; - } - } - } - - if(!elementId) { - elementId = elements.length === 1 - ? elements[0].id - : this.plugin.ea.getLargestElement(elements).id; - } - - const frames = elements.filter(el=>el.type==="frame"); - const hasFrame = frames.length === 1; - const hasGroup = elements.some(el=>el.groupIds && el.groupIds.length>0); - - let button = { - area: {caption: "Area", action:()=>{prefix="area="; return;}}, - link: {caption: "Link", action:()=>{prefix="";return}}, - group: {caption: "Group", action:()=>{prefix="group="; return;}}, - frame: {caption: "Frame", action:()=>{prefix="frame="; elementId = frames[0].id; return;}}, - clippedframe: {caption: "Clipped Frame", action:()=>{prefix="clippedframe="; ; elementId = frames[0].id; return;}}, - } - - let buttons = []; - switch(prefix) { - case "area=": - buttons = [ - button.area, - button.link, - ...hasGroup ? [button.group] : [], - ...hasFrame ? [button.frame, button.clippedframe] : [], - ]; - break; - case "group=": - buttons = [ - ...hasGroup ? [button.group] : [], - button.link, - button.area, - ...hasFrame ? [button.frame, button.clippedframe] : [], - ]; - break; - case "frame=": - buttons = [ - ...hasFrame ? [button.frame, button.clippedframe] : [], - ...hasGroup ? [button.group] : [], - button.link, - button.area, - ]; - break; - case "clippedframe=": - buttons = [ - ...hasFrame ? [button.clippedframe, button.frame] : [], - ...hasGroup ? [button.group] : [], - button.link, - button.area, - ]; - break; - default: - buttons = [ - {caption: "Link", action:()=>{prefix="";return}}, - {caption: "Area", action:()=>{prefix="area="; return;}}, - {caption: "Group", action:()=>{prefix="group="; return;}}, - ...hasFrame ? [button.frame, button.clippedframe] : [], - ] - } - - const alias = await ScriptEngine.inputPrompt( - this, - this.plugin, - this.app, - "Set link alias", - "Leave empty if you do not want to set an alias", - "", - buttons, - ); - navigator.clipboard.writeText( - `${prefix.length>0?"!":""}[[${this.file.path}#^${prefix}${elementId}${alias ? `|${alias}` : ``}]]`, - ); - new Notice(t("INSERT_LINK_TO_ELEMENT_READY")); - } - - public updateScene( - scene: { - elements?: ExcalidrawElement[]; - appState?: any; - files?: any; - storeAction?: "capture" | "none" | "update"; //https://github.com/excalidraw/excalidraw/pull/7898 - }, - shouldRestore: boolean = false, - ) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updateScene, "ExcalidrawView.updateScene", scene, shouldRestore); - const api = this.excalidrawAPI; - if (!api) { - return; - } - const shouldRestoreElements = scene.elements && shouldRestore; - if (shouldRestoreElements) { - scene.elements = restore(scene, null, null).elements; - } - if(Boolean(scene.appState)) { - //@ts-ignore - scene.forceFlushSync = true; - } - try { - api.updateScene(scene); - } catch (e) { - errorlog({ - where: "ExcalidrawView.updateScene 1st attempt", - fn: this.updateScene, - error: e, - scene, - willDoSecondAttempt: !shouldRestoreElements, - }); - if (!shouldRestoreElements) { - //second attempt - try { - scene.elements = restore(scene, null, null).elements; - api.updateScene(scene); - } catch (e) { - errorlog({ - where: "ExcalidrawView.updateScene 2nd attempt", - fn: this.updateScene, - error: e, - scene, - }); - warningUnknowSeriousError(); - } - } else { - warningUnknowSeriousError(); - } - } - } - - public updateEmbeddableRef(id: string, ref: HTMLIFrameElement | HTMLWebViewElement | null) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updateEmbeddableRef, "ExcalidrawView.updateEmbeddableRef", id, ref); - if (ref) { - this.embeddableRefs.set(id, ref); - } - } - - public getEmbeddableElementById(id: string): HTMLIFrameElement | HTMLWebViewElement | undefined { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getEmbeddableElementById, "ExcalidrawView.getEmbeddableElementById", id); - return this.embeddableRefs.get(id); - } - - public updateEmbeddableLeafRef(id: string, ref: any) { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updateEmbeddableLeafRef, "ExcalidrawView.updateEmbeddableLeafRef", id, ref); - if(ref) { - this.embeddableLeafRefs.set(id, ref); - } - } - - public getEmbeddableLeafElementById(id: string): {leaf: WorkspaceLeaf; node?: ObsidianCanvasNode; editNode?: Function} | null { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getEmbeddableLeafElementById, "ExcalidrawView.getEmbeddableLeafElementById", id); - if(!id) return null; - const ref = this.embeddableLeafRefs.get(id); - if(!ref) { - return null; - } - return ref as {leaf: WorkspaceLeaf; node?: ObsidianCanvasNode; editNode?: Function}; - } - - public getActiveEmbeddable ():{leaf: WorkspaceLeaf; node?: ObsidianCanvasNode; editNode?: Function}|null { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getActiveEmbeddable, "ExcalidrawView.getActiveEmbeddable"); - if(!this.excalidrawAPI) return null; - const api = this.excalidrawAPI as ExcalidrawImperativeAPI; - const st = api.getAppState(); - if(!st.activeEmbeddable || st.activeEmbeddable.state !== "active" ) return null; - return this.getEmbeddableLeafElementById(st.activeEmbeddable?.element?.id); - } - - get editor(): any { - const embeddable = this.getActiveEmbeddable(); - if(embeddable) { - if(embeddable.node && embeddable.node.isEditing) { - return embeddable.node.child.editor; - } - if(embeddable.leaf?.view instanceof MarkdownView) { - return embeddable.leaf.view.editor; - } - } - return null; - } -} - -export function getTextMode(data: string): TextMode { - (process.env.NODE_ENV === 'development') && DEBUGGING && debug(getTextMode, "ExcalidrawView.getTextMode", data); - const parsed = - data.search("excalidraw-plugin: parsed\n") > -1 || - data.search("excalidraw-plugin: locked\n") > -1; //locked for backward compatibility - return parsed ? TextMode.parsed : TextMode.raw; -} +import { + TextFileView, + WorkspaceLeaf, + normalizePath, + TFile, + WorkspaceItem, + Notice, + Menu, + MarkdownView, + request, + requireApiVersion, + HoverParent, + HoverPopover, +} from "obsidian"; +//import * as React from "react"; +//import * as ReactDOM from "react-dom"; +//import Excalidraw from "@zsviczian/excalidraw"; +import { + ExcalidrawElement, + ExcalidrawImageElement, + ExcalidrawMagicFrameElement, + ExcalidrawTextElement, + FileId, + NonDeletedExcalidrawElement, +} from "@zsviczian/excalidraw/types/excalidraw/element/types"; +import { + AppState, + BinaryFileData, + DataURL, + ExcalidrawImperativeAPI, + Gesture, + LibraryItems, + UIAppState, +} from "@zsviczian/excalidraw/types/excalidraw/types"; +import { + VIEW_TYPE_EXCALIDRAW, + ICON_NAME, + DISK_ICON_NAME, + SCRIPTENGINE_ICON_NAME, + TEXT_DISPLAY_RAW_ICON_NAME, + TEXT_DISPLAY_PARSED_ICON_NAME, + IMAGE_TYPES, + REG_LINKINDEX_INVALIDCHARS, + KEYCODE, + FRONTMATTER_KEYS, + DEVICE, + GITHUB_RELEASES, + EXPORT_IMG_ICON_NAME, + viewportCoordsToSceneCoords, + ERROR_IFRAME_CONVERSION_CANCELED, + restore, + obsidianToExcalidrawMap, + MAX_IMAGE_SIZE, + fileid, + sceneCoordsToViewportCoords, + MD_EX_SECTIONS, + refreshTextDimensions, + getContainerElement, +} from "../Constants/Constants"; +import ExcalidrawPlugin from "../Core/main"; +import { + repositionElementsToCursor, + ExcalidrawAutomate, + getTextElementsMatchingQuery, + cloneElement, + getFrameElementsMatchingQuery, + getElementsWithLinkMatchingQuery, + getImagesMatchingQuery, + getBoundTextElementId +} from "../Shared/ExcalidrawAutomate"; +import { t } from "../Lang/Helpers"; +import { + ExcalidrawData, + REG_LINKINDEX_HYPERLINK, + REGEX_LINK, + AutoexportPreference, + getExcalidrawMarkdownHeaderSection, +} from "../Shared/ExcalidrawData"; +import { + checkAndCreateFolder, + download, + getDataURLFromURL, + getIMGFilename, + getInternalLinkOrFileURLLink, + getMimeType, + getNewUniqueFilepath, + getURLImageExtension, +} from "../Utils/FileUtils"; +import { + checkExcalidrawVersion, + errorlog, + getEmbeddedFilenameParts, + getExportTheme, + getPNG, + getPNGScale, + getSVG, + getExportPadding, + getWithBackground, + hasExportTheme, + scaleLoadedImage, + svgToBase64, + hyperlinkIsImage, + hyperlinkIsYouTubeLink, + getYouTubeThumbnailLink, + isContainer, + fragWithHTML, + isMaskFile, + shouldEmbedScene, + _getContainerElement, + arrayToMap, +} from "../Utils/Utils"; +import { cleanBlockRef, cleanSectionHeading, closeLeafView, getAttachmentsFolderAndFilePath, getLeaf, getParentOfClass, obsidianPDFQuoteWithRef, openLeaf, setExcalidrawView } from "../Utils/ObsidianUtils"; +import { splitFolderAndFilename } from "../Utils/FileUtils"; +import { ConfirmationPrompt, GenericInputPrompt, NewFileActions, Prompt, linkPrompt } from "../Shared/Dialogs/Prompt"; +import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard"; +import { updateEquation } from "../Shared/LaTeX"; +import { + EmbeddedFile, + EmbeddedFilesLoader, + FileData, + generateIdFromFile, +} from "../Shared/EmbeddedFileLoader"; +import { ScriptInstallPrompt } from "../Shared/Dialogs/ScriptInstallPrompt"; +import { ObsidianMenu } from "./Components/Menu/ObsidianMenu"; +import { ToolsPanel } from "./Components/Menu/ToolsPanel"; +import { ScriptEngine } from "../Shared/Scripts"; +import { getTextElementAtPointer, getImageElementAtPointer, getElementWithLinkAtPointer } from "../Utils/GetElementAtPointer"; +import { excalidrawSword, ICONS, LogoWrapper, Rank, saveIcon, SwordColors } from "./Components/Menu/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 { 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 "../Utils/ImageCache"; +import { CanvasNodeFactory, ObsidianCanvasNode } from "../Utils/CanvasNodeFactory"; +import { EmbeddableMenu } from "./Components/Menu/EmbeddableActionsMenu"; +import { useDefaultExcalidrawFrame } from "../Utils/CustomEmbeddableUtils"; +import { UniversalInsertFileModal } from "../Shared/Dialogs/UniversalInsertFileModal"; +import { getMermaidText, shouldRenderMermaid } from "../Utils/MermaidUtils"; +import { nanoid } from "nanoid"; +import { CustomMutationObserver, DEBUGGING, debug, log} from "../Utils/DebugHelper"; +import { errorHTML, extractCodeBlocks, postOpenAI } from "../Utils/AIUtils"; +import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types"; +import { SelectCard } from "../Shared/Dialogs/SelectCard"; +import { Packages } from "../Types/Types"; +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"; + +const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000; +const PREVENT_RELOAD_TIMEOUT = 2000; +const RE_TAIL = /^## Drawing\n.*```\n%%$(.*)/ms; + +declare const PLUGIN_VERSION:string; + +declare module "obsidian" { + interface Workspace { + floatingSplit: any; + } + + interface WorkspaceSplit { + containerEl: HTMLDivElement; + } +} + +type SelectedElementWithLink = { id: string; text: string }; +type SelectedImage = { id: string; fileId: FileId }; + +export enum TextMode { + parsed = "parsed", + raw = "raw", +} + +interface WorkspaceItemExt extends WorkspaceItem { + containerEl: HTMLElement; +} + +export interface ExportSettings { + withBackground: boolean; + withTheme: boolean; + isMask: boolean; + frameRendering?: { //optional, overrides relevant appState settings for rendering the frame + enabled: boolean; + name: boolean; + outline: boolean; + clip: boolean; + }; + skipInliningFonts?: boolean; +} + +const HIDE = "excalidraw-hidden"; +const SHOW = "excalidraw-visible"; + +export const addFiles = async ( + files: FileData[], + view: ExcalidrawView, + isDark?: boolean, +) => { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(addFiles, "ExcalidrawView.addFiles", files, view, isDark); + if (!files || files.length === 0 || !view) { + return; + } + const api = view.excalidrawAPI; + if (!api) { + return; + } + + //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/544 + files = files.filter( + (f) => f && f.size && f.size.height > 0 && f.size.width > 0, + ); //height will be zero when file does not exisig in case of broken embedded file links + if (files.length === 0) { + return; + } + const s = scaleLoadedImage(view.getScene(), files); + if (isDark === undefined) { + isDark = s.scene.appState.theme; + } + // update element.crop naturalWidth and naturalHeight in case scale of PDF loading has changed + // update crop.x crop.y, crop.width, crop.height according to the new scale + files + .filter((f:FileData) => view.excalidrawData.getFile(f.id)?.file?.extension === "pdf") + .forEach((f:FileData) => { + s.scene.elements + .filter((el:ExcalidrawElement)=>el.type === "image" && el.fileId === f.id && el.crop && el.crop.naturalWidth !== f.size.width) + .forEach((el:Mutable) => { + s.dirty = true; + const scale = f.size.width / el.crop.naturalWidth; + el.crop = { + x: el.crop.x * scale, + y: el.crop.y * scale, + width: el.crop.width * scale, + height: el.crop.height * scale, + naturalWidth: f.size.width, + naturalHeight: f.size.height, + }; + }); + }); + + if (s.dirty) { + //debug({where:"ExcalidrawView.addFiles",file:view.file.name,dataTheme:view.excalidrawData.scene.appState.theme,before:"updateScene",state:scene.appState}) + view.updateScene({ + elements: s.scene.elements, + appState: s.scene.appState, + storeAction: "update", + }); + } + for (const f of files) { + if (view.excalidrawData.hasFile(f.id)) { + const embeddedFile = view.excalidrawData.getFile(f.id); + + embeddedFile.setImage( + f.dataURL, + f.mimeType, + f.size, + isDark, + f.hasSVGwithBitmap, + ); + } + if (view.excalidrawData.hasEquation(f.id)) { + const latex = view.excalidrawData.getEquation(f.id).latex; + view.excalidrawData.setEquation(f.id, { latex, isLoaded: true }); + } + } + api.addFiles(files); +}; + +const warningUnknowSeriousError = () => { + new Notice(t("WARNING_SERIOUS_ERROR"),60000); +}; + +type ActionButtons = "save" | "isParsed" | "isRaw" | "link" | "scriptInstall"; + +let windowMigratedDisableZoomOnce = false; + +export default class ExcalidrawView extends TextFileView implements HoverParent{ + public hoverPopover: HoverPopover; + private freedrawLastActiveTimestamp: number = 0; + public exportDialog: ExportDialog; + public excalidrawData: ExcalidrawData; + //public excalidrawRef: React.MutableRefObject = null; + public excalidrawRoot: any; + public excalidrawAPI:any = null; + public excalidrawWrapperRef: React.MutableRefObject = null; + public toolsPanelRef: React.MutableRefObject = null; + public embeddableMenuRef: React.MutableRefObject = null; + private parentMoveObserver: MutationObserver | CustomMutationObserver; + public linksAlwaysOpenInANewPane: boolean = false; //override the need for SHIFT+CTRL+click (used by ExcaliBrain) + public allowFrameButtonsInViewMode: boolean = false; //override for ExcaliBrain + private _hookServer: ExcalidrawAutomate; + public lastSaveTimestamp: number = 0; //used to validate if incoming file should sync with open file + 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 + //Obsidian 0.15.0 + private draginfoDiv: HTMLDivElement; + public canvasNodeFactory: CanvasNodeFactory; + private embeddableRefs = new Map(); + private embeddableLeafRefs = new Map(); + + public semaphores: ViewSemaphores | null = { + warnAboutLinearElementLinkClick: true, + embeddableIsEditingSelf: false, + popoutUnload: false, + viewunload: false, + scriptsReady: false, + justLoaded: false, + preventAutozoom: false, + autosaving: false, + dirty: null, + preventReload: false, + isEditingText: false, + saving: false, + forceSaving: false, + hoverSleep: false, + wheelTimeout: null, + }; + + public _plugin: ExcalidrawPlugin; + public autosaveTimer: any = null; + public textMode: TextMode = TextMode.raw; + private actionButtons: Record = {} as Record; + public compatibilityMode: boolean = false; + private obsidianMenu: ObsidianMenu; + private embeddableMenu: EmbeddableMenu; + private destroyers: Function[] = []; + + //https://stackoverflow.com/questions/27132796/is-there-any-javascript-event-fired-when-the-on-screen-keyboard-on-mobile-safari + private isEditingTextResetTimer: number = null; + private preventReloadResetTimer: number = null; + private editingSelfResetTimer: number = null; + private colorChangeTimer:number = null; + private previousSceneVersion = 0; + public previousBackgroundColor = ""; + public previousTheme = ""; + + //variables used to handle click events in view mode + private selectedTextElement: SelectedElementWithLink = null; + private selectedImageElement: SelectedImage = null; + private selectedElementWithLink: SelectedElementWithLink = null; + private blockOnMouseButtonDown = false; + private doubleClickTimestamp = Date.now(); + + private hoverPoint = { x: 0, y: 0 }; + private hoverPreviewTarget: EventTarget = null; + private viewModeEnabled:boolean = false; + private lastMouseEvent: any = null; + private editingTextElementId: string = null; //storing to handle on-screen keyboard hide events +/* private lastSceneSnapshot: any = null; + private lastViewDataSnapshot: any = null;*/ + + id: string = (this.leaf as any).id; + public packages: Packages = {react: null, reactDOM: null, excalidrawLib: null}; + + constructor(leaf: WorkspaceLeaf, plugin: ExcalidrawPlugin) { + super(leaf); + this._plugin = plugin; + this.excalidrawData = new ExcalidrawData(plugin); + this.canvasNodeFactory = new CanvasNodeFactory(this); + this.setHookServer(); + } + + get hookServer (): ExcalidrawAutomate { + return this._hookServer; + } + get plugin(): ExcalidrawPlugin { + return this._plugin; + } + get excalidrawContainer(): HTMLDivElement { + return this.excalidrawWrapperRef?.current?.firstElementChild; + } + get ownerDocument(): Document { + return DEVICE.isMobile?document:this.containerEl.ownerDocument; + } + get ownerWindow(): Window { + return this.ownerDocument.defaultView; + } + + setHookServer(ea?:ExcalidrawAutomate) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setHookServer, "ExcalidrawView.setHookServer", ea); + if(ea) { + this._hookServer = ea; + } else { + this._hookServer = this._plugin.ea; + } + } + + private getHookServer () { + return this.hookServer ?? this.plugin.ea; + } + + preventAutozoom() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.preventAutozoom, "ExcalidrawView.preventAutozoom"); + this.semaphores.preventAutozoom = true; + window.setTimeout(() => { + if(!this.semaphores) return; + this.semaphores.preventAutozoom = false; + }, 1500); + } + + public saveExcalidraw(scene?: any) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.saveExcalidraw, "ExcalidrawView.saveExcalidraw", scene); + if (!scene) { + if(!this.excalidrawAPI) { + return; + } + scene = this.getScene(); + } + const filepath = `${this.file.path.substring( + 0, + this.file.path.lastIndexOf(".md"), + )}.excalidraw`; + const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath)); + if (file && file instanceof TFile) { + this.app.vault.modify(file, JSON.stringify(scene, null, "\t")); + } else { + this.app.vault.create(filepath, JSON.stringify(scene, null, "\t")); + } + } + + public async exportExcalidraw(selectedOnly?: boolean) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.exportExcalidraw, "ExcalidrawView.exportExcalidraw", selectedOnly); + if (!this.excalidrawAPI || !this.file) { + return; + } + if (DEVICE.isMobile) { + const prompt = new Prompt( + this.app, + t("EXPORT_FILENAME_PROMPT"), + this.file.basename, + t("EXPORT_FILENAME_PROMPT_PLACEHOLDER"), + ); + prompt.openAndGetValue(async (filename: string) => { + if (!filename) { + return; + } + filename = `${filename}.excalidraw`; + const folderpath = splitFolderAndFilename(this.file.path).folderpath; + await checkAndCreateFolder(folderpath); //create folder if it does not exist + const fname = getNewUniqueFilepath( + this.app.vault, + filename, + folderpath, + ); + this.app.vault.create( + fname, + JSON.stringify(this.getScene(), null, "\t"), + ); + new Notice(`Exported to ${fname}`, 6000); + }); + return; + } + download( + "data:text/plain;charset=utf-8", + encodeURIComponent(JSON.stringify(this.getScene(selectedOnly), null, "\t")), + `${this.file.basename}.excalidraw`, + ); + } + + public async svg(scene: any, theme?:string, embedScene?: boolean, embedFont: boolean = false): Promise { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.svg, "ExcalidrawView.svg", scene, theme, embedScene); + const ed = this.exportDialog; + + const exportSettings: ExportSettings = { + withBackground: ed ? !ed.transparent : getWithBackground(this.plugin, this.file), + withTheme: true, + isMask: isMaskFile(this.plugin, this.file), + skipInliningFonts: !embedFont, + }; + + if(typeof embedScene === "undefined") { + embedScene = shouldEmbedScene(this.plugin, this.file); + } + + return await getSVG( + { + ...scene, + ...{ + appState: { + ...scene.appState, + theme: theme ?? (ed ? ed.theme : getExportTheme(this.plugin, this.file, scene.appState.theme)), + exportEmbedScene: typeof embedScene === "undefined" + ? (ed ? ed.embedScene : false) + : embedScene, + }, + }, + }, + exportSettings, + ed ? ed.padding : getExportPadding(this.plugin, this.file), + this.file, + ); + } + + public async saveSVG(scene?: any, embedScene?: boolean) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.saveSVG, "ExcalidrawView.saveSVG", scene, embedScene); + if (!scene) { + if (!this.excalidrawAPI) { + return false; + } + scene = this.getScene(); + } + + const exportImage = async (filepath:string, theme?:string) => { + const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath)); + + const svg = await this.svg(scene,theme, embedScene, true); + if (!svg) { + return; + } + //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2026 + const svgString = svg.outerHTML; + if (file && file instanceof TFile) { + await this.app.vault.modify(file, svgString); + } else { + await this.app.vault.create(filepath, svgString); + } + } + + if(this.plugin.settings.autoExportLightAndDark) { + await exportImage(getIMGFilename(this.file.path, "dark.svg"),"dark"); + await exportImage(getIMGFilename(this.file.path, "light.svg"),"light"); + } else { + await exportImage(getIMGFilename(this.file.path, "svg")); + } + } + + public async exportSVG(embedScene?: boolean, selectedOnly?: boolean):Promise { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.exportSVG, "ExcalidrawView.exportSVG", embedScene, selectedOnly); + if (!this.excalidrawAPI || !this.file) { + return; + } + + const svg = await this.svg(this.getScene(selectedOnly),undefined,embedScene, true); + if (!svg) { + return; + } + download( + null, + svgToBase64(svg.outerHTML), + `${this.file.basename}.svg`, + ); + } + + public async png(scene: any, theme?:string, embedScene?: boolean): Promise { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.png, "ExcalidrawView.png", scene, theme, embedScene); + const ed = this.exportDialog; + + const exportSettings: ExportSettings = { + withBackground: ed ? !ed.transparent : getWithBackground(this.plugin, this.file), + withTheme: true, + isMask: isMaskFile(this.plugin, this.file), + }; + + if(typeof embedScene === "undefined") { + embedScene = shouldEmbedScene(this.plugin, this.file); + } + + return await getPNG( + { + ...scene, + ...{ + appState: { + ...scene.appState, + theme: theme ?? (ed ? ed.theme : getExportTheme(this.plugin, this.file, scene.appState.theme)), + exportEmbedScene: typeof embedScene === "undefined" + ? (ed ? ed.embedScene : false) + : embedScene, + }, + }, + }, + exportSettings, + ed ? ed.padding : getExportPadding(this.plugin, this.file), + ed ? ed.scale : getPNGScale(this.plugin, this.file), + ); + } + + public async savePNG(scene?: any, embedScene?: boolean) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.savePNG, "ExcalidrawView.savePNG", scene, embedScene); + if (!scene) { + if (!this.excalidrawAPI) { + return false; + } + scene = this.getScene(); + } + + const exportImage = async (filepath:string, theme?:string) => { + const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath)); + + const png = await this.png(scene, theme, embedScene); + if (!png) { + return; + } + if (file && file instanceof TFile) { + await this.app.vault.modifyBinary(file, await png.arrayBuffer()); + } else { + await this.app.vault.createBinary(filepath, await png.arrayBuffer()); + } + } + + if(this.plugin.settings.autoExportLightAndDark) { + await exportImage(getIMGFilename(this.file.path, "dark.png"),"dark"); + await exportImage(getIMGFilename(this.file.path, "light.png"),"light"); + } else { + await exportImage(getIMGFilename(this.file.path, "png")); + } + } + + public async exportPNGToClipboard(embedScene?:boolean, selectedOnly?: boolean) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.exportPNGToClipboard, "ExcalidrawView.exportPNGToClipboard", embedScene, selectedOnly); + if (!this.excalidrawAPI || !this.file) { + return; + } + + const png = await this.png(this.getScene(selectedOnly), undefined, embedScene); + if (!png) { + return; + } + + // in Safari so far we need to construct the ClipboardItem synchronously + // (i.e. in the same tick) otherwise browser will complain for lack of + // user intent. Using a Promise ClipboardItem constructor solves this. + // https://bugs.webkit.org/show_bug.cgi?id=222262 + // + // not await so that we can detect whether the thrown error likely relates + // to a lack of support for the Promise ClipboardItem constructor + await navigator.clipboard.write([ + new window.ClipboardItem({ + "image/png": png, + }), + ]); + } + + public async exportPNG(embedScene?:boolean, selectedOnly?: boolean):Promise { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.exportPNG, "ExcalidrawView.exportPNG", embedScene, selectedOnly); + if (!this.excalidrawAPI || !this.file) { + return; + } + + const png = await this.png(this.getScene(selectedOnly), undefined, embedScene); + if (!png) { + return; + } + const reader = new FileReader(); + reader.readAsDataURL(png); + reader.onloadend = () => { + const base64data = reader.result; + download(null, base64data, `${this.file.basename}.png`); + }; + } + + public setPreventReload() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setPreventReload, "ExcalidrawView.setPreventReload"); + this.semaphores.preventReload = true; + this.preventReloadResetTimer = window.setTimeout(()=>this.semaphores.preventReload = false,PREVENT_RELOAD_TIMEOUT); + } + + public clearPreventReloadTimer() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearPreventReloadTimer, "ExcalidrawView.clearPreventReloadTimer"); + if(this.preventReloadResetTimer) { + window.clearTimeout(this.preventReloadResetTimer); + this.preventReloadResetTimer = null; + } + } + + public async setEmbeddableNodeIsEditing() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setEmbeddableNodeIsEditing, "ExcalidrawView.setEmbeddableNodeIsEditing"); + this.clearEmbeddableNodeIsEditingTimer(); + await this.forceSave(true); + this.semaphores.embeddableIsEditingSelf = true; + } + + public clearEmbeddableNodeIsEditingTimer () { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearEmbeddableNodeIsEditingTimer, "ExcalidrawView.clearEmbeddableNodeIsEditingTimer"); + if(this.editingSelfResetTimer) { + window.clearTimeout(this.editingSelfResetTimer); + this.editingSelfResetTimer = null; + } + } + + public clearEmbeddableNodeIsEditing() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearEmbeddableNodeIsEditing, "ExcalidrawView.clearEmbeddableNodeIsEditing"); + this.clearEmbeddableNodeIsEditingTimer(); + this.editingSelfResetTimer = window.setTimeout(()=>this.semaphores.embeddableIsEditingSelf = false,EMBEDDABLE_SEMAPHORE_TIMEOUT); + } + + async save(preventReload: boolean = true, forcesave: boolean = false, overrideEmbeddableIsEditingSelfDebounce: boolean = false) { + if ((process.env.NODE_ENV === 'development')) { + if (DEBUGGING) { + debug(this.save, "ExcalidrawView.save, enter", preventReload, forcesave); + console.trace(); + } + } + /*if(this.semaphores.viewunload && (this.ownerWindow !== window)) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.save, `ExcalidrawView.save, view is unloading, aborting save`); + return; + }*/ + + if(!this.isLoaded) { + return; + } + if (!overrideEmbeddableIsEditingSelfDebounce && this.semaphores.embeddableIsEditingSelf) { + return; + } + //console.log("saving - embeddable not editing") + //debug({where:"save", preventReload, forcesave, semaphores:this.semaphores}); + if (this.semaphores.saving) { + return; + } + this.semaphores.saving = true; + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.save, `ExcalidrawView.save, saving, dirty:${this.isDirty()}, preventReload:${preventReload}, forcesave:${forcesave}`); + + //if there were no changes to the file super save will not save + //and consequently main.ts modifyEventHandler will not fire + //this.reload will not be called + //triggerReload is used to flag if there were no changes but file should be reloaded anyway + let triggerReload:boolean = false; + + if ( + !this.excalidrawAPI || + !this.isLoaded || + !this.file || + !this.app.vault.getAbstractFileByPath(this.file.path) //file was recently deleted + ) { + this.semaphores.saving = false; + return; + } + + const allowSave = this.isDirty() || forcesave; //removed this.semaphores.autosaving + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.save, `ExcalidrawView.save, try saving, allowSave:${allowSave}, isDirty:${this.isDirty()}, isAutosaving:${this.semaphores.autosaving}, isForceSaving:${forcesave}`); + try { + if (allowSave) { + const scene = this.getScene(); + + if (this.compatibilityMode) { + await this.excalidrawData.syncElements(scene); + } else if ( + await this.excalidrawData.syncElements(scene, this.excalidrawAPI.getAppState().selectedElementIds) + && !this.semaphores.popoutUnload //Obsidian going black after REACT 18 migration when closing last leaf on popout + ) { + await this.loadDrawing( + false, + this.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted) + ); + } + + //reload() is triggered indirectly when saving by the modifyEventHandler in main.ts + //prevent reload is set here to override reload when not wanted: typically when the user is editing + //and we do not want to interrupt the flow by reloading the drawing into the canvas. + this.clearDirty(); + this.clearPreventReloadTimer(); + + this.semaphores.preventReload = preventReload; + await this.prepareGetViewData(); + + //added this to avoid Electron crash when terminating a popout window and saving the drawing, need to check back + //can likely be removed once this is resolved: https://github.com/electron/electron/issues/40607 + if(this.semaphores?.viewunload) { + await this.prepareGetViewData(); + const d = this.getViewData(); + const plugin = this.plugin; + const file = this.file; + window.setTimeout(async ()=>{ + await plugin.app.vault.modify(file,d); + await imageCache.addBAKToCache(file.path,d); + },200) + this.semaphores.saving = false; + return; + } + + await super.save(); + if (process.env.NODE_ENV === 'development') { + if (DEBUGGING) { + debug(this.save, `ExcalidrawView.save, super.save finished`, this.file); + console.trace(); + } + } + //saving to backup with a delay in case application closes in the meantime, I want to avoid both save and backup corrupted. + const path = this.file.path; + const data = this.lastSavedData; + window.setTimeout(()=>imageCache.addBAKToCache(path,data),50); + triggerReload = (this.lastSaveTimestamp === this.file.stat.mtime) && + !preventReload && forcesave; + this.lastSaveTimestamp = this.file.stat.mtime; + //this.clearDirty(); //moved to right after allow save, to avoid autosave collision with load drawing + + //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/629 + //there were odd cases when preventReload semaphore did not get cleared and consequently a synchronized image + //did not update the open drawing + if(preventReload) { + this.setPreventReload(); + } + } + + // !triggerReload means file has not changed. No need to re-export + //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1209 (added popout unload to the condition) + if (!triggerReload && !this.semaphores.autosaving && (!this.semaphores.viewunload || this.semaphores.popoutUnload)) { + const autoexportPreference = this.excalidrawData.autoexportPreference; + if ( + (autoexportPreference === AutoexportPreference.inherit && this.plugin.settings.autoexportSVG) || + autoexportPreference === AutoexportPreference.both || autoexportPreference === AutoexportPreference.svg + ) { + this.saveSVG(); + } + if ( + (autoexportPreference === AutoexportPreference.inherit && this.plugin.settings.autoexportPNG) || + autoexportPreference === AutoexportPreference.both || autoexportPreference === AutoexportPreference.png + ) { + this.savePNG(); + } + if ( + !this.compatibilityMode && + this.plugin.settings.autoexportExcalidraw + ) { + this.saveExcalidraw(); + } + } + } catch (e) { + errorlog({ + where: "ExcalidrawView.save", + fn: this.save, + error: e, + }); + warningUnknowSeriousError(); + } + this.semaphores.saving = false; + if(triggerReload) { + this.reload(true, this.file); + } + this.resetAutosaveTimer(); //next autosave period starts after save + } + + // get the new file content + // if drawing is in Text Element Edit Lock, then everything should be parsed and in sync + // if drawing is in Text Element Edit Unlock, then everything is raw and parse and so an async function is not required here + /** + * I moved the logic from getViewData to prepareGetViewData because getViewData is Sync and prepareGetViewData is async + * prepareGetViewData is async because of moving compression to a worker thread in 2.4.0 + */ + private viewSaveData: string = ""; + + async prepareGetViewData(): Promise { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.prepareGetViewData, "ExcalidrawView.prepareGetViewData"); + if (!this.excalidrawAPI || !this.excalidrawData.loaded) { + this.viewSaveData = this.data; + return; + } + + const scene = this.getScene(); + if(!scene) { + this.viewSaveData = this.data; + return; + } + + //include deleted elements in save in case saving in markdown mode + //deleted elements are only used if sync modifies files while Excalidraw is open + //otherwise deleted elements are discarded when loading the scene + if (!this.compatibilityMode) { + + const keys:[string,string][] = this.exportDialog?.dirty && this.exportDialog?.saveSettings + ? [ + [FRONTMATTER_KEYS["export-padding"].name, this.exportDialog.padding.toString()], + [FRONTMATTER_KEYS["export-pngscale"].name, this.exportDialog.scale.toString()], + [FRONTMATTER_KEYS["export-dark"].name, this.exportDialog.theme === "dark" ? "true" : "false"], + [FRONTMATTER_KEYS["export-transparent"].name, this.exportDialog.transparent ? "true" : "false"], + [FRONTMATTER_KEYS["plugin"].name, this.textMode === TextMode.raw ? "raw" : "parsed"], + [FRONTMATTER_KEYS["export-embed-scene"].name, this.exportDialog.embedScene ? "true" : "false"], + ] + : [ + [FRONTMATTER_KEYS["plugin"].name, this.textMode === TextMode.raw ? "raw" : "parsed"] + ]; + + if(this.exportDialog?.dirty) { + this.exportDialog.dirty = false; + } + + const header = getExcalidrawMarkdownHeaderSection(this.data, keys); + const tail = this.plugin.settings.zoteroCompatibility ? (RE_TAIL.exec(this.data)?.[1] ?? "") : ""; + + if (!this.excalidrawData.disableCompression) { + this.excalidrawData.disableCompression = this.plugin.settings.decompressForMDView && + this.isEditedAsMarkdownInOtherView(); + } + const result = IS_WORKER_SUPPORTED + ? (header + (await this.excalidrawData.generateMDAsync( + this.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted) //will be concatenated to scene.elements + )) + tail) + : (header + (this.excalidrawData.generateMDSync( + this.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted) //will be concatenated to scene.elements + )) + tail) + + this.excalidrawData.disableCompression = false; + this.viewSaveData = result; + return; + } + if (this.compatibilityMode) { + this.viewSaveData = JSON.stringify(scene, null, "\t"); + return; + } + + this.viewSaveData = this.data; + return; + } + + getViewData() { + return this.viewSaveData ?? this.data; + } + + private hiddenMobileLeaves:[WorkspaceLeaf,string][] = []; + + restoreMobileLeaves() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.restoreMobileLeaves, "ExcalidrawView.restoreMobileLeaves"); + if(this.hiddenMobileLeaves.length>0) { + this.hiddenMobileLeaves.forEach((x:[WorkspaceLeaf,string])=>{ + x[0].containerEl.style.display = x[1]; + }) + this.hiddenMobileLeaves = []; + } + } + + async openLaTeXEditor(eqId: string) { + if(await this.excalidrawData.syncElements(this.getScene())) { + //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1994 + await this.forceSave(true); + } + const el = this.getViewElements().find((el:ExcalidrawElement)=>el.id === eqId && el.type==="image") as ExcalidrawImageElement; + if(!el) { + return; + } + + const fileId = el.fileId; + + let equation = this.excalidrawData.getEquation(fileId)?.latex; + if(!equation) { + await this.save(false); + equation = this.excalidrawData.getEquation(fileId)?.latex; + if(!equation) return; + } + + GenericInputPrompt.Prompt(this,this.plugin,this.app,t("ENTER_LATEX"),undefined,equation, undefined, 3).then(async (formula: string) => { + if (!formula || formula === equation) { + return; + } + this.excalidrawData.setEquation(fileId, { + latex: formula, + isLoaded: false, + }); + await this.save(false); + await updateEquation( + formula, + fileId, + this, + addFiles, + ); + this.setDirty(1); + }); + } + + async openEmbeddedLinkEditor(imgId:string) { + const el = this.getViewElements().find((el:ExcalidrawElement)=>el.id === imgId && el.type==="image") as ExcalidrawImageElement; + if(!el) { + return; + } + const fileId = el.fileId; + const ef = this.excalidrawData.getFile(fileId); + if(!ef) { + return + } + if (!ef.isHyperLink && !ef.isLocalLink && ef.file) { + const handler = async (link:string) => { + if (!link || ef.linkParts.original === link) { + return; + } + ef.resetImage(this.file.path, link); + this.excalidrawData.setFile(fileId, ef); + this.setDirty(2); + await this.save(false); + await sleep(100); + if(!this.plugin.isExcalidrawFile(ef.file) && !link.endsWith("|100%")) { + const ea = getEA(this) as ExcalidrawAutomate; + let imgEl = this.getViewElements().find((x:ExcalidrawElement)=>x.id === el.id) as ExcalidrawImageElement; + if(!imgEl) { + ea.destroy(); + return; + } + if(imgEl && await ea.resetImageAspectRatio(imgEl)) { + await ea.addElementsToView(false); + } + ea.destroy(); + } + } + GenericInputPrompt.Prompt( + this, + this.plugin, + this.app, + t("MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT_TITLE"), + undefined, + ef.linkParts.original, + [{caption: "✅", action: (x:string)=>{x.replaceAll("\n","").trim()}}], + 3, + false, + (container) => container.createEl("p",{text: fragWithHTML(t("MARKDOWN_EMBED_CUSTOMIZE_LINK_PROMPT"))}), + false + ).then(handler.bind(this),()=>{}); + return; + } + } + + toggleDisableBinding() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.toggleDisableBinding, "ExcalidrawView.toggleDisableBinding"); + const newState = !this.excalidrawAPI.getAppState().invertBindingBehaviour; + this.updateScene({appState: {invertBindingBehaviour:newState}, storeAction: "update"}); + new Notice(newState ? t("ARROW_BINDING_INVERSE_MODE") : t("ARROW_BINDING_NORMAL_MODE")); + } + + toggleFrameRendering() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.toggleFrameRendering, "ExcalidrawView.toggleFrameRendering"); + const frameRenderingSt = (this.excalidrawAPI as ExcalidrawImperativeAPI).getAppState().frameRendering; + this.updateScene({appState: {frameRendering: {...frameRenderingSt, enabled: !frameRenderingSt.enabled}}, storeAction: "update"}); + new Notice(frameRenderingSt.enabled ? t("FRAME_CLIPPING_ENABLED") : t("FRAME_CLIPPING_DISABLED")); + } + + toggleFrameClipping() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.toggleFrameClipping, "ExcalidrawView.toggleFrameClipping"); + const frameRenderingSt = (this.excalidrawAPI as ExcalidrawImperativeAPI).getAppState().frameRendering; + this.updateScene({appState: {frameRendering: {...frameRenderingSt, clip: !frameRenderingSt.clip}}, storeAction: "update"}); + new Notice(frameRenderingSt.clip ? "Frame Clipping: Enabled" : "Frame Clipping: Disabled"); + } + + gotoFullscreen() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.gotoFullscreen, "ExcalidrawView.gotoFullscreen"); + if(this.plugin.leafChangeTimeout) { + window.clearTimeout(this.plugin.leafChangeTimeout); //leafChangeTimeout is created on window in main.ts!!! + this.plugin.clearLeafChangeTimeout(); + } + if (!this.excalidrawWrapperRef) { + return; + } + if (this.toolsPanelRef && this.toolsPanelRef.current) { + this.toolsPanelRef.current.setFullscreen(true); + } + + const hide = (el:HTMLElement) => { + let tmpEl = el; + while(tmpEl && !tmpEl.hasClass("workspace-split")) { + el.addClass(SHOW); + el = tmpEl; + tmpEl = el.parentElement; + } + if(el) { + el.addClass(SHOW); + el.querySelectorAll(`div.workspace-split:not(.${SHOW})`).forEach(node=>{ + if(node !== el) node.addClass(SHOW); + }); + el.querySelector(`div.workspace-leaf-content.${SHOW} > .view-header`).addClass(SHOW); + el.querySelectorAll(`div.workspace-tab-container.${SHOW} > div.workspace-leaf:not(.${SHOW})`).forEach(node=>node.addClass(SHOW)); + el.querySelectorAll(`div.workspace-tabs.${SHOW} > div.workspace-tab-header-container`).forEach(node=>node.addClass(SHOW)); + el.querySelectorAll(`div.workspace-split.${SHOW} > div.workspace-tabs:not(.${SHOW})`).forEach(node=>node.addClass(SHOW)); + } + const doc = this.ownerDocument; + doc.body.querySelectorAll(`div.workspace-split:not(.${SHOW})`).forEach(node=>{ + if(node !== tmpEl) { + node.addClass(HIDE); + } else { + node.addClass(SHOW); + } + }); + doc.body.querySelector(`div.workspace-leaf-content.${SHOW} > .view-header`).addClass(HIDE); + doc.body.querySelectorAll(`div.workspace-tab-container.${SHOW} > div.workspace-leaf:not(.${SHOW})`).forEach(node=>node.addClass(HIDE)); + doc.body.querySelectorAll(`div.workspace-tabs.${SHOW} > div.workspace-tab-header-container`).forEach(node=>node.addClass(HIDE)); + doc.body.querySelectorAll(`div.workspace-split.${SHOW} > div.workspace-tabs:not(.${SHOW})`).forEach(node=>node.addClass(HIDE)); + doc.body.querySelectorAll(`div.workspace-ribbon`).forEach(node=>node.addClass(HIDE)); + doc.body.querySelectorAll(`div.mobile-navbar`).forEach(node=>node.addClass(HIDE)); + doc.body.querySelectorAll(`div.status-bar`).forEach(node=>node.addClass(HIDE)); + } + + hide(this.contentEl); + } + + + isFullscreen(): boolean { + //(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.isFullscreen, "ExcalidrawView.isFullscreen"); + return Boolean(document.body.querySelector(".excalidraw-hidden")); + } + + exitFullscreen() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.exitFullscreen, "ExcalidrawView.exitFullscreen"); + if (this.toolsPanelRef && this.toolsPanelRef.current) { + this.toolsPanelRef.current.setFullscreen(false); + } + const doc = this.ownerDocument; + doc.querySelectorAll(".excalidraw-hidden").forEach(el=>el.removeClass(HIDE)); + doc.querySelectorAll(".excalidraw-visible").forEach(el=>el.removeClass(SHOW)); + } + + removeLinkTooltip() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.removeLinkTooltip, "ExcalidrawView.removeLinkTooltip"); + //.classList.remove("excalidraw-tooltip--visible");document.querySelector(".excalidraw-tooltip",); + const tooltip = this.ownerDocument.body.querySelector( + "body>div.excalidraw-tooltip,div.excalidraw-tooltip--visible", + ); + if (tooltip) { + tooltip.classList.remove("excalidraw-tooltip--visible") + //this.ownerDocument.body.removeChild(tooltip); + } + } + + handleLinkHookCall(element:ExcalidrawElement,link:string, event:any):boolean { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.handleLinkHookCall, "ExcalidrawView.handleLinkHookCall", element, link, event); + if(this.getHookServer().onLinkClickHook) { + try { + if(!this.getHookServer().onLinkClickHook( + element, + link, + event, + this, + this.getHookServer() + )) { + return true; + } + } catch (e) { + errorlog({where: "ExcalidrawView.onLinkOpen", fn: this.getHookServer().onLinkClickHook, error: e}); + } + } + return false; + } + + private getLinkTextForElement( + selectedText:SelectedElementWithLink, + selectedElementWithLink?:SelectedElementWithLink, + allowLinearElementClick: boolean = false, + ): { + linkText: string, + selectedElement: ExcalidrawElement, + isLinearElement: boolean, + } { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getLinkTextForElement, "ExcalidrawView.getLinkTextForElement", selectedText, selectedElementWithLink); + if (selectedText?.id || selectedElementWithLink?.id) { + let selectedTextElement: ExcalidrawTextElement = selectedText.id + ? this.excalidrawAPI.getSceneElements().find((el:ExcalidrawElement)=>el.id === selectedText.id) + : null; + + let selectedElement = selectedElementWithLink.id + ? this.excalidrawAPI.getSceneElements().find((el:ExcalidrawElement)=> + el.id === selectedElementWithLink.id) + : null; + + //if the user clicked on the label of an arrow then the label will be captured in selectedElement, because + //Excalidraw returns the container as the selected element. But in this case we want this to be treated as the + //text element, as the assumption is, if the user wants to invoke the linear element editor for an arrow that has + //a label with a link, then he/she should rather CTRL+click on the arrow line, not the label. CTRL+Click on + //the label is an indication of wanting to navigate. + if (!Boolean(selectedTextElement) && selectedElement?.type === "text") { + const container = getContainerElement(selectedElement, arrayToMap(this.excalidrawAPI.getSceneElements())); + if(container?.type === "arrow") { + const x = getTextElementAtPointer(this.currentPosition,this); + if(x?.id === selectedElement.id) { + selectedTextElement = selectedElement; + selectedElement = null; + } + } + } + + //CTRL click on a linear element with a link will navigate instead of line editor + if(!allowLinearElementClick && ["arrow", "line"].includes(selectedElement?.type)) { + return {linkText: selectedElement.link, selectedElement: selectedElement, isLinearElement: true}; + } + + if (!selectedTextElement && selectedElement?.type === "text") { + if(!allowLinearElementClick) { + //CTRL click on a linear element with a link will navigate instead of line editor + const container = getContainerElement(selectedElement, arrayToMap(this.excalidrawAPI.getSceneElements())); + if(container?.type !== "arrow") { + selectedTextElement = selectedElement as ExcalidrawTextElement; + selectedElement = null; + } else { + const x = this.processLinkText(selectedElement.rawText, selectedElement as ExcalidrawTextElement, container, false); + return {linkText: x.linkText, selectedElement: container, isLinearElement: true}; + } + } else { + selectedTextElement = selectedElement as ExcalidrawTextElement; + selectedElement = null; + } + } + + let linkText = + selectedElementWithLink?.text ?? + (this.textMode === TextMode.parsed + ? this.excalidrawData.getRawText(selectedText.id) + : selectedText.text); + + return {...this.processLinkText(linkText, selectedTextElement, selectedElement), isLinearElement: false}; + } + return {linkText: null, selectedElement: null, isLinearElement: false}; + } + + + processLinkText(linkText: string, selectedTextElement: ExcalidrawTextElement, selectedElement: ExcalidrawElement, shouldOpenLink: boolean = true) { + if(!linkText) { + return {linkText: null, selectedElement: null}; + } + + if(linkText.startsWith("#")) { + return {linkText, selectedElement: selectedTextElement ?? selectedElement}; + } + + const maybeObsidianLink = parseObsidianLink(linkText, this.app, shouldOpenLink); + if(typeof maybeObsidianLink === "string") { + linkText = maybeObsidianLink; + } + + const partsArray = REGEX_LINK.getResList(linkText); + if (!linkText || partsArray.length === 0) { + //the container link takes precedence over the text link + if(selectedTextElement?.containerId) { + const container = _getContainerElement(selectedTextElement, {elements: this.excalidrawAPI.getSceneElements()}); + if(container) { + linkText = container.link; + + if(linkText?.startsWith("#")) { + return {linkText, selectedElement: selectedTextElement ?? selectedElement}; + } + + const maybeObsidianLink = parseObsidianLink(linkText, this.app, shouldOpenLink); + if(typeof maybeObsidianLink === "string") { + linkText = maybeObsidianLink; + } + } + } + if(!linkText || partsArray.length === 0) { + linkText = selectedTextElement?.link; + } + } + return {linkText, selectedElement: selectedTextElement ?? selectedElement}; + } + + async linkClick( + ev: MouseEvent | null, + selectedText: SelectedElementWithLink, + selectedImage: SelectedImage, + selectedElementWithLink: SelectedElementWithLink, + keys?: ModifierKeys, + allowLinearElementClick: boolean = false, + ) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.linkClick, "ExcalidrawView.linkClick", ev, selectedText, selectedImage, selectedElementWithLink, keys); + if(!selectedText) selectedText = {id:null, text: null}; + if(!selectedImage) selectedImage = {id:null, fileId: null}; + if(!selectedElementWithLink) selectedElementWithLink = {id:null, text:null}; + if(!ev && !keys) keys = emulateKeysForLinkClick("new-tab"); + if( ev && !keys) keys = {shiftKey: ev.shiftKey, ctrlKey: ev.ctrlKey, metaKey: ev.metaKey, altKey: ev.altKey}; + + const linkClickType = linkClickModifierType(keys); + + let file = null; + let subpath: string = null; + let {linkText, selectedElement, isLinearElement} = this.getLinkTextForElement(selectedText, selectedElementWithLink, allowLinearElementClick); + + //if (selectedText?.id || selectedElementWithLink?.id) { + if (selectedElement) { + if (!allowLinearElementClick && linkText && isLinearElement) { + if(this.semaphores.warnAboutLinearElementLinkClick) { + new Notice(t("LINEAR_ELEMENT_LINK_CLICK_ERROR"), 20000); + this.semaphores.warnAboutLinearElementLinkClick = false; + } + return; + } + if (!linkText) { + return; + } + linkText = linkText.replaceAll("\n", ""); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187 + + if(this.handleLinkHookCall(selectedElement,linkText,ev)) return; + if(openExternalLink(linkText, this.app)) return; + + const maybeObsidianLink = parseObsidianLink(linkText,this.app); + if (typeof maybeObsidianLink === "boolean" && maybeObsidianLink) return; + if (typeof maybeObsidianLink === "string") { + linkText = maybeObsidianLink; + } + + const result = await linkPrompt(linkText, this.app, this); + if(!result) return; + [file, linkText, subpath] = result; + } + if (selectedImage?.id) { + const imageElement = this.getScene().elements.find((el:ExcalidrawElement)=>el.id === selectedImage.id) as ExcalidrawImageElement; + if (this.excalidrawData.hasEquation(selectedImage.fileId)) { + this.updateScene({appState: {contextMenu: null}}); + this.openLaTeXEditor(selectedImage.id); + return; + } + if (this.excalidrawData.hasMermaid(selectedImage.fileId) || getMermaidText(imageElement)) { + if(shouldRenderMermaid) { + const api = this.excalidrawAPI as ExcalidrawImperativeAPI; + api.updateScene({appState: {openDialog: { name: "ttd", tab: "mermaid" }}, storeAction: "update"}) + } + return; + } + + await this.save(false); //in case pasted images haven't been saved yet + if (this.excalidrawData.hasFile(selectedImage.fileId)) { + const fileId = selectedImage.fileId; + const ef = this.excalidrawData.getFile(fileId); + if (!ef.isHyperLink && !ef.isLocalLink && ef.file && linkClickType === "md-properties") { + this.updateScene({appState: {contextMenu: null}}); + this.openEmbeddedLinkEditor(selectedImage.id); + return; + } + let secondOrderLinks: string = " "; + + const backlinks = this.app.metadataCache?.getBacklinksForFile(ef.file)?.data; + const secondOrderLinksSet = new Set(); + if(backlinks && this.plugin.settings.showSecondOrderLinks) { + const linkPaths = Object.keys(backlinks) + .filter(path => (path !== this.file.path) && (path !== ef.file.path)) + .map(path => { + const filepathParts = splitFolderAndFilename(path); + if(secondOrderLinksSet.has(path)) return ""; + secondOrderLinksSet.add(path); + return `[[${path}|${t("LINKLIST_SECOND_ORDER_LINK")}: ${filepathParts.basename}]]`; + }); + secondOrderLinks += linkPaths.join(" "); + } + + if(this.plugin.settings.showSecondOrderLinks && this.plugin.isExcalidrawFile(ef.file)) { + secondOrderLinks += getExcalidrawFileForwardLinks(this.app, ef.file, secondOrderLinksSet); + } + + const linkString = (ef.isHyperLink || ef.isLocalLink + ? `[](${ef.hyperlink}) ` + : `[[${ef.linkParts.original}]] ` + ) + (imageElement.link + ? (imageElement.link.match(/$cmd:\/\/.*/) || imageElement.link.match(REG_LINKINDEX_HYPERLINK)) + ? `[](${imageElement.link})` + : imageElement.link + : ""); + + const result = await linkPrompt(linkString + secondOrderLinks, this.app, this); + if(!result) return; + [file, linkText, subpath] = result; + } + } + + if (!linkText) { + if(allowLinearElementClick) { + return; + } + new Notice(t("LINK_BUTTON_CLICK_NO_TEXT"), 20000); + return; + } + + const id = selectedImage.id??selectedText.id??selectedElementWithLink.id; + const el = this.excalidrawAPI.getSceneElements().filter((el:ExcalidrawElement)=>el.id === id)[0]; + if(this.handleLinkHookCall(el,linkText,ev)) return; + + try { + if (linkClickType !== "active-pane" && this.isFullscreen()) { + this.exitFullscreen(); + } + if (!file) { + new NewFileActions({ + plugin: this.plugin, + path: linkText, + keys, + view: this, + sourceElement: el + }).open(); + return; + } + if(this.linksAlwaysOpenInANewPane && !anyModifierKeysPressed(keys)) { + keys = emulateKeysForLinkClick("new-pane"); + } + + try { + const drawIO = this.app.plugins.plugins["drawio-obsidian"]; + if(drawIO && drawIO._loaded) { + if(file.extension === "svg") { + const svg = await this.app.vault.cachedRead(file); + if(/(<|\<)(mxfile|mxgraph)/i.test(svg)) { + const leaf = getLeaf(this.plugin,this.leaf,keys); + leaf.setViewState({ + type: "diagram-edit", + state: { + file: file.path + } + }); + return; + } + } + } + } catch(e) { + console.error(e); + } + + //if link will open in the same pane I want to save the drawing before opening the link + await this.forceSaveIfRequired(); + const { promise } = openLeaf({ + plugin: this.plugin, + fnGetLeaf: () => getLeaf(this.plugin,this.leaf,keys), + file, + openState: { + active: !this.linksAlwaysOpenInANewPane, + ...subpath ? { eState: { subpath } } : {} + } + }); //if file exists open file and jump to reference + await promise; + //view.app.workspace.setActiveLeaf(leaf, true, true); //0.15.4 ExcaliBrain focus issue + } catch (e) { + new Notice(e, 4000); + } + } + + async handleLinkClick(ev: MouseEvent | ModifierKeys, allowLinearElementClick: boolean = false) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.handleLinkClick, "ExcalidrawView.handleLinkClick", ev); + this.removeLinkTooltip(); + + const selectedText = this.getSelectedTextElement(); + const selectedImage = selectedText?.id + ? null + : this.getSelectedImageElement(); + const selectedElementWithLink = + (selectedImage?.id || selectedText?.id) + ? null + : this.getSelectedElementWithLink(); + this.linkClick( + ev instanceof MouseEvent ? ev : null, + selectedText, + selectedImage, + selectedElementWithLink, + ev instanceof MouseEvent ? null : ev, + allowLinearElementClick, + ); + } + + onResize() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onResize, "ExcalidrawView.onResize"); + super.onResize(); + if(this.plugin.leafChangeTimeout) return; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/723 + const api = this.excalidrawAPI; + if ( + !this.plugin.settings.zoomToFitOnResize || + !this.excalidrawAPI || + this.semaphores.isEditingText || + !api + ) { + return; + } + + //final fallback to prevent resizing when text element is in edit mode + //this is to prevent jumping text due to on-screen keyboard popup + if (api.getAppState()?.editingTextElement) { + return; + } + this.zoomToFit(false); + } + + excalidrawGetSceneVersion: (elements: ExcalidrawElement[]) => number; + getSceneVersion (elements: ExcalidrawElement[]):number { + if(!this.excalidrawGetSceneVersion) { + this.excalidrawGetSceneVersion = this.packages.excalidrawLib.getSceneVersion; + } + return this.excalidrawGetSceneVersion(elements.filter(el=>!el.isDeleted)); + } + + public async forceSave(silent:boolean=false) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.forceSave, "ExcalidrawView.forceSave"); + if (this.semaphores.autosaving || this.semaphores.saving) { + if(!silent) new Notice(t("FORCE_SAVE_ABORTED")) + return; + } + if(this.preventReloadResetTimer) { + window.clearTimeout(this.preventReloadResetTimer); + this.preventReloadResetTimer = null; + } + this.semaphores.preventReload = false; + this.semaphores.forceSaving = true; + await this.save(false, true, true); + this.plugin.triggerEmbedUpdates(); + this.loadSceneFiles(); + this.semaphores.forceSaving = false; + if(!silent) new Notice("Save successful", 1000); + } + + onload() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onload, "ExcalidrawView.onload"); + if(this.plugin.settings.overrideObsidianFontSize) { + document.documentElement.style.fontSize = ""; + } + + const apiMissing = Boolean(typeof this.containerEl.onWindowMigrated === "undefined") + this.packages = this.plugin.getPackage(this.ownerWindow); + + if(DEVICE.isDesktop && !apiMissing) { + this.destroyers.push( + //this.containerEl.onWindowMigrated(this.leaf.rebuildView.bind(this)) + this.containerEl.onWindowMigrated(async() => { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onload, "ExcalidrawView.onWindowMigrated"); + const f = this.file; + const l = this.leaf; + await closeLeafView(l); + windowMigratedDisableZoomOnce = true; + l.setViewState({ + type: VIEW_TYPE_EXCALIDRAW, + state: { + file: f.path, + } + }); + }) + ); + } + + this.semaphores.scriptsReady = true; + + const wheelEvent = (ev:WheelEvent) => { + if(this.semaphores.wheelTimeout) window.clearTimeout(this.semaphores.wheelTimeout); + if(this.semaphores.hoverSleep && this.excalidrawAPI) this.clearHoverPreview(); + this.semaphores.wheelTimeout = window.setTimeout(()=>{ + window.clearTimeout(this.semaphores.wheelTimeout); + this.semaphores.wheelTimeout = null; + },1000); + } + + this.registerDomEvent(this.containerEl,"wheel",wheelEvent, {passive: false}); + + this.actionButtons['scriptInstall'] = this.addAction(SCRIPTENGINE_ICON_NAME, t("INSTALL_SCRIPT_BUTTON"), () => { + new ScriptInstallPrompt(this.plugin).open(); + }); + + this.actionButtons['save'] = this.addAction( + DISK_ICON_NAME, + t("FORCE_SAVE"), + async () => this.forceSave(), + ); + + this.actionButtons['isRaw'] = this.addAction( + TEXT_DISPLAY_RAW_ICON_NAME, + t("RAW"), + () => this.changeTextMode(TextMode.parsed), + ); + this.actionButtons['isParsed'] = this.addAction( + TEXT_DISPLAY_PARSED_ICON_NAME, + t("PARSED"), + () => this.changeTextMode(TextMode.raw), + ); + + this.actionButtons['link'] = this.addAction("link", t("OPEN_LINK"), (ev) => + this.handleLinkClick(ev), + ); + + this.registerDomEvent(this.ownerWindow, "resize", this.onExcalidrawResize.bind(this)); + + this.app.workspace.onLayoutReady(async () => { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onload,`ExcalidrawView.onload > app.workspace.onLayoutReady, file:${this.file?.name}, isActiveLeaf:${this?.app?.workspace?.activeLeaf === this.leaf}, is activeExcalidrawView set:${Boolean(this?.plugin?.activeExcalidrawView)}`); + //Leaf was moved to new window and ExcalidrawView was destructed. + //Happens during Obsidian startup if View opens in new window. + if(!this.plugin) { + return; + } + await this.plugin.awaitInit(); + //implemented to overcome issue that activeLeafChangeEventHandler is not called when view is initialized from a saved workspace, since Obsidian 1.6.0 + let counter = 0; + while(counter++<50 && (!Boolean(this?.plugin?.activeLeafChangeEventHandler) || !Boolean(this.canvasNodeFactory))) { + await(sleep(50)); + if(!this?.plugin) return; + } + if(!Boolean(this?.plugin?.activeLeafChangeEventHandler)) return; + if (Boolean(this.plugin.activeLeafChangeEventHandler) && (this?.app?.workspace?.activeLeaf === this.leaf)) { + this.plugin.activeLeafChangeEventHandler(this.leaf); + } + this.canvasNodeFactory.initialize(); + this.contentEl.addClass("excalidraw-view"); + //https://github.com/zsviczian/excalibrain/issues/28 + await this.addSlidingPanesListner(); //awaiting this because when using workspaces, onLayoutReady comes too early + this.addParentMoveObserver(); + + const onKeyUp = (e: KeyboardEvent) => { + this.modifierKeyDown = { + shiftKey: e.shiftKey, + ctrlKey: e.ctrlKey, + altKey: e.altKey, + metaKey: e.metaKey + } + }; + + const onKeyDown = (e: KeyboardEvent) => { + this.modifierKeyDown = { + shiftKey: e.shiftKey, + ctrlKey: e.ctrlKey, + altKey: e.altKey, + metaKey: e.metaKey + } + }; + + const onBlurOrLeave = () => { + if(!this.excalidrawAPI || !this.excalidrawData.loaded || !this.isDirty()) { + return; + } + if((this.excalidrawAPI as ExcalidrawImperativeAPI).getAppState().activeTool.type !== "image") { + this.forceSave(true); + } + }; + + this.registerDomEvent(this.ownerWindow, "keydown", onKeyDown, false); + this.registerDomEvent(this.ownerWindow, "keyup", onKeyUp, false); + //this.registerDomEvent(this.contentEl, "mouseleave", onBlurOrLeave, false); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2004 + this.registerDomEvent(this.ownerWindow, "blur", onBlurOrLeave, false); + }); + + this.setupAutosaveTimer(); + super.onload(); + } + + //this is to solve sliding panes bug + //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/9 + private slidingPanesListner: ()=>void; + private async addSlidingPanesListner() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addSlidingPanesListner, "ExcalidrawView.addSlidingPanesListner"); + if(!this.plugin.settings.slidingPanesSupport) { + return; + } + + this.slidingPanesListner = () => { + if (this.excalidrawAPI) { + this.refreshCanvasOffset(); + } + }; + let rootSplit = this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt; + while(!rootSplit) { + await sleep(50); + rootSplit = this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt; + } + this.registerDomEvent(rootSplit.containerEl,"scroll",this.slidingPanesListner); + } + + private removeSlidingPanesListner() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.removeSlidingPanesListner, "ExcalidrawView.removeSlidingPanesListner"); + if (this.slidingPanesListner) { + ( + this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt + ).containerEl?.removeEventListener("scroll", this.slidingPanesListner); + this.slidingPanesListner = null; + } + } + + //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/572 + private offsetLeft: number = 0; + private offsetTop: number = 0; + private addParentMoveObserver() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addParentMoveObserver, "ExcalidrawView.addParentMoveObserver"); + + const parent = + getParentOfClass(this.containerEl, "popover") ?? + getParentOfClass(this.containerEl, "workspace-leaf"); + if (!parent) { + return; + } + + const inHoverEditorLeaf = parent.classList.contains("popover"); + + this.offsetLeft = parent.offsetLeft; + this.offsetTop = parent.offsetTop; + + //triggers when the leaf is moved in the workspace + const observerFn = async (m: MutationRecord[]) => { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(observerFn, `ExcalidrawView.parentMoveObserver, file:${this.file?.name}`); + const target = m[0].target; + if (!(target instanceof HTMLElement)) { + return; + } + const { offsetLeft, offsetTop } = target; + if (offsetLeft !== this.offsetLeft || offsetTop !== this.offsetTop) { + if (this.excalidrawAPI) { + this.refreshCanvasOffset(); + } + this.offsetLeft = offsetLeft; + this.offsetTop = offsetTop; + } + }; + this.parentMoveObserver = DEBUGGING + ? new CustomMutationObserver(observerFn, "parentMoveObserver") + : new MutationObserver(observerFn) + + this.parentMoveObserver.observe(parent, { + attributeOldValue: true, + attributeFilter: inHoverEditorLeaf + ? ["data-x", "data-y"] + : ["class", "style"], + }); + } + + private removeParentMoveObserver() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.removeParentMoveObserver, "ExcalidrawView.removeParentMoveObserver"); + if (this.parentMoveObserver) { + this.parentMoveObserver.disconnect(); + this.parentMoveObserver = null; + } + } + + public setTheme(theme: string) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setTheme, "ExcalidrawView.setTheme", theme); + const api = this.excalidrawAPI; + if (!api) { + return; + } + if (this.file) { + //if there is an export theme set, override the theme change + if (hasExportTheme(this.plugin, this.file)) { + return; + } + } + const st: AppState = api.getAppState(); + this.excalidrawData.scene.theme = theme; + //debug({where:"ExcalidrawView.setTheme",file:this.file.name,dataTheme:this.excalidrawData.scene.appState.theme,before:"updateScene"}); + this.updateScene({ + appState: { + ...st, + theme, + }, + storeAction: "update", + }); + } + + private prevTextMode: TextMode; + private blockTextModeChange: boolean = false; + public async changeTextMode(textMode: TextMode, reload: boolean = true) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.changeTextMode, "ExcalidrawView.changeTextMode", textMode, reload); + if(this.compatibilityMode) return; + if(this.blockTextModeChange) return; + this.blockTextModeChange = true; + this.textMode = textMode; + if (textMode === TextMode.parsed) { + this.actionButtons['isRaw'].hide(); + this.actionButtons['isParsed'].show(); + } else { + this.actionButtons['isRaw'].show(); + this.actionButtons['isParsed'].hide(); + } + if (this.toolsPanelRef && this.toolsPanelRef.current) { + this.toolsPanelRef.current.setPreviewMode(textMode === TextMode.parsed); + } + const api = this.excalidrawAPI; + if (api && reload) { + await this.save(); + this.preventAutozoom(); + await this.excalidrawData.loadData(this.data, this.file, this.textMode); + this.excalidrawData.scene.appState.theme = api.getAppState().theme; + await this.loadDrawing(false); + api.history.clear(); //to avoid undo replacing links with parsed text + } + this.prevTextMode = this.textMode; + this.blockTextModeChange = false; + } + + public autosaveFunction: Function; + get autosaveInterval() { + return DEVICE.isMobile ? this.plugin.settings.autosaveIntervalMobile : this.plugin.settings.autosaveIntervalDesktop; + } + + public setupAutosaveTimer() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setupAutosaveTimer, "ExcalidrawView.setupAutosaveTimer"); + + const timer = async () => { + if(!this.isLoaded) { + this.autosaveTimer = window.setTimeout( + timer, + this.autosaveInterval, + ); + return; + } + + const api = this.excalidrawAPI; + if (!api) { + warningUnknowSeriousError(); + return; + } + const st = api.getAppState() as AppState; + const isFreedrawActive = (st.activeTool?.type === "freedraw") && (this.freedrawLastActiveTimestamp > (Date.now()-2000)); + const isEditingText = st.editingTextElement !== null; + const isEditingNewElement = st.newElement !== null; + //this will reset positioning of the cursor in case due to the popup keyboard, + //or the command palette, or some other unexpected reason the onResize would not fire... + this.refreshCanvasOffset(); + if ( + this.isDirty() && + this.plugin.settings.autosave && + !this.semaphores.forceSaving && + !this.semaphores.autosaving && + !this.semaphores.embeddableIsEditingSelf && + !isFreedrawActive && + !isEditingText && + !isEditingNewElement //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/630 + ) { + //console.log("autosave"); + this.autosaveTimer = null; + if (this.excalidrawAPI) { + this.semaphores.autosaving = true; + //changed from await to then to avoid lag during saving of large file + this.save().then(()=>this.semaphores.autosaving = false); + } + this.autosaveTimer = window.setTimeout( + timer, + this.autosaveInterval, + ); + } else { + this.autosaveTimer = window.setTimeout( + timer, + this.plugin.activeExcalidrawView === this && + this.semaphores.dirty && + this.plugin.settings.autosave + ? 1000 //try again in 1 second + : this.autosaveInterval, + ); + } + }; + + this.autosaveFunction = timer; + this.resetAutosaveTimer(); + } + + + private resetAutosaveTimer() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.resetAutosaveTimer, "ExcalidrawView.resetAutosaveTimer"); + if(!this.autosaveFunction) return; + + if (this.autosaveTimer) { + window.clearTimeout(this.autosaveTimer); + this.autosaveTimer = null; + } // clear previous timer if one exists + this.autosaveTimer = window.setTimeout( + this.autosaveFunction, + this.autosaveInterval, + ); + } + + unload(): void { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.unload,`ExcalidrawView.unload, file:${this.file?.name}`); + super.unload(); + } + + async onUnloadFile(file: TFile): Promise { + //deliberately not calling super.onUnloadFile() to avoid autosave (saved in unload) + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onUnloadFile,`ExcalidrawView.onUnloadFile, file:${this.file?.name}`); + let counter = 0; + while (this.semaphores.saving && (counter++ < 200)) { + await sleep(50); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1988 + if(counter++ === 15) { + new Notice(t("SAVE_IS_TAKING_LONG")); + } + if(counter === 80) { + new Notice(t("SAVE_IS_TAKING_VERY_LONG")); + } + } + if(counter >= 200) { + new Notice("Unknown error, save is taking too long"); + return; + } + await this.forceSaveIfRequired(); + } + + private async forceSaveIfRequired():Promise { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.forceSaveIfRequired,`ExcalidrawView.forceSaveIfRequired`); + let watchdog = 0; + let dirty = false; + //if saving was already in progress + //the function awaits the save to finish. + while (this.semaphores.saving && watchdog++ < 200) { + dirty = true; + await sleep(40); + } + if(this.excalidrawAPI) { + this.checkSceneVersion(this.excalidrawAPI.getSceneElements()); + if(this.isDirty()) { + const path = this.file?.path; + const plugin = this.plugin; + window.setTimeout(() => { + plugin.triggerEmbedUpdates(path) + },400); + dirty = true; + await this.save(true,true,true); + } + } + return dirty; + } + + //onClose happens after onunload + protected async onClose(): Promise { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onClose,`ExcalidrawView.onClose, file:${this.file?.name}`); + this.exitFullscreen(); + await this.forceSaveIfRequired(); + if (this.excalidrawRoot) { + this.excalidrawRoot.unmount(); + this.excalidrawRoot = null; + } + + this.clearPreventReloadTimer(); + this.clearEmbeddableNodeIsEditingTimer(); + this.plugin.scriptEngine?.removeViewEAs(this); + this.excalidrawAPI = null; + if(this.draginfoDiv) { + this.ownerDocument.body.removeChild(this.draginfoDiv); + delete this.draginfoDiv; + } + if(this.canvasNodeFactory) { + this.canvasNodeFactory.destroy(); + } + this.canvasNodeFactory = null; + this.embeddableLeafRefs.clear(); + this.embeddableRefs.clear(); + Object.values(this.actionButtons).forEach((el) => el.remove()); + this.actionButtons = {} as Record; + if (this.excalidrawData) { + this.excalidrawData.destroy(); + this.excalidrawData = null; + }; + if(this.exportDialog) { + this.exportDialog.destroy(); + this.exportDialog = null; + } + this.hoverPreviewTarget = null; + if(this.plugin.ea?.targetView === this) { + this.plugin.ea.targetView = null; + } + if(this._hookServer?.targetView === this) { + this._hookServer.targetView = null; + } + this._hookServer = null; + this.containerEl.onWindowMigrated = null; + this.packages = {react:null, reactDOM:null, excalidrawLib:null}; + + let leafcount = 0; + this.app.workspace.iterateAllLeaves(l=>{ + if(l === this.leaf) return; + + if(l.containerEl?.ownerDocument.defaultView === this.ownerWindow) { + leafcount++; + } + }) + if(leafcount === 0) { + this.plugin.deletePackage(this.ownerWindow); + } + + this.lastMouseEvent = null; + this.requestSave = null; + this.leaf.tabHeaderInnerTitleEl.style.color = ""; + + //super.onClose will unmount Excalidraw, need to save before that + await super.onClose(); + tmpBruteForceCleanup(this); + } + + //onunload is called first + onunload() { + super.onunload(); + this.destroyers.forEach((destroyer) => destroyer()); + this.restoreMobileLeaves(); + this.semaphores.viewunload = true; + this.semaphores.popoutUnload = (this.ownerDocument !== document) && (this.ownerDocument.body.querySelectorAll(".workspace-tab-header").length === 0); + + if(this.getHookServer().onViewUnloadHook) { + try { + this.getHookServer().onViewUnloadHook(this); + } catch(e) { + errorlog({where: "ExcalidrawView.onunload", fn: this.getHookServer().onViewUnloadHook, error: e}); + } + } + const tooltip = this.containerEl?.ownerDocument?.body.querySelector( + "body>div.excalidraw-tooltip,div.excalidraw-tooltip--visible", + ); + if (tooltip) { + this.containerEl?.ownerDocument?.body.removeChild(tooltip); + } + this.removeParentMoveObserver(); + this.removeSlidingPanesListner(); + if (this.autosaveTimer) { + window.clearInterval(this.autosaveTimer); + this.autosaveTimer = null; + } + this.autosaveFunction = null; + + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onunload,`ExcalidrawView.onunload, completed`); + } + + /** + * reload is triggered by the modifyEventHandler in main.ts whenever an excalidraw drawing that is currently open + * in a workspace leaf is modified. There can be two reasons for the file change: + * - The user saves the drawing in the active view (either force-save or autosave) + * - The file is modified by some other process, typically as a result of background sync, or because the drawing is open + * side by side, e.g. the canvas in one view and markdown view in the other. + * @param fullreload + * @param file + * @returns + */ + public async reload(fullreload: boolean = false, file?: TFile) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.reload,`ExcalidrawView.reload, file:${this.file?.name}, fullreload:${fullreload}, file:${file?.name}`); + const loadOnModifyTrigger = file && file === this.file; + + //once you've finished editing the embeddable, the first time the file + //reloads will be because of the embeddable changed the file, + //there is a 2000 ms time window allowed for this, but typically this will + //happen within 100 ms. When this happens the timer is cleared and the + //next time reload triggers the file will be reloaded as normal. + if (this.semaphores.embeddableIsEditingSelf) { + //console.log("reload - embeddable is editing") + if(this.editingSelfResetTimer) { + this.clearEmbeddableNodeIsEditingTimer(); + this.semaphores.embeddableIsEditingSelf = false; + } + if(loadOnModifyTrigger) { + this.data = await this.app.vault.read(this.file); + } + return; + } + //console.log("reload - embeddable is not editing") + + if (this.semaphores.preventReload) { + this.semaphores.preventReload = false; + return; + } + if (this.semaphores.saving) return; + this.lastLoadedFile = null; + this.actionButtons['save'].querySelector("svg").removeClass("excalidraw-dirty"); + if (this.compatibilityMode) { + this.clearDirty(); + return; + } + const api = this.excalidrawAPI; + if (!this.file || !api) { + return; + } + + if (loadOnModifyTrigger) { + this.data = await this.app.vault.read(file); + this.preventAutozoom(); + } + if (fullreload) { + await this.excalidrawData.loadData(this.data, this.file, this.textMode); + } else { + await this.excalidrawData.setTextMode(this.textMode); + } + this.excalidrawData.scene.appState.theme = api.getAppState().theme; + await this.loadDrawing(loadOnModifyTrigger); + this.clearDirty(); + } + + async zoomToElementId(id: string, hasGroupref:boolean) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.zoomToElementId, "ExcalidrawView.zoomToElementId", id, hasGroupref); + let counter = 0; + while (!this.excalidrawAPI && counter++<100) await sleep(50); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/734 + const api = this.excalidrawAPI; + if (!api) { + return; + } + const sceneElements = api.getSceneElements(); + + let elements = sceneElements.filter((el: ExcalidrawElement) => el.id === id); + if(elements.length === 0) { + const frame = getFrameBasedOnFrameNameOrId(id, sceneElements); + if (frame) { + elements = [frame]; + } else { + return; + } + } + if(hasGroupref) { + const groupElements = this.plugin.ea.getElementsInTheSameGroupWithElement(elements[0],sceneElements) + if(groupElements.length>0) { + elements = groupElements; + } + } + + this.preventAutozoom(); + this.zoomToElements(!api.getAppState().viewModeEnabled, elements); + } + + setEphemeralState(state: any): void { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setEphemeralState, "ExcalidrawView.setEphemeralState", state); + if (!state) { + return; + } + + if (state.rename === "all") { + this.app.fileManager.promptForFileRename(this.file); + return; + } + + let query: string[] = null; + + if ( + state.match && + state.match.content && + state.match.matches && + state.match.matches.length >= 1 && + state.match.matches[0].length === 2 + ) { + query = [ + state.match.content.substring( + state.match.matches[0][0], + state.match.matches[0][1], + ), + ]; + } + + const waitForExcalidraw = async () => { + let counter = 0; + while ( + (this.semaphores.justLoaded || + !this.isLoaded || + !this.excalidrawAPI || + this.excalidrawAPI?.getAppState()?.isLoading) && + counter++<100 + ) await sleep(50); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/734 + } + + const filenameParts = getEmbeddedFilenameParts( + (state.subpath && state.subpath.startsWith("#^group") && !state.subpath.startsWith("#^group=")) + ? "#^group=" + state.subpath.substring(7) + : (state.subpath && state.subpath.startsWith("#^area") && !state.subpath.startsWith("#^area=")) + ? "#^area=" + state.subpath.substring(6) + : state.subpath + ); + if(filenameParts.hasBlockref) { + window.setTimeout(async () => { + await waitForExcalidraw(); + if(filenameParts.blockref && !filenameParts.hasGroupref) { + if(!this.getScene()?.elements.find((el:ExcalidrawElement)=>el.id === filenameParts.blockref)) { + const cleanQuery = cleanSectionHeading(filenameParts.blockref).replaceAll(" ",""); + const blocks = await this.getBackOfTheNoteBlocks(); + if(blocks.includes(cleanQuery)) { + this.setMarkdownView(state); + return; + } + } + } + window.setTimeout(()=>this.zoomToElementId(filenameParts.blockref, filenameParts.hasGroupref)); + }); + } + + if(filenameParts.hasSectionref) { + query = [`# ${filenameParts.sectionref}`] + } else if (state.line && state.line > 0) { + query = [this.data.split("\n")[state.line - 1]]; + } + + if (query) { + window.setTimeout(async () => { + await waitForExcalidraw(); + + const api = this.excalidrawAPI; + if (!api) return; + if (api.getAppState().isLoading) return; + + const elements = api.getSceneElements() as ExcalidrawElement[]; + + if(query.length === 1 && query[0].startsWith("[")) { + const partsArray = REGEX_LINK.getResList(query[0]); + let parts = partsArray[0]; + if(parts) { + const linkText = REGEX_LINK.getLink(parts); + if(linkText) { + const file = this.plugin.app.metadataCache.getFirstLinkpathDest(linkText, this.file.path); + if(file) { + let fileId:FileId[] = []; + this.excalidrawData.files.forEach((ef,fileID) => { + if(ef.file?.path === file.path) fileId.push(fileID); + }); + if(fileId.length>0) { + const images = elements.filter(el=>el.type === "image" && fileId.includes(el.fileId)); + if(images.length>0) { + this.preventAutozoom(); + window.setTimeout(()=>this.zoomToElements(!api.getAppState().viewModeEnabled, images)); + return; + } + } + } + } + } + } + + if(!this.selectElementsMatchingQuery( + elements, + query, + !api.getAppState().viewModeEnabled, + filenameParts.hasSectionref, + filenameParts.hasGroupref + )) { + const cleanQuery = cleanSectionHeading(query[0]); + const sections = await this.getBackOfTheNoteSections(); + if(sections.includes(cleanQuery) || this.data.includes(query[0])) { + this.setMarkdownView(state); + return; + } + } + }); + } + + //super.setEphemeralState(state); + } + + // clear the view content + clear() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clear, "ExcalidrawView.clear"); + this.semaphores.warnAboutLinearElementLinkClick = true; + this.viewSaveData = ""; + this.canvasNodeFactory.purgeNodes(); + this.embeddableRefs.clear(); + this.embeddableLeafRefs.clear(); + + delete this.exportDialog; + const api = this.excalidrawAPI; + if (!api) { + return; + } + if (this.activeLoader) { + this.activeLoader.terminate = true; + this.activeLoader = null; + } + this.nextLoader = null; + api.resetScene(); + this.previousSceneVersion = 0; + } + + public isLoaded: boolean = false; + async setViewData(data: string, clear: boolean = false) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setViewData, "ExcalidrawView.setViewData", data, clear); + //I am using last loaded file to control when the view reloads. + //It seems text file view gets the modified file event after sync before the modifyEventHandler in main.ts + //reload can only be triggered via reload() + await this.plugin.awaitInit(); + if(this.lastLoadedFile === this.file) return; + this.isLoaded = false; + if(!this.file) return; + if(this.plugin.settings.showNewVersionNotification) checkExcalidrawVersion(); + if(isMaskFile(this.plugin,this.file)) { + const notice = new Notice(t("MASK_FILE_NOTICE"), 5000); + //add click and hold event listner to the notice + let noticeTimeout:number; + this.registerDomEvent(notice.noticeEl,"pointerdown", (ev:MouseEvent) => { + noticeTimeout = window.setTimeout(()=>{ + window.open("https://youtu.be/uHFd0XoHRxE"); + },1000); + }) + this.registerDomEvent(notice.noticeEl,"pointerup", (ev:MouseEvent) => { + window.clearTimeout(noticeTimeout); + }) + } + if (clear) { + this.clear(); + } + this.lastSaveTimestamp = this.file.stat.mtime; + this.lastLoadedFile = this.file; + data = this.data = data.replaceAll("\r\n", "\n").replaceAll("\r", "\n"); + this.app.workspace.onLayoutReady(async () => { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setViewData, `ExcalidrawView.setViewData > app.workspace.onLayoutReady, file:${this.file?.name}, isActiveLeaf:${this?.app?.workspace?.activeLeaf === this.leaf}`); + //the leaf moved to a window and ExcalidrawView was destructed + //Happens during Obsidian startup if View opens in new window. + if(!this?.app) { + return; + } + await this.plugin.awaitInit(); + let counter = 0; + while ((!this.file || !this.plugin.fourthFontLoaded) && counter++<50) await sleep(50); + if(!this.file) return; + this.compatibilityMode = this.file.extension === "excalidraw"; + await this.plugin.loadSettings(); + if (this.compatibilityMode) { + this.plugin.enableLegacyFilePopoverObserver(); + this.actionButtons['isRaw'].hide(); + this.actionButtons['isParsed'].hide(); + this.actionButtons['link'].hide(); + this.textMode = TextMode.raw; + await this.excalidrawData.loadLegacyData(data, this.file); + if (!this.plugin.settings.compatibilityMode) { + new Notice(t("COMPATIBILITY_MODE"), 4000); + } + this.excalidrawData.disableCompression = true; + } else { + this.actionButtons['link'].show(); + this.excalidrawData.disableCompression = false; + const textMode = getTextMode(data); + this.changeTextMode(textMode, false); + try { + if ( + !(await this.excalidrawData.loadData( + data, + this.file, + this.textMode, + )) + ) { + return; + } + } catch (e) { + errorlog({ where: "ExcalidrawView.setViewData", error: e }); + if(e.message === ERROR_IFRAME_CONVERSION_CANCELED) { + this.setMarkdownView(); + return; + } + const file = this.file; + const plugin = this.plugin; + const leaf = this.leaf; + (async () => { + let confirmation:boolean = true; + let counter = 0; + const timestamp = Date.now(); + while (!imageCache.isReady() && confirmation) { + const message = `You've been now waiting for ${Math.round((Date.now()-timestamp)/1000)} seconds. ` + imageCache.initializationNotice = true; + const confirmationPrompt = new ConfirmationPrompt(plugin, + `${counter>0 + ? counter%4 === 0 + ? message + "The CACHE is still loading.

" + : counter%4 === 1 + ? message + "Watch the top right corner for the notification.

" + : counter%4 === 2 + ? message + "I really, really hope the backup will work for you!

" + : message + "I am sorry, it is taking a while, there is not much I can do...

" + : ""}${t("CACHE_NOT_READY")}`); + confirmation = await confirmationPrompt.waitForClose + counter++; + } + + const drawingBAK = await imageCache.getBAKFromCache(file.path); + if (!drawingBAK) { + new Notice( + `Error loading drawing:\n${e.message}${ + e.message === "Cannot read property 'index' of undefined" + ? "\n'# Drawing' section is likely missing" + : "" + }\n\nTry manually fixing the file or restoring an earlier version from sync history.`, + 10000, + ); + return; + } + const confirmationPrompt = new ConfirmationPrompt(plugin,t("BACKUP_AVAILABLE")); + confirmationPrompt.waitForClose.then(async (confirmed) => { + if (confirmed) { + await this.app.vault.modify(file, drawingBAK); + plugin.excalidrawFileModes[leaf.id || file.path] = VIEW_TYPE_EXCALIDRAW; + setExcalidrawView(leaf); + } + }); + + + })(); + this.setMarkdownView(); + return; + } + } + await this.loadDrawing(true); + + if(this.plugin.ea.onFileOpenHook) { + const tempEA = getEA(this); + try { + await this.plugin.ea.onFileOpenHook({ + ea: tempEA, + excalidrawFile: this.file, + view: this, + }); + } catch(e) { + errorlog({ where: "ExcalidrawView.setViewData.onFileOpenHook", error: e }); + } finally { + tempEA.destroy(); + } + } + + const script = this.excalidrawData.getOnLoadScript(); + if(script) { + const scriptname = this.file.basename+ "-onlaod-script"; + const runScript = () => { + if(!this.excalidrawAPI) { //need to wait for Excalidraw to initialize + window.setTimeout(runScript.bind(this),200); + return; + } + this.plugin.scriptEngine.executeScript(this,script,scriptname,this.file); + } + runScript(); + } + this.isLoaded = true; + }); + } + + private getGridColor(bgColor: string, st: AppState): { Bold: string, Regular: string } { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getGridColor, "ExcalidrawView.getGridColor", bgColor, st); + + const cm = this.plugin.ea.getCM(bgColor); + const isDark = cm.isDark(); + + let Regular: string; + let Bold: string; + const opacity = this.plugin.settings.gridSettings.OPACITY/100; + + if (this.plugin.settings.gridSettings.DYNAMIC_COLOR) { + // Dynamic color: concatenate opacity to the HEX string + Regular = (isDark ? cm.lighterBy(10) : cm.darkerBy(10)).alphaTo(opacity).stringRGB({ alpha: true }); + Bold = (isDark ? cm.lighterBy(5) : cm.darkerBy(5)).alphaTo(opacity).stringRGB({ alpha: true }); + } else { + // Custom color handling + const customCM = this.plugin.ea.getCM(this.plugin.settings.gridSettings.COLOR); + const customIsDark = customCM.isDark(); + + // Regular uses the custom color directly + Regular = customCM.alphaTo(opacity).stringRGB({ alpha: true }); + + // Bold is 7 shades lighter or darker based on the custom color's darkness + Bold = (customIsDark ? customCM.lighterBy(10) : customCM.darkerBy(10)).alphaTo(opacity).stringRGB({ alpha: true }); + } + + return { Bold, Regular }; + } + + + public activeLoader: EmbeddedFilesLoader = null; + private nextLoader: EmbeddedFilesLoader = null; + public async loadSceneFiles(isThemeChange: boolean = false) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.loadSceneFiles, "ExcalidrawView.loadSceneFiles", isThemeChange); + if (!this.excalidrawAPI) { + return; + } + const loader = new EmbeddedFilesLoader(this.plugin); + + const runLoader = (l: EmbeddedFilesLoader) => { + this.nextLoader = null; + this.activeLoader = l; + l.loadSceneFiles( + this.excalidrawData, + (files: FileData[], isDark: boolean, final:boolean = true) => { + if (!files) { + return; + } + addFiles(files, this, isDark); + if(!final) return; + this.activeLoader = null; + if (this.nextLoader) { + runLoader(this.nextLoader); + } else { + //in case one or more files have not loaded retry later hoping that sync has delivered the file in the mean time. + this.excalidrawData.getFiles().some(ef=>{ + if(ef && !ef.file && ef.attemptCounter<30) { + const currentFile = this.file.path; + window.setTimeout(async ()=>{ + if(this && this.excalidrawAPI && currentFile === this.file.path) { + this.loadSceneFiles(); + } + },2000) + return true; + } + return false; + }) + } + },0,isThemeChange, + ); + }; + if (!this.activeLoader) { + runLoader(loader); + } else { + this.nextLoader = loader; + } + } + + public async synchronizeWithData(inData: ExcalidrawData) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.synchronizeWithData, "ExcalidrawView.synchronizeWithData", inData); + if(this.semaphores.embeddableIsEditingSelf) { + return; + } + //console.log("synchronizeWithData - embeddable is not editing"); + //check if saving, wait until not + let counter = 0; + while(this.semaphores.saving && counter++<30) { + await sleep(100); + } + if(counter>=30) { + errorlog({ + where:"ExcalidrawView.synchronizeWithData", + message:`Aborting sync with received file (${this.file.path}) because semaphores.saving remained true for ower 3 seconds`, + "fn": this.synchronizeWithData + }); + return; + } + this.semaphores.saving = true; + let reloadFiles = false; + + try { + const deletedIds = inData.deletedElements.map(el=>el.id); + const sceneElements = this.excalidrawAPI.getSceneElementsIncludingDeleted() + //remove deleted elements + .filter((el: ExcalidrawElement)=>!deletedIds.contains(el.id)); + const sceneElementIds = sceneElements.map((el:ExcalidrawElement)=>el.id); + + const manageMapChanges = (incomingElement: ExcalidrawElement ) => { + switch(incomingElement.type) { + case "text": + this.excalidrawData.textElements.set( + incomingElement.id, + inData.textElements.get(incomingElement.id) + ); + break; + case "image": + if(inData.getFile(incomingElement.fileId)) { + this.excalidrawData.setFile( + incomingElement.fileId, + inData.getFile(incomingElement.fileId) + ); + reloadFiles = true; + } else if (inData.getEquation(incomingElement.fileId)) { + this.excalidrawData.setEquation( + incomingElement.fileId, + inData.getEquation(incomingElement.fileId) + ) + reloadFiles = true; + } + break; + } + + if(inData.elementLinks.has(incomingElement.id)) { + this.excalidrawData.elementLinks.set( + incomingElement.id, + inData.elementLinks.get(incomingElement.id) + ) + } + + } + + //update items with higher version number then in scene + inData.scene.elements.forEach(( + incomingElement:ExcalidrawElement, + idx: number, + inElements: ExcalidrawElement[] + )=>{ + const sceneElement:ExcalidrawElement = sceneElements.filter( + (element:ExcalidrawElement)=>element.id === incomingElement.id + )[0]; + if( + sceneElement && + (sceneElement.version < incomingElement.version || + //in case of competing versions of the truth, the incoming version will be honored + (sceneElement.version === incomingElement.version && + JSON.stringify(sceneElement) !== JSON.stringify(incomingElement)) + ) + ) { + manageMapChanges(incomingElement); + //place into correct element layer sequence + const currentLayer = sceneElementIds.indexOf(incomingElement.id); + //remove current element from scene + const elToMove = sceneElements.splice(currentLayer,1); + if(idx === 0) { + sceneElements.splice(0,0,incomingElement); + if(currentLayer!== 0) { + sceneElementIds.splice(currentLayer,1); + sceneElementIds.splice(0,0,incomingElement.id); + } + } else { + const prevId = inElements[idx-1].id; + const parentLayer = sceneElementIds.indexOf(prevId); + sceneElements.splice(parentLayer+1,0,incomingElement); + if(parentLayer!==currentLayer-1) { + sceneElementIds.splice(currentLayer,1) + sceneElementIds.splice(parentLayer+1,0,incomingElement.id); + } + } + return; + } else if(!sceneElement) { + manageMapChanges(incomingElement); + + if(idx === 0) { + sceneElements.splice(0,0,incomingElement); + sceneElementIds.splice(0,0,incomingElement.id); + } else { + const prevId = inElements[idx-1].id; + const parentLayer = sceneElementIds.indexOf(prevId); + sceneElements.splice(parentLayer+1,0,incomingElement); + sceneElementIds.splice(parentLayer+1,0,incomingElement.id); + } + } else if(sceneElement && incomingElement.type === "image") { //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/632 + const incomingFile = inData.getFile(incomingElement.fileId); + const sceneFile = this.excalidrawData.getFile(incomingElement.fileId); + + const shouldUpdate = Boolean(incomingFile) && ( + ((sceneElement as ExcalidrawImageElement).fileId !== incomingElement.fileId) || + (incomingFile.file && (sceneFile.file !== incomingFile.file)) || + (incomingFile.hyperlink && (sceneFile.hyperlink !== incomingFile.hyperlink)) || + (incomingFile.linkParts?.original && (sceneFile.linkParts?.original !== incomingFile.linkParts?.original)) + ) + if(shouldUpdate) { + this.excalidrawData.setFile( + incomingElement.fileId, + inData.getFile(incomingElement.fileId) + ); + reloadFiles = true; + } + } + }) + this.previousSceneVersion = this.getSceneVersion(sceneElements); + //changing files could result in a race condition for sync. If at the end of sync there are differences + //set dirty will trigger an autosave + if(this.getSceneVersion(inData.scene.elements) !== this.previousSceneVersion) { + this.setDirty(3); + } + this.updateScene({elements: sceneElements, storeAction: "capture"}); + if(reloadFiles) this.loadSceneFiles(); + } catch(e) { + errorlog({ + where:"ExcalidrawView.synchronizeWithData", + message:`Error during sync with received file (${this.file.path})`, + "fn": this.synchronizeWithData, + error: e + }); + } + this.semaphores.saving = false; + } + + /** + * + * @param justloaded - a flag to trigger zoom to fit after the drawing has been loaded + */ + public async loadDrawing(justloaded: boolean, deletedElements?: ExcalidrawElement[]) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.loadDrawing, "ExcalidrawView.loadDrawing", justloaded, deletedElements); + const excalidrawData = this.excalidrawData.scene; + this.semaphores.justLoaded = justloaded; + this.clearDirty(); + const om = this.excalidrawData.getOpenMode(); + this.semaphores.preventReload = false; + const penEnabled = this.plugin.isPenMode(); + const api = this.excalidrawAPI; + if (api) { + //isLoaded flags that a new file is being loaded, isLoaded will be true after loadDrawing completes + const viewModeEnabled = !this.isLoaded + ? (excalidrawData.elements.length > 0 ? om.viewModeEnabled : false) + : api.getAppState().viewModeEnabled; + const zenModeEnabled = !this.isLoaded + ? om.zenModeEnabled + : api.getAppState().zenModeEnabled; + //debug({where:"ExcalidrawView.loadDrawing",file:this.file.name,dataTheme:excalidrawData.appState.theme,before:"updateScene"}) + //api.setLocalFont(this.plugin.settings.experimentalEnableFourthFont); + + this.updateScene( + { + elements: excalidrawData.elements.concat(deletedElements??[]), //need to preserve deleted elements during autosave if images, links, etc. are updated + files: excalidrawData.files, + storeAction: justloaded ? "update" : "update", //was none, but I think based on a false understanding of none + }, + justloaded + ); + this.updateScene( + { + //elements: excalidrawData.elements.concat(deletedElements??[]), //need to preserve deleted elements during autosave if images, links, etc. are updated + appState: { + ...excalidrawData.appState, + ...this.excalidrawData.selectedElementIds //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/609 + ? this.excalidrawData.selectedElementIds + : {}, + zenModeEnabled, + viewModeEnabled, + linkOpacity: this.excalidrawData.getLinkOpacity(), + trayModeEnabled: this.plugin.settings.defaultTrayMode, + penMode: penEnabled, + penDetected: penEnabled, + allowPinchZoom: this.plugin.settings.allowPinchZoom, + allowWheelZoom: this.plugin.settings.allowWheelZoom, + pinnedScripts: this.plugin.settings.pinnedScripts, + customPens: this.plugin.settings.customPens.slice(0,this.plugin.settings.numberOfCustomPens), + }, + storeAction: justloaded ? "update" : "update", //was none, but I think based on a false understanding of none + }, + ); + if ( + this.app.workspace.getActiveViewOfType(ExcalidrawView) === this.leaf.view && + this.excalidrawWrapperRef + ) { + //.firstElmentChild solves this issue: https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/346 + this.excalidrawWrapperRef.current?.firstElementChild?.focus(); + } + //debug({where:"ExcalidrawView.loadDrawing",file:this.file.name,before:"this.loadSceneFiles"}); + this.onAfterLoadScene(justloaded); + } else { + this.instantiateExcalidraw({ + elements: excalidrawData.elements, + appState: { + ...excalidrawData.appState, + zenModeEnabled: om.zenModeEnabled, + viewModeEnabled: excalidrawData.elements.length > 0 ? om.viewModeEnabled : false, + linkOpacity: this.excalidrawData.getLinkOpacity(), + trayModeEnabled: this.plugin.settings.defaultTrayMode, + penMode: penEnabled, + penDetected: penEnabled, + allowPinchZoom: this.plugin.settings.allowPinchZoom, + allowWheelZoom: this.plugin.settings.allowWheelZoom, + pinnedScripts: this.plugin.settings.pinnedScripts, + customPens: this.plugin.settings.customPens.slice(0,this.plugin.settings.numberOfCustomPens), + }, + files: excalidrawData.files, + libraryItems: await this.getLibrary(), + }); + //files are loaded when excalidrawAPI is mounted + } + const isCompressed = this.data.match(/```compressed\-json\n/gm) !== null; + + if ( + !this.compatibilityMode && + this.plugin.settings.compress !== isCompressed && + !this.isEditedAsMarkdownInOtherView() + ) { + this.setDirty(4); + } + } + + isEditedAsMarkdownInOtherView(): boolean { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.isEditedAsMarkdownInOtherView, "ExcalidrawView.isEditedAsMarkdownInOtherView"); + //if the user is editing the same file in markdown mode, do not compress it + const leaves = this.app.workspace.getLeavesOfType("markdown"); + return ( + leaves.filter((leaf) => (leaf.view as MarkdownView).file === this.file) + .length > 0 + ); + } + + private onAfterLoadScene(justloaded: boolean) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onAfterLoadScene, "ExcalidrawView.onAfterLoadScene"); + this.loadSceneFiles(); + this.updateContainerSize(null, true, justloaded); + this.initializeToolsIconPanelAfterLoading(); + } + + public setDirty(location?:number) { + if(this.semaphores.saving) return; //do not set dirty if saving + if(!this.isDirty()) { + //the autosave timer should start when the first stroke was made... thus avoiding an immediate impact by saving right then + this.resetAutosaveTimer(); + } + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setDirty,`ExcalidrawView.setDirty, location:${location}`); + this.semaphores.dirty = this.file?.path; + this.actionButtons['save'].querySelector("svg").addClass("excalidraw-dirty"); + if(!this.semaphores.viewunload && this.toolsPanelRef?.current) { + this.toolsPanelRef.current.setDirty(true); + } + if(!DEVICE.isMobile) { + if(requireApiVersion("0.16.0")) { + this.leaf.tabHeaderInnerIconEl.style.color="var(--color-accent)" + this.leaf.tabHeaderInnerTitleEl.style.color="var(--color-accent)" + } + } + } + + public isDirty() { + return Boolean(this.semaphores?.dirty) && (this.semaphores.dirty === this.file?.path); + } + + public clearDirty() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearDirty,`ExcalidrawView.clearDirty`); + if(this.semaphores.viewunload) return; + const api = this.excalidrawAPI; + if (!api) { + return; + } + this.semaphores.dirty = null; + if(this.toolsPanelRef?.current) { + this.toolsPanelRef.current.setDirty(false); + } + const el = api.getSceneElements(); + if (el) { + this.previousSceneVersion = this.getSceneVersion(el); + } + this.actionButtons['save'].querySelector("svg").removeClass("excalidraw-dirty"); + if(!DEVICE.isMobile) { + if(requireApiVersion("0.16.0")) { + this.leaf.tabHeaderInnerIconEl.style.color="" + this.leaf.tabHeaderInnerTitleEl.style.color="" + } + } + } + + public async initializeToolsIconPanelAfterLoading() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.initializeToolsIconPanelAfterLoading,`ExcalidrawView.initializeToolsIconPanelAfterLoading`); + if(this.semaphores.viewunload) return; + const api = this.excalidrawAPI; + if (!api) { + return; + } + const st = api.getAppState(); + //since Obsidian 1.6.0 onLayoutReady calls happen asynchronously compared to starting Excalidraw view + //these validations are just to make sure that initialization is complete + let counter = 0; + while(!this.plugin.scriptEngine && counter++<50) { + sleep(50); + } + + const panel = this.toolsPanelRef?.current; + if (!panel || !this.plugin.scriptEngine) { + return; + } + + panel.setTheme(st.theme); + panel.setExcalidrawViewMode(st.viewModeEnabled); + panel.setPreviewMode( + this.compatibilityMode ? null : this.textMode === TextMode.parsed, + ); + panel.updateScriptIconMap(this.plugin.scriptEngine.scriptIconMap); + } + + //Compatibility mode with .excalidraw files + canAcceptExtension(extension: string) { + return extension === "excalidraw"; //["excalidraw","md"].includes(extension); + } + + // gets the title of the document + getDisplayText() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getDisplayText, "ExcalidrawView.getDisplayText", this.file?.basename ?? "NOFILE"); + if (this.file) { + return this.file.basename; + } + return t("NOFILE"); + } + + // the view type name + getViewType() { + return VIEW_TYPE_EXCALIDRAW; + } + + // icon for the view + getIcon() { + return ICON_NAME; + } + + async setMarkdownView(eState?: any) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setMarkdownView, "ExcalidrawView.setMarkdownView", eState); + //save before switching to markdown view. + //this would also happen onClose, but it does not hurt to save it here + //this way isDirty() will return false in onClose, thuse + //saving here will not result in double save + //there was a race condition when clicking a link with a section or block reference to the back-of-the-note + //that resulted in a call to save after the view has been destroyed + //The sleep is required for metadata cache to be updated with the location of the block or section + await this.forceSaveIfRequired(); + await sleep(200); //dirty hack to wait for Obsidian metadata to be updated, note that save may have been triggered elsewhere already + this.plugin.excalidrawFileModes[this.id || this.file.path] = "markdown"; + this.plugin.setMarkdownView(this.leaf, eState); + } + + public async openAsMarkdown(eState?: any) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.openAsMarkdown, "ExcalidrawView.openAsMarkdown", eState); + if (this.plugin.settings.compress && this.plugin.settings.decompressForMDView) { + this.excalidrawData.disableCompression = true; + await this.save(true, true, true); + } else if (this.isDirty()) { + await this.save(true, true, true); + } + this.setMarkdownView(eState); + } + + public async convertExcalidrawToMD() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.convertExcalidrawToMD, "ExcalidrawView.convertExcalidrawToMD"); + await this.save(); + const file = await this.plugin.convertSingleExcalidrawToMD(this.file); + await sleep(250); //dirty hack to wait for Obsidian metadata to be updated + this.plugin.openDrawing( + file, + "active-pane", + true + ); + } + + public convertTextElementToMarkdown(textElement: ExcalidrawTextElement, containerElement: ExcalidrawElement) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.convertTextElementToMarkdown, "ExcalidrawView.convertTextElementToMarkdown", textElement, containerElement); + if(!textElement) return; + const prompt = new Prompt( + this.app, + "Filename", + "", + "Leave blank to cancel this action", + ); + prompt.openAndGetValue(async (filename: string) => { + if (!filename) { + return; + } + filename = `${filename}.md`; + const folderpath = splitFolderAndFilename(this.file.path).folderpath; + await checkAndCreateFolder(folderpath); //create folder if it does not exist + const fname = getNewUniqueFilepath( + this.app.vault, + filename, + folderpath, + ); + const text:string[] = []; + if(containerElement && containerElement.link) text.push(containerElement.link); + text.push(textElement.rawText); + const f = await this.app.vault.create( + fname, + text.join("\n"), + ); + if(f) { + const ea:ExcalidrawAutomate = getEA(this); + const elements = containerElement ? [textElement,containerElement] : [textElement]; + ea.copyViewElementsToEAforEditing(elements); + ea.getElements().forEach(el=>el.isDeleted = true); + const [x,y,w,h] = containerElement + ? [containerElement.x,containerElement.y,containerElement.width,containerElement.height] + : [textElement.x, textElement.y, MAX_IMAGE_SIZE,MAX_IMAGE_SIZE]; + const id = ea.addEmbeddable(x,y,w,h, undefined,f); + if(containerElement) { + const props:(keyof ExcalidrawElement)[] = ["backgroundColor", "fillStyle","roughness","roundness","strokeColor","strokeStyle","strokeWidth"]; + props.forEach((prop)=>{ + const element = ea.getElement(id); + if (prop in element) { + (element as any)[prop] = containerElement[prop]; + } + }); + } + ea.getElement(id) + await ea.addElementsToView(); + ea.destroy(); + } + }); + } + + async addYouTubeThumbnail(link:string) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addYouTubeThumbnail, "ExcalidrawView.addYouTubeThumbnail", link); + const thumbnailLink = await getYouTubeThumbnailLink(link); + const ea = getEA(this) as ExcalidrawAutomate; + const id = await ea.addImage(0,0,thumbnailLink); + ea.getElement(id).link = link; + await ea.addElementsToView(true,true,true) + ea.destroy(); + + } + + async addImageWithURL(link:string) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addImageWithURL, "ExcalidrawView.addImageWithURL", link); + const ea = getEA(this) as ExcalidrawAutomate; + await ea.addImage(0,0,link); + await ea.addElementsToView(true,true,true); + ea.destroy(); + } + + async addImageSaveToVault(link:string) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addImageSaveToVault, "ExcalidrawView.addImageSaveToVault", link); + const ea = getEA(this) as ExcalidrawAutomate; + const mimeType = getMimeType(getURLImageExtension(link)); + const dataURL = await getDataURLFromURL(link,mimeType,3000); + const fileId = await generateIdFromFile((new TextEncoder()).encode(dataURL as string)) + const file = await this.excalidrawData.saveDataURLtoVault(dataURL,mimeType,fileId); + if(!file) { + new Notice(t("ERROR_SAVING_IMAGE")); + ea.destroy(); + return; + } + await ea.addImage(0,0,file); + await ea.addElementsToView(true,true,true); + ea.destroy(); + } + + async addTextWithIframely(text:string) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addTextWithIframely, "ExcalidrawView.addTextWithIframely", text); + const id = await this.addText(text); + const url = `http://iframely.server.crestify.com/iframely?url=${text}`; + try { + const data = JSON.parse(await request({ url })); + if (!data || data.error || !data.meta?.title) { + return; + } + const ea = getEA(this) as ExcalidrawAutomate; + const el = ea + .getViewElements() + .filter((el) => el.type==="text" && el.id === id); + if (el.length === 1) { + ea.copyViewElementsToEAforEditing(el); + const textElement = ea.getElement(el[0].id) as Mutable; + textElement.text = textElement.originalText = textElement.rawText = + `[${data.meta.title}](${text})`; + await ea.addElementsToView(false, false, false); + ea.destroy(); + } + } catch(e) { + }; + } + + onPaneMenu(menu: Menu, source: string): void { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onPaneMenu, "ExcalidrawView.onPaneMenu", menu, source); + if(this.excalidrawAPI && this.getViewSelectedElements().some(el=>el.type==="text")) { + menu.addItem(item => { + item + .setTitle(t("OPEN_LINK")) + .setIcon("external-link") + .setSection("pane") + .onClick(evt => { + this.handleLinkClick(evt as MouseEvent); + }); + }) + } + // Add a menu item to force the board to markdown view + if (!this.compatibilityMode) { + menu + .addItem((item) => { + item + .setTitle(t("OPEN_AS_MD")) + .setIcon("document") + .onClick(() => { + this.openAsMarkdown(); + }) + .setSection("pane"); + }) + } else { + menu.addItem((item) => { + item + .setTitle(t("CONVERT_FILE")) + .onClick(() => this.convertExcalidrawToMD()) + .setSection("pane"); + }); + } + menu + .addItem((item) => { + item + .setTitle(t("EXPORT_IMAGE")) + .setIcon(EXPORT_IMG_ICON_NAME) + .setSection("pane") + .onClick(async (ev) => { + if (!this.excalidrawAPI || !this.file) { + return; + } + if(!this.exportDialog) { + this.exportDialog = new ExportDialog(this.plugin, this,this.file); + this.exportDialog.createForm(); + } + this.exportDialog.open(); + }) + .setSection("pane"); + }) + .addItem(item => { + item + .setTitle(t("INSTALL_SCRIPT_BUTTON")) + .setIcon(SCRIPTENGINE_ICON_NAME) + .setSection("pane") + .onClick(()=>{ + new ScriptInstallPrompt(this.plugin).open(); + }) + }) + super.onPaneMenu(menu, source); + } + + async getLibrary() { + const data: any = this.plugin.getStencilLibrary(); + return data?.library ? data.library : data?.libraryItems ?? []; + } + + public setCurrentPositionToCenter(){ + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.setCurrentPositionToCenter, "ExcalidrawView.setCurrentPositionToCenter"); + const api = this.excalidrawAPI as ExcalidrawImperativeAPI; + if (!api) { + return; + } + const st = api.getAppState(); + const { width, height, offsetLeft, offsetTop } = st; + this.currentPosition = viewportCoordsToSceneCoords( + { + clientX: width / 2 + offsetLeft, + clientY: height / 2 + offsetTop, + }, + st, + ); + }; + + private getSelectedTextElement(): SelectedElementWithLink{ + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getSelectedTextElement, "ExcalidrawView.getSelectedTextElement"); + const api = this.excalidrawAPI; + if (!api) { + return { id: null, text: null }; + } + if (api.getAppState().viewModeEnabled) { + if (this.selectedTextElement) { + const retval = this.selectedTextElement; + this.selectedTextElement = null; + return retval; + } + //return { id: null, text: null }; + } + const selectedElement = api + .getSceneElements() + .filter( + (el: ExcalidrawElement) => + el.id === Object.keys(api.getAppState().selectedElementIds)[0], + ); + if (selectedElement.length === 0) { + return { id: null, text: null }; + } + + if (selectedElement[0].type === "text") { + return { id: selectedElement[0].id, text: selectedElement[0].text }; + } //a text element was selected. Return text + + if (["image","arrow"].contains(selectedElement[0].type)) { + return { id: null, text: null }; + } + + const boundTextElements = selectedElement[0].boundElements?.filter( + (be: any) => be.type === "text", + ); + if (boundTextElements?.length > 0) { + const textElement = api + .getSceneElements() + .filter( + (el: ExcalidrawElement) => el.id === boundTextElements[0].id, + ); + if (textElement.length > 0) { + return { id: textElement[0].id, text: textElement[0].text }; + } + } //is a text container selected? + + if (selectedElement[0].groupIds.length === 0) { + return { id: null, text: null }; + } //is the selected element part of a group? + + const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of + const textElement = api + .getSceneElements() + .filter((el: any) => el.groupIds?.includes(group)) + .filter((el: any) => el.type === "text"); //filter for text elements of the group + if (textElement.length === 0) { + return { id: null, text: null }; + } //the group had no text element member + + return { id: selectedElement[0].id, text: selectedElement[0].text }; //return text element text + }; + + private getSelectedImageElement(): SelectedImage { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getSelectedImageElement, "ExcalidrawView.getSelectedImageElement"); + const api = this.excalidrawAPI; + if (!api) { + return { id: null, fileId: null }; + } + if (api.getAppState().viewModeEnabled) { + if (this.selectedImageElement) { + const retval = this.selectedImageElement; + this.selectedImageElement = null; + return retval; + } + //return { id: null, fileId: null }; + } + const selectedElement = api + .getSceneElements() + .filter( + (el: any) => + el.id == Object.keys(api.getAppState().selectedElementIds)[0], + ); + if (selectedElement.length === 0) { + return { id: null, fileId: null }; + } + if (selectedElement[0].type == "image") { + return { + id: selectedElement[0].id, + fileId: selectedElement[0].fileId, + }; + } //an image element was selected. Return fileId + + if (selectedElement[0].type === "text") { + return { id: null, fileId: null }; + } + + if (selectedElement[0].groupIds.length === 0) { + return { id: null, fileId: null }; + } //is the selected element part of a group? + const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of + const imageElement = api + .getSceneElements() + .filter((el: any) => el.groupIds?.includes(group)) + .filter((el: any) => el.type == "image"); //filter for Image elements of the group + if (imageElement.length === 0) { + return { id: null, fileId: null }; + } //the group had no image element member + return { id: imageElement[0].id, fileId: imageElement[0].fileId }; //return image element fileId + }; + + private getSelectedElementWithLink(): { id: string; text: string } { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getSelectedElementWithLink, "ExcalidrawView.getSelectedElementWithLink"); + const api = this.excalidrawAPI; + if (!api) { + return { id: null, text: null }; + } + if (api.getAppState().viewModeEnabled) { + if (this.selectedElementWithLink) { + const retval = this.selectedElementWithLink; + this.selectedElementWithLink = null; + return retval; + } + //return { id: null, text: null }; + } + const selectedElement = api + .getSceneElements() + .filter( + (el: any) => + el.id == Object.keys(api.getAppState().selectedElementIds)[0], + ); + if (selectedElement.length === 0) { + return { id: null, text: null }; + } + if (selectedElement[0].link) { + return { + id: selectedElement[0].id, + text: selectedElement[0].link, + }; + } + + const textId = getBoundTextElementId(selectedElement[0]); + if (textId) { + const textElement = api + .getSceneElements() + .filter((el: any) => el.id === textId && el.link); + if (textElement.length > 0) { + return { id: textElement[0].id, text: textElement[0].text }; + } + } + + if (selectedElement[0].groupIds.length === 0) { + return { id: null, text: null }; + } //is the selected element part of a group? + const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of + const elementsWithLink = api + .getSceneElements() + .filter((el: any) => el.groupIds?.includes(group)) + .filter((el: any) => el.link); //filter for elements of the group that have a link + if (elementsWithLink.length === 0) { + return { id: null, text: null }; + } //the group had no image element member + return { id: elementsWithLink[0].id, text: elementsWithLink[0].link }; //return image element fileId + }; + + public async addLink( + markdownlink: string, + path: string, + alias: string, + originalLink?: string, + ) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addLink, "ExcalidrawView.addLink", markdownlink, path, alias); + const api = this.excalidrawAPI as ExcalidrawImperativeAPI; + const st = api.getAppState(); + if( + !st.selectedElementIds || + (st.selectedElementIds && Object.keys(st.selectedElementIds).length !== 1) + ) { + this.addText(markdownlink); + return; + } + const selectedElementId = Object.keys(api.getAppState().selectedElementIds)[0]; + const selectedElement = api.getSceneElements().find(el=>el.id === selectedElementId); + if(!selectedElement || (!Boolean(originalLink) && (selectedElement && selectedElement.link !== null) )) { + if(selectedElement) new Notice("Selected element already has a link. Inserting link as text."); + this.addText(markdownlink); + return; + } + const ea = getEA(this) as ExcalidrawAutomate; + ea.copyViewElementsToEAforEditing([selectedElement]); + if(originalLink?.match(/\[\[(.*?)\]\]/)?.[1]) { + markdownlink = originalLink.replace(/(\[\[.*?\]\])/,markdownlink); + } + ea.getElement(selectedElementId).link = markdownlink; + await ea.addElementsToView(false, true); + ea.destroy(); + if(Boolean(originalLink)) { + this.updateScene({ + appState: { + showHyperlinkPopup: { + newValue : "info", oldValue : "editor" + } + } + }); + } + } + + public async addText ( + text: string, + fontFamily?: 1 | 2 | 3 | 4, + save: boolean = true + ): Promise { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addText, "ExcalidrawView.addText", text, fontFamily, save); + const api = this.excalidrawAPI as ExcalidrawImperativeAPI; + if (!api) { + return; + } + const st: AppState = api.getAppState(); + const ea = getEA(this); + ea.style.strokeColor = st.currentItemStrokeColor ?? "black"; + ea.style.opacity = st.currentItemOpacity ?? 1; + ea.style.fontFamily = fontFamily ?? st.currentItemFontFamily ?? 1; + ea.style.fontSize = st.currentItemFontSize ?? 20; + ea.style.textAlign = st.currentItemTextAlign ?? "left"; + + const { width, height } = st; + + const top = viewportCoordsToSceneCoords( + { + clientX: 0, + clientY: 0, + }, + st, + ); + const bottom = viewportCoordsToSceneCoords( + { + clientX: width, + clientY: height, + }, + st, + ); + const isPointerOutsideVisibleArea = top.x>this.currentPosition.x || bottom.xthis.currentPosition.y || bottom.y { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addElements, "ExcalidrawView.addElements", newElements, repositionToCursor, save, images, newElementsOnTop, shouldRestoreElements); + const api = this.excalidrawAPI as ExcalidrawImperativeAPI; + if (!api) { + return false; + } + const elementsMap = arrayToMap(api.getSceneElements()); + const textElements = newElements.filter((el) => el.type == "text"); + for (let i = 0; i < textElements.length; i++) { + const textElement = textElements[i] as Mutable; + const {parseResult, link} = + await this.excalidrawData.addTextElement( + textElement.id, + textElement.text, + textElement.rawText, //TODO: implement originalText support in ExcalidrawAutomate + ); + if (link) { + textElement.link = link; + } + if (this.textMode === TextMode.parsed && !textElement?.isDeleted) { + const {text, x, y, width, height} = refreshTextDimensions( + textElement,null,elementsMap,parseResult + ); + textElement.text = text; + textElement.originalText = parseResult; + textElement.x = x; + textElement.y = y; + textElement.width = width; + textElement.height = height; + } + } + + if (repositionToCursor) { + newElements = repositionElementsToCursor( + newElements, + this.currentPosition, + true, + ); + } + + const newIds = newElements.map((e) => e.id); + const el: ExcalidrawElement[] = api.getSceneElements() as ExcalidrawElement[]; + const removeList: string[] = []; + + //need to update elements in scene.elements to maintain sequence of layers + for (let i = 0; i < el.length; i++) { + const id = el[i].id; + if (newIds.includes(id)) { + el[i] = newElements.filter((ne) => ne.id === id)[0]; + removeList.push(id); + } + } + + const elements = newElementsOnTop + ? el.concat(newElements.filter((e) => !removeList.includes(e.id))) + : newElements.filter((e) => !removeList.includes(e.id)).concat(el); + + this.updateScene( + { + elements, + storeAction: "capture", + }, + shouldRestoreElements, + ); + + if (images && Object.keys(images).length >0) { + const files: BinaryFileData[] = []; + Object.keys(images).forEach((k) => { + files.push({ + mimeType: images[k].mimeType, + id: images[k].id, + dataURL: images[k].dataURL, + created: images[k].created, + }); + if (images[k].file || images[k].isHyperLink || images[k].isLocalLink) { + const embeddedFile = new EmbeddedFile( + this.plugin, + this.file.path, + images[k].isHyperLink && !images[k].isLocalLink + ? images[k].hyperlink + : images[k].file, + ); + const st: AppState = api.getAppState(); + embeddedFile.setImage( + images[k].dataURL, + images[k].mimeType, + images[k].size, + st.theme === "dark", + images[k].hasSVGwithBitmap, + ); + this.excalidrawData.setFile(images[k].id, embeddedFile); + } + if (images[k].latex) { + this.excalidrawData.setEquation(images[k].id, { + latex: images[k].latex, + isLoaded: true, + }); + } + }); + api.addFiles(files); + } + api.updateContainerSize(api.getSceneElements().filter(el => newIds.includes(el.id)).filter(isContainer)); + if (save) { + await this.save(false); //preventReload=false will ensure that markdown links are paresed and displayed correctly + } else { + this.setDirty(5); + } + return true; + }; + + public getScene (selectedOnly?: boolean) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getScene, "ExcalidrawView.getScene", selectedOnly); +/* if (this.lastSceneSnapshot) { + return this.lastSceneSnapshot; + }*/ + const api = this.excalidrawAPI; + if (!api) { + return null; + } + const el: ExcalidrawElement[] = selectedOnly ? this.getViewSelectedElements() : api.getSceneElements(); + const st: AppState = api.getAppState(); + const files = {...api.getFiles()}; + + if (files) { + const imgIds = el + .filter((e) => e.type === "image") + .map((e: any) => e.fileId); + const toDelete = Object.keys(files).filter( + (k) => !imgIds.contains(k), + ); + toDelete.forEach((k) => delete files[k]); + } + + const activeTool = {...st.activeTool}; + if(!["freedraw","hand"].includes(activeTool.type)) { + activeTool.type = "selection"; + } + activeTool.customType = null; + activeTool.lastActiveTool = null; + + return { + type: "excalidraw", + version: 2, + source: GITHUB_RELEASES+PLUGIN_VERSION, + elements: el, + //see also ExcalidrawAutomate async create( + appState: { + theme: st.theme, + viewBackgroundColor: st.viewBackgroundColor, + currentItemStrokeColor: st.currentItemStrokeColor, + currentItemBackgroundColor: st.currentItemBackgroundColor, + currentItemFillStyle: st.currentItemFillStyle, + currentItemStrokeWidth: st.currentItemStrokeWidth, + currentItemStrokeStyle: st.currentItemStrokeStyle, + currentItemRoughness: st.currentItemRoughness, + currentItemOpacity: st.currentItemOpacity, + currentItemFontFamily: st.currentItemFontFamily, + currentItemFontSize: st.currentItemFontSize, + currentItemTextAlign: st.currentItemTextAlign, + currentItemStartArrowhead: st.currentItemStartArrowhead, + currentItemEndArrowhead: st.currentItemEndArrowhead, + currentItemArrowType: st.currentItemArrowType, + scrollX: st.scrollX, + scrollY: st.scrollY, + zoom: st.zoom, + currentItemRoundness: st.currentItemRoundness, + gridSize: st.gridSize, + gridStep: st.gridStep, + gridModeEnabled: st.gridModeEnabled, + gridColor: st.gridColor, + colorPalette: st.colorPalette, + currentStrokeOptions: st.currentStrokeOptions, + frameRendering: st.frameRendering, + objectsSnapModeEnabled: st.objectsSnapModeEnabled, + activeTool, + }, + prevTextMode: this.prevTextMode, + files, + }; + }; + + /** + * ExcalidrawAPI refreshes canvas offsets + * @returns + */ + private refreshCanvasOffset() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.refreshCanvasOffset, "ExcalidrawView.refreshCanvasOffset"); + if(this.contentEl.clientWidth === 0 || this.contentEl.clientHeight === 0) return; + const api = this.excalidrawAPI; + if (!api) { + return; + } + api.refresh(); + }; + + // depricated. kept for backward compatibility. e.g. used by the Slideshow plugin + // 2024.05.03 + public refresh() { + this.refreshCanvasOffset(); + } + + private clearHoverPreview() { + const hoverContainerEl = this.hoverPopover?.containerEl; + //don't auto hide hover-editor + if (this.hoverPopover && !hoverContainerEl?.parentElement?.hasClass("hover-editor")) { + this.hoverPreviewTarget = null; + //@ts-ignore + if(this.hoverPopover.embed?.editor) { + return; + } + this.hoverPopover?.hide(); + } else if (this.hoverPreviewTarget) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.clearHoverPreview, "ExcalidrawView.clearHoverPreview", this); + const event = new MouseEvent("click", { + view: this.ownerWindow, + bubbles: true, + cancelable: true, + }); + this.hoverPreviewTarget.dispatchEvent(event); + this.hoverPreviewTarget = null; + } + }; + + 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 + */ + private identifyElementClicked () { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.identifyElementClicked, "ExcalidrawView.identifyElementClicked"); + this.selectedTextElement = getTextElementAtPointer(this.currentPosition, this); + if (this.selectedTextElement && this.selectedTextElement.id) { + const event = new MouseEvent("click", { + ctrlKey: !(DEVICE.isIOS || DEVICE.isMacOS) || this.modifierKeyDown.ctrlKey, + metaKey: (DEVICE.isIOS || DEVICE.isMacOS) || this.modifierKeyDown.metaKey, + shiftKey: this.modifierKeyDown.shiftKey, + altKey: this.modifierKeyDown.altKey, + }); + this.handleLinkClick(event); + this.selectedTextElement = null; + return; + } + this.selectedImageElement = getImageElementAtPointer(this.currentPosition, this); + if (this.selectedImageElement && this.selectedImageElement.id) { + const event = new MouseEvent("click", { + ctrlKey: !(DEVICE.isIOS || DEVICE.isMacOS) || this.modifierKeyDown.ctrlKey, + metaKey: (DEVICE.isIOS || DEVICE.isMacOS) || this.modifierKeyDown.metaKey, + shiftKey: this.modifierKeyDown.shiftKey, + altKey: this.modifierKeyDown.altKey, + }); + this.handleLinkClick(event); + this.selectedImageElement = null; + return; + } + + this.selectedElementWithLink = getElementWithLinkAtPointer(this.currentPosition, this); + if (this.selectedElementWithLink && this.selectedElementWithLink.id) { + const event = new MouseEvent("click", { + ctrlKey: !(DEVICE.isIOS || DEVICE.isMacOS) || this.modifierKeyDown.ctrlKey, + metaKey: (DEVICE.isIOS || DEVICE.isMacOS) || this.modifierKeyDown.metaKey, + shiftKey: this.modifierKeyDown.shiftKey, + altKey: this.modifierKeyDown.altKey, + }); + this.handleLinkClick(event); + this.selectedElementWithLink = null; + return; + } + }; + + private showHoverPreview(linktext?: string, element?: ExcalidrawElement) { + //(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.showHoverPreview, "ExcalidrawView.showHoverPreview", linktext, element); + if(!this.lastMouseEvent) return; + const st = this.excalidrawAPI?.getAppState(); + if(st?.editingTextElement || st?.newElement) return; //should not activate hover preview when element is being edited or dragged + if(this.semaphores.wheelTimeout) return; + //if link text is not provided, try to get it from the element + if (!linktext) { + if(!this.currentPosition) return; + linktext = ""; + const selectedEl = getTextElementAtPointer(this.currentPosition, this); + if (!selectedEl || !selectedEl.text) { + const selectedImgElement = + getImageElementAtPointer(this.currentPosition, this); + const selectedElementWithLink = (selectedImgElement?.id || selectedImgElement?.id) + ? null + : getElementWithLinkAtPointer(this.currentPosition, this); + element = this.excalidrawAPI.getSceneElements().find((el:ExcalidrawElement)=>el.id === selectedImgElement.id); + if ((!selectedImgElement || !selectedImgElement.fileId) && !selectedElementWithLink?.id) { + return; + } + if (selectedImgElement?.id) { + if (!this.excalidrawData.hasFile(selectedImgElement.fileId)) { + return; + } + const ef = this.excalidrawData.getFile(selectedImgElement.fileId); + if ( + (ef.isHyperLink || ef.isLocalLink) || //web images don't have a preview + (IMAGE_TYPES.contains(ef.file.extension)) || //images don't have a preview + (ef.file.extension.toLowerCase() === "pdf") || //pdfs don't have a preview + (this.plugin.ea.isExcalidrawFile(ef.file)) + ) {//excalidraw files don't have a preview + linktext = getLinkTextFromLink(element.link); + if(!linktext) return; + } else { + const ref = ef.linkParts.ref + ? `#${ef.linkParts.isBlockRef ? "^" : ""}${ef.linkParts.ref}` + : ""; + linktext = + ef.file.path + ref; + } + } + if (selectedElementWithLink?.id) { + linktext = getLinkTextFromLink(selectedElementWithLink.text); + if(!linktext) return; + if(this.app.metadataCache.getFirstLinkpathDest(linktext.split("#")[0],this.file.path) === this.file) return; + } + } else { + const {linkText, selectedElement} = this.getLinkTextForElement(selectedEl, selectedEl); + element = selectedElement; + /*this.excalidrawAPI.getSceneElements().filter((el:ExcalidrawElement)=>el.id === selectedElement.id)[0]; + const text: string = + this.textMode === TextMode.parsed + ? this.excalidrawData.getRawText(selectedElement.id) + : selectedElement.text;*/ + + linktext = getLinkTextFromLink(linkText); + if(!linktext) return; + } + } + + if(this.getHookServer().onLinkHoverHook) { + try { + if(!this.getHookServer().onLinkHoverHook( + element, + linktext, + this, + this.getHookServer() + )) { + return; + } + } catch (e) { + errorlog({where: "ExcalidrawView.showHoverPreview", fn: this.getHookServer().onLinkHoverHook, error: e}); + } + } + + if (this.semaphores.hoverSleep) { + return; + } + + const f = this.app.metadataCache.getFirstLinkpathDest( + linktext.split("#")[0], + this.file.path, + ); + if (!f) { + return; + } + + if ( + this.ownerDocument.querySelector(`div.popover-title[data-path="${f.path}"]`) + ) { + return; + } + + this.semaphores.hoverSleep = true; + window.setTimeout(() => (this.semaphores.hoverSleep = false), 500); + this.plugin.hover.linkText = linktext; + this.plugin.hover.sourcePath = this.file.path; + this.hoverPreviewTarget = this.contentEl; //e.target; + this.app.workspace.trigger("hover-link", { + event: this.lastMouseEvent, + source: VIEW_TYPE_EXCALIDRAW, + hoverParent: this, + targetEl: this.hoverPreviewTarget, //null //0.15.0 hover editor!! + linktext: this.plugin.hover.linkText, + sourcePath: this.plugin.hover.sourcePath, + }); + this.hoverPoint = this.currentPosition; + if (this.isFullscreen()) { + window.setTimeout(() => { + const popover = + this.ownerDocument.querySelector(`div.popover-title[data-path="${f.path}"]`) + ?.parentElement?.parentElement?.parentElement ?? + this.ownerDocument.body.querySelector("div.popover"); + if (popover) { + this.contentEl.append(popover); + } + }, 400); + } + }; + + private isLinkSelected():boolean { + return Boolean ( + this.getSelectedTextElement().id || + this.getSelectedImageElement().id || + this.getSelectedElementWithLink().id + ) + }; + + private excalidrawDIVonKeyDown(event: KeyboardEvent) { + //(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.excalidrawDIVonKeyDown, "ExcalidrawView.excalidrawDIVonKeyDown", event); + if (this.semaphores?.viewunload) return; + if (event.target === this.excalidrawWrapperRef.current) { + return; + } //event should originate from the canvas + if (this.isFullscreen() && event.keyCode === KEYCODE.ESC) { + this.exitFullscreen(); + } + if (isWinCTRLorMacCMD(event) && !isSHIFT(event) && !isWinALTorMacOPT(event)) { + this.showHoverPreview(); + } + }; + + private onPointerDown(e: PointerEvent) { + if (!(isWinCTRLorMacCMD(e)||isWinMETAorMacCTRL(e))) { + return; + } + if (!this.plugin.settings.allowCtrlClick && !isWinMETAorMacCTRL(e)) { + return; + } + if (Boolean((this.excalidrawAPI as ExcalidrawImperativeAPI)?.getAppState().contextMenu)) { + return; + } + //added setTimeout when I changed onClick(e: MouseEvent) to onPointerDown() in 1.7.9. + //Timeout is required for Excalidraw to first complete the selection action before execution + //of the link click continues + window.setTimeout(()=>{ + if (!this.isLinkSelected()) return; + this.handleLinkClick(e); + }); + } + + private onMouseMove(e: MouseEvent) { + //@ts-ignore + this.lastMouseEvent = e.nativeEvent; + } + + private onMouseOver() { + 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"; + pointersMap: Gesture["pointers"]; + }) { + this.currentPosition = p.pointer; + if ( + this.hoverPreviewTarget && + (Math.abs(this.hoverPoint.x - p.pointer.x) > 50 || + Math.abs(this.hoverPoint.y - p.pointer.y) > 50) + ) { + this.clearHoverPreview(); + } + if (!this.viewModeEnabled) { + return; + } + + const buttonDown = !this.blockOnMouseButtonDown && p.button === "down"; + if (buttonDown) { + this.blockOnMouseButtonDown = true; + + //ctrl click + if (isWinCTRLorMacCMD(this.modifierKeyDown) || isWinMETAorMacCTRL(this.modifierKeyDown)) { + this.identifyElementClicked(); + return; + } + + if(this.plugin.settings.doubleClickLinkOpenViewMode) { + //dobule click + const now = Date.now(); + if ((now - this.doubleClickTimestamp) < 600 && (now - this.doubleClickTimestamp) > 40) { + this.identifyElementClicked(); + } + this.doubleClickTimestamp = now; + } + return; + } + if (p.button === "up") { + this.blockOnMouseButtonDown = false; + } + if (isWinCTRLorMacCMD(this.modifierKeyDown) || + (this.excalidrawAPI.getAppState().isViewModeEnabled && + this.plugin.settings.hoverPreviewWithoutCTRL)) { + + this.showHoverPreview(); + } + } + + public updateGridColor(canvasColor?: string, st?: any) { + if(!canvasColor) { + st = (this.excalidrawAPI as ExcalidrawImperativeAPI).getAppState(); + canvasColor = canvasColor ?? st.viewBackgroundColor === "transparent" ? "white" : st.viewBackgroundColor; + } + window.setTimeout(()=>this.updateScene({appState:{gridColor: this.getGridColor(canvasColor, st)}, storeAction: "update"})); + } + + private canvasColorChangeHook(st: AppState) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.canvasColorChangeHook, "ExcalidrawView.canvasColorChangeHook", st); + const canvasColor = st.viewBackgroundColor === "transparent" ? "white" : st.viewBackgroundColor; + this.updateGridColor(canvasColor,st); + setDynamicStyle(this.plugin.ea,this,canvasColor,this.plugin.settings.dynamicStyling); + if(this.plugin.ea.onCanvasColorChangeHook) { + try { + this.plugin.ea.onCanvasColorChangeHook( + this.plugin.ea, + this, + st.viewBackgroundColor + ) + } catch (e) { + errorlog({ + where: this.canvasColorChangeHook, + source: this.plugin.ea.onCanvasColorChangeHook, + error: e, + message: "ea.onCanvasColorChangeHook exception" + }) + } + } + } + + private checkSceneVersion(et: ExcalidrawElement[]) { + const sceneVersion = this.getSceneVersion(et); + if ( + ((sceneVersion > 0 || + (sceneVersion === 0 && et.length > 0)) && //Addressing the rare case when the last element is deleted from the scene + sceneVersion !== this.previousSceneVersion) + ) { + this.previousSceneVersion = sceneVersion; + this.setDirty(6.1); + } + } + + private onChange (et: ExcalidrawElement[], st: AppState) { + if(st.newElement?.type === "freedraw") { + this.freedrawLastActiveTimestamp = Date.now(); + } + if (st.newElement || st.editingTextElement || st.editingLinearElement) { + this.plugin.wasPenModeActivePreviously = st.penMode; + } + this.viewModeEnabled = st.viewModeEnabled; + if (this.semaphores.justLoaded) { + const elcount = this.excalidrawData?.scene?.elements?.length ?? 0; + if( elcount>0 && et.length===0 ) return; + this.semaphores.justLoaded = false; + if (!this.semaphores.preventAutozoom && this.plugin.settings.zoomToFitOnOpen) { + this.zoomToFit(false,true); + } + this.previousSceneVersion = this.getSceneVersion(et); + this.previousBackgroundColor = st.viewBackgroundColor; + this.previousTheme = st.theme; + this.canvasColorChangeHook(st); + return; + } + if(st.theme !== this.previousTheme && this.file === this.excalidrawData.file) { + this.previousTheme = st.theme; + this.setDirty(5.1); + } + if(st.viewBackgroundColor !== this.previousBackgroundColor && this.file === this.excalidrawData.file) { + this.previousBackgroundColor = st.viewBackgroundColor; + this.setDirty(6); + if(this.colorChangeTimer) { + window.clearTimeout(this.colorChangeTimer); + } + this.colorChangeTimer = window.setTimeout(()=>{ + this.canvasColorChangeHook(st); + this.colorChangeTimer = null; + },50); //just enough time if the user is playing with color picker, the change is not too frequent. + } + if (this.semaphores.dirty) { + return; + } + if ( + st.editingTextElement === null && + //Removed because of + //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/565 + /*st.resizingElement === null && + st.newElement === null && + st.editingGroupId === null &&*/ + st.editingLinearElement === null + ) { + this.checkSceneVersion(et); + } + } + + private onLibraryChange(items: LibraryItems) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onLibraryChange, "ExcalidrawView.onLibraryChange", items); + (async () => { + const lib = { + type: "excalidrawlib", + version: 2, + source: GITHUB_RELEASES+PLUGIN_VERSION, + libraryItems: items, + }; + this.plugin.setStencilLibrary(lib); + await this.plugin.saveSettings(); + })(); + } + + private onPaste (data: ClipboardData, event: ClipboardEvent | null) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onPaste, "ExcalidrawView.onPaste", data, event); + const api = this.excalidrawAPI as ExcalidrawImperativeAPI; + const ea = this.getHookServer(); + if(data?.elements) { + data.elements + .filter(el=>el.type==="text" && !el.hasOwnProperty("rawText")) + .forEach(el=>(el as Mutable).rawText = (el as ExcalidrawTextElement).originalText); + }; + if(data && ea.onPasteHook) { + const res = ea.onPasteHook({ + ea, + payload: data, + event, + excalidrawFile: this.file, + view: this, + pointerPosition: this.currentPosition, + }); + if(typeof res === "boolean" && res === false) return false; + } + + // Disables Middle Mouse Button Paste Functionality on Linux + if( + !this.modifierKeyDown.ctrlKey + && typeof event !== "undefined" + && event !== null + && DEVICE.isLinux + ) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onPaste,`ExcalidrawView.onPaste, Prevented what is likely middle mouse button paste.`); + return false; + }; + + if(data && data.text && hyperlinkIsImage(data.text)) { + this.addImageWithURL(data.text); + return false; + } + if(data && data.text && !this.modifierKeyDown.shiftKey) { + const isCodeblock = Boolean(data.text.replaceAll("\r\n", "\n").replaceAll("\r", "\n").match(/^`{3}[^\n]*\n.+\n`{3}\s*$/ms)); + if(isCodeblock) { + const clipboardText = data.text; + window.setTimeout(()=>this.pasteCodeBlock(clipboardText)); + return false; + } + + if(isTextImageTransclusion(data.text,this, async (link, file)=>{ + const ea = getEA(this) as ExcalidrawAutomate; + if(IMAGE_TYPES.contains(file.extension)) { + ea.selectElementsInView([await insertImageToView (ea, this.currentPosition, file)]); + ea.destroy(); + } else if(file.extension !== "pdf") { + ea.selectElementsInView([await insertEmbeddableToView (ea, this.currentPosition, file, link)]); + ea.destroy(); + } else { + if(link.match(/^[^#]*#page=\d*(&\w*=[^&]+){0,}&rect=\d*,\d*,\d*,\d*/g)) { + const ea = getEA(this) as ExcalidrawAutomate; + const imgID = await ea.addImage(this.currentPosition.x, this.currentPosition.y,link.split("&rect=")[0]); + const el = ea.getElement(imgID) as Mutable; + const fd = ea.imagesDict[el.fileId] as FileData; + el.crop = getPDFCropRect({ + scale: this.plugin.settings.pdfScale, + link, + naturalHeight: fd.size.height, + naturalWidth: fd.size.width, + }); + if(el.crop) { + el.width = el.crop.width/this.plugin.settings.pdfScale; + el.height = el.crop.height/this.plugin.settings.pdfScale; + } + el.link = `[[${link}]]`; + ea.addElementsToView(false,false).then(()=>ea.destroy()); + } else { + const modal = new UniversalInsertFileModal(this.plugin, this); + modal.open(file, this.currentPosition); + } + } + this.setDirty(9); + })) { + return false; + } + + const quoteWithRef = obsidianPDFQuoteWithRef(data.text); + if(quoteWithRef) { + const ea = getEA(this) as ExcalidrawAutomate; + const st = api.getAppState(); + const strokeC = st.currentItemStrokeColor; + const viewC = st.viewBackgroundColor; + ea.style.strokeColor = strokeC === "transparent" + ? ea.getCM(viewC === "transparent" ? "white" : viewC) + .invert() + .stringHEX({alpha: false}) + : strokeC; + ea.style.fontFamily = st.currentItemFontFamily; + ea.style.fontSize = st.currentItemFontSize; + const textDims = ea.measureText(quoteWithRef.quote); + const textWidth = textDims.width + 2*30; //default padding + const id = ea.addText(this.currentPosition.x, this.currentPosition.y, quoteWithRef.quote, { + box: true, + boxStrokeColor: "transparent", + width: Math.min(500,textWidth), + height: textDims.height + 2*30, + }) + ea.elementsDict[id].link = `[[${quoteWithRef.link}]]`; + ea.addElementsToView(false,false).then(()=>ea.destroy()); + + return false; + } + } + if (data.elements) { + window.setTimeout(() => this.save(), 30); //removed prevent reload = false, as reload was triggered when pasted containers were processed and there was a conflict with the new elements + } + + //process pasted text after it was processed into elements by Excalidraw + //I let Excalidraw handle the paste first, e.g. to split text by lines + //Only process text if it includes links or embeds that need to be parsed + if(data && data.text && data.text.match(/(\[\[[^\]]*]])|(\[[^\]]*]\([^)]*\))/gm)) { + const prevElements = api.getSceneElements().filter(el=>el.type === "text").map(el=>el.id); + + window.setTimeout(async ()=>{ + const sceneElements = api.getSceneElementsIncludingDeleted() as Mutable[]; + const newElements = sceneElements.filter(el=>el.type === "text" && !el.isDeleted && !prevElements.includes(el.id)) as ExcalidrawTextElement[]; + + //collect would-be image elements and their corresponding files and links + const imageElementsMap = new Map(); + let element: ExcalidrawTextElement; + const callback = (link: string, file: TFile) => { + imageElementsMap.set(element, [link, file]); + } + newElements.forEach((el:ExcalidrawTextElement)=>{ + element = el; + isTextImageTransclusion(el.originalText,this,callback); + }); + + //if there are no image elements, save and return + //Save will ensure links and embeds are parsed + if(imageElementsMap.size === 0) { + this.save(false); //saving because there still may be text transclusions + return; + }; + + //if there are image elements + //first delete corresponding "old" text elements + for(const [el, [link, file]] of imageElementsMap) { + const clone = cloneElement(el); + clone.isDeleted = true; + this.excalidrawData.deleteTextElement(clone.id); + sceneElements[sceneElements.indexOf(el)] = clone; + } + this.updateScene({elements: sceneElements, storeAction: "update"}); + + //then insert images and embeds + //shift text elements down to make space for images and embeds + const ea:ExcalidrawAutomate = getEA(this); + let offset = 0; + for(const el of newElements) { + const topleft = {x: el.x, y: el.y+offset}; + if(imageElementsMap.has(el)) { + const [link, file] = imageElementsMap.get(el); + if(IMAGE_TYPES.contains(file.extension)) { + const id = await insertImageToView (ea, topleft, file, undefined, false); + offset += ea.getElement(id).height - el.height; + } else if(file.extension !== "pdf") { + //isTextImageTransclusion will not return text only markdowns, this is here + //for the future when we may want to support other embeddables + const id = await insertEmbeddableToView (ea, topleft, file, link, false); + offset += ea.getElement(id).height - el.height; + } else { + const modal = new UniversalInsertFileModal(this.plugin, this); + modal.open(file, topleft); + } + } else { + if(offset !== 0) { + ea.copyViewElementsToEAforEditing([el]); + ea.getElement(el.id).y = topleft.y; + } + } + } + await ea.addElementsToView(false,true); + ea.selectElementsInView(newElements.map(el=>el.id)); + ea.destroy(); + },200) //parse transclusion and links after paste + } + return true; + } + + private async onThemeChange (newTheme: string) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onThemeChange, "ExcalidrawView.onThemeChange", newTheme); + //debug({where:"ExcalidrawView.onThemeChange",file:this.file.name,before:"this.loadSceneFiles",newTheme}); + this.excalidrawData.scene.appState.theme = newTheme; + this.loadSceneFiles(true); + this.toolsPanelRef?.current?.setTheme(newTheme); + //Timeout is to allow appState to update + window.setTimeout(()=>setDynamicStyle(this.plugin.ea,this,this.previousBackgroundColor,this.plugin.settings.dynamicStyling)); + } + + private onDrop (event: React.DragEvent): 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 + 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;iea.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 { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onBeforeTextEdit, "ExcalidrawView.onBeforeTextEdit", textElement); + /*const api = this.excalidrawAPI as ExcalidrawImperativeAPI; + const st = api.getAppState(); + setDynamicStyle( + this.plugin.ea, + this, + st.viewBackgroundColor === "transparent" ? "white" : st.viewBackgroundColor, + this.plugin.settings.dynamicStyling, + api.getColorAtScenePoint({sceneX: this.currentPosition.x, sceneY: this.currentPosition.y}) + );*/ + if(!isExistingElement) { + return; + } + window.clearTimeout(this.isEditingTextResetTimer); + this.isEditingTextResetTimer = null; + this.semaphores.isEditingText = true; //to prevent autoresize on mobile when keyboard pops up + if(this.compatibilityMode) { + return textElement.originalText ?? textElement.text; + } + const raw = this.excalidrawData.getRawText(textElement.id); + if (!raw) { + return textElement.rawText; + } + return raw; + } + + + private onBeforeTextSubmit ( + textElement: ExcalidrawTextElement, + nextText: string, + nextOriginalText: string, + isDeleted: boolean, + ): {updatedNextOriginalText: string, nextLink: string} { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onBeforeTextSubmit, "ExcalidrawView.onBeforeTextSubmit", textElement, nextText, nextOriginalText, isDeleted); + const api = this.excalidrawAPI; + if (!api) { + return {updatedNextOriginalText: null, nextLink: textElement?.link ?? null}; + } + + // 1. Set the isEditingText flag to true to prevent autoresize on mobile + // 1500ms is an empirical number, the on-screen keyboard usually disappears in 1-2 seconds + this.semaphores.isEditingText = true; + if(this.isEditingTextResetTimer) { + window.clearTimeout(this.isEditingTextResetTimer); + } + this.isEditingTextResetTimer = window.setTimeout(() => { + if(typeof this.semaphores?.isEditingText !== "undefined") { + this.semaphores.isEditingText = false; + } + this.isEditingTextResetTimer = null; + }, 1500); + + // 2. If the text element is deleted, remove it from ExcalidrawData + // parsed textElements cache + if (isDeleted) { + this.excalidrawData.deleteTextElement(textElement.id); + this.setDirty(7); + return {updatedNextOriginalText: null, nextLink: null}; + } + + // 3. Check if the user accidently pasted Excalidraw data from the clipboard + // as text. If so, update the parsed link in ExcalidrawData + // textElements cache and update the text element in the scene with a warning. + const FORBIDDEN_TEXT = `{"type":"excalidraw/clipboard","elements":[{"`; + const WARNING = t("WARNING_PASTING_ELEMENT_AS_TEXT"); + if(nextOriginalText.startsWith(FORBIDDEN_TEXT)) { + window.setTimeout(()=>{ + const elements = this.excalidrawAPI.getSceneElements(); + const el = elements.filter((el:ExcalidrawElement)=>el.id === textElement.id); + if(el.length === 1) { + const clone = cloneElement(el[0]); + clone.rawText = WARNING; + elements[elements.indexOf(el[0])] = clone; + this.excalidrawData.setTextElement(clone.id,WARNING,()=>{}); + this.updateScene({elements, storeAction: "update"}); + api.history.clear(); + } + }); + return {updatedNextOriginalText:WARNING, nextLink:null}; + } + + const containerId = textElement.containerId; + + // 4. Check if the text matches the transclusion pattern and if so, + // check if the link in the transclusion can be resolved to a file in the vault. + // If the link is an image or a PDF file, replace the text element with the image or the PDF. + // If the link is an embedded markdown file, then display a message, but otherwise transclude the text step 5. + // 1 2 + if(isTextImageTransclusion(nextOriginalText, this, (link, file)=>{ + window.setTimeout(async ()=>{ + const elements = this.excalidrawAPI.getSceneElements(); + const el = elements.filter((el:ExcalidrawElement)=>el.id === textElement.id) as ExcalidrawTextElement[]; + if(el.length === 1) { + const center = {x: el[0].x, y: el[0].y }; + const clone = cloneElement(el[0]); + clone.isDeleted = true; + this.excalidrawData.deleteTextElement(clone.id); + elements[elements.indexOf(el[0])] = clone; + this.updateScene({elements, storeAction: "update"}); + const ea:ExcalidrawAutomate = getEA(this); + if(IMAGE_TYPES.contains(file.extension)) { + ea.selectElementsInView([await insertImageToView (ea, center, file)]); + ea.destroy(); + } else if(file.extension !== "pdf") { + ea.selectElementsInView([await insertEmbeddableToView (ea, center, file, link)]); + ea.destroy(); + } else { + const modal = new UniversalInsertFileModal(this.plugin, this); + modal.open(file, center); + } + this.setDirty(9); + } + }); + })) { + return {updatedNextOriginalText: null, nextLink: textElement.link}; + } + + // 5. Check if the user made changes to the text, or + // the text is missing from ExcalidrawData textElements cache (recently copy/pasted) + if ( + nextOriginalText !== textElement.originalText || + !this.excalidrawData.getRawText(textElement.id) + ) { + //the user made changes to the text or the text is missing from Excalidraw Data (recently copy/pasted) + //setTextElement will attempt a quick parse (without processing transclusions) + this.setDirty(8); + + // setTextElement will invoke this callback function in case quick parse was not possible, the parsed text contains transclusions + // in this case I need to update the scene asynchronously when parsing is complete + const callback = async (parsedText:string) => { + //this callback function will only be invoked if quick parse fails, i.e. there is a transclusion in the raw text + if(this.textMode === TextMode.raw) return; + + const elements = this.excalidrawAPI.getSceneElements(); + const elementsMap = arrayToMap(elements); + const el = elements.filter((el:ExcalidrawElement)=>el.id === textElement.id); + if(el.length === 1) { + const container = getContainerElement(el[0],elementsMap); + const clone = cloneElement(el[0]); + if(!el[0]?.isDeleted) { + const {text, x, y, width, height} = refreshTextDimensions(el[0], container, elementsMap, parsedText); + + clone.x = x; + clone.y = y; + clone.width = width; + clone.height = height; + clone.originalText = parsedText; + clone.text = text; + } + + elements[elements.indexOf(el[0])] = clone; + this.updateScene({elements, storeAction: "update"}); + if(clone.containerId) this.updateContainerSize(clone.containerId); + this.setDirty(8.1); + } + api.history.clear(); + }; + + const [parseResultOriginal, link] = + this.excalidrawData.setTextElement( + textElement.id, + nextOriginalText, + callback, + ); + + // if quick parse was successful, + // - check if textElement is in a container and update the container size, + // because the parsed text will have a different size than the raw text had + // - depending on the textMode, return the text with markdown markup or the parsed text + // if quick parse was not successful return [null, null, null] to indicate that the no changes were made to the text element + if (parseResultOriginal) { + //there were no transclusions in the raw text, quick parse was successful + if (containerId) { + this.updateContainerSize(containerId, true); + } + if (this.textMode === TextMode.raw) { + return {updatedNextOriginalText: nextOriginalText, nextLink: link}; + } //text is displayed in raw, no need to clear the history, undo will not create problems + if (nextOriginalText === parseResultOriginal) { + if (link) { + //don't forget the case: link-prefix:"" && link-brackets:true + return {updatedNextOriginalText: parseResultOriginal, nextLink: link}; + } + return {updatedNextOriginalText: null, nextLink: textElement.link}; + } //There were no links to parse, raw text and parsed text are equivalent + api.history.clear(); + return {updatedNextOriginalText: parseResultOriginal, nextLink:link}; + } + return {updatedNextOriginalText: null, nextLink: textElement.link}; + } + // even if the text did not change, container sizes might need to be updated + if (containerId) { + this.updateContainerSize(containerId, true); + } + if (this.textMode === TextMode.parsed) { + const parseResultOriginal = this.excalidrawData.getParsedText(textElement.id); + return {updatedNextOriginalText: parseResultOriginal, nextLink: textElement.link}; + } + return {updatedNextOriginalText: null, nextLink: textElement.link}; + } + + private async onLinkOpen(element: ExcalidrawElement, e: any): Promise { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onLinkOpen, "ExcalidrawView.onLinkOpen", element, e); + e.preventDefault(); + if (!element) { + return; + } + let link = element.link; + if (!link || link === "") { + return; + } + window.setTimeout(()=>this.removeLinkTooltip(),500); + + let event = e?.detail?.nativeEvent; + if(this.handleLinkHookCall(element,element.link,event)) return; + //if(openExternalLink(element.link, this.app, !isSHIFT(event) && !isWinCTRLorMacCMD(event) && !isWinMETAorMacCTRL(event) && !isWinALTorMacOPT(event) ? element : undefined)) return; + if(openExternalLink(element.link, this.app)) return; + + //if element is type text and element has multiple links, then submit the element text to linkClick to trigger link suggester + if(element.type === "text") { + const linkText = element.rawText.replaceAll("\n", ""); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187 + const partsArray = REGEX_LINK.getResList(linkText); + if(partsArray.filter(p=>Boolean(p.value)).length > 1) { + link = linkText; + } + } + + if (!event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) { + event = emulateKeysForLinkClick("new-tab"); + } + + this.linkClick( + event, + null, + null, + {id: element.id, text: link}, + event, + true, + ); + return; + } + + private onLinkHover(element: NonDeletedExcalidrawElement, event: React.PointerEvent): void { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onLinkHover, "ExcalidrawView.onLinkHover", element, event); + if ( + element && + (this.plugin.settings.hoverPreviewWithoutCTRL || + isWinCTRLorMacCMD(event)) + ) { + this.lastMouseEvent = event; + this.lastMouseEvent.ctrlKey = !(DEVICE.isIOS || DEVICE.isMacOS) || this.lastMouseEvent.ctrlKey; + this.lastMouseEvent.metaKey = (DEVICE.isIOS || DEVICE.isMacOS) || this.lastMouseEvent.metaKey; + const link = element.link; + if (!link || link === "") { + return; + } + if (link.startsWith("[[")) { + const linkMatch = link.match(/\[\[(?.*?)\]\]/); + if (!linkMatch) { + return; + } + let linkText = linkMatch.groups.link; + this.showHoverPreview(linkText, element); + } + } + } + + private onViewModeChange(isViewModeEnabled: boolean) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onViewModeChange, "ExcalidrawView.onViewModeChange", isViewModeEnabled); + if(!this.semaphores.viewunload) { + this.toolsPanelRef?.current?.setExcalidrawViewMode( + isViewModeEnabled, + ); + } + if(this.getHookServer().onViewModeChangeHook) { + try { + this.getHookServer().onViewModeChangeHook(isViewModeEnabled,this,this.getHookServer()); + } catch(e) { + errorlog({where: "ExcalidrawView.onViewModeChange", fn: this.getHookServer().onViewModeChangeHook, error: e}); + } + + } + } + + private async getBackOfTheNoteSections() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getBackOfTheNoteSections, "ExcalidrawView.getBackOfTheNoteSections"); + return (await this.app.metadataCache.blockCache.getForFile({ isCancelled: () => false },this.file)) + .blocks.filter((b: any) => b.display && b.node?.type === "heading") + .filter((b: any) => !MD_EX_SECTIONS.includes(b.display)) + .map((b: any) => cleanSectionHeading(b.display)); + } + + private async getBackOfTheNoteBlocks() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getBackOfTheNoteBlocks, "ExcalidrawView.getBackOfTheNoteBlocks"); + return (await this.app.metadataCache.blockCache.getForFile({ isCancelled: () => false },this.file)) + .blocks.filter((b:any) => b.display && b.node && b.node.hasOwnProperty("type") && b.node.hasOwnProperty("id")) + .map((b:any) => cleanBlockRef(b.node.id)); + } + + public getSingleSelectedImage(): {imageEl: ExcalidrawImageElement, embeddedFile: EmbeddedFile} { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getSingleSelectedImage, "ExcalidrawView.getSingleSelectedImage"); + if(!this.excalidrawAPI) return null; + const els = this.getViewSelectedElements().filter(el=>el.type==="image"); + if(els.length !== 1) { + return null; + } + const el = els[0] as ExcalidrawImageElement; + const imageFile = this.excalidrawData.getFile(el.fileId); + return {imageEl: el, embeddedFile: imageFile}; + } + + public async insertBackOfTheNoteCard() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.insertBackOfTheNoteCard, "ExcalidrawView.insertBackOfTheNoteCard"); + const sections = await this.getBackOfTheNoteSections(); + const selectCardDialog = new SelectCard(this.app,this,sections); + selectCardDialog.start(); + } + + public async moveBackOfTheNoteCardToFile(id?: string) { + id = id ?? this.getViewSelectedElements().filter(el=>el.type==="embeddable")[0]?.id; + const embeddableData = this.getEmbeddableLeafElementById(id); + const child = embeddableData?.node?.child; + if(!child || (child.file !== this.file)) return; + + if(child.lastSavedData !== this.data) { + await this.forceSave(true); + if(child.lastSavedData !== this.data) { + new Notice(t("ERROR_TRY_AGAIN")); + return; + } + } + const {folder, filepath:_} = await getAttachmentsFolderAndFilePath( + this.app, + this.file.path, + "dummy", + ); + const filepath = getNewUniqueFilepath( + this.app.vault, + child.subpath.replaceAll("#",""), + folder, + ); + let path = await ScriptEngine.inputPrompt( + this, + this.plugin, + this.app, + "Set filename", + "Enter filename", + filepath, + undefined, + 3, + ); + if(!path) return; + if(!path.endsWith(".md")) { + path += ".md"; + } + const {folderpath, filename} = splitFolderAndFilename(path); + path = getNewUniqueFilepath(this.app.vault, filename, folderpath); + try { + const newFile = await this.app.vault.create(path, child.text); + if(!newFile) { + new Notice("Unexpected error"); + return; + } + const ea = getEA(this) as ExcalidrawAutomate; + ea.copyViewElementsToEAforEditing([this.getViewElements().find(el=>el.id === id)]); + ea.getElement(id).link = `[[${newFile.path}]]`; + this.data = this.data.split(child.heading+child.text).join(""); + await ea.addElementsToView(false); + ea.destroy(); + await this.forceSave(true); + } catch(e) { + new Notice(`Unexpected error: ${e.message}`); + return; + } + } + + public async pasteCodeBlock(data: string) { + try { + data = data.replaceAll("\r\n", "\n").replaceAll("\r", "\n").trim(); + const isCodeblock = Boolean(data.match(/^`{3}[^\n]*\n.+\n`{3}\s*$/ms)); + if(!isCodeblock) { + const codeblockType = await GenericInputPrompt.Prompt(this,this.plugin,this.app,"type codeblock type","javascript, html, python, etc.",""); + data = "```"+codeblockType.trim()+"\n"+data+"\n```"; + } + let title = (await GenericInputPrompt.Prompt(this,this.plugin,this.app,"Code Block Title","Enter title or leave empty for automatic title","")).trim(); + if (title === "") {title = "Code Block";}; + const sections = await this.getBackOfTheNoteSections(); + if (sections.includes(title)) { + let i=0; + while (sections.includes(`${title} ${++i}`)) {}; + title = `${title} ${i}`; + } + addBackOfTheNoteCard(this, title, false, data); + } catch (e) { + } + } + + public async convertImageElWithURLToLocalFile(data: {imageEl: ExcalidrawImageElement, embeddedFile: EmbeddedFile}) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.convertImageElWithURLToLocalFile, "ExcalidrawView.convertImageElWithURLToLocalFile", data); + const {imageEl, embeddedFile} = data; + const imageDataURL = embeddedFile.getImage(false); + if(!imageDataURL && !imageDataURL.startsWith("data:")) { + new Notice("Image not found"); + return false; + } + const ea = getEA(this) as ExcalidrawAutomate; + ea.copyViewElementsToEAforEditing([imageEl]); + const eaEl = ea.getElement(imageEl.id) as Mutable; + eaEl.fileId = fileid() as FileId; + if(!eaEl.link) {eaEl.link = embeddedFile.hyperlink}; + let dataURL = embeddedFile.getImage(false); + if(!dataURL.startsWith("data:")) { + new Notice("Attempting to download image from URL. This may take a long while. The operation will time out after max 1 minute"); + dataURL = await getDataURLFromURL(dataURL, embeddedFile.mimeType, 30000); + if(!dataURL.startsWith("data:")) { + new Notice("Failed. Could not download image!"); + return false; + } + } + const files: BinaryFileData[] = []; + files.push({ + mimeType: embeddedFile.mimeType, + id: eaEl.fileId, + dataURL: dataURL as DataURL, + created: embeddedFile.mtime, + }); + const api = this.excalidrawAPI as ExcalidrawImperativeAPI; + api.addFiles(files); + await ea.addElementsToView(false,true); + ea.destroy(); + new Notice("Image successfully converted to local file"); + } + + private insertLinkAction(linkVal: string) { + let link = linkVal.match(/\[\[(.*?)\]\]/)?.[1]; + if(!link) { + link = linkVal.replaceAll("[","").replaceAll("]",""); + link = link.split("|")[0].trim(); + } + this.plugin.insertLinkDialog.start(this.file.path, (markdownlink: string, path:string, alias:string) => this.addLink(markdownlink, path, alias, linkVal), link); + } + + private onContextMenu(elements: readonly ExcalidrawElement[], appState: AppState, onClose: (callback?: () => void) => void) { + const React = this.packages.react; + const contextMenuActions = []; + const api = this.excalidrawAPI as ExcalidrawImperativeAPI; + const selectedElementIds = Object.keys(api.getAppState().selectedElementIds); + const areElementsSelected = selectedElementIds.length > 0; + + if(this.isLinkSelected()) { + contextMenuActions.push([ + renderContextMenuAction( + React, + t("OPEN_LINK_CLICK"), + () => { + const event = emulateKeysForLinkClick("new-tab"); + this.handleLinkClick(event, true); + }, + onClose + ), + ]); + } + + if(appState.viewModeEnabled) { + const isLaserOn = appState.activeTool?.type === "laser"; + contextMenuActions.push([ + renderContextMenuAction( + React, + isLaserOn ? t("LASER_OFF") : t("LASER_ON"), + () => { + api.setActiveTool({type: isLaserOn ? "selection" : "laser"}); + }, + onClose + ), + ]); + } + + if(!appState.viewModeEnabled) { + const selectedTextElements = this.getViewSelectedElements().filter(el=>el.type === "text"); + if(selectedTextElements.length===1) { + const selectedTextElement = selectedTextElements[0] as ExcalidrawTextElement; + const containerElement = (this.getViewElements() as ExcalidrawElement[]).find(el=>el.id === selectedTextElement.containerId); + + //if the text element in the container no longer has a link associated with it... + if( + containerElement && + selectedTextElement.link && + this.excalidrawData.getParsedText(selectedTextElement.id) === selectedTextElement.rawText + ) { + contextMenuActions.push([ + renderContextMenuAction( + React, + t("REMOVE_LINK"), + async () => { + const ea = getEA(this) as ExcalidrawAutomate; + ea.copyViewElementsToEAforEditing([selectedTextElement]); + const el = ea.getElement(selectedTextElement.id) as Mutable; + el.link = null; + await ea.addElementsToView(false); + ea.destroy(); + }, + onClose + ), + ]); + } + + if(containerElement) { + contextMenuActions.push([ + renderContextMenuAction( + React, + t("SELECT_TEXTELEMENT_ONLY"), + () => { + window.setTimeout(()=> + (this.excalidrawAPI as ExcalidrawImperativeAPI).selectElements([selectedTextElement]) + ); + }, + onClose + ), + ]); + } + + if(!containerElement || (containerElement && containerElement.type !== "arrow")) { + contextMenuActions.push([ + renderContextMenuAction( + React, + t("CONVERT_TO_MARKDOWN"), + () => { + this.convertTextElementToMarkdown(selectedTextElement, containerElement); + }, + onClose + ), + ]); + } + } + + const img = this.getSingleSelectedImage(); + if(img && img.embeddedFile?.isHyperLink) { + contextMenuActions.push([ + renderContextMenuAction( + React, + t("CONVERT_URL_TO_FILE"), + () => { + window.setTimeout(()=>this.convertImageElWithURLToLocalFile(img)); + }, + onClose + ), + ]); + } + + if( + img && img.embeddedFile && img.embeddedFile.mimeType === "image/svg+xml" && + (!img.embeddedFile.file || (img.embeddedFile.file && !this.plugin.isExcalidrawFile(img.embeddedFile.file))) + ) { + contextMenuActions.push([ + renderContextMenuAction( + React, + t("IMPORT_SVG_CONTEXTMENU"), + async () => { + const base64Content = img.embeddedFile.getImage(false).split(',')[1]; + // Decoding the base64 content + const svg = atob(base64Content); + if(!svg || svg === "") return; + const ea = getEA(this) as ExcalidrawAutomate; + ea.importSVG(svg); + ea.addToGroup(ea.getElements().map(el=>el.id)); + await ea.addElementsToView(true, true, true,true); + ea.destroy(); + }, + onClose + ), + ]); + } + + if(areElementsSelected) { + contextMenuActions.push([ + renderContextMenuAction( + React, + t("COPY_ELEMENT_LINK"), + () => { + this.copyLinkToSelectedElementToClipboard(""); + }, + onClose + ), + ]); + } else { + contextMenuActions.push([ + renderContextMenuAction( + React, + t("COPY_DRAWING_LINK"), + () => { + const path = this.file.path.match(/(.*)(\.md)$/)?.[1]; + navigator.clipboard.writeText(`![[${path ?? this.file.path}]]`); + }, + onClose + ), + ]); + } + + if(this.getViewSelectedElements().filter(el=>el.type==="embeddable").length === 1) { + const embeddableData = this.getEmbeddableLeafElementById( + this.getViewSelectedElements().filter(el=>el.type==="embeddable")[0].id + ); + const child = embeddableData?.node?.child; + if(child && (child.file === this.file)) { + contextMenuActions.push([ + renderContextMenuAction( + React, + t("CONVERT_CARD_TO_FILE"), + () => { + this.moveBackOfTheNoteCardToFile(); + }, + onClose + ), + ]); + } + } + + contextMenuActions.push([ + renderContextMenuAction( + React, + t("INSERT_CARD"), + () => { + this.insertBackOfTheNoteCard(); + }, + onClose + ), + ]); + contextMenuActions.push([ + renderContextMenuAction( + React, + t("UNIVERSAL_ADD_FILE"), + () => { + const insertFileModal = new UniversalInsertFileModal(this.plugin, this); + insertFileModal.open(); + }, + onClose + ), + ]); + contextMenuActions.push([ + renderContextMenuAction( + React, + t("INSERT_LINK"), + () => { + this.plugin.insertLinkDialog.start(this.file.path, (markdownlink: string, path:string, alias:string) => this.addLink(markdownlink, path, alias)); + }, + onClose + ), + // Add more context menu actions here if needed + ]); + contextMenuActions.push([ + renderContextMenuAction( + React, + t("PASTE_CODEBLOCK"), + async () => { + const data = await navigator.clipboard?.readText(); + if(!data || data.trim() === "") return; + this.pasteCodeBlock(data); + }, + onClose + ), + ]) + } + + if(contextMenuActions.length === 0) return; + return React.createElement ( + "div", + {}, + ...contextMenuActions, + React.createElement( + "hr", + { + key: nanoid(), + className: "context-menu-item-separator", + }, + ) + ); + } + + private actionOpenScriptInstallPrompt() { + new ScriptInstallPrompt(this.plugin).open(); + } + + private actionOpenExportImageDialog() { + if(!this.exportDialog) { + this.exportDialog = new ExportDialog(this.plugin, this,this.file); + this.exportDialog.createForm(); + } + this.exportDialog.open(); + } + + private setExcalidrawAPI (api: ExcalidrawImperativeAPI) { + this.excalidrawAPI = api; + //api.setLocalFont(this.plugin.settings.experimentalEnableFourthFont); + window.setTimeout(() => { + this.onAfterLoadScene(true); + this.excalidrawContainer?.focus(); + }); + }; + + private ttdDialog() { + return this.packages.react.createElement( + this.packages.excalidrawLib.TTDDialog, + { + onTextSubmit: async (input:string) => { + try { + const response = await postOpenAI({ + systemPrompt: "The user will provide you with a text prompt. Your task is to generate a mermaid diagram based on the prompt. Use the graph, sequenceDiagram, flowchart or classDiagram types based on what best fits the request. Return a single message containing only the mermaid diagram in a codeblock. Avoid the use of `()` parenthesis in the mermaid script.", + text: input, + instruction: "Return a single message containing only the mermaid diagram in a codeblock.", + }) + + if(!response) { + return { + error: new Error("Request failed"), + }; + } + + const json = response.json; + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.ttdDialog, `ExcalidrawView.ttdDialog > onTextSubmit, openAI response`, response); + + if (json?.error) { + log(response); + return { + error: new Error(json.error.message), + }; + } + + if(!json?.choices?.[0]?.message?.content) { + log(response); + return { + error: new Error("Generation failed... see console log for details"), + }; + } + + let generatedResponse = extractCodeBlocks(json.choices[0]?.message?.content)[0]?.data; + + if(!generatedResponse) { + log(response); + return { + error: new Error("Generation failed... see console log for details"), + }; + } + + if(generatedResponse.startsWith("mermaid")) { + generatedResponse = generatedResponse.replace(/^mermaid/,"").trim(); + } + + return { generatedResponse, rateLimit:100, rateLimitRemaining:100 }; + } catch (err: any) { + throw new Error("Request failed"); + } + }, + } + ); + }; + + private diagramToCode() { + return this.packages.react.createElement( + this.packages.excalidrawLib.DiagramToCodePlugin, + { + generate: async ({ frame, children }: + {frame: ExcalidrawMagicFrameElement, children: readonly ExcalidrawElement[]}) => { + const appState = this.excalidrawAPI.getAppState(); + try { + const blob = await this.packages.excalidrawLib.exportToBlob({ + elements: children, + appState: { + ...appState, + exportBackground: true, + viewBackgroundColor: appState.viewBackgroundColor, + }, + exportingFrame: frame, + files: this.excalidrawAPI.getFiles(), + mimeType: "image/jpeg", + }); + + const dataURL = await this.packages.excalidrawLib.getDataURL(blob); + const textFromFrameChildren = this.packages.excalidrawLib.getTextFromElements(children); + + const response = await diagramToHTML ({ + image:dataURL, + apiKey: this.plugin.settings.openAIAPIToken, + text: textFromFrameChildren, + theme: appState.theme, + }); + + if (!response.ok) { + const json = await response.json(); + const text = json.error?.message || "Unknown error during generation"; + return { + html: errorHTML(text), + }; + } + + const json = await response.json(); + if(json.choices[0].message.content == null) { + return { + html: errorHTML("Nothing generated"), + }; + } + + const message = json.choices[0].message.content; + + const html = message.slice( + message.indexOf(""), + message.indexOf("") + "".length, + ); + + return { html }; + } catch (err: any) { + return { + html: errorHTML("Request failed"), + }; + } + }, + } + ); + } + + + private ttdDialogTrigger() { + return this.packages.react.createElement( + this.packages.excalidrawLib.TTDDialogTrigger, + {}, + ); + } + + private renderWelcomeScreen() { + if (!this.plugin.settings.showSplashscreen) return null; + const React = this.packages.react; + const { WelcomeScreen } = this.packages.excalidrawLib; + const filecount = this.app.vault.getFiles().filter(f => this.plugin.isExcalidrawFile(f)).length; + const rank = filecount < 200 ? "Bronze" : filecount < 750 ? "Silver" : filecount < 2000 ? "Gold" : "Platinum"; + const nextRankDelta = filecount < 200 ? 200 - filecount : filecount < 750 ? 750 - filecount : filecount < 2000 ? 2000 - filecount : 0; + const { decoration, title } = SwordColors[rank as Rank]; + return React.createElement( + WelcomeScreen, + {}, + React.createElement( + WelcomeScreen.Center, + {}, + React.createElement( + WelcomeScreen.Center.Logo, + {}, + React.createElement( + LogoWrapper, + {}, + excalidrawSword(rank as Rank), + ), + ), + React.createElement( + WelcomeScreen.Center.Heading, + { + color: decoration, + message: nextRankDelta > 0 + ? `${rank}: ${nextRankDelta} ${t("WELCOME_RANK_NEXT")}` + : `${rank}: ${t("WELCOME_RANK_LEGENDARY")}`, + }, + title, + ), + React.createElement( + WelcomeScreen.Center.Heading, + {}, + t("WELCOME_COMMAND_PALETTE"), + React.createElement("br"), + t("WELCOME_OBSIDIAN_MENU"), + React.createElement("br"), + t("WELCOME_SCRIPT_LIBRARY"), + React.createElement("br"), + t("WELCOME_HELP_MENU"), + ), + React.createElement( + WelcomeScreen.Center.Menu, + {}, + React.createElement( + WelcomeScreen.Center.MenuItemLink, + { + icon: ICONS.YouTube, + href: "https://www.youtube.com/@VisualPKM", + shortcut: null, + "aria-label": t("WELCOME_YOUTUBE_ARIA"), + }, + t("WELCOME_YOUTUBE_LINK") + ), + React.createElement( + WelcomeScreen.Center.MenuItemLink, + { + icon: ICONS.Discord, + href: "https://discord.gg/DyfAXFwUHc", + shortcut: null, + "aria-label": t("WELCOME_DISCORD_ARIA"), + }, + t("WELCOME_DISCORD_LINK") + ), + React.createElement( + WelcomeScreen.Center.MenuItemLink, + { + icon: ICONS.twitter, + href: "https://twitter.com/zsviczian", + shortcut: null, + "aria-label": t("WELCOME_TWITTER_ARIA"), + }, + t("WELCOME_TWITTER_LINK") + ), + React.createElement( + WelcomeScreen.Center.MenuItemLink, + { + icon: ICONS.Learn, + href: "https://visual-thinking-workshop.com", + shortcut: null, + "aria-label": t("WELCOME_LEARN_ARIA"), + }, + t("WELCOME_LEARN_LINK") + ), + React.createElement( + WelcomeScreen.Center.MenuItemLink, + { + icon: ICONS.heart, + href: "https://ko-fi.com/zsolt", + shortcut: null, + "aria-label": t("WELCOME_DONATE_ARIA"), + }, + t("WELCOME_DONATE_LINK") + ), + ) + ) + ); + } + + private renderCustomActionsMenu () { + const React = this.packages.react; + const {MainMenu} = this.packages.excalidrawLib; + + return React.createElement( + MainMenu, + {}, + React.createElement(MainMenu.DefaultItems.ChangeCanvasBackground), + React.createElement(MainMenu.DefaultItems.ToggleTheme), + React.createElement(MainMenu.Separator), + !DEVICE.isPhone ? React.createElement( + MainMenu.Item, + { + icon: ICONS.trayMode, + "aria-label": t("ARIA_LABEL_TRAY_MODE"), + onSelect: ()=> this.toggleTrayMode(), + }, + "Toggle tray-mode" + ) : null, + React.createElement( + MainMenu.Item, + { + icon: saveIcon(false), + "aria-label": t("FORCE_SAVE"), + onSelect: ()=> this.forceSave(), + }, + "Save" + ), + React.createElement( + MainMenu.Item, + { + icon: ICONS.scriptEngine, + "aria-label": "Explore the Excalidraw Script Library", + onSelect: ()=> this.actionOpenScriptInstallPrompt(), + }, + "Script Library" + ), + React.createElement( + MainMenu.Item, + { + icon: ICONS.ExportImage, + "aria-label": "Export image as PNG, SVG, or Excalidraw file", + onSelect: ()=> this.actionOpenExportImageDialog(), + }, + "Export Image..." + ), + React.createElement( + MainMenu.Item, + { + icon: ICONS.switchToMarkdown, + "aria-label": "Switch to markdown view", + onSelect: ()=> this.openAsMarkdown(), + }, + "Open as Markdown" + ), + React.createElement(MainMenu.Separator), + React.createElement(MainMenu.DefaultItems.Help), + React.createElement(MainMenu.DefaultItems.ClearCanvas), + ); + } + + private renderEmbeddable (element: NonDeletedExcalidrawElement, appState: UIAppState) { + const React = this.packages.react; + try { + const useExcalidrawFrame = useDefaultExcalidrawFrame(element); + + if(!this.file || !element || !element.link || element.link.length === 0 || useExcalidrawFrame) { + return null; + } + + if(element.link.match(REG_LINKINDEX_HYPERLINK) || element.link.startsWith("data:")) { + if(!useExcalidrawFrame) { + return renderWebView(element.link, this, element.id, appState); + } else { + return null; + } + } + + const res = REGEX_LINK.getRes(element.link).next(); + if(!res || (!res.value && res.done)) { + return null; + } + + let linkText = REGEX_LINK.getLink(res); + + if(linkText.match(REG_LINKINDEX_HYPERLINK)) { + if(!useExcalidrawFrame) { + return renderWebView(linkText, this, element.id, appState); + } else { + return null; + } + } + + return React.createElement(CustomEmbeddable, {element,view:this, appState, linkText}); + } catch(e) { + return null; + } + } + + private renderEmbeddableMenu(appState: AppState) { + return this.embeddableMenu?.renderButtons(appState); + } + + private renderToolsPanel(observer: any) { + const React = this.packages.react; + + return React.createElement( + ToolsPanel, + { + ref: this.toolsPanelRef, + visible: false, + view: new WeakRef(this), + centerPointer: ()=>this.setCurrentPositionToCenter(), + observer: new WeakRef(observer.current), + } + ); + } + + private renderTopRightUI (isMobile: boolean, appState: AppState) { + return this.obsidianMenu?.renderButton (isMobile, appState); + } + + private onExcalidrawResize () { + try { + const api = this.excalidrawAPI as ExcalidrawImperativeAPI; + if(!api) return; + const width = this.contentEl.clientWidth; + const height = this.contentEl.clientHeight; + if(width === 0 || height === 0) return; + + //this is an aweful hack to prevent the on-screen keyboard pushing the canvas out of view. + //The issue is that contrary to Excalidraw.com where the page is simply pushed up, in + //Obsidian the leaf has a fixed top. As a consequence the top of excalidrawWrapperDiv does not get pushed out of view + //but shirnks. But the text area is positioned relative to excalidrawWrapperDiv and consequently does not fit, which + //the distorts the whole layout. + //I hope to grow up one day and clean up this mess of a workaround, that resets the top of excalidrawWrapperDiv + //to a negative value, and manually scrolls back elements that were scrolled off screen + //I tried updating setDimensions with the value for top... but setting top and height using setDimensions did not do the trick + //I found that adding and removing this style solves the issue. + //...again, just aweful, but works. + const st = api.getAppState(); + //isEventOnSameElement attempts to solve https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1729 + //the issue is that when the user hides the keyboard with the keyboard hide button and not tapping on the screen, then editingTextElement is not null + const isEventOnSameElement = this.editingTextElementId === st.editingTextElement?.id; + const isKeyboardOutEvent:Boolean = st.editingTextElement && !isEventOnSameElement; + const isKeyboardBackEvent:Boolean = (this.semaphores.isEditingText || isEventOnSameElement) && !isKeyboardOutEvent; + this.editingTextElementId = isKeyboardOutEvent ? st.editingTextElement.id : null; + if(isKeyboardOutEvent) { + const appToolHeight = (this.contentEl.querySelector(".Island.App-toolbar") as HTMLElement)?.clientHeight ?? 0; + const editingElViewY = sceneCoordsToViewportCoords({sceneX:0, sceneY:st.editingTextElement.y}, st).y; + const scrollViewY = sceneCoordsToViewportCoords({sceneX:0, sceneY:-st.scrollY}, st).y; + const delta = editingElViewY - scrollViewY; + const isElementAboveKeyboard = height > (delta + appToolHeight*2) + const excalidrawWrapper = this.excalidrawWrapperRef.current; + //console.log({isElementAboveKeyboard}); + if(excalidrawWrapper && !isElementAboveKeyboard) { + excalidrawWrapper.style.top = `${-(st.height - height)}px`; + excalidrawWrapper.style.height = `${st.height}px`; + this.excalidrawContainer?.querySelector(".App-bottom-bar")?.scrollIntoView(); + this.headerEl?.scrollIntoView(); + } + } + if(isKeyboardBackEvent) { + const excalidrawWrapper = this.excalidrawWrapperRef.current; + const appButtonBar = this.excalidrawContainer?.querySelector(".App-bottom-bar"); + const headerEl = this.headerEl; + if(excalidrawWrapper) { + excalidrawWrapper.style.top = ""; + excalidrawWrapper.style.height = ""; + appButtonBar?.scrollIntoView(); + headerEl?.scrollIntoView(); + } + } + //end of aweful hack + + if (this.toolsPanelRef && this.toolsPanelRef.current) { + this.toolsPanelRef.current.updatePosition(); + } + if(this.ownerDocument !== document) { + this.refreshCanvasOffset(); //because resizeobserver in Excalidraw does not seem to work when in Obsidian Window + } + } catch (err) { + errorlog({ + where: "Excalidraw React-Wrapper, onResize", + error: err, + }); + } + }; + + private excalidrawRootElement( + initdata: { + elements: any, + appState: any, + files: any, + libraryItems: any + }, + ) { + const React = this.packages.react; + const {Excalidraw} = this.packages.excalidrawLib; + + const excalidrawWrapperRef = React.useRef(null); + const toolsPanelRef = React.useRef(null); + const embeddableMenuRef = React.useRef(null); + this.toolsPanelRef = toolsPanelRef; + // const [dimensions, setDimensions] = React.useState({ + // width: undefined, + // height: undefined, + // }); + + React.useEffect(() => { + this.embeddableMenuRef = embeddableMenuRef; + this.obsidianMenu = new ObsidianMenu(this.plugin, toolsPanelRef, this); + this.embeddableMenu = new EmbeddableMenu(this, embeddableMenuRef); + this.excalidrawWrapperRef = excalidrawWrapperRef; + return () => { + this.obsidianMenu.destroy(); + this.obsidianMenu = null; + this.embeddableMenu.destroy(); + this.embeddableMenu = null; + this.toolsPanelRef.current = null; + this.embeddableMenuRef.current = null; + this.excalidrawWrapperRef.current = null; + } + }, []); + + //React.useEffect(() => { + // setDimensions({ + // width: this.contentEl.clientWidth, + // height: this.contentEl.clientHeight, + // }); + + // const onResize = () => { + // const width = this.contentEl.clientWidth; + // const height = this.contentEl.clientHeight; + // setDimensions({ width, height }); + // }; + + // this.ownerWindow.addEventListener("resize", onResize); + // return () => { + // this.ownerWindow.removeEventListener("resize", onResize); + // }; + // }, [excalidrawWrapperRef]); + + const observer = React.useRef( + new ResizeObserver((entries) => { + if(!toolsPanelRef || !toolsPanelRef.current) return; + const { width, height } = entries[0].contentRect; + if(width===0 || height ===0) return; + const dx = toolsPanelRef.current.onRightEdge + ? toolsPanelRef.current.previousWidth - width + : 0; + const dy = toolsPanelRef.current.onBottomEdge + ? toolsPanelRef.current.previousHeight - height + : 0; + toolsPanelRef.current.updatePosition(dy, dx); + }), + ); + + React.useEffect(() => { + if (toolsPanelRef?.current) { + observer.current.observe(toolsPanelRef.current.containerRef.current); + } + return () => { + //unobserve is done in ToolsPanel componentWillUnmount + }; + }, [toolsPanelRef, observer]); + + //--------------------------------------------------------------------------------- + //--------------------------------------------------------------------------------- + // Render Excalidraw DIV + //--------------------------------------------------------------------------------- + //--------------------------------------------------------------------------------- + return React.createElement( + React.Fragment, + null, + React.createElement( + "div", + { + className: "excalidraw-wrapper", + ref: excalidrawWrapperRef, + key: "abc", + tabIndex: 0, + onKeyDown: this.excalidrawDIVonKeyDown.bind(this), + 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), + }, + React.createElement( + Excalidraw, + { + excalidrawAPI: (this.setExcalidrawAPI.bind(this)), + width: "100%", //dimensions.width, + height: "100%", //dimensions.height, + UIOptions: + { + canvasActions: + { + loadScene: false, + saveScene: false, + saveAsScene: false, + export: false, + saveAsImage: false, + saveToActiveFile: false, + }, + }, + initState: initdata?.appState, + initialData: initdata, + detectScroll: true, + onPointerUpdate: this.onPointerUpdate.bind(this), + libraryReturnUrl: "app://obsidian.md", + autoFocus: true, + langCode: obsidianToExcalidrawMap[this.plugin.locale]??"en-EN", + aiEnabled: true, + onChange: this.onChange.bind(this), + onLibraryChange: this.onLibraryChange.bind(this), + renderTopRightUI: this.renderTopRightUI.bind(this), //(isMobile: boolean, appState: AppState) => this.obsidianMenu.renderButton (isMobile, appState), + renderEmbeddableMenu: this.renderEmbeddableMenu.bind(this), + onPaste: this.onPaste.bind(this), + onThemeChange: this.onThemeChange.bind(this), + onDrop: this.onDrop.bind(this), + onBeforeTextEdit: this.onBeforeTextEdit.bind(this), + onBeforeTextSubmit: this.onBeforeTextSubmit.bind(this), + onLinkOpen: this.onLinkOpen.bind(this), + onLinkHover: this.onLinkHover.bind(this), + onContextMenu: this.onContextMenu.bind(this), + onViewModeChange: this.onViewModeChange.bind(this), + validateEmbeddable: true, + renderWebview: DEVICE.isDesktop, + renderEmbeddable: this.renderEmbeddable.bind(this), + renderMermaid: shouldRenderMermaid, + showDeprecatedFonts: true, + insertLinkAction: this.insertLinkAction.bind(this), + }, + this.renderCustomActionsMenu(), + this.renderWelcomeScreen(), + this.ttdDialog(), + this.diagramToCode(), + this.ttdDialogTrigger(), + ), + this.renderToolsPanel(observer), + ), + ); + } + + private async instantiateExcalidraw( + initdata: { + elements: any, + appState: any, + files: any, + libraryItems: any + } + ) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.instantiateExcalidraw, "ExcalidrawView.instantiateExcalidraw", initdata); + await this.plugin.awaitInit(); + while(!this.semaphores.scriptsReady) { + await sleep(50); + } + const React = this.packages.react; + const ReactDOM = this.packages.reactDOM; + //console.log("ExcalidrawView.instantiateExcalidraw()"); + this.clearDirty(); + + this.excalidrawRoot = ReactDOM.createRoot(this.contentEl); + this.excalidrawRoot.render(React.createElement(this.excalidrawRootElement.bind(this,initdata))); + } + + private updateContainerSize(containerId?: string, delay: boolean = false, justloaded: boolean = false) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updateContainerSize, "ExcalidrawView.updateContainerSize", containerId, delay); + const api = this.excalidrawAPI; + if (!api) { + return; + } + const update = () => { + const containers = containerId + ? api + .getSceneElements() + .filter((el: ExcalidrawElement) => el.id === containerId && el.type!=="arrow") + : api + .getSceneElements() + .filter(isContainer); + if (containers.length > 0) { + if (justloaded) { + //updateContainerSize will bump scene version which will trigger a false autosave + //after load, which will lead to a ping-pong between two synchronizing devices + this.semaphores.justLoaded = true; + } + api.updateContainerSize(containers); + } + }; + if (delay) { + window.setTimeout(() => update(), 50); + } else { + update(); + } + } + + public zoomToFit(delay: boolean = true, justLoaded: boolean = false) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.zoomToFit, "ExcalidrawView.zoomToFit", delay, justLoaded); + //view is closing via onWindowMigrated + if(this.semaphores?.viewunload) { + return; + } + const modalContainer = document.body.querySelector("div.modal-container"); + if(modalContainer) return; //do not autozoom when the command palette or other modal container is envoked on iPad + const api = this.excalidrawAPI; + if (!api || this.semaphores.isEditingText || this.semaphores.preventAutozoom) { + return; + } + if (windowMigratedDisableZoomOnce) { + windowMigratedDisableZoomOnce = false; + return; + } + const maxZoom = this.plugin.settings.zoomToFitMaxLevel; + const elements = api.getSceneElements().filter((el:ExcalidrawElement)=>el.width<10000 && el.height<10000); + if((DEVICE.isMobile && elements.length>1000) || elements.length>2500) { + if(justLoaded) api.scrollToContent(); + return; + } + if (delay) { + //time for the DOM to render, I am sure there is a more elegant solution + window.setTimeout( + () => api.zoomToFit(elements, maxZoom, this.isFullscreen() ? 0 : 0.05), + 100, + ); + } else { + api.zoomToFit(elements, maxZoom, this.isFullscreen() ? 0 : 0.05); + } + } + + public updatePinnedScripts() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updatePinnedScripts, "ExcalidrawView.updatePinnedScripts"); + const api = this.excalidrawAPI as ExcalidrawImperativeAPI; + if (!api) { + return false; + } + api.updateScene({ + appState: { pinnedScripts: this.plugin.settings.pinnedScripts }, + storeAction: "update", + }); + } + + public updatePinnedCustomPens() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updatePinnedCustomPens, "ExcalidrawView.updatePinnedCustomPens"); + const api = this.excalidrawAPI as ExcalidrawImperativeAPI; + if (!api) { + return false; + } + api.updateScene({ + appState: { + customPens: this.plugin.settings.customPens.slice(0,this.plugin.settings.numberOfCustomPens), + }, + storeAction: "update", + }); + } + + public updatePinchZoom() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updatePinchZoom, "ExcalidrawView.updatePinchZoom"); + const api = this.excalidrawAPI as ExcalidrawImperativeAPI; + if (!api) { + return false; + } + api.updateScene({ + appState: { allowPinchZoom: this.plugin.settings.allowPinchZoom }, + storeAction: "update", + }); + } + + public updateWheelZoom() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updateWheelZoom, "ExcalidrawView.updateWheelZoom"); + const api = this.excalidrawAPI as ExcalidrawImperativeAPI; + if (!api) { + return false; + } + api.updateScene({ + appState: { allowWheelZoom: this.plugin.settings.allowWheelZoom }, + storeAction: "update", + }); + } + + public async toggleTrayMode() { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.toggleTrayMode, "ExcalidrawView.toggleTrayMode"); + const api = this.excalidrawAPI as ExcalidrawImperativeAPI; + if (!api) { + return false; + } + const st = api.getAppState(); + api.updateScene({ + appState: { trayModeEnabled: !st.trayModeEnabled }, + storeAction: "update", + }); + + //just in case settings were updated via Obsidian sync + await this.plugin.loadSettings(); + this.plugin.settings.defaultTrayMode = !st.trayModeEnabled; + this.plugin.saveSettings(); + } + + /** + * + * @param elements + * @param query + * @param selectResult + * @param exactMatch + * @param selectGroup + * @returns true if element found, false if no element is found. + */ + + public selectElementsMatchingQuery( + elements: ExcalidrawElement[], + query: string[], + selectResult: boolean = true, + exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530 + selectGroup: boolean = false, + ):boolean { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.selectElementsMatchingQuery, "ExcalidrawView.selectElementsMatchingQuery", query, selectResult, exactMatch, selectGroup); + let match = getTextElementsMatchingQuery( + elements.filter((el: ExcalidrawElement) => el.type === "text"), + query, + exactMatch, + ).concat(getFrameElementsMatchingQuery( + elements.filter((el: ExcalidrawElement) => el.type === "frame"), + query, + exactMatch, + )).concat(getElementsWithLinkMatchingQuery( + elements.filter((el: ExcalidrawElement) => el.link), + query, + exactMatch, + )).concat(getImagesMatchingQuery( + elements, + query, + this.excalidrawData, + exactMatch, + )); + + if (match.length === 0) { + new Notice(t("NO_SEARCH_RESULT")); + return false; + } + + if(selectGroup) { + const groupElements = this.plugin.ea.getElementsInTheSameGroupWithElement(match[0],elements) + if(groupElements.length>0) { + match = groupElements; + } + } + + this.zoomToElements(selectResult,match); + return true; + } + + public zoomToElements( + selectResult: boolean, + elements: ExcalidrawElement[] + ) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.zoomToElements, "ExcalidrawView.zoomToElements", selectResult, elements); + const api = this.excalidrawAPI; + if (!api) return; + + const zoomLevel = this.plugin.settings.zoomToFitMaxLevel; + if (selectResult) { + api.selectElements(elements, true); + } + api.zoomToFit(elements, zoomLevel, 0.05); + } + + public getViewElements(): ExcalidrawElement[] { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getViewElements, "ExcalidrawView.getViewElements"); + const api = this.excalidrawAPI; + if (!api) { + return []; + } + return api.getSceneElements(); + } + + /** + * + * @param deepSelect: if set to true, child elements of the selected frame will also be selected + * @returns + */ + public getViewSelectedElements(includFrameChildren: boolean = true): ExcalidrawElement[] { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getViewSelectedElements, "ExcalidrawView.getViewSelectedElements"); + const api = this.excalidrawAPI as ExcalidrawImperativeAPI; + if (!api) { + return []; + } + const selectedElements = api.getAppState()?.selectedElementIds; + if (!selectedElements) { + return []; + } + const selectedElementsKeys = Object.keys(selectedElements); + if (!selectedElementsKeys) { + return []; + } + + const elementIDs = new Set(); + + const elements: ExcalidrawElement[] = api + .getSceneElements() + .filter((e: any) => selectedElementsKeys.includes(e.id)); + + const containerBoundTextElmenetsReferencedInElements = elements + .filter( + (el) => + el.boundElements && + el.boundElements.filter((be) => be.type === "text").length > 0, + ) + .map( + (el) => + el.boundElements + .filter((be) => be.type === "text") + .map((be) => be.id)[0], + ); + + if(includFrameChildren && elements.some(el=>el.type === "frame")) { + elements.filter(el=>el.type === "frame").forEach(frameEl => { + api.getSceneElements() + .filter(el=>el.frameId === frameEl.id) + .forEach(el=>elementIDs.add(el.id)) + }) + } + + elements.forEach(el=>elementIDs.add(el.id)); + containerBoundTextElmenetsReferencedInElements.forEach(id=>elementIDs.add(id)); + + return api + .getSceneElements() + .filter((el: ExcalidrawElement) => elementIDs.has(el.id)); + } + + /** + * + * @param prefix - defines the default button. + * @returns + */ + public async copyLinkToSelectedElementToClipboard(prefix:string) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.copyLinkToSelectedElementToClipboard, "ExcalidrawView.copyLinkToSelectedElementToClipboard", prefix); + const elements = this.getViewSelectedElements(); + if (elements.length < 1) { + new Notice(t("INSERT_LINK_TO_ELEMENT_ERROR")); + return; + } + + let elementId:string = undefined; + + if(elements.length === 2) { + const textEl = elements.filter(el=>el.type==="text"); + if(textEl.length===1 && (textEl[0] as ExcalidrawTextElement).containerId) { + const container = elements.filter(el=>el.boundElements.some(be=>be.type==="text")) + if(container.length===1) { + elementId = textEl[0].id; + } + } + } + + if(!elementId) { + elementId = elements.length === 1 + ? elements[0].id + : this.plugin.ea.getLargestElement(elements).id; + } + + const frames = elements.filter(el=>el.type==="frame"); + const hasFrame = frames.length === 1; + const hasGroup = elements.some(el=>el.groupIds && el.groupIds.length>0); + + let button = { + area: {caption: "Area", action:()=>{prefix="area="; return;}}, + link: {caption: "Link", action:()=>{prefix="";return}}, + group: {caption: "Group", action:()=>{prefix="group="; return;}}, + frame: {caption: "Frame", action:()=>{prefix="frame="; elementId = frames[0].id; return;}}, + clippedframe: {caption: "Clipped Frame", action:()=>{prefix="clippedframe="; ; elementId = frames[0].id; return;}}, + } + + let buttons = []; + switch(prefix) { + case "area=": + buttons = [ + button.area, + button.link, + ...hasGroup ? [button.group] : [], + ...hasFrame ? [button.frame, button.clippedframe] : [], + ]; + break; + case "group=": + buttons = [ + ...hasGroup ? [button.group] : [], + button.link, + button.area, + ...hasFrame ? [button.frame, button.clippedframe] : [], + ]; + break; + case "frame=": + buttons = [ + ...hasFrame ? [button.frame, button.clippedframe] : [], + ...hasGroup ? [button.group] : [], + button.link, + button.area, + ]; + break; + case "clippedframe=": + buttons = [ + ...hasFrame ? [button.clippedframe, button.frame] : [], + ...hasGroup ? [button.group] : [], + button.link, + button.area, + ]; + break; + default: + buttons = [ + {caption: "Link", action:()=>{prefix="";return}}, + {caption: "Area", action:()=>{prefix="area="; return;}}, + {caption: "Group", action:()=>{prefix="group="; return;}}, + ...hasFrame ? [button.frame, button.clippedframe] : [], + ] + } + + const alias = await ScriptEngine.inputPrompt( + this, + this.plugin, + this.app, + "Set link alias", + "Leave empty if you do not want to set an alias", + "", + buttons, + ); + navigator.clipboard.writeText( + `${prefix.length>0?"!":""}[[${this.file.path}#^${prefix}${elementId}${alias ? `|${alias}` : ``}]]`, + ); + new Notice(t("INSERT_LINK_TO_ELEMENT_READY")); + } + + public updateScene( + scene: { + elements?: ExcalidrawElement[]; + appState?: any; + files?: any; + storeAction?: "capture" | "none" | "update"; //https://github.com/excalidraw/excalidraw/pull/7898 + }, + shouldRestore: boolean = false, + ) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updateScene, "ExcalidrawView.updateScene", scene, shouldRestore); + const api = this.excalidrawAPI; + if (!api) { + return; + } + const shouldRestoreElements = scene.elements && shouldRestore; + if (shouldRestoreElements) { + scene.elements = restore(scene, null, null).elements; + } + if(Boolean(scene.appState)) { + //@ts-ignore + scene.forceFlushSync = true; + } + try { + api.updateScene(scene); + } catch (e) { + errorlog({ + where: "ExcalidrawView.updateScene 1st attempt", + fn: this.updateScene, + error: e, + scene, + willDoSecondAttempt: !shouldRestoreElements, + }); + if (!shouldRestoreElements) { + //second attempt + try { + scene.elements = restore(scene, null, null).elements; + api.updateScene(scene); + } catch (e) { + errorlog({ + where: "ExcalidrawView.updateScene 2nd attempt", + fn: this.updateScene, + error: e, + scene, + }); + warningUnknowSeriousError(); + } + } else { + warningUnknowSeriousError(); + } + } + } + + public updateEmbeddableRef(id: string, ref: HTMLIFrameElement | HTMLWebViewElement | null) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updateEmbeddableRef, "ExcalidrawView.updateEmbeddableRef", id, ref); + if (ref) { + this.embeddableRefs.set(id, ref); + } + } + + public getEmbeddableElementById(id: string): HTMLIFrameElement | HTMLWebViewElement | undefined { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getEmbeddableElementById, "ExcalidrawView.getEmbeddableElementById", id); + return this.embeddableRefs.get(id); + } + + public updateEmbeddableLeafRef(id: string, ref: any) { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.updateEmbeddableLeafRef, "ExcalidrawView.updateEmbeddableLeafRef", id, ref); + if(ref) { + this.embeddableLeafRefs.set(id, ref); + } + } + + public getEmbeddableLeafElementById(id: string): {leaf: WorkspaceLeaf; node?: ObsidianCanvasNode; editNode?: Function} | null { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getEmbeddableLeafElementById, "ExcalidrawView.getEmbeddableLeafElementById", id); + if(!id) return null; + const ref = this.embeddableLeafRefs.get(id); + if(!ref) { + return null; + } + return ref as {leaf: WorkspaceLeaf; node?: ObsidianCanvasNode; editNode?: Function}; + } + + public getActiveEmbeddable ():{leaf: WorkspaceLeaf; node?: ObsidianCanvasNode; editNode?: Function}|null { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getActiveEmbeddable, "ExcalidrawView.getActiveEmbeddable"); + if(!this.excalidrawAPI) return null; + const api = this.excalidrawAPI as ExcalidrawImperativeAPI; + const st = api.getAppState(); + if(!st.activeEmbeddable || st.activeEmbeddable.state !== "active" ) return null; + return this.getEmbeddableLeafElementById(st.activeEmbeddable?.element?.id); + } + + get editor(): any { + const embeddable = this.getActiveEmbeddable(); + if(embeddable) { + if(embeddable.node && embeddable.node.isEditing) { + return embeddable.node.child.editor; + } + if(embeddable.leaf?.view instanceof MarkdownView) { + return embeddable.leaf.view.editor; + } + } + return null; + } +} + +export function getTextMode(data: string): TextMode { + (process.env.NODE_ENV === 'development') && DEBUGGING && debug(getTextMode, "ExcalidrawView.getTextMode", data); + const parsed = + data.search("excalidraw-plugin: parsed\n") > -1 || + data.search("excalidraw-plugin: locked\n") > -1; //locked for backward compatibility + return parsed ? TextMode.parsed : TextMode.raw; +} diff --git a/src/constants/startupScript.md b/src/constants/Assets/startupScript.md similarity index 100% rename from src/constants/startupScript.md rename to src/constants/Assets/startupScript.md diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 3cdbc60..8c7b74b 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -1,8 +1,8 @@ import { customAlphabet } from "nanoid"; -import { DeviceType } from "../types/types"; -import { ExcalidrawLib } from "../ExcalidrawLib"; +import { DeviceType } from "../Types/Types"; +import { ExcalidrawLib } from "../Types/ExcalidrawLib"; import { moment } from "obsidian"; -import ExcalidrawPlugin from "src/main"; +import ExcalidrawPlugin from "src/Core/main"; //This is only for backward compatibility because an early version of obsidian included an encoding to avoid fantom links from littering Obsidian graph view declare const PLUGIN_VERSION:string; export let EXCALIDRAW_PLUGIN: ExcalidrawPlugin = null; diff --git a/src/lang/helpers.ts b/src/lang/helpers.ts index 88cc24c..5edd4ce 100644 --- a/src/lang/helpers.ts +++ b/src/lang/helpers.ts @@ -1,7 +1,7 @@ //Solution copied from obsidian-kanban: https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/lang/helpers.ts -import { LOCALE } from "src/constants/constants"; -import en from "./locale/en"; +import { LOCALE } from "src/Constants/Constants"; +import en from "./Locale/en"; declare const PLUGIN_LANGUAGES: Record; declare var LZString: any; diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 722ac90..c071596 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -2,9 +2,9 @@ import { DEVICE, FRONTMATTER_KEYS, CJK_FONTS, -} from "src/constants/constants"; -import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/constSettingsTags"; -import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper"; +} from "src/Constants/Constants"; +import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/Constants/ConstSettingsTags"; +import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/Utils/ModifierkeyHelper"; declare const PLUGIN_VERSION:string; diff --git a/src/lang/locale/ru.ts b/src/lang/locale/ru.ts index 8912d93..b15f9e2 100644 --- a/src/lang/locale/ru.ts +++ b/src/lang/locale/ru.ts @@ -1,6 +1,6 @@ -import { DEVICE, FRONTMATTER_KEYS, CJK_FONTS } from "src/constants/constants"; -import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/constSettingsTags"; -import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper"; +import { DEVICE, FRONTMATTER_KEYS, CJK_FONTS } from "src/Constants/Constants"; +import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/Constants/ConstSettingsTags"; +import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/Utils/ModifierkeyHelper"; // русский export default { diff --git a/src/lang/locale/zh-cn.ts b/src/lang/locale/zh-cn.ts index dba3913..8c0d02e 100644 --- a/src/lang/locale/zh-cn.ts +++ b/src/lang/locale/zh-cn.ts @@ -2,9 +2,9 @@ import { DEVICE, FRONTMATTER_KEYS, CJK_FONTS -} from "src/constants/constants"; -import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/constSettingsTags"; -import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/utils/ModifierkeyHelper"; +} from "src/Constants/Constants"; +import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/Constants/ConstSettingsTags"; +import { labelALT, labelCTRL, labelMETA, labelSHIFT } from "src/Utils/ModifierkeyHelper"; declare const PLUGIN_VERSION:string; diff --git a/src/ExcalidrawLib.d.ts b/src/types/ExcalidrawLib.d.ts similarity index 100% rename from src/ExcalidrawLib.d.ts rename to src/types/ExcalidrawLib.d.ts diff --git a/src/types/ExcalidrawViewTypes.ts b/src/types/ExcalidrawViewTypes.ts new file mode 100644 index 0000000..774e76c --- /dev/null +++ b/src/types/ExcalidrawViewTypes.ts @@ -0,0 +1,73 @@ +// src/types/ExcalidrawViewTypes.ts + +import { WorkspaceLeaf, TFile } from "obsidian"; +import { ExcalidrawElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types"; +import { ObsidianCanvasNode } from "../Utils/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 interface SelectedElementWithLink { + id: string | null; + text: string | null; +} + +export interface SelectedImage { + id: string | null; + fileId: FileId | null; +} + +export interface EmbeddableLeafRef { + leaf: WorkspaceLeaf; + node?: ObsidianCanvasNode; + editNode?: Function; +} + +export interface ViewSemaphores { + warnAboutLinearElementLinkClick: boolean; + //flag to prevent overwriting the changes the user makes in an embeddable view editing the back side of the drawing + embeddableIsEditingSelf: boolean; + popoutUnload: boolean; //the unloaded Excalidraw view was the last leaf in the popout window + viewunload: boolean; + //first time initialization of the view + scriptsReady: boolean; + + //The role of justLoaded is to capture the Excalidraw.onChange event that fires right after the canvas was loaded for the first time to + //- prevent the first onChange event to mark the file as dirty and to consequently cause a save right after load, causing sync issues in turn + //- trigger autozoom (in conjunction with preventAutozoomOnLoad) + justLoaded: boolean; + + //the modifyEventHandler in main.ts will fire when an Excalidraw file has changed (e.g. due to sync) + //when a drawing that is currently open in a view receives a sync update, excalidraw reload() is triggered + //the preventAutozoomOnLoad flag will prevent the open drawing from autozooming when it is reloaded + preventAutozoom: boolean; + + autosaving: boolean; //flags that autosaving is in progress. Autosave is an async timer, the flag prevents collision with force save + forceSaving: boolean; //flags that forcesaving is in progress. The flag prevents collision with autosaving + dirty: string; //null if there are no changes to be saved, the path of the file if the drawing has unsaved changes + + //reload() is triggered by modifyEventHandler in main.ts. preventReload is a one time flag to abort reloading + //to avoid interrupting the flow of drawing by the user. + preventReload: boolean; + + isEditingText: boolean; //https://stackoverflow.com/questions/27132796/is-there-any-javascript-event-fired-when-the-on-screen-keyboard-on-mobile-safari + + //Save is triggered by multiple threads when an Excalidraw pane is terminated + //- by the view itself + //- by the activeLeafChangeEventHandler change event handler + //- by monkeypatches on detach(next) + //This semaphore helps avoid collision of saves + saving: boolean; + hoverSleep: boolean; //flag with timer to prevent hover preview from being triggered dozens of times + wheelTimeout:number; //used to avoid hover preview while zooming +} \ No newline at end of file diff --git a/src/types/types.d.ts b/src/types/types.d.ts index 0cdd0ac..1797fc5 100644 --- a/src/types/types.d.ts +++ b/src/types/types.d.ts @@ -1,6 +1,6 @@ import { TFile } from "obsidian"; -import { ExcalidrawAutomate } from "../ExcalidrawAutomate"; -import { ExcalidrawLib } from "../ExcalidrawLib"; +import { ExcalidrawAutomate } from "../Shared/ExcalidrawAutomate"; +import { ExcalidrawLib } from "./ExcalidrawLib"; export type ConnectionPoint = "top" | "bottom" | "left" | "right" | null; diff --git a/src/utils/AIUtils.ts b/src/utils/AIUtils.ts index 4977f96..942fc86 100644 --- a/src/utils/AIUtils.ts +++ b/src/utils/AIUtils.ts @@ -1,6 +1,5 @@ -import { DEVICE } from "../constants/constants"; import { Notice, RequestUrlResponse, requestUrl } from "obsidian"; -import ExcalidrawPlugin from "src/main"; +import ExcalidrawPlugin from "src/Core/main"; type MessageContent = | string diff --git a/src/utils/CJKLoader.ts b/src/utils/CJKLoader.ts index 250f207..0beaedd 100644 --- a/src/utils/CJKLoader.ts +++ b/src/utils/CJKLoader.ts @@ -1,4 +1,4 @@ -import ExcalidrawPlugin from "src/main"; +import ExcalidrawPlugin from "src/Core/main"; import { PromisePool, promiseTry } from "./Utils"; import { blobToBase64 } from "./FileUtils"; diff --git a/src/utils/CanvasNodeFactory.ts b/src/utils/CanvasNodeFactory.ts index c1a1e29..7a55db6 100644 --- a/src/utils/CanvasNodeFactory.ts +++ b/src/utils/CanvasNodeFactory.ts @@ -9,7 +9,7 @@ container.appendChild(node.contentEl) */ import { TFile, WorkspaceLeaf, WorkspaceSplit } from "obsidian"; -import ExcalidrawView from "src/ExcalidrawView"; +import ExcalidrawView from "src/View/ExcalidrawView"; import { getContainerForDocument, ConstructableWorkspaceSplit, isObsidianThemeDark } from "./ObsidianUtils"; import { CustomMutationObserver, DEBUGGING } from "./DebugHelper"; diff --git a/src/utils/CarveOut.ts b/src/utils/CarveOut.ts index 05a6552..1ce91f3 100644 --- a/src/utils/CarveOut.ts +++ b/src/utils/CarveOut.ts @@ -1,7 +1,7 @@ import { ExcalidrawEmbeddableElement, ExcalidrawFrameElement, ExcalidrawImageElement } from "@zsviczian/excalidraw/types/excalidraw/element/types"; import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types"; -import { getEA } from "src"; -import { ExcalidrawAutomate } from "src/ExcalidrawAutomate"; +import { getEA } from "src/Core"; +import { ExcalidrawAutomate } from "src/Shared/ExcalidrawAutomate"; import { getCropFileNameAndFolder, getListOfTemplateFiles, splitFolderAndFilename } from "./FileUtils"; import { Notice, TFile } from "obsidian"; import { Radians } from "@zsviczian/excalidraw/types/math"; diff --git a/src/utils/CropImage.ts b/src/utils/CropImage.ts index 24fbaa4..bcfd45b 100644 --- a/src/utils/CropImage.ts +++ b/src/utils/CropImage.ts @@ -3,10 +3,10 @@ import { BinaryFileData } from "@zsviczian/excalidraw/types/excalidraw/types"; import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types"; import { Notice } from "obsidian"; -import { getEA } from "src"; -import { ExcalidrawAutomate, cloneElement } from "src/ExcalidrawAutomate"; -import { ExportSettings } from "src/ExcalidrawView"; -import { nanoid } from "src/constants/constants"; +import { getEA } from "src/Core"; +import { ExcalidrawAutomate, cloneElement } from "src/Shared/ExcalidrawAutomate"; +import { ExportSettings } from "src/View/ExcalidrawView"; +import { nanoid } from "src/Constants/Constants"; import { svgToBase64 } from "./Utils"; export class CropImage { diff --git a/src/utils/CustomEmbeddableUtils.ts b/src/utils/CustomEmbeddableUtils.ts index 8044422..bfd99c8 100644 --- a/src/utils/CustomEmbeddableUtils.ts +++ b/src/utils/CustomEmbeddableUtils.ts @@ -1,9 +1,9 @@ import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types"; -import { DEVICE, REG_LINKINDEX_INVALIDCHARS } from "src/constants/constants"; +import { DEVICE, REG_LINKINDEX_INVALIDCHARS } from "src/Constants/Constants"; import { getParentOfClass } from "./ObsidianUtils"; import { TFile, WorkspaceLeaf } from "obsidian"; import { getLinkParts } from "./Utils"; -import ExcalidrawView from "src/ExcalidrawView"; +import ExcalidrawView from "src/View/ExcalidrawView"; export const useDefaultExcalidrawFrame = (element: NonDeletedExcalidrawElement) => { return !(element.link.startsWith("[") || element.link.startsWith("file:") || element.link.startsWith("data:")); // && !element.link.match(TWITTER_REG); diff --git a/src/utils/DynamicStyling.ts b/src/utils/DynamicStyling.ts index 6ec8f09..64f963f 100644 --- a/src/utils/DynamicStyling.ts +++ b/src/utils/DynamicStyling.ts @@ -1,12 +1,12 @@ import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types"; import { ColorMaster } from "@zsviczian/colormaster"; -import { ExcalidrawAutomate } from "src/ExcalidrawAutomate"; -import ExcalidrawView from "src/ExcalidrawView"; -import { DynamicStyle } from "src/types/types"; -import { cloneElement } from "src/ExcalidrawAutomate"; +import { ExcalidrawAutomate } from "src/Shared/ExcalidrawAutomate"; +import ExcalidrawView from "src/View/ExcalidrawView"; +import { DynamicStyle } from "src/Types/Types"; +import { cloneElement } from "src/Shared/ExcalidrawAutomate"; import { ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/excalidraw/element/types"; import { addAppendUpdateCustomData } from "./Utils"; -import { mutateElement } from "src/constants/constants"; +import { mutateElement } from "src/Constants/Constants"; export const setDynamicStyle = ( ea: ExcalidrawAutomate, diff --git a/src/utils/ExcalidrawConfig.ts b/src/utils/ExcalidrawConfig.ts index f45544f..c0296dd 100644 --- a/src/utils/ExcalidrawConfig.ts +++ b/src/utils/ExcalidrawConfig.ts @@ -1,5 +1,5 @@ -import { DEVICE } from "src/constants/constants"; -import ExcalidrawPlugin from "src/main"; +import { DEVICE } from "src/Constants/Constants"; +import ExcalidrawPlugin from "src/Core/main"; export class ExcalidrawConfig { public areaLimit: number = 16777216; diff --git a/src/utils/ExcalidrawViewUtils.ts b/src/utils/ExcalidrawViewUtils.ts index c9fa602..7b70358 100644 --- a/src/utils/ExcalidrawViewUtils.ts +++ b/src/utils/ExcalidrawViewUtils.ts @@ -1,17 +1,17 @@ -import { MAX_IMAGE_SIZE, IMAGE_TYPES, ANIMATED_IMAGE_TYPES, MD_EX_SECTIONS } from "src/constants/constants"; +import { MAX_IMAGE_SIZE, IMAGE_TYPES, ANIMATED_IMAGE_TYPES, MD_EX_SECTIONS } from "src/Constants/Constants"; import { App, Modal, Notice, TFile, WorkspaceLeaf } from "obsidian"; -import { ExcalidrawAutomate } from "src/ExcalidrawAutomate"; -import { REGEX_LINK, REG_LINKINDEX_HYPERLINK, getExcalidrawMarkdownHeaderSection, REGEX_TAGS } from "src/ExcalidrawData"; -import ExcalidrawView from "src/ExcalidrawView"; +import { ExcalidrawAutomate } from "src/Shared/ExcalidrawAutomate"; +import { REGEX_LINK, REG_LINKINDEX_HYPERLINK, getExcalidrawMarkdownHeaderSection, REGEX_TAGS } from "../Shared/ExcalidrawData"; +import ExcalidrawView from "src/View/ExcalidrawView"; import { ExcalidrawElement, ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/excalidraw/element/types"; import { getEmbeddedFilenameParts, getLinkParts, isImagePartRef } from "./Utils"; import { cleanSectionHeading } from "./ObsidianUtils"; -import { getEA } from "src"; +import { getEA } from "src/Core"; import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types"; -import { EmbeddableMDCustomProps } from "src/dialogs/EmbeddableSettings"; +import { EmbeddableMDCustomProps } from "src/Shared/Dialogs/EmbeddableSettings"; import { nanoid } from "nanoid"; -import { t } from "src/lang/helpers"; +import { t } from "src/Lang/Helpers"; export async function insertImageToView( ea: ExcalidrawAutomate, diff --git a/src/utils/FileUtils.ts b/src/utils/FileUtils.ts index 5f566a7..0aa9d3a 100644 --- a/src/utils/FileUtils.ts +++ b/src/utils/FileUtils.ts @@ -1,10 +1,10 @@ import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types"; import { App, loadPdfJs, normalizePath, Notice, requestUrl, RequestUrlResponse, TAbstractFile, TFile, TFolder, Vault } from "obsidian"; -import { DEVICE, EXCALIDRAW_PLUGIN, FRONTMATTER_KEYS, URLFETCHTIMEOUT } from "src/constants/constants"; -import { IMAGE_MIME_TYPES, MimeType } from "src/EmbeddedFileLoader"; -import { ExcalidrawSettings } from "src/settings"; +import { DEVICE, EXCALIDRAW_PLUGIN, FRONTMATTER_KEYS, URLFETCHTIMEOUT } from "src/Constants/Constants"; +import { IMAGE_MIME_TYPES, MimeType } from "../Shared/EmbeddedFileLoader"; +import { ExcalidrawSettings } from "src/Core/settings"; import { errorlog, getDataURL } from "./Utils"; -import ExcalidrawPlugin from "src/main"; +import ExcalidrawPlugin from "src/Core/main"; import { ANNOTATED_PREFIX, CROPPED_PREFIX } from "./CarveOut"; import { getAttachmentsFolderAndFilePath } from "./ObsidianUtils"; diff --git a/src/utils/GetElementAtPointer.ts b/src/utils/GetElementAtPointer.ts index bedc5e1..bb9810d 100644 --- a/src/utils/GetElementAtPointer.ts +++ b/src/utils/GetElementAtPointer.ts @@ -1,8 +1,8 @@ import { ExcalidrawElement, ExcalidrawImageElement, ExcalidrawTextElement } from "@zsviczian/excalidraw/types/excalidraw/element/types"; -import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "src/ExcalidrawData"; -import ExcalidrawView, { TextMode } from "src/ExcalidrawView"; +import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "../Shared/ExcalidrawData"; +import ExcalidrawView, { TextMode } from "src/View/ExcalidrawView"; import { rotatedDimensions } from "./Utils"; -import { getBoundTextElementId } from "src/ExcalidrawAutomate"; +import { getBoundTextElementId } from "src/Shared/ExcalidrawAutomate"; export const getElementsAtPointer = ( pointer: any, diff --git a/src/utils/ImageCache.ts b/src/utils/ImageCache.ts index 18fdadf..f8de8d8 100644 --- a/src/utils/ImageCache.ts +++ b/src/utils/ImageCache.ts @@ -1,5 +1,5 @@ import { App, Notice, TFile } from "obsidian"; -import ExcalidrawPlugin from "src/main"; +import ExcalidrawPlugin from "src/Core/main"; import { convertSVGStringToElement } from "./Utils"; import { FILENAMEPARTS, PreviewImageType } from "./UtilTypes"; import { hasExcalidrawEmbeddedImagesTreeChanged } from "./FileUtils"; diff --git a/src/utils/ModifierkeyHelper.ts b/src/utils/ModifierkeyHelper.ts index 8ca155b..8f14574 100644 --- a/src/utils/ModifierkeyHelper.ts +++ b/src/utils/ModifierkeyHelper.ts @@ -1,7 +1,7 @@ import { Modifier } from "obsidian"; -import { DEVICE } from "src/constants/constants"; -import { t } from "src/lang/helpers"; -import { ExcalidrawSettings } from "src/settings"; +import { DEVICE } from "src/Constants/Constants"; +import { t } from "src/Lang/Helpers"; +import { ExcalidrawSettings } from "src/Core/settings"; export type ModifierKeys = {shiftKey:boolean, ctrlKey: boolean, metaKey: boolean, altKey: boolean}; export type KeyEvent = PointerEvent | MouseEvent | KeyboardEvent | React.DragEvent | React.PointerEvent | React.MouseEvent | ModifierKeys; export type PaneTarget = "active-pane"|"new-pane"|"popout-window"|"new-tab"|"md-properties"; @@ -113,7 +113,7 @@ export const mdPropModifier = (ev: KeyEvent): boolean => !isSHIFT(ev) && isWinCT export const scaleToFullsizeModifier = (ev: KeyEvent) => { const settings:ExcalidrawSettings = window.ExcalidrawAutomate.plugin.settings; const keySet = ((DEVICE.isMacOS || DEVICE.isIOS) ? settings.modifierKeyConfig.Mac : settings.modifierKeyConfig.Win )["InternalDragAction"]; - const rule = keySet.rules.find(r => r.result === "image-fullsize"); + const rule:ModifierKey = keySet.rules.find((r:ModifierKey) => r.result === "image-fullsize"); if(!rule) return false; const { shift, ctrl_cmd, alt_opt, meta_ctrl, result } = rule; return ( @@ -162,7 +162,7 @@ export const emulateKeysForLinkClick = (action: PaneTarget): ModifierKeys => { const config = modifierKeyConfig[platform]?.LinkClickAction; if (config) { - const rule = config.rules.find(rule => rule.result === action); + const rule:ModifierKey = config.rules.find((rule:ModifierKey) => rule.result === action); if (rule) { setCTRL(ev, rule.ctrl_cmd); setALT(ev, rule.alt_opt); diff --git a/src/utils/ObsidianUtils.ts b/src/utils/ObsidianUtils.ts index a098f2f..0ab6dd6 100644 --- a/src/utils/ObsidianUtils.ts +++ b/src/utils/ObsidianUtils.ts @@ -5,13 +5,13 @@ import { MarkdownView, normalizePath, OpenViewState, parseFrontMatterEntry, TFile, View, ViewState, Workspace, WorkspaceLeaf, WorkspaceSplit } from "obsidian"; -import ExcalidrawPlugin from "../main"; +import ExcalidrawPlugin from "../Core/main"; import { checkAndCreateFolder, splitFolderAndFilename } from "./FileUtils"; import { linkClickModifierType, ModifierKeys } from "./ModifierkeyHelper"; -import { EXCALIDRAW_PLUGIN, REG_BLOCK_REF_CLEAN, REG_SECTION_REF_CLEAN, VIEW_TYPE_EXCALIDRAW } from "src/constants/constants"; +import { EXCALIDRAW_PLUGIN, REG_BLOCK_REF_CLEAN, REG_SECTION_REF_CLEAN, VIEW_TYPE_EXCALIDRAW } from "src/Constants/Constants"; import yaml from "js-yaml"; import { debug, DEBUGGING } from "./DebugHelper"; -import ExcalidrawView from "src/ExcalidrawView"; +import ExcalidrawView from "src/View/ExcalidrawView"; export const getParentOfClass = (element: Element, cssClass: string):HTMLElement | null => { let parent = element.parentElement; diff --git a/src/utils/Pens.ts b/src/utils/Pens.ts index d8b5644..dd8fbce 100644 --- a/src/utils/Pens.ts +++ b/src/utils/Pens.ts @@ -1,4 +1,4 @@ -import { PenStyle, PenType } from "src/types/PenTypes"; +import { PenStyle, PenType } from "src/Types/PenTypes"; export const PENS:Record = { "default": { diff --git a/src/utils/StylesManager.ts b/src/utils/StylesManager.ts index d6f57af..864db46 100644 --- a/src/utils/StylesManager.ts +++ b/src/utils/StylesManager.ts @@ -1,5 +1,5 @@ import { WorkspaceWindow } from "obsidian"; -import ExcalidrawPlugin from "src/main"; +import ExcalidrawPlugin from "src/Core/main"; import { getAllWindowDocuments } from "./ObsidianUtils"; import { DEBUGGING, debug } from "./DebugHelper"; diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index e9d3b68..c95f705 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -16,22 +16,22 @@ import { getCommonBoundingBox, DEVICE, getContainerElement, -} from "../constants/constants"; -import ExcalidrawPlugin from "../main"; +} from "../Constants/Constants"; +import ExcalidrawPlugin from "../Core/main"; import { ExcalidrawElement, ExcalidrawTextElement, ImageCrop } from "@zsviczian/excalidraw/types/excalidraw/element/types"; -import { ExportSettings } from "../ExcalidrawView"; +import { ExportSettings } from "../View/ExcalidrawView"; import { getDataURLFromURL, getIMGFilename, getMimeType, getURLImageExtension } from "./FileUtils"; import { generateEmbeddableLink } from "./CustomEmbeddableUtils"; import { FILENAMEPARTS } from "./UtilTypes"; import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types"; import { cleanBlockRef, cleanSectionHeading, getFileCSSClasses } from "./ObsidianUtils"; -import { updateElementLinksToObsidianLinks } from "src/ExcalidrawAutomate"; +import { updateElementLinksToObsidianLinks } from "src/Shared/ExcalidrawAutomate"; import { CropImage } from "./CropImage"; import opentype from 'opentype.js'; -import { runCompressionWorker } from "src/workers/compression-worker"; +import { runCompressionWorker } from "src/Shared/Workers/compression-worker"; import Pool from "es6-promise-pool"; -import { FileData } from "src/EmbeddedFileLoader"; -import { t } from "src/lang/helpers"; +import { FileData } from "../Shared/EmbeddedFileLoader"; +import { t } from "src/Lang/Helpers"; declare const PLUGIN_VERSION:string; declare var LZString: any; diff --git a/src/utils/matic.ts b/src/utils/matic.ts index 3adffee..18fd968 100644 --- a/src/utils/matic.ts +++ b/src/utils/matic.ts @@ -1,4 +1,4 @@ -import { THEME } from "../constants/constants"; +import { THEME } from "../Constants/Constants"; import type { Theme } from "@zsviczian/excalidraw/types/excalidraw/element/types"; import type { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types"; import type { OpenAIInput, OpenAIOutput } from "@zsviczian/excalidraw/types/excalidraw/data/ai/types"; diff --git a/tsconfig.dev.json b/tsconfig.dev.json index 5a0297d..ec3036e 100644 --- a/tsconfig.dev.json +++ b/tsconfig.dev.json @@ -22,6 +22,6 @@ "include": [ "**/*.ts", "**/*.tsx", "src/Dialogs/OpenDrawing.ts", - "src/types/types.d.ts", + "src/Types/Types.d.ts", ] } \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 07e8ad6..4f4050c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,7 @@ }, "include": [ "**/*.ts", - "**/*.tsx", "src/Dialogs/OpenDrawing.ts", - "src/types/types.d.ts", + "**/*.tsx", "src/Shared/Dialogs/OpenDrawing.ts", + "src/Types/Types.d.ts", ] } \ No newline at end of file