diff --git a/rollup.config.js b/rollup.config.js index 0c81fb7..e6deb4e 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -105,6 +105,7 @@ const BUILD_CONFIG = { "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV), }), babel({ + babelHelpers: 'bundled', presets: [['@babel/preset-env', { targets: { ios: "15", // ios Compatibility //esmodules: true, diff --git a/src/ExcalidrawData.ts b/src/ExcalidrawData.ts index 11a250e..c66af6f 100644 --- a/src/ExcalidrawData.ts +++ b/src/ExcalidrawData.ts @@ -49,6 +49,7 @@ 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"; type SceneDataWithFiles = SceneData & { files: BinaryFiles }; @@ -222,11 +223,12 @@ export function getMarkdownDrawingSection( jsonString: string, compressed: boolean, ) { - return compressed + const result = compressed ? `## Drawing\n\x60\x60\x60compressed-json\n${compress( jsonString, )}\n\x60\x60\x60\n%%` : `## Drawing\n\x60\x60\x60json\n${jsonString}\n\x60\x60\x60\n%%`; + return result; } /** @@ -1090,8 +1092,6 @@ export class ExcalidrawData { return result; } - let jsonString = JSON.stringify(this.scene); - let id: string; //will be used to hold the new 8 char long ID for textelements that don't yet appear under # Text Elements for (const el of elements) { @@ -1102,11 +1102,10 @@ export class ExcalidrawData { if (el.id.length > 8) { result = true; id = nanoid(); - jsonString = jsonString.replaceAll(el.id, id); //brute force approach to replace all occurrences (e.g. links, groups,etc.) + updateElementIdsInScene(this.scene, el, id); } this.elementLinks.set(id, el.link); } - this.scene = JSON.parse(jsonString); return result; } @@ -1118,9 +1117,7 @@ export class ExcalidrawData { //console.log("Excalidraw.Data.findNewTextElementsInScene()"); //get scene text elements this.selectedElementIds = selectedElementIds; - const texts = this.scene.elements?.filter((el: any) => el.type === "text"); - - let jsonString = JSON.stringify(this.scene); + const texts = this.scene.elements?.filter((el: any) => el.type === "text") as ExcalidrawTextElement[]; let dirty: boolean = false; //to keep track if the json has changed let id: string; //will be used to hold the new 8 char long ID for textelements that don't yet appear under # Text Elements @@ -1136,7 +1133,7 @@ export class ExcalidrawData { delete this.selectedElementIds[te.id]; this.selectedElementIds[id] = true; } - jsonString = jsonString.replaceAll(te.id, id); //brute force approach to replace all occurrences (e.g. links, groups,etc.) + updateElementIdsInScene(this.scene, te, id); if (this.textElements.has(te.id)) { //element was created with onBeforeTextSubmit const text = this.textElements.get(te.id); @@ -1158,11 +1155,6 @@ export class ExcalidrawData { } } - if (dirty) { - //reload scene json in case it has changed - this.scene = JSON.parse(jsonString); - } - return dirty; } @@ -1470,7 +1462,7 @@ export class ExcalidrawData { appState: this.scene.appState, files: this.scene.files }, null, "\t"); - return ( + const result = ( outString + (this.textElementCommentedOut ? "" : "%%\n") + getMarkdownDrawingSection( @@ -1478,6 +1470,7 @@ export class ExcalidrawData { this.disableCompression ? false : this.plugin.settings.compress, ) ); + return result; } public async saveDataURLtoVault(dataURL: DataURL, mimeType: MimeType, key: FileId) { @@ -2027,7 +2020,7 @@ export class ExcalidrawData { } public getEquationEntries() { - return this.equations.entries(); + return this.equations?.entries(); } public deleteEquation(fileId: FileId) { diff --git a/src/ExcalidrawView.ts b/src/ExcalidrawView.ts index a90e38c..dba01df 100644 --- a/src/ExcalidrawView.ts +++ b/src/ExcalidrawView.ts @@ -404,7 +404,10 @@ export default class ExcalidrawView extends TextFileView { preventAutozoom() { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.preventAutozoom, "ExcalidrawView.preventAutozoom"); this.semaphores.preventAutozoom = true; - window.setTimeout(() => (this.semaphores.preventAutozoom = false), 1500); + window.setTimeout(() => { + if(!this.semaphores) return; + this.semaphores.preventAutozoom = false; + }, 1500); } public saveExcalidraw(scene?: any) { @@ -734,10 +737,9 @@ export default class ExcalidrawView extends TextFileView { 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 { - 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}`); - if (allowSave) { const scene = this.getScene(); @@ -2064,6 +2066,7 @@ export default class ExcalidrawView extends TextFileView { if(images.length>0) { this.preventAutozoom(); window.setTimeout(()=>this.zoomToElements(!api.getAppState().viewModeEnabled, images)); + return; } } } @@ -2473,7 +2476,7 @@ export default class ExcalidrawView extends TextFileView { * * @param justloaded - a flag to trigger zoom to fit after the drawing has been loaded */ - private async loadDrawing(justloaded: boolean, deletedElements?: ExcalidrawElement[]) { + 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; diff --git a/src/dialogs/ImportSVGDialog.ts b/src/dialogs/ImportSVGDialog.ts index 3a6ec11..adfacbf 100644 --- a/src/dialogs/ImportSVGDialog.ts +++ b/src/dialogs/ImportSVGDialog.ts @@ -7,7 +7,6 @@ import { getEA } from "src"; import { ExcalidrawAutomate } from "src/ExcalidrawAutomate"; export class ImportSVGDialog extends FuzzySuggestModal { - public app: App; public plugin: ExcalidrawPlugin; private view: ExcalidrawView; diff --git a/src/dialogs/InsertCommandDialog.ts b/src/dialogs/InsertCommandDialog.ts index 5c1a4c3..bf99d80 100644 --- a/src/dialogs/InsertCommandDialog.ts +++ b/src/dialogs/InsertCommandDialog.ts @@ -3,7 +3,6 @@ import { REG_LINKINDEX_INVALIDCHARS } from "../constants/constants"; import { t } from "../lang/helpers"; export class InsertCommandDialog extends FuzzySuggestModal { - public app: App; private addText: Function; destroy() { diff --git a/src/dialogs/InsertImageDialog.ts b/src/dialogs/InsertImageDialog.ts index f726468..bbcadea 100644 --- a/src/dialogs/InsertImageDialog.ts +++ b/src/dialogs/InsertImageDialog.ts @@ -7,7 +7,6 @@ import ExcalidrawPlugin from "../main"; import { getEA } from "src"; export class InsertImageDialog extends FuzzySuggestModal { - public app: App; public plugin: ExcalidrawPlugin; private view: ExcalidrawView; diff --git a/src/dialogs/InsertLinkDialog.ts b/src/dialogs/InsertLinkDialog.ts index 3f86eb3..a6728d7 100644 --- a/src/dialogs/InsertLinkDialog.ts +++ b/src/dialogs/InsertLinkDialog.ts @@ -5,7 +5,6 @@ import ExcalidrawPlugin from "src/main"; import { getLink } from "src/utils/FileUtils"; export class InsertLinkDialog extends FuzzySuggestModal { - public app: App; private addText: Function; private drawingPath: string; diff --git a/src/dialogs/InsertMDDialog.ts b/src/dialogs/InsertMDDialog.ts index 3ece94b..f296bce 100644 --- a/src/dialogs/InsertMDDialog.ts +++ b/src/dialogs/InsertMDDialog.ts @@ -5,7 +5,6 @@ import ExcalidrawPlugin from "../main"; import { getEA } from "src"; export class InsertMDDialog extends FuzzySuggestModal { - public app: App; public plugin: ExcalidrawPlugin; private view: ExcalidrawView; diff --git a/src/dialogs/OpenDrawing.ts b/src/dialogs/OpenDrawing.ts index 53555ce..88399d5 100644 --- a/src/dialogs/OpenDrawing.ts +++ b/src/dialogs/OpenDrawing.ts @@ -9,7 +9,6 @@ export enum openDialogAction { } export class OpenFileDialog extends FuzzySuggestModal { - public app: App; private plugin: ExcalidrawPlugin; private action: openDialogAction; private onNewPane: boolean; diff --git a/src/types/worker.d.ts b/src/types/worker.d.ts new file mode 100644 index 0000000..3edba2b --- /dev/null +++ b/src/types/worker.d.ts @@ -0,0 +1,4 @@ +declare module "web-worker:*" { + const WorkerFactory: new (options: any) => Worker; + export default WorkerFactory; +} \ No newline at end of file diff --git a/src/utils/DebugHelper.ts b/src/utils/DebugHelper.ts index ba13b0d..9d53764 100644 --- a/src/utils/DebugHelper.ts +++ b/src/utils/DebugHelper.ts @@ -14,6 +14,29 @@ export const debug = (fn: Function, fnName: string, ...messages: unknown[]) => { console.log(fnName, ...messages); }; +let timestamp: number[] = []; +let tsOrigin: number = 0; + +export function tsInit(msg: string) { + tsOrigin = Date.now(); + timestamp = [tsOrigin, tsOrigin, tsOrigin, tsOrigin, tsOrigin]; // Initialize timestamps for L0 to L4 + console.log("0ms: " + msg); +} + +export function ts(msg: string, level: number) { + if (level < 0 || level > 4) { + console.error("Invalid level. Please use level 0, 1, 2, 3, or 4."); + return; + } + + const now = Date.now(); + const diff = now - timestamp[level]; + timestamp[level] = now; + + const elapsedFromOrigin = now - tsOrigin; + console.log(`L${level} (${elapsedFromOrigin}ms) ${diff}ms: ${msg}`); +} + export class CustomMutationObserver { private originalCallback: MutationCallback; private observer: MutationObserver | null; diff --git a/src/utils/ExcalidrawSceneUtils.ts b/src/utils/ExcalidrawSceneUtils.ts new file mode 100644 index 0000000..d61d5d1 --- /dev/null +++ b/src/utils/ExcalidrawSceneUtils.ts @@ -0,0 +1,45 @@ +import { ExcalidrawArrowElement, ExcalidrawElement, ExcalidrawTextElement } from "@zsviczian/excalidraw/types/excalidraw/element/types"; +import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types"; + + +export function updateElementIdsInScene( + {elements: sceneElements}: {elements: Mutable[]}, + elementToChange: Mutable, + newID: string +) { + if(elementToChange.type === "text") { + const textElement = elementToChange as Mutable; + if(textElement.containerId) { + const containerEl = sceneElements.find(el=>el.id === textElement.containerId) as unknown as Mutable; + containerEl.boundElements?.filter(x=>x.id === textElement.id).forEach( x => { + (x.id as Mutable) = newID; + }); + } + } + + if(elementToChange.boundElements?.length>0) { + elementToChange.boundElements.forEach( binding => { + const boundEl = sceneElements.find(el=>el.id === binding.id) as unknown as Mutable; + boundEl.boundElements?.filter(x=>x.id === elementToChange.id).forEach( x => { + (x.id as Mutable) = newID; + }); + if(boundEl.type === "arrow") { + const arrow = boundEl as Mutable; + if(arrow.startBinding?.elementId === elementToChange.id) { + arrow.startBinding.elementId = newID; + } + if(arrow.endBinding?.elementId === elementToChange.id) { + arrow.endBinding.elementId = newID; + } + } + }); + } + + if(elementToChange.type === "frame") { + sceneElements.filter(el=>el.frameId === elementToChange.id).forEach(x => { + (x.frameId as Mutable) = newID; + }); + } + + elementToChange.id = newID; +} diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index fbe99a6..d37f1ad 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -529,11 +529,29 @@ export function getLinkParts (fname: string, file?: TFile): LinkParts { }; export function compress (data: string): string { - return LZString.compressToBase64(data).replace(/(.{256})/g, "$1\n\n"); + const compressed = LZString.compressToBase64(data); + + let result = ''; + const chunkSize = 256; + for (let i = 0; i < compressed.length; i += chunkSize) { + result += compressed.slice(i, i + chunkSize) + '\n\n'; + } + + return result.trim(); }; export function decompress (data: string): string { - return LZString.decompressFromBase64(data.replaceAll("\n", "").replaceAll("\r", "")); + 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); }; export function isMaskFile ( diff --git a/src/workers/save-worker.ts b/src/workers/save-worker.ts new file mode 100644 index 0000000..be58ec4 --- /dev/null +++ b/src/workers/save-worker.ts @@ -0,0 +1,153 @@ +import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types"; +import ExcalidrawView from "src/ExcalidrawView"; +import { debug, DEBUGGING, ts } from "src/utils/DebugHelper"; +import { imageCache } from "src/utils/ImageCache"; + +interface SaveWorkerMessageData { + superSave: () => Promise; + view: ExcalidrawView; + forcesave: boolean; + preventReload: boolean; +} + +onmessage = async function (e: MessageEvent) { + const { superSave, view, forcesave, preventReload } = e.data; + + try { + (process.env.NODE_ENV === 'development') && ts("allow save",1); + const scene = view.getScene(); + + if (view.compatibilityMode) { + await view.excalidrawData.syncElements(scene); + } else if ( + await view.excalidrawData.syncElements(scene, view.excalidrawAPI.getAppState().selectedElementIds) + && !view.semaphores.popoutUnload //Obsidian going black after REACT 18 migration when closing last leaf on popout + ) { + (process.env.NODE_ENV === 'development') && ts("ExcalidrawView.beforeLoadDrawing",1); + await view.loadDrawing( + false, + view.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted) + ); + (process.env.NODE_ENV === 'development') && ts("ExcalidrawView.afterLoadDrawing",1); + } + (process.env.NODE_ENV === 'development') && ts("after sync elements",1); + + //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. + view.clearDirty(); + view.clearPreventReloadTimer(); + + view.semaphores.preventReload = preventReload; + //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(view.semaphores?.viewunload) { + const d = view.getViewData(); + const plugin = view.plugin; + const file = view.file; + window.setTimeout(async ()=>{ + await plugin.app.vault.modify(file,d); + await imageCache.addBAKToCache(file.path,d); + },200) + return; + } + + (process.env.NODE_ENV === 'development') && ts("before super.save",1); + await superSave(); + (process.env.NODE_ENV === 'development') && ts("after super.save",1); + if (process.env.NODE_ENV === 'development') { + if (DEBUGGING) { + debug(self.onmessage, `ExcalidrawView.save, super.save finished`, view.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 = view.file.path; + //@ts-ignore + const data = view.lastSavedData; + window.setTimeout(()=>imageCache.addBAKToCache(path,data),50); + const triggerReload = (view.lastSaveTimestamp === view.file.stat.mtime) && + !preventReload && forcesave; + view.lastSaveTimestamp = view.file.stat.mtime; + //view.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) { + view.setPreventReload(); + } + + postMessage({ success: true, triggerReload }); + } catch (error) { + postMessage({ success: false, error: (error as Error).message }); + } +}; + + +/* ExcalidrawView + +import SaveWorker from "web-worker:./workers/save-worker.ts"; + +//... + + try { + if (allowSave) { + const worker = new SaveWorker({name: "Excalidraw File Save Worker"}); + const promise = new Promise<{success:boolean, error?:Error, triggerReload?: boolean}>((resolve, reject) => { + worker.onmessage = (e: MessageEvent<{ success: boolean; error?: Error; triggerReload?: boolean }>) => { + resolve(e.data); + }; + worker.onerror = (e: ErrorEvent) => { + reject(new Error(e.message)); + }; + worker.postMessage({ + superSave: super.save.bind(this), + view: this, + preventReload, + forcesave, + }); + }); + const { success, error, triggerReload: tr } = await promise; + worker.terminate(); + if(error) { + throw error; + } + if(typeof tr !== "undefined") { + triggerReload = tr; + } + } + + // !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(); + } + +*/ \ No newline at end of file diff --git a/tsconfig.dev.json b/tsconfig.dev.json index 48b9bdb..ca6b33c 100644 --- a/tsconfig.dev.json +++ b/tsconfig.dev.json @@ -2,7 +2,7 @@ "compilerOptions": { "baseUrl": ".", "sourceMap": false, - "module": "ES2015", + "module": "es2015", "target": "es2018", //es2017 because script engine requires for async execution "allowJs": true, "noImplicitAny": true, diff --git a/tsconfig.json b/tsconfig.json index ccf4076..54b4f2a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,7 @@ "dom", "scripthost", "es2015", - "ESNext", + "esnext", "DOM.Iterable" ], "jsx": "react",