mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd476b564a | ||
|
|
b7a7c9473e | ||
|
|
cc7dc16810 | ||
|
|
c45faef141 | ||
|
|
ae31ad0870 | ||
|
|
ba88ced2ba | ||
|
|
803fb9e234 | ||
|
|
d77249088f |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "1.2.1",
|
||||
"version": "1.2.7",
|
||||
"minAppVersion": "0.11.13",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "@excalidraw/excalidraw/types/element/types";
|
||||
import {
|
||||
normalizePath,
|
||||
Notice,
|
||||
TFile
|
||||
} from "obsidian"
|
||||
import ExcalidrawView from "./ExcalidrawView";
|
||||
@@ -516,5 +517,4 @@ async function getTemplate(fileWithPath: string):Promise<{elements: any,appState
|
||||
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'json\n'
|
||||
+ data + '\n'
|
||||
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96);
|
||||
//+data.replaceAll("[","[");
|
||||
}
|
||||
@@ -18,11 +18,11 @@ const DRAWING_REG = /\n# Drawing\n(```json\n)?(.*)(```)?/gm;
|
||||
export const REG_LINK_BACKETS = /(!)?\[\[([^|\]]+)\|?(.+)?]]|(!)?\[(.*)\]\((.*)\)/g;
|
||||
|
||||
export function getJSON(data:string):string {
|
||||
const findJSON = DRAWING_REG;
|
||||
const res = data.matchAll(findJSON);
|
||||
const res = data.matchAll(DRAWING_REG);
|
||||
const parts = res.next();
|
||||
if(parts.value && parts.value.length>1) {
|
||||
return parts.value[2];
|
||||
const result = parts.value[2];
|
||||
return result.substr(0,result.lastIndexOf("}")+1); //this is a workaround in case sync merges two files together and one version is still an old version without the ```codeblock
|
||||
}
|
||||
return data;
|
||||
}
|
||||
@@ -72,7 +72,8 @@ export class ExcalidrawData {
|
||||
let parts = data.matchAll(DRAWING_REG).next();
|
||||
if(!(parts.value && parts.value.length>1)) return false; //JSON not found or invalid
|
||||
if(!this.scene) { //scene was not loaded from .excalidraw
|
||||
this.scene = JSON_parse(parts.value[2]);
|
||||
const scene = parts.value[2];
|
||||
this.scene = JSON_parse(scene.substr(0,scene.lastIndexOf("}")+1)); //this is a workaround to address when files are mereged by sync and one version is still an old markdown without the codeblock ```
|
||||
}
|
||||
//Trim data to remove the JSON string
|
||||
data = data.substring(0,parts.value.index);
|
||||
@@ -354,4 +355,4 @@ export class ExcalidrawData {
|
||||
return showLinkBrackets != this.showLinkBrackets;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "obsidian";
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import Excalidraw, {exportToSvg, getSceneVersion} from "@excalidraw/excalidraw";
|
||||
import Excalidraw, {exportToSvg, getSceneVersion, loadLibraryFromBlob} from "@excalidraw/excalidraw";
|
||||
import { ExcalidrawElement } from "@excalidraw/excalidraw/types/element/types";
|
||||
import {
|
||||
AppState,
|
||||
@@ -33,6 +33,7 @@ import ExcalidrawPlugin from './main';
|
||||
import {ExcalidrawAutomate} from './ExcalidrawAutomate';
|
||||
import { t } from "./lang/helpers";
|
||||
import { ExcalidrawData, REG_LINK_BACKETS } from "./ExcalidrawData";
|
||||
import { download } from "./Utils";
|
||||
|
||||
declare let window: ExcalidrawAutomate;
|
||||
|
||||
@@ -58,8 +59,9 @@ export default class ExcalidrawView extends TextFileView {
|
||||
private excalidrawRef: React.MutableRefObject<any> = null;
|
||||
private justLoaded: boolean = false;
|
||||
private plugin: ExcalidrawPlugin;
|
||||
private dirty: boolean = false;
|
||||
private dirty: string = null;
|
||||
public autosaveTimer: any = null;
|
||||
public autosaving:boolean = false;
|
||||
public isTextLocked:boolean = false;
|
||||
private lockedElement:HTMLElement;
|
||||
private unlockedElement:HTMLElement;
|
||||
@@ -81,8 +83,8 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
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));//data.replaceAll("[","["));
|
||||
else this.app.vault.create(filepath,JSON.stringify(scene));//.replaceAll("[","["));
|
||||
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) {
|
||||
@@ -133,6 +135,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
async save(preventReload:boolean=true) {
|
||||
this.preventReload = preventReload;
|
||||
this.dirty = null;
|
||||
await super.save();
|
||||
}
|
||||
|
||||
@@ -141,9 +144,9 @@ export default class ExcalidrawView extends TextFileView {
|
||||
// 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 && !this.compatibilityMode) {
|
||||
|
||||
if(this.excalidrawData.syncElements(this.getScene())) {
|
||||
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");
|
||||
@@ -151,22 +154,28 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if(trimLocation == -1) return this.data;
|
||||
|
||||
const scene = this.excalidrawData.scene;
|
||||
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
|
||||
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
|
||||
if(this.plugin.settings.autoexportExcalidraw) this.saveExcalidraw(scene);
|
||||
if(!this.autosaving) {
|
||||
this.autosaving = false;
|
||||
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.isTextLocked ? "locked\n" : "unlocked\n"));
|
||||
return header + this.excalidrawData.generateMD();
|
||||
}
|
||||
if(this.getScene && this.compatibilityMode) {
|
||||
if(this.compatibilityMode) {
|
||||
this.excalidrawData.syncElements(this.getScene());
|
||||
const scene = this.excalidrawData.scene;
|
||||
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
|
||||
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
|
||||
if(!this.autosaving) {
|
||||
this.autosaving = false;
|
||||
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
|
||||
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
|
||||
}
|
||||
return JSON.stringify(scene);
|
||||
}
|
||||
else return this.data;
|
||||
return this.data;
|
||||
}
|
||||
|
||||
handleLinkClick(view: ExcalidrawView, ev:MouseEvent) {
|
||||
@@ -216,16 +225,6 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
}
|
||||
|
||||
download(encoding:string,data:any,filename:string) {
|
||||
let element = document.createElement('a');
|
||||
element.setAttribute('href', (encoding ? encoding + ',' : '') + data);
|
||||
element.setAttribute('download', filename);
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
onload() {
|
||||
//console.log("ExcalidrawView.onload()");
|
||||
this.addAction(DISK_ICON_NAME,t("FORCE_SAVE"),async (ev)=> {
|
||||
@@ -265,16 +264,16 @@ export default class ExcalidrawView extends TextFileView {
|
||||
public setupAutosaveTimer() {
|
||||
const timer = async () => {
|
||||
//console.log("ExcalidrawView.autosaveTimer(), dirty", this.dirty);
|
||||
if(this.dirty) {
|
||||
console.log("autosave",Date.now());
|
||||
this.dirty = false;
|
||||
if(this dirty && (this.dirty == this.file?.path)) {
|
||||
this.dirty = null;
|
||||
this.autosaving=true;
|
||||
if(this.excalidrawRef) await this.save();
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
//this.plugin.triggerEmbedUpdates();
|
||||
}
|
||||
}
|
||||
if(this.plugin.settings.autosave) {
|
||||
this.autosaveTimer = setInterval(timer,30000);
|
||||
}
|
||||
//if(this.plugin.settings.autosave) {
|
||||
this.autosaveTimer = setInterval(timer,30000);
|
||||
//}
|
||||
}
|
||||
|
||||
//save current drawing when user closes workspace leaf
|
||||
@@ -306,9 +305,10 @@ export default class ExcalidrawView extends TextFileView {
|
||||
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";
|
||||
this.plugin.settings.drawingOpenCount++;
|
||||
this.plugin.saveSettings();
|
||||
await this.plugin.loadSettings();
|
||||
this.plugin.opencount++;
|
||||
if(this.compatibilityMode) {
|
||||
this.unlockedElement.hide();
|
||||
this.lockedElement.hide();
|
||||
@@ -322,25 +322,26 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
if(clear) this.clear();
|
||||
this.loadDrawing(true)
|
||||
this.dirty = false;
|
||||
});
|
||||
}
|
||||
|
||||
private loadDrawing (justloaded:boolean) {
|
||||
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.updateScene({
|
||||
elements: excalidrawData.elements,
|
||||
appState: excalidrawData.appState,
|
||||
commitToHistory: false,
|
||||
});
|
||||
if(justloaded) this.excalidrawRef.current.scrollToContent();
|
||||
} else {
|
||||
(async() => {
|
||||
this.instantiateExcalidraw({
|
||||
elements: excalidrawData.elements,
|
||||
appState: excalidrawData.appState,
|
||||
// scrollToContent: true,
|
||||
libraryItems: await this.getLibrary(),
|
||||
});
|
||||
})();
|
||||
@@ -387,7 +388,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
.setIcon(ICON_NAME)
|
||||
.onClick( async (ev) => {
|
||||
if(!this.getScene || !this.file) return;
|
||||
this.download('data:text/plain;charset=utf-8',encodeURIComponent(JSON.stringify(this.getScene())), this.file.basename+'.excalidraw');
|
||||
download('data:text/plain;charset=utf-8',encodeURIComponent(JSON.stringify(this.getScene())), this.file.basename+'.excalidraw');
|
||||
});
|
||||
});
|
||||
} else {
|
||||
@@ -420,7 +421,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
const self = this;
|
||||
reader.onloadend = function() {
|
||||
let base64data = reader.result;
|
||||
self.download(null,base64data,self.file.basename+'.png');
|
||||
download(null,base64data,self.file.basename+'.png');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -441,7 +442,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
let svg = await ExcalidrawView.getSVG(this.getScene(),exportSettings);
|
||||
if(!svg) return null;
|
||||
svg = ExcalidrawView.embedFontsInSVG(svg);
|
||||
this.download("data:image/svg+xml;base64",btoa(unescape(encodeURIComponent(svg.outerHTML))),this.file.basename+'.svg');
|
||||
download("data:image/svg+xml;base64",btoa(unescape(encodeURIComponent(svg.outerHTML))),this.file.basename+'.svg');
|
||||
return;
|
||||
}
|
||||
this.saveSVG()
|
||||
@@ -452,14 +453,14 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
|
||||
async getLibrary() {
|
||||
const data = JSON_parse(this.plugin.settings.library);
|
||||
const data = JSON_parse(this.plugin.getStencilLibrary());
|
||||
return data?.library ? data.library : [];
|
||||
}
|
||||
|
||||
|
||||
private instantiateExcalidraw(initdata: any) {
|
||||
//console.log("ExcalidrawView.instantiateExcalidraw()");
|
||||
this.dirty = false;
|
||||
this.dirty = null;
|
||||
const reactElement = React.createElement(() => {
|
||||
let previousSceneVersion = 0;
|
||||
let currentPosition = {x:0, y:0};
|
||||
@@ -581,9 +582,27 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if(!excalidrawRef?.current) return;
|
||||
excalidrawRef.current.refresh();
|
||||
};
|
||||
|
||||
let timestamp = (new Date()).getTime();
|
||||
|
||||
|
||||
let timestamp = 0;
|
||||
|
||||
const dblclickEvent = (e: Event):boolean => {
|
||||
if((e.target instanceof HTMLCanvasElement) && this.getSelectedText(true)) { //text element is selected
|
||||
const now = (new Date()).getTime();
|
||||
if(now-timestamp < 600) { //double click
|
||||
if(this.isTextLocked) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
new Notice(t("UNLOCK_TO_EDIT"));
|
||||
}
|
||||
timestamp = 0;
|
||||
this.lock(false);
|
||||
return true;
|
||||
}
|
||||
timestamp = now;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return React.createElement(
|
||||
React.Fragment,
|
||||
null,
|
||||
@@ -593,22 +612,15 @@ export default class ExcalidrawView extends TextFileView {
|
||||
className: "excalidraw-wrapper",
|
||||
ref: excalidrawWrapperRef,
|
||||
key: "abc",
|
||||
onTouchEnd: (e: TouchEvent) => {
|
||||
//@ts-ignore
|
||||
if (!this.app.isMobile) return;
|
||||
if (dblclickEvent(e)) return;
|
||||
},
|
||||
onClick: (e:MouseEvent):any => {
|
||||
if(this.isTextLocked && (e.target instanceof HTMLCanvasElement) && this.getSelectedText(true)) { //text element is selected
|
||||
const now = (new Date()).getTime();
|
||||
if(now-timestamp < 600) { //double click
|
||||
let event = new MouseEvent('dblclick', {
|
||||
'view': window,
|
||||
'bubbles': true,
|
||||
'cancelable': true,
|
||||
});
|
||||
e.target.dispatchEvent(event);
|
||||
new Notice(t("UNLOCK_TO_EDIT"));
|
||||
timestamp = now;
|
||||
return;
|
||||
}
|
||||
timestamp = now;
|
||||
}
|
||||
//@ts-ignore
|
||||
if(this.app.isMobile) return;
|
||||
if(this.isTextLocked && dblclickEvent(e)) return;
|
||||
if(!(e.ctrlKey||e.metaKey)) return;
|
||||
if(!(this.plugin.settings.allowCtrlClick)) return;
|
||||
if(!this.getSelectedId()) return;
|
||||
@@ -619,15 +631,9 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if(ev.keyCode!=13) return; //not an enter
|
||||
if(!(ev.target instanceof HTMLDivElement)) return;
|
||||
if(!this.getSelectedId()) return;
|
||||
const event = new MouseEvent('dblclick', {
|
||||
'view': window,
|
||||
'bubbles': true,
|
||||
'cancelable': true,
|
||||
});
|
||||
ev.target.querySelector("canvas").dispatchEvent(event);
|
||||
this.lock(false);
|
||||
new Notice(t("UNLOCK_TO_EDIT"));
|
||||
},
|
||||
|
||||
},
|
||||
React.createElement(Excalidraw.default, {
|
||||
ref: excalidrawRef,
|
||||
@@ -651,23 +657,26 @@ export default class ExcalidrawView extends TextFileView {
|
||||
onChange: (et:ExcalidrawElement[],st:AppState) => {
|
||||
if(this.justLoaded) {
|
||||
this.justLoaded = false;
|
||||
previousSceneVersion = Excalidraw.getSceneVersion(et);
|
||||
const e = new KeyboardEvent("keydown", {bubbles : true, cancelable : true, shiftKey : true, code:"Digit1"});
|
||||
this.contentEl.querySelector("canvas")?.dispatchEvent(e);
|
||||
const el = this.containerEl;
|
||||
setTimeout(()=>{
|
||||
const e = new KeyboardEvent("keydown", {bubbles : true, cancelable : true, shiftKey : true, code:"Digit1"});
|
||||
el.querySelector("canvas")?.dispatchEvent(e);
|
||||
},200)
|
||||
previousSceneVersion = getSceneVersion(et);
|
||||
}
|
||||
if (st.editingElement == null && st.resizingElement == null &&
|
||||
st.draggingElement == null && st.editingGroupId == null &&
|
||||
st.editingLinearElement == null ) {
|
||||
const sceneVersion = Excalidraw.getSceneVersion(et);
|
||||
const sceneVersion = getSceneVersion(et);
|
||||
if(sceneVersion != previousSceneVersion) {
|
||||
previousSceneVersion = sceneVersion;
|
||||
this.dirty=true;
|
||||
this.dirty=this.file?.path;
|
||||
}
|
||||
}
|
||||
},
|
||||
onLibraryChange: (items:LibraryItems) => {
|
||||
(async () => {
|
||||
this.plugin.settings.library = EXCALIDRAW_LIB_HEADER+JSON.stringify(items)+'}';
|
||||
this.plugin.setStencilLibrary(EXCALIDRAW_LIB_HEADER+JSON.stringify(items)+'}');
|
||||
await this.plugin.saveSettings();
|
||||
})();
|
||||
}
|
||||
@@ -687,7 +696,6 @@ export default class ExcalidrawView extends TextFileView {
|
||||
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
|
||||
... scene.appState,},
|
||||
exportPadding:10,
|
||||
//metadata: "Generated by Excalidraw-Obsidian plugin",
|
||||
});
|
||||
} catch (error) {
|
||||
return null;
|
||||
@@ -710,4 +718,4 @@ export default class ExcalidrawView extends TextFileView {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
42
src/Utils.ts
Normal file
42
src/Utils.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { normalizePath, TFolder } from "obsidian";
|
||||
|
||||
/**
|
||||
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
|
||||
* @param filepath
|
||||
*/
|
||||
export function splitFolderAndFilename(filepath: string):{folderpath: string, filename: string} {
|
||||
let folderpath: string, filename:string;
|
||||
const lastIndex = filepath.lastIndexOf("/");
|
||||
return {
|
||||
folderpath: normalizePath(filepath.substr(0,lastIndex)),
|
||||
filename: lastIndex==-1 ? filepath : filepath.substr(lastIndex+1),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Download data as file from Obsidian, to store on local device
|
||||
* @param encoding
|
||||
* @param data
|
||||
* @param filename
|
||||
*/
|
||||
export function download(encoding:string,data:any,filename:string) {
|
||||
let element = document.createElement('a');
|
||||
element.setAttribute('href', (encoding ? encoding + ',' : '') + data);
|
||||
element.setAttribute('download', filename);
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the image filename based on the excalidraw filename
|
||||
* @param excalidrawPath - Full filepath of ExclidrawFile
|
||||
* @param newExtension - extension of IMG file in ".extension" format
|
||||
* @returns
|
||||
*/
|
||||
export function getIMGPathFromExcalidrawFile (excalidrawPath:string,newExtension:string):string {
|
||||
const isLegacyFile:boolean = excalidrawPath.endsWith(".excalidraw");
|
||||
const replaceExtension:string = isLegacyFile ? ".excalidraw" : ".md";
|
||||
return excalidrawPath.substring(0,excalidrawPath.lastIndexOf(replaceExtension)) + newExtension;
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
//This is to avoid brackets littering graph view with links
|
||||
//export function JSON_stringify(x:any):string {return JSON.stringify(x).replaceAll("[","[");}
|
||||
//This is only for backward compatibility because an early version of obsidian included an encoding to avoid fantom links from littering Obsidian graph view
|
||||
export function JSON_parse(x:string):any {return JSON.parse(x.replaceAll("[","["));}
|
||||
|
||||
import {customAlphabet} from "nanoid";
|
||||
@@ -13,7 +12,7 @@ export const MAX_COLORS = 5;
|
||||
export const COLOR_FREQ = 6;
|
||||
export const RERENDER_EVENT = "excalidraw-embed-rerender";
|
||||
export const BLANK_DRAWING = '{"type":"excalidraw","version":2,"source":"https://excalidraw.com","elements":[],"appState":{"gridSize":null,"viewBackgroundColor":"#ffffff"}}';
|
||||
export const FRONTMATTER = ["---","",`${FRONTMATTER_KEY}: unlocked`,"","---", "", ""].join("\n");
|
||||
export const FRONTMATTER = ["---","",`${FRONTMATTER_KEY}: unlocked`,"","---", "==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠==", "",""].join("\n");
|
||||
export const EMPTY_MESSAGE = "Hit enter to create a new drawing";
|
||||
export const LOCK_ICON_NAME = "lock";
|
||||
export const LOCK_ICON = `<path fill="currentColor" stroke="currentColor" d="M35.715 42.855h28.57v-10.71c0-3.946-1.394-7.313-4.183-10.102-2.793-2.79-6.157-4.188-10.102-4.188-3.945 0-7.309 1.399-10.102 4.188-2.789 2.789-4.183 6.156-4.183 10.102zm46.43 5.36v32.14c0 1.489-.524 2.754-1.563 3.797-1.043 1.043-2.309 1.563-3.797 1.563h-53.57c-1.488 0-2.754-.52-3.797-1.563-1.04-1.043-1.563-2.308-1.563-3.797v-32.14c0-1.488.524-2.754 1.563-3.797 1.043-1.04 2.309-1.563 3.797-1.563H25v-10.71c0-6.848 2.457-12.727 7.367-17.637S43.157 7.145 50 7.145c6.844 0 12.723 2.453 17.633 7.363C72.543 19.418 75 25.297 75 32.145v10.71h1.785c1.488 0 2.754.524 3.797 1.563 1.04 1.043 1.563 2.309 1.563 3.797zm0 0"/>`
|
||||
|
||||
@@ -10,6 +10,7 @@ export default {
|
||||
CREATE_NEW : "New Excalidraw drawing",
|
||||
CONVERT_FILE_KEEP_EXT: "*.excalidraw => *.excalidraw.md",
|
||||
CONVERT_FILE_REPLACE_EXT: "*.excalidraw => *.md (Logseq compatibility)",
|
||||
DOWNLOAD_LIBRARY: "Export stencil library as an *.excalidrawlib file",
|
||||
OPEN_EXISTING_NEW_PANE: "Open an existing drawing - IN A NEW PANE",
|
||||
OPEN_EXISTING_ACTIVE_PANE: "Open an existing drawing - IN THE CURRENT ACTIVE PANE",
|
||||
TRANSCLUDE: "Transclude (embed) a drawing",
|
||||
@@ -31,7 +32,7 @@ export default {
|
||||
SAVE_AS_SVG: "Save as SVG into Vault (CTRL/META+CLICK to export)",
|
||||
OPEN_LINK: "Open selected text as link\n(SHIFT+CLICK to open in a new pane)",
|
||||
EXPORT_EXCALIDRAW: "Export to an .Excalidraw file",
|
||||
UNLOCK_TO_EDIT: "UNLOCK Text Elements to edit",
|
||||
UNLOCK_TO_EDIT: "UNLOCKED Text Elements automatically to allow edit",
|
||||
LINK_BUTTON_CLICK_NO_TEXT: 'Select a Text Element containing an internal or external link.\n'+
|
||||
'SHIFT CLICK this button to open the link in a new pane.\n'+
|
||||
'CTRL/META CLICK the Text Element on the canvas has the same effect!',
|
||||
|
||||
83
src/main.ts
83
src/main.ts
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
TFile,
|
||||
TFolder,
|
||||
Plugin,
|
||||
WorkspaceLeaf,
|
||||
addIcon,
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
MarkdownRenderer,
|
||||
ViewState,
|
||||
Notice,
|
||||
TFolder,
|
||||
} from "obsidian";
|
||||
|
||||
import {
|
||||
@@ -58,17 +58,20 @@ import { Prompt } from "./Prompt";
|
||||
import { around } from "monkey-around";
|
||||
import { t } from "./lang/helpers";
|
||||
import { MigrationPrompt } from "./MigrationPrompt";
|
||||
import { download, getIMGPathFromExcalidrawFile, splitFolderAndFilename } from "./Utils";
|
||||
|
||||
export default class ExcalidrawPlugin extends Plugin {
|
||||
public excalidrawFileModes: { [file: string]: string } = {};
|
||||
private _loaded: boolean = false;
|
||||
public settings: ExcalidrawSettings;
|
||||
//public stencilLibrary: any = null;
|
||||
private openDialog: OpenFileDialog;
|
||||
private activeExcalidrawView: ExcalidrawView = null;
|
||||
public lastActiveExcalidrawFilePath: string = null;
|
||||
private hover: {linkText: string, sourcePath: string} = {linkText: null, sourcePath: null};
|
||||
private observer: MutationObserver;
|
||||
private fileExplorerObserver: MutationObserver;
|
||||
public opencount:number = 0;
|
||||
|
||||
constructor(app: App, manifest: PluginManifest) {
|
||||
super(app, manifest);
|
||||
@@ -102,7 +105,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
//inspiration taken from kanban:
|
||||
//https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/main.ts#L267
|
||||
this.registerMonkeyPatches();
|
||||
if(this.settings.loadCount<3) this.migrationNotice();
|
||||
if(this.settings.loadCount<1) this.migrationNotice();
|
||||
|
||||
}
|
||||
|
||||
@@ -110,7 +113,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
const self = this;
|
||||
this.app.workspace.onLayoutReady(async () => {
|
||||
self.settings.loadCount++;
|
||||
self.saveSettings();
|
||||
//self.saveSettings();
|
||||
const files = this.app.vault.getFiles().filter((f)=>f.extension=="excalidraw");
|
||||
if(files.length>0) {
|
||||
const prompt = new MigrationPrompt(self.app, self);
|
||||
@@ -156,7 +159,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
|
||||
if(parts.fheight) img.setAttribute("height",parts.fheight);
|
||||
img.addClass(parts.style);
|
||||
img.setAttribute("src","data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.outerHTML))));
|
||||
img.setAttribute("src","data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.outerHTML.replaceAll(" "," ")))));
|
||||
return img;
|
||||
}
|
||||
|
||||
@@ -239,7 +242,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
//@ts-ignore
|
||||
this.app.workspace.on('hover-link',hoverEvent)
|
||||
);
|
||||
|
||||
|
||||
//monitoring for div.popover.hover-popover.file-embed.is-loaded to be added to the DOM tree
|
||||
this.observer = new MutationObserver((m)=>{
|
||||
if(m.length == 0) return;
|
||||
@@ -411,6 +414,14 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
this.app.workspace.on("file-menu", fileMenuHandlerConvertReplaceExtension)
|
||||
);
|
||||
|
||||
this.addCommand({
|
||||
id: "excalidraw-download-lib",
|
||||
name: t("DOWNLOAD_LIBRARY"),
|
||||
callback: () => {
|
||||
download('data:text/plain;charset=utf-8',encodeURIComponent(this.settings.library), 'my-obsidian-library.excalidrawlib');
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "excalidraw-open",
|
||||
name: t("OPEN_EXISTING_NEW_PANE"),
|
||||
@@ -505,10 +516,6 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return this.app.workspace.activeLeaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW;
|
||||
/* if(this.app.workspace.activeLeaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW) {
|
||||
return !(this.app.workspace.activeLeaf.view as ExcalidrawView).compatibilityMode;
|
||||
}
|
||||
return false;*/
|
||||
} else {
|
||||
const view = this.app.workspace.activeLeaf.view;
|
||||
if (view instanceof ExcalidrawView) {
|
||||
@@ -671,6 +678,16 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
const fname = this.getNewUniqueFilepath(filename,normalizePath(file.path.substr(0,file.path.lastIndexOf(file.name))));
|
||||
console.log(fname);
|
||||
const result = await this.app.vault.create(fname,FRONTMATTER + exportSceneToMD(data));
|
||||
if (this.settings.keepInSync) {
|
||||
['.svg','.png'].forEach( (ext:string)=>{
|
||||
const oldIMGpath = file.path.substring(0,file.path.lastIndexOf(".excalidraw")) + ext;
|
||||
const imgFile = this.app.vault.getAbstractFileByPath(normalizePath(oldIMGpath));
|
||||
if(imgFile && imgFile instanceof TFile) {
|
||||
const newIMGpath = fname.substr(0,fname.lastIndexOf(".md")) + ext;
|
||||
this.app.vault.rename(imgFile,newIMGpath);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!keepOriginal) this.app.vault.delete(file);
|
||||
return result;
|
||||
}
|
||||
@@ -778,16 +795,18 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
const self = this;
|
||||
this.app.workspace.onLayoutReady(async () => {
|
||||
|
||||
|
||||
|
||||
//watch filename change to rename .svg, .png; to sync to .md; to update links
|
||||
const renameEventHandler = async (file:TAbstractFile,oldPath:string) => {
|
||||
if(!(file instanceof TFile)) return;
|
||||
if (!self.isExcalidrawFile(file)) return;
|
||||
if (!self.settings.keepInSync) return;
|
||||
if(!self.isExcalidrawFile(file)) return;
|
||||
if(!self.settings.keepInSync) return;
|
||||
['.svg','.png','.excalidraw'].forEach(async (ext:string)=>{
|
||||
const oldIMGpath = oldPath.substring(0,oldPath.lastIndexOf('.md')) + ext;
|
||||
const oldIMGpath = getIMGPathFromExcalidrawFile(oldPath,ext);
|
||||
const imgFile = self.app.vault.getAbstractFileByPath(normalizePath(oldIMGpath));
|
||||
if(imgFile && imgFile instanceof TFile) {
|
||||
const newIMGpath = file.path.substring(0,file.path.lastIndexOf('.md')) + ext;
|
||||
const newIMGpath = getIMGPathFromExcalidrawFile(file.path,ext);
|
||||
await self.app.vault.rename(imgFile,newIMGpath);
|
||||
}
|
||||
});
|
||||
@@ -816,7 +835,9 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
const deleteEventHandler = async (file:TFile) => {
|
||||
if (!(file instanceof TFile)) return;
|
||||
//@ts-ignore
|
||||
if (file.unsaveCachedData && !file.unsafeCachedData.search(/---\n[\s\S]*excalidraw-plugin:\s*(locked|unlocked)\n[\s\S]*---/gm)==-1) return;
|
||||
const isExcalidarwFile = (file.unsafeCachedData && file.unsafeCachedData.search(/---\n[\s\S]*excalidraw-plugin:\s*(locked|unlocked)\n[\s\S]*---/gm)>-1)
|
||||
|| (file.extension=="excalidraw");
|
||||
if(!isExcalidarwFile) return;
|
||||
|
||||
//close excalidraw view where this file is open
|
||||
const leaves = self.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
@@ -829,7 +850,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
//delete PNG and SVG files as well
|
||||
if (self.settings.keepInSync) {
|
||||
['.svg','.png','.excalidraw'].forEach(async (ext:string) => {
|
||||
const imgPath = file.path.substring(0,file.path.lastIndexOf('.md')) + ext;
|
||||
const imgPath = getIMGPathFromExcalidrawFile(file.path,ext);
|
||||
const imgFile = self.app.vault.getAbstractFileByPath(normalizePath(imgPath));
|
||||
if(imgFile && imgFile instanceof TFile) {
|
||||
await self.app.vault.delete(imgFile);
|
||||
@@ -847,6 +868,9 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
for (let i=0;i<leaves.length;i++) {
|
||||
(leaves[i].view as ExcalidrawView).save();
|
||||
}
|
||||
this.settings.drawingOpenCount += this.opencount;
|
||||
this.settings.loadCount++;
|
||||
this.saveSettings();
|
||||
}
|
||||
self.registerEvent(
|
||||
self.app.workspace.on("quit",quitEventHandler)
|
||||
@@ -881,7 +905,9 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
excalidrawLeaves.forEach((leaf) => {
|
||||
this.setMarkdownView(leaf);
|
||||
});
|
||||
|
||||
this.settings.drawingOpenCount += this.opencount;
|
||||
this.settings.loadCount++;
|
||||
this.saveSettings();
|
||||
}
|
||||
|
||||
public embedDrawing(data:string) {
|
||||
@@ -894,7 +920,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
|
||||
}
|
||||
|
||||
private async loadSettings() {
|
||||
public async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
@@ -902,6 +928,14 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
public getStencilLibrary():string {
|
||||
return this.settings.library;
|
||||
}
|
||||
|
||||
public setStencilLibrary(library:string) {
|
||||
this.settings.library = library;
|
||||
}
|
||||
|
||||
public triggerEmbedUpdates(filepath?:string){
|
||||
const e = document.createEvent("Event")
|
||||
e.initEvent(RERENDER_EVENT,true,false);
|
||||
@@ -961,10 +995,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
|
||||
public async createDrawing(filename: string, onNewPane: boolean, foldername?: string, initData?:string) {
|
||||
const folderpath = normalizePath(foldername ? foldername: this.settings.folder);
|
||||
const folder = this.app.vault.getAbstractFileByPath(folderpath);
|
||||
if (!(folder && folder instanceof TFolder)) {
|
||||
await this.app.vault.createFolder(folderpath);
|
||||
}
|
||||
await this.checkAndCreateFolder(folderpath); //create folder if it does not exist
|
||||
|
||||
const fname = this.getNewUniqueFilepath(filename,folderpath);
|
||||
|
||||
@@ -1018,4 +1049,14 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
const fileCache = this.app.metadataCache.getFileCache(f);
|
||||
return !!fileCache?.frontmatter && !!fileCache.frontmatter[FRONTMATTER_KEY];
|
||||
}
|
||||
|
||||
/**
|
||||
* Open or create a folderpath if it does not exist
|
||||
* @param folderpath
|
||||
*/
|
||||
public async checkAndCreateFolder(folderpath:string) {
|
||||
let folder = this.app.vault.getAbstractFileByPath(folderpath);
|
||||
if(folder && folder instanceof TFolder) return;
|
||||
await this.app.vault.createFolder(folderpath);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import {
|
||||
App,
|
||||
PluginSettingTab,
|
||||
Setting
|
||||
Setting,
|
||||
TFile
|
||||
} from 'obsidian';
|
||||
import { VIEW_TYPE_EXCALIDRAW } from './constants';
|
||||
import ExcalidrawView from './ExcalidrawView';
|
||||
import { t } from './lang/helpers';
|
||||
import type ExcalidrawPlugin from "./main";
|
||||
import { splitFolderAndFilename } from './Utils';
|
||||
|
||||
export interface ExcalidrawSettings {
|
||||
folder: string,
|
||||
@@ -16,7 +18,7 @@ export interface ExcalidrawSettings {
|
||||
width: string,
|
||||
showLinkBrackets: boolean,
|
||||
linkPrefix: string,
|
||||
autosave: boolean;
|
||||
//autosave: boolean;
|
||||
allowCtrlClick: boolean, //if disabled only the link button in the view header will open links
|
||||
exportWithTheme: boolean,
|
||||
exportWithBackground: boolean,
|
||||
@@ -25,12 +27,12 @@ export interface ExcalidrawSettings {
|
||||
autoexportPNG: boolean,
|
||||
autoexportExcalidraw: boolean,
|
||||
syncExcalidraw: boolean,
|
||||
library: string,
|
||||
compatibilityMode: boolean,
|
||||
experimentalFileType: boolean,
|
||||
experimentalFileTag: string,
|
||||
loadCount: number, //version 1.2 migration counter
|
||||
drawingOpenCount: number,
|
||||
library: string,
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
@@ -39,9 +41,9 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
drawingFilenamePrefix: 'Drawing ',
|
||||
drawingFilenameDateTime: 'YYYY-MM-DD HH.mm.ss',
|
||||
width: '400',
|
||||
linkPrefix: ">> ",
|
||||
linkPrefix: "🔸",
|
||||
showLinkBrackets: true,
|
||||
autosave: false,
|
||||
//autosave: false,
|
||||
allowCtrlClick: true,
|
||||
exportWithTheme: true,
|
||||
exportWithBackground: true,
|
||||
@@ -50,12 +52,12 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
autoexportPNG: false,
|
||||
autoexportExcalidraw: false,
|
||||
syncExcalidraw: false,
|
||||
library: `{"type":"excalidrawlib","version":1,"library":[]}`,
|
||||
experimentalFileType: false,
|
||||
experimentalFileTag: "✏️",
|
||||
compatibilityMode: false,
|
||||
loadCount: 0,
|
||||
drawingOpenCount: 0,
|
||||
library: `{"type":"excalidrawlib","version":1,"library":[]}`,
|
||||
}
|
||||
|
||||
export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
@@ -92,7 +94,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
/* new Setting(containerEl)
|
||||
.setName(t("AUTOSAVE_NAME"))
|
||||
.setDesc(t("AUTOSAVE_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
@@ -112,7 +114,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}));*/
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("FILENAME_HEAD")});
|
||||
containerEl.createDiv('',(el) => {
|
||||
@@ -185,7 +187,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.setName(t("LINK_PREFIX_NAME"))
|
||||
.setDesc(t("LINK_PREFIX_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder('>> ')
|
||||
.setPlaceholder('🔸')
|
||||
.setValue(this.plugin.settings.linkPrefix)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.linkPrefix = value;
|
||||
@@ -271,7 +273,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
this.plugin.settings.autoexportPNG = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("COMPATIBILITY_HEAD")});
|
||||
|
||||
new Setting(containerEl)
|
||||
|
||||
@@ -63,7 +63,7 @@ button.ToolIcon_type_button[title="Export"] {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
@font-face {
|
||||
font-family: "Virgil";
|
||||
src: url("https://excalidraw.com/Virgil.woff2");
|
||||
@@ -71,4 +71,4 @@ button.ToolIcon_type_button[title="Export"] {
|
||||
@font-face {
|
||||
font-family: "Cascadia";
|
||||
src: url("https://excalidraw.com/Cascadia.woff2");
|
||||
}
|
||||
}*/
|
||||
|
||||
Reference in New Issue
Block a user