Compare commits

..

6 Commits

Author SHA1 Message Date
zsviczian
8d2c064ee3 Update README1.2.md 2021-07-10 14:31:03 +02:00
Zsolt Viczian
b8a95392f1 readme 1.2 - draft 2021-07-10 14:20:20 +02:00
zsviczian
5430bc4b38 Update README1.2.md 2021-07-10 13:20:30 +02:00
Zsolt Viczian
c9d12c7295 readme 1.2 2021-07-10 12:26:44 +02:00
Zsolt Viczian
c37cbd7e31 improved hover preivew, experimental file tagging 2021-07-10 11:59:18 +02:00
Zsolt Viczian
fbc342189b getScene() returns JSON object not string 2021-07-09 20:28:06 +02:00
9 changed files with 391 additions and 99 deletions

82
README1.2.md Normal file
View File

@@ -0,0 +1,82 @@
The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/), a feature rich sketching tool, into Obsidian. You can store and edit Excalidraw files in your vault, you can embed drawings into your documents, and you can link to documents and other drawings to/and from Excalidraw. For a showcase of Excalidraw features, please read my blog post [here](https://www.zsolt.blog/2021/03/showcasing-excalidraw.html) and/or watch the videos below.
![image](https://user-images.githubusercontent.com/14358394/125159831-336d6880-e17a-11eb-8a3d-ceabc2555a08.png)
# Important notice to the 1.2.0 update
This version comes with tons of new features and possibilities.
Drawings you've created with version 1.1.x need to be converted to take advantage of the new features. If you want, you can also continue to use your exisiting drawings in compatibility mode (e.g. if you use Logseq and Obsidian in parallel). During conversion your existing `*.excalidraw` files will be replaced with new `*.excalidraw.md` files.
## Conversion and compatibility
To convert files you have the following options:
- Click `CONVERT FILES` in the migration dialog when installing 1.2.0
- In the Command Palette select `Excalidraw: Convert *.excalidraw files to *.excalidraw.md files` to convert all `*.excalidraw` files to `*.excalidraw.md` files.
- To convert files individually:
- Right click an `*.excalidraw` file in File Explorer and select one of the following options:
- `*.excalidraw => *.excalidraw.md`
- `*.excalidraw => *.md (Logseq compatibility)`: This option will retain the original *.excalidraw file next to the new Obsidian format. Make sure you also enable additional `Compatibility features` in `Settings` for a full solution.
- Open a legacy `*.excalidraw` file and select `Convert to new format` from the `Options Menu` in the Excalidraw view.
# Video walkthrough
| | | |
|----|----|----|
|[![Obsidian-Excalidraw 1.2.0 update - Major IMPROVEMENTS](https://user-images.githubusercontent.com/14358394/124356817-7b3f3d80-dc18-11eb-932d-363bb373c5ab.jpg)](https://youtu.be/UxJLLYtgDKE)|[![1 Getting Started](https://user-images.githubusercontent.com/14358394/125160304-7f211180-e17c-11eb-8363-c52723de1ffd.jpg)](https://youtu.be/sY4FoflGaiM)|[![2 Basic shapes and features](https://user-images.githubusercontent.com/14358394/125160312-8a743d00-e17c-11eb-9fa2-490ef4cbd59e.jpg)](https://youtu.be/Iy_oVTq12Gw)|
|[![3 Groups](https://user-images.githubusercontent.com/14358394/125160323-96f89580-e17c-11eb-9bce-8eb1067a51bb.jpg)](https://youtu.be/QOL1KF7-kdc)|[![4 Stencil](https://user-images.githubusercontent.com/14358394/125160332-9f50d080-e17c-11eb-98e9-fec60fe147d9.jpg)](https://youtu.be/aSgcbfspvfo)|[![5 embedding](https://user-images.githubusercontent.com/14358394/125160341-a546b180-e17c-11eb-9de8-d87fdc844c9c.jpg)](https://youtu.be/MaJ5jJwBRWs)|
|[![6 Links](https://user-images.githubusercontent.com/14358394/125160346-aa0b6580-e17c-11eb-930b-4024807040d1.jpg)](https://youtu.be/MXzeCOEExNo)|[![7 Markdown](https://user-images.githubusercontent.com/14358394/125160354-b2fc3700-e17c-11eb-81af-9e71e461f6dd.jpg)](https://youtu.be/R0IAg0s-wQE)|[![8 Templates](https://user-images.githubusercontent.com/14358394/125160360-b8f21800-e17c-11eb-8bd8-79d4e3f6e92d.jpg)](https://youtu.be/ibdS7ykwpW4)|
|[![9 Excalidraw Automate](https://user-images.githubusercontent.com/14358394/125160367-bdb6cc00-e17c-11eb-92f1-6f59faea85fd.jpg)](https://youtu.be/VRZVujfVab0)|[![10 Miscellaneous](https://user-images.githubusercontent.com/14358394/125160374-c3141680-e17c-11eb-8cc2-dfaffd903d15.jpg)](https://youtu.be/D1iBYo1_jjc)||
# Key features
- The plugin aims to integrate Excalidraw seemlessly into Obsidian including Command Palette actions, File Explorer features, Option Menu commands, and the Ribbon Button.
- CTRL+Click on the ribbon button, or in the file explorer to create / open drawings in a new pane.
- Settings will allow you to customzie Excalidraw to your needs:
- Default folder for new drawings and define custom filename pattern for new drawings.
- Template for new drawings. The template will restore stroke properties. This means you can set up defaults in your template for stroke color, stroke width, opacity, font family, font size, fill style, stroke style, etc. This also applies to ExcalidrawAutomate.
- If portability is important to you: Auto-export SVG and/or PNG files including keep-in-sync feature so you can embed svg/png into your documents instead of embedding excalidraw files.
- Specify the default width of embedded drawings.
- Compatibility features to auto-export and keep in sync markdown excalidraw files and legacy .excalidraw files.
- Experimental feature to add custom TAG to file expolorer to mark drawing files.
- Enable / disable autosave.
- You can customize the size and position of the embedded images using the `[[image.excalidraw|100]]`, `[[image.excalidraw|100x100]]`, `[[image.excalidraw|100|left]]`, `[[image.excalidraw|right-wrap]]`, formatting options. `[[<filename.excalidraw>|<width>x<height>|<alignment>]]`. You can add your custom alignment via css. Any text that appears in `<alignment>` will be added to the rendered SVG element style and to the wrapper DIV element. Check below and styles.css for more insight.
- Supports hyperlinks e.g. `https://zsolt.blog`, `[Obsidian](https://obsidian.md)`, and internal links e.g. `[[My file in vault|Alias]]` in drawing text.
- Links will update when files are moved or renamed, if you have the Obsidian setting Files & Links/Automatically Update Internal Links enalbled.
- Links in drawings will show up in backlinks of documents
- Transclusions are supported i.e. `![[myfile#^blockref]]` will convert in the drawing into the transcluded text
- For convenience you can also use the command palette to insert links into drawings
- CTRL/META + CLICK a text element to open it as a link.
- CTRL/META + ALT + CLICK to create the file (if it does not yet exist) and open it
- CTRL/META + SHIFT + CLICK to open the file in a new pane
- CTRL/META + ALT + SHIFT + CLICK to create the file (if it does not yet exist) and open it in a new pane
- Using the block reference you can also reference & transclude text that appears on drawings, in other documents
- Insert LaTex symbols and simple formulas using the Command Palette action "Insert LaTeX-symbol". Some symbols may not display properly using the "Hand-drawn" font. If that is the case try using the "Normal" or "Code" fonts.
- Since 1.2.0 Drawing files are stored in Markdown files
- You can add tags to drawings
- You can add metadata to the YAML front matter of drawings
- Anything you add between the frontmatter and the `# Text Elements` heading will be ignored by Excalidraw, i.e. you can add whatever you like here, it will be preserved as part of the document.
- Excalidraw documents now show in graph view.
- Includes full [Templater](https://silentvoid13.github.io/Templater/) and [Dataview](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/) support through ExcalidrawAutomate. Check out the [detailed help + examples](https://zsviczian.github.io/obsidian-excalidraw-plugin/)
- REQUIRES AN OBSIDIAN SYNC SUBSCRIPTION: Full drawing file history and synchronization between devices
- Multilanguage support: if you'd like to help out by translating the plugin, please get in contact with me.
# Known issues
- Mobile support
- Positioning of the pen gets misaligned after you open the command palette.
- Partially mitigated in 1.0.10 by the introduction of autosave: Your drawing will not be saved when you terminate the mobile app by closing the Obsidian task.
# Tips and tricks
- If you want to sketch in fullscreen, I recommend installing the [Fullscreen Focus Mode](https://github.com/razumihin/obsidian-fullscreen-plugin) plugin.
- [Ozan's Image in Editor Plugin](https://github.com/ozntel/oz-image-in-editor-obsidian). In a nice collaboration with Ozan, his Image-in-Editor plugin now supports Excalidraw. I recommend installing his plugin to display drawings also in Edit mode. Note that Ozan's plugin will only display Excalidraw drawings if the link ends with `.md` or `.excalidraw`. i.e. the following drawing will show in Edit Mode `![[My Drawing.md]]`, but wiki links such as `[[My Drawing]]` will not.
# Feedback, questions, ideas, problems
Join the conversation about the Excalidraw plugin on [forum.obsidian.md](https://forum.obsidian.md/t/excalidraw-full-featured-sketching-plugin-in-obsidian)
Please head over to [GitHub](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues) to report a bug or request an enhancement.
# Say Thank You
If you are enjoying Excalidraw then please support my work and enthusiasm by buying me a coffee on [https://ko-fi/zsolt](https://ko-fi.com/zsolt).
Please also help spread the word by sharing about the Obsidian Excalidraw Plugin on Twitter, Reddit, or any other social media platform you regularly use.
You can find me on Twitter [@zsviczian](https://twitter.com/zsviczian), and on my blog [zsolt.blog](https://zsolt.blog).
[<img style="float:left" src="https://user-images.githubusercontent.com/14358394/115450238-f39e8100-a21b-11eb-89d0-fa4b82cdbce8.png" width="200">](https://ko-fi.com/zsolt)

View File

@@ -212,7 +212,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
elements.push(this.elementsDict[this.elementIds[i]]);
}
return ExcalidrawView.getSVG(
JSON_stringify({
{//createDrawing
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
@@ -221,7 +221,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
"theme": template ? template.appState.theme : this.canvas.theme,
"viewBackgroundColor": template? template.appState.viewBackgroundColor : this.canvas.viewBackgroundColor
}
}),
},//),
{
withBackground: plugin.settings.exportWithBackground,
withTheme: plugin.settings.exportWithTheme
@@ -235,7 +235,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
elements.push(this.elementsDict[this.elementIds[i]]);
}
return ExcalidrawView.getPNG(
JSON_stringify({
{ //JSON_stringify(
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
@@ -244,7 +244,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
"theme": template ? template.appState.theme : this.canvas.theme,
"viewBackgroundColor": template? template.appState.viewBackgroundColor : this.canvas.viewBackgroundColor
}
}),
},//),
{
withBackground: plugin.settings.exportWithBackground,
withTheme: plugin.settings.exportWithTheme

View File

@@ -98,6 +98,17 @@ export class ExcalidrawData {
return true;
}
public async loadLegacyData(data: string,file: TFile):Promise<boolean> {
this.file = file;
this.textElements = new Map<string,{raw:string, parsed:string}>();
this.setShowLinkBrackets();
this.setLinkPrefix();
this.scene = JSON.parse(data);
this.findNewTextElementsInScene();
await this.setAllowParse(false,true);
return true;
}
public async setAllowParse(allowParse:boolean,forceupdate:boolean=false) {
this.allowParse = allowParse;
await this.updateSceneTextElements(forceupdate);
@@ -295,7 +306,7 @@ export class ExcalidrawData {
public syncElements(newScene:any):boolean {
//console.log("Excalidraw.Data.syncElements()");
this.scene = JSON_parse(newScene);
this.scene = newScene;//JSON_parse(newScene);
const result = this.setLinkPrefix() || this.setShowLinkBrackets() || this.findNewTextElementsInScene();
this.updateTextElementsFromSceneRawOnly();
return result;

View File

@@ -65,6 +65,7 @@ export default class ExcalidrawView extends TextFileView {
private lockedElement:HTMLElement;
private unlockedElement:HTMLElement;
private preventReload:boolean = true;
public compatibilityMode: boolean = false;
id: string = (this.leaf as any).id;
@@ -74,29 +75,29 @@ export default class ExcalidrawView extends TextFileView {
this.excalidrawData = new ExcalidrawData(plugin);
}
public saveExcalidraw(data?: string){
if(!data) {
public saveExcalidraw(scene?: any){
if(!scene) {
if (!this.getScene) return false;
data = this.getScene();
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,data.replaceAll("&#91;","["));
else this.app.vault.create(filepath,data.replaceAll("&#91;","["));
if(file && file instanceof TFile) this.app.vault.modify(file,JSON.stringify(scene));//data.replaceAll("&#91;","["));
else this.app.vault.create(filepath,JSON.stringify(scene));//.replaceAll("&#91;","["));
}
public async saveSVG(data?: string) {
if(!data) {
public async saveSVG(scene?: any) {
if(!scene) {
if (!this.getScene) return false;
data = this.getScene();
scene = this.getScene();
}
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf('.md')) + '.svg';
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 = ExcalidrawView.getSVG(data,exportSettings);
const svg = 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);
@@ -114,18 +115,18 @@ export default class ExcalidrawView extends TextFileView {
return svg;
}
public async savePNG(data?: string) {
if(!data) {
public async savePNG(scene?: any) {
if(!scene) {
if (!this.getScene) return false;
data = this.getScene();
scene = this.getScene();
}
const exportSettings: ExportSettings = {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: this.plugin.settings.exportWithTheme
}
const png = await ExcalidrawView.getPNG(data,exportSettings);
const png = await ExcalidrawView.getPNG(scene,exportSettings);
if(!png) return;
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf('.md')) + '.png';
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());
@@ -141,7 +142,7 @@ 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) {
if(this.getScene && !this.compatibilityMode) {
if(this.excalidrawData.syncElements(this.getScene())) {
this.loadDrawing(false);
@@ -150,7 +151,7 @@ export default class ExcalidrawView extends TextFileView {
if(trimLocation == -1) trimLocation = this.data.search("# Drawing\n");
if(trimLocation == -1) return this.data;
const scene = JSON_stringify(this.excalidrawData.scene);
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);
@@ -159,6 +160,13 @@ export default class ExcalidrawView extends TextFileView {
.replace(/excalidraw-plugin:\s.*\n/,FRONTMATTER_KEY+": " + (this.isTextLocked ? "locked\n" : "unlocked\n"));
return header + this.excalidrawData.generateMD();
}
if(this.getScene && 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);
return JSON.stringify(scene);
}
else return this.data;
}
@@ -296,13 +304,23 @@ export default class ExcalidrawView extends TextFileView {
}
async setViewData (data: string, clear: boolean) {
async setViewData (data: string, clear: boolean = false) {
this.app.workspace.onLayoutReady(async ()=>{
//console.log("ExcalidrawView.setViewData()");
this.compatibilityMode = this.file.extension == "excalidraw";
this.plugin.settings.drawingOpenCount++;
this.plugin.saveSettings();
this.lock(data.search("excalidraw-plugin: locked\n")>-1,false);
if(!(await this.excalidrawData.loadData(data, this.file,this.isTextLocked))) return;
if(this.compatibilityMode) {
this.unlockedElement.hide();
this.lockedElement.hide();
await this.excalidrawData.loadLegacyData(data,this.file);
if (!this.plugin.settings.compatibilityMode) {
new Notice(t("COMPATIBILITY_MODE"),4000);
}
} else {
this.lock(data.search("excalidraw-plugin: locked\n")>-1,false);
if(!(await this.excalidrawData.loadData(data, this.file,this.isTextLocked))) return;
}
if(clear) this.clear();
this.loadDrawing(true)
this.dirty = false;
@@ -330,6 +348,10 @@ export default class ExcalidrawView extends TextFileView {
}
}
//Compatibility mode with .excalidraw files
canAcceptExtension(extension: string) {
return extension == "excalidraw";
}
// gets the title of the document
getDisplayText() {
@@ -349,25 +371,38 @@ export default class ExcalidrawView extends TextFileView {
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;
this.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("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;
this.download('data:text/plain;charset=utf-8',encodeURIComponent(this.getScene().replaceAll("&#91;","[")), this.file.basename+'.excalidraw');
});
})
.addItem((item) => {
item
.setTitle(t("SAVE_AS_PNG"))
@@ -516,7 +551,7 @@ export default class ExcalidrawView extends TextFileView {
}
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
const st: AppState = excalidrawRef.current.getAppState();
return JSON_stringify({
return { //JSON_stringify(
type: "excalidraw",
version: 2,
source: "https://excalidraw.com",
@@ -540,7 +575,7 @@ export default class ExcalidrawView extends TextFileView {
currentItemLinearStrokeSharpness: st.currentItemLinearStrokeSharpness,
gridSize: st.gridSize,
}
});
};//);
};
this.refresh = () => {
@@ -618,7 +653,7 @@ export default class ExcalidrawView extends TextFileView {
},
onLibraryChange: (items:LibraryItems) => {
(async () => {
this.plugin.settings.library = EXCALIDRAW_LIB_HEADER+JSON_stringify(items)+'}';
this.plugin.settings.library = EXCALIDRAW_LIB_HEADER+JSON.stringify(items)+'}';
await this.plugin.saveSettings();
})();
}
@@ -629,15 +664,14 @@ export default class ExcalidrawView extends TextFileView {
ReactDOM.render(reactElement,(this as any).contentEl);
}
public static getSVG(data:string, exportSettings:ExportSettings):SVGSVGElement {
public static getSVG(scene:any, exportSettings:ExportSettings):SVGSVGElement {
try {
const excalidrawData = JSON_parse(data);
return exportToSvg({
elements: excalidrawData.elements,
elements: scene.elements,
appState: {
exportBackground: exportSettings.withBackground,
exportWithDarkMode: exportSettings.withTheme ? (excalidrawData.appState?.theme=="light" ? false : true) : false,
... excalidrawData.appState,},
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
... scene.appState,},
exportPadding:10,
metadata: "Generated by Excalidraw-Obsidian plugin",
});
@@ -646,15 +680,14 @@ export default class ExcalidrawView extends TextFileView {
}
}
public static async getPNG(data:string, exportSettings:ExportSettings) {
public static async getPNG(scene:any, exportSettings:ExportSettings) {
try {
const excalidrawData = JSON_parse(data);
return await Excalidraw.exportToBlob({
elements: excalidrawData.elements,
elements: scene.elements,
appState: {
exportBackground: exportSettings.withBackground,
exportWithDarkMode: exportSettings.withTheme ? (excalidrawData.appState?.theme=="light" ? false : true) : false,
... excalidrawData.appState,},
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
... scene.appState,},
mimeType: "image/png",
exportWithDarkMode: "true",
metadata: "Generated by Excalidraw-Obsidian plugin",

View File

@@ -23,19 +23,20 @@ export class MigrationPrompt extends Modal {
const div = this.contentEl.createDiv();
div.addClass("excalidarw-prompt-div");
div.style.maxWidth = "600px";
div.createEl('p',{text: "This version comes with many new features and possibilities. Please read the description in Community Plugins to find out more."});
div.createEl('p',{text: "This version comes with tons of new features and possibilities. Please read the description in Community Plugins to find out more."});
div.createEl('p',{text: ""} , (el) => {
el.innerHTML = "<b>⚠ ATTENTION</b>: Drawings you've created with version 1.1.x need to be converted, they WILL NOT WORK out of the box. "+
el.innerHTML = "Drawings you've created with version 1.1.x need to be converted to take advantage of the new features. You can also continue to use them in compatibility mode. "+
"During conversion your old *.excalidraw files will be replaced with new *.excalidraw.md files.";
});
div.createEl('p',{text: ""}, (el) => {//files manually follow one of two options:
el.innerHTML = "To convert your drawings you have the following options:<br><ul>" +
"<li>Click <code>CONVERT</code> to convert all of your *.excalidraw files now, or if you prefer to make a backup first, then click <code>CANCEL</code>.</li>" +
"<li>Using the Command Palette select <code>Excalidraw: Convert *.excalidraw files to *.excalidraw.md files</code></li>" +
"<li>Right click an *.excalidraw file in File Explorer and select one of the following to convert files individually: <ul>"+
"<li>Click <code>CONVERT FILES</code> now to convert all of your *.excalidraw files, or if you prefer to make a backup first, then click <code>CANCEL</code>.</li>" +
"<li>In the Command Palette select <code>Excalidraw: Convert *.excalidraw files to *.excalidraw.md files</code></li>" +
"<li>Right click an <code>*.excalidraw</code> file in File Explorer and select one of the following options to convert files one by one: <ul>"+
"<li><code>*.excalidraw => *.excalidraw.md</code></li>"+
"<li><code>*.excalidraw => *.md (Logseq compatibility)</code>. This option will retain the original *.excalidraw file next to the new Obsidian format. " +
"Make sure you also enable <code>Compatibility features</code> in Settings for a full solution.</li></ul></li></ul>";
"Make sure you also enable <code>Compatibility features</code> in Settings for a full solution.</li></ul></li>" +
"<li>Open a drawing in compatibility mode and select <code>Convert to new format</code> from the <code>Options Menu</code></li></ul>";
});
div.createEl('p',{text: "This message will only appear maximum 3 times in case you have *.excalidraw files in your Vault."});
const bConvert = div.createEl('button', {text: "CONVERT FILES"});

View File

@@ -42,6 +42,8 @@ export default {
LOCK: "Text Elements are unlocked. Click to LOCK.",
UNLOCK: "Text Elements are locked. Click to UNLOCK.",
NOFILE: "Excalidraw (no file)",
COMPATIBILITY_MODE: "*.excalidraw file opened in compatibility mode. Convert to new format for full plugin functionality.",
CONVERT_FILE: "Convert to new format",
//settings.ts
FOLDER_NAME: "Excalidraw folder",
@@ -49,7 +51,9 @@ export default {
TEMPLATE_NAME: "Excalidraw template file",
TEMPLATE_DESC: "Full filepath to the Excalidraw template. " +
"E.g.: If your template is in the default Excalidraw folder and it's name is " +
"Template.excalidraw, the setting would be: Excalidraw/Template.excalidraw",
"Template.md, the setting would be: Excalidraw/Template.md " +
"If you are using Excalidraw in compatibility mode, then your template must be a legacy excalidraw file as well " +
"such as Excalidraw/Template.excalidraw.",
AUTOSAVE_NAME: "Autosave",
AUTOSAVE_DESC: "Automatically save the active drawing every 30 seconds. Save normally happens when you close Excalidraw or Obsidian, or move "+
"focus to another pane. In rare cases autosave may slightly disrupt your drawing flow. I created this feature with mobile " +
@@ -110,6 +114,18 @@ export default {
SYNC_EXCALIDRAW_NAME: "Sync *.excalidraw with *.md version of the same drawing",
SYNC_EXCALIDRAW_DESC: "If the modified date of the *.excalidraw file is more recent than the modified date of the *.md file " +
"then update the drawing in the .md file based on the .excalidraw file",
COMPATIBILITY_MODE_NAME: "New drawings as legacy files",
COMPATIBILITY_MODE_DESC: "By enabling this feature drawings you create with the ribbon icon, the command palette actions, "+
"and the file explorer are going to be all legacy *.excalidraw files. This setting will also turn off the reminder message " +
"when you open a legacy file for editing.",
EXPERIMENTAL_HEAD: "Experimental features",
EXPERIMENTAL_DESC: "These setting will not take effect immediately, only when the File Explorer is refreshed, or Obsidian restarted.",
FILETYPE_NAME: "Display TAG (✏️) for excalidraw.md files in File Explorer",
FILETYPE_DESC: "Excalidraw files will be tagged using the tag defined in the next setting.",
FILETAG_NAME: "Set the TAG for excalidraw.md files",
FILETAG_DESC: "The text or emojii to display as tag.",
//openDrawings.ts
SELECT_FILE: "Select a file then press enter.",

View File

@@ -35,7 +35,8 @@ import {
LOCK_ICON,
LOCK_ICON_NAME,
UNLOCK_ICON_NAME,
UNLOCK_ICON
UNLOCK_ICON,
JSON_parse
} from "./constants";
import ExcalidrawView, {ExportSettings} from "./ExcalidrawView";
import {getJSON} from "./ExcalidrawData";
@@ -67,6 +68,7 @@ export default class ExcalidrawPlugin extends Plugin {
public lastActiveExcalidrawFilePath: string = null;
private hover: {linkText: string, sourcePath: string} = {linkText: null, sourcePath: null};
private observer: MutationObserver;
private fileExplorerObserver: MutationObserver;
constructor(app: App, manifest: PluginManifest) {
super(app, manifest);
@@ -89,7 +91,11 @@ export default class ExcalidrawPlugin extends Plugin {
(leaf: WorkspaceLeaf) => new ExcalidrawView(leaf, this)
);
//Compatibility mode with .excalidraw files
this.registerExtensions(["excalidraw"],VIEW_TYPE_EXCALIDRAW);
this.addMarkdownPostProcessor();
this.experimentalFileTypeDisplayToggle(this.settings.experimentalFileType);
this.registerCommands();
this.registerEventListeners();
@@ -97,6 +103,7 @@ export default class ExcalidrawPlugin extends Plugin {
//https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/main.ts#L267
this.registerMonkeyPatches();
if(this.settings.loadCount<3) this.migrationNotice();
}
private migrationNotice(){
@@ -139,7 +146,7 @@ export default class ExcalidrawPlugin extends Plugin {
withBackground: this.settings.exportWithBackground,
withTheme: this.settings.exportWithTheme
}
let svg = ExcalidrawView.getSVG(getJSON(content),exportSettings);
let svg = ExcalidrawView.getSVG(JSON_parse(getJSON(content)),exportSettings);
if(!svg) return null;
svg = ExcalidrawView.embedFontsInSVG(svg);
const img = createEl("img");
@@ -219,17 +226,12 @@ export default class ExcalidrawPlugin extends Plugin {
* @returns
*/
const hoverEvent = (e:any) => {
if(!(e.event.ctrlKey||e.event.metaKey)) return;
if(!e.linktext) return;
this.hover.linkText = e.linktext;
this.hover.sourcePath = e.sourcePath;
const file = this.app.vault.getAbstractFileByPath(e.linktext);
if(file && file instanceof TFile && !this.isExcalidrawFile(file)) {
this.hover.linkText = null;
return;
if(!e.linktext) {
this.hover.linkText = null;
return;
}
this.hover.linkText = e.linktext;
this.hover.sourcePath = e.sourcePath;
};
this.registerEvent(
//@ts-ignore
@@ -238,8 +240,16 @@ export default class ExcalidrawPlugin extends Plugin {
//monitoring for div.popover.hover-popover.file-embed.is-loaded to be added to the DOM tree
this.observer = new MutationObserver((m)=>{
if(!this.hover.linkText) return;
if(m.length == 0) return;
if(!this.hover.linkText) return;
const file = this.app.metadataCache.getFirstLinkpathDest(this.hover.linkText, this.hover.sourcePath?this.hover.sourcePath:"");
if(!file || !(file instanceof TFile) || !this.isExcalidrawFile(file)) return
if((file as TFile).extension == "excalidraw") {
observerForLegacyFileFormat(m,file);
return;
}
let i=0;
//@ts-ignore
while(i<m.length && m[i].target?.className!="markdown-preview-sizer markdown-preview-section") i++;
@@ -252,24 +262,93 @@ export default class ExcalidrawPlugin extends Plugin {
//@ts-ignore
if(m[i].addedNodes[0].firstElementChild?.firstElementChild?.className=="excalidraw-svg") return;
const file = this.app.metadataCache.getFirstLinkpathDest(this.hover.linkText, this.hover.sourcePath?this.hover.sourcePath:"");
if(file) {
//this div will be on top of original DIV. By stopping the propagation of the click
//I prevent the default Obsidian feature of openning the link in the native app
const div = createDiv("",async (el)=>{
const img = await getIMG({fname:file.path,fwidth:"300",fheight:null,style:"excalidraw-svg"});
el.appendChild(img);
el.setAttribute("src",file.path);
el.onClickEvent((ev)=>{
ev.stopImmediatePropagation();
let src = el.getAttribute("src");
if(src) this.openDrawing(this.app.vault.getAbstractFileByPath(src) as TFile,ev.ctrlKey||ev.metaKey);
//this div will be on top of original DIV. By stopping the propagation of the click
//I prevent the default Obsidian feature of openning the link in the native app
const div = createDiv("",async (el)=>{
const img = await getIMG({fname:file.path,fwidth:"300",fheight:null,style:"excalidraw-svg"});
el.appendChild(img);
el.setAttribute("src",file.path);
el.onClickEvent((ev)=>{
ev.stopImmediatePropagation();
let src = el.getAttribute("src");
if(src) this.openDrawing(this.app.vault.getAbstractFileByPath(src) as TFile,ev.ctrlKey||ev.metaKey);
});
});
m[i].addedNodes[0].insertBefore(div,m[i].addedNodes[0].firstChild)
});
//compatibility: .excalidraw file observer
let observerForLegacyFileFormat = (m:MutationRecord[], file:TFile) => {
if(!this.hover.linkText) return;
if(m.length!=1) return;
if(m[0].addedNodes.length != 1) return;
//@ts-ignore
if(m[0].addedNodes[0].className!="popover hover-popover file-embed is-loaded") return;
const node = m[0].addedNodes[0];
node.empty();
//this div will be on top of original DIV. By stopping the propagation of the click
//I prevent the default Obsidian feature of openning the link in the native app
const div = createDiv("",async (el)=>{
const img = await getIMG({fname:file.path,fwidth:"300",fheight:null,style:"excalidraw-svg"});
el.appendChild(img);
el.setAttribute("src",file.path);
el.onClickEvent((ev)=>{
ev.stopImmediatePropagation();
let src = el.getAttribute("src");
if(src) this.openDrawing(this.app.vault.getAbstractFileByPath(src) as TFile,ev.ctrlKey||ev.metaKey);
});
});
node.appendChild(div);
}
this.observer.observe(document, {childList: true, subtree: true});
}
public experimentalFileTypeDisplayToggle(enabled: boolean) {
if(enabled) {
this.experimentalFileTypeDisplay();
return;
}
if(this.fileExplorerObserver) this.fileExplorerObserver.disconnect();
this.fileExplorerObserver = null;
}
private experimentalFileTypeDisplay() {
this.fileExplorerObserver = new MutationObserver((m)=>{
const mutationsWithNodes = m.filter((v)=>v.addedNodes.length > 0);
mutationsWithNodes.forEach((mu)=>{
mu.addedNodes.forEach((n)=>{
if(!(n instanceof Element)) return;
n.querySelectorAll(".nav-file-title").forEach((el)=>{
if(el.childElementCount == 1) {
//@ts-ignore
if(this.isExcalidrawFile(this.app.vault.getAbstractFileByPath(el.attributes["data-path"].value))) {
el.insertBefore(createDiv({cls:"nav-file-tag",text:this.settings.experimentalFileTag}),el.firstChild);
}
}
});
});
m[i].addedNodes[0].insertBefore(div,m[i].addedNodes[0].firstChild)
}
});
});
this.observer.observe(document, {childList: true, subtree: true});
const self = this;
this.app.workspace.onLayoutReady(()=>{
document.querySelectorAll(".nav-file-title").forEach((el)=>{
if(el.childElementCount == 1) {
//@ts-ignore
if(this.isExcalidrawFile(this.app.vault.getAbstractFileByPath(el.attributes["data-path"].value))) {
el.insertBefore(createDiv({cls:"nav-file-tag",text:this.settings.experimentalFileTag}),el.firstChild);
}
}
});
this.fileExplorerObserver.observe(document.querySelector(".workspace"), {childList: true, subtree: true});
});
}
private registerCommands() {
@@ -421,6 +500,10 @@ 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) {
@@ -455,7 +538,10 @@ export default class ExcalidrawPlugin extends Plugin {
name: t("TOGGLE_LOCK"),
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) {
@@ -519,6 +605,9 @@ export default class ExcalidrawPlugin extends Plugin {
const fileIsExcalidraw = this.isExcalidrawFile(activeFile);
if (checking) {
if(this.app.workspace.activeLeaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW) {
return !(this.app.workspace.activeLeaf.view as ExcalidrawView).compatibilityMode;
}
return fileIsExcalidraw;
}
@@ -571,13 +660,14 @@ export default class ExcalidrawPlugin extends Plugin {
});
}
public async convertSingleExcalidrawToMD(file: TFile, replaceExtension:boolean = false, keepOriginal:boolean = false) {
public async convertSingleExcalidrawToMD(file: TFile, replaceExtension:boolean = false, keepOriginal:boolean = false):Promise<TFile> {
const data = await this.app.vault.read(file);
const filename = file.name.substr(0,file.name.lastIndexOf(".excalidraw")) + (replaceExtension ? ".md" : ".excalidraw.md");
const fname = this.getNewUniqueFilepath(filename,normalizePath(file.path.substr(0,file.path.lastIndexOf(file.name))));
console.log(fname);
await this.app.vault.create(fname,FRONTMATTER + exportSceneToMD(data));
const result = await this.app.vault.create(fname,FRONTMATTER + exportSceneToMD(data));
if (!keepOriginal) this.app.vault.delete(file);
return result;
}
public async convertExcalidrawToMD(replaceExtension:boolean = false, keepOriginal:boolean = false) {
@@ -781,6 +871,7 @@ export default class ExcalidrawPlugin extends Plugin {
onunload() {
destroyExcalidrawAutomate();
this.observer.disconnect();
if (this.fileExplorerObserver) this.fileExplorerObserver.disconnect();
const excalidrawLeaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
excalidrawLeaves.forEach((leaf) => {
this.setMarkdownView(leaf);
@@ -840,15 +931,22 @@ export default class ExcalidrawPlugin extends Plugin {
}
private getNextDefaultFilename():string {
return this.settings.drawingFilenamePrefix + window.moment().format(this.settings.drawingFilenameDateTime)+'.excalidraw.md';
return this.settings.drawingFilenamePrefix + window.moment().format(this.settings.drawingFilenameDateTime)
+ (this.settings.compatibilityMode ? '.excalidraw' : '.excalidraw.md');
}
private async getBlankDrawing():Promise<string> {
const template = this.app.metadataCache.getFirstLinkpathDest(normalizePath(this.settings.templateFilePath),"");
if(template && template instanceof TFile) {
const data = await this.app.vault.read(template);
if (data) return data;
if( (template.extension == "md" && !this.settings.compatibilityMode)
|| (template.extension == "excalidraw" && this.settings.compatibilityMode)) {
const data = await this.app.vault.read(template);
if (data) return data;
}
}
if (this.settings.compatibilityMode) {
return BLANK_DRAWING;
}
return FRONTMATTER + '\n# Drawing\n'+ BLANK_DRAWING;
}
@@ -908,6 +1006,7 @@ export default class ExcalidrawPlugin extends Plugin {
}
isExcalidrawFile(f:TFile) {
if(f.extension=="excalidraw") return true;
const fileCache = this.app.metadataCache.getFileCache(f);
return !!fileCache?.frontmatter && !!fileCache.frontmatter[FRONTMATTER_KEY];
}

View File

@@ -26,6 +26,9 @@ export interface ExcalidrawSettings {
autoexportExcalidraw: boolean,
syncExcalidraw: boolean,
library: string,
compatibilityMode: boolean,
experimentalFileType: boolean,
experimentalFileTag: string,
loadCount: number, //version 1.2 migration counter
drawingOpenCount: number,
}
@@ -48,6 +51,9 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
autoexportExcalidraw: false,
syncExcalidraw: false,
library: `{"type":"excalidrawlib","version":1,"library":[]}`,
experimentalFileType: false,
experimentalFileTag: "✏️",
compatibilityMode: false,
loadCount: 0,
drawingOpenCount: 0,
}
@@ -268,6 +274,16 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.containerEl.createEl('h1', {text: t("COMPATIBILITY_HEAD")});
new Setting(containerEl)
.setName(t("COMPATIBILITY_MODE_NAME"))
.setDesc(t("COMPATIBILITY_MODE_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.compatibilityMode)
.onChange(async (value) => {
this.plugin.settings.compatibilityMode = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName(t("EXPORT_EXCALIDRAW_NAME"))
.setDesc(t("EXPORT_EXCALIDRAW_DESC"))
@@ -287,6 +303,30 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.plugin.settings.syncExcalidraw = value;
await this.plugin.saveSettings();
}));
this.containerEl.createEl('h1', {text: t("EXPERIMENTAL_HEAD")});
this.containerEl.createEl('p', {text: t("EXPERIMENTAL_DESC")});
new Setting(containerEl)
.setName(t("FILETYPE_NAME"))
.setDesc(t("FILETYPE_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.experimentalFileType)
.onChange(async (value) => {
this.plugin.settings.experimentalFileType = value;
this.plugin.experimentalFileTypeDisplayToggle(value);
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName(t("FILETAG_NAME"))
.setDesc(t("FILETAG_DESC"))
.addText(text => text
.setPlaceholder('✏️')
.setValue(this.plugin.settings.experimentalFileTag)
.onChange(async (value) => {
this.plugin.settings.experimentalFileTag = value;
await this.plugin.saveSettings();
}));
}
}

View File

@@ -61,4 +61,14 @@ button.ToolIcon_type_button[title="Export"] {
.excalidraw-prompt-input {
flex-grow: 1;
}
}
@font-face {
font-family: "Virgil";
src: url("https://excalidraw.com/Virgil.woff2");
}
@font-face {
font-family: "Cascadia";
src: url("https://excalidraw.com/Cascadia.woff2");
}