Compare commits

..

13 Commits

Author SHA1 Message Date
zsviczian
6c75f6d69b Update README.md 2021-06-21 09:19:27 +02:00
Zsolt Viczian
5b90ff486f update readme 2021-06-20 20:05:50 +02:00
Zsolt Viczian
da163344af 1.1.9 Readme 2021-06-20 19:59:20 +02:00
Zsolt Viczian
81550b61ce 1.1.9 readme update 2021-06-20 19:58:11 +02:00
Zsolt Viczian
4cf623065a 1.1.9 2021-06-20 19:53:15 +02:00
Zsolt Viczian
7ea7cf5f65 false alarm... 2021-06-06 21:36:18 +02:00
Zsolt Viczian
081f2c0368 1.1.8 2021-06-06 15:29:48 +02:00
Zsolt Viczian
0205847751 link index draft - not working yet 2021-06-05 21:18:33 +02:00
Zsolt Viczian
b796ba12f2 quick-preview-beta 2021-06-05 12:39:24 +02:00
Zsolt Viczian
21c564f59c spelling errors 2021-05-29 06:27:47 +02:00
Zsolt Viczian
5bbe90182d 1.1.7 2021-05-27 21:39:53 +02:00
Zsolt Viczian
6174e45c3f 1.1.6 2021-05-25 22:07:19 +02:00
Zsolt Viczian
caebd71dc8 1.1.5 2021-05-24 15:17:31 +02:00
22 changed files with 759 additions and 148 deletions

View File

