Files
obsidian-excalidraw-plugin/src/ExcalidrawView.ts
Zsolt Viczian 2e36d83abc 1.2.11
2021-08-02 20:34:56 +02:00

816 lines
32 KiB
TypeScript

import {
TextFileView,
WorkspaceLeaf,
normalizePath,
TFile,
WorkspaceItem,
Notice,
Menu,
TAbstractFile,
} from "obsidian";
import * as React from "react";
import * as ReactDOM from "react-dom";
import Excalidraw, {exportToSvg, getSceneVersion, loadLibraryFromBlob} from "@zsviczian/excalidraw";
import { ExcalidrawElement,ExcalidrawTextElement } from "@zsviczian/excalidraw/types/element/types";
import {
AppState,
LibraryItems
} from "@zsviczian/excalidraw/types/types";
import {
VIEW_TYPE_EXCALIDRAW,
ICON_NAME,
EXCALIDRAW_LIB_HEADER,
VIRGIL_FONT,
CASCADIA_FONT,
DISK_ICON_NAME,
PNG_ICON_NAME,
SVG_ICON_NAME,
FRONTMATTER_KEY,
TEXT_DISPLAY_RAW_ICON_NAME,
TEXT_DISPLAY_PARSED_ICON_NAME,
EXIT_FULLSCREEN_ICON_NAME,
FULLSCREEN_ICON_NAME,
JSON_parse,
nanoid
} from './constants';
import ExcalidrawPlugin from './main';
import {ExcalidrawAutomate} from './ExcalidrawAutomate';
import { t } from "./lang/helpers";
import { ExcalidrawData, REG_LINK_BACKETS } from "./ExcalidrawData";
import { checkAndCreateFolder, download, getNewUniqueFilepath, splitFolderAndFilename } from "./Utils";
import { Prompt } from "./Prompt";
declare let window: ExcalidrawAutomate;
export enum TextMode {
parsed,
raw
}
interface WorkspaceItemExt extends WorkspaceItem {
containerEl: HTMLElement;
}
export interface ExportSettings {
withBackground: boolean,
withTheme: boolean
}
const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;
const REG_LINKINDEX_INVALIDCHARS = /[<>:"\\|?*]/g;
export default class ExcalidrawView extends TextFileView {
private excalidrawData: ExcalidrawData;
private getScene: Function = null;
private getSelectedText: Function = null;
private getSelectedId: Function = null;
public addText:Function = null;
private refresh: Function = null;
private excalidrawRef: React.MutableRefObject<any> = null;
private justLoaded: boolean = false;
private plugin: ExcalidrawPlugin;
private dirty: string = null;
public autosaveTimer: any = null;
public autosaving:boolean = false;
public textMode:TextMode = TextMode.raw;
private textIsParsed_Element:HTMLElement;
private textIsRaw_Element:HTMLElement;
private gotoFullscreen:HTMLElement;
private exitFullscreen:HTMLElement;
private preventReload:boolean = true;
public compatibilityMode: boolean = false;
id: string = (this.leaf as any).id;
constructor(leaf: WorkspaceLeaf, plugin: ExcalidrawPlugin) {
super(leaf);
this.plugin = plugin;
this.excalidrawData = new ExcalidrawData(plugin);
}
public saveExcalidraw(scene?: any){
if(!scene) {
if (!this.getScene) return false;
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));
else this.app.vault.create(filepath,JSON.stringify(scene));
}
public async saveSVG(scene?: any) {
if(!scene) {
if (!this.getScene) return false;
scene = this.getScene();
}
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf(this.compatibilityMode ? '.excalidraw':'.md')) + '.svg';
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
const exportSettings: ExportSettings = {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: this.plugin.settings.exportWithTheme
}
const svg = await ExcalidrawView.getSVG(scene,exportSettings);
if(!svg) return;
const svgString = ExcalidrawView.embedFontsInSVG(svg).outerHTML;
if(file && file instanceof TFile) await this.app.vault.modify(file,svgString);
else await this.app.vault.create(filepath,svgString);
}
public static embedFontsInSVG(svg:SVGSVGElement):SVGSVGElement {
//replace font references with base64 fonts
const includesVirgil = svg.querySelector("text[font-family^='Virgil']") != null;
const includesCascadia = svg.querySelector("text[font-family^='Cascadia']") != null;
const defs = svg.querySelector("defs");
if (defs && (includesCascadia || includesVirgil)) {
defs.innerHTML = "<style>" + (includesVirgil ? VIRGIL_FONT : "") + (includesCascadia ? CASCADIA_FONT : "")+"</style>";
}
return svg;
}
public async savePNG(scene?: any) {
if(!scene) {
if (!this.getScene) return false;
scene = this.getScene();
}
const exportSettings: ExportSettings = {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: this.plugin.settings.exportWithTheme
}
const png = await ExcalidrawView.getPNG(scene,exportSettings,this.plugin.settings.pngExportScale);
if(!png) return;
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf(this.compatibilityMode ? '.excalidraw':'.md')) + '.png';
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
if(file && file instanceof TFile) await this.app.vault.modifyBinary(file,await png.arrayBuffer());
else await this.app.vault.createBinary(filepath,await png.arrayBuffer());
}
async save(preventReload:boolean=true) {
this.preventReload = preventReload;
this.dirty = null;
await super.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
getViewData () {
//console.log("ExcalidrawView.getViewData()");
if(!this.getScene) return this.data;
if(!this.compatibilityMode) {
if(this.excalidrawData.syncElements(this.getScene()) && !this.autosaving) {
this.loadDrawing(false);
}
let trimLocation = this.data.search("# Text Elements\n");
if(trimLocation == -1) trimLocation = this.data.search("# Drawing\n");
if(trimLocation == -1) return this.data;
const scene = this.excalidrawData.scene;
if(!this.autosaving) {
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
if(this.plugin.settings.autoexportExcalidraw) this.saveExcalidraw(scene);
}
const header = this.data.substring(0,trimLocation)
.replace(/excalidraw-plugin:\s.*\n/,FRONTMATTER_KEY+": " + ( (this.textMode == TextMode.raw) ? "raw\n" : "parsed\n"));
return header + this.excalidrawData.generateMD();
}
if(this.compatibilityMode) {
this.excalidrawData.syncElements(this.getScene());
const scene = this.excalidrawData.scene;
if(!this.autosaving) {
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
}
return JSON.stringify(scene);
}
return this.data;
}
handleLinkClick(view: ExcalidrawView, ev:MouseEvent) {
let text:string = (this.textMode == TextMode.parsed)
? this.excalidrawData.getRawText(this.getSelectedId())
: this.getSelectedText();
if(!text) {
new Notice(t("LINK_BUTTON_CLICK_NO_TEXT"),20000);
return;
}
if(text.match(REG_LINKINDEX_HYPERLINK)) {
window.open(text,"_blank");
return; }
//![[link|alias]]![alias](link)
//1 2 3 4 5 6
const parts = text.matchAll(REG_LINK_BACKETS).next();
if(!parts.value) {
const tags = text.matchAll(/#([\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+)/ug).next();
if(!tags.value || tags.value.length<2) {
new Notice(t("TEXT_ELEMENT_EMPTY"),4000);
return;
}
const search=this.app.workspace.getLeavesOfType("search");
if(search.length==0) return;
//@ts-ignore
search[0].view.setQuery("tag:"+tags.value[1]);
this.app.workspace.revealLeaf(search[0]);
if(this.gotoFullscreen.style.display=="none") this.toggleFullscreen();
return;
}
text = parts.value[2] ? parts.value[2]:parts.value[6];
if(text.match(REG_LINKINDEX_HYPERLINK)) {
window.open(text,"_blank");
return;
}
if(text.search("#")>-1) text = text.substring(0,text.search("#"));
if(text.match(REG_LINKINDEX_INVALIDCHARS)) {
new Notice(t("FILENAME_INVALID_CHARS"),4000);
return;
}
if (!ev.altKey) {
const file = view.app.metadataCache.getFirstLinkpathDest(text,view.file.path);
if (!file) {
new Notice(t("FILE_DOES_NOT_EXIST"), 4000);
return;
}
}
try {
const f = view.file;
if(ev.shiftKey && this.gotoFullscreen.style.display=="none") this.toggleFullscreen();
view.app.workspace.openLinkText(text,view.file.path,ev.shiftKey);
} catch (e) {
new Notice(e,4000);
}
}
onload() {
//console.log("ExcalidrawView.onload()");
this.addAction(DISK_ICON_NAME,t("FORCE_SAVE"),async (ev)=> {
await this.save(false);
this.plugin.triggerEmbedUpdates();
});
this.textIsRaw_Element = this.addAction(TEXT_DISPLAY_RAW_ICON_NAME,t("RAW"), (ev) => this.changeTextMode(TextMode.parsed));
this.textIsParsed_Element = this.addAction(TEXT_DISPLAY_PARSED_ICON_NAME,t("PARSED"), (ev) => this.changeTextMode(TextMode.raw));
this.addAction("link",t("OPEN_LINK"), (ev)=>this.handleLinkClick(this,ev));
this.gotoFullscreen = this.addAction(FULLSCREEN_ICON_NAME,"",()=>this.toggleFullscreen());
this.exitFullscreen = this.addAction(EXIT_FULLSCREEN_ICON_NAME,"",()=>this.toggleFullscreen());
this.exitFullscreen.hide();
//@ts-ignore
if(this.app.isMobile) this.gotoFullscreen.hide();
//this is to solve sliding panes bug
if (this.app.workspace.layoutReady) {
(this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt).containerEl.addEventListener('scroll',(e)=>{if(this.refresh) this.refresh();});
} else {
this.app.workspace.onLayoutReady(
async () => (this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt).containerEl.addEventListener('scroll',(e)=>{if(this.refresh) this.refresh();})
);
}
this.setupAutosaveTimer();
}
private toggleFullscreen() {
//@ts-ignore
if(this.app.isMobile) return;
if(this.exitFullscreen.style.display=="none") {
this.containerEl.requestFullscreen();
this.gotoFullscreen.hide();
this.exitFullscreen.show();
} else {
document.exitFullscreen();
this.gotoFullscreen.show();
this.exitFullscreen.hide();
}
this.zoomToFit();
}
public async changeTextMode(textMode:TextMode,reload:boolean=true) {
this.textMode = textMode;
if(textMode == TextMode.parsed) {
this.textIsRaw_Element.hide();
this.textIsParsed_Element.show();
} else {
this.textIsRaw_Element.show();
this.textIsParsed_Element.hide();
}
if(reload) {
await this.save(false);
this.excalidrawRef.current.history.clear(); //to avoid undo replacing links with parsed text
}
}
public setupAutosaveTimer() {
const timer = async () => {
if(this.dirty && (this.dirty == this.file?.path)) {
this.dirty = null;
this.autosaving=true;
if(this.excalidrawRef) await this.save();
this.autosaving=false;
}
}
if(this.autosaveTimer) clearInterval(this.autosaveTimer); // clear previous timer if one exists
//if(this.plugin.settings.autosave) {
this.autosaveTimer = setInterval(timer,20000);
//}
}
//save current drawing when user closes workspace leaf
async onunload() {
//console.log("ExcalidrawView.onunload()");
if(this.autosaveTimer) {
clearInterval(this.autosaveTimer);
this.autosaveTimer = null;
}
//if(this.excalidrawRef) await this.save();
}
public async reload(fullreload:boolean = false, file?:TFile){
//console.log("ExcalidrawView.reload(), fullreload",fullreload,"preventReload",this.preventReload);
if(this.preventReload) {
this.preventReload = false;
return;
}
if(!this.excalidrawRef) return;
if(!this.file) return;
if(file) this.data = await this.app.vault.read(file);
if(fullreload) await this.excalidrawData.loadData(this.data, this.file,this.textMode);
else await this.excalidrawData.setTextMode(this.textMode);
this.loadDrawing(false);
this.dirty = null;
}
// clear the view content
clear() {
}
async setViewData (data: string, clear: boolean = false) {
this.app.workspace.onLayoutReady(async ()=>{
//console.log("ExcalidrawView.setViewData()");
this.dirty = null;
this.compatibilityMode = this.file.extension == "excalidraw";
await this.plugin.loadSettings();
this.plugin.opencount++;
if(this.compatibilityMode) {
this.textIsRaw_Element.hide();
this.textIsParsed_Element.hide();
await this.excalidrawData.loadLegacyData(data,this.file);
if (!this.plugin.settings.compatibilityMode) {
new Notice(t("COMPATIBILITY_MODE"),4000);
}
} else {
const parsed = data.search("excalidraw-plugin: parsed\n")>-1 || data.search("excalidraw-plugin: locked\n")>-1; //locked for backward compatibility
this.changeTextMode(parsed ? TextMode.parsed : TextMode.raw,false);
if(!(await this.excalidrawData.loadData(data, this.file,this.textMode))) return;
}
if(clear) this.clear();
this.loadDrawing(true)
});
}
private loadDrawing(justloaded:boolean) {
//console.log("ExcalidrawView.loadDrawing, justloaded", justloaded);
this.justLoaded = justloaded; //a flag to trigger zoom to fit after the drawing has been loaded
const excalidrawData = this.excalidrawData.scene;
if(this.excalidrawRef) {
if(justloaded) {
this.excalidrawRef.current.resetScene();
this.excalidrawRef.current.history.clear();
this.justLoaded = justloaded; //reset screen will clear justLoaded, so need to set it again
}
this.excalidrawRef.current.updateScene({
elements: excalidrawData.elements,
appState: excalidrawData.appState,
commitToHistory: true,
});
} else {
(async() => {
this.instantiateExcalidraw({
elements: excalidrawData.elements,
appState: excalidrawData.appState,
libraryItems: await this.getLibrary(),
});
})();
}
}
//Compatibility mode with .excalidraw files
canAcceptExtension(extension: string) {
return extension == "excalidraw";
}
// gets the title of the document
getDisplayText() {
if(this.file) return this.file.basename;
else return t("NOFILE");
}
// the view type name
getViewType() {
return VIEW_TYPE_EXCALIDRAW;
}
// icon for the view
getIcon() {
return ICON_NAME;
}
onMoreOptionsMenu(menu: Menu) {
// 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(async () => {
this.plugin.excalidrawFileModes[this.id || this.file.path] = "markdown";
this.plugin.setMarkdownView(this.leaf);
});
})
.addItem((item) => {
item
.setTitle(t("EXPORT_EXCALIDRAW"))
.setIcon(ICON_NAME)
.onClick( async (ev) => {
if(!this.getScene || !this.file) return;
//@ts-ignore
if(this.app.isMobile) {
const prompt = new Prompt(this.app, "Please provide filename",this.file.basename,'filename, leave blank to cancel action');
prompt.openAndGetValue( async (filename:string)=> {
if(!filename) return;
filename = filename + ".excalidraw";
const folderpath = splitFolderAndFilename(this.file.path).folderpath;
await checkAndCreateFolder(this.app.vault,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()));
new Notice("Exported to " + fname,6000);
});
return;
}
download('data:text/plain;charset=utf-8',encodeURIComponent(JSON.stringify(this.getScene())), this.file.basename+'.excalidraw');
});
});
} else {
menu
.addItem((item) => {
item
.setTitle(t("CONVERT_FILE"))
.onClick(async () => {
await this.save();
this.plugin.openDrawing(await this.plugin.convertSingleExcalidrawToMD(this.file),false);
});
});
}
menu
.addItem((item) => {
item
.setTitle(t("SAVE_AS_PNG"))
.setIcon(PNG_ICON_NAME)
.onClick( async (ev)=> {
if(!this.getScene || !this.file) return;
if(ev.ctrlKey || ev.metaKey) {
const exportSettings: ExportSettings = {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: this.plugin.settings.exportWithTheme
}
const png = await ExcalidrawView.getPNG(this.getScene(),exportSettings,this.plugin.settings.pngExportScale);
if(!png) return;
let reader = new FileReader();
reader.readAsDataURL(png);
const self = this;
reader.onloadend = function() {
let base64data = reader.result;
download(null,base64data,self.file.basename+'.png');
}
return;
}
this.savePNG();
});
})
.addItem((item) => {
item
.setTitle(t("SAVE_AS_SVG"))
.setIcon(SVG_ICON_NAME)
.onClick(async (ev)=> {
if(!this.getScene || !this.file) return;
if(ev.ctrlKey || ev.metaKey) {
const exportSettings: ExportSettings = {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: this.plugin.settings.exportWithTheme
}
let svg = await ExcalidrawView.getSVG(this.getScene(),exportSettings);
if(!svg) return null;
svg = ExcalidrawView.embedFontsInSVG(svg);
download("data:image/svg+xml;base64",btoa(unescape(encodeURIComponent(svg.outerHTML))),this.file.basename+'.svg');
return;
}
this.saveSVG()
});
})
.addSeparator();
super.onMoreOptionsMenu(menu);
}
async getLibrary() {
const data = JSON_parse(this.plugin.getStencilLibrary());
return data?.library ? data.library : [];
}
private instantiateExcalidraw(initdata: any) {
//console.log("ExcalidrawView.instantiateExcalidraw()");
this.dirty = null;
const reactElement = React.createElement(() => {
let previousSceneVersion = 0;
let currentPosition = {x:0, y:0};
const excalidrawRef = React.useRef(null);
const excalidrawWrapperRef = React.useRef(null);
const [dimensions, setDimensions] = React.useState({
width: undefined,
height: undefined
});
this.excalidrawRef = excalidrawRef;
React.useEffect(() => {
setDimensions({
width: this.contentEl.clientWidth,
height: this.contentEl.clientHeight,
});
const onResize = () => {
try {
setDimensions({
width: this.contentEl.clientWidth,
height: this.contentEl.clientHeight,
});
} catch(err) {console.log ("Excalidraw React-Wrapper, onResize ",err)}
};
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, [excalidrawWrapperRef]);
this.getSelectedId = ():string => {
if(!excalidrawRef?.current) return null;
const selectedElement = excalidrawRef.current.getSceneElements().filter((el:any)=>el.id==Object.keys(excalidrawRef.current.getAppState().selectedElementIds)[0]);
if(selectedElement.length==0) return null;
if(selectedElement[0].type == "text") return selectedElement[0].id; //a text element was selected. Retrun text
if(selectedElement[0].groupIds.length == 0) return 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 = excalidrawRef
.current
.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 null; //the group had no text element member
return textElement[0].id; //return text element text
};
this.getSelectedText = (textonly:boolean=false):string => {
if(!excalidrawRef?.current) return null;
const selectedElement = excalidrawRef.current.getSceneElements().filter((el:any)=>el.id==Object.keys(excalidrawRef.current.getAppState().selectedElementIds)[0]);
if(selectedElement.length==0) return null;
if(selectedElement[0].type == "text") return selectedElement[0].text; //a text element was selected. Retrun text
if(textonly) return null;
if(selectedElement[0].groupIds.length == 0) return 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 = excalidrawRef
.current
.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 null; //the group had no text element member
return textElement[0].text; //return text element text
};
this.addText = (text:string, fontFamily?:1|2|3) => {
if(!excalidrawRef?.current) {
return;
}
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
const st: AppState = excalidrawRef.current.getAppState();
const id = nanoid();
window.ExcalidrawAutomate.reset();
window.ExcalidrawAutomate.style.strokeColor = st.currentItemStrokeColor;
window.ExcalidrawAutomate.style.opacity = st.currentItemOpacity;
window.ExcalidrawAutomate.style.fontFamily = fontFamily ? fontFamily: st.currentItemFontFamily;
window.ExcalidrawAutomate.style.fontSize = st.currentItemFontSize;
window.ExcalidrawAutomate.style.textAlign = st.currentItemTextAlign;
const addText = (text:string) => {
window.ExcalidrawAutomate.addText(currentPosition.x, currentPosition.y, text,null,id);
//@ts-ignore
const textElement = window.ExcalidrawAutomate.elementsDict[id];
el.push(textElement);
excalidrawRef.current.updateScene({
elements: el,
appState: st,
});
this.save(false);
}
const self = this;
//setTextElement will attempt a quick parse (without processing transclusions)
const parseResult = this.excalidrawData.setTextElement(id, text,async (parsedText:string)=>{
addText(self.textMode==TextMode.parsed?parsedText:text);
});
if(parseResult) { //there were no transclusions in the raw text, quick parse was successful
addText(self.textMode==TextMode.parsed?parseResult:text);
}
}
this.getScene = () => {
if(!excalidrawRef?.current) {
return null;
}
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
const st: AppState = excalidrawRef.current.getAppState();
return {
type: "excalidraw",
version: 2,
source: "https://excalidraw.com",
elements: el,
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,
currentItemStrokeSharpness: st.currentItemStrokeSharpness,
currentItemStartArrowhead: st.currentItemStartArrowhead,
currentItemEndArrowhead: st.currentItemEndArrowhead,
currentItemLinearStrokeSharpness: st.currentItemLinearStrokeSharpness,
gridSize: st.gridSize,
}
};
};
this.refresh = () => {
if(!excalidrawRef?.current) return;
excalidrawRef.current.refresh();
};
return React.createElement(
React.Fragment,
null,
React.createElement(
"div",
{
className: "excalidraw-wrapper",
ref: excalidrawWrapperRef,
key: "abc",
onClick: (e:MouseEvent):any => {
//@ts-ignore
if(!(e.ctrlKey||e.metaKey)) return;
if(!(this.plugin.settings.allowCtrlClick)) return;
if(!this.getSelectedId()) return;
this.handleLinkClick(this,e);
}
},
React.createElement(Excalidraw.default, {
ref: excalidrawRef,
width: dimensions.width,
height: dimensions.height,
UIOptions: {
canvasActions: {
loadScene: false,
saveScene: false,
saveAsScene: false,
export: { saveFileToDisk: false },
saveAsImage: false,
saveToActiveFile: false,
},
},
initialData: initdata,
detectScroll: true,
onPointerUpdate: (p:any) => {
currentPosition = p.pointer;
},
onChange: (et:ExcalidrawElement[],st:AppState) => {
if(this.justLoaded) {
this.justLoaded = false;
this.zoomToFit();
previousSceneVersion = getSceneVersion(et);
return;
}
if (st.editingElement == null && st.resizingElement == null &&
st.draggingElement == null && st.editingGroupId == null &&
st.editingLinearElement == null ) {
const sceneVersion = getSceneVersion(et);
if(sceneVersion != previousSceneVersion) {
previousSceneVersion = sceneVersion;
this.dirty=this.file?.path;
}
}
},
onLibraryChange: (items:LibraryItems) => {
(async () => {
this.plugin.setStencilLibrary(EXCALIDRAW_LIB_HEADER+JSON.stringify(items)+'}');
await this.plugin.saveSettings();
})();
},
/*onPaste: (data: ClipboardData, event: ClipboardEvent | null) => {
console.log(data,event);
return true;
},*/
onBeforeTextEdit: (textElement: ExcalidrawTextElement) => {
if(this.autosaveTimer) { //stopping autosave to avoid autosave overwriting text while the user edits it
clearInterval(this.autosaveTimer);
this.autosaveTimer = null;
}
if(this.textMode==TextMode.parsed) return this.excalidrawData.getRawText(textElement.id);
return null;
},
onBeforeTextSubmit: (textElement: ExcalidrawTextElement, text:string, isDeleted:boolean) => {
if(isDeleted) {
this.excalidrawData.deleteTextElement(textElement.id);
this.dirty=this.file?.path;
this.setupAutosaveTimer();
return;
}
//If the parsed text is different than the raw text, and if View is in TextMode.parsed
//Then I need to clear the undo history to avoid overwriting raw text with parsed text and losing links
if(text!=textElement.text) { //the user made changes to the text
//setTextElement will attempt a quick parse (without processing transclusions)
const parseResult = this.excalidrawData.setTextElement(textElement.id, text,async ()=>{
await this.save(false);
//this callback function will only be invoked if quick parse fails, i.e. there is a transclusion in the raw text
//thus I only check if TextMode.parsed, text is always != with parseResult
if(this.textMode == TextMode.parsed) this.excalidrawRef.current.history.clear();
this.setupAutosaveTimer();
});
if(parseResult) { //there were no transclusions in the raw text, quick parse was successful
this.setupAutosaveTimer();
if(this.textMode == TextMode.raw) return; //text is displayed in raw, no need to clear the history, undo will not create problems
if(text == parseResult) return; //There were no links to parse, raw text and parsed text are equivalent
this.excalidrawRef.current.history.clear();
return parseResult;
}
return;
}
this.setupAutosaveTimer();
if(this.textMode==TextMode.parsed) return this.excalidrawData.getParsedText(textElement.id);
}
})
)
);
});
ReactDOM.render(reactElement,(this as any).contentEl);
}
private zoomToFit() {
const el = this.containerEl;
setTimeout(()=>{
const e = new KeyboardEvent("keydown", {bubbles : true, cancelable : true, shiftKey : true, code:"Digit1"});
el.querySelector("canvas")?.dispatchEvent(e);
},400)
}
public static async getSVG(scene:any, exportSettings:ExportSettings):Promise<SVGSVGElement> {
try {
return exportToSvg({
elements: scene.elements,
appState: {
exportBackground: exportSettings.withBackground,
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
... scene.appState,},
exportPadding:10,
});
} catch (error) {
return null;
}
}
public static async getPNG(scene:any, exportSettings:ExportSettings, scale:number = 1) {
try {
return await Excalidraw.exportToBlob({
elements: scene.elements,
appState: {
exportBackground: exportSettings.withBackground,
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
... scene.appState,},
mimeType: "image/png",
exportWithDarkMode: "true",
metadata: "Generated by Excalidraw-Obsidian plugin",
getDimensions: (width:number, height:number) => ({ width:width*scale, height:height*scale, scale:scale })
});
} catch (error) {
return null;
}
}
}