mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
replace json.stringify with proper processing, fix small issues with Ephemral state, added worker (inactive)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -7,7 +7,6 @@ import { getEA } from "src";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
|
||||
export class ImportSVGDialog extends FuzzySuggestModal<TFile> {
|
||||
public app: App;
|
||||
public plugin: ExcalidrawPlugin;
|
||||
private view: ExcalidrawView;
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
|
||||
import { t } from "../lang/helpers";
|
||||
|
||||
export class InsertCommandDialog extends FuzzySuggestModal<TFile> {
|
||||
public app: App;
|
||||
private addText: Function;
|
||||
|
||||
destroy() {
|
||||
|
||||
@@ -7,7 +7,6 @@ import ExcalidrawPlugin from "../main";
|
||||
import { getEA } from "src";
|
||||
|
||||
export class InsertImageDialog extends FuzzySuggestModal<TFile> {
|
||||
public app: App;
|
||||
public plugin: ExcalidrawPlugin;
|
||||
private view: ExcalidrawView;
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import ExcalidrawPlugin from "src/main";
|
||||
import { getLink } from "src/utils/FileUtils";
|
||||
|
||||
export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
|
||||
public app: App;
|
||||
private addText: Function;
|
||||
private drawingPath: string;
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import ExcalidrawPlugin from "../main";
|
||||
import { getEA } from "src";
|
||||
|
||||
export class InsertMDDialog extends FuzzySuggestModal<TFile> {
|
||||
public app: App;
|
||||
public plugin: ExcalidrawPlugin;
|
||||
private view: ExcalidrawView;
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ export enum openDialogAction {
|
||||
}
|
||||
|
||||
export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
||||
public app: App;
|
||||
private plugin: ExcalidrawPlugin;
|
||||
private action: openDialogAction;
|
||||
private onNewPane: boolean;
|
||||
|
||||
4
src/types/worker.d.ts
vendored
Normal file
4
src/types/worker.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module "web-worker:*" {
|
||||
const WorkerFactory: new (options: any) => Worker;
|
||||
export default WorkerFactory;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
45
src/utils/ExcalidrawSceneUtils.ts
Normal file
45
src/utils/ExcalidrawSceneUtils.ts
Normal file
@@ -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<ExcalidrawElement>[]},
|
||||
elementToChange: Mutable<ExcalidrawElement>,
|
||||
newID: string
|
||||
) {
|
||||
if(elementToChange.type === "text") {
|
||||
const textElement = elementToChange as Mutable<ExcalidrawTextElement>;
|
||||
if(textElement.containerId) {
|
||||
const containerEl = sceneElements.find(el=>el.id === textElement.containerId) as unknown as Mutable<ExcalidrawElement>;
|
||||
containerEl.boundElements?.filter(x=>x.id === textElement.id).forEach( x => {
|
||||
(x.id as Mutable<string>) = newID;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if(elementToChange.boundElements?.length>0) {
|
||||
elementToChange.boundElements.forEach( binding => {
|
||||
const boundEl = sceneElements.find(el=>el.id === binding.id) as unknown as Mutable<ExcalidrawElement>;
|
||||
boundEl.boundElements?.filter(x=>x.id === elementToChange.id).forEach( x => {
|
||||
(x.id as Mutable<string>) = newID;
|
||||
});
|
||||
if(boundEl.type === "arrow") {
|
||||
const arrow = boundEl as Mutable<ExcalidrawArrowElement>;
|
||||
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<string>) = newID;
|
||||
});
|
||||
}
|
||||
|
||||
elementToChange.id = newID;
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
153
src/workers/save-worker.ts
Normal file
153
src/workers/save-worker.ts
Normal file
@@ -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<void>;
|
||||
view: ExcalidrawView;
|
||||
forcesave: boolean;
|
||||
preventReload: boolean;
|
||||
}
|
||||
|
||||
onmessage = async function (e: MessageEvent<SaveWorkerMessageData>) {
|
||||
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();
|
||||
}
|
||||
|
||||
*/
|
||||
@@ -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,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"dom",
|
||||
"scripthost",
|
||||
"es2015",
|
||||
"ESNext",
|
||||
"esnext",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"jsx": "react",
|
||||
|
||||
Reference in New Issue
Block a user