mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
44 Commits
1.3.12
...
Image-Elem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e676255d69 | ||
|
|
691c60be24 | ||
|
|
f4a458061a | ||
|
|
c88c898f4a | ||
|
|
d2da408a59 | ||
|
|
3b9a6404c5 | ||
|
|
d9306922c3 | ||
|
|
578cc7a99c | ||
|
|
aa9f9ba91f | ||
|
|
b9251d4f1d | ||
|
|
13a980afed | ||
|
|
c911e0118f | ||
|
|
eca02a5941 | ||
|
|
9a57db43f2 | ||
|
|
f6b65ac3e9 | ||
|
|
929348b390 | ||
|
|
57f1b9f8da | ||
|
|
fed106c811 | ||
|
|
739e919a43 | ||
|
|
e85cf4e196 | ||
|
|
0c42353fce | ||
|
|
7ebdec7713 | ||
|
|
1917dad8cd | ||
|
|
3100e2d70f | ||
|
|
7712cd49b6 | ||
|
|
856573763e | ||
|
|
3bbff7f8d5 | ||
|
|
034927ada0 | ||
|
|
0cccdad13f | ||
|
|
fe7f3f58c5 | ||
|
|
48fd854944 | ||
|
|
8f9746393f | ||
|
|
23da271b73 | ||
|
|
627775c6c3 | ||
|
|
59db43c3f0 | ||
|
|
597ee4f70e | ||
|
|
8222d8c146 | ||
|
|
f785d756be | ||
|
|
78fb37b173 | ||
|
|
a17638717f | ||
|
|
70de8ba2f8 | ||
|
|
e8a29a2715 | ||
|
|
7b1f13391c | ||
|
|
33081b1a84 |
14
TODO.md
Normal file
14
TODO.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[x] do not embed font into SVG when embedding Excalidraw into other Excalidraw
|
||||||
|
[x] add ```html <SVG>...</SVG> ``` codeblock to excalidraw markdown
|
||||||
|
[x] read pre-saved `<SVG>` when generating image preview
|
||||||
|
[x] update code to adopt change files moving from AppState to App
|
||||||
|
- Add "files" to legacy excalidraw export
|
||||||
|
|
||||||
|
[x] PNG preview
|
||||||
|
[x] markdown embed SVG 190
|
||||||
|
[x] markdown embed PNG
|
||||||
|
[x] embed Excalidraw into other Excalidraw
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -102,6 +102,8 @@ export interface ExcalidrawAutomate extends Window {
|
|||||||
targetView: ExcalidrawView;
|
targetView: ExcalidrawView;
|
||||||
setView (view:ExcalidrawView|"first"|"active"):ExcalidrawView;
|
setView (view:ExcalidrawView|"first"|"active"):ExcalidrawView;
|
||||||
getExcalidrawAPI ():any;
|
getExcalidrawAPI ():any;
|
||||||
|
getViewElements ():ExcalidrawElement[];
|
||||||
|
deleteViewElements (el: ExcalidrawElement[]):boolean;
|
||||||
getViewSelectedElement( ):ExcalidrawElement;
|
getViewSelectedElement( ):ExcalidrawElement;
|
||||||
getViewSelectedElements ():ExcalidrawElement[];
|
getViewSelectedElements ():ExcalidrawElement[];
|
||||||
viewToggleFullScreen (forceViewMode?:boolean):void;
|
viewToggleFullScreen (forceViewMode?:boolean):void;
|
||||||
@@ -117,6 +119,19 @@ export interface ExcalidrawAutomate extends Window {
|
|||||||
}
|
}
|
||||||
):boolean;
|
):boolean;
|
||||||
addElementsToView (repositionToCursor:boolean, save:boolean):Promise<boolean>;
|
addElementsToView (repositionToCursor:boolean, save:boolean):Promise<boolean>;
|
||||||
|
onDropHook (data: {
|
||||||
|
ea: ExcalidrawAutomate,
|
||||||
|
event: React.DragEvent<HTMLDivElement>,
|
||||||
|
draggable: any, //Obsidian draggable object
|
||||||
|
type: "file"|"text"|"unknown",
|
||||||
|
payload: {
|
||||||
|
files: TFile[], //TFile[] array of dropped files
|
||||||
|
text: string, //string
|
||||||
|
},
|
||||||
|
excalidrawFile: TFile, //the file receiving the drop event
|
||||||
|
view: ExcalidrawView, //the excalidraw view receiving the drop
|
||||||
|
pointerPosition: {x:number, y:number} //the pointer position on canvas at the time of drop
|
||||||
|
}):boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -114,6 +114,28 @@ getExcalidrawAPI():any
|
|||||||
Returns the native Excalidraw API (ref.current) for the active drawing specified in `targetView`.
|
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
|
See Excalidraw documentation here: https://www.npmjs.com/package/@excalidraw/excalidraw#ref
|
||||||
|
|
||||||
|
#### getViewElements()
|
||||||
|
```typescript
|
||||||
|
getViewElements():ExcalidrawElement[]
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns all the elements from the view.
|
||||||
|
|
||||||
|
#### deleteViewElements()
|
||||||
|
```typescript
|
||||||
|
deleteViewElements(elToDelete: ExcalidrawElement[]):boolean
|
||||||
|
```
|
||||||
|
|
||||||
|
Deletes those elements from the view that match the elements provided as the input parameter.
|
||||||
|
|
||||||
|
Example to delete the selected elements from the view:
|
||||||
|
```typescript
|
||||||
|
ea = ExcalidrawAutomate;
|
||||||
|
ea.setView("active");
|
||||||
|
el = ea.getViewSelectedElements();
|
||||||
|
ea.deleteViewElements();
|
||||||
|
```
|
||||||
|
|
||||||
#### getViewSelectedElement()
|
#### getViewSelectedElement()
|
||||||
```typescript
|
```typescript
|
||||||
getViewSelectedElement():ExcalidrawElement
|
getViewSelectedElement():ExcalidrawElement
|
||||||
@@ -163,3 +185,37 @@ Adds elements created with ExcalidrawAutomate to the target ExcalidrawView.
|
|||||||
`save` default is false
|
`save` default is false
|
||||||
- true: the drawing will be saved after the elements were added.
|
- 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.
|
- 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.
|
||||||
|
|
||||||
|
### onDropHook
|
||||||
|
```typescript
|
||||||
|
onDropHook (data: {
|
||||||
|
ea: ExcalidrawAutomate,
|
||||||
|
event: React.DragEvent<HTMLDivElement>,
|
||||||
|
draggable: any, //Obsidian draggable object
|
||||||
|
type: "file"|"text"|"unknown",
|
||||||
|
payload: {
|
||||||
|
files: TFile[], //TFile[] array of dropped files
|
||||||
|
text: string, //string
|
||||||
|
},
|
||||||
|
excalidrawFile: TFile, //the file receiving the drop event
|
||||||
|
view: ExcalidrawView, //the excalidraw view receiving the drop
|
||||||
|
pointerPosition: {x:number, y:number} //the pointer position on canvas at the time of drop
|
||||||
|
}):boolean;
|
||||||
|
```
|
||||||
|
|
||||||
|
Callback function triggered when an draggable item is dropped on Excalidraw.
|
||||||
|
The function should return a boolean value. True if the drop was handled by the hook and futher native processing should be stopped, and false if Excalidraw should continue with the processing of the drop.
|
||||||
|
type of drop can be one of:
|
||||||
|
- "file" if a file from Obsidian file explorer is dropped onto Excalidraw. In this case payload.files will contain the list of files dropped.
|
||||||
|
- "text" if a link (e.g. url, or wiki link) or other text is dropped. In this case payload.text will contain the received string
|
||||||
|
- "unknown" if Excalidraw plugin does not recognize the type of dropped object. In this case you can use React.DragEvent to analysed the dropped object.
|
||||||
|
|
||||||
|
Use Templater startup templates or similar to set the Hook function.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
ea = ExcalidrawAutomate;
|
||||||
|
ea.onDropHook = (data) => {
|
||||||
|
console.log(data);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-excalidraw-plugin",
|
"id": "obsidian-excalidraw-plugin",
|
||||||
"name": "Excalidraw",
|
"name": "Excalidraw",
|
||||||
"version": "1.3.12",
|
"version": "1.3.20",
|
||||||
"minAppVersion": "0.12.0",
|
"minAppVersion": "0.12.0",
|
||||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||||
"author": "Zsolt Viczian",
|
"author": "Zsolt Viczian",
|
||||||
|
|||||||
26
package.json
26
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-excalidraw-plugin",
|
"name": "obsidian-excalidraw-plugin",
|
||||||
"version": "1.1.10",
|
"version": "1.3.21",
|
||||||
"description": "This is an Obsidian.md plugin that lets you view and edit Excalidraw drawings",
|
"description": "This is an Obsidian.md plugin that lets you view and edit Excalidraw drawings",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@zsviczian/excalidraw": "0.9.0-obsidian-8",
|
"@zsviczian/excalidraw": "0.10.0-obsidian-2",
|
||||||
"monkey-around": "^2.2.0",
|
"monkey-around": "^2.2.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
@@ -19,22 +19,24 @@
|
|||||||
"roughjs": "4.4.1"
|
"roughjs": "4.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.14.6",
|
"@babel/core": "^7.15.5",
|
||||||
"@babel/preset-env": "^7.3.1",
|
"@babel/preset-env": "^7.15.6",
|
||||||
"@babel/preset-react": "^7.14.5",
|
"@babel/preset-react": "^7.14.5",
|
||||||
"@rollup/plugin-babel": "^5.3.0",
|
"@rollup/plugin-babel": "^5.3.0",
|
||||||
"@rollup/plugin-commonjs": "^15.1.0",
|
"@rollup/plugin-commonjs": "^21.0.0",
|
||||||
"@rollup/plugin-node-resolve": "^13.0.0",
|
"@rollup/plugin-node-resolve": "^13.0.5",
|
||||||
"@rollup/plugin-replace": "^2.4.2",
|
"@rollup/plugin-replace": "^2.4.2",
|
||||||
"@rollup/plugin-typescript": "^8.2.1",
|
"@rollup/plugin-typescript": "^8.2.5",
|
||||||
|
"@types/js-beautify": "^1.13.3",
|
||||||
"@types/node": "^15.12.4",
|
"@types/node": "^15.12.4",
|
||||||
"@types/react-dom": "^17.0.8",
|
"@types/react-dom": "^17.0.9",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
|
"js-beautify": "1.13.3",
|
||||||
"nanoid": "^3.1.23",
|
"nanoid": "^3.1.23",
|
||||||
"obsidian": "^0.12.11",
|
"obsidian": "^0.12.16",
|
||||||
"rollup": "^2.52.3",
|
"rollup": "^2.52.3",
|
||||||
"rollup-plugin-visualizer": "^5.5.0",
|
"rollup-plugin-visualizer": "^5.5.2",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.1",
|
||||||
"typescript": "^4.3.4"
|
"typescript": "^4.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,15 +9,17 @@ import {
|
|||||||
normalizePath,
|
normalizePath,
|
||||||
TFile
|
TFile
|
||||||
} from "obsidian"
|
} from "obsidian"
|
||||||
import ExcalidrawView from "./ExcalidrawView";
|
import ExcalidrawView, { TextMode } from "./ExcalidrawView";
|
||||||
import { getJSON } from "./ExcalidrawData";
|
import { ExcalidrawData, getJSON, getSVGString } from "./ExcalidrawData";
|
||||||
import {
|
import {
|
||||||
FRONTMATTER,
|
FRONTMATTER,
|
||||||
nanoid,
|
nanoid,
|
||||||
JSON_parse,
|
JSON_parse,
|
||||||
VIEW_TYPE_EXCALIDRAW
|
VIEW_TYPE_EXCALIDRAW,
|
||||||
|
MAX_IMAGE_SIZE
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import { wrapText } from "./Utils";
|
import { embedFontsInSVG, generateSVGString, getObsidianImage, getPNG, getSVG, loadSceneFiles, scaleLoadedImage, svgToBase64, wrapText } from "./Utils";
|
||||||
|
import { AppState } from "@zsviczian/excalidraw/types/types";
|
||||||
|
|
||||||
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
|
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
|
||||||
|
|
||||||
@@ -25,6 +27,7 @@ export interface ExcalidrawAutomate extends Window {
|
|||||||
ExcalidrawAutomate: {
|
ExcalidrawAutomate: {
|
||||||
plugin: ExcalidrawPlugin;
|
plugin: ExcalidrawPlugin;
|
||||||
elementsDict: {};
|
elementsDict: {};
|
||||||
|
imagesDict: {};
|
||||||
style: {
|
style: {
|
||||||
strokeColor: string;
|
strokeColor: string;
|
||||||
backgroundColor: string;
|
backgroundColor: string;
|
||||||
@@ -70,7 +73,7 @@ export interface ExcalidrawAutomate extends Window {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
):Promise<string>;
|
):Promise<string>;
|
||||||
createSVG (templatePath?:string):Promise<SVGSVGElement>;
|
createSVG (templatePath?:string, embedFont?:boolean):Promise<SVGSVGElement>;
|
||||||
createPNG (templatePath?:string):Promise<any>;
|
createPNG (templatePath?:string):Promise<any>;
|
||||||
wrapText (text:string, lineLen:number):string;
|
wrapText (text:string, lineLen:number):string;
|
||||||
addRect (topX:number, topY:number, width:number, height:number):string;
|
addRect (topX:number, topY:number, width:number, height:number):string;
|
||||||
@@ -101,6 +104,7 @@ export interface ExcalidrawAutomate extends Window {
|
|||||||
endObjectId?:string
|
endObjectId?:string
|
||||||
}
|
}
|
||||||
):string ;
|
):string ;
|
||||||
|
addImage(topX:number, topY:number, imageFile: TFile):Promise<string>;
|
||||||
connectObjects (
|
connectObjects (
|
||||||
objectA: string,
|
objectA: string,
|
||||||
connectionA: ConnectionPoint,
|
connectionA: ConnectionPoint,
|
||||||
@@ -120,7 +124,9 @@ export interface ExcalidrawAutomate extends Window {
|
|||||||
targetView: ExcalidrawView;
|
targetView: ExcalidrawView;
|
||||||
setView (view:ExcalidrawView|"first"|"active"):ExcalidrawView;
|
setView (view:ExcalidrawView|"first"|"active"):ExcalidrawView;
|
||||||
getExcalidrawAPI ():any;
|
getExcalidrawAPI ():any;
|
||||||
getViewSelectedElement( ):ExcalidrawElement;
|
getViewElements ():ExcalidrawElement[];
|
||||||
|
deleteViewElements (el: ExcalidrawElement[]):boolean;
|
||||||
|
getViewSelectedElement ():ExcalidrawElement;
|
||||||
getViewSelectedElements ():ExcalidrawElement[];
|
getViewSelectedElements ():ExcalidrawElement[];
|
||||||
viewToggleFullScreen (forceViewMode?:boolean):void;
|
viewToggleFullScreen (forceViewMode?:boolean):void;
|
||||||
connectObjectWithViewSelectedElement (
|
connectObjectWithViewSelectedElement (
|
||||||
@@ -135,6 +141,19 @@ export interface ExcalidrawAutomate extends Window {
|
|||||||
}
|
}
|
||||||
):boolean;
|
):boolean;
|
||||||
addElementsToView (repositionToCursor:boolean, save:boolean):Promise<boolean>;
|
addElementsToView (repositionToCursor:boolean, save:boolean):Promise<boolean>;
|
||||||
|
onDropHook (data: {
|
||||||
|
ea: ExcalidrawAutomate,
|
||||||
|
event: React.DragEvent<HTMLDivElement>,
|
||||||
|
draggable: any, //Obsidian draggable object
|
||||||
|
type: "file"|"text"|"unknown",
|
||||||
|
payload: {
|
||||||
|
files: TFile[], //TFile[] array of dropped files
|
||||||
|
text: string, //string
|
||||||
|
},
|
||||||
|
excalidrawFile: TFile, //the file receiving the drop event
|
||||||
|
view: ExcalidrawView, //the excalidraw view receiving the drop
|
||||||
|
pointerPosition: {x:number, y:number} //the pointer position on canvas at the time of drop
|
||||||
|
}):boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,6 +163,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
|||||||
window.ExcalidrawAutomate = {
|
window.ExcalidrawAutomate = {
|
||||||
plugin: plugin,
|
plugin: plugin,
|
||||||
elementsDict: {},
|
elementsDict: {},
|
||||||
|
imagesDict: {},
|
||||||
style: {
|
style: {
|
||||||
strokeColor: "#000000",
|
strokeColor: "#000000",
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
@@ -263,7 +283,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
):Promise<string> {
|
):Promise<string> {
|
||||||
const template = params?.templatePath ? (await getTemplate(params.templatePath)) : null;
|
const template = params?.templatePath ? (await getTemplate(params.templatePath,true)) : null;
|
||||||
let elements = template ? template.elements : [];
|
let elements = template ? template.elements : [];
|
||||||
elements = elements.concat(this.getElements());
|
elements = elements.concat(this.getElements());
|
||||||
let frontmatter:string;
|
let frontmatter:string;
|
||||||
@@ -281,73 +301,83 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
|||||||
} else {
|
} else {
|
||||||
frontmatter = template?.frontmatter ? template.frontmatter : FRONTMATTER;
|
frontmatter = template?.frontmatter ? template.frontmatter : FRONTMATTER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scene = {
|
||||||
|
type: "excalidraw",
|
||||||
|
version: 2,
|
||||||
|
source: "https://excalidraw.com",
|
||||||
|
elements: elements,
|
||||||
|
appState: {
|
||||||
|
theme: template?.appState?.theme ?? this.canvas.theme,
|
||||||
|
viewBackgroundColor: template?.appState?.viewBackgroundColor ?? this.canvas.viewBackgroundColor,
|
||||||
|
currentItemStrokeColor: template?.appState?.currentItemStrokeColor ?? this.style.strokeColor,
|
||||||
|
currentItemBackgroundColor: template?.appState?.currentItemBackgroundColor ?? this.style.backgroundColor,
|
||||||
|
currentItemFillStyle: template?.appState?.currentItemFillStyle ?? this.style.fillStyle,
|
||||||
|
currentItemStrokeWidth: template?.appState?.currentItemStrokeWidth ?? this.style.strokeWidth,
|
||||||
|
currentItemStrokeStyle: template?.appState?.currentItemStrokeStyle ?? this.style.strokeStyle,
|
||||||
|
currentItemRoughness: template?.appState?.currentItemRoughness ?? this.style.roughness,
|
||||||
|
currentItemOpacity: template?.appState?.currentItemOpacity ?? this.style.opacity,
|
||||||
|
currentItemFontFamily: template?.appState?.currentItemFontFamily ?? this.style.fontFamily,
|
||||||
|
currentItemFontSize: template?.appState?.currentItemFontSize ?? this.style.fontSize,
|
||||||
|
currentItemTextAlign: template?.appState?.currentItemTextAlign ?? this.style.textAlign,
|
||||||
|
currentItemStrokeSharpness: template?.appState?.currentItemStrokeSharpness ?? this.style.strokeSharpness,
|
||||||
|
currentItemStartArrowhead: template?.appState?.currentItemStartArrowhead ?? this.style.startArrowHead,
|
||||||
|
currentItemEndArrowhead: template?.appState?.currentItemEndArrowhead ?? this.style.endArrowHead,
|
||||||
|
currentItemLinearStrokeSharpness: template?.appState?.currentItemLinearStrokeSharpness ?? this.style.strokeSharpness,
|
||||||
|
gridSize: template?.appState?.gridSize ?? this.canvas.gridSize,
|
||||||
|
},
|
||||||
|
files: template?.files ?? {},
|
||||||
|
};
|
||||||
|
|
||||||
return plugin.createDrawing(
|
return plugin.createDrawing(
|
||||||
params?.filename ? params.filename + '.excalidraw.md' : this.plugin.getNextDefaultFilename(),
|
params?.filename ? params.filename + '.excalidraw.md' : this.plugin.getNextDefaultFilename(),
|
||||||
params?.onNewPane ? params.onNewPane : false,
|
params?.onNewPane ? params.onNewPane : false,
|
||||||
params?.foldername ? params.foldername : this.plugin.settings.folder,
|
params?.foldername ? params.foldername : this.plugin.settings.folder,
|
||||||
frontmatter + plugin.exportSceneToMD(
|
this.plugin.settings.compatibilityMode
|
||||||
JSON.stringify({
|
? JSON.stringify(scene,null,"\t")
|
||||||
|
: frontmatter + await plugin.exportSceneToMD(JSON.stringify(scene,null,"\t"))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
async createSVG(templatePath?:string,embedFont:boolean = false):Promise<SVGSVGElement> {
|
||||||
|
const automateElements = this.getElements();
|
||||||
|
const template = templatePath ? (await getTemplate(templatePath,true)) : null;
|
||||||
|
let elements = template ? template.elements : [];
|
||||||
|
elements = elements.concat(automateElements);
|
||||||
|
const svg = await getSVG(
|
||||||
|
{//createDrawing
|
||||||
type: "excalidraw",
|
type: "excalidraw",
|
||||||
version: 2,
|
version: 2,
|
||||||
source: "https://excalidraw.com",
|
source: "https://excalidraw.com",
|
||||||
elements: elements,
|
elements: elements,
|
||||||
appState: {
|
appState: {
|
||||||
theme: template ? template.appState.theme : this.canvas.theme,
|
theme: template?.appState?.theme ?? this.canvas.theme,
|
||||||
viewBackgroundColor: template? template.appState.viewBackgroundColor : this.canvas.viewBackgroundColor,
|
viewBackgroundColor: template?.appState?.viewBackgroundColor ?? this.canvas.viewBackgroundColor,
|
||||||
currentItemStrokeColor: template? template.appState.currentItemStrokeColor : this.style.strokeColor,
|
},
|
||||||
currentItemBackgroundColor: template? template.appState.currentItemBackgroundColor : this.style.backgroundColor,
|
files: template?.files ?? {}
|
||||||
currentItemFillStyle: template? template.appState.currentItemFillStyle : this.style.fillStyle,
|
},
|
||||||
currentItemStrokeWidth: template? template.appState.currentItemStrokeWidth : this.style.strokeWidth,
|
|
||||||
currentItemStrokeStyle: template? template.appState.currentItemStrokeStyle : this.style.strokeStyle,
|
|
||||||
currentItemRoughness: template? template.appState.currentItemRoughness : this.style.roughness,
|
|
||||||
currentItemOpacity: template? template.appState.currentItemOpacity : this.style.opacity,
|
|
||||||
currentItemFontFamily: template? template.appState.currentItemFontFamily : this.style.fontFamily,
|
|
||||||
currentItemFontSize: template? template.appState.currentItemFontSize : this.style.fontSize,
|
|
||||||
currentItemTextAlign: template? template.appState.currentItemTextAlign : this.style.textAlign,
|
|
||||||
currentItemStrokeSharpness: template? template.appState.currentItemStrokeSharpness : this.style.strokeSharpness,
|
|
||||||
currentItemStartArrowhead: template? template.appState.currentItemStartArrowhead: this.style.startArrowHead,
|
|
||||||
currentItemEndArrowhead: template? template.appState.currentItemEndArrowhead : this.style.endArrowHead,
|
|
||||||
currentItemLinearStrokeSharpness: template? template.appState.currentItemLinearStrokeSharpness : this.style.strokeSharpness,
|
|
||||||
gridSize: template ? template.appState.gridSize : this.canvas.gridSize
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
},
|
|
||||||
async createSVG(templatePath?:string):Promise<SVGSVGElement> {
|
|
||||||
const template = templatePath ? (await getTemplate(templatePath)) : null;
|
|
||||||
let elements = template ? template.elements : [];
|
|
||||||
elements = elements.concat(this.getElements());
|
|
||||||
return await ExcalidrawView.getSVG(
|
|
||||||
{//createDrawing
|
|
||||||
"type": "excalidraw",
|
|
||||||
"version": 2,
|
|
||||||
"source": "https://excalidraw.com",
|
|
||||||
"elements": elements,
|
|
||||||
"appState": {
|
|
||||||
"theme": template ? template.appState.theme : this.canvas.theme,
|
|
||||||
"viewBackgroundColor": template? template.appState.viewBackgroundColor : this.canvas.viewBackgroundColor
|
|
||||||
}
|
|
||||||
},//),
|
|
||||||
{
|
{
|
||||||
withBackground: plugin.settings.exportWithBackground,
|
withBackground: plugin.settings.exportWithBackground,
|
||||||
withTheme: plugin.settings.exportWithTheme
|
withTheme: plugin.settings.exportWithTheme
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
return embedFont ? embedFontsInSVG(svg) : svg;
|
||||||
},
|
},
|
||||||
async createPNG(templatePath?:string, scale:number=1) {
|
async createPNG(templatePath?:string, scale:number=1) {
|
||||||
const template = templatePath ? (await getTemplate(templatePath)) : null;
|
const automateElements = this.getElements();
|
||||||
|
const template = templatePath ? (await getTemplate(templatePath,true)) : null;
|
||||||
let elements = template ? template.elements : [];
|
let elements = template ? template.elements : [];
|
||||||
elements = elements.concat(this.getElements());
|
elements = elements.concat(automateElements);
|
||||||
return ExcalidrawView.getPNG(
|
return getPNG(
|
||||||
{
|
{
|
||||||
"type": "excalidraw",
|
type: "excalidraw",
|
||||||
"version": 2,
|
version: 2,
|
||||||
"source": "https://excalidraw.com",
|
source: "https://excalidraw.com",
|
||||||
"elements": elements,
|
elements: elements,
|
||||||
"appState": {
|
appState: {
|
||||||
"theme": template ? template.appState.theme : this.canvas.theme,
|
theme: template?.appState?.theme ?? this.canvas.theme,
|
||||||
"viewBackgroundColor": template? template.appState.viewBackgroundColor : this.canvas.viewBackgroundColor
|
viewBackgroundColor: template?.appState?.viewBackgroundColor ?? this.canvas.viewBackgroundColor,
|
||||||
}
|
},
|
||||||
|
files: template?.files ?? {}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
withBackground: plugin.settings.exportWithBackground,
|
withBackground: plugin.settings.exportWithBackground,
|
||||||
@@ -497,6 +527,27 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
|||||||
}
|
}
|
||||||
return id;
|
return id;
|
||||||
},
|
},
|
||||||
|
async addImage(topX:number, topY:number, imageFile: TFile):Promise<string> {
|
||||||
|
const id = nanoid();
|
||||||
|
const image = await getObsidianImage(this.plugin.app,imageFile);
|
||||||
|
if(!image) return null;
|
||||||
|
this.imagesDict[image.fileId] = {
|
||||||
|
mimeType: image.mimeType,
|
||||||
|
id: image.fileId,
|
||||||
|
dataURL: image.dataURL,
|
||||||
|
created: image.created,
|
||||||
|
file: imageFile.path
|
||||||
|
}
|
||||||
|
if (Math.max(image.size.width,image.size.height) > MAX_IMAGE_SIZE) {
|
||||||
|
const scale = MAX_IMAGE_SIZE/Math.max(image.size.width,image.size.height);
|
||||||
|
image.size.width = scale*image.size.width;
|
||||||
|
image.size.height = scale*image.size.height;
|
||||||
|
}
|
||||||
|
this.elementsDict[id] = boxedElement(id,"image",topX,topY,image.size.width,image.size.height);
|
||||||
|
this.elementsDict[id].fileId = image.fileId;
|
||||||
|
this.elementsDict[id].scale = [1,1];
|
||||||
|
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])) {
|
if(!(this.elementsDict[objectA] && this.elementsDict[objectB])) {
|
||||||
return;
|
return;
|
||||||
@@ -536,6 +587,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
|||||||
},
|
},
|
||||||
clear() {
|
clear() {
|
||||||
this.elementsDict = {};
|
this.elementsDict = {};
|
||||||
|
this.imagesDict = {};
|
||||||
},
|
},
|
||||||
reset() {
|
reset() {
|
||||||
this.clear();
|
this.clear();
|
||||||
@@ -581,7 +633,33 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
|||||||
errorMessage("targetView not set", "getExcalidrawAPI()");
|
errorMessage("targetView not set", "getExcalidrawAPI()");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (this.targetView as ExcalidrawView).excalidrawRef.current;
|
return (this.targetView as ExcalidrawView).excalidrawAPI;
|
||||||
|
},
|
||||||
|
getViewElements ():ExcalidrawElement[] {
|
||||||
|
if (!this.targetView || !this.targetView?._loaded) {
|
||||||
|
errorMessage("targetView not set", "getViewSelectedElements()");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const current = this.targetView?.excalidrawRef?.current;
|
||||||
|
if(!current) return [];
|
||||||
|
return current?.getSceneElements();
|
||||||
|
},
|
||||||
|
deleteViewElements (elToDelete: ExcalidrawElement[]):boolean {
|
||||||
|
if (!this.targetView || !this.targetView?._loaded) {
|
||||||
|
errorMessage("targetView not set", "getViewSelectedElements()");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const current = this.targetView?.excalidrawRef?.current;
|
||||||
|
if(!current) return false;
|
||||||
|
const el: ExcalidrawElement[] = current.getSceneElements();
|
||||||
|
const st: AppState = current.getAppState();
|
||||||
|
current.updateScene({
|
||||||
|
elements: el.filter((e:ExcalidrawElement)=>!elToDelete.includes(e)),
|
||||||
|
appState: st,
|
||||||
|
commitToHistory: true,
|
||||||
|
});
|
||||||
|
this.targetView.save();
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
getViewSelectedElement():any {
|
getViewSelectedElement():any {
|
||||||
const elements = this.getViewSelectedElements();
|
const elements = this.getViewSelectedElements();
|
||||||
@@ -640,9 +718,9 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const elements = this.getElements();
|
const elements = this.getElements();
|
||||||
return await this.targetView.addElements(elements,repositionToCursor,save);
|
return await this.targetView.addElements(elements,repositionToCursor,save,this.imagesDict);
|
||||||
},
|
},
|
||||||
|
onDropHook:null,
|
||||||
};
|
};
|
||||||
await initFonts();
|
await initFonts();
|
||||||
}
|
}
|
||||||
@@ -738,27 +816,61 @@ export function measureText (newText:string, fontSize:number, fontFamily:number)
|
|||||||
return {w: width, h: height, baseline: baseline };
|
return {w: width, h: height, baseline: baseline };
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getTemplate(fileWithPath: string):Promise<{elements: any,appState: any, frontmatter: string}> {
|
async function getTemplate(fileWithPath:string, loadFiles:boolean = false):Promise<{
|
||||||
|
elements: any,
|
||||||
|
appState: any,
|
||||||
|
frontmatter: string,
|
||||||
|
files: any,
|
||||||
|
svgSnapshot: string
|
||||||
|
}> {
|
||||||
const app = window.ExcalidrawAutomate.plugin.app;
|
const app = window.ExcalidrawAutomate.plugin.app;
|
||||||
const vault = app.vault;
|
const vault = app.vault;
|
||||||
const file = app.metadataCache.getFirstLinkpathDest(normalizePath(fileWithPath),'');
|
const file = app.metadataCache.getFirstLinkpathDest(normalizePath(fileWithPath),'');
|
||||||
if(file && file instanceof TFile) {
|
if(file && file instanceof TFile) {
|
||||||
const data = await vault.read(file);
|
const data = (await vault.read(file)).replaceAll("\r\n","\n").replaceAll("\r","\n");
|
||||||
|
let excalidrawData:ExcalidrawData = new ExcalidrawData(window.ExcalidrawAutomate.plugin);
|
||||||
|
|
||||||
|
if(file.extension === "excalidraw") {
|
||||||
|
await excalidrawData.loadLegacyData(data,file);
|
||||||
|
return {
|
||||||
|
elements: excalidrawData.scene.elements,
|
||||||
|
appState: excalidrawData.scene.appState,
|
||||||
|
frontmatter: "",
|
||||||
|
files: excalidrawData.scene.files,
|
||||||
|
svgSnapshot: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = data.search("excalidraw-plugin: parsed\n")>-1 || data.search("excalidraw-plugin: locked\n")>-1; //locked for backward compatibility
|
||||||
|
await excalidrawData.loadData(data,file,parsed ? TextMode.parsed : TextMode.raw)
|
||||||
|
|
||||||
let trimLocation = data.search("# Text Elements\n");
|
let trimLocation = data.search("# Text Elements\n");
|
||||||
if(trimLocation == -1) trimLocation = data.search("# Drawing\n");
|
if(trimLocation == -1) trimLocation = data.search("# Drawing\n");
|
||||||
|
|
||||||
const excalidrawData = JSON_parse(getJSON(data)[0]);
|
if(loadFiles) {
|
||||||
|
await loadSceneFiles(app,excalidrawData.files,(fileArray:any)=>{
|
||||||
|
for(const f of fileArray) {
|
||||||
|
excalidrawData.scene.files[f.id] = f;
|
||||||
|
}
|
||||||
|
let foo;
|
||||||
|
[foo,excalidrawData] = scaleLoadedImage(excalidrawData,fileArray);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements: excalidrawData.elements,
|
elements: excalidrawData.scene.elements,
|
||||||
appState: excalidrawData.appState,
|
appState: excalidrawData.scene.appState,
|
||||||
frontmatter: data.substring(0,trimLocation)
|
frontmatter: data.substring(0,trimLocation),
|
||||||
|
files: excalidrawData.scene.files,
|
||||||
|
svgSnapshot: excalidrawData.svgSnapshot
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
elements: [],
|
elements: [],
|
||||||
appState: {},
|
appState: {},
|
||||||
frontmatter: null
|
frontmatter: null,
|
||||||
|
files: [],
|
||||||
|
svgSnapshot: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,11 @@ import {
|
|||||||
JSON_parse
|
JSON_parse
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import { TextMode } from "./ExcalidrawView";
|
import { TextMode } from "./ExcalidrawView";
|
||||||
import { wrapText } from "./Utils";
|
import { getAttachmentsFolderAndFilePath, getBinaryFileFromDataURL, wrapText } from "./Utils";
|
||||||
|
import { ExcalidrawImageElement, ExcalidrawTextElement, FileId } from "@zsviczian/excalidraw/types/element/types";
|
||||||
|
import { BinaryFiles, SceneData } from "@zsviczian/excalidraw/types/types";
|
||||||
|
|
||||||
|
type SceneDataWithFiles = SceneData & { files: BinaryFiles};
|
||||||
|
|
||||||
declare module "obsidian" {
|
declare module "obsidian" {
|
||||||
interface MetadataCache {
|
interface MetadataCache {
|
||||||
@@ -24,8 +27,11 @@ declare module "obsidian" {
|
|||||||
|
|
||||||
export const REGEX_LINK = {
|
export const REGEX_LINK = {
|
||||||
//![[link|alias]] [alias](link){num}
|
//![[link|alias]] [alias](link){num}
|
||||||
//12 3 4 5 6 7 8
|
// 1 2 3 4 5 6 7 8 9
|
||||||
EXPR: /(!)?(\[\[([^|\]]+)\|?(.+)?]]|\[(.*)\]\((.*)\))(\{(\d+)\})?/g,
|
EXPR: /(!)?(\[\[([^|\]]+)\|?([^\]]+)?]]|\[([^\]]*)]\(([^)]*)\))(\{(\d+)\})?/g, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187
|
||||||
|
getRes: (text:string):IterableIterator<RegExpMatchArray> => {
|
||||||
|
return text.matchAll(REGEX_LINK.EXPR);
|
||||||
|
},
|
||||||
isTransclusion: (parts: IteratorResult<RegExpMatchArray, any>):boolean => {
|
isTransclusion: (parts: IteratorResult<RegExpMatchArray, any>):boolean => {
|
||||||
return parts.value[1] ? true:false;
|
return parts.value[1] ? true:false;
|
||||||
},
|
},
|
||||||
@@ -48,7 +54,7 @@ export const REGEX_LINK = {
|
|||||||
|
|
||||||
export const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;
|
export const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;
|
||||||
|
|
||||||
const DRAWING_REG = /\n%%\n# Drawing\n(```json\n)(.*)\n```%%/gm;
|
const DRAWING_REG = /\n%%\n# Drawing\n[^`]*(```json\n)([\s\S]*?)```/gm; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/182
|
||||||
const DRAWING_REG_FALLBACK = /\n# Drawing\n(```json\n)?(.*)(```)?(%%)?/gm;
|
const DRAWING_REG_FALLBACK = /\n# Drawing\n(```json\n)?(.*)(```)?(%%)?/gm;
|
||||||
export function getJSON(data:string):[string,number] {
|
export function getJSON(data:string):[string,number] {
|
||||||
let res = data.matchAll(DRAWING_REG);
|
let res = data.matchAll(DRAWING_REG);
|
||||||
@@ -64,10 +70,24 @@ export function getJSON(data:string):[string,number] {
|
|||||||
const result = parts.value[2];
|
const result = parts.value[2];
|
||||||
return [result.substr(0,result.lastIndexOf("}")+1),parts.value.index]; //this is a workaround in case sync merges two files together and one version is still an old version without the ```codeblock
|
return [result.substr(0,result.lastIndexOf("}")+1),parts.value.index]; //this is a workaround in case sync merges two files together and one version is still an old version without the ```codeblock
|
||||||
}
|
}
|
||||||
return [data,parts.value.index];
|
return [data,parts.value ? parts.value.index : 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
//extracts SVG snapshot from Excalidraw Markdown string
|
||||||
|
const SVG_REG = /.*?```html\n([\s\S]*?)```/gm;
|
||||||
|
export function getSVGString(data:string):string {
|
||||||
|
let res = data.matchAll(SVG_REG);
|
||||||
|
|
||||||
|
let parts;
|
||||||
|
parts = res.next();
|
||||||
|
if(parts.value && parts.value.length>1) {
|
||||||
|
return parts.value[1];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExcalidrawData {
|
export class ExcalidrawData {
|
||||||
|
public svgSnapshot: string = null;
|
||||||
private textElements:Map<string,{raw:string, parsed:string}> = null;
|
private textElements:Map<string,{raw:string, parsed:string}> = null;
|
||||||
public scene:any = null;
|
public scene:any = null;
|
||||||
private file:TFile = null;
|
private file:TFile = null;
|
||||||
@@ -78,10 +98,13 @@ export class ExcalidrawData {
|
|||||||
private textMode: TextMode = TextMode.raw;
|
private textMode: TextMode = TextMode.raw;
|
||||||
private plugin: ExcalidrawPlugin;
|
private plugin: ExcalidrawPlugin;
|
||||||
public loaded: boolean = false;
|
public loaded: boolean = false;
|
||||||
|
public files:Map<FileId,string> = null; //fileId, path
|
||||||
|
private compatibilityMode:boolean = false;
|
||||||
|
|
||||||
constructor(plugin: ExcalidrawPlugin) {
|
constructor(plugin: ExcalidrawPlugin) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
this.app = plugin.app;
|
this.app = plugin.app;
|
||||||
|
this.files = new Map<FileId,string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,6 +116,8 @@ export class ExcalidrawData {
|
|||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.textElements = new Map<string,{raw:string, parsed:string}>();
|
this.textElements = new Map<string,{raw:string, parsed:string}>();
|
||||||
|
this.files.clear();
|
||||||
|
this.compatibilityMode = false;
|
||||||
|
|
||||||
//I am storing these because if the settings change while a drawing is open parsing will run into errors during save
|
//I am storing these because if the settings change while a drawing is open parsing will run into errors during save
|
||||||
//The drawing will use these values until next drawing is loaded or this drawing is re-loaded
|
//The drawing will use these values until next drawing is loaded or this drawing is re-loaded
|
||||||
@@ -123,6 +148,13 @@ export class ExcalidrawData {
|
|||||||
if (!this.scene) {
|
if (!this.scene) {
|
||||||
this.scene = JSON_parse(scene); //this is a workaround to address when files are mereged by sync and one version is still an old markdown without the codeblock ```
|
this.scene = JSON_parse(scene); //this is a workaround to address when files are mereged by sync and one version is still an old markdown without the codeblock ```
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!this.scene.files) {
|
||||||
|
this.scene.files = {}; //loading legacy scenes that do not yet have the files attribute.
|
||||||
|
}
|
||||||
|
|
||||||
|
this.svgSnapshot = getSVGString(data.substr(pos+scene.length));
|
||||||
|
|
||||||
data = data.substring(0,pos);
|
data = data.substring(0,pos);
|
||||||
|
|
||||||
//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 Markdown # Text Elements take priority over the JSON text elements. Assuming the scenario in which the link was updated due to filename changes
|
||||||
@@ -136,17 +168,34 @@ export class ExcalidrawData {
|
|||||||
}
|
}
|
||||||
position += data.match(/((^%%\n)?# Text Elements\n)/m)[0].length
|
position += data.match(/((^%%\n)?# Text Elements\n)/m)[0].length
|
||||||
|
|
||||||
|
data = data.substring(position);
|
||||||
|
position = 0;
|
||||||
|
|
||||||
//iterating through all the text elements in .md
|
//iterating through all the text elements in .md
|
||||||
//Text elements always contain the raw value
|
//Text elements always contain the raw value
|
||||||
const BLOCKREF_LEN:number = " ^12345678\n\n".length;
|
const BLOCKREF_LEN:number = " ^12345678\n\n".length;
|
||||||
const res = data.matchAll(/\s\^(.{8})[\n]+/g);
|
let res = data.matchAll(/\s\^(.{8})[\n]+/g);
|
||||||
let parts;
|
let parts;
|
||||||
while(!(parts = res.next()).done) {
|
while(!(parts = res.next()).done) {
|
||||||
const text = data.substring(position,parts.value.index);
|
const text = data.substring(position,parts.value.index);
|
||||||
this.textElements.set(parts.value[1],{raw: text, parsed: await this.parse(text)});
|
const id:string = parts.value[1];
|
||||||
|
this.textElements.set(id,{raw: text, parsed: await this.parse(text)});
|
||||||
|
//this will set the rawText field of text elements imported from files before 1.3.14, and from other instances of Excalidraw
|
||||||
|
const textEl = this.scene.elements.filter((el:any)=>el.id===id)[0];
|
||||||
|
if(textEl && (!textEl.rawText || textEl.rawText === "")) textEl.rawText = text;
|
||||||
|
|
||||||
position = parts.value.index + BLOCKREF_LEN;
|
position = parts.value.index + BLOCKREF_LEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Load Embedded files
|
||||||
|
const REG_FILEID_FILEPATH = /([\w\d]*):\s*\[\[([^\]]*)]]\n/gm;
|
||||||
|
data = data.substring(data.indexOf("# Embedded files\n")+"# Embedded files\n".length);
|
||||||
|
res = data.matchAll(REG_FILEID_FILEPATH);
|
||||||
|
while(!(parts = res.next()).done) {
|
||||||
|
this.files.set(parts.value[1] as FileId,parts.value[2]);
|
||||||
|
}
|
||||||
|
|
||||||
//Check to see if there are text elements in the JSON that were missed from the # Text Elements section
|
//Check to see if there are text elements in the JSON that were missed from the # Text Elements section
|
||||||
//e.g. if the entire text elements section was deleted.
|
//e.g. if the entire text elements section was deleted.
|
||||||
this.findNewTextElementsInScene();
|
this.findNewTextElementsInScene();
|
||||||
@@ -156,12 +205,17 @@ export class ExcalidrawData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async loadLegacyData(data: string,file: TFile):Promise<boolean> {
|
public async loadLegacyData(data: string,file: TFile):Promise<boolean> {
|
||||||
|
this.compatibilityMode = true;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
this.textElements = new Map<string,{raw:string, parsed:string}>();
|
this.textElements = new Map<string,{raw:string, parsed:string}>();
|
||||||
this.setShowLinkBrackets();
|
this.setShowLinkBrackets();
|
||||||
this.setLinkPrefix();
|
this.setLinkPrefix();
|
||||||
this.setUrlPrefix();
|
this.setUrlPrefix();
|
||||||
this.scene = JSON.parse(data);
|
this.scene = JSON.parse(data);
|
||||||
|
if(!this.scene.files) {
|
||||||
|
this.scene.files = {}; //loading legacy scenes without the files element
|
||||||
|
}
|
||||||
|
this.files.clear();
|
||||||
this.findNewTextElementsInScene();
|
this.findNewTextElementsInScene();
|
||||||
await this.setTextMode(TextMode.raw,true); //legacy files are always displayed in raw mode.
|
await this.setTextMode(TextMode.raw,true); //legacy files are always displayed in raw mode.
|
||||||
return true;
|
return true;
|
||||||
@@ -241,8 +295,9 @@ export class ExcalidrawData {
|
|||||||
dirty = true;
|
dirty = true;
|
||||||
} else if(!this.textElements.has(id)) {
|
} else if(!this.textElements.has(id)) {
|
||||||
dirty = true;
|
dirty = true;
|
||||||
this.textElements.set(id,{raw: te.text, parsed: null});
|
const raw = (te.rawText && te.rawText!==""?te.rawText:te.text); //this is for compatibility with drawings created before the rawText change on ExcalidrawTextElement
|
||||||
this.parseasync(id,te.text);
|
this.textElements.set(id,{raw: raw, parsed: null});
|
||||||
|
this.parseasync(id,raw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(dirty) { //reload scene json in case it has changed
|
if(dirty) { //reload scene json in case it has changed
|
||||||
@@ -263,13 +318,9 @@ export class ExcalidrawData {
|
|||||||
if(el.length==0) {
|
if(el.length==0) {
|
||||||
this.textElements.delete(key); //if no longer in the scene, delete the text element
|
this.textElements.delete(key); //if no longer in the scene, delete the text element
|
||||||
} else {
|
} else {
|
||||||
if(!this.textElements.has(key)) {
|
const text = await this.getText(key);
|
||||||
|
if(text != el[0].text) {
|
||||||
this.textElements.set(key,{raw: el[0].text,parsed: await this.parse(el[0].text)});
|
this.textElements.set(key,{raw: el[0].text,parsed: await this.parse(el[0].text)});
|
||||||
} else {
|
|
||||||
const text = await this.getText(key);
|
|
||||||
if(text != el[0].text) {
|
|
||||||
this.textElements.set(key,{raw: el[0].text,parsed: await this.parse(el[0].text)});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,14 +337,24 @@ export class ExcalidrawData {
|
|||||||
(this.showLinkBrackets ? "]]" : "");
|
(this.showLinkBrackets ? "]]" : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param text
|
||||||
|
* @returns [string,number] - the transcluded text, and the line number for the location of the text
|
||||||
|
*/
|
||||||
public async getTransclusion (text:string):Promise<[string,number]> {
|
public async getTransclusion (text:string):Promise<[string,number]> {
|
||||||
//file-name#^blockref
|
//file-name#^blockref
|
||||||
//1 2 3
|
//1 2 3
|
||||||
const REG_FILE_BLOCKREF = /(.*)#(\^)?(.*)/g;
|
const REG_FILE_BLOCKREF = /(.*)#(\^)?(.*)/g;
|
||||||
const parts=text.matchAll(REG_FILE_BLOCKREF).next();
|
const parts=text.matchAll(REG_FILE_BLOCKREF).next();
|
||||||
if(parts.done || !parts.value[1] || !parts.value[3]) return [text,0]; //filename and/or blockref not found
|
if(!parts.done && !parts.value[1]) return [text,0]; //filename not found
|
||||||
const file = this.app.metadataCache.getFirstLinkpathDest(parts.value[1],this.file.path);
|
const filename = parts.done ? text : parts.value[1];
|
||||||
|
const file = this.app.metadataCache.getFirstLinkpathDest(filename,this.file.path);
|
||||||
|
if(!file || !(file instanceof TFile)) return [text,0];
|
||||||
const contents = await this.app.vault.cachedRead(file);
|
const contents = await this.app.vault.cachedRead(file);
|
||||||
|
if(parts.done) { //no blockreference
|
||||||
|
return([contents.substr(0,this.plugin.settings.pageTransclusionCharLimit),0]);
|
||||||
|
}
|
||||||
const isParagraphRef = parts.value[2] ? true : false; //does the reference contain a ^ character?
|
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 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");
|
const blocks = (await this.app.metadataCache.blockCache.getForFile({isCancelled: ()=>false},file)).blocks.filter((block:any)=>block.node.type!="comment");
|
||||||
@@ -335,7 +396,7 @@ export class ExcalidrawData {
|
|||||||
private async parse(text:string):Promise<string>{
|
private async parse(text:string):Promise<string>{
|
||||||
let outString = "";
|
let outString = "";
|
||||||
let position = 0;
|
let position = 0;
|
||||||
const res = text.matchAll(REGEX_LINK.EXPR);
|
const res = REGEX_LINK.getRes(text);
|
||||||
let linkIcon = false;
|
let linkIcon = false;
|
||||||
let urlIcon = false;
|
let urlIcon = false;
|
||||||
let parts;
|
let parts;
|
||||||
@@ -375,7 +436,7 @@ export class ExcalidrawData {
|
|||||||
*/
|
*/
|
||||||
private quickParse(text:string):string {
|
private quickParse(text:string):string {
|
||||||
const hasTransclusion = (text:string):boolean => {
|
const hasTransclusion = (text:string):boolean => {
|
||||||
const res = text.matchAll(REGEX_LINK.EXPR);
|
const res = REGEX_LINK.getRes(text);
|
||||||
let parts;
|
let parts;
|
||||||
while(!(parts=res.next()).done) {
|
while(!(parts=res.next()).done) {
|
||||||
if (REGEX_LINK.isTransclusion(parts)) return true;
|
if (REGEX_LINK.isTransclusion(parts)) return true;
|
||||||
@@ -386,7 +447,7 @@ export class ExcalidrawData {
|
|||||||
|
|
||||||
let outString = "";
|
let outString = "";
|
||||||
let position = 0;
|
let position = 0;
|
||||||
const res = text.matchAll(REGEX_LINK.EXPR);
|
const res = REGEX_LINK.getRes(text);
|
||||||
let linkIcon = false;
|
let linkIcon = false;
|
||||||
let urlIcon = false;
|
let urlIcon = false;
|
||||||
let parts;
|
let parts;
|
||||||
@@ -420,23 +481,68 @@ export class ExcalidrawData {
|
|||||||
for(const key of this.textElements.keys()){
|
for(const key of this.textElements.keys()){
|
||||||
outString += this.textElements.get(key).raw+' ^'+key+'\n\n';
|
outString += this.textElements.get(key).raw+' ^'+key+'\n\n';
|
||||||
}
|
}
|
||||||
return outString + this.plugin.getMarkdownDrawingSection(JSON.stringify(this.scene));
|
if(this.files.size>0) {
|
||||||
|
outString += '\n# Embedded files\n';
|
||||||
|
for(const key of this.files.keys()) {
|
||||||
|
outString += key +': [['+this.files.get(key) + ']]\n';
|
||||||
|
}
|
||||||
|
outString += '\n';
|
||||||
|
}
|
||||||
|
return outString + this.plugin.getMarkdownDrawingSection(JSON.stringify(this.scene,null,"\t"),this.svgSnapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async syncFiles(scene:SceneDataWithFiles):Promise<boolean> {
|
||||||
|
let dirty = false;
|
||||||
|
|
||||||
|
//remove files that no longer have a corresponding image element
|
||||||
|
const fileIds = (scene.elements.filter((e)=>e.type==="image") as ExcalidrawImageElement[]).map((e)=>e.fileId);
|
||||||
|
this.files.forEach((value,key)=>{
|
||||||
|
if(!fileIds.contains(key)) {
|
||||||
|
this.files.delete(key);
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//check if there are any images that need to be processed in the new scene
|
||||||
|
if(!scene.files || scene.files == {}) return false;
|
||||||
|
|
||||||
|
for(const key of Object.keys(scene.files)) {
|
||||||
|
if(!this.files.has(key as FileId)) {
|
||||||
|
dirty = true;
|
||||||
|
let fname = "Pasted Image "+window.moment().format("YYYYMMDDHHmmss_SSS");
|
||||||
|
switch(scene.files[key].mimeType) {
|
||||||
|
case "image/png": fname += ".png"; break;
|
||||||
|
case "image/jpeg": fname += ".jpg"; break;
|
||||||
|
case "image/svg+xml": fname += ".svg"; break;
|
||||||
|
case "image/gif": fname += ".gif"; break;
|
||||||
|
default: fname += ".png";
|
||||||
|
}
|
||||||
|
const [folder,filepath] = await getAttachmentsFolderAndFilePath(this.app,this.file.path,fname);
|
||||||
|
await this.app.vault.createBinary(filepath,getBinaryFileFromDataURL(scene.files[key].dataURL));
|
||||||
|
this.files.set(key as FileId,filepath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dirty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async syncElements(newScene:any):Promise<boolean> {
|
public async syncElements(newScene:any):Promise<boolean> {
|
||||||
//console.log("Excalidraw.Data.syncElements()");
|
this.scene = newScene;
|
||||||
this.scene = newScene;//JSON_parse(newScene);
|
let result = false;
|
||||||
const result = this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets() || this.findNewTextElementsInScene();
|
if(!this.compatibilityMode) {
|
||||||
|
result = await this.syncFiles(newScene);
|
||||||
|
this.scene.files = {};
|
||||||
|
}
|
||||||
|
result = result || this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets();
|
||||||
await this.updateTextElementsFromScene();
|
await this.updateTextElementsFromScene();
|
||||||
return result;
|
return result || this.findNewTextElementsInScene();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateScene(newScene:any){
|
public async updateScene(newScene:any){
|
||||||
//console.log("Excalidraw.Data.updateScene()");
|
//console.log("Excalidraw.Data.updateScene()");
|
||||||
this.scene = JSON_parse(newScene);
|
this.scene = JSON_parse(newScene);
|
||||||
const result = this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets() || this.findNewTextElementsInScene();
|
const result = this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets();
|
||||||
await this.updateTextElementsFromScene();
|
await this.updateTextElementsFromScene();
|
||||||
if(result) {
|
if(result || this.findNewTextElementsInScene()) {
|
||||||
await this.updateSceneTextElements();
|
await this.updateSceneTextElements();
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@@ -509,5 +615,3 @@ export class ExcalidrawData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import {
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
import * as ReactDOM from "react-dom";
|
||||||
import Excalidraw, {exportToSvg, getSceneVersion} from "@zsviczian/excalidraw";
|
import Excalidraw, {exportToSvg, getSceneVersion} from "@zsviczian/excalidraw";
|
||||||
import { ExcalidrawElement,ExcalidrawTextElement } from "@zsviczian/excalidraw/types/element/types";
|
import { ExcalidrawElement,ExcalidrawImageElement,ExcalidrawTextElement, FileId } from "@zsviczian/excalidraw/types/element/types";
|
||||||
import {
|
import {
|
||||||
AppState,
|
AppState,
|
||||||
|
BinaryFileData,
|
||||||
LibraryItems
|
LibraryItems
|
||||||
} from "@zsviczian/excalidraw/types/types";
|
} from "@zsviczian/excalidraw/types/types";
|
||||||
import {
|
import {
|
||||||
@@ -28,14 +29,17 @@ import {
|
|||||||
TEXT_DISPLAY_RAW_ICON_NAME,
|
TEXT_DISPLAY_RAW_ICON_NAME,
|
||||||
TEXT_DISPLAY_PARSED_ICON_NAME,
|
TEXT_DISPLAY_PARSED_ICON_NAME,
|
||||||
FULLSCREEN_ICON_NAME,
|
FULLSCREEN_ICON_NAME,
|
||||||
JSON_parse
|
JSON_parse,
|
||||||
|
IMAGE_TYPES
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import ExcalidrawPlugin from './main';
|
import ExcalidrawPlugin from './main';
|
||||||
import {estimateBounds, ExcalidrawAutomate, repositionElementsToCursor} from './ExcalidrawAutomate';
|
import {ExcalidrawAutomate, repositionElementsToCursor} from './ExcalidrawAutomate';
|
||||||
import { t } from "./lang/helpers";
|
import { t } from "./lang/helpers";
|
||||||
import { ExcalidrawData, REG_LINKINDEX_HYPERLINK, REGEX_LINK } from "./ExcalidrawData";
|
import { ExcalidrawData, REG_LINKINDEX_HYPERLINK, REGEX_LINK } from "./ExcalidrawData";
|
||||||
import { checkAndCreateFolder, download, getNewUniqueFilepath, splitFolderAndFilename, viewportCoordsToSceneCoords } from "./Utils";
|
import { checkAndCreateFolder, download, embedFontsInSVG, generateSVGString, getNewOrAdjacentLeaf, getNewUniqueFilepath, getPNG, getSVG, loadSceneFiles, rotatedDimensions, scaleLoadedImage, splitFolderAndFilename, svgToBase64, viewportCoordsToSceneCoords } from "./Utils";
|
||||||
import { Prompt } from "./Prompt";
|
import { Prompt } from "./Prompt";
|
||||||
|
import { ClipboardData } from "@zsviczian/excalidraw/types/clipboard";
|
||||||
|
import { ifStatement } from "@babel/types";
|
||||||
|
|
||||||
declare let window: ExcalidrawAutomate;
|
declare let window: ExcalidrawAutomate;
|
||||||
|
|
||||||
@@ -60,9 +64,11 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
private getScene: Function = null;
|
private getScene: Function = null;
|
||||||
public addElements: Function = null; //add elements to the active Excalidraw drawing
|
public addElements: Function = null; //add elements to the active Excalidraw drawing
|
||||||
private getSelectedTextElement: Function = null;
|
private getSelectedTextElement: Function = null;
|
||||||
|
private getSelectedImageElement: Function = null;
|
||||||
public addText:Function = null;
|
public addText:Function = null;
|
||||||
private refresh: Function = null;
|
private refresh: Function = null;
|
||||||
public excalidrawRef: React.MutableRefObject<any> = null;
|
public excalidrawRef: React.MutableRefObject<any> = null;
|
||||||
|
public excalidrawAPI: any = null;
|
||||||
private excalidrawWrapperRef: React.MutableRefObject<any> = null;
|
private excalidrawWrapperRef: React.MutableRefObject<any> = null;
|
||||||
private justLoaded: boolean = false;
|
private justLoaded: boolean = false;
|
||||||
private plugin: ExcalidrawPlugin;
|
private plugin: ExcalidrawPlugin;
|
||||||
@@ -78,7 +84,6 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
private ctrlKeyDown = false;
|
private ctrlKeyDown = false;
|
||||||
private shiftKeyDown = false;
|
private shiftKeyDown = false;
|
||||||
private altKeyDown = false;
|
private altKeyDown = false;
|
||||||
private mouseEvent:any = null;
|
|
||||||
|
|
||||||
id: string = (this.leaf as any).id;
|
id: string = (this.leaf as any).id;
|
||||||
|
|
||||||
@@ -95,8 +100,8 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
}
|
}
|
||||||
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf('.md')) + '.excalidraw';
|
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf('.md')) + '.excalidraw';
|
||||||
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
|
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
|
||||||
if(file && file instanceof TFile) this.app.vault.modify(file,JSON.stringify(scene));
|
if(file && file instanceof TFile) this.app.vault.modify(file,JSON.stringify(scene,null,"\t"));
|
||||||
else this.app.vault.create(filepath,JSON.stringify(scene));
|
else this.app.vault.create(filepath,JSON.stringify(scene,null,"\t"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public saveSVG(scene?: any) {
|
public saveSVG(scene?: any) {
|
||||||
@@ -111,26 +116,15 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
withBackground: this.plugin.settings.exportWithBackground,
|
withBackground: this.plugin.settings.exportWithBackground,
|
||||||
withTheme: this.plugin.settings.exportWithTheme
|
withTheme: this.plugin.settings.exportWithTheme
|
||||||
}
|
}
|
||||||
const svg = await ExcalidrawView.getSVG(scene,exportSettings);
|
const svg = await getSVG(scene,exportSettings);
|
||||||
if(!svg) return;
|
if(!svg) return;
|
||||||
let serializer =new XMLSerializer();
|
let serializer =new XMLSerializer();
|
||||||
const svgString = serializer.serializeToString(ExcalidrawView.embedFontsInSVG(svg));
|
const svgString = serializer.serializeToString(embedFontsInSVG(svg));
|
||||||
if(file && file instanceof TFile) await this.app.vault.modify(file,svgString);
|
if(file && file instanceof TFile) await this.app.vault.modify(file,svgString);
|
||||||
else await this.app.vault.create(filepath,svgString);
|
else await this.app.vault.create(filepath,svgString);
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static embedFontsInSVG(svg:SVGSVGElement):SVGSVGElement {
|
|
||||||
//replace font references with base64 fonts
|
|
||||||
const includesVirgil = svg.querySelector("text[font-family^='Virgil']") != null;
|
|
||||||
const includesCascadia = svg.querySelector("text[font-family^='Cascadia']") != null;
|
|
||||||
const defs = svg.querySelector("defs");
|
|
||||||
if (defs && (includesCascadia || includesVirgil)) {
|
|
||||||
defs.innerHTML = "<style>" + (includesVirgil ? VIRGIL_FONT : "") + (includesCascadia ? CASCADIA_FONT : "")+"</style>";
|
|
||||||
}
|
|
||||||
return svg;
|
|
||||||
}
|
|
||||||
|
|
||||||
public savePNG(scene?: any) {
|
public savePNG(scene?: any) {
|
||||||
if(!scene) {
|
if(!scene) {
|
||||||
if (!this.getScene) return false;
|
if (!this.getScene) return false;
|
||||||
@@ -145,7 +139,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
withBackground: this.plugin.settings.exportWithBackground,
|
withBackground: this.plugin.settings.exportWithBackground,
|
||||||
withTheme: this.plugin.settings.exportWithTheme
|
withTheme: this.plugin.settings.exportWithTheme
|
||||||
}
|
}
|
||||||
const png = await ExcalidrawView.getPNG(scene,exportSettings,this.plugin.settings.pngExportScale);
|
const png = await getPNG(scene,exportSettings,this.plugin.settings.pngExportScale);
|
||||||
if(!png) return;
|
if(!png) return;
|
||||||
if(file && file instanceof TFile) await this.app.vault.modifyBinary(file,await png.arrayBuffer());
|
if(file && file instanceof TFile) await this.app.vault.modifyBinary(file,await png.arrayBuffer());
|
||||||
else await this.app.vault.createBinary(filepath,await png.arrayBuffer());
|
else await this.app.vault.createBinary(filepath,await png.arrayBuffer());
|
||||||
@@ -156,13 +150,16 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
if(!this.getScene) return;
|
if(!this.getScene) return;
|
||||||
this.preventReload = preventReload;
|
this.preventReload = preventReload;
|
||||||
this.dirty = null;
|
this.dirty = null;
|
||||||
|
const scene = this.getScene();
|
||||||
|
|
||||||
if(this.compatibilityMode) {
|
if(this.compatibilityMode) {
|
||||||
await this.excalidrawData.syncElements(this.getScene());
|
await this.excalidrawData.syncElements(scene);
|
||||||
} else {
|
} else {
|
||||||
if(await this.excalidrawData.syncElements(this.getScene()) && !this.autosaving) {
|
if(await this.excalidrawData.syncElements(scene) && !this.autosaving) {
|
||||||
await this.loadDrawing(false);
|
await this.loadDrawing(false);
|
||||||
}
|
}
|
||||||
|
//generate SVG preview snapshot
|
||||||
|
this.excalidrawData.svgSnapshot = await generateSVGString(this.getScene(),this.plugin.settings);
|
||||||
}
|
}
|
||||||
await super.save();
|
await super.save();
|
||||||
}
|
}
|
||||||
@@ -174,12 +171,12 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
//console.log("ExcalidrawView.getViewData()");
|
//console.log("ExcalidrawView.getViewData()");
|
||||||
if(!this.getScene) return this.data;
|
if(!this.getScene) return this.data;
|
||||||
if(!this.excalidrawData.loaded) return this.data;
|
if(!this.excalidrawData.loaded) return this.data;
|
||||||
|
const scene = this.getScene();
|
||||||
if(!this.compatibilityMode) {
|
if(!this.compatibilityMode) {
|
||||||
let trimLocation = this.data.search(/(^%%\n)?# Text Elements\n/m);
|
let trimLocation = this.data.search(/(^%%\n)?# Text Elements\n/m);
|
||||||
if(trimLocation == -1) trimLocation = this.data.search(/(%%\n)?# Drawing\n/);
|
if(trimLocation == -1) trimLocation = this.data.search(/(%%\n)?# Drawing\n/);
|
||||||
if(trimLocation == -1) return this.data;
|
if(trimLocation == -1) return this.data;
|
||||||
|
|
||||||
const scene = this.excalidrawData.scene;
|
|
||||||
if(!this.autosaving) {
|
if(!this.autosaving) {
|
||||||
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
|
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
|
||||||
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
|
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
|
||||||
@@ -191,71 +188,88 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
return header + this.excalidrawData.generateMD();
|
return header + this.excalidrawData.generateMD();
|
||||||
}
|
}
|
||||||
if(this.compatibilityMode) {
|
if(this.compatibilityMode) {
|
||||||
const scene = this.excalidrawData.scene;
|
|
||||||
if(!this.autosaving) {
|
if(!this.autosaving) {
|
||||||
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
|
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
|
||||||
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
|
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
|
||||||
}
|
}
|
||||||
return JSON.stringify(scene);
|
return JSON.stringify(scene,null,"\t");
|
||||||
}
|
}
|
||||||
return this.data;
|
return this.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleLinkClick(view: ExcalidrawView, ev:MouseEvent) {
|
async handleLinkClick(view: ExcalidrawView, ev:MouseEvent) {
|
||||||
let text:string = (this.textMode == TextMode.parsed)
|
const selectedText = this.getSelectedTextElement();
|
||||||
? this.excalidrawData.getRawText(this.getSelectedTextElement().id)
|
let file = null;
|
||||||
: this.getSelectedTextElement().text;
|
let lineNum = 0;
|
||||||
if(!text) {
|
let linkText:string = null;
|
||||||
|
|
||||||
|
if(selectedText?.id) {
|
||||||
|
linkText = (this.textMode == TextMode.parsed)
|
||||||
|
? this.excalidrawData.getRawText(selectedText.id)
|
||||||
|
: selectedText.text;
|
||||||
|
|
||||||
|
linkText = linkText.replaceAll("\n",""); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187
|
||||||
|
if(linkText.match(REG_LINKINDEX_HYPERLINK)) {
|
||||||
|
window.open(linkText,"_blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = REGEX_LINK.getRes(linkText).next();
|
||||||
|
if(!parts.value) {
|
||||||
|
const tags = linkText.matchAll(/#([\p{Letter}\p{Emoji_Presentation}\p{Number}\/_-]+)/ug).next();
|
||||||
|
if(!tags.value || tags.value.length<2) {
|
||||||
|
new Notice(t("TEXT_ELEMENT_EMPTY"),4000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const search=this.app.workspace.getLeavesOfType("search");
|
||||||
|
if(search.length==0) return;
|
||||||
|
//@ts-ignore
|
||||||
|
search[0].view.setQuery("tag:"+tags.value[1]);
|
||||||
|
this.app.workspace.revealLeaf(search[0]);
|
||||||
|
|
||||||
|
if(document.fullscreenElement === this.contentEl) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
this.zoomToFit();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
linkText = REGEX_LINK.getLink(parts);
|
||||||
|
|
||||||
|
if(linkText.match(REG_LINKINDEX_HYPERLINK)) {
|
||||||
|
window.open(linkText,"_blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(linkText.search("#")>-1) {
|
||||||
|
let t;
|
||||||
|
[t,lineNum] = await this.excalidrawData.getTransclusion(linkText);
|
||||||
|
linkText = linkText.substring(0,linkText.search("#"));
|
||||||
|
}
|
||||||
|
if(linkText.match(REG_LINKINDEX_INVALIDCHARS)) {
|
||||||
|
new Notice(t("FILENAME_INVALID_CHARS"),4000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
file = view.app.metadataCache.getFirstLinkpathDest(linkText,view.file.path);
|
||||||
|
if (!ev.altKey && !file) {
|
||||||
|
new Notice(t("FILE_DOES_NOT_EXIST"), 4000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const selectedImage = this.getSelectedImageElement();
|
||||||
|
if(selectedImage?.id) {
|
||||||
|
await this.save(true); //in case pasted images haven't been saved yet
|
||||||
|
if(this.excalidrawData.files.has(selectedImage.fileId)) {
|
||||||
|
linkText = this.excalidrawData.files.get(selectedImage.fileId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!linkText) {
|
||||||
new Notice(t("LINK_BUTTON_CLICK_NO_TEXT"),20000);
|
new Notice(t("LINK_BUTTON_CLICK_NO_TEXT"),20000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(text.match(REG_LINKINDEX_HYPERLINK)) {
|
|
||||||
window.open(text,"_blank");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
new Notice(t("TEXT_ELEMENT_EMPTY"),4000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const search=this.app.workspace.getLeavesOfType("search");
|
|
||||||
if(search.length==0) return;
|
|
||||||
//@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(document.fullscreenElement === this.contentEl) {
|
|
||||||
document.exitFullscreen();
|
|
||||||
this.zoomToFit();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
text = REGEX_LINK.getLink(parts); //parts.value[2] ? parts.value[2]:parts.value[6];
|
|
||||||
|
|
||||||
if(text.match(REG_LINKINDEX_HYPERLINK)) {
|
|
||||||
window.open(text,"_blank");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let lineNum = null;
|
|
||||||
if(text.search("#")>-1) {
|
|
||||||
let t;
|
|
||||||
[t,lineNum] = await this.excalidrawData.getTransclusion(text);
|
|
||||||
text = text.substring(0,text.search("#"));
|
|
||||||
}
|
|
||||||
if(text.match(REG_LINKINDEX_INVALIDCHARS)) {
|
|
||||||
new Notice(t("FILENAME_INVALID_CHARS"),4000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const file = view.app.metadataCache.getFirstLinkpathDest(text,view.file.path);
|
|
||||||
if (!ev.altKey && !file) {
|
|
||||||
new Notice(t("FILE_DOES_NOT_EXIST"), 4000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const f = view.file;
|
const f = view.file;
|
||||||
@@ -263,17 +277,24 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
document.exitFullscreen();
|
document.exitFullscreen();
|
||||||
this.zoomToFit();
|
this.zoomToFit();
|
||||||
}
|
}
|
||||||
if(lineNum) {
|
const leaf = ev.shiftKey ? getNewOrAdjacentLeaf(this.plugin,view.leaf) : view.leaf;
|
||||||
const leaf = ev.shiftKey ? view.app.workspace.createLeafBySplit(view.leaf) : view.leaf;
|
view.app.workspace.setActiveLeaf(leaf);
|
||||||
leaf.openFile(file,{eState: {line: lineNum-1}});
|
if(file) {
|
||||||
return;
|
leaf.openFile(file,{eState: {line: lineNum-1}}); //if file exists open file and jump to reference
|
||||||
|
} else {
|
||||||
|
leaf.view.app.workspace.openLinkText(linkText,view.file.path);
|
||||||
}
|
}
|
||||||
view.app.workspace.openLinkText(text,view.file.path,ev.shiftKey);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
new Notice(e,4000);
|
new Notice(e,4000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onResize() {
|
||||||
|
if(!this.plugin.settings.zoomToFitOnResize) return;
|
||||||
|
if(!this.excalidrawRef) return;
|
||||||
|
this.zoomToFit(false);
|
||||||
|
}
|
||||||
|
|
||||||
onload() {
|
onload() {
|
||||||
//console.log("ExcalidrawView.onload()");
|
//console.log("ExcalidrawView.onload()");
|
||||||
this.addAction(DISK_ICON_NAME,t("FORCE_SAVE"),async (ev)=> {
|
this.addAction(DISK_ICON_NAME,t("FORCE_SAVE"),async (ev)=> {
|
||||||
@@ -318,7 +339,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
}
|
}
|
||||||
if(reload) {
|
if(reload) {
|
||||||
await this.save(false);
|
await this.save(false);
|
||||||
this.excalidrawRef.current.history.clear(); //to avoid undo replacing links with parsed text
|
this.excalidrawAPI.history.clear(); //to avoid undo replacing links with parsed text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,6 +369,10 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
this.preventReload = false;
|
this.preventReload = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if(this.compatibilityMode) {
|
||||||
|
this.dirty = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(!this.excalidrawRef) return;
|
if(!this.excalidrawRef) return;
|
||||||
if(!this.file) return;
|
if(!this.file) return;
|
||||||
if(file) this.data = await this.app.vault.cachedRead(file);
|
if(file) this.data = await this.app.vault.cachedRead(file);
|
||||||
@@ -360,8 +385,8 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
// clear the view content
|
// clear the view content
|
||||||
clear() {
|
clear() {
|
||||||
if(!this.excalidrawRef) return;
|
if(!this.excalidrawRef) return;
|
||||||
this.excalidrawRef.current.resetScene();
|
this.excalidrawAPI.resetScene();
|
||||||
this.excalidrawRef.current.history.clear();
|
this.excalidrawAPI.history.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
async setViewData (data: string, clear: boolean = false) {
|
async setViewData (data: string, clear: boolean = false) {
|
||||||
@@ -369,7 +394,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
data = this.data = data.replaceAll("\r\n","\n").replaceAll("\r","\n");
|
data = this.data = data.replaceAll("\r\n","\n").replaceAll("\r","\n");
|
||||||
this.app.workspace.onLayoutReady(async ()=>{
|
this.app.workspace.onLayoutReady(async ()=>{
|
||||||
this.dirty = null;
|
this.dirty = null;
|
||||||
this.compatibilityMode = this.file.extension == "excalidraw";
|
this.compatibilityMode = this.file.extension === "excalidraw";
|
||||||
await this.plugin.loadSettings();
|
await this.plugin.loadSettings();
|
||||||
this.plugin.opencount++;
|
this.plugin.opencount++;
|
||||||
if(this.compatibilityMode) {
|
if(this.compatibilityMode) {
|
||||||
@@ -407,29 +432,48 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
const excalidrawData = this.excalidrawData.scene;
|
const excalidrawData = this.excalidrawData.scene;
|
||||||
this.justLoaded = justloaded;
|
this.justLoaded = justloaded;
|
||||||
if(this.excalidrawRef) {
|
if(this.excalidrawRef) {
|
||||||
const viewModeEnabled = this.excalidrawRef.current.getAppState().viewModeEnabled;
|
const viewModeEnabled = this.excalidrawAPI.getAppState().viewModeEnabled;
|
||||||
const zenModeEnabled = this.excalidrawRef.current.getAppState().zenModeEnabled;
|
const zenModeEnabled = this.excalidrawAPI.getAppState().zenModeEnabled;
|
||||||
this.excalidrawRef.current.updateScene({
|
this.excalidrawAPI.updateScene({
|
||||||
elements: excalidrawData.elements,
|
elements: excalidrawData.elements,
|
||||||
appState: {
|
appState: {
|
||||||
zenModeEnabled: zenModeEnabled,
|
zenModeEnabled: zenModeEnabled,
|
||||||
viewModeEnabled: viewModeEnabled,
|
viewModeEnabled: viewModeEnabled,
|
||||||
... excalidrawData.appState,
|
... excalidrawData.appState,
|
||||||
},
|
},
|
||||||
|
files: excalidrawData.files,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
});
|
});
|
||||||
if((this.app.workspace.activeLeaf === this.leaf) && this.excalidrawWrapperRef) {
|
if((this.app.workspace.activeLeaf === this.leaf) && this.excalidrawWrapperRef) {
|
||||||
this.excalidrawWrapperRef.current.focus();
|
this.excalidrawWrapperRef.current.focus();
|
||||||
}
|
}
|
||||||
|
loadSceneFiles(this.app,this.excalidrawData.files,(files:any)=>this.addFiles(files));
|
||||||
} else {
|
} else {
|
||||||
this.instantiateExcalidraw({
|
this.instantiateExcalidraw({
|
||||||
elements: excalidrawData.elements,
|
elements: excalidrawData.elements,
|
||||||
appState: excalidrawData.appState,
|
appState: excalidrawData.appState,
|
||||||
|
files: excalidrawData.files,
|
||||||
libraryItems: await this.getLibrary(),
|
libraryItems: await this.getLibrary(),
|
||||||
});
|
});
|
||||||
|
//files are loaded on excalidrawRef readyPromise
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private addFiles(files:any) {
|
||||||
|
if(files.length === 0) return;
|
||||||
|
const [dirty, scene] = scaleLoadedImage(this.getScene(),files);
|
||||||
|
|
||||||
|
if(dirty) {
|
||||||
|
this.excalidrawAPI.updateScene({
|
||||||
|
elements: scene.elements,
|
||||||
|
appState: scene.appState,
|
||||||
|
commitToHistory: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.excalidrawAPI.addFiles(files);
|
||||||
|
}
|
||||||
|
|
||||||
//Compatibility mode with .excalidraw files
|
//Compatibility mode with .excalidraw files
|
||||||
canAcceptExtension(extension: string) {
|
canAcceptExtension(extension: string) {
|
||||||
return extension == "excalidraw";
|
return extension == "excalidraw";
|
||||||
@@ -452,6 +496,12 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setMarkdownView() {
|
setMarkdownView() {
|
||||||
|
if(this.excalidrawRef) {
|
||||||
|
const el = this.excalidrawAPI.getSceneElements();
|
||||||
|
if(el.filter((e:any)=>e.type==="image").length>0) {
|
||||||
|
new Notice(t("DRAWING_CONTAINS_IMAGE"),6000);
|
||||||
|
}
|
||||||
|
}
|
||||||
this.plugin.excalidrawFileModes[this.id || this.file.path] = "markdown";
|
this.plugin.excalidrawFileModes[this.id || this.file.path] = "markdown";
|
||||||
this.plugin.setMarkdownView(this.leaf);
|
this.plugin.setMarkdownView(this.leaf);
|
||||||
}
|
}
|
||||||
@@ -483,12 +533,12 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
const folderpath = splitFolderAndFilename(this.file.path).folderpath;
|
const folderpath = splitFolderAndFilename(this.file.path).folderpath;
|
||||||
await checkAndCreateFolder(this.app.vault,folderpath); //create folder if it does not exist
|
await checkAndCreateFolder(this.app.vault,folderpath); //create folder if it does not exist
|
||||||
const fname = getNewUniqueFilepath(this.app.vault,filename,folderpath);
|
const fname = getNewUniqueFilepath(this.app.vault,filename,folderpath);
|
||||||
this.app.vault.create(fname,JSON.stringify(this.getScene()));
|
this.app.vault.create(fname,JSON.stringify(this.getScene(),null,"\t"));
|
||||||
new Notice("Exported to " + fname,6000);
|
new Notice("Exported to " + fname,6000);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
download('data:text/plain;charset=utf-8',encodeURIComponent(JSON.stringify(this.getScene())), this.file.basename+'.excalidraw');
|
download('data:text/plain;charset=utf-8',encodeURIComponent(JSON.stringify(this.getScene(),null,"\t")), this.file.basename+'.excalidraw');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -514,7 +564,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
withBackground: this.plugin.settings.exportWithBackground,
|
withBackground: this.plugin.settings.exportWithBackground,
|
||||||
withTheme: this.plugin.settings.exportWithTheme
|
withTheme: this.plugin.settings.exportWithTheme
|
||||||
}
|
}
|
||||||
const png = await ExcalidrawView.getPNG(this.getScene(),exportSettings,this.plugin.settings.pngExportScale);
|
const png = await getPNG(this.getScene(),exportSettings,this.plugin.settings.pngExportScale);
|
||||||
if(!png) return;
|
if(!png) return;
|
||||||
let reader = new FileReader();
|
let reader = new FileReader();
|
||||||
reader.readAsDataURL(png);
|
reader.readAsDataURL(png);
|
||||||
@@ -539,10 +589,10 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
withBackground: this.plugin.settings.exportWithBackground,
|
withBackground: this.plugin.settings.exportWithBackground,
|
||||||
withTheme: this.plugin.settings.exportWithTheme
|
withTheme: this.plugin.settings.exportWithTheme
|
||||||
}
|
}
|
||||||
let svg = await ExcalidrawView.getSVG(this.getScene(),exportSettings);
|
let svg = await getSVG(this.getScene(),exportSettings);
|
||||||
if(!svg) return null;
|
if(!svg) return null;
|
||||||
svg = ExcalidrawView.embedFontsInSVG(svg);
|
svg = embedFontsInSVG(svg);
|
||||||
download("data:image/svg+xml;base64",btoa(unescape(encodeURIComponent(svg.outerHTML))),this.file.basename+'.svg');
|
download(null,svgToBase64(svg.outerHTML),this.file.basename+'.svg');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.saveSVG()
|
this.saveSVG()
|
||||||
@@ -564,13 +614,45 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
const reactElement = React.createElement(() => {
|
const reactElement = React.createElement(() => {
|
||||||
let previousSceneVersion = 0;
|
let previousSceneVersion = 0;
|
||||||
let currentPosition = {x:0, y:0};
|
let currentPosition = {x:0, y:0};
|
||||||
const excalidrawRef = React.useRef(null);
|
|
||||||
const excalidrawWrapperRef = React.useRef(null);
|
const excalidrawWrapperRef = React.useRef(null);
|
||||||
const [dimensions, setDimensions] = React.useState({
|
const [dimensions, setDimensions] = React.useState({
|
||||||
width: undefined,
|
width: undefined,
|
||||||
height: undefined
|
height: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//excalidrawRef readypromise based on
|
||||||
|
//https://codesandbox.io/s/eexcalidraw-resolvable-promise-d0qg3?file=/src/App.js:167-760
|
||||||
|
const resolvablePromise = () => {
|
||||||
|
let resolve;
|
||||||
|
let reject;
|
||||||
|
const promise = new Promise((_resolve, _reject) => {
|
||||||
|
resolve = _resolve;
|
||||||
|
reject = _reject;
|
||||||
|
});
|
||||||
|
//@ts-ignore
|
||||||
|
promise.resolve = resolve;
|
||||||
|
//@ts-ignore
|
||||||
|
promise.reject = reject;
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
// To memoize value between rerenders
|
||||||
|
const excalidrawRef = React.useMemo(
|
||||||
|
() => ({
|
||||||
|
current: {
|
||||||
|
readyPromise: resolvablePromise()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
excalidrawRef.current.readyPromise.then((api) => {
|
||||||
|
this.excalidrawAPI = api;
|
||||||
|
loadSceneFiles(this.app,this.excalidrawData.files,(files:any)=>this.addFiles(files));
|
||||||
|
});
|
||||||
|
}, [excalidrawRef]);
|
||||||
|
|
||||||
this.excalidrawRef = excalidrawRef;
|
this.excalidrawRef = excalidrawRef;
|
||||||
this.excalidrawWrapperRef = excalidrawWrapperRef;
|
this.excalidrawWrapperRef = excalidrawWrapperRef;
|
||||||
|
|
||||||
@@ -592,24 +674,23 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
return () => window.removeEventListener("resize", onResize);
|
return () => window.removeEventListener("resize", onResize);
|
||||||
}, [excalidrawWrapperRef]);
|
}, [excalidrawWrapperRef]);
|
||||||
|
|
||||||
|
|
||||||
this.getSelectedTextElement = ():{id: string, text:string} => {
|
this.getSelectedTextElement = ():{id: string, text:string} => {
|
||||||
if(!excalidrawRef?.current) return {id:null,text:null};
|
if(!excalidrawRef?.current) return {id:null,text:null};
|
||||||
if(this.excalidrawRef.current.getAppState().viewModeEnabled) {
|
if(this.excalidrawAPI.getAppState().viewModeEnabled) {
|
||||||
if(selectedTextElement) {
|
if(selectedTextElement) {
|
||||||
const retval = selectedTextElement;
|
const retval = selectedTextElement;
|
||||||
selectedTextElement == null;
|
selectedTextElement = null;
|
||||||
return retval;
|
return retval;
|
||||||
}
|
}
|
||||||
return {id:null,text:null};
|
return {id:null,text:null};
|
||||||
}
|
}
|
||||||
const selectedElement = excalidrawRef.current.getSceneElements().filter((el:any)=>el.id==Object.keys(excalidrawRef.current.getAppState().selectedElementIds)[0]);
|
const selectedElement = this.excalidrawAPI.getSceneElements().filter((el:any)=>el.id==Object.keys(this.excalidrawAPI.getAppState().selectedElementIds)[0]);
|
||||||
if(selectedElement.length==0) return {id:null,text:null};
|
if(selectedElement.length==0) return {id:null,text:null};
|
||||||
if(selectedElement[0].type == "text") return {id:selectedElement[0].id, text:selectedElement[0].text}; //a text element was selected. Return text
|
if(selectedElement[0].type == "text") return {id:selectedElement[0].id, text:selectedElement[0].text}; //a text element was selected. Return text
|
||||||
if(selectedElement[0].groupIds.length == 0) return {id:null,text:null}; //is the selected element part of a group?
|
if(selectedElement[0].groupIds.length == 0) return {id:null,text:null}; //is the selected element part of a group?
|
||||||
const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of
|
const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of
|
||||||
const textElement = excalidrawRef
|
const textElement = this
|
||||||
.current
|
.excalidrawAPI
|
||||||
.getSceneElements()
|
.getSceneElements()
|
||||||
.filter((el:any)=>el.groupIds?.includes(group))
|
.filter((el:any)=>el.groupIds?.includes(group))
|
||||||
.filter((el:any)=>el.type=="text"); //filter for text elements of the group
|
.filter((el:any)=>el.type=="text"); //filter for text elements of the group
|
||||||
@@ -617,12 +698,36 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
return {id:selectedElement[0].id, text:selectedElement[0].text}; //return text element text
|
return {id:selectedElement[0].id, text:selectedElement[0].text}; //return text element text
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.getSelectedImageElement = ():{id: string, fileId:string} => {
|
||||||
|
if(!excalidrawRef?.current) return {id:null,fileId:null};
|
||||||
|
if(this.excalidrawAPI.getAppState().viewModeEnabled) {
|
||||||
|
if(selectedImageElement) {
|
||||||
|
const retval = selectedImageElement;
|
||||||
|
selectedImageElement = null;
|
||||||
|
return retval;
|
||||||
|
}
|
||||||
|
return {id:null,fileId:null};
|
||||||
|
}
|
||||||
|
const selectedElement = this.excalidrawAPI.getSceneElements().filter((el:any)=>el.id==Object.keys(this.excalidrawAPI.getAppState().selectedElementIds)[0]);
|
||||||
|
if(selectedElement.length===0) return {id:null,fileId:null};
|
||||||
|
if(selectedElement[0].type == "image") return {id:selectedElement[0].id, fileId:selectedElement[0].fileId}; //an image element was selected. Return fileId
|
||||||
|
if(selectedElement[0].groupIds.length === 0) return {id:null,fileId:null}; //is the selected element part of a group?
|
||||||
|
const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of
|
||||||
|
const imageElement = this
|
||||||
|
.excalidrawAPI
|
||||||
|
.getSceneElements()
|
||||||
|
.filter((el:any)=>el.groupIds?.includes(group))
|
||||||
|
.filter((el:any)=>el.type=="image"); //filter for Image elements of the group
|
||||||
|
if(imageElement.length===0) return {id:null,fileId:null}; //the group had no image element member
|
||||||
|
return {id:selectedElement[0].id, fileId:selectedElement[0].fileId}; //return image element fileId
|
||||||
|
};
|
||||||
|
|
||||||
this.addText = (text:string, fontFamily?:1|2|3) => {
|
this.addText = (text:string, fontFamily?:1|2|3) => {
|
||||||
if(!excalidrawRef?.current) {
|
if(!excalidrawRef?.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
|
const el: ExcalidrawElement[] = this.excalidrawAPI.getSceneElements();
|
||||||
const st: AppState = excalidrawRef.current.getAppState();
|
const st: AppState = this.excalidrawAPI.getAppState();
|
||||||
window.ExcalidrawAutomate.reset();
|
window.ExcalidrawAutomate.reset();
|
||||||
window.ExcalidrawAutomate.style.strokeColor = st.currentItemStrokeColor;
|
window.ExcalidrawAutomate.style.strokeColor = st.currentItemStrokeColor;
|
||||||
window.ExcalidrawAutomate.style.opacity = st.currentItemOpacity;
|
window.ExcalidrawAutomate.style.opacity = st.currentItemOpacity;
|
||||||
@@ -633,7 +738,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
this.addElements(window.ExcalidrawAutomate.getElements(),false,true);
|
this.addElements(window.ExcalidrawAutomate.getElements(),false,true);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.addElements = async (newElements:ExcalidrawElement[],repositionToCursor:boolean = false, save:boolean=false):Promise<boolean> => {
|
this.addElements = async (newElements:ExcalidrawElement[],repositionToCursor:boolean = false, save:boolean=false, images:any):Promise<boolean> => {
|
||||||
if(!excalidrawRef?.current) return false;
|
if(!excalidrawRef?.current) return false;
|
||||||
|
|
||||||
const textElements = newElements.filter((el)=>el.type=="text");
|
const textElements = newElements.filter((el)=>el.type=="text");
|
||||||
@@ -645,14 +750,28 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
|
const el: ExcalidrawElement[] = this.excalidrawAPI.getSceneElements();
|
||||||
const st: AppState = excalidrawRef.current.getAppState();
|
let st: AppState = this.excalidrawAPI.getAppState();
|
||||||
|
|
||||||
if(repositionToCursor) newElements = repositionElementsToCursor(newElements,currentPosition,true);
|
if(repositionToCursor) newElements = repositionElementsToCursor(newElements,currentPosition,true);
|
||||||
this.excalidrawRef.current.updateScene({
|
this.excalidrawAPI.updateScene({
|
||||||
elements: el.concat(newElements),
|
elements: el.concat(newElements),
|
||||||
appState: st,
|
appState: st,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
});
|
});
|
||||||
|
if(images) {
|
||||||
|
let files:BinaryFileData[] = [];
|
||||||
|
Object.keys(images).forEach((k)=>{
|
||||||
|
files.push({
|
||||||
|
mimeType :images[k].mimeType,
|
||||||
|
id: images[k].id,
|
||||||
|
dataURL: images[k].dataURL,
|
||||||
|
created: images[k].created
|
||||||
|
});
|
||||||
|
this.excalidrawData.files.set(images[k].id,images[k].file);
|
||||||
|
});
|
||||||
|
this.excalidrawAPI.addFiles(files);
|
||||||
|
}
|
||||||
if(save) this.save(); else this.dirty = this.file?.path;
|
if(save) this.save(); else this.dirty = this.file?.path;
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@@ -661,8 +780,16 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
if(!excalidrawRef?.current) {
|
if(!excalidrawRef?.current) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
|
const el: ExcalidrawElement[] = this.excalidrawAPI.getSceneElements();
|
||||||
const st: AppState = excalidrawRef.current.getAppState();
|
const st: AppState = this.excalidrawAPI.getAppState();
|
||||||
|
const files = this.excalidrawAPI.getFiles();
|
||||||
|
|
||||||
|
if(files) {
|
||||||
|
const imgIds = el.filter((e)=>e.type=="image").map((e:any)=>e.fileId);
|
||||||
|
const toDelete = Object.keys(files).filter((k)=>!imgIds.contains(k));
|
||||||
|
toDelete.forEach((k)=>delete files[k]);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "excalidraw",
|
type: "excalidraw",
|
||||||
version: 2,
|
version: 2,
|
||||||
@@ -686,29 +813,60 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
currentItemEndArrowhead: st.currentItemEndArrowhead,
|
currentItemEndArrowhead: st.currentItemEndArrowhead,
|
||||||
currentItemLinearStrokeSharpness: st.currentItemLinearStrokeSharpness,
|
currentItemLinearStrokeSharpness: st.currentItemLinearStrokeSharpness,
|
||||||
gridSize: st.gridSize,
|
gridSize: st.gridSize,
|
||||||
}
|
},
|
||||||
|
files: files,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
this.refresh = () => {
|
this.refresh = () => {
|
||||||
if(!excalidrawRef?.current) return;
|
if(!excalidrawRef?.current) return;
|
||||||
excalidrawRef.current.refresh();
|
this.excalidrawAPI.refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
//variables used to handle click events in view mode
|
//variables used to handle click events in view mode
|
||||||
let selectedTextElement:{id:string,text:string} = null;
|
let selectedTextElement:{id:string,text:string} = null;
|
||||||
|
let selectedImageElement:{id:string,fileId:string} = null;
|
||||||
let timestamp = 0;
|
let timestamp = 0;
|
||||||
let blockOnMouseButtonDown = false;
|
let blockOnMouseButtonDown = false;
|
||||||
|
|
||||||
const getTextElementAtPointer = (pointer:any) => {
|
const getTextElementAtPointer = (pointer:any) => {
|
||||||
const elements = this.excalidrawRef.current.getSceneElements()
|
const elements = this.excalidrawAPI.getSceneElements()
|
||||||
.filter((e:ExcalidrawElement)=>{
|
.filter((e:ExcalidrawElement)=>{
|
||||||
return e.type == "text"
|
if (e.type !== "text") return false;
|
||||||
&& e.x<=pointer.x && (e.x+e.width)>=pointer.x
|
const [x,y,w,h] = rotatedDimensions(e);
|
||||||
&& e.y<=pointer.y && (e.y+e.height)>=pointer.y;
|
return x<=pointer.x && x+w>=pointer.x
|
||||||
|
&& y<=pointer.y && y+h>=pointer.y;
|
||||||
});
|
});
|
||||||
if(elements.length==0) return null;
|
if(elements.length==0) return {id:null, text:null};
|
||||||
return {id:elements[0].id,text:elements[0].text};
|
if(elements.length===1) return {id:elements[0].id,text:elements[0].text};
|
||||||
|
//if more than 1 text elements are at the location, look for one that has a link
|
||||||
|
const elementsWithLinks = elements.filter((e:ExcalidrawTextElement)=> {
|
||||||
|
const text:string = (this.textMode == TextMode.parsed)
|
||||||
|
? this.excalidrawData.getRawText(e.id)
|
||||||
|
: e.text;
|
||||||
|
if(!text) return false;
|
||||||
|
if(text.match(REG_LINKINDEX_HYPERLINK)) return true;
|
||||||
|
const parts = REGEX_LINK.getRes(text).next();
|
||||||
|
if(!parts.value) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
//if there are no text elements with links, return the first element without a link
|
||||||
|
if(elementsWithLinks.length==0) return {id:elements[0].id,text:elements[0].text};
|
||||||
|
//if there are still multiple text elements with links on top of each other, return the first
|
||||||
|
return {id:elementsWithLinks[0].id,text:elementsWithLinks[0].text};
|
||||||
|
}
|
||||||
|
|
||||||
|
const getImageElementAtPointer = (pointer:any) => {
|
||||||
|
const elements = this.excalidrawAPI.getSceneElements()
|
||||||
|
.filter((e:ExcalidrawElement)=>{
|
||||||
|
if (e.type !== "image") return false;
|
||||||
|
const [x,y,w,h] = rotatedDimensions(e);
|
||||||
|
return x<=pointer.x && x+w>=pointer.x
|
||||||
|
&& y<=pointer.y && y+h>=pointer.y;
|
||||||
|
});
|
||||||
|
if(elements.length===0) return {id:null, fileId:null};
|
||||||
|
if(elements.length>=1) return {id:elements[0].id,fileId:elements[0].fileId};
|
||||||
|
//if more than 1 image elements are at the location, return the first
|
||||||
}
|
}
|
||||||
|
|
||||||
let hoverPoint = {x:0,y:0};
|
let hoverPoint = {x:0,y:0};
|
||||||
@@ -740,6 +898,24 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
if (transfer.types?.includes('text/html') || transfer.types?.includes('text/plain')) return 'copy';
|
if (transfer.types?.includes('text/html') || transfer.types?.includes('text/plain')) return 'copy';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let viewModeEnabled = false;
|
||||||
|
const handleLinkClick = () => {
|
||||||
|
selectedTextElement = getTextElementAtPointer(currentPosition);
|
||||||
|
if(selectedTextElement && selectedTextElement.id) {
|
||||||
|
const event = new MouseEvent("click", {ctrlKey: true, shiftKey: this.shiftKeyDown, altKey:this.altKeyDown});
|
||||||
|
this.handleLinkClick(this,event);
|
||||||
|
selectedTextElement = null;
|
||||||
|
}
|
||||||
|
selectedImageElement = getImageElementAtPointer(currentPosition);
|
||||||
|
if(selectedImageElement && selectedImageElement.id) {
|
||||||
|
const event = new MouseEvent("click", {ctrlKey: true, shiftKey: this.shiftKeyDown, altKey:this.altKeyDown});
|
||||||
|
this.handleLinkClick(this,event);
|
||||||
|
selectedImageElement = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mouseEvent:any = null;
|
||||||
|
|
||||||
const excalidrawDiv = React.createElement(
|
const excalidrawDiv = React.createElement(
|
||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
@@ -748,10 +924,13 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
key: "abc",
|
key: "abc",
|
||||||
tabIndex: 0,
|
tabIndex: 0,
|
||||||
onKeyDown: (e:any) => {
|
onKeyDown: (e:any) => {
|
||||||
|
//@ts-ignore
|
||||||
|
if(e.target === excalidrawDiv.ref.current) return; //event should originate from the canvas
|
||||||
if(document.fullscreenEnabled && document.fullscreenElement == this.contentEl && e.keyCode==27) {
|
if(document.fullscreenEnabled && document.fullscreenElement == this.contentEl && e.keyCode==27) {
|
||||||
document.exitFullscreen();
|
document.exitFullscreen();
|
||||||
this.zoomToFit();
|
this.zoomToFit();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ctrlKeyDown = e.ctrlKey || e.metaKey;
|
this.ctrlKeyDown = e.ctrlKey || e.metaKey;
|
||||||
this.shiftKeyDown = e.shiftKey;
|
this.shiftKeyDown = e.shiftKey;
|
||||||
this.altKeyDown = e.altKey;
|
this.altKeyDown = e.altKey;
|
||||||
@@ -767,7 +946,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
if(!text) return;
|
if(!text) return;
|
||||||
if(text.match(REG_LINKINDEX_HYPERLINK)) return;
|
if(text.match(REG_LINKINDEX_HYPERLINK)) return;
|
||||||
|
|
||||||
const parts = text.matchAll(REGEX_LINK.EXPR).next();
|
const parts = REGEX_LINK.getRes(text).next();
|
||||||
if(!parts.value) return;
|
if(!parts.value) return;
|
||||||
let linktext = REGEX_LINK.getLink(parts); //parts.value[2] ? parts.value[2]:parts.value[6];
|
let linktext = REGEX_LINK.getLink(parts); //parts.value[2] ? parts.value[2]:parts.value[6];
|
||||||
|
|
||||||
@@ -777,7 +956,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
this.plugin.hover.sourcePath = this.file.path;
|
this.plugin.hover.sourcePath = this.file.path;
|
||||||
hoverPreviewTarget = this.contentEl; //e.target;
|
hoverPreviewTarget = this.contentEl; //e.target;
|
||||||
this.app.workspace.trigger('hover-link', {
|
this.app.workspace.trigger('hover-link', {
|
||||||
event: this.mouseEvent,
|
event: mouseEvent,
|
||||||
source: VIEW_TYPE_EXCALIDRAW,
|
source: VIEW_TYPE_EXCALIDRAW,
|
||||||
hoverParent: hoverPreviewTarget,
|
hoverParent: hoverPreviewTarget,
|
||||||
targetEl: hoverPreviewTarget,
|
targetEl: hoverPreviewTarget,
|
||||||
@@ -803,12 +982,12 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
if(!(e.ctrlKey||e.metaKey)) return;
|
if(!(e.ctrlKey||e.metaKey)) return;
|
||||||
if(!(this.plugin.settings.allowCtrlClick)) return;
|
if(!(this.plugin.settings.allowCtrlClick)) return;
|
||||||
if(!this.getSelectedTextElement().id) return;
|
if(!(this.getSelectedTextElement().id || this.getSelectedImageElement().id)) return;
|
||||||
this.handleLinkClick(this,e);
|
this.handleLinkClick(this,e);
|
||||||
},
|
},
|
||||||
onMouseMove: (e:MouseEvent) => {
|
onMouseMove: (e:MouseEvent) => {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
this.mouseEvent = e.nativeEvent;
|
mouseEvent = e.nativeEvent;
|
||||||
},
|
},
|
||||||
onMouseOver: (e:MouseEvent) => {
|
onMouseOver: (e:MouseEvent) => {
|
||||||
clearHoverPreview();
|
clearHoverPreview();
|
||||||
@@ -842,17 +1021,9 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
onPointerUpdate: (p:any) => {
|
onPointerUpdate: (p:any) => {
|
||||||
currentPosition = p.pointer;
|
currentPosition = p.pointer;
|
||||||
if(hoverPreviewTarget && (Math.abs(hoverPoint.x-p.pointer.x)>50 || Math.abs(hoverPoint.y-p.pointer.y)>50)) clearHoverPreview();
|
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;
|
if(!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";
|
const buttonDown = !blockOnMouseButtonDown && p.button === "down";
|
||||||
if(buttonDown) {
|
if(buttonDown) {
|
||||||
blockOnMouseButtonDown = true;
|
blockOnMouseButtonDown = true;
|
||||||
|
|
||||||
@@ -870,11 +1041,12 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
timestamp = now;
|
timestamp = now;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (p.button=="up") {
|
if (p.button === "up") {
|
||||||
blockOnMouseButtonDown=false;
|
blockOnMouseButtonDown=false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onChange: (et:ExcalidrawElement[],st:AppState) => {
|
onChange: (et:ExcalidrawElement[],st:AppState) => {
|
||||||
|
viewModeEnabled = st.viewModeEnabled;
|
||||||
if(this.justLoaded) {
|
if(this.justLoaded) {
|
||||||
this.justLoaded = false;
|
this.justLoaded = false;
|
||||||
this.zoomToFit(false);
|
this.zoomToFit(false);
|
||||||
@@ -897,32 +1069,85 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
})();
|
})();
|
||||||
},
|
},
|
||||||
/*onPaste: (data: ClipboardData, event: ClipboardEvent | null) => {
|
onPaste: (data: ClipboardData, event: ClipboardEvent | null) => {
|
||||||
console.log(data,event);
|
if(data.elements) {
|
||||||
return false;
|
const self = this;
|
||||||
},*/
|
setTimeout(()=>self.save(false),300);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
onDrop: (event: React.DragEvent<HTMLDivElement>):boolean => {
|
onDrop: (event: React.DragEvent<HTMLDivElement>):boolean => {
|
||||||
const st: AppState = excalidrawRef.current.getAppState();
|
const st: AppState = this.excalidrawAPI.getAppState();
|
||||||
currentPosition = viewportCoordsToSceneCoords({ clientX: event.clientX, clientY: event.clientY },st);
|
currentPosition = viewportCoordsToSceneCoords({ clientX: event.clientX, clientY: event.clientY },st);
|
||||||
|
|
||||||
const draggable = (this.app as any).dragManager.draggable;
|
const draggable = (this.app as any).dragManager.draggable;
|
||||||
|
const onDropHook = (type:"file"|"text"|"unknown", files:TFile[], text:string):boolean => {
|
||||||
|
if (window.ExcalidrawAutomate.onDropHook) {
|
||||||
|
try {
|
||||||
|
return window.ExcalidrawAutomate.onDropHook({
|
||||||
|
//@ts-ignore
|
||||||
|
ea: window.ExcalidrawAutomate, //the Excalidraw Automate object
|
||||||
|
event: event, //React.DragEvent<HTMLDivElement>
|
||||||
|
draggable: draggable, //Obsidian draggable object
|
||||||
|
type: type, //"file"|"text"
|
||||||
|
payload: {
|
||||||
|
files: files, //TFile[] array of dropped files
|
||||||
|
text: text, //string
|
||||||
|
},
|
||||||
|
excalidrawFile: this.file, //the file receiving the drop event
|
||||||
|
view: this, //the excalidraw view receiving the drop
|
||||||
|
pointerPosition: currentPosition //the pointer position on canvas at the time of drop
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
new Notice("on drop hook error. See console log for details");
|
||||||
|
console.log(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
switch(draggable?.type) {
|
switch(draggable?.type) {
|
||||||
case "file":
|
case "file":
|
||||||
this.addText(`[[${this.app.metadataCache.fileToLinktext(draggable.file,this.file.path,true)}]]`);
|
if (!onDropHook("file",[draggable.file],null)) {
|
||||||
|
if((event.ctrlKey || event.metaKey)
|
||||||
|
&& (IMAGE_TYPES.contains(draggable.file.extension)
|
||||||
|
|| this.plugin.isExcalidrawFile(draggable.file))) {
|
||||||
|
const f = draggable.file;
|
||||||
|
const topX = currentPosition.x;
|
||||||
|
const topY = currentPosition.y;
|
||||||
|
const ea = window.ExcalidrawAutomate;
|
||||||
|
ea.reset();
|
||||||
|
ea.setView(this);
|
||||||
|
(async () => {
|
||||||
|
await ea.addImage(currentPosition.x,currentPosition.y,draggable.file);
|
||||||
|
ea.addElementsToView(false,false);
|
||||||
|
})();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.addText(`[[${this.app.metadataCache.fileToLinktext(draggable.file,this.file.path,true)}]]`);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
case "files":
|
case "files":
|
||||||
for(const f of draggable.files) {
|
if (!onDropHook("file",draggable.files,null)) {
|
||||||
this.addText(`[[${this.app.metadataCache.fileToLinktext(f,this.file.path,true)}]]`);
|
for(const f of draggable.files) {
|
||||||
currentPosition.y+=st.currentItemFontSize*2;
|
this.addText(`[[${this.app.metadataCache.fileToLinktext(f,this.file.path,true)}]]`);
|
||||||
|
currentPosition.y+=st.currentItemFontSize*2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (event.dataTransfer.types.includes("text/plain")) {
|
if (event.dataTransfer.types.includes("text/plain")) {
|
||||||
const text:string = event.dataTransfer.getData("text");
|
const text:string = event.dataTransfer.getData("text");
|
||||||
if(!text) return true;
|
if(!text) return true;
|
||||||
this.addText(text.replace(/(!\[\[.*#[^\]]*\]\])/g,"$1{40}"));
|
if (!onDropHook("text",null,text)) {
|
||||||
|
this.addText(text.replace(/(!\[\[.*#[^\]]*\]\])/g,"$1{40}"));
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if(onDropHook("unknown",null,null)) return false;
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
onBeforeTextEdit: (textElement: ExcalidrawTextElement) => {
|
onBeforeTextEdit: (textElement: ExcalidrawTextElement) => {
|
||||||
@@ -930,8 +1155,12 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
clearInterval(this.autosaveTimer);
|
clearInterval(this.autosaveTimer);
|
||||||
this.autosaveTimer = null;
|
this.autosaveTimer = null;
|
||||||
}
|
}
|
||||||
if(this.textMode==TextMode.parsed) return this.excalidrawData.getRawText(textElement.id);
|
//if(this.textMode==TextMode.parsed) {
|
||||||
return null;
|
const raw = this.excalidrawData.getRawText(textElement.id);
|
||||||
|
if(!raw) return textElement.rawText;
|
||||||
|
return raw;
|
||||||
|
/*}
|
||||||
|
return null;*/
|
||||||
},
|
},
|
||||||
onBeforeTextSubmit: (textElement: ExcalidrawTextElement, text:string, isDeleted:boolean) => {
|
onBeforeTextSubmit: (textElement: ExcalidrawTextElement, text:string, isDeleted:boolean) => {
|
||||||
if(isDeleted) {
|
if(isDeleted) {
|
||||||
@@ -942,20 +1171,20 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
}
|
}
|
||||||
//If the parsed text is different than the raw text, and if View is in TextMode.parsed
|
//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
|
//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
|
if(text!=textElement.text || !this.excalidrawData.getRawText(textElement.id)) { //the user made changes to the text or the text is missing from Excalidraw Data (recently copy/pasted)
|
||||||
//setTextElement will attempt a quick parse (without processing transclusions)
|
//setTextElement will attempt a quick parse (without processing transclusions)
|
||||||
const parseResult = this.excalidrawData.setTextElement(textElement.id, text,async ()=>{
|
const parseResult = this.excalidrawData.setTextElement(textElement.id, text,async ()=>{
|
||||||
await this.save(false);
|
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
|
//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
|
//thus I only check if TextMode.parsed, text is always != with parseResult
|
||||||
if(this.textMode == TextMode.parsed) this.excalidrawRef.current.history.clear();
|
if(this.textMode == TextMode.parsed) this.excalidrawAPI.history.clear();
|
||||||
this.setupAutosaveTimer();
|
this.setupAutosaveTimer();
|
||||||
});
|
});
|
||||||
if(parseResult) { //there were no transclusions in the raw text, quick parse was successful
|
if(parseResult) { //there were no transclusions in the raw text, quick parse was successful
|
||||||
this.setupAutosaveTimer();
|
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(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
|
if(text == parseResult) return; //There were no links to parse, raw text and parsed text are equivalent
|
||||||
this.excalidrawRef.current.history.clear();
|
this.excalidrawAPI.history.clear();
|
||||||
return parseResult;
|
return parseResult;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -973,51 +1202,21 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
);
|
);
|
||||||
|
|
||||||
});
|
});
|
||||||
ReactDOM.render(reactElement,this.contentEl,()=>this.excalidrawWrapperRef.current.focus());
|
|
||||||
|
ReactDOM.render(reactElement,this.contentEl,()=>{
|
||||||
|
this.excalidrawWrapperRef.current.focus();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public zoomToFit(delay:boolean = true) {
|
public zoomToFit(delay:boolean = true) {
|
||||||
if(!this.excalidrawRef) return;
|
if(!this.excalidrawRef) return;
|
||||||
const current = this.excalidrawRef.current;
|
const current = this.excalidrawAPI;
|
||||||
const fullscreen = (document.fullscreenElement==this.contentEl);
|
const fullscreen = (document.fullscreenElement==this.contentEl);
|
||||||
const elements = current.getSceneElements();
|
const elements = current.getSceneElements();
|
||||||
if(delay) { //time for the DOM to render, I am sure there is a more elegant solution
|
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);
|
setTimeout(() => current.zoomToFit(elements,2,fullscreen?0:0.05),100);
|
||||||
} else {
|
} else {
|
||||||
current.zoomToFit(elements,10,fullscreen?0:0.05);
|
current.zoomToFit(elements,2,fullscreen?0:0.05);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async getSVG(scene:any, exportSettings:ExportSettings):Promise<SVGSVGElement> {
|
|
||||||
try {
|
|
||||||
return exportToSvg({
|
|
||||||
elements: scene.elements,
|
|
||||||
appState: {
|
|
||||||
exportBackground: exportSettings.withBackground,
|
|
||||||
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
|
|
||||||
... scene.appState,},
|
|
||||||
exportPadding:10,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async getPNG(scene:any, exportSettings:ExportSettings, scale:number = 1) {
|
|
||||||
try {
|
|
||||||
return await Excalidraw.exportToBlob({
|
|
||||||
elements: scene.elements,
|
|
||||||
appState: {
|
|
||||||
exportBackground: exportSettings.withBackground,
|
|
||||||
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
|
|
||||||
... scene.appState,},
|
|
||||||
mimeType: "image/png",
|
|
||||||
exportWithDarkMode: "true",
|
|
||||||
metadata: "Generated by Excalidraw-Obsidian plugin",
|
|
||||||
getDimensions: (width:number, height:number) => ({ width:width*scale, height:height*scale, scale:scale })
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ export class MigrationPrompt extends Modal {
|
|||||||
|
|
||||||
createForm(): void {
|
createForm(): void {
|
||||||
const div = this.contentEl.createDiv();
|
const div = this.contentEl.createDiv();
|
||||||
div.addClass("excalidarw-prompt-div");
|
div.addClass("excalidraw-prompt-div");
|
||||||
div.style.maxWidth = "600px";
|
div.style.maxWidth = "600px";
|
||||||
div.createEl('p',{text: "This version comes with tons of new features and possibilities. Please read the description in Community Plugins to find out more."});
|
div.createEl('p',{text: "This version comes with tons of new features and possibilities. Please read the description in Community Plugins to find out more."});
|
||||||
div.createEl('p',{text: ""} , (el) => {
|
div.createEl('p',{text: ""} , (el) => {
|
||||||
|
|||||||
295
src/Utils.ts
295
src/Utils.ts
@@ -1,6 +1,28 @@
|
|||||||
import { normalizePath, TAbstractFile, TFolder, Vault } from "obsidian";
|
import Excalidraw,{exportToSvg} from "@zsviczian/excalidraw";
|
||||||
|
import { App, normalizePath, TAbstractFile, TFile, TFolder, Vault, WorkspaceLeaf } from "obsidian";
|
||||||
import { Random } from "roughjs/bin/math";
|
import { Random } from "roughjs/bin/math";
|
||||||
import { Zoom } from "@zsviczian/excalidraw/types/types";
|
import { BinaryFileData, DataURL, Zoom } from "@zsviczian/excalidraw/types/types";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { CASCADIA_FONT, IMAGE_TYPES, VIRGIL_FONT } from "./constants";
|
||||||
|
import {ExcalidrawAutomate} from './ExcalidrawAutomate';
|
||||||
|
import ExcalidrawPlugin from "./main";
|
||||||
|
import { ExcalidrawElement, FileId } from "@zsviczian/excalidraw/types/element/types";
|
||||||
|
import { ExportSettings } from "./ExcalidrawView";
|
||||||
|
import { ExcalidrawSettings } from "./settings";
|
||||||
|
import { html_beautify } from "js-beautify"
|
||||||
|
|
||||||
|
declare module "obsidian" {
|
||||||
|
interface Workspace {
|
||||||
|
getAdjacentLeafInDirection(leaf: WorkspaceLeaf, direction: string): WorkspaceLeaf;
|
||||||
|
}
|
||||||
|
interface Vault {
|
||||||
|
getConfig(option:"attachmentFolderPath"): string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare let window: ExcalidrawAutomate;
|
||||||
|
|
||||||
|
export declare type MimeType = "image/svg+xml" | "image/png" | "image/jpeg" | "image/gif" | "application/octet-stream";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
|
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
|
||||||
@@ -102,6 +124,38 @@ export function wrapText(text:string, lineLen:number, forceWrap:boolean=false):s
|
|||||||
return outstring.replace(/\n$/, '');
|
return outstring.replace(/\n$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rotate = (
|
||||||
|
pointX: number,
|
||||||
|
pointY: number,
|
||||||
|
centerX: number,
|
||||||
|
centerY: number,
|
||||||
|
angle: number,
|
||||||
|
): [number, number] =>
|
||||||
|
// 𝑎′𝑥=(𝑎𝑥−𝑐𝑥)cos𝜃−(𝑎𝑦−𝑐𝑦)sin𝜃+𝑐𝑥
|
||||||
|
// 𝑎′𝑦=(𝑎𝑥−𝑐𝑥)sin𝜃+(𝑎𝑦−𝑐𝑦)cos𝜃+𝑐𝑦.
|
||||||
|
// https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line
|
||||||
|
[
|
||||||
|
(pointX - centerX) * Math.cos(angle) - (pointY - centerY) * Math.sin(angle) + centerX,
|
||||||
|
(pointX - centerX) * Math.sin(angle) + (pointY - centerY) * Math.cos(angle) + centerY,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const rotatedDimensions = (
|
||||||
|
element: ExcalidrawElement
|
||||||
|
): [number, number, number, number] => {
|
||||||
|
if(element.angle===0) [element.x,element.y,element.width,element.height];
|
||||||
|
const centerX = element.x+element.width/2;
|
||||||
|
const centerY = element.y+element.height/2;
|
||||||
|
const [left,top] = rotate(element.x,element.y,centerX,centerY,element.angle);
|
||||||
|
const [right,bottom] = rotate(element.x+element.width,element.y+element.height,centerX,centerY,element.angle);
|
||||||
|
return [
|
||||||
|
left<right ? left : right,
|
||||||
|
top<bottom ? top : bottom,
|
||||||
|
Math.abs(left-right),
|
||||||
|
Math.abs(top-bottom)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const viewportCoordsToSceneCoords = (
|
export const viewportCoordsToSceneCoords = (
|
||||||
{ clientX, clientY }: { clientX: number; clientY: number },
|
{ clientX, clientY }: { clientX: number; clientY: number },
|
||||||
{
|
{
|
||||||
@@ -123,3 +177,240 @@ export const viewportCoordsToSceneCoords = (
|
|||||||
const y = (clientY - zoom.translation.y - offsetTop) * invScale - scrollY;
|
const y = (clientY - zoom.translation.y - offsetTop) * invScale - scrollY;
|
||||||
return { x, y };
|
return { x, y };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getNewOrAdjacentLeaf = (plugin: ExcalidrawPlugin, leaf: WorkspaceLeaf):WorkspaceLeaf => {
|
||||||
|
if(plugin.settings.openInAdjacentPane) {
|
||||||
|
let leafToUse = plugin.app.workspace.getAdjacentLeafInDirection(leaf, "right");
|
||||||
|
if(!leafToUse){leafToUse = plugin.app.workspace.getAdjacentLeafInDirection(leaf, "left");}
|
||||||
|
if(!leafToUse){leafToUse = plugin.app.workspace.getAdjacentLeafInDirection(leaf, "bottom");}
|
||||||
|
if(!leafToUse){leafToUse = plugin.app.workspace.getAdjacentLeafInDirection(leaf, "top");}
|
||||||
|
if(!leafToUse){leafToUse = plugin.app.workspace.createLeafBySplit(leaf);}
|
||||||
|
return leafToUse;
|
||||||
|
}
|
||||||
|
return plugin.app.workspace.createLeafBySplit(leaf);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getObsidianImage = async (app: App, file: TFile)
|
||||||
|
:Promise<{
|
||||||
|
mimeType: MimeType,
|
||||||
|
fileId: FileId,
|
||||||
|
dataURL: DataURL,
|
||||||
|
created: number,
|
||||||
|
size: {height: number, width: number},
|
||||||
|
}> => {
|
||||||
|
if(!app || !file) return null;
|
||||||
|
const isExcalidrawFile = window.ExcalidrawAutomate.isExcalidrawFile(file);
|
||||||
|
if (!(IMAGE_TYPES.contains(file.extension) || isExcalidrawFile)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const ab = await app.vault.readBinary(file);
|
||||||
|
const excalidrawSVG = isExcalidrawFile
|
||||||
|
? svgToBase64((await window.ExcalidrawAutomate.createSVG(file.path,true)).outerHTML) as DataURL
|
||||||
|
: null;
|
||||||
|
let mimeType:MimeType = "image/svg+xml";
|
||||||
|
if (!isExcalidrawFile) {
|
||||||
|
switch (file.extension) {
|
||||||
|
case "png": mimeType = "image/png";break;
|
||||||
|
case "jpeg":mimeType = "image/jpeg";break;
|
||||||
|
case "jpg": mimeType = "image/jpeg";break;
|
||||||
|
case "gif": mimeType = "image/gif";break;
|
||||||
|
case "svg": mimeType = "image/svg+xml";break;
|
||||||
|
default: mimeType = "application/octet-stream";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
mimeType: mimeType,
|
||||||
|
fileId: await generateIdFromFile(ab),
|
||||||
|
dataURL: excalidrawSVG ?? (file.extension==="svg" ? await getSVGData(app,file) : await getDataURL(ab)),
|
||||||
|
created: file.stat.mtime,
|
||||||
|
size: await getImageSize(app,excalidrawSVG??app.vault.getResourcePath(file))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const getSVGData = async (app: App, file: TFile): Promise<DataURL> => {
|
||||||
|
const svg = await app.vault.read(file);
|
||||||
|
return svgToBase64(svg) as DataURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const svgToBase64 = (svg:string):string => {
|
||||||
|
return "data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.replaceAll(" "," "))));
|
||||||
|
}
|
||||||
|
const getDataURL = async (file: ArrayBuffer): Promise<DataURL> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const dataURL = reader.result as DataURL;
|
||||||
|
resolve(dataURL);
|
||||||
|
};
|
||||||
|
reader.onerror = (error) => reject(error);
|
||||||
|
reader.readAsDataURL(new Blob([new Uint8Array(file)]));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateIdFromFile = async (file: ArrayBuffer):Promise<FileId> => {
|
||||||
|
let id: FileId;
|
||||||
|
try {
|
||||||
|
const hashBuffer = await window.crypto.subtle.digest(
|
||||||
|
"SHA-1",
|
||||||
|
file,
|
||||||
|
);
|
||||||
|
id =
|
||||||
|
// convert buffer to byte array
|
||||||
|
Array.from(new Uint8Array(hashBuffer))
|
||||||
|
// convert to hex string
|
||||||
|
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||||
|
.join("") as FileId;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
id = nanoid(40) as FileId;
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImageSize = async (app: App, src:string):Promise<{height:number, width:number}> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let img = new Image()
|
||||||
|
img.onload = () => resolve({height: img.height, width:img.width});
|
||||||
|
img.onerror = reject;
|
||||||
|
img.src = src;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getBinaryFileFromDataURL = (dataURL:string):ArrayBuffer => {
|
||||||
|
if(!dataURL) return null;
|
||||||
|
const parts = dataURL.matchAll(/base64,(.*)/g).next();
|
||||||
|
const binary_string = window.atob(parts.value[1]);
|
||||||
|
const len = binary_string.length;
|
||||||
|
const bytes = new Uint8Array(len);
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
|
bytes[i] = binary_string.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAttachmentsFolderAndFilePath = async (app:App, activeViewFilePath:string, newFileName:string):Promise<[string,string]> => {
|
||||||
|
let folder = 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(activeViewFilePath).folderpath + "/";
|
||||||
|
folder = normalizePath(activeFileFolder + folder.substring(2));
|
||||||
|
}
|
||||||
|
if(!folder) folder = "";
|
||||||
|
await checkAndCreateFolder(app.vault,folder);
|
||||||
|
return [folder,normalizePath(folder + "/" + newFileName)];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSVG = async (scene:any, exportSettings:ExportSettings):Promise<SVGSVGElement> => {
|
||||||
|
try {
|
||||||
|
return exportToSvg({
|
||||||
|
elements: scene.elements,
|
||||||
|
appState: {
|
||||||
|
exportBackground: exportSettings.withBackground,
|
||||||
|
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
|
||||||
|
... scene.appState,},
|
||||||
|
files: scene.files,
|
||||||
|
exportPadding:10,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateSVGString = async (scene:any, settings: ExcalidrawSettings):Promise<string> => {
|
||||||
|
const exportSettings: ExportSettings = {
|
||||||
|
withBackground: settings.exportWithBackground,
|
||||||
|
withTheme: settings.exportWithTheme
|
||||||
|
}
|
||||||
|
const svg = await getSVG(scene,exportSettings);
|
||||||
|
if(svg) {
|
||||||
|
|
||||||
|
return html_beautify(svg.outerHTML,{"indent_with_tabs": true});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPNG = async (scene:any, exportSettings:ExportSettings, scale:number = 1) => {
|
||||||
|
try {
|
||||||
|
return await Excalidraw.exportToBlob({
|
||||||
|
elements: scene.elements,
|
||||||
|
appState: {
|
||||||
|
exportBackground: exportSettings.withBackground,
|
||||||
|
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
|
||||||
|
... scene.appState,},
|
||||||
|
files: scene.files,
|
||||||
|
mimeType: "image/png",
|
||||||
|
exportWithDarkMode: "true",
|
||||||
|
metadata: "Generated by Excalidraw-Obsidian plugin",
|
||||||
|
getDimensions: (width:number, height:number) => ({ width:width*scale, height:height*scale, scale:scale })
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const embedFontsInSVG = (svg:SVGSVGElement):SVGSVGElement => {
|
||||||
|
//replace font references with base64 fonts
|
||||||
|
const includesVirgil = svg.querySelector("text[font-family^='Virgil']") != null;
|
||||||
|
const includesCascadia = svg.querySelector("text[font-family^='Cascadia']") != null;
|
||||||
|
const defs = svg.querySelector("defs");
|
||||||
|
if (defs && (includesCascadia || includesVirgil)) {
|
||||||
|
defs.innerHTML = "<style>" + (includesVirgil ? VIRGIL_FONT : "") + (includesCascadia ? CASCADIA_FONT : "")+"</style>";
|
||||||
|
}
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const loadSceneFiles = async (app:App, filesMap: Map<FileId, string>,addFiles:Function) => {
|
||||||
|
const entries = filesMap.entries();
|
||||||
|
let entry;
|
||||||
|
let files:BinaryFileData[] = [];
|
||||||
|
while(!(entry = entries.next()).done) {
|
||||||
|
const file = app.vault.getAbstractFileByPath(entry.value[1]);
|
||||||
|
if(file && file instanceof TFile) {
|
||||||
|
const data = await getObsidianImage(app,file);
|
||||||
|
files.push({
|
||||||
|
mimeType : data.mimeType,
|
||||||
|
id: entry.value[0],
|
||||||
|
dataURL: data.dataURL,
|
||||||
|
created: data.created,
|
||||||
|
//@ts-ignore
|
||||||
|
size: data.size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try { //in try block because by the time files are loaded the user may have closed the view
|
||||||
|
addFiles(files);
|
||||||
|
} catch(e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const scaleLoadedImage = (scene:any, files:any):[boolean,any] => {
|
||||||
|
let dirty = false;
|
||||||
|
for(const f of files) {
|
||||||
|
const [w_image,h_image] = [f.size.width,f.size.height];
|
||||||
|
const imageAspectRatio = f.size.width/f.size.height;
|
||||||
|
scene
|
||||||
|
.elements
|
||||||
|
.filter((e:any)=>(e.type === "image" && e.fileId === f.id))
|
||||||
|
.forEach((el:any)=>{
|
||||||
|
const [w_old,h_old] = [el.width,el.height];
|
||||||
|
const elementAspectRatio = w_old/h_old;
|
||||||
|
if(imageAspectRatio != elementAspectRatio) {
|
||||||
|
dirty = true;
|
||||||
|
const h_new = Math.sqrt(w_old*h_old*h_image/w_image);
|
||||||
|
const w_new = Math.sqrt(w_old*h_old*w_image/h_image);
|
||||||
|
el.height = h_new;
|
||||||
|
el.width = w_new;
|
||||||
|
el.y += (h_old-h_new)/2;
|
||||||
|
el.x += (w_old-w_new)/2;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return [dirty,scene];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ export function JSON_parse(x:string):any {return JSON.parse(x.replaceAll("["
|
|||||||
|
|
||||||
import {customAlphabet} from "nanoid";
|
import {customAlphabet} from "nanoid";
|
||||||
export const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',8);
|
export const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',8);
|
||||||
|
export const IMAGE_TYPES = ['jpeg', 'jpg', 'png', 'gif', 'svg', 'bmp'];
|
||||||
|
export const MAX_IMAGE_SIZE = 600;
|
||||||
export const FRONTMATTER_KEY = "excalidraw-plugin";
|
export const FRONTMATTER_KEY = "excalidraw-plugin";
|
||||||
export const FRONTMATTER_KEY_CUSTOM_PREFIX = "excalidraw-link-prefix";
|
export const FRONTMATTER_KEY_CUSTOM_PREFIX = "excalidraw-link-prefix";
|
||||||
export const FRONTMATTER_KEY_CUSTOM_URL_PREFIX = "excalidraw-url-prefix";
|
export const FRONTMATTER_KEY_CUSTOM_URL_PREFIX = "excalidraw-url-prefix";
|
||||||
@@ -13,7 +15,8 @@ export const MAX_COLORS = 5;
|
|||||||
export const COLOR_FREQ = 6;
|
export const COLOR_FREQ = 6;
|
||||||
export const RERENDER_EVENT = "excalidraw-embed-rerender";
|
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 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 DARK_BLANK_DRAWING = '{"type":"excalidraw","version":2,"source":"https://excalidraw.com","elements":[],"appState":{"theme":"dark","gridSize":null,"viewBackgroundColor":"#ffffff"}}';
|
||||||
|
export const FRONTMATTER = ["---","",`${FRONTMATTER_KEY}: parsed`,"","---", "==⚠ 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 EMPTY_MESSAGE = "Hit enter to create a new drawing";
|
||||||
export const TEXT_DISPLAY_PARSED_ICON_NAME = "quote-glyph";
|
export const TEXT_DISPLAY_PARSED_ICON_NAME = "quote-glyph";
|
||||||
export const TEXT_DISPLAY_RAW_ICON_NAME = "presentation";
|
export const TEXT_DISPLAY_RAW_ICON_NAME = "presentation";
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ export default {
|
|||||||
SAVE_AS_SVG: "Save as SVG into Vault (CTRL/META+CLICK to export)",
|
SAVE_AS_SVG: "Save as SVG into Vault (CTRL/META+CLICK to export)",
|
||||||
OPEN_LINK: "Open selected text as link\n(SHIFT+CLICK to open in a new pane)",
|
OPEN_LINK: "Open selected text as link\n(SHIFT+CLICK to open in a new pane)",
|
||||||
EXPORT_EXCALIDRAW: "Export to an .Excalidraw file",
|
EXPORT_EXCALIDRAW: "Export to an .Excalidraw file",
|
||||||
LINK_BUTTON_CLICK_NO_TEXT: 'Select a Text Element containing an internal or external link.\n'+
|
LINK_BUTTON_CLICK_NO_TEXT: 'Select a an ImageElement, or select a TextElement that contains an internal or external link.\n'+
|
||||||
'SHIFT CLICK this button to open the link in a new pane.\n'+
|
'SHIFT CLICK this button to open the link in a new pane.\n'+
|
||||||
'CTRL/META CLICK the Text Element on the canvas has the same effect!',
|
'CTRL/META CLICK the Image or TextElement on the canvas has the same effect!',
|
||||||
TEXT_ELEMENT_EMPTY: "Text Element is empty, or [[valid-link|alias]] or [alias](valid-link) is not found",
|
TEXT_ELEMENT_EMPTY: "No ImageElement is selected or TextElement is empty, or [[valid-link|alias]] or [alias](valid-link) is not found",
|
||||||
FILENAME_INVALID_CHARS: 'File name cannot contain any of the following characters: * " \\ < > : | ?',
|
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.",
|
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)",
|
FORCE_SAVE: "Force-save to update transclusions in adjacent panes.\n(Please note, that autosave is always on)",
|
||||||
@@ -44,6 +44,8 @@ export default {
|
|||||||
NOFILE: "Excalidraw (no file)",
|
NOFILE: "Excalidraw (no file)",
|
||||||
COMPATIBILITY_MODE: "*.excalidraw file opened in compatibility mode. Convert to new format for full plugin functionality.",
|
COMPATIBILITY_MODE: "*.excalidraw file opened in compatibility mode. Convert to new format for full plugin functionality.",
|
||||||
CONVERT_FILE: "Convert to new format",
|
CONVERT_FILE: "Convert to new format",
|
||||||
|
DRAWING_CONTAINS_IMAGE: "Warning! The drawing contains image elements. Depending on the number and size of the images, " +
|
||||||
|
"loading Markdown View may take a while. Please be patient. ",
|
||||||
|
|
||||||
//settings.ts
|
//settings.ts
|
||||||
FOLDER_NAME: "Excalidraw folder",
|
FOLDER_NAME: "Excalidraw folder",
|
||||||
@@ -69,6 +71,12 @@ export default {
|
|||||||
FILENAME_PREFIX_DESC: "The first part of the filename",
|
FILENAME_PREFIX_DESC: "The first part of the filename",
|
||||||
FILENAME_DATE_NAME: "Filename date",
|
FILENAME_DATE_NAME: "Filename date",
|
||||||
FILENAME_DATE_DESC: "The second part of the filename",
|
FILENAME_DATE_DESC: "The second part of the filename",
|
||||||
|
DISPLAY_HEAD: "Display",
|
||||||
|
MATCH_THEME_NAME: "New drawing to match Obsidian theme",
|
||||||
|
MATCH_THEME_DESC: "If theme is dark, new drawing will be created in dark mode. This does not apply when you use a template for new drawings. " +
|
||||||
|
"Also this will not effect when you open an existing drawing. Those will follow the theme of the template/drawing respectively.",
|
||||||
|
ZOOM_TO_FIT_NAME: "Zoom to fit on view resize",
|
||||||
|
ZOOM_TO_FIT_DESC: "Zoom to fit drawing when the pane is resized",
|
||||||
LINKS_HEAD: "Links and transclusion",
|
LINKS_HEAD: "Links and transclusion",
|
||||||
LINKS_DESC: "CTRL/META + CLICK on Text Elements to open them as links. " +
|
LINKS_DESC: "CTRL/META + CLICK on Text Elements to open them as links. " +
|
||||||
"If the selected text has more than one [[valid Obsidian links]], only the first will be opened. " +
|
"If the selected text has more than one [[valid Obsidian links]], only the first will be opened. " +
|
||||||
@@ -76,6 +84,11 @@ export default {
|
|||||||
"the plugin will open it in a browser. " +
|
"the plugin will open it in a browser. " +
|
||||||
"When Obsidian files change, the matching [[link]] in your drawings will also change. " +
|
"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]].",
|
"If you don't want text accidentally changing in your drawings use [[links|with aliases]].",
|
||||||
|
ADJACENT_PANE_NAME: "Open in adjacent pane",
|
||||||
|
ADJACENT_PANE_DESC: "When CTRL+SHIFT clicking a link in Excalidraw by default the plugin will open the link in a new pane. " +
|
||||||
|
"Turning this setting on, Excalidraw will first look for an existing adjacent pane, and try to open the link there. " +
|
||||||
|
"Excalidraw will first look too the right, then to the left, then down, then up. If no pane is found, Excalidraw will open " +
|
||||||
|
"a new pane.",
|
||||||
LINK_BRACKETS_NAME: "Show [[brackets]] around links",
|
LINK_BRACKETS_NAME: "Show [[brackets]] around links",
|
||||||
LINK_BRACKETS_DESC: "In PREVIEW 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 +
|
"You can override this setting for a specific drawing by adding '" + FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS +
|
||||||
@@ -94,7 +107,10 @@ export default {
|
|||||||
TRANSCLUSION_WRAP_NAME: "Overflow wrap behavior of transcluded text",
|
TRANSCLUSION_WRAP_NAME: "Overflow wrap behavior of transcluded text",
|
||||||
TRANSCLUSION_WRAP_DESC: "Number specifies the character count where the text should be wrapped. " +
|
TRANSCLUSION_WRAP_DESC: "Number specifies the character count where the text should be wrapped. " +
|
||||||
"Set the text wrapping behavior of transcluded text. Turn this ON to force-wrap " +
|
"Set the text wrapping behavior of transcluded text. Turn this ON to force-wrap " +
|
||||||
"text (i.e. no overflow), or OFF to soft-warp text (at the nearest whitespace).",
|
"text (i.e. no overflow), or OFF to soft-wrap text (at the nearest whitespace).",
|
||||||
|
PAGE_TRANSCLUSION_CHARCOUNT_NAME: "Page transclusion max char count",
|
||||||
|
PAGE_TRANSCLUSION_CHARCOUNT_DESC: "The maximum number of characters to display from the page when transcluding an entire page with the "+
|
||||||
|
"![[markdown page]] format.",
|
||||||
EMBED_HEAD: "Embed & Export",
|
EMBED_HEAD: "Embed & Export",
|
||||||
EMBED_PREVIEW_SVG_NAME: "Display SVG in markdown preview",
|
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_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.",
|
||||||
|
|||||||
79
src/main.ts
79
src/main.ts
@@ -15,9 +15,7 @@ import {
|
|||||||
MarkdownRenderer,
|
MarkdownRenderer,
|
||||||
ViewState,
|
ViewState,
|
||||||
Notice,
|
Notice,
|
||||||
request,
|
|
||||||
} from "obsidian";
|
} from "obsidian";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BLANK_DRAWING,
|
BLANK_DRAWING,
|
||||||
VIEW_TYPE_EXCALIDRAW,
|
VIEW_TYPE_EXCALIDRAW,
|
||||||
@@ -33,10 +31,11 @@ import {
|
|||||||
FRONTMATTER_KEY,
|
FRONTMATTER_KEY,
|
||||||
FRONTMATTER,
|
FRONTMATTER,
|
||||||
JSON_parse,
|
JSON_parse,
|
||||||
nanoid
|
nanoid,
|
||||||
|
DARK_BLANK_DRAWING
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import ExcalidrawView, {ExportSettings, TextMode} from "./ExcalidrawView";
|
import ExcalidrawView, {ExportSettings, TextMode} from "./ExcalidrawView";
|
||||||
import {getJSON} from "./ExcalidrawData";
|
import {getJSON, getSVGString} from "./ExcalidrawData";
|
||||||
import {
|
import {
|
||||||
ExcalidrawSettings,
|
ExcalidrawSettings,
|
||||||
DEFAULT_SETTINGS,
|
DEFAULT_SETTINGS,
|
||||||
@@ -57,16 +56,12 @@ import { Prompt } from "./Prompt";
|
|||||||
import { around } from "monkey-around";
|
import { around } from "monkey-around";
|
||||||
import { t } from "./lang/helpers";
|
import { t } from "./lang/helpers";
|
||||||
import { MigrationPrompt } from "./MigrationPrompt";
|
import { MigrationPrompt } from "./MigrationPrompt";
|
||||||
import { checkAndCreateFolder, download, getIMGPathFromExcalidrawFile, getNewUniqueFilepath, splitFolderAndFilename } from "./Utils";
|
import { checkAndCreateFolder, download, embedFontsInSVG, generateSVGString, getAttachmentsFolderAndFilePath, getIMGPathFromExcalidrawFile, getNewUniqueFilepath, getPNG, getSVG, splitFolderAndFilename, svgToBase64 } from "./Utils";
|
||||||
import { tsExpressionWithTypeArguments } from "@babel/types";
|
|
||||||
|
|
||||||
declare module "obsidian" {
|
declare module "obsidian" {
|
||||||
interface App {
|
interface App {
|
||||||
isMobile():boolean;
|
isMobile():boolean;
|
||||||
}
|
}
|
||||||
interface Vault {
|
|
||||||
getConfig(option:"attachmentFolderPath"): string;
|
|
||||||
}
|
|
||||||
interface Workspace {
|
interface Workspace {
|
||||||
on(name: 'hover-link', callback: (e:MouseEvent) => any, ctx?: any): EventRef;
|
on(name: 'hover-link', callback: (e:MouseEvent) => any, ctx?: any): EventRef;
|
||||||
}
|
}
|
||||||
@@ -76,7 +71,6 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
public excalidrawFileModes: { [file: string]: string } = {};
|
public excalidrawFileModes: { [file: string]: string } = {};
|
||||||
private _loaded: boolean = false;
|
private _loaded: boolean = false;
|
||||||
public settings: ExcalidrawSettings;
|
public settings: ExcalidrawSettings;
|
||||||
//public stencilLibrary: any = null;
|
|
||||||
private openDialog: OpenFileDialog;
|
private openDialog: OpenFileDialog;
|
||||||
private insertLinkDialog: InsertLinkDialog;
|
private insertLinkDialog: InsertLinkDialog;
|
||||||
private activeExcalidrawView: ExcalidrawView = null;
|
private activeExcalidrawView: ExcalidrawView = null;
|
||||||
@@ -226,24 +220,39 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
if(imgAttributes.fheight) img.setAttribute("height",imgAttributes.fheight);
|
if(imgAttributes.fheight) img.setAttribute("height",imgAttributes.fheight);
|
||||||
img.addClass(imgAttributes.style);
|
img.addClass(imgAttributes.style);
|
||||||
|
|
||||||
|
const [scene,pos] = getJSON(content);
|
||||||
|
const svgSnapshot = getSVGString(content.substr(pos+scene.length));
|
||||||
|
|
||||||
if(!this.settings.displaySVGInPreview) {
|
//Removed in 1.4.0 when implementing ImageElement. Key reason for removing this
|
||||||
|
//is to use SVG snapshot in file, to avoid resource intensive process to generating PNG
|
||||||
|
//due to the need to load excalidraw plus all linked images
|
||||||
|
/* if(!this.settings.displaySVGInPreview) {
|
||||||
const width = parseInt(imgAttributes.fwidth);
|
const width = parseInt(imgAttributes.fwidth);
|
||||||
let scale = 1;
|
let scale = 1;
|
||||||
if(width>=800) scale = 2;
|
if(width>=800) scale = 2;
|
||||||
if(width>=1600) scale = 3;
|
if(width>=1600) scale = 3;
|
||||||
if(width>=2400) scale = 4;
|
if(width>=2400) scale = 4;
|
||||||
const png = await ExcalidrawView.getPNG(JSON_parse(getJSON(content)[0]),exportSettings, scale);
|
const png = await getPNG(JSON_parse(scene),exportSettings, scale);
|
||||||
if(!png) return null;
|
if(!png) return null;
|
||||||
img.src = URL.createObjectURL(png);
|
img.src = URL.createObjectURL(png);
|
||||||
return img;
|
return img;
|
||||||
|
}*/
|
||||||
|
let svg:SVGSVGElement = null;
|
||||||
|
if(svgSnapshot) {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.innerHTML = svgSnapshot;
|
||||||
|
const firstChild = el.firstChild;
|
||||||
|
if(firstChild instanceof SVGSVGElement) {
|
||||||
|
svg=firstChild;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
svg = await getSVG(JSON_parse(scene),exportSettings);
|
||||||
}
|
}
|
||||||
let svg = await ExcalidrawView.getSVG(JSON_parse(getJSON(content)[0]),exportSettings);
|
|
||||||
if(!svg) return null;
|
if(!svg) return null;
|
||||||
svg = ExcalidrawView.embedFontsInSVG(svg);
|
svg = embedFontsInSVG(svg);
|
||||||
svg.removeAttribute('width');
|
svg.removeAttribute('width');
|
||||||
svg.removeAttribute('height');
|
svg.removeAttribute('height');
|
||||||
img.setAttribute("src","data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.outerHTML.replaceAll(" "," ")))));
|
img.setAttribute("src",svgToBase64(svg.outerHTML));
|
||||||
return img;
|
return img;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,21 +588,11 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
const insertDrawingToDoc = async (inNewPane:boolean) => {
|
const insertDrawingToDoc = async (inNewPane:boolean) => {
|
||||||
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||||
if(!activeView) return;
|
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)
|
const filename = activeView.file.basename + "_" + window.moment().format(this.settings.drawingFilenameDateTime)
|
||||||
+ (this.settings.compatibilityMode ? '.excalidraw' : '.excalidraw.md');
|
+ (this.settings.compatibilityMode ? '.excalidraw' : '.excalidraw.md');
|
||||||
this.embedDrawing(normalizePath(folder + "/" + filename));
|
const [folder, filepath] = await getAttachmentsFolderAndFilePath(this.app,activeView.file.path,filename);
|
||||||
this.createDrawing(filename, inNewPane,folder==""?null:folder);
|
this.embedDrawing(filepath);
|
||||||
|
this.createDrawing(filename, inNewPane, folder===""?null:folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
@@ -789,7 +788,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
const filename = file.name.substr(0,file.name.lastIndexOf(".excalidraw")) + (replaceExtension ? ".md" : ".excalidraw.md");
|
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))));
|
const fname = getNewUniqueFilepath(this.app.vault,filename,normalizePath(file.path.substr(0,file.path.lastIndexOf(file.name))));
|
||||||
console.log(fname);
|
console.log(fname);
|
||||||
const result = await this.app.vault.create(fname,FRONTMATTER + this.exportSceneToMD(data));
|
const result = await this.app.vault.create(fname,FRONTMATTER + await this.exportSceneToMD(data));
|
||||||
if (this.settings.keepInSync) {
|
if (this.settings.keepInSync) {
|
||||||
['.svg','.png'].forEach( (ext:string)=>{
|
['.svg','.png'].forEach( (ext:string)=>{
|
||||||
const oldIMGpath = file.path.substring(0,file.path.lastIndexOf(".excalidraw")) + ext;
|
const oldIMGpath = file.path.substring(0,file.path.lastIndexOf(".excalidraw")) + ext;
|
||||||
@@ -1105,16 +1104,24 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.settings.compatibilityMode) {
|
if (this.settings.compatibilityMode) {
|
||||||
return BLANK_DRAWING;
|
return this.settings.matchTheme && document.body.classList.contains("theme-dark") ? DARK_BLANK_DRAWING : BLANK_DRAWING;
|
||||||
}
|
}
|
||||||
return FRONTMATTER + '\n' + this.getMarkdownDrawingSection(BLANK_DRAWING);
|
const blank = this.settings.matchTheme && document.body.classList.contains("theme-dark") ? DARK_BLANK_DRAWING : BLANK_DRAWING;
|
||||||
|
return FRONTMATTER + '\n' + this.getMarkdownDrawingSection(blank,'<SVG></SVG>');
|
||||||
}
|
}
|
||||||
|
|
||||||
public getMarkdownDrawingSection(jsonString: string) {
|
public getMarkdownDrawingSection(jsonString: string,svgString: string) {
|
||||||
return '%%\n# Drawing\n'
|
return '%%\n# Drawing\n'
|
||||||
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'json\n'
|
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'json\n'
|
||||||
+ jsonString + '\n'
|
+ jsonString + '\n'
|
||||||
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96) + '%%';
|
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)
|
||||||
|
+ (svgString ?
|
||||||
|
'\n\n# SVG snapshot\n'
|
||||||
|
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'html\n'
|
||||||
|
+ svgString + '\n'
|
||||||
|
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)
|
||||||
|
: '')
|
||||||
|
+ '\n%%';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1122,9 +1129,10 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
* @param {string} data - Excalidraw scene JSON string
|
* @param {string} data - Excalidraw scene JSON string
|
||||||
* @returns {string} - Text starting with the "# Text Elements" header and followed by each "## id-value" and text
|
* @returns {string} - Text starting with the "# Text Elements" header and followed by each "## id-value" and text
|
||||||
*/
|
*/
|
||||||
public exportSceneToMD(data:string): string {
|
public async exportSceneToMD(data:string): Promise<string> {
|
||||||
if(!data) return "";
|
if(!data) return "";
|
||||||
const excalidrawData = JSON_parse(data);
|
const excalidrawData = JSON_parse(data);
|
||||||
|
const svgString = await generateSVGString(excalidrawData,this.settings);
|
||||||
const textElements = excalidrawData.elements?.filter((el:any)=> el.type=="text")
|
const textElements = excalidrawData.elements?.filter((el:any)=> el.type=="text")
|
||||||
let outString = '# Text Elements\n';
|
let outString = '# Text Elements\n';
|
||||||
let id:string;
|
let id:string;
|
||||||
@@ -1139,7 +1147,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
outString += te.text+' ^'+id+'\n\n';
|
outString += te.text+' ^'+id+'\n\n';
|
||||||
}
|
}
|
||||||
return outString + this.getMarkdownDrawingSection(data);
|
return outString + this.getMarkdownDrawingSection(JSON.stringify(JSON_parse(data),null,"\t"),svgString);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createDrawing(filename: string, onNewPane: boolean, foldername?: string, initData?:string):Promise<string> {
|
public async createDrawing(filename: string, onNewPane: boolean, foldername?: string, initData?:string):Promise<string> {
|
||||||
@@ -1183,3 +1191,4 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,17 @@ export interface ExcalidrawSettings {
|
|||||||
templateFilePath: string,
|
templateFilePath: string,
|
||||||
drawingFilenamePrefix: string,
|
drawingFilenamePrefix: string,
|
||||||
drawingFilenameDateTime: string,
|
drawingFilenameDateTime: string,
|
||||||
displaySVGInPreview: boolean,
|
//displaySVGInPreview: boolean,
|
||||||
width: string,
|
width: string,
|
||||||
|
matchTheme: boolean,
|
||||||
|
zoomToFitOnResize: boolean,
|
||||||
|
openInAdjacentPane: boolean,
|
||||||
showLinkBrackets: boolean,
|
showLinkBrackets: boolean,
|
||||||
linkPrefix: string,
|
linkPrefix: string,
|
||||||
urlPrefix: string,
|
urlPrefix: string,
|
||||||
allowCtrlClick: boolean, //if disabled only the link button in the view header will open links
|
allowCtrlClick: boolean, //if disabled only the link button in the view header will open links
|
||||||
forceWrap: boolean,
|
forceWrap: boolean,
|
||||||
|
pageTransclusionCharLimit: number,
|
||||||
pngExportScale: number,
|
pngExportScale: number,
|
||||||
exportWithTheme: boolean,
|
exportWithTheme: boolean,
|
||||||
exportWithBackground: boolean,
|
exportWithBackground: boolean,
|
||||||
@@ -45,13 +49,17 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
|||||||
templateFilePath: 'Excalidraw/Template.excalidraw',
|
templateFilePath: 'Excalidraw/Template.excalidraw',
|
||||||
drawingFilenamePrefix: 'Drawing ',
|
drawingFilenamePrefix: 'Drawing ',
|
||||||
drawingFilenameDateTime: 'YYYY-MM-DD HH.mm.ss',
|
drawingFilenameDateTime: 'YYYY-MM-DD HH.mm.ss',
|
||||||
displaySVGInPreview: true,
|
//displaySVGInPreview: true,
|
||||||
width: '400',
|
width: '400',
|
||||||
|
matchTheme: false,
|
||||||
|
zoomToFitOnResize: true,
|
||||||
linkPrefix: "📍",
|
linkPrefix: "📍",
|
||||||
urlPrefix: "🌐",
|
urlPrefix: "🌐",
|
||||||
|
openInAdjacentPane: false,
|
||||||
showLinkBrackets: true,
|
showLinkBrackets: true,
|
||||||
allowCtrlClick: true,
|
allowCtrlClick: true,
|
||||||
forceWrap: false,
|
forceWrap: false,
|
||||||
|
pageTransclusionCharLimit: 200,
|
||||||
pngExportScale: 1,
|
pngExportScale: 1,
|
||||||
exportWithTheme: true,
|
exportWithTheme: true,
|
||||||
exportWithBackground: true,
|
exportWithBackground: true,
|
||||||
@@ -184,10 +192,42 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
this.applySettingsUpdate();
|
this.applySettingsUpdate();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
this.containerEl.createEl('h1', {text: t("DISPLAY_HEAD")});
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName(t("MATCH_THEME_NAME"))
|
||||||
|
.setDesc(t("MATCH_THEME_DESC"))
|
||||||
|
.addToggle(toggle => toggle
|
||||||
|
.setValue(this.plugin.settings.matchTheme)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.matchTheme = value;
|
||||||
|
this.applySettingsUpdate();
|
||||||
|
}));
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName(t("ZOOM_TO_FIT_NAME"))
|
||||||
|
.setDesc(t("ZOOM_TO_FIT_DESC"))
|
||||||
|
.addToggle(toggle => toggle
|
||||||
|
.setValue(this.plugin.settings.zoomToFitOnResize)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.zoomToFitOnResize = value;
|
||||||
|
this.applySettingsUpdate();
|
||||||
|
}));
|
||||||
|
|
||||||
this.containerEl.createEl('h1', {text: t("LINKS_HEAD")});
|
this.containerEl.createEl('h1', {text: t("LINKS_HEAD")});
|
||||||
this.containerEl.createEl('p',{
|
this.containerEl.createEl('p',{
|
||||||
text: t("LINKS_DESC")});
|
text: t("LINKS_DESC")});
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName(t("ADJACENT_PANE_NAME"))
|
||||||
|
.setDesc(t("ADJACENT_PANE_DESC"))
|
||||||
|
.addToggle(toggle => toggle
|
||||||
|
.setValue(this.plugin.settings.openInAdjacentPane)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.openInAdjacentPane = value;
|
||||||
|
this.applySettingsUpdate(true);
|
||||||
|
}));
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName(t("LINK_BRACKETS_NAME"))
|
.setName(t("LINK_BRACKETS_NAME"))
|
||||||
.setDesc(t("LINK_BRACKETS_DESC"))
|
.setDesc(t("LINK_BRACKETS_DESC"))
|
||||||
@@ -242,10 +282,33 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
}));
|
}));
|
||||||
s.descEl.innerHTML="<code>![[doc#^ref]]{number}</code> "+t("TRANSCLUSION_WRAP_DESC");
|
s.descEl.innerHTML="<code>![[doc#^ref]]{number}</code> "+t("TRANSCLUSION_WRAP_DESC");
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName(t("PAGE_TRANSCLUSION_CHARCOUNT_NAME"))
|
||||||
|
.setDesc(t("PAGE_TRANSCLUSION_CHARCOUNT_DESC"))
|
||||||
|
.addText(text => text
|
||||||
|
.setPlaceholder('Enter a number')
|
||||||
|
.setValue(this.plugin.settings.pageTransclusionCharLimit.toString())
|
||||||
|
.onChange(async (value) => {
|
||||||
|
const intVal = parseInt(value);
|
||||||
|
if(isNaN(intVal) && value!=="") {
|
||||||
|
text.setValue(this.plugin.settings.pageTransclusionCharLimit.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.requestEmbedUpdate = true;
|
||||||
|
if(value === "") {
|
||||||
|
this.plugin.settings.pageTransclusionCharLimit = 10;
|
||||||
|
this.applySettingsUpdate(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.plugin.settings.pageTransclusionCharLimit = intVal;
|
||||||
|
text.setValue(this.plugin.settings.pageTransclusionCharLimit.toString());
|
||||||
|
this.applySettingsUpdate(true);
|
||||||
|
}));
|
||||||
|
|
||||||
this.containerEl.createEl('h1', {text: t("EMBED_HEAD")});
|
this.containerEl.createEl('h1', {text: t("EMBED_HEAD")});
|
||||||
|
|
||||||
|
//Removed in 1.4.0 when implementing ImageElement.
|
||||||
new Setting(containerEl)
|
/* new Setting(containerEl)
|
||||||
.setName(t("EMBED_PREVIEW_SVG_NAME"))
|
.setName(t("EMBED_PREVIEW_SVG_NAME"))
|
||||||
.setDesc(t("EMBED_PREVIEW_SVG_DESC"))
|
.setDesc(t("EMBED_PREVIEW_SVG_DESC"))
|
||||||
.addToggle(toggle => toggle
|
.addToggle(toggle => toggle
|
||||||
@@ -253,8 +316,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.displaySVGInPreview = value;
|
this.plugin.settings.displaySVGInPreview = value;
|
||||||
this.applySettingsUpdate();
|
this.applySettingsUpdate();
|
||||||
}));
|
}));*/
|
||||||
|
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName(t("EMBED_WIDTH_NAME"))
|
.setName(t("EMBED_WIDTH_NAME"))
|
||||||
@@ -267,6 +329,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
this.applySettingsUpdate();
|
this.applySettingsUpdate();
|
||||||
this.requestEmbedUpdate = true;
|
this.requestEmbedUpdate = true;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let dropdown: DropdownComponent;
|
let dropdown: DropdownComponent;
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"1.3.12": "0.11.13"
|
"1.3.20": "0.11.13"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user