@@ -4,7 +4,7 @@ Excalidraw Automate allows you to create Excalidraw drawings using the [Template
With a little work, using Excalidraw Automate you can generate simple mindmaps, fill out SVG forms, create customized charts, etc. based on documents in your vault.
You can access Excalidraw Automate via the ExcalidrawAutomate object. I recommend staring your Automate scripts with the following code.
You can access Excalidraw Automate via the ExcalidrawAutomate object. I recommend starting your Automate scripts with the following code.
*Use CTRL+Shift+V to paste code into Obsidian!*
```javascript
@@ -288,7 +288,7 @@ Groups objects listed in `objectIds`.
```typescript
async toClipboard(templatePath?:string)
```
Places the generated drawing to the clipboard. Useful when you don't want to create a new drawing, but want to paste additional items onto an exising drawing.
Places the generated drawing to the clipboard. Useful when you don't want to create a new drawing, but want to paste additional items onto an existing drawing.
### create()
```typescript

View File

@@ -2,21 +2,13 @@ The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/),
![image](https://user-images.githubusercontent.com/14358394/115983515-d06c2c80-a5a1-11eb-8d12-c7df91d18107.png)
# Important notice to the 1.1.x update!
## Important notice to the 1.1.x update!
Thank you for updating to Excalidraw 1.1.x!
I have improved how drawings are embedded! You no longer need an Excalidraw codeblock. You can now embed drawings just like any other images: `![[my drawing.excalidraw]]` or `![[my drawing.excalidraw|500|left]]` or `![[my drawing.excalidraw|right-wrap]]`, `![alttext|500|right](drawing.excalidraw)`, `![](folder/drawing.excalidraw)` etc. You get the idea.
I have improved how drawings are embedded! You no longer need an Excalidraw codeblock. You can now embed drawings just like any other images: `![[my drawing.excalidraw]]` or `![[my drawing.excalidraw|500|left]]` or `![[my drawing.excalidraw|right-wrap]]`, `![alttext|500|right](drawing.excalidraw)`, `![](folder/drawing.excalidraw)`, etc. You get the idea.
ALT+Enter and CTRL+ALT+Enter on the filename in edit mode will open up the Excalidraw editor. Click and CTRL+Click on the image in preview mode will also bring up the Excalidraw editor as expected.
I have also added two new Command Palette commands. Both create a new drawing and immediately embed it in the document you are editing, one will open the drawing in a new workspace pane, the other within the currently active pane.
### MIGRATION
I have added a Migration command to the Command Palette. When you select this, the program will run a search and replace for all the excalidraw codeblocks in your vault and will convert them to the new format.
### [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.
### Detailed release notes are under the How to videos.
# Key features
- The plugin saves drawings to your vault as a file with the *.excalidraw* file extension.
@@ -25,6 +17,7 @@ In a nice collaboration with Ozan, his Image in Editor plugin now supports Excal
- Find and edit existing drawings in your vault,
- Transclude (embed) a drawing into a document, and
- Export a drawing as PNG or SVG.
- Insert vault internal-link into drawing
- You can also use the **file explorer** in your vault to open existing Excalidraw files.
- Use the **ribbon button** to create a new drawing, CTRL+Click to open on a new page.
- Open settings to set up
@@ -32,12 +25,13 @@ In a nice collaboration with Ozan, his Image in Editor plugin now supports Excal
- a **Template** by first creating a drawing, customizing it the way you like it, and specifying the file as the template in settings,
- Excalidraw to **automatically export SVG and/or PNG** files for your drawings, and to keep those in sync with your drawing,
- default width of embedded drawings
- You can also ustomize the **size and position of the embedded image** 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 as style to the SVG element and the wrapper DIV element. Check below and styles.css for more insight.
- You can also customize the **size and position of the embedded image** 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 as style to the SVG element and the wrapper DIV element. Check below and styles.css for more insight.
- Supports hyperlinks e.g. `https://zsolt.blog` and internal links e.g. `[[My file in vault]]` in drawing text. Ctrl/meta + click on a text element.
- Square brackets can be omitted if the entire text element is an internal link. i.e. the following two text elements `Check out the [[requirements specification]]!!` and `requirements specification` will both represent a link to `requirements specification.md`.
- When files are moved/renamed in your vault, text elements that are recognized links will also get updated. Check corresponding setting.
- Includes full [Templater](https://silentvoid13.github.io/Templater/) and [Dataview](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/) support through ExcalidrawAutomate. Read detailed help + examples: [here](https://zsviczian.github.io/obsidian-excalidraw-plugin/)
- REQUIRES AN OBSIDIAN SYNC SUBSCRIPTION: Temporary hack/workaround to enable Obsidian Sync for Excalidraw files. This enables almost real-time two-way sync for Excalidraw files between your devices. You can draw on your iPad with your pencil, on your Android with your stylus, and the image will be available in Obsidian on your desktop as well and vice versa.
### Please find release notes for new releases below the How-to videos.
# How to?
Part 1: Intro to Obsidian-Excalidraw - Start a new drawing (3:12)
@@ -65,6 +59,41 @@ Part 6: Intro to Obsidian-Excalidraw: Embedding drawings (2:08)
# Release Notes
## 1.1.9
- I modified the behavior of Excalidraw text element links.
- 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
- I added a setting to limit link functionality to `[[valid Obsidian links]]` only. By default, the full text of a text element is treated as a link unless it contains a `[[valid internal link]]`, in which case only the `[[internal link]]` is used. The new setting may be beneficial if you want to avoid unexpected updates to text in your drawings. This may happen if a text element in a drawing accidentally matches a file in your vault, and you happen to rename or move that file. By limiting the link behavior to `[[valid internal links]]` only, these accidental matches can be avoided. This is not frequent but happened to me recently.
- LaTeX symbol support. I resolved issue [#75](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/75) by adding a new command palette option ("Insert LaTeX-symbol") to insert an expression containing a LaTeX symbol or a simple formula. Some symbols may not display properly using the "Hand-drawn" font. If that is the case try using the "Normal" or "Code" fonts.
## 1.1.8
- Improvements to links
- You can now use square brackets to denote links. i.e. the text element `Which are my [[favorite books]]?` will be a link to `favorite books.md`.
- Square brackets can still be omitted if the entire text element is an internal link. i.e. the following two text elements `Check out the [[requirements specification]]!!` and `requirements specification` will both represent a link to `requirements specification.md`.
- When files are moved/renamed in your vault, text elements that are recognized links will also get updated in your drawings.
- I added a new command palette option to insert an internal link into a file in your vault to the active drawing. While a drawing is open press ctrl/cmd+p and select `Excalidraw: Insert link to file`.
- I Added CTRL/CMD + hover quick preview for Excalidraw files
[![Obsidian-Excalidraw 1.1.8 - Links enhanced](https://user-images.githubusercontent.com/14358394/120925953-31c40700-c6db-11eb-904d-65300e91815e.jpg)](https://youtu.be/qT_NQAojkzg)
## 1.1.6
[![Obsidian-Excalidraw 1.1.6 - Links](https://user-images.githubusercontent.com/14358394/119559279-bdb46580-bda2-11eb-88cb-7614dc452034.jpg)](https://youtu.be/FDsMH-aLw_I)
## 1.1.5
- The template will now 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.
- Added settings to customize the autogenerated filename
- Minor fixes for occasional console.log errors.
## 1.1.0
- ALT+Enter and CTRL+ALT+Enter on the filename in edit mode will open up the Excalidraw editor. Click and CTRL+Click on the image in preview mode will also bring up the Excalidraw editor as expected.
- I have also added two new Command Palette commands. Both create a new drawing and immediately embed it in the document you are editing, one will open the drawing in a new workspace pane, the other within the currently active pane.
- [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.
### MIGRATION to 1.1.0
I have added a Migration command to the Command Palette. When you select this, the program will run a search and replace for all the excalidraw codeblocks in your vault and will convert them to the new format.
## 1.0.12 Freehand drawing
- now includes the new freehand drawing features from Excalidraw.com
- If you use Obsydian sync with Excalidraw sync, be sure to update all your devices to the new version, as the old excalidraw will simply delete the freehand drawn images and/or simply not show the drawing.
@@ -77,10 +106,10 @@ Part 6: Intro to Obsidian-Excalidraw: Embedding drawings (2:08)
### QoL improvement
- I added an autosave feature. Your active drawing gets saved every 30 seconds if you've made changes to it. Drawings otherwise get saved when the window loses focus, or when you close the drawing, etc. Autosave limits the risk of accidental data loss on mobiles when you "swipe out" Obsidian to close it.
## 1.0.10 update
## 1.0.10
[![Obsidian-Excalidraw 1.0.10 update](https://user-images.githubusercontent.com/14358394/117579017-60a58800-b0f1-11eb-8553-7820964662aa.jpg)](https://youtu.be/W7pWXGIe4rQ)
## 1.0.8 and 1.0.9 (minor fixes) update
## 1.0.8 and 1.0.9 (minor fixes)
[![Obsidian-Excalidraw 1.0.8 update](https://user-images.githubusercontent.com/14358394/117492534-029e6680-af72-11eb-90a3-086e67e70c1c.jpg)](https://youtu.be/AtEhmHJjnxM)
### QoL improvements
@@ -100,7 +129,7 @@ You now have ultimate flexibility over your Excalidraw templates using Templater
- Complex use-case: Create a mindmap from a tabulated outline.
![Drawing 2021-05-05 20 52 34](https://user-images.githubusercontent.com/14358394/117194124-00a69d00-ade4-11eb-8b75-5e18a9cbc3cd.png)
## 1.0.6 and 1.0.7 update
## 1.0.6 and 1.0.7
[![1.0.6 Update](https://user-images.githubusercontent.com/14358394/116312909-58725200-a7ad-11eb-89b9-c67cb48ffebb.jpg)](https://youtu.be/ipZPbcP2B0M)
### SVG styling when embedding

View File

@@ -1,5 +1,5 @@
# [◀ Excalidraw Automate How To](../readme.md)
## Attributes and functions overivew
## Attributes and functions overview
Here's the interface implemented by ExcalidrawAutomate:
```javascript

View File

@@ -1,6 +1,6 @@
# [◀ Excalidraw Automate How To](../readme.md)
## Introduction to the API
You can access Excalidraw Automate via the ExcalidrawAutomate object. I recommend staring your Automate scripts with the following code.
You can access Excalidraw Automate via the ExcalidrawAutomate object. I recommend starting your Automate scripts with the following code.
*Use CTRL+Shift+V to paste code into Obsidian!*
```javascript

View File

@@ -10,7 +10,7 @@
```typescript
async toClipboard(templatePath?:string)
```
Places the generated drawing to the clipboard. Useful when you don't want to create a new drawing, but want to paste additional items onto an exising drawing.
Places the generated drawing to the clipboard. Useful when you don't want to create a new drawing, but want to paste additional items onto an existing drawing.
### create()
```typescript

View File

@@ -43,7 +43,7 @@ function buildMindmap(subtasks, depth, offset, parentObjectID) {
for (let i = 0; i < subtasks.length; i++) {
task = subtasks[i]
if (depth == 1) ea.style.strokeColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16);
if (depth == 1) ea.style.strokeColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16).padStart(6,"0");
task["objectID"] = ea.addText((task.size/2+offset)*width,depth*height,task.text,{box:true})
ea.connectObjects(parentObjectID,"top",task.objectID,"bottom",{startArrowHead: 'arrow', endArrowHead: 'dot'});
if (i >= 1) {

View File

@@ -41,7 +41,7 @@ ea.reset();
function buildMindmap(subtasks, depth, offset, parentObjectID) {
if (subtasks.length == 0) return;
for (let task of subtasks) {
if (depth == 1) ea.style.strokeColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16);
if (depth == 1) ea.style.strokeColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16).padStart(6,"0");
task["objectID"] = ea.addText(depth*width,(task.size/2+offset)*height,task.text,{box:true})
ea.connectObjects(parentObjectID,"right",task.objectID,"left",{startArrowHead: 'dot'});
buildMindmap(task.subtasks, depth+1,offset,task.objectID);

View File

@@ -9,7 +9,7 @@ This [Templater](https://github.com/SilentVoid13/Templater) template will prompt
const title = await tp.system.prompt("Title of the drawing?", defaultTitle);
const folder = tp.file.folder(true);
const transcludePath = (folder== '/' ? '' : folder + '/') + title + '.excalidraw';
tR = String.fromCharCode(96,96,96)+'excalidraw\n[['+transcludePath+']]\n'+String.fromCharCode(96,96,96);
tR = '![['+transcludePath+']]';
const ea = ExcalidrawAutomate;
ea.reset();
ea.setTheme(1); //set Theme to dark

View File

@@ -81,7 +81,7 @@ 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);
if (depth == 1) ea.style.strokeColor = '#'+(Math.random()*0xFFFFFF<<0).toString(16).padStart(6,"0");
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]);

View File

@@ -2,10 +2,10 @@
Excalidraw Automate allows you to create Excalidraw drawings using the [Templater](https://silentvoid13.github.io/Templater/docs/) plugin, and to generate embedded SVG and PNG images using [DataviewJS](https://blacksmithgu.github.io/obsidian-dataview/docs/api/intro/)
With a little work, using Excalidraw Automate you can generate simple mindmaps, build a faimly tree, fill out SVG forms, create customized charts, etc. based on documents in your vault.
With a little work, using Excalidraw Automate you can generate simple mindmaps, build a family tree, fill out SVG forms, create customized charts, etc. based on documents in your vault.
![image](https://user-images.githubusercontent.com/14358394/117549619-bae41180-b03b-11eb-968d-c909e79a7524.png)
## API documenation
## API documentation
- [Introduction to the API](API/introduction.md)
- [Overview of Attributes and Functions](API/attributes_functions_overview.md)
- [Element Sytle](API/element_style.md)

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "1.1.4",
"version": "1.1.9",
"minAppVersion": "0.11.13",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",

153
src/ExcalidrawLinkIndex.ts Normal file
View File

@@ -0,0 +1,153 @@
import {TFile,TAbstractFile, App} from 'obsidian';
import {EXCALIDRAW_FILE_EXTENSION, REG_LINKINDEX_BRACKETS, REG_LINKINDEX_HYPERLINK, REG_LINKINDEX_INVALIDCHARS } from './constants';
import ExcalidrawPlugin from './main';
export default class ExcalidrawLinkIndex {
private app: App;
private plugin: ExcalidrawPlugin;
public link2ex: Map<string, Set<string>>;
private ex2link: Map<string, Set<{link:string, text:string}>>;
private vaultEventHandlers:Map<string,any>;
constructor(plugin:ExcalidrawPlugin) {
this.app = plugin.app;
this.plugin = plugin;
this.link2ex = new Map<string,Set<string>>(); //file is referenced by set of excalidraw drawings
this.ex2link = new Map<string,Set<{link:string, text:string}>>(); //excalidraw drawing references these files
this.vaultEventHandlers = new Map();
}
async reloadIndex() {
this.initialize();
}
async initialize(): Promise<void> {
const link2ex = new Map<string,Set<string>>();
const ex2link = new Map<string,Set<{link:string,text:string}>>();
const timeStart = new Date().getTime();
let counter=0;
const files = this.app.vault.getFiles().filter((f)=>f.extension==EXCALIDRAW_FILE_EXTENSION);
for (const file of files) {
const links = await this.parseLinks(file);
if (links.size > 0) {
counter += links.size;
ex2link.set(file.path, links);
links.forEach((link)=>{
if(link2ex.has(link.link)) link2ex.set(link.link,link2ex.get(link.link).add(file.path));
else link2ex.set(link.link,(new Set<string>()).add(file.path));
});
}
}
this.link2ex = link2ex;
this.ex2link = ex2link;
const totalTimeMs = new Date().getTime() - timeStart;
console.log(
`Excalidraw: Parsed ${files.length} drawings and indexed ${counter} links
(${totalTimeMs / 1000.0}s)`,
);
this.registerEventHandlers();
}
public indexFile(file: TFile) {
if(file.extension != EXCALIDRAW_FILE_EXTENSION) return;
this.clearIndex(file);
this.parseLinks(file).then((links) => {
if(links.size == 0) return;
this.ex2link.set(file.path, links);
links.forEach((link)=>{
if(this.link2ex.has(link.link)) {
this.link2ex.set(link.link,this.link2ex.get(link.link).add(file.path));
}
else this.link2ex.set(link.link,(new Set<string>()).add(file.path));
});
});
//console.log("IndexFile: ", file.path, this.ex2link.get(file.path));
}
private clearIndex(file?: TFile,path?:string) {
if(file && file.extension != EXCALIDRAW_FILE_EXTENSION) return;
if(!path) path = file.path;
if(!this.ex2link.get(path)) return;
this.ex2link.get(path).forEach((ex)=> {
if(!this.link2ex.has(ex.link)) return;
const files = this.link2ex.get(ex.link);
files.delete(path);
if(files.size>0) this.link2ex.set(ex.link,files);
else this.link2ex.delete(ex.link);
});
this.ex2link.delete(path);
}
public static getLinks(textElements:any,filepath:string,app:App,validLinksOnly: boolean): Set<{link:string,text:string}>{
const links = new Set<{link:string,text:string}>();
if(!textElements) return links;
let parts, f, text;
for (const element of textElements) {
text = element.text;
parts = text?.matchAll(REG_LINKINDEX_BRACKETS).next();
if(validLinksOnly) text = ''; //clear text, if it is a valid link, parts.value[1] will hold a value
if(parts && parts.value) text = parts.value[1];
if(text!='' && !text?.match(REG_LINKINDEX_HYPERLINK) && !text?.match(REG_LINKINDEX_INVALIDCHARS)) { //not empty, not a hyperlink and not invalid filename
f = app.metadataCache.getFirstLinkpathDest(text,filepath);
if(f) {
links.add({link:f.path,text:text});
}
}
}
return links;
}
private async parseLinks(file: TFile): Promise<Set<{link:string, text:string}>> {
const fileContents = await this.app.vault.read(file);
const textElements = JSON.parse(fileContents)?.elements?.filter((el:any)=> el.type=="text");
return ExcalidrawLinkIndex.getLinks(textElements,file.path,this.app, this.plugin.settings.validLinksOnly);
}
public updateKey(oldpath:string, newpath:string) {
if (!this.link2ex.has(oldpath)) return;
this.link2ex.set(newpath,this.link2ex.get(oldpath));
this.link2ex.delete(oldpath); //old link2ex will be deleted when the .excalidraw updates trigger
}
public getLinkTextForDrawing(drawPath:string, link:string):string {
if(!this.ex2link.has(drawPath)) return;
for(const item of this.ex2link.get(drawPath)) {
if(item.link == link) return item.text;
}
return;
}
private registerEventHandlers() {
const indexAbstractFile = (file: TAbstractFile) => {
if (!(file instanceof TFile)) return;
if (file.extension != EXCALIDRAW_FILE_EXTENSION) return;
this.indexFile(file as TFile);
}
const clearIndex = (file: TFile) => {
this.clearIndex(file);
}
const rename = (file:TAbstractFile, oldPath: string) => {
if(!oldPath.endsWith("."+EXCALIDRAW_FILE_EXTENSION)) return;
this.clearIndex(null,oldPath);
indexAbstractFile(file);
}
this.app.vault.on('create', indexAbstractFile);
this.vaultEventHandlers.set("create",indexAbstractFile);
this.app.vault.on('modify', indexAbstractFile);
this.vaultEventHandlers.set("modify",indexAbstractFile);
this.app.vault.on('delete', clearIndex);
this.vaultEventHandlers.set("delete",clearIndex);
this.app.vault.on('rename', rename);
this.vaultEventHandlers.set("rename",rename);
}
public deregisterEventHandlers() {
for(const key of this.vaultEventHandlers.keys())
this.app.vault.off(key,this.vaultEventHandlers.get(key))
}
}

View File

@@ -39,25 +39,25 @@ export interface ExcalidrawAutomate extends Window {
endArrowHead: string;
}
canvas: {theme: string, viewBackgroundColor: string};
setFillStyle: Function;
setStrokeStyle: Function;
setStrokeSharpness: Function;
setFontFamily: Function;
setTheme: Function;
addRect: Function;
addDiamond: Function;
addEllipse: Function;
addText: Function;
addLine: Function;
addArrow: Function;
connectObjects: Function;
addToGroup: Function;
toClipboard: Function;
create: Function;
createPNG: Function;
createSVG: Function;
clear: Function;
reset: Function;
setFillStyle(val:number): void;
setStrokeStyle(val:number): void;
setStrokeSharpness(val:number): void;
setFontFamily(val:number): void;
setTheme(val:number): void;
addToGroup(objectIds:[]):void;
toClipboard(templatePath?:string): void;
create(params?:{filename: string, foldername:string, templatePath:string, onNewPane: boolean}):Promise<void>;
createSVG(templatePath?:string):Promise<SVGSVGElement>;
createPNG(templatePath?:string):Promise<any>;
addRect(topX:number, topY:number, width:number, height:number):string;
addDiamond(topX:number, topY:number, width:number, height:number):string;
addEllipse(topX:number, topY:number, width:number, height:number):string;
addText(topX:number, topY:number, text:string, formatting?:{width:number, height:number,textAlign: string, verticalAlign:string, box: boolean, boxPadding: number}):string;
addLine(points: [[x:number,y:number]]):void;
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead:string,endArrowHead:string,startObjectId:string,endObjectId:string}):void ;
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints: number,startArrowHead:string,endArrowHead:string, padding: number}):void;
clear(): void;
reset(): void;
};
}
@@ -174,18 +174,32 @@ export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
params?.onNewPane ? params.onNewPane : false,
params?.foldername ? params.foldername : this.plugin.settings.folder,
JSON.stringify({
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": elements,
"appState": {
"theme": template ? template.appState.theme : this.canvas.theme,
"viewBackgroundColor": template? template.appState.viewBackgroundColor : this.canvas.viewBackgroundColor
type: "excalidraw",
version: 2,
source: "https://excalidraw.com",
elements: elements,
appState: {
theme: template ? template.appState.theme : this.canvas.theme,
viewBackgroundColor: template? template.appState.viewBackgroundColor : this.canvas.viewBackgroundColor,
currentItemStrokeColor: template? template.appState.currentItemStrokeColor : this.style.strokeColor,
currentItemBackgroundColor: template? template.appState.currentItemBackgroundColor : this.style.backgroundColor,
currentItemFillStyle: template? template.appState.currentItemFillStyle : this.style.fillStyle,
currentItemStrokeWidth: template? template.appState.currentItemStrokeWidth : this.style.strokeWidth,
currentItemStrokeStyle: template? template.appState.currentItemStrokeStyle : this.style.strokeStyle,
currentItemRoughness: template? template.appState.currentItemRoughness : this.style.roughness,
currentItemOpacity: template? template.appState.currentItemOpacity : this.style.opacity,
currentItemFontFamily: template? template.appState.currentItemFontFamily : this.style.fontFamily,
currentItemFontSize: template? template.appState.currentItemFontSize : this.style.fontSize,
currentItemTextAlign: template? template.appState.currentItemTextAlign : this.style.textAlign,
currentItemStrokeSharpness: template? template.appState.currentItemStrokeSharpness : this.style.strokeSharpness,
currentItemStartArrowhead: template? template.appState.currentItemStartArrowhead: this.style.startArrowHead,
currentItemEndArrowhead: template? template.appState.currentItemEndArrowhead : this.style.endArrowHead,
currentItemLinearStrokeSharpness: template? template.appState.currentItemLinearStrokeSharpness : this.style.strokeSharpness,
}
})
);
},
async createSVG(templatePath?:string) {
async createSVG(templatePath?:string):Promise<SVGSVGElement> {
const template = templatePath ? (await getTemplate(templatePath)) : null;
let elements = template ? template.elements : [];
for (let i=0;i<this.elementIds.length;i++) {
@@ -251,7 +265,7 @@ export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
},
addText(topX:number, topY:number, text:string, formatting?:{width:number, height:number,textAlign: string, verticalAlign:string, box: boolean, boxPadding: number}):string {
const id = nanoid();
const {w, h, baseline} = measureText(text);
const {w, h, baseline} = measureText(text, this.style.fontSize,this.style.fontFamily);
const width = formatting?.width ? formatting.width : w;
const height = formatting?.height ? formatting.height : h;
this.elementIds.push(id);
@@ -425,13 +439,12 @@ async function initFonts () {
}
}
function measureText (newText:string) {
export function measureText (newText:string, fontSize:number, fontFamily:number) {
const line = document.createElement("div");
const body = document.body;
line.style.position = "absolute";
line.style.whiteSpace = "pre";
line.style.font = window.ExcalidrawAutomate.style.fontSize.toString()+'px ' +
getFontFamily(window.ExcalidrawAutomate.style.fontFamily);
line.style.font = fontSize.toString()+'px ' + getFontFamily(fontFamily);
// await (document as any).fonts.load(line.style.font);
body.appendChild(line);
line.innerText = newText

View File

@@ -3,7 +3,8 @@ import {
WorkspaceLeaf,
normalizePath,
TFile,
WorkspaceItem
WorkspaceItem,
Notice
} from "obsidian";
import * as React from "react";
import * as ReactDOM from "react-dom";
@@ -23,9 +24,14 @@ import {
CASCADIA_FONT,
DISK_ICON_NAME,
PNG_ICON_NAME,
SVG_ICON_NAME
SVG_ICON_NAME,
REG_LINKINDEX_BRACKETS,
REG_LINKINDEX_HYPERLINK,
REG_LINKINDEX_INVALIDCHARS
} from './constants';
import ExcalidrawPlugin from './main';
import {ExcalidrawAutomate} from './ExcalidrawTemplate';
declare let window: ExcalidrawAutomate;
interface WorkspaceItemExt extends WorkspaceItem {
containerEl: HTMLElement;
@@ -38,6 +44,8 @@ export interface ExportSettings {
export default class ExcalidrawView extends TextFileView {
private getScene: Function;
private getSelectedText: Function;
public addText:Function;
private refresh: Function;
private excalidrawRef: React.MutableRefObject<any>;
private justLoaded: boolean;
@@ -49,6 +57,8 @@ export default class ExcalidrawView extends TextFileView {
constructor(leaf: WorkspaceLeaf, plugin: ExcalidrawPlugin) {
super(leaf);
this.getScene = null;
this.getSelectedText = null;
this.addText = null;
this.refresh = null;
this.excalidrawRef = null;
this.plugin = plugin;
@@ -56,6 +66,7 @@ export default class ExcalidrawView extends TextFileView {
this.dirty = false;
this.autosaveTimer = null;
this.previousSceneVersion = 0;
}
public async saveSVG(data?: string) {
@@ -115,6 +126,50 @@ export default class ExcalidrawView extends TextFileView {
else return this.data;
}
handleLinkClick(view: ExcalidrawView, ev:MouseEvent) {
let text = this.getSelectedText();
if(!text) {
new Notice('Select a text element.\n'+
'If it is a web link, it will open in a new browser window.\n'+
'Else, if it is a valid filename, Excalidraw will handle it as an Obsidian internal link.\n'+
'Use Shift+click to open it in a new pane.\n'+
'You can also ctrl/meta click on the text element in the drawing as a shortcut to using this button.',20000);
return;
}
if(text.match(REG_LINKINDEX_HYPERLINK)) {
window.open(text,"_blank");
return;
}
const parts = text.matchAll(REG_LINKINDEX_BRACKETS).next();
if(view.plugin.settings.validLinksOnly) text = ''; //clear text, if it is a valid link, parts.value[1] will hold a value
if(parts.value) text = parts.value[1];
if(text=='') {
new Notice('Text element is empty, or [[valid links only]] setting is enabled in settings, and text does not contain a [[valid Obsidian link]]',4000);
return;
}
if(text.match(REG_LINKINDEX_INVALIDCHARS)) {
new Notice('File name cannot contain any of the following characters: * " \\  < > : | ?',4000);
return;
}
if (!ev.altKey) {
const file = view.app.metadataCache.getFirstLinkpathDest(text,view.file.path);
if (!file) {
new Notice("File does not exist. Hold down ALT (or ALT+SHIFT) and click link button to create a new file.", 4000);
return;
}
}
try {
const f = view.file;
view.app.workspace.openLinkText(text,view.file.path,ev.shiftKey).then( ()=> {
if(ev.altKey) //create new: need to reindex excalidraw file
view.plugin.linkIndex.indexFile(f);
});
} catch (e) {
new Notice(e,4000);
}
}
async onload() {
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();
@@ -122,6 +177,8 @@ export default class ExcalidrawView extends TextFileView {
});
this.addAction(PNG_ICON_NAME,"Export as PNG",async (ev)=>this.savePNG());
this.addAction(SVG_ICON_NAME,"Export as SVG",async (ev)=>this.saveSVG());
this.addAction("link","Open selected text as link\n(SHIFT+click to open in a new pane)", (ev)=>this.handleLinkClick(this,ev));
//this is to solve sliding panes bug
if (this.app.workspace.layoutReady) {
(this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt).containerEl.addEventListener('scroll',(e)=>{if(this.refresh) this.refresh();});
@@ -159,12 +216,12 @@ export default class ExcalidrawView extends TextFileView {
// clear the view content
clear() {
if(this.excalidrawRef) {
/*if(this.excalidrawRef) {
this.excalidrawRef = null;
this.getScene = null;
this.refresh = null;
ReactDOM.unmountComponentAtNode(this.contentEl);
}
}*/
}
private async loadDrawing (data:string, clear:boolean) {
@@ -217,6 +274,7 @@ export default class ExcalidrawView extends TextFileView {
this.dirty = false;
this.previousSceneVersion = 0;
const reactElement = React.createElement(() => {
let currentPosition = {x:0, y:0};
const excalidrawRef = React.useRef(null);
const excalidrawWrapperRef = React.useRef(null);
const [dimensions, setDimensions] = React.useState({
@@ -243,6 +301,36 @@ export default class ExcalidrawView extends TextFileView {
return () => window.removeEventListener("resize", onResize);
}, [excalidrawWrapperRef]);
this.getSelectedText = ():string => {
if(!excalidrawRef?.current) return null;
const selectedElement = excalidrawRef.current.getSceneElements().filter((el:any)=>el.id==Object.keys(excalidrawRef.current.getAppState().selectedElementIds)[0]);
if(selectedElement.length==0) return null;
if(selectedElement[0].type != "text") return null;
return selectedElement[0].text;
};
this.addText = (text:string, fontFamily?:1|2|3) => {
if(!excalidrawRef?.current) {
return;
}
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
const st: AppState = excalidrawRef.current.getAppState();
window.ExcalidrawAutomate.reset();
window.ExcalidrawAutomate.style.strokeColor = st.currentItemStrokeColor;
window.ExcalidrawAutomate.style.opacity = st.currentItemOpacity;
window.ExcalidrawAutomate.style.fontFamily = fontFamily ? fontFamily: st.currentItemFontFamily;
window.ExcalidrawAutomate.style.fontSize = st.currentItemFontSize;
window.ExcalidrawAutomate.style.textAlign = st.currentItemTextAlign;
const id = window.ExcalidrawAutomate.addText(currentPosition.x, currentPosition.y, text);
//@ts-ignore
el.push(window.ExcalidrawAutomate.elementsDict[id]);
excalidrawRef.current.updateScene({
elements: el,
appState: st,
});
//console.log(currentPosition,el,st);
}
this.getScene = () => {
if(!excalidrawRef?.current) {
return null;
@@ -250,13 +338,27 @@ export default class ExcalidrawView extends TextFileView {
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
const st: AppState = excalidrawRef.current.getAppState();
return JSON.stringify({
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": el,
"appState": {
"theme": st.theme,
"viewBackgroundColor": st.viewBackgroundColor,
type: "excalidraw",
version: 2,
source: "https://excalidraw.com",
elements: el,
appState: {
theme: st.theme,
viewBackgroundColor: st.viewBackgroundColor,
currentItemStrokeColor: st.currentItemStrokeColor,
currentItemBackgroundColor: st.currentItemBackgroundColor,
currentItemFillStyle: st.currentItemFillStyle,
currentItemStrokeWidth: st.currentItemStrokeWidth,
currentItemStrokeStyle: st.currentItemStrokeStyle,
currentItemRoughness: st.currentItemRoughness,
currentItemOpacity: st.currentItemOpacity,
currentItemFontFamily: st.currentItemFontFamily,
currentItemFontSize: st.currentItemFontSize,
currentItemTextAlign: st.currentItemTextAlign,
currentItemStrokeSharpness: st.currentItemStrokeSharpness,
currentItemStartArrowhead: st.currentItemStartArrowhead,
currentItemEndArrowhead: st.currentItemEndArrowhead,
currentItemLinearStrokeSharpness: st.currentItemLinearStrokeSharpness,
}
});
};
@@ -275,6 +377,11 @@ export default class ExcalidrawView extends TextFileView {
className: "excalidraw-wrapper",
ref: excalidrawWrapperRef,
key: "abc",
onClick: (e:MouseEvent):any => {
if(!(e.ctrlKey||e.metaKey)) return;
if(!this.getSelectedText()) return;
this.handleLinkClick(this,e);
},
},
React.createElement(Excalidraw.default, {
ref: excalidrawRef,
@@ -290,6 +397,9 @@ export default class ExcalidrawView extends TextFileView {
},
initialData: initdata,
detectScroll: true,
onPointerUpdate: (p:any) => {
currentPosition = p.pointer;
},
onChange: (et:ExcalidrawElement[],st:AppState) => {
if(this.justLoaded) {
this.justLoaded = false;

45
src/Prompt.ts Normal file
View File

@@ -0,0 +1,45 @@
import { App, Modal } from "obsidian";
export class Prompt extends Modal {
private promptEl: HTMLInputElement;
private resolve: (value: string) => void;
constructor(app: App, private prompt_text: string, private default_value: string) {
super(app);
}
onOpen(): void {
this.titleEl.setText(this.prompt_text);
this.createForm();
}
onClose(): void {
this.contentEl.empty();
}
createForm(): void {
const div = this.contentEl.createDiv();
div.addClass("excalidarw-prompt-div");
const form = div.createEl("form");
form.addClass("excalidraw-prompt-form");
form.type = "submit";
form.onsubmit = (e: Event) => {
e.preventDefault();
this.resolve(this.promptEl.value);
this.close();
}
this.promptEl = form.createEl("input");
this.promptEl.type = "text";
this.promptEl.placeholder = "$\\theta$";
this.promptEl.value = this.default_value ?? "";
this.promptEl.addClass("excalidraw-prompt-input")
this.promptEl.select();
}
async openAndGetValue(resolve: (value: string) => void): Promise<void> {
this.resolve = resolve;
this.open();
}
}

View File

@@ -2,7 +2,10 @@ export const VIEW_TYPE_EXCALIDRAW = "excalidraw";
export const EXCALIDRAW_FILE_EXTENSION = "excalidraw";
export const EXCALIDRAW_FILE_EXTENSION_LEN = EXCALIDRAW_FILE_EXTENSION.length;
export const ICON_NAME = "excalidraw-icon";
export const CODEBLOCK_EXCALIDRAW = "excalidraw";
//export const CODEBLOCK_EXCALIDRAW = "excalidraw";
export const REG_LINKINDEX_BRACKETS = /\[\[(.+)]]/gm;
export const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;
export const REG_LINKINDEX_INVALIDCHARS = /[<>:"\\|?*]/g;
export const MAX_COLORS = 5;
export const COLOR_FREQ = 6;
export const RERENDER_EVENT = "excalidraw-embed-rerender";

View File

@@ -14,7 +14,9 @@ import {
TAbstractFile,
Notice,
Tasks,
} from 'obsidian';
Workspace,
MarkdownRenderer,
} from "obsidian";
import {
BLANK_DRAWING,
@@ -31,22 +33,28 @@ import {
SVG_ICON_NAME,
RERENDER_EVENT,
VIRGIL_FONT,
CASCADIA_FONT
} from './constants';
import ExcalidrawView, {ExportSettings} from './ExcalidrawView';
CASCADIA_FONT,
REG_LINKINDEX_BRACKETS,
REG_LINKINDEX_HYPERLINK,
REG_LINKINDEX_INVALIDCHARS
} from "./constants";
import ExcalidrawView, {ExportSettings} from "./ExcalidrawView";
import {
ExcalidrawSettings,
DEFAULT_SETTINGS,
ExcalidrawSettingTab
} from './settings';
} from "./settings";
import {
openDialogAction,
OpenFileDialog
} from './openDrawing';
} from "./openDrawing";
import {
initExcalidrawAutomate,
destroyExcalidrawAutomate
} from './ExcalidrawTemplate';
destroyExcalidrawAutomate,
measureText
} from "./ExcalidrawTemplate";
import ExcalidrawLinkIndex from "./ExcalidrawLinkIndex";
import { Prompt } from "./Prompt";
export interface ExcalidrawAutomate extends Window {
ExcalidrawAutomate: {
@@ -62,6 +70,9 @@ export default class ExcalidrawPlugin extends Plugin {
public lastActiveExcalidrawFilePath: string;
private workspaceEventHandlers:Map<string,any>;
private vaultEventHandlers:Map<string,any>;
private hover: {linkText: string, sourcePath: string};
private observer: MutationObserver;
public linkIndex: ExcalidrawLinkIndex;
/*Excalidraw Sync Begin*/
private excalidrawSync: Set<string>;
private syncModifyCreate: any;
@@ -73,6 +84,8 @@ export default class ExcalidrawPlugin extends Plugin {
this.lastActiveExcalidrawFilePath = null;
this.workspaceEventHandlers = new Map();
this.vaultEventHandlers = new Map();
this.hover = {linkText: null, sourcePath: null};
/*Excalidraw Sync Begin*/
this.excalidrawSync = new Set<string>();
this.syncModifyCreate = null;
@@ -103,10 +116,12 @@ export default class ExcalidrawPlugin extends Plugin {
this.addMarkdownPostProcessor();
this.addCommands();
this.linkIndex = new ExcalidrawLinkIndex(this);
if (this.app.workspace.layoutReady) {
this.addEventListeners(this);
} else {
this.registerEvent(this.app.workspace.on('layout-ready', async () => this.addEventListeners(this)));
this.registerEvent(this.app.workspace.on("layout-ready", async () => this.addEventListeners(this)));
}
}
@@ -131,9 +146,9 @@ export default class ExcalidrawPlugin extends Plugin {
svg.removeAttribute('height');
img.setAttribute("width",parts.fwidth);
if(parts.fheight) img.setAttribute("height",parts.fheight);//img.style.setProperty('height',parts.fheight);
if(parts.fheight) img.setAttribute("height",parts.fheight);
img.addClass(parts.style);
img.setAttribute("src","data:image/svg+xml;base64,"+btoa(svg.outerHTML));
img.setAttribute("src","data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.outerHTML))));
return img;
}
@@ -145,11 +160,12 @@ export default class ExcalidrawPlugin extends Plugin {
fwidth = drawing.getAttribute("width");
fheight = drawing.getAttribute("height");
alt = drawing.getAttribute("alt");
if(alt == fname) alt = ""; //when the filename starts with numbers followed by a space Obsidian recognizes the filename as alt-text
divclass = "excalidraw-svg";
if(alt) {
//for some reason ![]() is rendered in a DIV and ![[]] in a span by Obsidian
//also the alt text of the DIV follows does not include the altext of the image
//thus need to add an additional | when its a span
//also the alt text of the DIV does not include the altext of the image
//thus need to add an additional "|" character when its a span
if(drawing.tagName.toLowerCase()=="span") alt = "|"+alt;
parts = alt.match(/[^\|]*\|?(\d*)x?(\d*)\|?(.*)/);
fwidth = parts[1]? parts[1] : this.settings.width;
@@ -168,7 +184,7 @@ export default class ExcalidrawPlugin extends Plugin {
el.onClickEvent((ev)=>{
if(ev.target instanceof Element && ev.target.tagName.toLowerCase() != "img") return;
let src = el.getAttribute("src");
if(src) this.openDrawing(this.app.vault.getAbstractFileByPath(src) as TFile,ev.ctrlKey);
if(src) this.openDrawing(this.app.vault.getAbstractFileByPath(src) as TFile,ev.ctrlKey||ev.metaKey);
});
el.addEventListener(RERENDER_EVENT, async(e) => {
e.stopPropagation;
@@ -182,7 +198,7 @@ export default class ExcalidrawPlugin extends Plugin {
el.append(img);
});
});
} else { //file does not exist. Replace standard Obsidian div, with mine to create a drawing on click
} else { //file does not exist. Replace standard Obsidian div with mine to create a new drawing on click
div = createDiv("excalidraw-new",(el)=> {
el.setAttribute("src",fname);
el.createSpan("internal-embed file-embed mod-empty is-loaded", (el) => {
@@ -205,13 +221,57 @@ export default class ExcalidrawPlugin extends Plugin {
this.registerMarkdownPostProcessor(markdownPostProcessor);
/*****************************
internal-link quick preview
******************************/
const hoverEvent = (e:any) => {
//@ts-ignore
if(!e.linktext) return;
if(!e.linktext.endsWith('.'+EXCALIDRAW_FILE_EXTENSION)) {
this.hover.linkText = null;
return;
}
this.hover.linkText = e.linktext;
this.hover.sourcePath = e.sourcePath;
};
//@ts-ignore
this.app.workspace.on('hover-link',hoverEvent);
this.workspaceEventHandlers.set('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(!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();
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);
});
});
node.appendChild(div);
}
});
this.observer.observe(document, {childList: true, subtree: true});
}
private addCommands() {
this.openDialog = new OpenFileDialog(this.app, this);
this.addRibbonIcon(ICON_NAME, 'Create a new drawing in Excalidraw', async (e) => {
this.createDrawing(this.getNextDefaultFilename(), e.ctrlKey);
this.createDrawing(this.getNextDefaultFilename(), e.ctrlKey||e.metaKey);
});
const fileMenuHandler = (menu: Menu, file: TFile) => {
@@ -255,7 +315,7 @@ export default class ExcalidrawPlugin extends Plugin {
if (checking) {
return this.app.workspace.activeLeaf.view.getViewType() == "markdown";
} else {
this.openDialog.start(openDialogAction.insertLink, false);
this.openDialog.start(openDialogAction.insertLinkToDrawing, false);
return true;
}
},
@@ -328,8 +388,8 @@ export default class ExcalidrawPlugin extends Plugin {
return this.app.workspace.activeLeaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW;
} else {
const view = this.app.workspace.activeLeaf.view;
if(view.getViewType() == VIEW_TYPE_EXCALIDRAW) {
(this.app.workspace.activeLeaf.view as ExcalidrawView).saveSVG();
if (view instanceof ExcalidrawView) {
view.saveSVG();
return true;
}
else return false;
@@ -345,8 +405,8 @@ export default class ExcalidrawPlugin extends Plugin {
return this.app.workspace.activeLeaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW;
} else {
const view = this.app.workspace.activeLeaf.view;
if(view.getViewType() == VIEW_TYPE_EXCALIDRAW) {
(this.app.workspace.activeLeaf.view as ExcalidrawView).savePNG();
if (view instanceof ExcalidrawView) {
view.savePNG();
return true;
}
else return false;
@@ -354,6 +414,48 @@ export default class ExcalidrawPlugin extends Plugin {
},
});
this.addCommand({
id: 'insert-link',
name: 'Insert link to file',
checkCallback: (checking: boolean) => {
if (checking) {
return this.app.workspace.activeLeaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW;
} else {
const view = this.app.workspace.activeLeaf.view;
if (view instanceof ExcalidrawView) {
this.openDialog.insertLink(view.file.path,view.addText);
return true;
}
else return false;
}
},
});
this.addCommand({
id: 'insert-LaTeX-symbol',
name: 'Insert LaTeX-symbol (e.g. $\\theta$)',
checkCallback: (checking: boolean) => {
if (checking) {
return this.app.workspace.activeLeaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW;
} else {
const view = this.app.workspace.activeLeaf.view;
if (view instanceof ExcalidrawView) {
const prompt = new Prompt(this.app, 'Enter a valid LaTeX expression','');
prompt.openAndGetValue( async (formula:string)=> {
if(!formula) return;
const el = createEl('p');
await MarkdownRenderer.renderMarkdown(formula,el,'',this)
view.addText(el.getText());
el.empty();
});
return true;
}
else return false;
}
},
});
/*1.1 migration command*/
const migrateCodeblock = async () => {
const timeStart = new Date().getTime();
@@ -396,21 +498,12 @@ export default class ExcalidrawPlugin extends Plugin {
}
/*Excalidraw Sync End*/
public async reloadIndex() {
this.linkIndex.reloadIndex();
}
private async addEventListeners(plugin: ExcalidrawPlugin) {
const notice = new Notice(
"Welcome to Excalidraw 1.1!\n\n"+
"This is a temporary notice. I will remove this in a week.\n\n"+
"I have improved embedding of drawings. "+
"You no longer need a code block, simply embed Excalidraw like any other image: " +
"![[my drawing.excalidraw]] or ![[my drawing.excalidraw|500|left]] or "+
"![[my drawing.excalidraw|right-wrap]] or ![alt-text|500](my drawing.excalidraw) etc.\n\n"+
"ALT+Enter and CTRL+ALT+Enter on the filename will open the drawing in the Excalidraw editor. "+
"Click and CTRL+Click on the image in preview mode will also open the editor as expected.\n\n"+
"MIGRATION\n"+
'See "Migrate" in Command Palette. Selecting this will convert all the '+
"excalidraw code blocks in your vault to the new embed format.", 60000);
plugin.linkIndex.initialize();
const closeDrawing = async (filePath:string) => {
const leaves = plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
for (let i=0;i<leaves.length;i++) {
@@ -423,7 +516,6 @@ export default class ExcalidrawPlugin extends Plugin {
}
}
/*Excalidraw Sync Begin*/
const reloadDrawing = async (oldPath:string, newPath: string) => {
const file = plugin.app.vault.getAbstractFileByPath(newPath);
if(!(file && file instanceof TFile)) return;
@@ -436,6 +528,8 @@ export default class ExcalidrawPlugin extends Plugin {
plugin.triggerEmbedUpdates(oldPath);
}
/****************************/
/*Excalidraw Sync Begin*/
const createPathIfNotThere = async (path:string) => {
const folderArray = path.split("/");
folderArray.pop();
@@ -491,15 +585,17 @@ export default class ExcalidrawPlugin extends Plugin {
};
this.syncModifyCreate = syncModifyCreate;
plugin.app.vault.on('create', syncModifyCreate);
plugin.app.vault.on('modify', syncModifyCreate);
this.vaultEventHandlers.set('create',syncModifyCreate);
this.vaultEventHandlers.set('modify',syncModifyCreate);
plugin.app.vault.on("create", syncModifyCreate);
plugin.app.vault.on("modify", syncModifyCreate);
this.vaultEventHandlers.set("create",syncModifyCreate);
this.vaultEventHandlers.set("modify",syncModifyCreate);
/*Excalidraw Sync End*/
/****************************/
//watch filename change to rename .svg
//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;
/****************************/
/*Excalidraw Sync Begin*/
if(plugin.settings.excalidrawSync) {
if(plugin.excalidrawSync.has(file.path)) {
@@ -537,6 +633,50 @@ export default class ExcalidrawPlugin extends Plugin {
}
}
/*Excalidraw Sync End*/
/****************************/
/****************************/
/*Update link in drawing Begin*/
const getPath = (f: TFile):string =>
f.extension == "md" ? f.path.substr(0,f.path.length-3) : f.path
if(plugin.linkIndex.link2ex.has(oldPath)) {
//console.log("RENAME oldPath:",oldPath,"newPath:",file.path);
let text, parts, savedText,drawing,drawingFile,measure;
plugin.linkIndex.updateKey(oldPath,file.path);
for (const drawingPath of plugin.linkIndex.link2ex.get(file.path)) {
drawingFile = plugin.app.vault.getAbstractFileByPath(drawingPath);
if(drawingFile && drawingFile instanceof TFile) {
drawing = JSON.parse(await plugin.app.vault.read(drawingFile));
savedText = plugin.linkIndex.getLinkTextForDrawing(drawingPath,oldPath);
//console.log("RENAME savedText:", savedText);
for(const element of drawing.elements.filter((el:any)=>el.type=="text")) {
text = element.text;
parts = text?.matchAll(REG_LINKINDEX_BRACKETS).next();
if(parts && parts.value) text = parts.value[1];
if(!text?.match(REG_LINKINDEX_HYPERLINK) && !text?.match(REG_LINKINDEX_INVALIDCHARS)) { //not a hyperlink and not invalid filename
if(savedText == text) {
if(parts && parts.value) { //link is enclosed in [[]]
element.text = element.text.replace("[["+text+"]]","[["+getPath(file)+"]]");
} else {
element.text = getPath(file);
}
measure = measureText(element.text,element.fontSize,element.fontFamily);
element.width = measure.w;
element.height = measure.h;
element.baseline = measure.baseline;
}
}
}
await plugin.app.vault.modify(drawingFile,JSON.stringify(drawing));
//console.log("RENAME updated drawing:", drawingPath, drawing);
reloadDrawing(drawingPath,drawingPath);
}
}
}
/*Update link in drawing End*/
/****************************/
if (file.extension != EXCALIDRAW_FILE_EXTENSION) return;
if (!plugin.settings.keepInSync) return;
const oldSVGpath = oldPath.substring(0,oldPath.lastIndexOf('.'+EXCALIDRAW_FILE_EXTENSION)) + '.svg';
@@ -546,8 +686,8 @@ export default class ExcalidrawPlugin extends Plugin {
await plugin.app.vault.rename(svgFile,newSVGpath);
}
};
plugin.app.vault.on('rename',renameEventHandler);
this.vaultEventHandlers.set('rename',renameEventHandler);
plugin.app.vault.on("rename",renameEventHandler);
this.vaultEventHandlers.set("rename",renameEventHandler);
//watch file delete and delete corresponding .svg
@@ -593,7 +733,7 @@ export default class ExcalidrawPlugin extends Plugin {
}
}
}
plugin.app.vault.on('delete',deleteEventHandler);
plugin.app.vault.on("delete",deleteEventHandler);
this.vaultEventHandlers.set("delete",deleteEventHandler);
//save open drawings when user quits the application
@@ -603,20 +743,20 @@ export default class ExcalidrawPlugin extends Plugin {
(leaves[i].view as ExcalidrawView).save();
}
}
plugin.app.workspace.on('quit',quitEventHandler);
plugin.app.workspace.on("quit",quitEventHandler);
this.workspaceEventHandlers.set("quit",quitEventHandler);
//save Excalidraw leaf and update embeds when switching to another leaf
const activeLeafChangeEventHandler = (leaf:WorkspaceLeaf) => {
if(plugin.activeExcalidrawView) {
plugin.activeExcalidrawView.save();
plugin.triggerEmbedUpdates(plugin.activeExcalidrawView.file.path);
plugin.triggerEmbedUpdates(plugin.activeExcalidrawView.file?.path);
}
plugin.activeExcalidrawView = (leaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW) ? leaf.view as ExcalidrawView : null;
if(plugin.activeExcalidrawView)
plugin.lastActiveExcalidrawFilePath = plugin.activeExcalidrawView.file.path;
plugin.lastActiveExcalidrawFilePath = plugin.activeExcalidrawView.file?.path;
};
plugin.app.workspace.on('active-leaf-change',activeLeafChangeEventHandler);
plugin.app.workspace.on("active-leaf-change",activeLeafChangeEventHandler);
this.workspaceEventHandlers.set("active-leaf-change",activeLeafChangeEventHandler);
}
@@ -626,6 +766,8 @@ export default class ExcalidrawPlugin extends Plugin {
this.app.vault.off(key,this.vaultEventHandlers.get(key))
for(const key of this.workspaceEventHandlers.keys())
this.app.workspace.off(key,this.workspaceEventHandlers.get(key));
this.observer.disconnect();
this.linkIndex.deregisterEventHandlers();
}
public embedDrawing(data:string) {
@@ -650,7 +792,7 @@ export default class ExcalidrawPlugin extends Plugin {
const e = document.createEvent("Event")
e.initEvent(RERENDER_EVENT,true,false);
document
.querySelectorAll("div[class^='excalidraw-svg']"+ (filepath ? "[src='"+filepath+"']" : ""))
.querySelectorAll("div[class^='excalidraw-svg']"+ (filepath ? "[src='"+filepath.replaceAll("'","\\'")+"']" : ""))
.forEach((el) => el.dispatchEvent(e));
}
@@ -680,25 +822,33 @@ export default class ExcalidrawPlugin extends Plugin {
}
private getNextDefaultFilename():string {
return 'Drawing ' + window.moment().format('YYYY-MM-DD HH.mm.ss')+'.'+EXCALIDRAW_FILE_EXTENSION;
return this.settings.drawingFilenamePrefix + window.moment().format(this.settings.drawingFilenameDateTime)+'.'+EXCALIDRAW_FILE_EXTENSION;
}
public async createDrawing(filename: string, onNewPane: boolean, foldername?: string, initData?:string) {
const folderpath = normalizePath(foldername ? foldername: this.settings.folder);
const fname = folderpath +'/'+ filename;
let fname = folderpath +'/'+ filename;
const folder = this.app.vault.getAbstractFileByPath(folderpath);
if (!(folder && folder instanceof TFolder)) {
await this.app.vault.createFolder(folderpath);
}
let file:TAbstractFile = this.app.vault.getAbstractFileByPath(fname);
let i = 0;
while(file) {
fname = folderpath + '/' + filename.slice(0,filename.lastIndexOf("."))+"_"+i+filename.slice(filename.lastIndexOf("."));
i++;
file = this.app.vault.getAbstractFileByPath(fname);
}
if(initData) {
this.openDrawing(await this.app.vault.create(fname,initData),onNewPane);
return;
}
const file = this.app.vault.getAbstractFileByPath(normalizePath(this.settings.templateFilePath));
if(file && file instanceof TFile) {
const content = await this.app.vault.read(file);
const template = this.app.vault.getAbstractFileByPath(normalizePath(this.settings.templateFilePath));
if(template && template instanceof TFile) {
const content = await this.app.vault.read(template);
this.openDrawing(await this.app.vault.create(fname,content==''?BLANK_DRAWING:content), onNewPane);
} else {
this.openDrawing(await this.app.vault.create(fname,BLANK_DRAWING), onNewPane);

View File

@@ -11,7 +11,8 @@ import {
export enum openDialogAction {
openFile,
insertLink,
insertLinkToDrawing,
insertLink
}
export class OpenFileDialog extends FuzzySuggestModal<TFile> {
@@ -19,17 +20,15 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
private plugin: ExcalidrawPlugin;
private action: openDialogAction;
private onNewPane: boolean;
private addText: Function;
private drawingPath: string;
constructor(app: App, plugin: ExcalidrawPlugin) {
super(app);
this.app = app;
this.action = openDialogAction.openFile;
this.plugin = plugin;
this.onNewPane = false;
this.setInstructions([{
command: "Type name of drawing to select.",
purpose: "",
}]);
this.inputEl.onkeyup = (e) => {
if(e.key=="Enter" && this.action == openDialogAction.openFile) {
@@ -43,11 +42,11 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
getItems(): TFile[] {
const excalidrawFiles = this.app.vault.getFiles();
return (excalidrawFiles || []).filter((f:TFile) => (f.extension==EXCALIDRAW_FILE_EXTENSION));
return (excalidrawFiles || []).filter((f:TFile) => (this.action == openDialogAction.insertLink) || (f.extension==EXCALIDRAW_FILE_EXTENSION));
}
getItemText(item: TFile): string {
return item.basename;
return item.path; //this.action == openDialogAction.insertLink ? item.path : item.basename;
}
onChooseItem(item: TFile, _evt: MouseEvent | KeyboardEvent): void {
@@ -55,13 +54,35 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
case(openDialogAction.openFile):
this.plugin.openDrawing(item, this.onNewPane);
break;
case(openDialogAction.insertLink):
case(openDialogAction.insertLinkToDrawing):
this.plugin.embedDrawing(item.path);
break;
case(openDialogAction.insertLink):
//@ts-ignore
const filepath = this.app.metadataCache.getLinkpathDest(item.path,this.drawingPath)[0].path;
this.addText("[["+(filepath.endsWith(".md")?filepath.substr(0,filepath.length-3):filepath)+"]]"); //.md files don't need the extension
break;
}
}
start(action:openDialogAction, onNewPane: boolean): void {
public insertLink(drawingPath:string, addText: Function) {
this.action = openDialogAction.insertLink;
this.addText = addText;
this.drawingPath = drawingPath;
this.setInstructions([{
command: "Select a file then hit enter.",
purpose: "",
}]);
this.emptyStateText = "No file matches your query.";
this.setPlaceholder("Select existing file to insert link into drawing.");
this.open();
}
public start(action:openDialogAction, onNewPane: boolean): void {
this.setInstructions([{
command: "Type name of drawing to select.",
purpose: "",
}]);
this.action = action;
this.onNewPane = onNewPane;
switch(action) {
@@ -69,7 +90,7 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
this.emptyStateText = EMPTY_MESSAGE;
this.setPlaceholder("Select existing drawing or type name of new and hit enter.");
break;
case (openDialogAction.insertLink):
case (openDialogAction.insertLinkToDrawing):
this.emptyStateText = "No file matches your query.";
this.setPlaceholder("Select existing drawing to insert into document.");
break;

View File

@@ -4,12 +4,16 @@ import {
PluginSettingTab,
Setting
} from 'obsidian';
import { EXCALIDRAW_FILE_EXTENSION } from './constants';
import type ExcalidrawPlugin from "./main";
export interface ExcalidrawSettings {
folder: string,
templateFilePath: string,
drawingFilenamePrefix: string,
drawingFilenameDateTime: string,
width: string,
validLinksOnly: boolean, //valid link as in [[valid Obsidian link]] - how to treat text elements in drawings
exportWithTheme: boolean,
exportWithBackground: boolean,
autoexportSVG: boolean,
@@ -25,7 +29,10 @@ export interface ExcalidrawSettings {
export const DEFAULT_SETTINGS: ExcalidrawSettings = {
folder: 'Excalidraw',
templateFilePath: 'Excalidraw/Template.excalidraw',
drawingFilenamePrefix: 'Drawing ',
drawingFilenameDateTime: 'YYYY-MM-DD HH.mm.ss',
width: '400',
validLinksOnly: false,
exportWithTheme: true,
exportWithBackground: true,
autoexportSVG: false,
@@ -50,6 +57,20 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
let {containerEl} = this;
this.containerEl.empty();
new Setting(containerEl)
.setName('Default width of embedded (transcluded) image')
.setDesc('The default width of an embedded drawing. You can specify a different ' +
'width when embedding an image using the ![[drawing.excalidraw|100]] or ' +
'[[drawing.excalidraw|100x100]] format.')
.addText(text => text
.setPlaceholder('400')
.setValue(this.plugin.settings.width)
.onChange(async (value) => {
this.plugin.settings.width = value;
await this.plugin.saveSettings();
this.plugin.triggerEmbedUpdates();
}));
new Setting(containerEl)
.setName('Excalidraw folder')
.setDesc('Default location for your Excalidraw drawings. Leaving this empty means drawings will be created in the Vault root.')
@@ -74,20 +95,73 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
}));
this.containerEl.createEl('h1', {text: 'New drawing filename'});
containerEl.createDiv('',(el) => {
el.innerHTML = '<p>The automatically generated filename consists of a prefix and a date. ' +
'e.g."Drawing 2021-05-24 12.58.07".</p>'+
'<p>Click this link for the <a href="https://momentjs.com/docs/#/displaying/format/">'+
'date and time format reference</a>.</p>';
});
const getFilenameSample = () => {
return 'The current file format is: <b>' +
this.plugin.settings.drawingFilenamePrefix +
window.moment().format(this.plugin.settings.drawingFilenameDateTime) +
'.' + EXCALIDRAW_FILE_EXTENSION + '</b>';
};
const filenameEl = containerEl.createEl('p',{text: ''});
filenameEl.innerHTML = getFilenameSample();
new Setting(containerEl)
.setName('Default width of embedded (transcluded) image')
.setDesc('The default width of an embedded drawing. You can specify a different ' +
'width when embedding an image using the [[drawing.excalidraw|100]] or ' +
'[[drawing.excalidraw|100x100]] format.')
.setName('Filename prefix')
.setDesc('The first part of the filename')
.addText(text => text
.setPlaceholder('400')
.setValue(this.plugin.settings.width)
.setPlaceholder('Drawing ')
.setValue(this.plugin.settings.drawingFilenamePrefix)
.onChange(async (value) => {
this.plugin.settings.width = value;
this.plugin.settings.drawingFilenamePrefix = value.replaceAll(/[<>:"/\\|?*]/g,'_');
text.setValue(this.plugin.settings.drawingFilenamePrefix);
filenameEl.innerHTML = getFilenameSample();
await this.plugin.saveSettings();
this.plugin.triggerEmbedUpdates();
}));
new Setting(containerEl)
.setName('Filename date')
.setDesc('The second part of the filename')
.addText(text => text
.setPlaceholder('YYYY-MM-DD HH.mm.ss')
.setValue(this.plugin.settings.drawingFilenameDateTime)
.onChange(async (value) => {
this.plugin.settings.drawingFilenameDateTime = value.replaceAll(/[<>:"/\\|?*]/g,'_');
text.setValue(this.plugin.settings.drawingFilenameDateTime);
filenameEl.innerHTML = getFilenameSample();
await this.plugin.saveSettings();
}));
this.containerEl.createEl('h1', {text: 'Links in drawings'});
this.containerEl.createEl('p',{
text: 'You can CTRL/META + click on text elements in your drawings to open them as links. ' +
'By default the plugin will handle any text as a link, and will try to open it. ' +
'If the text element includes a [[valid Obsidian link]] then the rest of the text element will be ignored ' +
'and only the [[valid Obsidian link]] will be processed as a link. ' +
'If the text element starts as a valid web link (i.e. https:// or http://), then it will be treated as a web link ' +
'and the plugin will try to open it in a browser window. ' +
'The plugin indexes your drawings, and when Obsidian files change, the matching text in your drawings will also change. ' +
'If you don\'t want text accidentally changing in your drawings, you can set the below toggle to limit the link ' +
'feature to only [[valid Obsidian links]].'});
new Setting(containerEl)
.setName('Accept only [[valid Obsidian links]]')
.setDesc('If this is on, text in text elements will be ignored unless they contain a [[valid Obsidian link]]')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.validLinksOnly)
.onChange(async (value) => {
this.plugin.settings.validLinksOnly = value;
this.plugin.reloadIndex();
await this.plugin.saveSettings();
}));
this.containerEl.createEl('h1', {text: 'Embedded image settings'});
new Setting(containerEl)

View File

@@ -48,3 +48,16 @@ div.excalidraw-svg-left {
button.ToolIcon_type_button[title="Export"] {
display:none;
}
.excalidraw-prompt-div {
display: flex;
}
.excalidraw-prompt-form {
display: flex;
flex-grow: 1;
}
.excalidraw-prompt-input {
flex-grow: 1;
}

View File

@@ -11,9 +11,9 @@
"importHelpers": true,
"lib": [
"dom",
"es5",
"scripthost",
"es2020",
"esnext",
"DOM.Iterable"
],
"jsx": "react",

View File

@@ -1,3 +1,3 @@
{
"1.1.4": "0.11.13"
"1.1.9": "0.11.13"
}