mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f28c974f7 | ||
|
|
1b9fa6a790 | ||
|
|
4dd8271223 | ||
|
|
3e62cd33a2 | ||
|
|
c670ecb09c | ||
|
|
f5307db33e | ||
|
|
7e216306c0 | ||
|
|
e3daf5d22e | ||
|
|
a85a46cbae | ||
|
|
ac16f5427b | ||
|
|
edc92472df | ||
|
|
e66b7aef7f | ||
|
|
7e9d5e8867 | ||
|
|
167918f718 | ||
|
|
65af29c2ef | ||
|
|
b19e1b6dcb | ||
|
|
f7263543fa | ||
|
|
c6339b28ac | ||
|
|
f24e4fce9c | ||
|
|
5eff9b2e54 | ||
|
|
d89c019612 | ||
|
|
01d3c13cce | ||
|
|
de68ebbe7d | ||
|
|
caee4f7500 | ||
|
|
6d28546677 | ||
|
|
ec5a13f9e4 | ||
|
|
b788118880 |
@@ -41,7 +41,10 @@ To convert files you have the following options:
|
||||
- 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
|
||||
- Transclusions are supported
|
||||
- `![[myfile#^blockref]]` will convert in the drawing into the transcluded text of the block
|
||||
- `![[myfile#section]]` also works, this will transclude the section
|
||||
- you can also specify word wrapping for transcluded text by adding the max character count in curly brackets right after the transclusion e.g. `![[myfile#^blockref]]{40}` will wrap text at 40 characters.
|
||||
- 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
|
||||
@@ -54,6 +57,10 @@ To convert files you have the following options:
|
||||
- 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.
|
||||
- The following front matter keys will customize how the drawing is displayed - overriding general settings:
|
||||
- `excalidraw-link-prefix: "📍"` preview prefix for internal links
|
||||
- `excalidraw-url-prefix: "🌐"` preview prefix for external links
|
||||
- `excalidraw-link-brackets: true|false` whether or not to display brackets around links in preview
|
||||
- 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.
|
||||
|
||||
@@ -2,44 +2,58 @@
|
||||
## Attributes and functions overview
|
||||
Here's the interface implemented by ExcalidrawAutomate:
|
||||
|
||||
```javascript
|
||||
ExcalidrawAutomate: {
|
||||
style: {
|
||||
strokeColor: string;
|
||||
backgroundColor: string;
|
||||
angle: number;
|
||||
fillStyle: FillStyle;
|
||||
strokeWidth: number;
|
||||
storkeStyle: StrokeStyle;
|
||||
roughness: number;
|
||||
opacity: number;
|
||||
strokeSharpness: StrokeSharpness;
|
||||
fontFamily: FontFamily;
|
||||
fontSize: number;
|
||||
textAlign: string;
|
||||
verticalAlign: string;
|
||||
startArrowHead: string;
|
||||
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;
|
||||
```typescript
|
||||
ExcalidrawAutomate: {
|
||||
plugin: ExcalidrawPlugin;
|
||||
elementsDict: {};
|
||||
style: {
|
||||
strokeColor: string;
|
||||
backgroundColor: string;
|
||||
angle: number;
|
||||
fillStyle: FillStyle;
|
||||
strokeWidth: number;
|
||||
storkeStyle: StrokeStyle;
|
||||
roughness: number;
|
||||
opacity: number;
|
||||
strokeSharpness: StrokeSharpness;
|
||||
fontFamily: number;
|
||||
fontSize: number;
|
||||
textAlign: string;
|
||||
verticalAlign: string;
|
||||
startArrowHead: string;
|
||||
endArrowHead: string;
|
||||
}
|
||||
canvas: {theme: string, viewBackgroundColor: string, gridSize: number};
|
||||
setFillStyle(val:number): void;
|
||||
setStrokeStyle(val:number): void;
|
||||
setStrokeSharpness(val:number): void;
|
||||
setFontFamily(val:number): void;
|
||||
setTheme(val:number): void;
|
||||
addToGroup(objectIds:[]):string;
|
||||
toClipboard(templatePath?:string): void;
|
||||
getElements():ExcalidrawElement[];
|
||||
getElement(id:string):ExcalidrawElement;
|
||||
create(params?:{filename?: string, foldername?:string, templatePath?:string, onNewPane?: boolean}):Promise<void>;
|
||||
createSVG(templatePath?:string):Promise<SVGSVGElement>;
|
||||
createPNG(templatePath?:string):Promise<any>;
|
||||
wrapText(text:string, lineLen:number):string;
|
||||
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},id?:string):string;
|
||||
addLine(points: [[x:number,y:number]]):string;
|
||||
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead?:string,endArrowHead?:string,startObjectId?:string,endObjectId?:string}):string;
|
||||
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints?: number,startArrowHead?:string,endArrowHead?:string, padding?: number}):void;
|
||||
clear(): void;
|
||||
reset(): void;
|
||||
isExcalidrawFile(f:TFile): boolean;
|
||||
//view manipulation
|
||||
targetView: ExcalidrawView;
|
||||
setView(view:ExcalidrawView|"first"|"active"):ExcalidrawView;
|
||||
getExcalidrawAPI():any;
|
||||
getViewSelectedElement():ExcalidrawElement;
|
||||
connectObjectWithViewSelectedElement(objectA:string,connectionA: ConnectionPoint, connectionB: ConnectionPoint, formatting?:{numberOfPoints?: number,startArrowHead?:string,endArrowHead?:string, padding?: number}):boolean;
|
||||
addElementsToView(repositionToCursor:boolean, save:boolean):Promise<boolean>;
|
||||
};
|
||||
```
|
||||
```
|
||||
|
||||
|
||||
@@ -12,4 +12,7 @@ String. Valid values are "light" and "dark".
|
||||
### viewBackgroundColor
|
||||
String. This is the fill color of an object. [CSS Legal Color Values](https://www.w3schools.com/cssref/css_colors_legal.asp)
|
||||
|
||||
Allowed values are [HTML color names](https://www.w3schools.com/colors/colors_names.asp), hexadecimal RGB strings e.g. `#FF0000` for red, or `transparent`.
|
||||
Allowed values are [HTML color names](https://www.w3schools.com/colors/colors_names.asp), hexadecimal RGB strings e.g. `#FF0000` for red, or `transparent`.
|
||||
|
||||
### gridSize
|
||||
Number. The size of the grid. If set to zero, no grid is displayed.
|
||||
@@ -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 starting 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
|
||||
@@ -15,11 +15,11 @@ The second line resets ExcalidrawAutomate to defaults. This is important as you
|
||||
### Basic logic of using Excalidraw Automate
|
||||
1. Set the styling of the elements you want to draw
|
||||
2. Add elements. As you add elements, each new element is added one layer above the previous, thus in case of overlapping objects the later one will be on the top of the prior one.
|
||||
3. Call `await ea.create();` to instantiate the drawing
|
||||
3. Call `await ea.create();` to instantiate the drawing, or use `ea.setView();` followed by `ea.addElementsToView();` to add your elements to an existing view, or create a PNG or SVG image out of your elements using `await ea.createSVG();` or `await ea.createPNG();`;
|
||||
|
||||
You can change styling between adding different elements. My logic for separating element styling and creation is based on the assumption that you will probably set a stroke color, stroke style, stroke roughness, etc. and draw most of your elements using this. There would be no point in setting all these parameters each time you add an element.
|
||||
You can change the styling between adding different elements. My logic for separating element styling and creation is based on the assumption that you will probably set a stroke color, stroke style, stroke roughness, etc. and draw most of your elements using that. There would be no point in setting all these parameters each time you add an element.
|
||||
|
||||
### Before we dive deeper, here are two a simple example scripts
|
||||
### Before we dive deeper, here are three a simple example Templater scripts
|
||||
#### Create a new drawing with custom name, in a custom folder, using a template
|
||||
This simple script gives you significant additional flexibility over Excalidraw Plugin settings to name your drawings, place them into folders, and to apply templates.
|
||||
|
||||
@@ -55,4 +55,46 @@ This simple script gives you significant additional flexibility over Excalidraw
|
||||
```
|
||||
The script will generate the following drawing:
|
||||
|
||||

|
||||

|
||||
|
||||
#### Add a TextElement in a box to an open Excalidraw View.
|
||||
Position the new element under the currently selected element, with an arrow from the selected element to the added text.
|
||||
|
||||
*Use CTRL+Shift+V to paste code into Obsidian!*
|
||||
```javascript
|
||||
<%*
|
||||
const ea = ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
ea.setView("first");
|
||||
selectedElement = ea.getViewSelectedElement();
|
||||
ea.setStrokeSharpness(0);
|
||||
const boxPadding = 5;
|
||||
id = ea.addText(
|
||||
selectedElement.x + boxPadding,
|
||||
selectedElement.y+selectedElement.height+100,
|
||||
"[[Next process step]]",
|
||||
{
|
||||
textAlign:"center",
|
||||
box:true,
|
||||
boxPadding:boxPadding,
|
||||
width:selectedElement.width-boxPadding*2,
|
||||
}
|
||||
);
|
||||
ea.setStrokeSharpness(1);
|
||||
ea.style.roughness= 0;
|
||||
ea.connectObjectWithViewSelectedElement(
|
||||
id,
|
||||
"top",
|
||||
"bottom",
|
||||
{
|
||||
numberOfPoints:2,
|
||||
startArrowHead:"arrow",
|
||||
endArrowHead:"dot",
|
||||
padding:5
|
||||
});
|
||||
ea.addElementsToView();
|
||||
%>
|
||||
```
|
||||
[Click here to view animation](https://user-images.githubusercontent.com/14358394/131967188-2a488e38-f742-49d9-ae98-33238a8d4712.mp4)
|
||||
|
||||
|
||||
|
||||
@@ -28,13 +28,15 @@ Returns the `id` of the object. The `id` is required when connecting objects wit
|
||||
|
||||
### addLine()
|
||||
```typescript
|
||||
addLine(points: [[x:number,y:number]]):void
|
||||
addLine(points: [[x:number,y:number]]):string
|
||||
```
|
||||
Adds a line following the points provided. Must include at least two points `points.length >= 2`. If more than 2 points are provided the interim points will be added as breakpoints. The line will break with angles if `strokeSharpness` is set to "sharp" and will be curvey if it is set to "round".
|
||||
|
||||
Returns the `id` of the object.
|
||||
|
||||
### addArrow()
|
||||
```typescript
|
||||
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead:string,endArrowHead:string,startObjectId:string,endObjectId:string}):void
|
||||
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead?:string,endArrowHead?:string,startObjectId?:string,endObjectId?:string}):string ;
|
||||
```
|
||||
|
||||
Adds an arrow following the points provided. Must include at least two points `points.length >= 2`. If more than 2 points are provided the interim points will be added as breakpoints. The line will break with angles if element `style.strokeSharpness` is set to "sharp" and will be curvey if it is set to "round".
|
||||
@@ -43,6 +45,8 @@ Adds an arrow following the points provided. Must include at least two points `p
|
||||
|
||||
`startObjectId` and `endObjectId` are the object id's of connected objects. I recommend using `connectObjects` instead calling addArrow() for the purpose of connecting objects.
|
||||
|
||||
Returns the `id` of the object.
|
||||
|
||||
### connectObjects()
|
||||
```typescript
|
||||
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
|
||||
@@ -60,6 +64,6 @@ Connects two objects with an arrow.
|
||||
|
||||
### addToGroup()
|
||||
```typescript
|
||||
addToGroup(objectIds:[]):void
|
||||
addToGroup(objectIds:[]):string
|
||||
```
|
||||
Groups objects listed in `objectIds`.
|
||||
Groups objects listed in `objectIds`. Returns the `id` of the group.
|
||||
@@ -18,6 +18,19 @@ 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 existing drawing.
|
||||
|
||||
### getElements()
|
||||
```typescript
|
||||
getElements():ExcalidrawElement[];
|
||||
```
|
||||
Returns the elements in ExcalidrawAutomate as an array of ExcalidrawElements. This format is usefull when working with ExcalidrawRef.
|
||||
|
||||
### getElement()
|
||||
```typescript
|
||||
getElement(id:string):ExcalidrawElement;
|
||||
```
|
||||
|
||||
Returns the element object matching the id. If the element does not exist, returns null.
|
||||
|
||||
### create()
|
||||
```typescript
|
||||
async create(params?:{filename: string, foldername:string, templatePath:string, onNewPane: boolean})
|
||||
@@ -46,4 +59,63 @@ Returns an HTML SVGSVGElement containing the generated drawing.
|
||||
```typescript
|
||||
async createPNG(templatePath?:string, scale:number=1)
|
||||
```
|
||||
Returns a blob containing a PNG image of the generated drawing.
|
||||
Returns a blob containing a PNG image of the generated drawing.
|
||||
|
||||
### wrapText()
|
||||
```typescript
|
||||
wrapText(text:string, lineLen:number):string
|
||||
```
|
||||
Returns a string wrapped to the provided max lineLen.
|
||||
|
||||
|
||||
### Accessing the open Excalidraw view
|
||||
You first need to initialize targetView, before using any of the view manipulation functions.
|
||||
|
||||
#### targetView
|
||||
```typescript
|
||||
targetView: ExcalidrawView
|
||||
```
|
||||
The open Excalidraw View configured as the target of the view operations. User `setView` to initialize.
|
||||
|
||||
#### setView()
|
||||
```typescript
|
||||
setView(view:ExcalidrawView|"first"|"active"):ExcalidrawView
|
||||
```
|
||||
Setting the ExcalidrawView that will be the target of the View operations. Valid `view` input values are:
|
||||
- an object instance of ExcalidrawView
|
||||
- "first": meaning if there are multiple Excalidraw Views open, pick the first that is returned by `app.workspace.getLeavesOfType("Excalidraw")`
|
||||
- "active": meaning the currently active view
|
||||
|
||||
#### getExcalidrawAPI()
|
||||
```typescript
|
||||
getExcalidrawAPI():any
|
||||
```
|
||||
Returns the native Excalidraw API (ref.current) for the active drawing specified in `targetView`.
|
||||
See Excalidraw documentation here: https://www.npmjs.com/package/@excalidraw/excalidraw#ref
|
||||
|
||||
#### getViewSelectedElement()
|
||||
```typescript
|
||||
getViewSelectedElement():ExcalidrawElement
|
||||
```
|
||||
If an element is selected in the targetView the function returns the selected element. If multiple elements are selected, either by SHIFT+Clicking to select multiple elements, or by selecting a group, the first of the elements will be selected. If you want to specify which element to select from a group, double click the desired element in the group.
|
||||
|
||||
This function is helpful if you want to add a new element in relation to an existing element in your drawing.
|
||||
|
||||
#### connectObjectWithViewSelectedElement()
|
||||
```typescript
|
||||
connectObjectWithViewSelectedElement(objectA:string,connectionA: ConnectionPoint, connectionB: ConnectionPoint, formatting?:{numberOfPoints?: number,startArrowHead?:string,endArrowHead?:string, padding?: number}):boolean
|
||||
```
|
||||
Same as `connectObjects()`, but ObjectB is the currently selected element in the target ExcalidrawView. The function helps with placing an arrow between a newly created object and the selected element in the target ExcalidrawView.
|
||||
|
||||
#### addElementsToView()
|
||||
```typescript
|
||||
async addElementsToView(repositionToCursor:boolean=false, save:boolean=false):Promise<boolean>
|
||||
```
|
||||
Adds elements created with ExcalidrawAutomate to the target ExcalidrawView.
|
||||
`repositionToCursor` dafault is false
|
||||
- true: the elements will be moved such that the center point of the elements will be aligned with the current position of the pointer on ExcalidrawView. You can point and place elements to a desired location in your drawing using this switch.
|
||||
- false: elements will be positioned as defined by the x&y coordinates of each element.
|
||||
|
||||
`save` default is false
|
||||
- true: the drawing will be saved after the elements were added.
|
||||
- false: the drawing will be saved at the next autosave cycle. Use false when adding multiple elements one after the other. Else, best to use true, to minimize risk of data loss.
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "1.2.12",
|
||||
"version": "1.3.2",
|
||||
"minAppVersion": "0.12.0",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@zsviczian/excalidraw": "^0.9.0-onTextEditEvents-4",
|
||||
"@zsviczian/excalidraw": "0.9.0-obsidian-7",
|
||||
"monkey-around": "^2.2.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
|
||||
@@ -3,10 +3,10 @@ import {
|
||||
FillStyle,
|
||||
StrokeStyle,
|
||||
StrokeSharpness,
|
||||
ExcalidrawElement,
|
||||
} from "@zsviczian/excalidraw/types/element/types";
|
||||
import {
|
||||
normalizePath,
|
||||
Notice,
|
||||
TFile
|
||||
} from "obsidian"
|
||||
import ExcalidrawView from "./ExcalidrawView";
|
||||
@@ -14,16 +14,17 @@ import { getJSON } from "./ExcalidrawData";
|
||||
import {
|
||||
FRONTMATTER,
|
||||
nanoid,
|
||||
JSON_parse
|
||||
JSON_parse,
|
||||
VIEW_TYPE_EXCALIDRAW
|
||||
} from "./constants";
|
||||
import { wrapText } from "./Utils";
|
||||
|
||||
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
|
||||
|
||||
export interface ExcalidrawAutomate extends Window {
|
||||
ExcalidrawAutomate: {
|
||||
plugin: ExcalidrawPlugin;
|
||||
elementIds: [];
|
||||
elementsDict: {},
|
||||
elementsDict: {};
|
||||
style: {
|
||||
strokeColor: string;
|
||||
backgroundColor: string;
|
||||
@@ -41,27 +42,37 @@ export interface ExcalidrawAutomate extends Window {
|
||||
startArrowHead: string;
|
||||
endArrowHead: string;
|
||||
}
|
||||
canvas: {theme: string, viewBackgroundColor: string};
|
||||
canvas: {theme: string, viewBackgroundColor: string, gridSize: number};
|
||||
setFillStyle(val:number): void;
|
||||
setStrokeStyle(val:number): void;
|
||||
setStrokeSharpness(val:number): void;
|
||||
setFontFamily(val:number): void;
|
||||
setTheme(val:number): void;
|
||||
addToGroup(objectIds:[]):void;
|
||||
addToGroup(objectIds:[]):string;
|
||||
toClipboard(templatePath?:string): void;
|
||||
create(params?:{filename: string, foldername:string, templatePath:string, onNewPane: boolean}):Promise<void>;
|
||||
getElements():ExcalidrawElement[];
|
||||
getElement(id:string):ExcalidrawElement;
|
||||
create(params?:{filename?: string, foldername?:string, templatePath?:string, onNewPane?: boolean}):Promise<void>;
|
||||
createSVG(templatePath?:string):Promise<SVGSVGElement>;
|
||||
createPNG(templatePath?:string):Promise<any>;
|
||||
wrapText(text:string, lineLen:number):string;
|
||||
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},id?:string):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;
|
||||
addLine(points: [[x:number,y:number]]):string;
|
||||
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead?:string,endArrowHead?:string,startObjectId?:string,endObjectId?:string}):string ;
|
||||
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints?: number,startArrowHead?:string,endArrowHead?:string, padding?: number}):void;
|
||||
clear(): void;
|
||||
reset(): void;
|
||||
isExcalidrawFile(f:TFile): boolean;
|
||||
//view manipulation
|
||||
targetView: ExcalidrawView;
|
||||
setView(view:ExcalidrawView|"first"|"active"):ExcalidrawView;
|
||||
getExcalidrawAPI():any;
|
||||
getViewSelectedElement():ExcalidrawElement;
|
||||
connectObjectWithViewSelectedElement(objectA:string,connectionA: ConnectionPoint, connectionB: ConnectionPoint, formatting?:{numberOfPoints?: number,startArrowHead?:string,endArrowHead?:string, padding?: number}):boolean;
|
||||
addElementsToView(repositionToCursor:boolean, save:boolean):Promise<boolean>;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -70,7 +81,7 @@ declare let window: ExcalidrawAutomate;
|
||||
export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
window.ExcalidrawAutomate = {
|
||||
plugin: plugin,
|
||||
elementIds: [],
|
||||
//elementIds: [],
|
||||
elementsDict: {},
|
||||
style: {
|
||||
strokeColor: "#000000",
|
||||
@@ -87,9 +98,9 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
textAlign: "left",
|
||||
verticalAlign: "top",
|
||||
startArrowHead: null,
|
||||
endArrowHead: "arrow"
|
||||
endArrowHead: "arrow",
|
||||
},
|
||||
canvas: {theme: "light", viewBackgroundColor: "#FFFFFF"},
|
||||
canvas: {theme: "light", viewBackgroundColor: "#FFFFFF", gridSize: 0},
|
||||
setFillStyle (val:number) {
|
||||
switch(val) {
|
||||
case 0:
|
||||
@@ -149,35 +160,43 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
return "dark";
|
||||
}
|
||||
},
|
||||
addToGroup(objectIds:[]):void {
|
||||
addToGroup(objectIds:[]):string {
|
||||
const id = nanoid();
|
||||
objectIds.forEach((objectId)=>{
|
||||
this.elementsDict[objectId]?.groupIds?.push(id);
|
||||
});
|
||||
return id;
|
||||
},
|
||||
async toClipboard(templatePath?:string) {
|
||||
const template = templatePath ? (await getTemplate(templatePath)) : null;
|
||||
let elements = template ? template.elements : [];
|
||||
for (let i=0;i<this.elementIds.length;i++) {
|
||||
elements.push(this.elementsDict[this.elementIds[i]]);
|
||||
}
|
||||
elements.concat(this.getElements());
|
||||
navigator.clipboard.writeText(
|
||||
JSON.stringify({
|
||||
"type":"excalidraw/clipboard",
|
||||
"elements": elements,
|
||||
}));
|
||||
},
|
||||
async create(params?:{filename: string, foldername:string, templatePath:string, onNewPane: boolean}) {
|
||||
getElements():ExcalidrawElement[] {
|
||||
let elements=[];
|
||||
const elementIds = Object.keys(this.elementsDict);
|
||||
for (let i=0;i<elementIds.length;i++) {
|
||||
elements.push(this.elementsDict[elementIds[i]]);
|
||||
}
|
||||
return elements;
|
||||
},
|
||||
getElement(id:string):ExcalidrawElement {
|
||||
return this.elementsDict[id];
|
||||
},
|
||||
async create(params?:{filename?: string, foldername?:string, templatePath?:string, onNewPane?: boolean}) {
|
||||
const template = params?.templatePath ? (await getTemplate(params.templatePath)) : null;
|
||||
let elements = template ? template.elements : [];
|
||||
for (let i=0;i<this.elementIds.length;i++) {
|
||||
elements.push(this.elementsDict[this.elementIds[i]]);
|
||||
}
|
||||
elements = elements.concat(this.getElements());
|
||||
plugin.createDrawing(
|
||||
params?.filename ? params.filename + '.excalidraw.md' : this.plugin.getNextDefaultFilename(),
|
||||
params?.onNewPane ? params.onNewPane : false,
|
||||
params?.foldername ? params.foldername : this.plugin.settings.folder,
|
||||
FRONTMATTER + exportSceneToMD(
|
||||
FRONTMATTER + plugin.exportSceneToMD(
|
||||
JSON.stringify({
|
||||
type: "excalidraw",
|
||||
version: 2,
|
||||
@@ -200,6 +219,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
currentItemStartArrowhead: template? template.appState.currentItemStartArrowhead: this.style.startArrowHead,
|
||||
currentItemEndArrowhead: template? template.appState.currentItemEndArrowhead : this.style.endArrowHead,
|
||||
currentItemLinearStrokeSharpness: template? template.appState.currentItemLinearStrokeSharpness : this.style.strokeSharpness,
|
||||
gridSize: template ? template.appState.gridSize : this.canvas.gridSize
|
||||
}
|
||||
}))
|
||||
);
|
||||
@@ -207,9 +227,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
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++) {
|
||||
elements.push(this.elementsDict[this.elementIds[i]]);
|
||||
}
|
||||
elements.concat(this.getElements());
|
||||
return await ExcalidrawView.getSVG(
|
||||
{//createDrawing
|
||||
"type": "excalidraw",
|
||||
@@ -230,9 +248,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
async createPNG(templatePath?:string, scale:number=1) {
|
||||
const template = templatePath ? (await getTemplate(templatePath)) : null;
|
||||
let elements = template ? template.elements : [];
|
||||
for (let i=0;i<this.elementIds.length;i++) {
|
||||
elements.push(this.elementsDict[this.elementIds[i]]);
|
||||
}
|
||||
elements.concat(this.getElements());
|
||||
return ExcalidrawView.getPNG(
|
||||
{
|
||||
"type": "excalidraw",
|
||||
@@ -251,21 +267,24 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
scale
|
||||
)
|
||||
},
|
||||
wrapText(text:string, lineLen:number):string {
|
||||
return wrapText(text,lineLen);
|
||||
},
|
||||
addRect(topX:number, topY:number, width:number, height:number):string {
|
||||
const id = nanoid();
|
||||
this.elementIds.push(id);
|
||||
//this.elementIds.push(id);
|
||||
this.elementsDict[id] = boxedElement(id,"rectangle",topX,topY,width,height);
|
||||
return id;
|
||||
},
|
||||
addDiamond(topX:number, topY:number, width:number, height:number):string {
|
||||
const id = nanoid();
|
||||
this.elementIds.push(id);
|
||||
//this.elementIds.push(id);
|
||||
this.elementsDict[id] = boxedElement(id,"diamond",topX,topY,width,height);
|
||||
return id;
|
||||
},
|
||||
addEllipse(topX:number, topY:number, width:number, height:number):string {
|
||||
const id = nanoid();
|
||||
this.elementIds.push(id);
|
||||
//this.elementIds.push(id);
|
||||
this.elementsDict[id] = boxedElement(id,"ellipse",topX,topY,width,height);
|
||||
return id;
|
||||
},
|
||||
@@ -274,7 +293,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
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);
|
||||
//this.elementIds.push(id);
|
||||
this.elementsDict[id] = {
|
||||
text: text,
|
||||
fontSize: window.ExcalidrawAutomate.style.fontSize,
|
||||
@@ -285,17 +304,17 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
... boxedElement(id,"text",topX,topY,width,height)
|
||||
};
|
||||
if(formatting?.box) {
|
||||
const boxPadding = formatting?.boxPadding ? formatting.boxPadding : 10;
|
||||
const boxPadding = formatting?.boxPadding!=null ? formatting.boxPadding : 10;
|
||||
const boxId = this.addRect(topX-boxPadding,topY-boxPadding,width+2*boxPadding,height+2*boxPadding);
|
||||
this.addToGroup([id,boxId])
|
||||
return boxId;
|
||||
}
|
||||
return id;
|
||||
},
|
||||
addLine(points: [[x:number,y:number]]):void {
|
||||
addLine(points: [[x:number,y:number]]):string {
|
||||
const box = getLineBox(points);
|
||||
const id = nanoid();
|
||||
this.elementIds.push(id);
|
||||
//this.elementIds.push(id);
|
||||
this.elementsDict[id] = {
|
||||
points: normalizeLinePoints(points),
|
||||
lastCommittedPoint: null,
|
||||
@@ -305,11 +324,12 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
endArrowhead: null,
|
||||
... boxedElement(id,"line",box.x,box.y,box.w,box.h)
|
||||
};
|
||||
return id;
|
||||
},
|
||||
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead:string,endArrowHead:string,startObjectId:string,endObjectId:string}):void {
|
||||
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead?:string,endArrowHead?:string,startObjectId?:string,endObjectId?:string}):string {
|
||||
const box = getLineBox(points);
|
||||
const id = nanoid();
|
||||
this.elementIds.push(id);
|
||||
//this.elementIds.push(id);
|
||||
this.elementsDict[id] = {
|
||||
points: normalizeLinePoints(points),
|
||||
lastCommittedPoint: null,
|
||||
@@ -319,10 +339,17 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
endArrowhead: formatting?.endArrowHead ? formatting.endArrowHead : this.style.endArrowHead,
|
||||
... boxedElement(id,"arrow",box.x,box.y,box.w,box.h)
|
||||
};
|
||||
if(formatting?.startObjectId) this.elementsDict[formatting.startObjectId].boundElementIds.push(id);
|
||||
if(formatting?.endObjectId) this.elementsDict[formatting.endObjectId].boundElementIds.push(id);
|
||||
if(formatting?.startObjectId) {
|
||||
if(!this.elementsDict[formatting.startObjectId].boundElementIds) this.elementsDict[formatting.startObjectId].boundElementIds = [];
|
||||
this.elementsDict[formatting.startObjectId].boundElementIds.push(id);
|
||||
}
|
||||
if(formatting?.endObjectId) {
|
||||
if(!this.elementsDict[formatting.endObjectId].boundElementIds) this.elementsDict[formatting.endObjectId].boundElementIds = [];
|
||||
this.elementsDict[formatting.endObjectId].boundElementIds.push(id);
|
||||
}
|
||||
return id;
|
||||
},
|
||||
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints: number,startArrowHead:string,endArrowHead:string, padding: number}):void {
|
||||
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints?: number,startArrowHead?:string,endArrowHead?:string, padding?: number}):void {
|
||||
if(!(this.elementsDict[objectA] && this.elementsDict[objectB])) {
|
||||
return;
|
||||
}
|
||||
@@ -354,7 +381,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
});
|
||||
},
|
||||
clear() {
|
||||
this.elementIds = [];
|
||||
//this.elementIds = [];
|
||||
this.elementsDict = {};
|
||||
},
|
||||
reset() {
|
||||
@@ -376,10 +403,53 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
this.style.endArrowHead= "arrow";
|
||||
this.canvas.theme = "light";
|
||||
this.canvas.viewBackgroundColor="#FFFFFF";
|
||||
this.canvas.gridSize = 0;
|
||||
},
|
||||
isExcalidrawFile(f:TFile):boolean {
|
||||
return this.plugin.isExcalidrawFile(f);
|
||||
}
|
||||
},
|
||||
targetView: null,
|
||||
setView(view:ExcalidrawView|"first"|"active"):ExcalidrawView {
|
||||
if(view == "active") {
|
||||
const v = this.plugin.app.workspace.activeLeaf.view;
|
||||
if(!(v instanceof ExcalidrawView)) return;
|
||||
this.targetView = v;
|
||||
}
|
||||
if(view == "first") {
|
||||
const leaves = this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
if(!leaves || leaves.length == 0) return;
|
||||
this.targetView = (leaves[0].view as ExcalidrawView);
|
||||
}
|
||||
if(view instanceof ExcalidrawView) this.targetView = view;
|
||||
return this.targetView;
|
||||
},
|
||||
getExcalidrawAPI():any {
|
||||
if (!this.targetView || !this.targetView?._loaded) return null;
|
||||
return (this.targetView as ExcalidrawView).excalidrawRef.current;
|
||||
},
|
||||
getViewSelectedElement():any {
|
||||
if (!this.targetView || !this.targetView?._loaded) return null;
|
||||
const current = this.targetView?.excalidrawRef?.current;
|
||||
const selectedElements = current?.getAppState()?.selectedElementIds;
|
||||
if(!selectedElements) return null;
|
||||
const selectedElement = Object.keys(selectedElements)[0];
|
||||
if(!selectedElement) return null;
|
||||
return current.getSceneElements().filter((e:any)=>e.id==selectedElement)[0];
|
||||
},
|
||||
connectObjectWithViewSelectedElement(objectA:string,connectionA: ConnectionPoint, connectionB: ConnectionPoint, formatting?:{numberOfPoints?: number,startArrowHead?:string,endArrowHead?:string, padding?: number}):boolean {
|
||||
const el = this.getViewSelectedElement();
|
||||
if(!el) return false;
|
||||
const id = el.id;
|
||||
this.elementsDict[id] = el;
|
||||
this.connectObjects(objectA,connectionA,id,connectionB,formatting);
|
||||
delete this.elementsDict[id];
|
||||
return true;
|
||||
},
|
||||
async addElementsToView(repositionToCursor:boolean = false, save:boolean=false):Promise<boolean> {
|
||||
if (!this.targetView || !this.targetView?._loaded) return false;
|
||||
const elements = this.getElements();
|
||||
return await this.targetView.addElements(elements,repositionToCursor,save);
|
||||
},
|
||||
|
||||
};
|
||||
await initFonts();
|
||||
@@ -476,8 +546,9 @@ export function measureText (newText:string, fontSize:number, fontFamily:number)
|
||||
};
|
||||
|
||||
async function getTemplate(fileWithPath: string):Promise<{elements: any,appState: any}> {
|
||||
const vault = window.ExcalidrawAutomate.plugin.app.vault;
|
||||
const file = vault.getAbstractFileByPath(normalizePath(fileWithPath));
|
||||
const app = window.ExcalidrawAutomate.plugin.app;
|
||||
const vault = app.vault;
|
||||
const file = app.metadataCache.getFirstLinkpathDest(normalizePath(fileWithPath),'');
|
||||
if(file && file instanceof TFile) {
|
||||
const data = await vault.read(file);
|
||||
const excalidrawData = JSON_parse(getJSON(data));
|
||||
@@ -492,30 +563,3 @@ async function getTemplate(fileWithPath: string):Promise<{elements: any,appState
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the text elements from an Excalidraw scene into a string of ids as headers followed by the text contents
|
||||
* @param {string} data - Excalidraw scene JSON string
|
||||
* @returns {string} - Text starting with the "# Text Elements" header and followed by each "## id-value" and text
|
||||
*/
|
||||
export function exportSceneToMD(data:string): string {
|
||||
if(!data) return "";
|
||||
const excalidrawData = JSON_parse(data);
|
||||
const textElements = excalidrawData.elements?.filter((el:any)=> el.type=="text")
|
||||
let outString = '# Text Elements\n';
|
||||
let id:string;
|
||||
for (const te of textElements) {
|
||||
id = te.id;
|
||||
//replacing Excalidraw text IDs with my own, because default IDs may contain
|
||||
//characters not recognized by Obsidian block references
|
||||
//also Excalidraw IDs are inconveniently long
|
||||
if(te.id.length>8) {
|
||||
id=nanoid();
|
||||
data = data.replaceAll(te.id,id); //brute force approach to replace all occurances.
|
||||
}
|
||||
outString += te.text+' ^'+id+'\n\n';
|
||||
}
|
||||
return outString + '# Drawing\n'
|
||||
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'json\n'
|
||||
+ data + '\n'
|
||||
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
nanoid,
|
||||
FRONTMATTER_KEY_CUSTOM_PREFIX,
|
||||
FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS,
|
||||
FRONTMATTER_KEY_CUSTOM_URL_PREFIX,
|
||||
} from "./constants";
|
||||
import { measureText } from "./ExcalidrawAutomate";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
@@ -10,16 +11,46 @@ import { ExcalidrawSettings } from "./settings";
|
||||
import {
|
||||
JSON_parse
|
||||
} from "./constants";
|
||||
import { ExcalidrawTextElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { rawListeners } from "process";
|
||||
import { TextMode } from "./ExcalidrawView";
|
||||
import { link } from "fs";
|
||||
import { wrapText } from "./Utils";
|
||||
|
||||
const DRAWING_REG = /\n# Drawing\n(```json\n)?(.*)(```)?/gm;
|
||||
|
||||
//![[link|alias]]
|
||||
//1 2 3 4 5 6
|
||||
export const REG_LINK_BACKETS = /(!)?\[\[([^|\]]+)\|?(.+)?]]|(!)?\[(.*)\]\((.*)\)/g;
|
||||
declare module "obsidian" {
|
||||
interface MetadataCache {
|
||||
blockCache: {
|
||||
getForFile(x:any,f:TAbstractFile):any;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const DRAWING_REG = /[\r\n]# Drawing[\r\n](```json[\r\n])?(.*)(```)?(%%)?/gm;
|
||||
|
||||
export const REGEX_LINK = {
|
||||
//![[link|alias]] [alias](link){num}
|
||||
//12 3 4 5 6 7 8
|
||||
EXPR: /(!)?(\[\[([^|\]]+)\|?(.+)?]]|\[(.*)\]\((.*)\))(\{(\d+)\})?/g,
|
||||
isTransclusion: (parts: IteratorResult<RegExpMatchArray, any>):boolean => {
|
||||
return parts.value[1] ? true:false;
|
||||
},
|
||||
getLink: (parts: IteratorResult<RegExpMatchArray, any>):string => {
|
||||
return parts.value[3] ? parts.value[3] : parts.value[6];
|
||||
},
|
||||
isWikiLink: (parts: IteratorResult<RegExpMatchArray, any>):boolean => {
|
||||
return parts.value[3] ? true:false;
|
||||
},
|
||||
getAliasOrLink: (parts: IteratorResult<RegExpMatchArray, any>):string => {
|
||||
return REGEX_LINK.isWikiLink(parts)
|
||||
? (parts.value[4] ? parts.value[4] : parts.value[3])
|
||||
: (parts.value[5] ? parts.value[5] : parts.value[6]);
|
||||
},
|
||||
getWrapLength: (parts: IteratorResult<RegExpMatchArray, any>):number => {
|
||||
return parts.value[8];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;
|
||||
|
||||
export function getJSON(data:string):string {
|
||||
const res = data.matchAll(DRAWING_REG);
|
||||
@@ -35,13 +66,15 @@ export class ExcalidrawData {
|
||||
private textElements:Map<string,{raw:string, parsed:string}> = null;
|
||||
public scene:any = null;
|
||||
private file:TFile = null;
|
||||
private settings:ExcalidrawSettings;
|
||||
private app:App;
|
||||
private showLinkBrackets: boolean;
|
||||
private linkPrefix: string;
|
||||
private urlPrefix: string;
|
||||
private textMode: TextMode = TextMode.raw;
|
||||
private plugin: ExcalidrawPlugin;
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin) {
|
||||
this.settings = plugin.settings;
|
||||
this.plugin = plugin;
|
||||
this.app = plugin.app;
|
||||
}
|
||||
|
||||
@@ -59,9 +92,15 @@ export class ExcalidrawData {
|
||||
//The drawing will use these values until next drawing is loaded or this drawing is re-loaded
|
||||
this.setShowLinkBrackets();
|
||||
this.setLinkPrefix();
|
||||
this.setUrlPrefix();
|
||||
|
||||
this.scene = null;
|
||||
if (this.settings.syncExcalidraw) {
|
||||
|
||||
//In compatibility mode if the .excalidraw file was more recently updated than the .md file, then the .excalidraw file
|
||||
//should be loaded as the scene.
|
||||
//This feature is mostly likely only relevant to people who use Obsidian and Logseq on the same vault and edit .excalidraw
|
||||
//drawings in Logseq.
|
||||
if (this.plugin.settings.syncExcalidraw) {
|
||||
const excalfile = file.path.substring(0,file.path.lastIndexOf('.md')) + '.excalidraw';
|
||||
const f = this.app.vault.getAbstractFileByPath(excalfile);
|
||||
if(f && f instanceof TFile && f.stat.mtime>file.stat.mtime) { //the .excalidraw file is newer then the .md file
|
||||
@@ -76,19 +115,22 @@ export class ExcalidrawData {
|
||||
if(!this.scene) { //scene was not loaded from .excalidraw
|
||||
const scene = parts.value[2];
|
||||
this.scene = JSON_parse(scene.substr(0,scene.lastIndexOf("}")+1)); //this is a workaround to address when files are mereged by sync and one version is still an old markdown without the codeblock ```
|
||||
//using JSON_parse for legacy compatibiltiy. In an earlier version Excalidraw JSON was not enclosed in a codeblock
|
||||
}
|
||||
//Trim data to remove the JSON string
|
||||
data = data.substring(0,parts.value.index);
|
||||
|
||||
//The Markdown # Text Elements take priority over the JSON text elements.
|
||||
//i.e. if the JSON is modified to reflect the MD in case of difference
|
||||
//The Markdown # Text Elements take priority over the JSON text elements. Assuming the scenario in which the link was updated due to filename changes
|
||||
//The .excalidraw JSON is modified to reflect the MD in case of difference
|
||||
//Read the text elements into the textElements Map
|
||||
let position = data.search("# Text Elements");
|
||||
if(position==-1) return true; //Text Elements header does not exist
|
||||
position += "# Text Elements\n".length;
|
||||
|
||||
//iterating through all the text elements in .md
|
||||
//Text elements always contain the raw value
|
||||
const BLOCKREF_LEN:number = " ^12345678\n\n".length;
|
||||
const res = data.matchAll(/\s\^(.{8})\n/g);
|
||||
const res = data.matchAll(/\s\^(.{8})[\r\n]/g);
|
||||
while(!(parts = res.next()).done) {
|
||||
const text = data.substring(position,parts.value.index);
|
||||
this.textElements.set(parts.value[1],{raw: text, parsed: await this.parse(text)});
|
||||
@@ -107,9 +149,10 @@ export class ExcalidrawData {
|
||||
this.textElements = new Map<string,{raw:string, parsed:string}>();
|
||||
this.setShowLinkBrackets();
|
||||
this.setLinkPrefix();
|
||||
this.setUrlPrefix();
|
||||
this.scene = JSON.parse(data);
|
||||
this.findNewTextElementsInScene();
|
||||
await this.setTextMode(TextMode.raw,true);
|
||||
await this.setTextMode(TextMode.raw,true); //legacy files are always displayed in raw mode.
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -184,7 +227,7 @@ export class ExcalidrawData {
|
||||
if(te.id.length > 8 && this.textElements.has(te.id)) { //element was created with onBeforeTextSubmit
|
||||
const element = this.textElements.get(te.id);
|
||||
this.textElements.set(id,{raw: element.raw, parsed: element.parsed})
|
||||
this.textElements.delete(te.id);
|
||||
this.textElements.delete(te.id); //delete the old ID from the Map
|
||||
dirty = true;
|
||||
} else if(!this.textElements.has(id)) {
|
||||
dirty = true;
|
||||
@@ -193,7 +236,7 @@ export class ExcalidrawData {
|
||||
}
|
||||
}
|
||||
if(dirty) { //reload scene json in case it has changed
|
||||
this.scene = JSON_parse(jsonString);
|
||||
this.scene = JSON.parse(jsonString);
|
||||
}
|
||||
|
||||
return dirty;
|
||||
@@ -204,7 +247,6 @@ export class ExcalidrawData {
|
||||
* and updating the textElement map based on the text updated in the scene
|
||||
*/
|
||||
private async updateTextElementsFromScene() {
|
||||
//console.log("Excalidraw.Data.updateTextElementesFromScene()");
|
||||
for(const key of this.textElements.keys()){
|
||||
//find text element in the scene
|
||||
const el = this.scene.elements?.filter((el:any)=> el.type=="text" && el.id==key);
|
||||
@@ -223,50 +265,15 @@ export class ExcalidrawData {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* update text element map by deleting entries that are no long in the scene
|
||||
* and updating the textElement map based on the text updated in the scene
|
||||
*/
|
||||
private updateTextElementsFromSceneRawOnly() {
|
||||
//console.log("Excalidraw.Data.updateTextElementsFromSceneRawOnly()");
|
||||
for(const key of this.textElements.keys()){
|
||||
//find text element in the scene
|
||||
const el = this.scene.elements?.filter((el:any)=> el.type=="text" && el.id==key);
|
||||
if(el.length==0) {
|
||||
this.textElements.delete(key); //if no longer in the scene, delete the text element
|
||||
} else {
|
||||
if(!this.textElements.has(key)) {
|
||||
this.textElements.set(key,{raw: el[0].text,parsed: null});
|
||||
this.parseasync(key,el[0].text);
|
||||
} else {
|
||||
const text = (this.textMode == TextMode.parsed) ? this.textElements.get(key).parsed : this.textElements.get(key).raw;
|
||||
if(text != el[0].text) {
|
||||
this.textElements.set(key,{raw: el[0].text,parsed: null});
|
||||
this.parseasync(key,el[0].text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async parseasync(key:string, raw:string) {
|
||||
this.textElements.set(key,{raw:raw,parsed: await this.parse(raw)});
|
||||
}
|
||||
|
||||
private parseLinks(text:string, position:number, parts:any):string {
|
||||
let outString = null;
|
||||
if (parts.value[2]) {
|
||||
outString = text.substring(position,parts.value.index) +
|
||||
(this.showLinkBrackets ? "[[" : "") +
|
||||
(parts.value[3] ? parts.value[3]:parts.value[2]) + //insert alias or link text
|
||||
(this.showLinkBrackets ? "]]" : "");
|
||||
} else {
|
||||
outString = text.substring(position,parts.value.index) +
|
||||
(this.showLinkBrackets ? "[[" : "") +
|
||||
(parts.value[5] ? parts.value[5]:parts.value[6]) + //insert alias or link text
|
||||
(this.showLinkBrackets ? "]]" : "");
|
||||
}
|
||||
return outString;
|
||||
return text.substring(position,parts.value.index) +
|
||||
(this.showLinkBrackets ? "[[" : "") +
|
||||
REGEX_LINK.getAliasOrLink(parts) +
|
||||
(this.showLinkBrackets ? "]]" : "");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -277,33 +284,57 @@ export class ExcalidrawData {
|
||||
private async parse(text:string):Promise<string>{
|
||||
const getTransclusion = async (text:string) => {
|
||||
//file-name#^blockref
|
||||
//1 2
|
||||
const REG_FILE_BLOCKREF = /(.*)#\^(.*)/g;
|
||||
//1 2 3
|
||||
const REG_FILE_BLOCKREF = /(.*)#(\^)?(.*)/g;
|
||||
const parts=text.matchAll(REG_FILE_BLOCKREF).next();
|
||||
if(parts.done || !parts.value[1] || !parts.value[2]) return text; //filename and/or blockref not found
|
||||
if(parts.done || !parts.value[1] || !parts.value[3]) return text; //filename and/or blockref not found
|
||||
const file = this.app.metadataCache.getFirstLinkpathDest(parts.value[1],this.file.path);
|
||||
const contents = await this.app.vault.cachedRead(file);
|
||||
//get transcluded line and take the part before ^blockref
|
||||
const REG_TRANSCLUDE = new RegExp("(.*)\\s\\^" + parts.value[2]);
|
||||
const res = contents.match(REG_TRANSCLUDE);
|
||||
if(res) return res[1];
|
||||
return text;//if blockref not found in file, return the input string
|
||||
const isParagraphRef = parts.value[2] ? true : false; //does the reference contain a ^ character?
|
||||
const id = parts.value[3]; //the block ID or heading text
|
||||
const blocks = (await this.app.metadataCache.blockCache.getForFile({isCancelled: ()=>false},file)).blocks.filter((block:any)=>block.node.type!="comment");
|
||||
if(!blocks) return text;
|
||||
if(isParagraphRef) {
|
||||
let para = blocks.filter((block:any)=>block.node.id == id)[0]?.node;
|
||||
if(!para) return text;
|
||||
if(["blockquote","listItem"].includes(para.type)) para = para.children[0]; //blockquotes are special, they have one child, which has the paragraph
|
||||
const startPos = para.position.start.offset;
|
||||
const endPos = para.children[para.children.length-1]?.position.start.offset-1; //alternative: filter((c:any)=>c.type=="blockid")[0]
|
||||
return contents.substr(startPos,endPos-startPos)
|
||||
|
||||
} else {
|
||||
const headings = blocks.filter((block:any)=>block.display.startsWith("#"));
|
||||
let startPos:number = null;
|
||||
let endPos:number = null;
|
||||
for(let i=0;i<headings.length;i++) {
|
||||
if(startPos && !endPos) {
|
||||
endPos = headings[i].node.position.start.offset-1;
|
||||
return contents.substr(startPos,endPos-startPos)
|
||||
}
|
||||
if(!startPos && headings[i].node.children[0]?.value == id) startPos = headings[i].node.children[0]?.position.start.offset; //
|
||||
}
|
||||
if(startPos) return contents.substr(startPos);
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
let outString = "";
|
||||
let position = 0;
|
||||
const res = text.matchAll(REG_LINK_BACKETS);
|
||||
const res = text.matchAll(REGEX_LINK.EXPR);
|
||||
let linkIcon = false;
|
||||
let urlIcon = false;
|
||||
let parts;
|
||||
while(!(parts=res.next()).done) {
|
||||
if (parts.value[1] || parts.value[4]) { //transclusion
|
||||
if (REGEX_LINK.isTransclusion(parts)) { //transclusion //parts.value[1] || parts.value[4]
|
||||
outString += text.substring(position,parts.value.index) +
|
||||
await getTransclusion(parts.value[1] ? parts.value[2] : parts.value[6]);
|
||||
wrapText(await getTransclusion(REGEX_LINK.getLink(parts)),REGEX_LINK.getWrapLength(parts));
|
||||
} else {
|
||||
const parsedLink = this.parseLinks(text,position,parts);
|
||||
if(parsedLink) {
|
||||
linkIcon = true;
|
||||
outString += parsedLink;
|
||||
if(!(urlIcon || linkIcon))
|
||||
if(REGEX_LINK.getLink(parts).match(REG_LINKINDEX_HYPERLINK)) urlIcon = true;
|
||||
else linkIcon = true;
|
||||
}
|
||||
}
|
||||
position = parts.value.index + parts.value[0].length;
|
||||
@@ -312,6 +343,9 @@ export class ExcalidrawData {
|
||||
if (linkIcon) {
|
||||
outString = this.linkPrefix + outString;
|
||||
}
|
||||
if (urlIcon) {
|
||||
outString = this.urlPrefix + outString;
|
||||
}
|
||||
|
||||
return outString;
|
||||
}
|
||||
@@ -325,10 +359,10 @@ export class ExcalidrawData {
|
||||
*/
|
||||
private quickParse(text:string):string {
|
||||
const hasTransclusion = (text:string):boolean => {
|
||||
const res = text.matchAll(REG_LINK_BACKETS);
|
||||
const res = text.matchAll(REGEX_LINK.EXPR);
|
||||
let parts;
|
||||
while(!(parts=res.next()).done) {
|
||||
if (parts.value[1] || parts.value[4]) return true;
|
||||
if (REGEX_LINK.isTransclusion(parts)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -336,14 +370,17 @@ export class ExcalidrawData {
|
||||
|
||||
let outString = "";
|
||||
let position = 0;
|
||||
const res = text.matchAll(REG_LINK_BACKETS);
|
||||
const res = text.matchAll(REGEX_LINK.EXPR);
|
||||
let linkIcon = false;
|
||||
let urlIcon = false;
|
||||
let parts;
|
||||
while(!(parts=res.next()).done) {
|
||||
const parsedLink = this.parseLinks(text,position,parts);
|
||||
if(parsedLink) {
|
||||
linkIcon = true;
|
||||
outString += parsedLink;
|
||||
if(!(urlIcon || linkIcon))
|
||||
if(REGEX_LINK.getLink(parts).match(REG_LINKINDEX_HYPERLINK)) urlIcon = true;
|
||||
else linkIcon = true;
|
||||
}
|
||||
position = parts.value.index + parts.value[0].length;
|
||||
}
|
||||
@@ -351,6 +388,9 @@ export class ExcalidrawData {
|
||||
if (linkIcon) {
|
||||
outString = this.linkPrefix + outString;
|
||||
}
|
||||
if (urlIcon) {
|
||||
outString = this.urlPrefix + outString;
|
||||
}
|
||||
return outString;
|
||||
}
|
||||
|
||||
@@ -360,29 +400,25 @@ export class ExcalidrawData {
|
||||
* @returns markdown string
|
||||
*/
|
||||
generateMD():string {
|
||||
//console.log("Excalidraw.Data.generateMD()");
|
||||
let outString = '# Text Elements\n';
|
||||
for(const key of this.textElements.keys()){
|
||||
outString += this.textElements.get(key).raw+' ^'+key+'\n\n';
|
||||
}
|
||||
return outString + '# Drawing\n'
|
||||
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'json\n'
|
||||
+ JSON.stringify(this.scene) + '\n'
|
||||
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96);
|
||||
return outString + this.plugin.getMarkdownDrawingSection(JSON.stringify(this.scene));
|
||||
}
|
||||
|
||||
public syncElements(newScene:any):boolean {
|
||||
public async syncElements(newScene:any):Promise<boolean> {
|
||||
//console.log("Excalidraw.Data.syncElements()");
|
||||
this.scene = newScene;//JSON_parse(newScene);
|
||||
const result = this.setLinkPrefix() || this.setShowLinkBrackets() || this.findNewTextElementsInScene();
|
||||
this.updateTextElementsFromSceneRawOnly();
|
||||
const result = this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets() || this.findNewTextElementsInScene();
|
||||
await this.updateTextElementsFromScene();
|
||||
return result;
|
||||
}
|
||||
|
||||
public async updateScene(newScene:any){
|
||||
//console.log("Excalidraw.Data.updateScene()");
|
||||
this.scene = JSON_parse(newScene);
|
||||
const result = this.setLinkPrefix() || this.setShowLinkBrackets() || this.findNewTextElementsInScene();
|
||||
const result = this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets() || this.findNewTextElementsInScene();
|
||||
await this.updateTextElementsFromScene();
|
||||
if(result) {
|
||||
await this.updateSceneTextElements();
|
||||
@@ -413,6 +449,12 @@ export class ExcalidrawData {
|
||||
return null;
|
||||
}
|
||||
|
||||
public async addTextElement(elementID:string, rawText:string):Promise<string> {
|
||||
const parseResult = await this.parse(rawText);
|
||||
this.textElements.set(elementID,{raw: rawText,parsed: parseResult});
|
||||
return parseResult;
|
||||
}
|
||||
|
||||
public deleteTextElement(id:string) {
|
||||
this.textElements.delete(id);
|
||||
}
|
||||
@@ -423,20 +465,33 @@ export class ExcalidrawData {
|
||||
if (fileCache?.frontmatter && fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_PREFIX]!=null) {
|
||||
this.linkPrefix=fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_PREFIX];
|
||||
} else {
|
||||
this.linkPrefix = this.settings.linkPrefix;
|
||||
this.linkPrefix = this.plugin.settings.linkPrefix;
|
||||
}
|
||||
return linkPrefix != this.linkPrefix;
|
||||
}
|
||||
|
||||
private setUrlPrefix():boolean {
|
||||
const urlPrefix = this.urlPrefix;
|
||||
const fileCache = this.app.metadataCache.getFileCache(this.file);
|
||||
if (fileCache?.frontmatter && fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_URL_PREFIX]!=null) {
|
||||
this.urlPrefix=fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_URL_PREFIX];
|
||||
} else {
|
||||
this.urlPrefix = this.plugin.settings.urlPrefix;
|
||||
}
|
||||
return urlPrefix != this.urlPrefix;
|
||||
}
|
||||
|
||||
private setShowLinkBrackets():boolean {
|
||||
const showLinkBrackets = this.showLinkBrackets;
|
||||
const fileCache = this.app.metadataCache.getFileCache(this.file);
|
||||
if (fileCache?.frontmatter && fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS]!=null) {
|
||||
this.showLinkBrackets=fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS]!=false;
|
||||
} else {
|
||||
this.showLinkBrackets = this.settings.showLinkBrackets;
|
||||
this.showLinkBrackets = this.plugin.settings.showLinkBrackets;
|
||||
}
|
||||
return showLinkBrackets != this.showLinkBrackets;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,11 +6,10 @@ import {
|
||||
WorkspaceItem,
|
||||
Notice,
|
||||
Menu,
|
||||
TAbstractFile,
|
||||
} from "obsidian";
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import Excalidraw, {exportToSvg, getSceneVersion, loadLibraryFromBlob} from "@zsviczian/excalidraw";
|
||||
import Excalidraw, {exportToSvg, getSceneVersion} from "@zsviczian/excalidraw";
|
||||
import { ExcalidrawElement,ExcalidrawTextElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
import {
|
||||
AppState,
|
||||
@@ -28,18 +27,15 @@ import {
|
||||
FRONTMATTER_KEY,
|
||||
TEXT_DISPLAY_RAW_ICON_NAME,
|
||||
TEXT_DISPLAY_PARSED_ICON_NAME,
|
||||
EXIT_FULLSCREEN_ICON_NAME,
|
||||
FULLSCREEN_ICON_NAME,
|
||||
JSON_parse,
|
||||
nanoid
|
||||
JSON_parse
|
||||
} from './constants';
|
||||
import ExcalidrawPlugin from './main';
|
||||
import {ExcalidrawAutomate} from './ExcalidrawAutomate';
|
||||
import { t } from "./lang/helpers";
|
||||
import { ExcalidrawData, REG_LINK_BACKETS } from "./ExcalidrawData";
|
||||
import { checkAndCreateFolder, download, getNewUniqueFilepath, splitFolderAndFilename } from "./Utils";
|
||||
import { ExcalidrawData, REG_LINKINDEX_HYPERLINK, REGEX_LINK } from "./ExcalidrawData";
|
||||
import { checkAndCreateFolder, download, getNewUniqueFilepath, splitFolderAndFilename, viewportCoordsToSceneCoords } from "./Utils";
|
||||
import { Prompt } from "./Prompt";
|
||||
import { isRTL } from "@zsviczian/excalidraw/types/utils";
|
||||
|
||||
declare let window: ExcalidrawAutomate;
|
||||
|
||||
@@ -57,16 +53,17 @@ export interface ExportSettings {
|
||||
withTheme: boolean
|
||||
}
|
||||
|
||||
const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;
|
||||
const REG_LINKINDEX_INVALIDCHARS = /[<>:"\\|?*]/g;
|
||||
|
||||
export default class ExcalidrawView extends TextFileView {
|
||||
private excalidrawData: ExcalidrawData;
|
||||
private getScene: Function = null;
|
||||
public addElements: Function = null; //add elements to the active Excalidraw drawing
|
||||
private getSelectedTextElement: Function = null;
|
||||
public addText:Function = null;
|
||||
private refresh: Function = null;
|
||||
private excalidrawRef: React.MutableRefObject<any> = null;
|
||||
public excalidrawRef: React.MutableRefObject<any> = null;
|
||||
private excalidrawWrapperRef: React.MutableRefObject<any> = null;
|
||||
private justLoaded: boolean = false;
|
||||
private plugin: ExcalidrawPlugin;
|
||||
private dirty: string = null;
|
||||
@@ -75,10 +72,13 @@ export default class ExcalidrawView extends TextFileView {
|
||||
public textMode:TextMode = TextMode.raw;
|
||||
private textIsParsed_Element:HTMLElement;
|
||||
private textIsRaw_Element:HTMLElement;
|
||||
private gotoFullscreen:HTMLElement;
|
||||
private exitFullscreen:HTMLElement;
|
||||
private preventReload:boolean = true;
|
||||
public compatibilityMode: boolean = false;
|
||||
//store key state for view mode link resolution
|
||||
private ctrlKeyDown = false;
|
||||
private shiftKeyDown = false;
|
||||
private altKeyDown = false;
|
||||
private mouseEvent:any = null;
|
||||
|
||||
id: string = (this.leaf as any).id;
|
||||
|
||||
@@ -146,8 +146,17 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
|
||||
async save(preventReload:boolean=true) {
|
||||
if(!this.getScene) return;
|
||||
this.preventReload = preventReload;
|
||||
this.dirty = null;
|
||||
|
||||
if(this.compatibilityMode) {
|
||||
await this.excalidrawData.syncElements(this.getScene());
|
||||
} else {
|
||||
if(await this.excalidrawData.syncElements(this.getScene()) && !this.autosaving) {
|
||||
await this.loadDrawing(false);
|
||||
}
|
||||
}
|
||||
await super.save();
|
||||
}
|
||||
|
||||
@@ -158,9 +167,6 @@ export default class ExcalidrawView extends TextFileView {
|
||||
//console.log("ExcalidrawView.getViewData()");
|
||||
if(!this.getScene) return this.data;
|
||||
if(!this.compatibilityMode) {
|
||||
if(this.excalidrawData.syncElements(this.getScene()) && !this.autosaving) {
|
||||
this.loadDrawing(false);
|
||||
}
|
||||
let trimLocation = this.data.search("# Text Elements\n");
|
||||
if(trimLocation == -1) trimLocation = this.data.search("# Drawing\n");
|
||||
if(trimLocation == -1) return this.data;
|
||||
@@ -177,7 +183,6 @@ export default class ExcalidrawView extends TextFileView {
|
||||
return header + this.excalidrawData.generateMD();
|
||||
}
|
||||
if(this.compatibilityMode) {
|
||||
this.excalidrawData.syncElements(this.getScene());
|
||||
const scene = this.excalidrawData.scene;
|
||||
if(!this.autosaving) {
|
||||
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
|
||||
@@ -198,11 +203,10 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
if(text.match(REG_LINKINDEX_HYPERLINK)) {
|
||||
window.open(text,"_blank");
|
||||
return; }
|
||||
return;
|
||||
}
|
||||
|
||||
//![[link|alias]]
|
||||
//1 2 3 4 5 6
|
||||
const parts = text.matchAll(REG_LINK_BACKETS).next();
|
||||
const parts = text.matchAll(REGEX_LINK.EXPR).next();
|
||||
if(!parts.value) {
|
||||
const tags = text.matchAll(/#([\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+)/ug).next();
|
||||
if(!tags.value || tags.value.length<2) {
|
||||
@@ -214,11 +218,13 @@ export default class ExcalidrawView extends TextFileView {
|
||||
//@ts-ignore
|
||||
search[0].view.setQuery("tag:"+tags.value[1]);
|
||||
this.app.workspace.revealLeaf(search[0]);
|
||||
if(this.gotoFullscreen.style.display=="none") this.toggleFullscreen();
|
||||
//if(this.gotoFullscreen.style.display=="none") this.toggleFullscreen();
|
||||
document.exitFullscreen();
|
||||
this.zoomToFit();
|
||||
return;
|
||||
}
|
||||
|
||||
text = parts.value[2] ? parts.value[2]:parts.value[6];
|
||||
text = REGEX_LINK.getLink(parts); //parts.value[2] ? parts.value[2]:parts.value[6];
|
||||
|
||||
if(text.match(REG_LINKINDEX_HYPERLINK)) {
|
||||
window.open(text,"_blank");
|
||||
@@ -239,7 +245,10 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
try {
|
||||
const f = view.file;
|
||||
if(ev.shiftKey && this.gotoFullscreen.style.display=="none") this.toggleFullscreen();
|
||||
if(ev.shiftKey) {
|
||||
document.exitFullscreen();
|
||||
this.zoomToFit();
|
||||
}
|
||||
view.app.workspace.openLinkText(text,view.file.path,ev.shiftKey);
|
||||
} catch (e) {
|
||||
new Notice(e,4000);
|
||||
@@ -258,12 +267,16 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
this.addAction("link",t("OPEN_LINK"), (ev)=>this.handleLinkClick(this,ev));
|
||||
|
||||
|
||||
this.gotoFullscreen = this.addAction(FULLSCREEN_ICON_NAME,"",()=>this.toggleFullscreen());
|
||||
this.exitFullscreen = this.addAction(EXIT_FULLSCREEN_ICON_NAME,"",()=>this.toggleFullscreen());
|
||||
this.exitFullscreen.hide();
|
||||
//@ts-ignore
|
||||
if(this.app.isMobile) this.gotoFullscreen.hide();
|
||||
if(!this.app.isMobile) {
|
||||
this.addAction(FULLSCREEN_ICON_NAME,"Press ESC to exit fullscreen mode",()=>{
|
||||
this.contentEl.requestFullscreen();//{navigationUI: "hide"});
|
||||
if(this.excalidrawWrapperRef) this.excalidrawWrapperRef.current.focus();
|
||||
});
|
||||
this.contentEl.onfullscreenchange = () => {
|
||||
this.zoomToFit();
|
||||
}
|
||||
}
|
||||
|
||||
//this is to solve sliding panes bug
|
||||
if (this.app.workspace.layoutReady) {
|
||||
@@ -276,21 +289,6 @@ export default class ExcalidrawView extends TextFileView {
|
||||
this.setupAutosaveTimer();
|
||||
}
|
||||
|
||||
private toggleFullscreen() {
|
||||
//@ts-ignore
|
||||
if(this.app.isMobile) return;
|
||||
if(this.exitFullscreen.style.display=="none") {
|
||||
this.containerEl.requestFullscreen();
|
||||
this.gotoFullscreen.hide();
|
||||
this.exitFullscreen.show();
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
this.gotoFullscreen.show();
|
||||
this.exitFullscreen.hide();
|
||||
}
|
||||
this.zoomToFit();
|
||||
}
|
||||
|
||||
public async changeTextMode(textMode:TextMode,reload:boolean=true) {
|
||||
this.textMode = textMode;
|
||||
if(textMode == TextMode.parsed) {
|
||||
@@ -316,23 +314,18 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
}
|
||||
if(this.autosaveTimer) clearInterval(this.autosaveTimer); // clear previous timer if one exists
|
||||
//if(this.plugin.settings.autosave) {
|
||||
this.autosaveTimer = setInterval(timer,20000);
|
||||
//}
|
||||
}
|
||||
|
||||
//save current drawing when user closes workspace leaf
|
||||
async onunload() {
|
||||
//console.log("ExcalidrawView.onunload()");
|
||||
if(this.autosaveTimer) {
|
||||
clearInterval(this.autosaveTimer);
|
||||
this.autosaveTimer = null;
|
||||
}
|
||||
//if(this.excalidrawRef) await this.save();
|
||||
}
|
||||
|
||||
public async reload(fullreload:boolean = false, file?:TFile){
|
||||
//console.log("ExcalidrawView.reload(), fullreload",fullreload,"preventReload",this.preventReload);
|
||||
if(this.preventReload) {
|
||||
this.preventReload = false;
|
||||
return;
|
||||
@@ -342,7 +335,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if(file) this.data = await this.app.vault.read(file);
|
||||
if(fullreload) await this.excalidrawData.loadData(this.data, this.file,this.textMode);
|
||||
else await this.excalidrawData.setTextMode(this.textMode);
|
||||
this.loadDrawing(false);
|
||||
await this.loadDrawing(false);
|
||||
this.dirty = null;
|
||||
}
|
||||
|
||||
@@ -371,7 +364,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if(!(await this.excalidrawData.loadData(data, this.file,this.textMode))) return;
|
||||
}
|
||||
if(clear) this.clear();
|
||||
this.loadDrawing(true)
|
||||
await this.loadDrawing(true)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -379,7 +372,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
*
|
||||
* @param justloaded - a flag to trigger zoom to fit after the drawing has been loaded
|
||||
*/
|
||||
private loadDrawing(justloaded:boolean) {
|
||||
private async loadDrawing(justloaded:boolean) {
|
||||
const excalidrawData = this.excalidrawData.scene;
|
||||
if(this.excalidrawRef) {
|
||||
const viewModeEnabled = this.excalidrawRef.current.getAppState().viewModeEnabled;
|
||||
@@ -398,15 +391,16 @@ export default class ExcalidrawView extends TextFileView {
|
||||
},
|
||||
commitToHistory: true,
|
||||
});
|
||||
if((this.app.workspace.activeLeaf === this.leaf) && this.excalidrawWrapperRef) {
|
||||
this.excalidrawWrapperRef.current.focus();
|
||||
}
|
||||
} else {
|
||||
this.justLoaded = justloaded;
|
||||
(async() => {
|
||||
this.instantiateExcalidraw({
|
||||
elements: excalidrawData.elements,
|
||||
appState: excalidrawData.appState,
|
||||
libraryItems: await this.getLibrary(),
|
||||
});
|
||||
})();
|
||||
this.instantiateExcalidraw({
|
||||
elements: excalidrawData.elements,
|
||||
appState: excalidrawData.appState,
|
||||
libraryItems: await this.getLibrary(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,6 +542,8 @@ export default class ExcalidrawView extends TextFileView {
|
||||
});
|
||||
|
||||
this.excalidrawRef = excalidrawRef;
|
||||
this.excalidrawWrapperRef = excalidrawWrapperRef;
|
||||
|
||||
React.useEffect(() => {
|
||||
setDimensions({
|
||||
width: this.contentEl.clientWidth,
|
||||
@@ -570,9 +566,9 @@ export default class ExcalidrawView extends TextFileView {
|
||||
this.getSelectedTextElement = ():{id: string, text:string} => {
|
||||
if(!excalidrawRef?.current) return {id:null,text:null};
|
||||
if(this.excalidrawRef.current.getAppState().viewModeEnabled) {
|
||||
if(selected) {
|
||||
const retval = selected;
|
||||
selected == null;
|
||||
if(selectedTextElement) {
|
||||
const retval = selectedTextElement;
|
||||
selectedTextElement == null;
|
||||
return retval;
|
||||
}
|
||||
return {id:null,text:null};
|
||||
@@ -597,35 +593,70 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
|
||||
const st: AppState = excalidrawRef.current.getAppState();
|
||||
const id = nanoid();
|
||||
window.ExcalidrawAutomate.reset();
|
||||
window.ExcalidrawAutomate.style.strokeColor = st.currentItemStrokeColor;
|
||||
window.ExcalidrawAutomate.style.opacity = st.currentItemOpacity;
|
||||
window.ExcalidrawAutomate.style.fontFamily = fontFamily ? fontFamily: st.currentItemFontFamily;
|
||||
window.ExcalidrawAutomate.style.fontSize = st.currentItemFontSize;
|
||||
window.ExcalidrawAutomate.style.textAlign = st.currentItemTextAlign;
|
||||
|
||||
const addText = (text:string) => {
|
||||
window.ExcalidrawAutomate.addText(currentPosition.x, currentPosition.y, text,null,id);
|
||||
//@ts-ignore
|
||||
const textElement = window.ExcalidrawAutomate.elementsDict[id];
|
||||
el.push(textElement);
|
||||
excalidrawRef.current.updateScene({
|
||||
elements: el,
|
||||
appState: st,
|
||||
});
|
||||
this.save(false);
|
||||
}
|
||||
const self = this;
|
||||
//setTextElement will attempt a quick parse (without processing transclusions)
|
||||
const parseResult = this.excalidrawData.setTextElement(id, text,async (parsedText:string)=>{
|
||||
addText(self.textMode==TextMode.parsed?parsedText:text);
|
||||
});
|
||||
if(parseResult) { //there were no transclusions in the raw text, quick parse was successful
|
||||
addText(self.textMode==TextMode.parsed?parseResult:text);
|
||||
}
|
||||
const id:string = window.ExcalidrawAutomate.addText(currentPosition.x, currentPosition.y, text);
|
||||
this.addElements(window.ExcalidrawAutomate.getElements(),false,true);
|
||||
}
|
||||
|
||||
this.addElements = async (newElements:ExcalidrawElement[],repositionToCursor:boolean = false, save:boolean=false):Promise<boolean> => {
|
||||
if(!excalidrawRef?.current) return false;
|
||||
|
||||
const estimateElementBounds = (element:ExcalidrawElement):[number,number,number,number] => {
|
||||
return[element.x,element.y,element.x+element.width,element.y+element.height];
|
||||
}
|
||||
|
||||
const estimateBounds = (elements:ExcalidrawElement[]):[number,number,number,number] => {
|
||||
if(!elements.length) return [0,0,0,0];
|
||||
let minX = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let minY = Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
elements.forEach((element)=>{
|
||||
const [x1,y1,x2,y2] = estimateElementBounds(element);
|
||||
minX = Math.min(minX, x1);
|
||||
minY = Math.min(minY, y1);
|
||||
maxX = Math.max(maxX, x2);
|
||||
maxY = Math.max(maxY, y2);
|
||||
});
|
||||
return [minX,minY,maxX,maxY];
|
||||
}
|
||||
|
||||
const repositionElementsToCursor = (elements:ExcalidrawElement[]):ExcalidrawElement[] => {
|
||||
const [x1,y1,x2,y2] = estimateBounds(elements);
|
||||
const [offsetX,offsetY] = [currentPosition.x-(x1+x2)/2,currentPosition.y-(y1+y2)/2]
|
||||
elements.forEach((element:any)=>{ //using any so I can write read-only propery x & y
|
||||
element.x=element.x+offsetX;
|
||||
element.y=element.y+offsetY;
|
||||
});
|
||||
return elements;
|
||||
}
|
||||
|
||||
const textElements = newElements.filter((el)=>el.type=="text");
|
||||
for(let i=0;i<textElements.length;i++) {
|
||||
//@ts-ignore
|
||||
const parseResult = await this.excalidrawData.addTextElement(textElements[i].id,textElements[i].text);
|
||||
//@ts-ignore
|
||||
if(this.textMode==TextMode.parsed) textElements[i].text = parseResult;
|
||||
};
|
||||
|
||||
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
|
||||
const st: AppState = excalidrawRef.current.getAppState();
|
||||
if(repositionToCursor) newElements = repositionElementsToCursor(newElements);
|
||||
this.excalidrawRef.current.updateScene({
|
||||
elements: el.concat(newElements),
|
||||
appState: st,
|
||||
commitToHistory: true,
|
||||
});
|
||||
if(save) this.save(); else this.dirty = this.file?.path;
|
||||
return true;
|
||||
};
|
||||
|
||||
this.getScene = () => {
|
||||
if(!excalidrawRef?.current) {
|
||||
return null;
|
||||
@@ -665,156 +696,291 @@ export default class ExcalidrawView extends TextFileView {
|
||||
};
|
||||
|
||||
//variables used to handle click events in view mode
|
||||
let selected:{id:string,text:string} = null;
|
||||
let ctrlKeyDown = false;
|
||||
let block = false;
|
||||
let selectedTextElement:{id:string,text:string} = null;
|
||||
let timestamp = 0;
|
||||
let blockOnMouseButtonDown = false;
|
||||
|
||||
const getTextElementAtPointer = (pointer:any) => {
|
||||
const elements = this.excalidrawRef.current.getSceneElements()
|
||||
.filter((e:ExcalidrawElement)=>{
|
||||
return e.type == "text"
|
||||
&& e.x<=pointer.x && (e.x+e.width)>=pointer.x
|
||||
&& e.y<=pointer.y && (e.y+e.height)>=pointer.y;
|
||||
});
|
||||
if(elements.length==0) return null;
|
||||
return {id:elements[0].id,text:elements[0].text};
|
||||
}
|
||||
|
||||
let hoverPoint = {x:0,y:0};
|
||||
let hoverPreviewTarget:EventTarget = null;
|
||||
const clearHoverPreview = () => {
|
||||
if(hoverPreviewTarget) {
|
||||
const event = new MouseEvent('click', {
|
||||
'view': window,
|
||||
'bubbles': true,
|
||||
'cancelable': true,
|
||||
});
|
||||
hoverPreviewTarget.dispatchEvent(event);
|
||||
hoverPreviewTarget = null;
|
||||
}
|
||||
}
|
||||
|
||||
const dropAction = (transfer: DataTransfer) => {
|
||||
// Return a 'copy' or 'link' action according to the content types, or undefined if no recognized type
|
||||
|
||||
//if (transfer.types.includes('text/uri-list')) return 'link';
|
||||
let files = (this.app as any).dragManager.draggable?.files;
|
||||
if(files) {
|
||||
if(files[0] == this.file) {
|
||||
files.shift();
|
||||
(this.app as any).dragManager.draggable.title = files.length + " files";
|
||||
}
|
||||
}
|
||||
if (['file', 'files'].includes((this.app as any).dragManager.draggable?.type)) return 'link';
|
||||
//if (transfer.types.includes('text/html') || transfer.types.includes('text/plain')) return 'copy';
|
||||
}
|
||||
|
||||
const excalidrawDiv = React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "excalidraw-wrapper",
|
||||
ref: excalidrawWrapperRef,
|
||||
key: "abc",
|
||||
tabIndex: 0,
|
||||
onKeyDown: (e:any) => {
|
||||
if(document.fullscreenEnabled && document.fullscreenElement == this.contentEl && e.keyCode==27) {
|
||||
document.exitFullscreen();
|
||||
this.zoomToFit();
|
||||
}
|
||||
this.ctrlKeyDown = e.ctrlKey;
|
||||
this.shiftKeyDown = e.shiftKey;
|
||||
this.altKeyDown = e.altKey;
|
||||
|
||||
if(e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
|
||||
const selectedElement = getTextElementAtPointer(currentPosition);
|
||||
if(!selectedElement) return;
|
||||
|
||||
const text:string = (this.textMode == TextMode.parsed)
|
||||
? this.excalidrawData.getRawText(selectedElement.id)
|
||||
: selectedElement.text;
|
||||
|
||||
if(!text) return;
|
||||
if(text.match(REG_LINKINDEX_HYPERLINK)) return;
|
||||
|
||||
const parts = text.matchAll(REGEX_LINK.EXPR).next();
|
||||
if(!parts.value) return;
|
||||
let linktext = REGEX_LINK.getLink(parts); //parts.value[2] ? parts.value[2]:parts.value[6];
|
||||
|
||||
if(linktext.match(REG_LINKINDEX_HYPERLINK)) return;
|
||||
|
||||
this.plugin.hover.linkText = linktext;
|
||||
this.plugin.hover.sourcePath = this.file.path;
|
||||
hoverPreviewTarget = this.contentEl; //e.target;
|
||||
this.app.workspace.trigger('hover-link', {
|
||||
event: this.mouseEvent,
|
||||
source: VIEW_TYPE_EXCALIDRAW,
|
||||
hoverParent: hoverPreviewTarget,
|
||||
targetEl: hoverPreviewTarget,
|
||||
linktext: this.plugin.hover.linkText,
|
||||
sourcePath: this.plugin.hover.sourcePath
|
||||
});
|
||||
hoverPoint = currentPosition;
|
||||
if(document.fullscreenElement === this.contentEl) {
|
||||
const self = this;
|
||||
setTimeout(()=>{
|
||||
const popover = document.body.querySelector("div.popover");
|
||||
if(popover) self.contentEl.append(popover);
|
||||
},100)
|
||||
}
|
||||
}
|
||||
},
|
||||
onKeyUp: (e:any) => {
|
||||
this.ctrlKeyDown = e.ctrlKey;
|
||||
this.shiftKeyDown = e.shiftKey;
|
||||
this.altKeyDown = e.altKey;
|
||||
},
|
||||
onClick: (e:MouseEvent):any => {
|
||||
//@ts-ignore
|
||||
if(!(e.ctrlKey||e.metaKey)) return;
|
||||
if(!(this.plugin.settings.allowCtrlClick)) return;
|
||||
if(!this.getSelectedTextElement().id) return;
|
||||
this.handleLinkClick(this,e);
|
||||
},
|
||||
onMouseMove: (e:MouseEvent) => {
|
||||
//@ts-ignore
|
||||
this.mouseEvent = e.nativeEvent;
|
||||
},
|
||||
onMouseOver: (e:MouseEvent) => {
|
||||
clearHoverPreview();
|
||||
//console.log(e);
|
||||
},
|
||||
onDragOver: (e:any) => {
|
||||
const action = dropAction(e.dataTransfer);
|
||||
if (action) {
|
||||
e.dataTransfer.dropEffect = action;
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
},
|
||||
onDragLeave: () => { },
|
||||
},
|
||||
React.createElement(Excalidraw.default, {
|
||||
ref: excalidrawRef,
|
||||
width: dimensions.width,
|
||||
height: dimensions.height,
|
||||
UIOptions: {
|
||||
canvasActions: {
|
||||
loadScene: false,
|
||||
saveScene: false,
|
||||
saveAsScene: false,
|
||||
export: { saveFileToDisk: false },
|
||||
saveAsImage: false,
|
||||
saveToActiveFile: false,
|
||||
},
|
||||
},
|
||||
initialData: initdata,
|
||||
detectScroll: true,
|
||||
onPointerUpdate: (p:any) => {
|
||||
currentPosition = p.pointer;
|
||||
if(hoverPreviewTarget && (Math.abs(hoverPoint.x-p.pointer.x)>50 || Math.abs(hoverPoint.y-p.pointer.y)>50)) clearHoverPreview();
|
||||
if(!this.excalidrawRef.current.getAppState().viewModeEnabled) return;
|
||||
const handleLinkClick = () => {
|
||||
selectedTextElement = getTextElementAtPointer(p.pointer);
|
||||
if(selectedTextElement) {
|
||||
const event = new MouseEvent("click", {ctrlKey: true, shiftKey: this.shiftKeyDown, altKey:this.altKeyDown});
|
||||
this.handleLinkClick(this,event);
|
||||
selectedTextElement = null;
|
||||
}
|
||||
}
|
||||
|
||||
const buttonDown = !blockOnMouseButtonDown && p.button=="down";
|
||||
if(buttonDown) {
|
||||
blockOnMouseButtonDown = true;
|
||||
|
||||
//ctrl click
|
||||
if(this.ctrlKeyDown) {
|
||||
handleLinkClick();
|
||||
return;
|
||||
}
|
||||
|
||||
//dobule click
|
||||
const now = (new Date()).getTime();
|
||||
if(now-timestamp < 600) {
|
||||
handleLinkClick();
|
||||
}
|
||||
timestamp = now;
|
||||
return;
|
||||
}
|
||||
if (p.button=="up") {
|
||||
blockOnMouseButtonDown=false;
|
||||
}
|
||||
},
|
||||
onChange: (et:ExcalidrawElement[],st:AppState) => {
|
||||
if(this.justLoaded) {
|
||||
this.justLoaded = false;
|
||||
this.zoomToFit(false);
|
||||
previousSceneVersion = getSceneVersion(et);
|
||||
return;
|
||||
}
|
||||
if (st.editingElement == null && st.resizingElement == null &&
|
||||
st.draggingElement == null && st.editingGroupId == null &&
|
||||
st.editingLinearElement == null ) {
|
||||
const sceneVersion = getSceneVersion(et);
|
||||
if(sceneVersion != previousSceneVersion) {
|
||||
previousSceneVersion = sceneVersion;
|
||||
this.dirty=this.file?.path;
|
||||
}
|
||||
}
|
||||
},
|
||||
onLibraryChange: (items:LibraryItems) => {
|
||||
(async () => {
|
||||
this.plugin.setStencilLibrary(EXCALIDRAW_LIB_HEADER+JSON.stringify(items)+'}');
|
||||
await this.plugin.saveSettings();
|
||||
})();
|
||||
},
|
||||
/*onPaste: (data: ClipboardData, event: ClipboardEvent | null) => {
|
||||
console.log(data,event);
|
||||
return true;
|
||||
},*/
|
||||
onDrop: (event: React.DragEvent<HTMLDivElement>):boolean => {
|
||||
const st: AppState = excalidrawRef.current.getAppState();
|
||||
currentPosition = viewportCoordsToSceneCoords({ clientX: event.clientX, clientY: event.clientY },st);
|
||||
const draggable = (this.app as any).dragManager.draggable;
|
||||
switch(draggable?.type) {
|
||||
case "file":
|
||||
this.addText(`[[${this.app.metadataCache.fileToLinktext(draggable.file,this.file.path,true)}]]`);
|
||||
return true;
|
||||
case "files":
|
||||
for(const f of draggable.files) {
|
||||
this.addText(`[[${this.app.metadataCache.fileToLinktext(f,this.file.path,true)}]]`);
|
||||
currentPosition.y+=st.currentItemFontSize*2;
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
onBeforeTextEdit: (textElement: ExcalidrawTextElement) => {
|
||||
if(this.autosaveTimer) { //stopping autosave to avoid autosave overwriting text while the user edits it
|
||||
clearInterval(this.autosaveTimer);
|
||||
this.autosaveTimer = null;
|
||||
}
|
||||
if(this.textMode==TextMode.parsed) return this.excalidrawData.getRawText(textElement.id);
|
||||
return null;
|
||||
},
|
||||
onBeforeTextSubmit: (textElement: ExcalidrawTextElement, text:string, isDeleted:boolean) => {
|
||||
if(isDeleted) {
|
||||
this.excalidrawData.deleteTextElement(textElement.id);
|
||||
this.dirty=this.file?.path;
|
||||
this.setupAutosaveTimer();
|
||||
return;
|
||||
}
|
||||
//If the parsed text is different than the raw text, and if View is in TextMode.parsed
|
||||
//Then I need to clear the undo history to avoid overwriting raw text with parsed text and losing links
|
||||
if(text!=textElement.text) { //the user made changes to the text
|
||||
//setTextElement will attempt a quick parse (without processing transclusions)
|
||||
const parseResult = this.excalidrawData.setTextElement(textElement.id, text,async ()=>{
|
||||
await this.save(false);
|
||||
//this callback function will only be invoked if quick parse fails, i.e. there is a transclusion in the raw text
|
||||
//thus I only check if TextMode.parsed, text is always != with parseResult
|
||||
if(this.textMode == TextMode.parsed) this.excalidrawRef.current.history.clear();
|
||||
this.setupAutosaveTimer();
|
||||
});
|
||||
if(parseResult) { //there were no transclusions in the raw text, quick parse was successful
|
||||
this.setupAutosaveTimer();
|
||||
if(this.textMode == TextMode.raw) return; //text is displayed in raw, no need to clear the history, undo will not create problems
|
||||
if(text == parseResult) return; //There were no links to parse, raw text and parsed text are equivalent
|
||||
this.excalidrawRef.current.history.clear();
|
||||
return parseResult;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.setupAutosaveTimer();
|
||||
if(this.textMode==TextMode.parsed) return this.excalidrawData.getParsedText(textElement.id);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return React.createElement(
|
||||
React.Fragment,
|
||||
null,
|
||||
React.createElement(
|
||||
"div",
|
||||
{
|
||||
className: "excalidraw-wrapper",
|
||||
ref: excalidrawWrapperRef,
|
||||
key: "abc",
|
||||
onKeyDown: (e:any) => ctrlKeyDown = e.ctrlKey,
|
||||
onKeyUp: (e:any) => ctrlKeyDown = e.ctrlKey,
|
||||
onClick: (e:MouseEvent):any => {
|
||||
//@ts-ignore
|
||||
if(!(e.ctrlKey||e.metaKey)) return;
|
||||
if(!(this.plugin.settings.allowCtrlClick)) return;
|
||||
if(!this.getSelectedTextElement().id) return;
|
||||
this.handleLinkClick(this,e);
|
||||
}
|
||||
},
|
||||
React.createElement(Excalidraw.default, {
|
||||
ref: excalidrawRef,
|
||||
width: dimensions.width,
|
||||
height: dimensions.height,
|
||||
UIOptions: {
|
||||
canvasActions: {
|
||||
loadScene: false,
|
||||
saveScene: false,
|
||||
saveAsScene: false,
|
||||
export: { saveFileToDisk: false },
|
||||
saveAsImage: false,
|
||||
saveToActiveFile: false,
|
||||
},
|
||||
},
|
||||
initialData: initdata,
|
||||
detectScroll: true,
|
||||
onPointerUpdate: (p:any) => {
|
||||
currentPosition = p.pointer;
|
||||
if(!this.excalidrawRef.current.getAppState().viewModeEnabled) return;
|
||||
if (ctrlKeyDown && !block && p.button=="down") {
|
||||
block = true; //this is to avoid handleLinkClick firing multiple times on a single click
|
||||
const elements = this.excalidrawRef.current.getSceneElements()
|
||||
.filter((e:ExcalidrawElement)=>{
|
||||
return e.type == "text"
|
||||
&& e.x<=p.pointer.x && (e.x+e.width)>=p.pointer.x
|
||||
&& e.y<=p.pointer.y && (e.y+e.height)>=p.pointer.y;
|
||||
});
|
||||
if(elements.length>0) {
|
||||
selected = {id:elements[0].id,text:elements[0].text};
|
||||
this.handleLinkClick(this,new MouseEvent("click", {ctrlKey: true}));
|
||||
selected = null;
|
||||
}
|
||||
}
|
||||
if (p.button=="up") block=false;
|
||||
},
|
||||
onChange: (et:ExcalidrawElement[],st:AppState) => {
|
||||
if(this.justLoaded) {
|
||||
this.justLoaded = false;
|
||||
this.zoomToFit();
|
||||
previousSceneVersion = getSceneVersion(et);
|
||||
return;
|
||||
}
|
||||
if (st.editingElement == null && st.resizingElement == null &&
|
||||
st.draggingElement == null && st.editingGroupId == null &&
|
||||
st.editingLinearElement == null ) {
|
||||
const sceneVersion = getSceneVersion(et);
|
||||
if(sceneVersion != previousSceneVersion) {
|
||||
previousSceneVersion = sceneVersion;
|
||||
this.dirty=this.file?.path;
|
||||
}
|
||||
}
|
||||
},
|
||||
onLibraryChange: (items:LibraryItems) => {
|
||||
(async () => {
|
||||
this.plugin.setStencilLibrary(EXCALIDRAW_LIB_HEADER+JSON.stringify(items)+'}');
|
||||
await this.plugin.saveSettings();
|
||||
})();
|
||||
},
|
||||
/*onPaste: (data: ClipboardData, event: ClipboardEvent | null) => {
|
||||
console.log(data,event);
|
||||
return true;
|
||||
},*/
|
||||
onBeforeTextEdit: (textElement: ExcalidrawTextElement) => {
|
||||
if(this.autosaveTimer) { //stopping autosave to avoid autosave overwriting text while the user edits it
|
||||
clearInterval(this.autosaveTimer);
|
||||
this.autosaveTimer = null;
|
||||
}
|
||||
if(this.textMode==TextMode.parsed) return this.excalidrawData.getRawText(textElement.id);
|
||||
return null;
|
||||
},
|
||||
onBeforeTextSubmit: (textElement: ExcalidrawTextElement, text:string, isDeleted:boolean) => {
|
||||
if(isDeleted) {
|
||||
this.excalidrawData.deleteTextElement(textElement.id);
|
||||
this.dirty=this.file?.path;
|
||||
this.setupAutosaveTimer();
|
||||
return;
|
||||
}
|
||||
//If the parsed text is different than the raw text, and if View is in TextMode.parsed
|
||||
//Then I need to clear the undo history to avoid overwriting raw text with parsed text and losing links
|
||||
if(text!=textElement.text) { //the user made changes to the text
|
||||
//setTextElement will attempt a quick parse (without processing transclusions)
|
||||
const parseResult = this.excalidrawData.setTextElement(textElement.id, text,async ()=>{
|
||||
await this.save(false);
|
||||
//this callback function will only be invoked if quick parse fails, i.e. there is a transclusion in the raw text
|
||||
//thus I only check if TextMode.parsed, text is always != with parseResult
|
||||
if(this.textMode == TextMode.parsed) this.excalidrawRef.current.history.clear();
|
||||
this.setupAutosaveTimer();
|
||||
});
|
||||
if(parseResult) { //there were no transclusions in the raw text, quick parse was successful
|
||||
this.setupAutosaveTimer();
|
||||
if(this.textMode == TextMode.raw) return; //text is displayed in raw, no need to clear the history, undo will not create problems
|
||||
if(text == parseResult) return; //There were no links to parse, raw text and parsed text are equivalent
|
||||
this.excalidrawRef.current.history.clear();
|
||||
return parseResult;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.setupAutosaveTimer();
|
||||
if(this.textMode==TextMode.parsed) return this.excalidrawData.getParsedText(textElement.id);
|
||||
}
|
||||
})
|
||||
)
|
||||
excalidrawDiv
|
||||
);
|
||||
|
||||
});
|
||||
ReactDOM.render(reactElement,this.contentEl);
|
||||
ReactDOM.render(reactElement,this.contentEl,()=>this.excalidrawWrapperRef.current.focus());
|
||||
}
|
||||
|
||||
private zoomToFit() {
|
||||
//when viewmode is enabled Excalidraw only listens to Alt+R
|
||||
const el = this.containerEl;
|
||||
const pattern = this.excalidrawRef.current.getAppState().viewModeEnabled
|
||||
? [250,500,750] : [null,250,null];
|
||||
if(pattern[0])
|
||||
setTimeout(()=>{
|
||||
const e = new KeyboardEvent("keydown", {bubbles : true, cancelable : true, altKey : true, code:"KeyR"});
|
||||
el.querySelector("canvas")?.dispatchEvent(e);
|
||||
},pattern[0]);
|
||||
if(pattern[1])
|
||||
setTimeout(()=>{
|
||||
const e = new KeyboardEvent("keydown", {bubbles : true, cancelable : true, shiftKey : true, code:"Digit1"});
|
||||
el.querySelector("canvas")?.dispatchEvent(e);
|
||||
},pattern[1])
|
||||
if(pattern[2])
|
||||
setTimeout(()=>{
|
||||
const e = new KeyboardEvent("keydown", {bubbles : true, cancelable : true, altKey : true, code:"KeyR"});
|
||||
el.querySelector("canvas")?.dispatchEvent(e);
|
||||
},pattern[2]);
|
||||
private zoomToFit(delay:boolean = true) {
|
||||
if(!this.excalidrawRef) return;
|
||||
const current = this.excalidrawRef.current;
|
||||
const fullscreen = (document.fullscreenElement==this.contentEl);
|
||||
const elements = current.getSceneElements();
|
||||
if(delay) { //time for the DOM to render, I am sure there is a more elegant solution
|
||||
setTimeout(() => current.zoomToFit(elements,10,fullscreen?0:0.05),100);
|
||||
} else {
|
||||
current.zoomToFit(elements,10,fullscreen?0:0.05);
|
||||
}
|
||||
}
|
||||
|
||||
public static async getSVG(scene:any, exportSettings:ExportSettings):Promise<SVGSVGElement> {
|
||||
|
||||
61
src/Utils.ts
61
src/Utils.ts
@@ -1,5 +1,6 @@
|
||||
import { Modal, normalizePath, TAbstractFile, TFolder, Vault } from "obsidian";
|
||||
import { normalizePath, TAbstractFile, TFolder, Vault } from "obsidian";
|
||||
import { Random } from "roughjs/bin/math";
|
||||
import { Zoom } from "@zsviczian/excalidraw/types/types";
|
||||
|
||||
/**
|
||||
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
|
||||
@@ -60,15 +61,53 @@ export function getIMGPathFromExcalidrawFile (excalidrawPath:string,newExtension
|
||||
return fname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open or create a folderpath if it does not exist
|
||||
* @param folderpath
|
||||
*/
|
||||
export async function checkAndCreateFolder(vault:Vault,folderpath:string) {
|
||||
let folder = vault.getAbstractFileByPath(folderpath);
|
||||
if(folder && folder instanceof TFolder) return;
|
||||
await vault.createFolder(folderpath);
|
||||
}
|
||||
/**
|
||||
* Open or create a folderpath if it does not exist
|
||||
* @param folderpath
|
||||
*/
|
||||
export async function checkAndCreateFolder(vault:Vault,folderpath:string) {
|
||||
folderpath = normalizePath(folderpath);
|
||||
let folder = vault.getAbstractFileByPath(folderpath);
|
||||
if(folder && folder instanceof TFolder) return;
|
||||
await vault.createFolder(folderpath);
|
||||
}
|
||||
|
||||
let random = new Random(Date.now());
|
||||
export const randomInteger = () => Math.floor(random.next() * 2 ** 31);
|
||||
export const randomInteger = () => Math.floor(random.next() * 2 ** 31);
|
||||
|
||||
//https://macromates.com/blog/2006/wrapping-text-with-regular-expressions/
|
||||
export function wrapText(text:string, lineLen:number):string {
|
||||
if(!lineLen) return text;
|
||||
let outstring = "";
|
||||
// 1 2
|
||||
const reg = new RegExp(`(.{1,${lineLen}})(\\s+|$\\n?)|([^\\s]+)(\\s+|$\\n?)`,'gm');
|
||||
const res = text.matchAll(reg);
|
||||
let parts;
|
||||
while(!(parts = res.next()).done) {
|
||||
outstring += parts.value[1] ? parts.value[1].trimEnd() : parts.value[3].trimEnd();
|
||||
outstring += (parts.value[2]=='\n' || parts.value[4]=='\n') ?'\n\n':'\n';
|
||||
}
|
||||
return outstring.trimEnd();
|
||||
}
|
||||
|
||||
export const viewportCoordsToSceneCoords = (
|
||||
{ clientX, clientY }: { clientX: number; clientY: number },
|
||||
{
|
||||
zoom,
|
||||
offsetLeft,
|
||||
offsetTop,
|
||||
scrollX,
|
||||
scrollY,
|
||||
}: {
|
||||
zoom: Zoom;
|
||||
offsetLeft: number;
|
||||
offsetTop: number;
|
||||
scrollX: number;
|
||||
scrollY: number;
|
||||
},
|
||||
) => {
|
||||
const invScale = 1 / zoom.value;
|
||||
const x = (clientX - zoom.translation.x - offsetLeft) * invScale - scrollX;
|
||||
const y = (clientY - zoom.translation.y - offsetTop) * invScale - scrollY;
|
||||
return { x, y };
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import {customAlphabet} from "nanoid";
|
||||
export const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',8);
|
||||
export const FRONTMATTER_KEY = "excalidraw-plugin";
|
||||
export const FRONTMATTER_KEY_CUSTOM_PREFIX = "excalidraw-link-prefix";
|
||||
export const FRONTMATTER_KEY_CUSTOM_URL_PREFIX = "excalidraw-url-prefix";
|
||||
export const FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS = "excalidraw-link-brackets";
|
||||
export const VIEW_TYPE_EXCALIDRAW = "excalidraw";
|
||||
export const ICON_NAME = "excalidraw-icon";
|
||||
@@ -14,8 +15,8 @@ export const RERENDER_EVENT = "excalidraw-embed-rerender";
|
||||
export const BLANK_DRAWING = '{"type":"excalidraw","version":2,"source":"https://excalidraw.com","elements":[],"appState":{"gridSize":null,"viewBackgroundColor":"#ffffff"}}';
|
||||
export const FRONTMATTER = ["---","",`${FRONTMATTER_KEY}: unlocked`,"","---", "==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠==", "",""].join("\n");
|
||||
export const EMPTY_MESSAGE = "Hit enter to create a new drawing";
|
||||
export const TEXT_DISPLAY_PARSED_ICON_NAME = "presentation";
|
||||
export const TEXT_DISPLAY_RAW_ICON_NAME = "quote-glyph";
|
||||
export const TEXT_DISPLAY_PARSED_ICON_NAME = "quote-glyph";
|
||||
export const TEXT_DISPLAY_RAW_ICON_NAME = "presentation";
|
||||
export const FULLSCREEN_ICON_NAME="fullscreen";
|
||||
export const EXIT_FULLSCREEN_ICON_NAME = "exit-fullscreen";
|
||||
export const DISK_ICON_NAME = "disk";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS, FRONTMATTER_KEY_CUSTOM_PREFIX } from "src/constants";
|
||||
import { FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS, FRONTMATTER_KEY_CUSTOM_PREFIX, FRONTMATTER_KEY_CUSTOM_URL_PREFIX } from "src/constants";
|
||||
|
||||
// English
|
||||
export default {
|
||||
@@ -21,7 +21,7 @@ export default {
|
||||
NEW_IN_ACTIVE_PANE_EMBED: "Create a new drawing - IN THE CURRENT ACTIVE PANE - and embed into active document",
|
||||
EXPORT_SVG: "Save as SVG next to the current file",
|
||||
EXPORT_PNG: "Save as PNG next to the current file",
|
||||
TOGGLE_LOCK: "Toggle Text Element edit LOCK/UNLOCK",
|
||||
TOGGLE_LOCK: "Toggle Text Element edit RAW/PREVIEW",
|
||||
INSERT_LINK: "Insert link to file",
|
||||
INSERT_LATEX: "Insert LaTeX-symbol (e.g. $\\theta$)",
|
||||
ENTER_LATEX: "Enter a valid LaTeX expression",
|
||||
@@ -39,8 +39,8 @@ export default {
|
||||
FILENAME_INVALID_CHARS: 'File name cannot contain any of the following characters: * " \\ < > : | ?',
|
||||
FILE_DOES_NOT_EXIST: "File does not exist. Hold down ALT (or ALT+SHIFT) and CLICK link button to create a new file.",
|
||||
FORCE_SAVE: "Force-save to update transclusions in adjacent panes.\n(Please note, that autosave is always on)",
|
||||
RAW: "Text-elements are displayed in RAW mode. Click button to change to PREVIEW mode.",
|
||||
PARSED: "Text-elements are displayed in PREVIEW mode. Click button to change to RAW mode.",
|
||||
RAW: "Change to PREVIEW mode (only effects text-elements with links or transclusions)",
|
||||
PARSED: "Change to RAW mode (only effects text-elements with links or transclusions)",
|
||||
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",
|
||||
@@ -77,17 +77,23 @@ export default {
|
||||
"When Obsidian files change, the matching [[link]] in your drawings will also change. " +
|
||||
"If you don't want text accidentally changing in your drawings use [[links|with aliases]].",
|
||||
LINK_BRACKETS_NAME: "Show [[brackets]] around links",
|
||||
LINK_BRACKETS_DESC: "In preview (locked) mode, when parsing Text Elements, place brackets around links. " +
|
||||
LINK_BRACKETS_DESC: "In PREVIEW mode, when parsing Text Elements, place brackets around links. " +
|
||||
"You can override this setting for a specific drawing by adding '" + FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS +
|
||||
": true/false' to the file\'s frontmatter.",
|
||||
LINK_PREFIX_NAME:"Link prefix",
|
||||
LINK_PREFIX_DESC:"In preview (locked) mode, if the Text Element contains a link, precede the text with these characters. " +
|
||||
LINK_PREFIX_DESC:"In PREVIEW mode, if the Text Element contains a link, precede the text with these characters. " +
|
||||
"You can override this setting for a specific drawing by adding \'" + FRONTMATTER_KEY_CUSTOM_PREFIX +
|
||||
': "👉 "\' to the file\'s frontmatter.',
|
||||
': "📍 "\' to the file\'s frontmatter.',
|
||||
URL_PREFIX_NAME:"URL prefix",
|
||||
URL_PREFIX_DESC:"In PREVIEW mode, if the Text Element contains a URL link, precede the text with these characters. " +
|
||||
"You can override this setting for a specific drawing by adding \'" + FRONTMATTER_KEY_CUSTOM_URL_PREFIX +
|
||||
': "🌐 "\' to the file\'s frontmatter.',
|
||||
LINK_CTRL_CLICK_NAME: "CTRL + CLICK on text to open them as links",
|
||||
LINK_CTRL_CLICK_DESC: "You can turn this feature off if it interferes with default Excalidraw features you want to use. If " +
|
||||
"this is turned off, only the link button in the title bar of the drawing pane will open links.",
|
||||
EMBED_HEAD: "Embed & Export",
|
||||
EMBED_PREVIEW_SVG_NAME: "Display SVG in markdown preview",
|
||||
EMBED_PREVIEW_SVG_DESC: "The default is to display drawings as SVG images in the markdown preview. Turning this feature off, the markdown preview will display the drawing as an embedded PNG image.",
|
||||
EMBED_WIDTH_NAME: "Default width of embedded (transcluded) image",
|
||||
EMBED_WIDTH_DESC: "The default width of an embedded drawing. You can specify a custom " +
|
||||
"width when embedding an image using the ![[drawing.excalidraw|100]] or " +
|
||||
@@ -126,7 +132,7 @@ export default {
|
||||
FILETYPE_DESC: "Excalidraw files will receive an indicator using the emojii or text defined in the next setting.",
|
||||
FILETAG_NAME: "Set the type indicator for excalidraw.md files",
|
||||
FILETAG_DESC: "The text or emojii to display as type indicator.",
|
||||
|
||||
INSERT_EMOJI: "Insert an emoji",
|
||||
|
||||
|
||||
//openDrawings.ts
|
||||
|
||||
221
src/main.ts
221
src/main.ts
@@ -15,8 +15,6 @@ import {
|
||||
MarkdownRenderer,
|
||||
ViewState,
|
||||
Notice,
|
||||
TFolder,
|
||||
Modal,
|
||||
} from "obsidian";
|
||||
|
||||
import {
|
||||
@@ -33,11 +31,8 @@ import {
|
||||
RERENDER_EVENT,
|
||||
FRONTMATTER_KEY,
|
||||
FRONTMATTER,
|
||||
//LOCK_ICON,
|
||||
TEXT_DISPLAY_PARSED_ICON_NAME,
|
||||
TEXT_DISPLAY_RAW_ICON_NAME,
|
||||
//UNLOCK_ICON,
|
||||
JSON_parse
|
||||
JSON_parse,
|
||||
nanoid
|
||||
} from "./constants";
|
||||
import ExcalidrawView, {ExportSettings, TextMode} from "./ExcalidrawView";
|
||||
import {getJSON} from "./ExcalidrawData";
|
||||
@@ -52,14 +47,25 @@ import {
|
||||
} from "./openDrawing";
|
||||
import {
|
||||
initExcalidrawAutomate,
|
||||
destroyExcalidrawAutomate,
|
||||
exportSceneToMD,
|
||||
destroyExcalidrawAutomate
|
||||
} from "./ExcalidrawAutomate";
|
||||
import { Prompt } from "./Prompt";
|
||||
import { around } from "monkey-around";
|
||||
import { t } from "./lang/helpers";
|
||||
import { MigrationPrompt } from "./MigrationPrompt";
|
||||
import { checkAndCreateFolder, download, getIMGPathFromExcalidrawFile, getNewUniqueFilepath } from "./Utils";
|
||||
import { checkAndCreateFolder, download, getIMGPathFromExcalidrawFile, getNewUniqueFilepath, splitFolderAndFilename } from "./Utils";
|
||||
|
||||
declare module "obsidian" {
|
||||
interface App {
|
||||
isMobile():boolean;
|
||||
}
|
||||
interface Vault {
|
||||
getConfig(option:"attachmentFolderPath"): string;
|
||||
}
|
||||
interface Workspace {
|
||||
on(name: 'hover-link', callback: (e:MouseEvent) => any, ctx?: any): EventRef;
|
||||
}
|
||||
}
|
||||
|
||||
export default class ExcalidrawPlugin extends Plugin {
|
||||
public excalidrawFileModes: { [file: string]: string } = {};
|
||||
@@ -69,7 +75,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
private openDialog: OpenFileDialog;
|
||||
private activeExcalidrawView: ExcalidrawView = null;
|
||||
public lastActiveExcalidrawFilePath: string = null;
|
||||
private hover: {linkText: string, sourcePath: string} = {linkText: null, sourcePath: null};
|
||||
public hover: {linkText: string, sourcePath: string} = {linkText: null, sourcePath: null};
|
||||
private observer: MutationObserver;
|
||||
private fileExplorerObserver: MutationObserver;
|
||||
public opencount:number = 0;
|
||||
@@ -83,8 +89,6 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
addIcon(DISK_ICON_NAME,DISK_ICON);
|
||||
addIcon(PNG_ICON_NAME,PNG_ICON);
|
||||
addIcon(SVG_ICON_NAME,SVG_ICON);
|
||||
//addIcon(TEXT_DISPLAY_PARSED_ICON_NAME,LOCK_ICON);
|
||||
//addIcon(TEXT_DISPLAY_RAW_ICON_NAME,UNLOCK_ICON);
|
||||
|
||||
await this.loadSettings();
|
||||
this.addSettingTab(new ExcalidrawSettingTab(this.app, this));
|
||||
@@ -122,12 +126,14 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a transcluded .excalidraw image in markdown preview mode
|
||||
*/
|
||||
private addMarkdownPostProcessor() {
|
||||
|
||||
interface imgElementAttributes {
|
||||
file?: TFile,
|
||||
fname: string, //Excalidraw filename
|
||||
fwidth: string, //Display width of image
|
||||
fheight: string, //Display height of image
|
||||
@@ -135,14 +141,18 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an img element with the .excalidraw drawing encoded as a base64 svg
|
||||
* Generates an img element with the drawing encoded as a base64 SVG or a PNG (depending on settings)
|
||||
* @param parts {imgElementAttributes} - display properties of the image
|
||||
* @returns {Promise<HTMLElement>} - the IMG HTML element containing the encoded SVG image
|
||||
* @returns {Promise<HTMLElement>} - the IMG HTML element containing the image
|
||||
*/
|
||||
const getIMG = async (parts:imgElementAttributes):Promise<HTMLElement> => {
|
||||
const file = this.app.vault.getAbstractFileByPath(parts.fname);
|
||||
if(!(file && file instanceof TFile)) {
|
||||
return null;
|
||||
const getIMG = async (imgAttributes:imgElementAttributes):Promise<HTMLElement> => {
|
||||
let file = imgAttributes.file;
|
||||
if(!imgAttributes.file) {
|
||||
const f = this.app.vault.getAbstractFileByPath(imgAttributes.fname);
|
||||
if(!(f && f instanceof TFile)) {
|
||||
return null;
|
||||
}
|
||||
file = f;
|
||||
}
|
||||
|
||||
const content = await this.app.vault.read(file);
|
||||
@@ -150,16 +160,28 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
withBackground: this.settings.exportWithBackground,
|
||||
withTheme: this.settings.exportWithTheme
|
||||
}
|
||||
const img = createEl("img");
|
||||
img.setAttribute("width",imgAttributes.fwidth);
|
||||
if(imgAttributes.fheight) img.setAttribute("height",imgAttributes.fheight);
|
||||
img.addClass(imgAttributes.style);
|
||||
|
||||
|
||||
if(!this.settings.displaySVGInPreview) {
|
||||
const width = parseInt(imgAttributes.fwidth);
|
||||
let scale = 1;
|
||||
if(width>=800) scale = 2;
|
||||
if(width>=1600) scale = 3;
|
||||
if(width>=2400) scale = 4;
|
||||
const png = await ExcalidrawView.getPNG(JSON_parse(getJSON(content)),exportSettings, scale);
|
||||
if(!png) return null;
|
||||
img.src = URL.createObjectURL(png);
|
||||
return img;
|
||||
}
|
||||
let svg = await ExcalidrawView.getSVG(JSON_parse(getJSON(content)),exportSettings);
|
||||
if(!svg) return null;
|
||||
svg = ExcalidrawView.embedFontsInSVG(svg);
|
||||
const img = createEl("img");
|
||||
svg.removeAttribute('width');
|
||||
svg.removeAttribute('height');
|
||||
img.setAttribute("width",parts.fwidth);
|
||||
|
||||
if(parts.fheight) img.setAttribute("height",parts.fheight);
|
||||
img.addClass(parts.style);
|
||||
img.setAttribute("src","data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.outerHTML.replaceAll(" "," ")))));
|
||||
return img;
|
||||
}
|
||||
@@ -170,12 +192,12 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
* @param ctx
|
||||
*/
|
||||
const markdownPostProcessor = async (el:HTMLElement,ctx:MarkdownPostProcessorContext) => {
|
||||
const drawings = el.querySelectorAll('.internal-embed');
|
||||
if(drawings.length==0) return;
|
||||
const embeddedItems = el.querySelectorAll('.internal-embed');
|
||||
if(embeddedItems.length==0) return;
|
||||
|
||||
let attr:imgElementAttributes={fname:"",fheight:"",fwidth:"",style:""};
|
||||
let alt:string, img:any, parts, div, file:TFile;
|
||||
for (const drawing of drawings) {
|
||||
let alt:string, parts, div, file:TFile;
|
||||
for (const drawing of embeddedItems) {
|
||||
attr.fname = drawing.getAttribute("src");
|
||||
file = this.app.metadataCache.getFirstLinkpathDest(attr.fname, ctx.sourcePath);
|
||||
if(file && file instanceof TFile && this.isExcalidrawFile(file)) {
|
||||
@@ -185,19 +207,20 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
if(alt == attr.fname) alt = ""; //when the filename starts with numbers followed by a space Obsidian recognizes the filename as alt-text
|
||||
attr.style = "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 does not include the altext of the image
|
||||
//thus need to add an additional "|" character when its a span
|
||||
//for some reason Obsidian renders ![]() in a DIV and ![[]] in a SPAN
|
||||
//also the alt-text of the DIV does not include the alt-text 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*)\|?(.*)/);
|
||||
//1:width, 2:height, 3:style 1 2 3
|
||||
parts = alt.match(/[^\|]*\|?(\d*%?)x?(\d*%?)\|?(.*)/);
|
||||
attr.fwidth = parts[1]? parts[1] : this.settings.width;
|
||||
attr.fheight = parts[2];
|
||||
if(parts[3]!=attr.fname) attr.style = "excalidraw-svg" + (parts[3] ? "-" + parts[3] : "");
|
||||
}
|
||||
|
||||
attr.fname = file?.path;
|
||||
img = await getIMG(attr);
|
||||
div = createDiv(attr.style, (el)=>{
|
||||
div = createDiv(attr.style, async (el)=>{
|
||||
const img = await getIMG(attr);
|
||||
el.append(img);
|
||||
el.setAttribute("src",file.path);
|
||||
if(attr.fwidth) el.setAttribute("w",attr.fwidth);
|
||||
@@ -240,18 +263,17 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
this.hover.sourcePath = e.sourcePath;
|
||||
};
|
||||
this.registerEvent(
|
||||
//@ts-ignore
|
||||
this.app.workspace.on('hover-link',hoverEvent)
|
||||
);
|
||||
|
||||
//monitoring for div.popover.hover-popover.file-embed.is-loaded to be added to the DOM tree
|
||||
this.observer = new MutationObserver((m)=>{
|
||||
this.observer = new MutationObserver(async (m)=>{
|
||||
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") {
|
||||
if(file.extension == "excalidraw") {
|
||||
observerForLegacyFileFormat(m,file);
|
||||
return;
|
||||
}
|
||||
@@ -269,12 +291,14 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
if(m[i].addedNodes[0].firstElementChild?.firstElementChild?.className=="excalidraw-svg") return;
|
||||
|
||||
//@ts-ignore
|
||||
if(!m[i].addedNodes[0].matchParent(".hover-popover")) return;
|
||||
const hoverPopover = m[i].addedNodes[0].matchParent(".hover-popover");
|
||||
if(!hoverPopover) return;
|
||||
const node = m[i].addedNodes[0];
|
||||
|
||||
//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"});
|
||||
const div = createDiv("", async (el)=>{
|
||||
const img = await getIMG({file:file,fname:file.path,fwidth:"300",fheight:null,style:"excalidraw-svg"});
|
||||
el.appendChild(img);
|
||||
el.setAttribute("src",file.path);
|
||||
el.onClickEvent((ev)=>{
|
||||
@@ -283,12 +307,11 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
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)
|
||||
|
||||
node.insertBefore(div,node.firstChild)
|
||||
});
|
||||
|
||||
//compatibility: .excalidraw file observer
|
||||
let observerForLegacyFileFormat = (m:MutationRecord[], file:TFile) => {
|
||||
let observerForLegacyFileFormat = async (m:MutationRecord[], file:TFile) => {
|
||||
if(!this.hover.linkText) return;
|
||||
if(m.length!=1) return;
|
||||
if(m[0].addedNodes.length != 1) return;
|
||||
@@ -299,8 +322,8 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
|
||||
//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 img = await getIMG({file:file,fname:file.path,fwidth:"300",fheight:null,style:"excalidraw-svg"});
|
||||
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)=>{
|
||||
@@ -326,35 +349,32 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
this.fileExplorerObserver = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display characters configured in settings, in front of the filename, if the markdown file is an excalidraw drawing
|
||||
*/
|
||||
private experimentalFileTypeDisplay() {
|
||||
const insertFiletype = (el: HTMLElement) => {
|
||||
if(el.childElementCount != 1) return;
|
||||
//@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 = 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
n.querySelectorAll(".nav-file-title").forEach(insertFiletype);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
document.querySelectorAll(".nav-file-title").forEach(insertFiletype); //apply filetype to files already displayed
|
||||
this.fileExplorerObserver.observe(document.querySelector(".workspace"), {childList: true, subtree: true});
|
||||
});
|
||||
|
||||
@@ -419,7 +439,6 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
id: "excalidraw-download-lib",
|
||||
name: t("DOWNLOAD_LIBRARY"),
|
||||
callback: async () => {
|
||||
//@ts-ignore
|
||||
if(this.app.isMobile) {
|
||||
const prompt = new Prompt(this.app, "Please provide a filename",'my-library','filename, leave blank to cancel action');
|
||||
prompt.openAndGetValue( async (filename:string)=> {
|
||||
@@ -495,6 +514,26 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
},
|
||||
});
|
||||
|
||||
const insertDrawingToDoc = async (inNewPane:boolean) => {
|
||||
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if(!activeView) return;
|
||||
let folder = this.app.vault.getConfig("attachmentFolderPath");
|
||||
// folder == null: save to vault root
|
||||
// folder == "./" save to same folder as current file
|
||||
// folder == "folder" save to specific folder in vault
|
||||
// folder == "./folder" save to specific subfolder of current active folder
|
||||
if(folder && folder.startsWith("./")) { // folder relative to current file
|
||||
const activeFileFolder = splitFolderAndFilename(activeView.file.path).folderpath + "/";
|
||||
folder = normalizePath(activeFileFolder + folder.substring(2));
|
||||
}
|
||||
if(!folder) folder = "";
|
||||
await checkAndCreateFolder(this.app.vault,folder);
|
||||
const filename = activeView.file.basename + "_" + window.moment().format(this.settings.drawingFilenameDateTime)
|
||||
+ (this.settings.compatibilityMode ? '.excalidraw' : '.excalidraw.md');
|
||||
this.embedDrawing(normalizePath(folder + "/" + filename));
|
||||
this.createDrawing(filename, inNewPane,folder==""?null:folder);
|
||||
}
|
||||
|
||||
this.addCommand({
|
||||
id: "excalidraw-autocreate-and-embed",
|
||||
name: t("NEW_IN_NEW_PANE_EMBED"),
|
||||
@@ -502,9 +541,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
if (checking) {
|
||||
return (this.app.workspace.activeLeaf.view.getViewType() == "markdown");
|
||||
} else {
|
||||
const filename = this.getNextDefaultFilename();
|
||||
this.embedDrawing(filename);
|
||||
this.createDrawing(filename, true);
|
||||
insertDrawingToDoc(true);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
@@ -517,9 +554,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
if (checking) {
|
||||
return (this.app.workspace.activeLeaf.view.getViewType() == "markdown");
|
||||
} else {
|
||||
const filename = this.getNextDefaultFilename();
|
||||
this.embedDrawing(filename);
|
||||
this.createDrawing(filename, false);
|
||||
insertDrawingToDoc(false);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
@@ -692,7 +727,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
const filename = file.name.substr(0,file.name.lastIndexOf(".excalidraw")) + (replaceExtension ? ".md" : ".excalidraw.md");
|
||||
const fname = getNewUniqueFilepath(this.app.vault,filename,normalizePath(file.path.substr(0,file.path.lastIndexOf(file.name))));
|
||||
console.log(fname);
|
||||
const result = await this.app.vault.create(fname,FRONTMATTER + exportSceneToMD(data));
|
||||
const result = await this.app.vault.create(fname,FRONTMATTER + this.exportSceneToMD(data));
|
||||
if (this.settings.keepInSync) {
|
||||
['.svg','.png'].forEach( (ext:string)=>{
|
||||
const oldIMGpath = file.path.substring(0,file.path.lastIndexOf(".excalidraw")) + ext;
|
||||
@@ -850,7 +885,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
const deleteEventHandler = async (file:TFile) => {
|
||||
if (!(file instanceof TFile)) return;
|
||||
//@ts-ignore
|
||||
const isExcalidarwFile = (file.unsafeCachedData && file.unsafeCachedData.search(/---\n[\s\S]*excalidraw-plugin:\s*(locked|unlocked)\n[\s\S]*---/gm)>-1)
|
||||
const isExcalidarwFile = (file.unsafeCachedData && file.unsafeCachedData.search(/---[\r\n][\s\S]*excalidraw-plugin:\s*(locked|unlocked)[\r\n][\s\S]*---/gm)>-1)
|
||||
|| (file.extension=="excalidraw");
|
||||
if(!isExcalidarwFile) return;
|
||||
|
||||
@@ -878,14 +913,11 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
);
|
||||
|
||||
//save open drawings when user quits the application
|
||||
const quitEventHandler = (tasks: Tasks) => {
|
||||
const quitEventHandler = async (tasks: Tasks) => {
|
||||
const leaves = self.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
for (let i=0;i<leaves.length;i++) {
|
||||
(leaves[i].view as ExcalidrawView).save();
|
||||
await (leaves[i].view as ExcalidrawView).save(true);
|
||||
}
|
||||
this.settings.drawingOpenCount += this.opencount;
|
||||
this.settings.loadCount++;
|
||||
//this.saveSettings();
|
||||
}
|
||||
self.registerEvent(
|
||||
self.app.workspace.on("quit",quitEventHandler)
|
||||
@@ -948,7 +980,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
}
|
||||
|
||||
public setStencilLibrary(library:string) {
|
||||
this.settings.library = library;
|
||||
this.settings.library = library;
|
||||
}
|
||||
|
||||
public triggerEmbedUpdates(filepath?:string){
|
||||
@@ -1002,10 +1034,39 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
if (this.settings.compatibilityMode) {
|
||||
return BLANK_DRAWING;
|
||||
}
|
||||
return FRONTMATTER + '\n# Drawing\n'
|
||||
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'json\n'
|
||||
+ BLANK_DRAWING + '\n'
|
||||
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96);
|
||||
return FRONTMATTER + '\n' + this.getMarkdownDrawingSection(BLANK_DRAWING);
|
||||
}
|
||||
|
||||
public getMarkdownDrawingSection(jsonString: string) {
|
||||
return '%%\n# Drawing\n'
|
||||
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'json\n'
|
||||
+ jsonString + '\n'
|
||||
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96) + '%%';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the text elements from an Excalidraw scene into a string of ids as headers followed by the text contents
|
||||
* @param {string} data - Excalidraw scene JSON string
|
||||
* @returns {string} - Text starting with the "# Text Elements" header and followed by each "## id-value" and text
|
||||
*/
|
||||
public exportSceneToMD(data:string): string {
|
||||
if(!data) return "";
|
||||
const excalidrawData = JSON_parse(data);
|
||||
const textElements = excalidrawData.elements?.filter((el:any)=> el.type=="text")
|
||||
let outString = '# Text Elements\n';
|
||||
let id:string;
|
||||
for (const te of textElements) {
|
||||
id = te.id;
|
||||
//replacing Excalidraw text IDs with my own, because default IDs may contain
|
||||
//characters not recognized by Obsidian block references
|
||||
//also Excalidraw IDs are inconveniently long
|
||||
if(te.id.length>8) {
|
||||
id=nanoid();
|
||||
data = data.replaceAll(te.id,id); //brute force approach to replace all occurances.
|
||||
}
|
||||
outString += te.text+' ^'+id+'\n\n';
|
||||
}
|
||||
return outString + this.getMarkdownDrawingSection(data);
|
||||
}
|
||||
|
||||
public async createDrawing(filename: string, onNewPane: boolean, foldername?: string, initData?:string) {
|
||||
@@ -1041,7 +1102,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
} as ViewState);
|
||||
}
|
||||
|
||||
isExcalidrawFile(f:TFile) {
|
||||
public isExcalidrawFile(f:TFile) {
|
||||
if(f.extension=="excalidraw") return true;
|
||||
const fileCache = this.app.metadataCache.getFileCache(f);
|
||||
return !!fileCache?.frontmatter && !!fileCache.frontmatter[FRONTMATTER_KEY];
|
||||
|
||||
111
src/settings.ts
111
src/settings.ts
@@ -14,10 +14,11 @@ export interface ExcalidrawSettings {
|
||||
templateFilePath: string,
|
||||
drawingFilenamePrefix: string,
|
||||
drawingFilenameDateTime: string,
|
||||
displaySVGInPreview: boolean,
|
||||
width: string,
|
||||
showLinkBrackets: boolean,
|
||||
linkPrefix: string,
|
||||
//autosave: boolean;
|
||||
urlPrefix: string,
|
||||
allowCtrlClick: boolean, //if disabled only the link button in the view header will open links
|
||||
pngExportScale: number,
|
||||
exportWithTheme: boolean,
|
||||
@@ -40,10 +41,11 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
templateFilePath: 'Excalidraw/Template.excalidraw',
|
||||
drawingFilenamePrefix: 'Drawing ',
|
||||
drawingFilenameDateTime: 'YYYY-MM-DD HH.mm.ss',
|
||||
displaySVGInPreview: true,
|
||||
width: '400',
|
||||
linkPrefix: "📍",
|
||||
urlPrefix: "🌐",
|
||||
showLinkBrackets: true,
|
||||
//autosave: false,
|
||||
allowCtrlClick: true,
|
||||
pngExportScale: 1,
|
||||
exportWithTheme: true,
|
||||
@@ -63,16 +65,45 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
|
||||
export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
plugin: ExcalidrawPlugin;
|
||||
private requestEmbedUpdate:boolean = false;
|
||||
private requestReloadDrawings:boolean = false;
|
||||
|
||||
constructor(app: App, plugin: ExcalidrawPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
async hide() {
|
||||
if(this.requestReloadDrawings) {
|
||||
const exs = this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
for(const v of exs) {
|
||||
if(v.view instanceof ExcalidrawView) {
|
||||
await v.view.save(false);
|
||||
await v.view.reload(true);
|
||||
}
|
||||
}
|
||||
this.requestEmbedUpdate = true;
|
||||
}
|
||||
if(this.requestEmbedUpdate) this.plugin.triggerEmbedUpdates();
|
||||
}
|
||||
|
||||
display(): void {
|
||||
this.requestEmbedUpdate = false;
|
||||
this.requestReloadDrawings = false;
|
||||
let {containerEl} = this;
|
||||
this.containerEl.empty();
|
||||
|
||||
const coffeeDiv = containerEl.createDiv('coffee');
|
||||
coffeeDiv.addClass('ex-coffee-div');
|
||||
const coffeeLink = coffeeDiv.createEl('a', { href: 'https://ko-fi.com/zsolt' });
|
||||
const coffeeImg = coffeeLink.createEl('img', {
|
||||
attr: {
|
||||
src: 'https://cdn.ko-fi.com/cdn/kofi3.png?v=3',
|
||||
},
|
||||
});
|
||||
coffeeImg.height = 45;
|
||||
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("FOLDER_NAME"))
|
||||
.setDesc(t("FOLDER_DESC"))
|
||||
@@ -95,28 +126,6 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
/* new Setting(containerEl)
|
||||
.setName(t("AUTOSAVE_NAME"))
|
||||
.setDesc(t("AUTOSAVE_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autosave)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autosave = value;
|
||||
await this.plugin.saveSettings();
|
||||
const exs = this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
for(const v of exs) {
|
||||
if(v.view instanceof ExcalidrawView) {
|
||||
if(v.view.autosaveTimer) {
|
||||
clearInterval(v.view.autosaveTimer)
|
||||
v.view.autosaveTimer = null;
|
||||
}
|
||||
if(value) {
|
||||
v.view.setupAutosaveTimer();
|
||||
}
|
||||
}
|
||||
}
|
||||
}));*/
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("FILENAME_HEAD")});
|
||||
containerEl.createDiv('',(el) => {
|
||||
el.innerHTML = t("FILENAME_DESC");
|
||||
@@ -161,17 +170,6 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
this.containerEl.createEl('h1', {text: t("LINKS_HEAD")});
|
||||
this.containerEl.createEl('p',{
|
||||
text: t("LINKS_DESC")});
|
||||
|
||||
const reloadDrawings = async () => {
|
||||
const exs = this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
for(const v of exs) {
|
||||
if(v.view instanceof ExcalidrawView) {
|
||||
await v.view.save(false);
|
||||
v.view.reload(true);
|
||||
}
|
||||
}
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
}
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("LINK_BRACKETS_NAME"))
|
||||
@@ -181,19 +179,32 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.showLinkBrackets = value;
|
||||
await this.plugin.saveSettings();
|
||||
reloadDrawings();
|
||||
this.requestReloadDrawings = true;
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("LINK_PREFIX_NAME"))
|
||||
.setDesc(t("LINK_PREFIX_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder('📍')
|
||||
.setPlaceholder(t("INSERT_EMOJI"))
|
||||
.setValue(this.plugin.settings.linkPrefix)
|
||||
.onChange(async (value) => {
|
||||
.onChange((value) => {
|
||||
console.log(value);
|
||||
this.plugin.settings.linkPrefix = value;
|
||||
this.plugin.saveSettings();
|
||||
this.requestReloadDrawings = true;
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("URL_PREFIX_NAME"))
|
||||
.setDesc(t("URL_PREFIX_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder(t("INSERT_EMOJI"))
|
||||
.setValue(this.plugin.settings.urlPrefix)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.urlPrefix = value;
|
||||
await this.plugin.saveSettings();
|
||||
reloadDrawings();
|
||||
this.requestReloadDrawings = true;
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
@@ -208,6 +219,18 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("EMBED_HEAD")});
|
||||
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EMBED_PREVIEW_SVG_NAME"))
|
||||
.setDesc(t("EMBED_PREVIEW_SVG_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.displaySVGInPreview)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.displaySVGInPreview = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EMBED_WIDTH_NAME"))
|
||||
.setDesc(t("EMBED_WIDTH_DESC"))
|
||||
@@ -217,7 +240,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.width = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
this.requestEmbedUpdate = true;
|
||||
}));
|
||||
|
||||
let scaleText:HTMLDivElement;
|
||||
@@ -231,7 +254,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.onChange(async (value)=> {
|
||||
scaleText.innerText = " " + value.toString();
|
||||
this.plugin.settings.pngExportScale = value;
|
||||
this.plugin.saveSettings();
|
||||
await this.plugin.saveSettings();
|
||||
}))
|
||||
.settingEl.createDiv('',(el)=>{
|
||||
scaleText = el;
|
||||
@@ -248,7 +271,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.exportWithBackground = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
this.requestEmbedUpdate = true;
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
@@ -259,7 +282,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.exportWithTheme = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
this.requestEmbedUpdate = true;
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("EXPORT_HEAD")});
|
||||
@@ -345,7 +368,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.setName(t("FILETAG_NAME"))
|
||||
.setDesc(t("FILETAG_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder('✏️')
|
||||
.setPlaceholder(t("INSERT_EMOJI"))
|
||||
.setValue(this.plugin.settings.experimentalFileTag)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.experimentalFileTag = value;
|
||||
|
||||
21
styles.css
21
styles.css
@@ -81,12 +81,17 @@ li[data-testid] {
|
||||
box-shadow: 0 !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
/*
|
||||
@font-face {
|
||||
font-family: "Virgil";
|
||||
src: url("https://excalidraw.com/Virgil.woff2");
|
||||
|
||||
.disable-zen-mode--visible {
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
|
||||
.disable-zen-mode {
|
||||
width: 9em !important;
|
||||
}
|
||||
|
||||
.ex-coffee-div {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Cascadia";
|
||||
src: url("https://excalidraw.com/Cascadia.woff2");
|
||||
}*/
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"1.2.12": "0.11.13"
|
||||
"1.3.2": "0.11.13"
|
||||
}
|
||||
|
||||
14
yarn.lock
14
yarn.lock
@@ -1024,10 +1024,10 @@
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
|
||||
"@zsviczian/excalidraw@^0.9.0-onTextEditEvents-4":
|
||||
"integrity" "sha512-4W/s1gbsOCpSerqgog6Nu38doy6n1h+aEy5q7DS/nodJVca5bc6wj+OcUJJeajw7NabkKofOI2RYOVw3oMgJcw=="
|
||||
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.9.0-onTextEditEvents-4.tgz"
|
||||
"version" "0.9.0-onTextEditEvents-4"
|
||||
"@zsviczian/excalidraw@0.9.0-obsidian-7":
|
||||
"integrity" "sha512-7J10Klh7QWyJoie/nymezF3YALJtk0NORGPwKEP0vQybmPHCSaPCmAoF6GFnrSvC5UiFoa8RYK00F0sa3HJulg=="
|
||||
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.9.0-obsidian-7.tgz"
|
||||
"version" "0.9.0-obsidian-7"
|
||||
|
||||
"abab@^1.0.3":
|
||||
"integrity" "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4="
|
||||
@@ -2788,12 +2788,6 @@
|
||||
optionalDependencies:
|
||||
"fsevents" "~2.3.2"
|
||||
|
||||
"chokidar2@file:./chokidar2":
|
||||
"resolved" "file:node_modules/watchpack/chokidar2"
|
||||
"version" "2.0.0"
|
||||
dependencies:
|
||||
"chokidar" "^2.1.8"
|
||||
|
||||
"ci-info@^1.5.0":
|
||||
"integrity" "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A=="
|
||||
"resolved" "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz"
|
||||
|
||||
Reference in New Issue
Block a user