mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
11 Commits
1.0.8-test
...
1.0.8-test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d15278e70e | ||
|
|
4be1ff89fe | ||
|
|
c962168c52 | ||
|
|
660f6e03b1 | ||
|
|
a26e565d04 | ||
|
|
0debaace4e | ||
|
|
126086f9f1 | ||
|
|
baf2cdd5d8 | ||
|
|
793302a1f5 | ||
|
|
0d361340c1 | ||
|
|
e192da8668 |
@@ -337,7 +337,7 @@ This example is similar to the first one, but rotated 90°, and using a template
|
||||
### Generating a simple mindmap from a text outline
|
||||
This is a slightly more elaborate example. This will generate an a mindmap from a tabulated outline.
|
||||
|
||||

|
||||

|
||||
|
||||
Example input:
|
||||
```
|
||||
@@ -414,13 +414,13 @@ offsets = [0];
|
||||
for(i=0;i<=linecount;i++) {
|
||||
depth = tree[i][IDX.depth];
|
||||
if (depth == 1) ea.style.strokeColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16);
|
||||
tree[i][IDX.objectId] = ea.addText(depth*width,((tree[i][IDX.size]/2)+offsets[depth])*height,tree[i][IDX.text],{startArrowHead: 'dot',box:true});
|
||||
tree[i][IDX.objectId] = ea.addText(depth*width,((tree[i][IDX.size]/2)+offsets[depth])*height,tree[i][IDX.text],{box:true});
|
||||
//set child offset equal to parent offset
|
||||
if((depth+1)>offsets.length) offsets.push(offsets[depth]);
|
||||
else offsets[depth+1] = offsets[depth];
|
||||
offsets[depth] += tree[i][IDX.size];
|
||||
if(tree[i][IDX.parent]!=-1) {
|
||||
ea.connectObjects(tree[tree[i][IDX.parent]][IDX.objectId],"right",tree[i][IDX.objectId],"left",);
|
||||
ea.connectObjects(tree[tree[i][IDX.parent]][IDX.objectId],"right",tree[i][IDX.objectId],"left",{startArrowHead: 'dot'});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ div.excalidraw-svg-left {
|
||||
|
||||
|
||||
## Known issues
|
||||
- On mobile (iOS and Android): As you draw left to right it opens left sidebar. Draw right to left, opens right sidebar. Draw down, opens commands palette. So seems open is emulating the gestures, even when drawing towards the center. I understand that the issue will be resolved in the next release of Obsidian mobile.
|
||||
- On mobile (iOS and Android): As you draw left to right it opens left sidebar. Draw right to left, opens right sidebar. Draw down, opens commands palette. So seems open is emulating the gestures, even when drawing towards the center. Obsidian mobile 0.18 has resolved this issue.
|
||||
- I have seen two cases when adding a stencil library did not work. In both cases, the end solution was a reinstall of Obsidian. The root cause is not clear, but maybe because of the incremental updates of Obsidian from an early version.
|
||||
- Sync does not support .excalidraw files. This issue will be addressed in a later release of Obsidian sync. Until then, here are two hacks you can play with:
|
||||
- You have the option to use OneDrive, Google Drive, iCloud, DropBox, etc. to sync your vault between devices.
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
WorkspaceLeaf,
|
||||
normalizePath,
|
||||
TFile,
|
||||
Menu,
|
||||
WorkspaceItem
|
||||
} from "obsidian";
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
@@ -26,13 +26,18 @@ import {
|
||||
} from './constants';
|
||||
import ExcalidrawPlugin from './main';
|
||||
|
||||
interface WorkspaceItemExt extends WorkspaceItem {
|
||||
containerEl: HTMLElement;
|
||||
}
|
||||
|
||||
export interface ExportSettings {
|
||||
withBackground: boolean,
|
||||
withTheme: boolean
|
||||
}
|
||||
|
||||
export default class ExcalidrawView extends TextFileView {
|
||||
private getScene: any;
|
||||
private getScene: Function;
|
||||
private refresh: Function;
|
||||
private excalidrawRef: React.MutableRefObject<any>;
|
||||
private justLoaded: boolean;
|
||||
private plugin: ExcalidrawPlugin;
|
||||
@@ -40,6 +45,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
constructor(leaf: WorkspaceLeaf, plugin: ExcalidrawPlugin) {
|
||||
super(leaf);
|
||||
this.getScene = null;
|
||||
this.refresh = null;
|
||||
this.excalidrawRef = null;
|
||||
this.plugin = plugin;
|
||||
this.justLoaded = false;
|
||||
@@ -97,20 +103,26 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
else return this.data;
|
||||
}
|
||||
|
||||
|
||||
async onload() {
|
||||
this.addAction(DISK_ICON_NAME,"Save drawing",async (ev)=> {
|
||||
this.addAction(DISK_ICON_NAME,"Force-save now to update transclusion visible in adjacent workspace pane\n(Please note, that autosave is always on)",async (ev)=> {
|
||||
await this.save();
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
});
|
||||
this.addAction(PNG_ICON_NAME,"Export as PNG",async (ev)=>this.savePNG());
|
||||
this.addAction(SVG_ICON_NAME,"Export as SVG",async (ev)=>this.saveSVG());
|
||||
if (this.app.workspace.layoutReady) {
|
||||
(this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt).containerEl.addEventListener('scroll',(e)=>{if(this.refresh) this.refresh();});
|
||||
} else {
|
||||
this.registerEvent(this.app.workspace.on('layout-ready', async () => (this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt).containerEl.addEventListener('scroll',(e)=>{if(this.refresh) this.refresh();})));
|
||||
}
|
||||
}
|
||||
|
||||
//save current drawing when user closes workspace leaf
|
||||
async onunload() {
|
||||
if(this.excalidrawRef) await this.save();
|
||||
}
|
||||
|
||||
|
||||
setViewData (data: string, clear: boolean) {
|
||||
if (this.app.workspace.layoutReady) {
|
||||
this.loadDrawing(data,clear);
|
||||
@@ -124,6 +136,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if(this.excalidrawRef) {
|
||||
this.excalidrawRef = null;
|
||||
this.getScene = null;
|
||||
this.refresh = null;
|
||||
ReactDOM.unmountComponentAtNode(this.contentEl);
|
||||
}
|
||||
}
|
||||
@@ -182,7 +195,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
width: undefined,
|
||||
height: undefined
|
||||
});
|
||||
|
||||
|
||||
this.excalidrawRef = excalidrawRef;
|
||||
React.useEffect(() => {
|
||||
setDimensions({
|
||||
@@ -196,6 +209,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
width: this.contentEl.clientWidth,
|
||||
height: this.contentEl.clientHeight,
|
||||
});
|
||||
console.log("Excalidraw resize",this.contentEl.clientWidth,this.contentEl.clientHeight);
|
||||
} catch(err) {console.log ("Excalidraw React-Wrapper, onResize ",err)}
|
||||
};
|
||||
window.addEventListener("resize", onResize);
|
||||
@@ -219,6 +233,11 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.refresh = () => {
|
||||
if(!excalidrawRef?.current) return;
|
||||
excalidrawRef.current.refresh();
|
||||
};
|
||||
|
||||
return React.createElement(
|
||||
React.Fragment,
|
||||
@@ -232,7 +251,6 @@ export default class ExcalidrawView extends TextFileView {
|
||||
},
|
||||
React.createElement(Excalidraw.default, {
|
||||
ref: excalidrawRef,
|
||||
key: "xyz",
|
||||
width: dimensions.width,
|
||||
height: dimensions.height,
|
||||
UIOptions: {
|
||||
@@ -244,6 +262,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
},
|
||||
},
|
||||
initialData: initdata,
|
||||
detectScroll: true,
|
||||
onChange: (et:ExcalidrawElement[],st:AppState) => {
|
||||
if(this.justLoaded) {
|
||||
this.justLoaded = false;
|
||||
|
||||
117
src/TransclusionIndex.ts
Normal file
117
src/TransclusionIndex.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import {Vault,TFile,TAbstractFile} from 'obsidian';
|
||||
|
||||
export default class TransclusionIndex {
|
||||
private vault: Vault;
|
||||
private doc2ex: Map<string, Set<string>>;
|
||||
private ex2doc: Map<string, Set<string>>;
|
||||
|
||||
constructor(vault: Vault) {
|
||||
this.vault = vault;
|
||||
this.doc2ex = new Map<string,Set<string>>(); //markdown document includes these excalidraw drawings
|
||||
this.ex2doc = new Map<string,Set<string>>(); //excalidraw drawings are referenced in these markdown documents
|
||||
}
|
||||
|
||||
async reloadIndex() {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
const doc2ex = new Map<string,Set<string>>();
|
||||
const ex2doc = new Map<string,Set<string>>();
|
||||
|
||||
const markdownFiles = this.vault.getMarkdownFiles();
|
||||
for (const file of markdownFiles) {
|
||||
const drawings = await this.parseTransclusionsInFile(file);
|
||||
if (drawings.size > 0) {
|
||||
doc2ex.set(file.path, drawings);
|
||||
drawings.forEach((drawing)=>{
|
||||
if(ex2doc.has(drawing)) ex2doc.set(drawing,ex2doc.get(drawing).add(file.path));
|
||||
else ex2doc.set(drawing,(new Set<string>()).add(file.path));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.doc2ex = doc2ex;
|
||||
this.ex2doc = ex2doc;
|
||||
this.registerEventHandlers();
|
||||
}
|
||||
|
||||
private updateMarkdownFile(file:TFile,oldExPath:string,newExPath:string) {
|
||||
const fileContents = this.vault.read(file);
|
||||
fileContents.then((c: string) => this.vault.modify(file, c.split("[["+oldExPath).join("[["+newExPath)));
|
||||
const exlist = this.doc2ex.get(file.path);
|
||||
exlist.delete(oldExPath);
|
||||
exlist.add(newExPath);
|
||||
this.doc2ex.set(file.path,exlist);
|
||||
}
|
||||
|
||||
public updateTransclusion(oldExPath: string, newExPath: string): void {
|
||||
if(!this.ex2doc.has(oldExPath)) return; //drawing is not transcluded in any markdown document
|
||||
for(const filePath of this.ex2doc.get(oldExPath)) {
|
||||
this.updateMarkdownFile(this.vault.getAbstractFileByPath(filePath) as TFile,oldExPath,newExPath);
|
||||
}
|
||||
this.ex2doc.set(newExPath, this.ex2doc.get(oldExPath));
|
||||
this.ex2doc.delete(oldExPath);
|
||||
}
|
||||
|
||||
private indexAbstractFile(file: TAbstractFile) {
|
||||
if (!(file instanceof TFile)) return;
|
||||
if (file.extension.toLowerCase() != "md") return; //not a markdown document
|
||||
this.indexFile(file as TFile);
|
||||
}
|
||||
|
||||
private indexFile(file: TFile) {
|
||||
this.clearIndex(file.path);
|
||||
this.parseTransclusionsInFile(file).then((drawings) => {
|
||||
if(drawings.size == 0) return;
|
||||
this.doc2ex.set(file.path, drawings);
|
||||
drawings.forEach((drawing)=>{
|
||||
if(this.ex2doc.has(drawing)) {
|
||||
this.ex2doc.set(drawing,this.ex2doc.get(drawing).add(file.path));
|
||||
}
|
||||
else this.ex2doc.set(drawing,(new Set<string>()).add(file.path));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private clearIndex(path: string) {
|
||||
if(!this.doc2ex.get(path)) return;
|
||||
this.doc2ex.get(path).forEach((ex)=> {
|
||||
const files = this.ex2doc.get(ex);
|
||||
files.delete(path);
|
||||
if(files.size>0) this.ex2doc.set(ex,files);
|
||||
else this.ex2doc.delete(ex);
|
||||
});
|
||||
this.doc2ex.delete(path);
|
||||
}
|
||||
|
||||
private async parseTransclusionsInFile(file: TFile): Promise<Set<string>> {
|
||||
const fileContents = await this.vault.cachedRead(file);
|
||||
const pattern = new RegExp('('+String.fromCharCode(96,96,96)+'excalidraw\\s+.*\\[{2})([^|\\]]*).*\\]{2}[\\s]+'+String.fromCharCode(96,96,96),'gm');
|
||||
const transclusions = new Set<string>();
|
||||
for(const transclusion of [...fileContents.matchAll(pattern)]) {
|
||||
if(transclusion[2] && transclusion[2].endsWith('.excalidraw'))
|
||||
transclusions.add(transclusion[2]);
|
||||
}
|
||||
return transclusions;
|
||||
}
|
||||
|
||||
private registerEventHandlers() {
|
||||
this.vault.on('create', (file: TAbstractFile) => {
|
||||
this.indexAbstractFile(file);
|
||||
});
|
||||
this.vault.on('modify', (file: TAbstractFile) => {
|
||||
this.indexAbstractFile(file);
|
||||
});
|
||||
this.vault.on('delete', (file: TAbstractFile) => {
|
||||
this.clearIndex(file.path);
|
||||
});
|
||||
// We could simply change the references to the old path, but parsing again does the trick as well
|
||||
this.vault.on('rename', (file: TAbstractFile, oldPath: string) => {
|
||||
this.clearIndex(oldPath);
|
||||
this.indexAbstractFile(file);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
20
src/main.ts
20
src/main.ts
@@ -43,7 +43,7 @@ import {
|
||||
initExcalidrawAutomate,
|
||||
destroyExcalidrawAutomate
|
||||
} from './ExcalidrawTemplate';
|
||||
import { norm } from '@excalidraw/excalidraw/types/ga';
|
||||
import TransclusionIndex from './TransclusionIndex';
|
||||
|
||||
export interface ExcalidrawAutomate extends Window {
|
||||
ExcalidrawAutomate: {
|
||||
@@ -55,7 +55,7 @@ export interface ExcalidrawAutomate extends Window {
|
||||
export default class ExcalidrawPlugin extends Plugin {
|
||||
public settings: ExcalidrawSettings;
|
||||
private openDialog: OpenFileDialog;
|
||||
private excalidrawAutomate: ExcalidrawAutomate;
|
||||
private transclusionIndex: TransclusionIndex;
|
||||
|
||||
constructor(app: App, manifest: PluginManifest) {
|
||||
super(app, manifest);
|
||||
@@ -185,7 +185,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
item.setTitle("Create Excalidraw drawing")
|
||||
.setIcon(ICON_NAME)
|
||||
.onClick(evt => {
|
||||
this.createDrawing(file.path+this.getNextDefaultFilename(),false,file.path);
|
||||
this.createDrawing(this.getNextDefaultFilename(),false,file.path);
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -194,6 +194,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
|
||||
//watch filename change to rename .svg
|
||||
this.app.vault.on('rename',async (file,oldPath) => {
|
||||
this.transclusionIndex.updateTransclusion(oldPath,file.path);
|
||||
if (!(this.settings.keepInSync && file instanceof TFile)) return;
|
||||
if (file.extension != EXCALIDRAW_FILE_EXTENSION) return;
|
||||
const oldSVGpath = oldPath.substring(0,oldPath.lastIndexOf('.'+EXCALIDRAW_FILE_EXTENSION)) + '.svg';
|
||||
@@ -228,6 +229,17 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//save open drawings when user quits the application
|
||||
this.app.workspace.on('quit',(tasks) => {
|
||||
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
for (let i=0;i<leaves.length;i++) {
|
||||
(leaves[i].view as ExcalidrawView).save();
|
||||
}
|
||||
});
|
||||
|
||||
this.transclusionIndex = new TransclusionIndex(this.app.vault);
|
||||
this.transclusionIndex.initialize();
|
||||
}
|
||||
|
||||
onunload() {
|
||||
@@ -319,7 +331,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
.forEach((el) => el.dispatchEvent(e));
|
||||
}
|
||||
|
||||
public async openDrawing(drawingFile: TFile, onNewPane: boolean) {
|
||||
public openDrawing(drawingFile: TFile, onNewPane: boolean) {
|
||||
const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
let leaf:WorkspaceLeaf = null;
|
||||
|
||||
|
||||
@@ -37,4 +37,8 @@ div.excalidraw-svg-right {
|
||||
|
||||
div.excalidraw-svg-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
button.ToolIcon_type_button[title="Export"] {
|
||||
display:none;
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
"dom",
|
||||
"es5",
|
||||
"scripthost",
|
||||
"es2015",
|
||||
"es2020",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"jsx": "react",
|
||||
|
||||
Reference in New Issue
Block a user