mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
38 Commits
ImageEleme
...
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 |
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-excalidraw-plugin",
|
"id": "obsidian-excalidraw-plugin",
|
||||||
"name": "Excalidraw",
|
"name": "Excalidraw",
|
||||||
"version": "1.3.16",
|
"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",
|
||||||
|
|||||||
24
package.json
24
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-image-support-3",
|
"@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.16",
|
"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,8 +9,8 @@ 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,
|
||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
VIEW_TYPE_EXCALIDRAW,
|
VIEW_TYPE_EXCALIDRAW,
|
||||||
MAX_IMAGE_SIZE
|
MAX_IMAGE_SIZE
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import { getObsidianImage, wrapText } from "./Utils";
|
import { embedFontsInSVG, generateSVGString, getObsidianImage, getPNG, getSVG, loadSceneFiles, scaleLoadedImage, svgToBase64, wrapText } from "./Utils";
|
||||||
import { AppState } from "@zsviczian/excalidraw/types/types";
|
import { AppState } from "@zsviczian/excalidraw/types/types";
|
||||||
|
|
||||||
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
|
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
|
||||||
@@ -73,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;
|
||||||
@@ -283,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;
|
||||||
@@ -301,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,
|
||||||
@@ -521,10 +531,12 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
|||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
const image = await getObsidianImage(this.plugin.app,imageFile);
|
const image = await getObsidianImage(this.plugin.app,imageFile);
|
||||||
if(!image) return null;
|
if(!image) return null;
|
||||||
this.imagesDict[image.imageId] = {
|
this.imagesDict[image.fileId] = {
|
||||||
type:"image",
|
mimeType: image.mimeType,
|
||||||
id: image.imageId,
|
id: image.fileId,
|
||||||
dataURL: image.dataURL
|
dataURL: image.dataURL,
|
||||||
|
created: image.created,
|
||||||
|
file: imageFile.path
|
||||||
}
|
}
|
||||||
if (Math.max(image.size.width,image.size.height) > MAX_IMAGE_SIZE) {
|
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);
|
const scale = MAX_IMAGE_SIZE/Math.max(image.size.width,image.size.height);
|
||||||
@@ -532,7 +544,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
|||||||
image.size.height = scale*image.size.height;
|
image.size.height = scale*image.size.height;
|
||||||
}
|
}
|
||||||
this.elementsDict[id] = boxedElement(id,"image",topX,topY,image.size.width,image.size.height);
|
this.elementsDict[id] = boxedElement(id,"image",topX,topY,image.size.width,image.size.height);
|
||||||
this.elementsDict[id].imageId = image.imageId;
|
this.elementsDict[id].fileId = image.fileId;
|
||||||
this.elementsDict[id].scale = [1,1];
|
this.elementsDict[id].scale = [1,1];
|
||||||
return id;
|
return id;
|
||||||
},
|
},
|
||||||
@@ -621,7 +633,7 @@ 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[] {
|
getViewElements ():ExcalidrawElement[] {
|
||||||
if (!this.targetView || !this.targetView?._loaded) {
|
if (!this.targetView || !this.targetView?._loaded) {
|
||||||
@@ -804,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 {
|
||||||
@@ -25,7 +28,10 @@ declare module "obsidian" {
|
|||||||
export const REGEX_LINK = {
|
export const REGEX_LINK = {
|
||||||
//![[link|alias]] [alias](link){num}
|
//![[link|alias]] [alias](link){num}
|
||||||
// 1 2 3 4 5 6 7 8 9
|
// 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,10 +168,13 @@ 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);
|
||||||
@@ -152,6 +187,15 @@ export class ExcalidrawData {
|
|||||||
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();
|
||||||
@@ -161,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;
|
||||||
@@ -347,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;
|
||||||
@@ -387,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;
|
||||||
@@ -398,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;
|
||||||
@@ -432,13 +481,58 @@ 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();
|
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 || this.findNewTextElementsInScene();
|
return result || this.findNewTextElementsInScene();
|
||||||
}
|
}
|
||||||
@@ -520,6 +614,4 @@ export class ExcalidrawData {
|
|||||||
return showLinkBrackets != this.showLinkBrackets;
|
return showLinkBrackets != this.showLinkBrackets;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -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 {
|
||||||
@@ -32,12 +33,13 @@ import {
|
|||||||
IMAGE_TYPES
|
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 { ClipboardData } from "@zsviczian/excalidraw/types/clipboard";
|
||||||
|
import { ifStatement } from "@babel/types";
|
||||||
|
|
||||||
declare let window: ExcalidrawAutomate;
|
declare let window: ExcalidrawAutomate;
|
||||||
|
|
||||||
@@ -62,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;
|
||||||
@@ -80,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;
|
||||||
|
|
||||||
@@ -97,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) {
|
||||||
@@ -113,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;
|
||||||
@@ -147,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());
|
||||||
@@ -158,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();
|
||||||
}
|
}
|
||||||
@@ -176,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);
|
||||||
@@ -193,84 +188,102 @@ 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;
|
||||||
if(ev.shiftKey && document.fullscreenElement === this.contentEl) {
|
if(ev.shiftKey && document.fullscreenElement === this.contentEl) {
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -326,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,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);
|
||||||
@@ -368,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) {
|
||||||
@@ -377,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) {
|
||||||
@@ -415,27 +432,46 @@ 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
|
||||||
@@ -461,7 +497,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
|
|
||||||
setMarkdownView() {
|
setMarkdownView() {
|
||||||
if(this.excalidrawRef) {
|
if(this.excalidrawRef) {
|
||||||
const el = this.excalidrawRef.current.getSceneElements();
|
const el = this.excalidrawAPI.getSceneElements();
|
||||||
if(el.filter((e:any)=>e.type==="image").length>0) {
|
if(el.filter((e:any)=>e.type==="image").length>0) {
|
||||||
new Notice(t("DRAWING_CONTAINS_IMAGE"),6000);
|
new Notice(t("DRAWING_CONTAINS_IMAGE"),6000);
|
||||||
}
|
}
|
||||||
@@ -497,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 {
|
||||||
@@ -528,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);
|
||||||
@@ -553,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()
|
||||||
@@ -578,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;
|
||||||
|
|
||||||
@@ -606,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
|
||||||
@@ -631,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;
|
||||||
@@ -646,7 +737,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
const id:string = window.ExcalidrawAutomate.addText(currentPosition.x, currentPosition.y, text);
|
const id:string = window.ExcalidrawAutomate.addText(currentPosition.x, currentPosition.y, text);
|
||||||
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, images:any):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;
|
||||||
|
|
||||||
@@ -659,27 +750,28 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
|
const el: ExcalidrawElement[] = this.excalidrawAPI.getSceneElements();
|
||||||
let st: AppState = excalidrawRef.current.getAppState();
|
let st: AppState = this.excalidrawAPI.getAppState();
|
||||||
if(!st.files) {
|
|
||||||
st.files = {};
|
|
||||||
}
|
|
||||||
if(images) {
|
|
||||||
Object.keys(images).forEach((k)=>{
|
|
||||||
st.files[k]={
|
|
||||||
type:images[k].type,
|
|
||||||
id: images[k].id,
|
|
||||||
dataURL: images[k].dataURL
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//merge appstate.files with files
|
|
||||||
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;
|
||||||
};
|
};
|
||||||
@@ -688,15 +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(st.files) {
|
if(files) {
|
||||||
const imgIds = el.filter((e)=>e.type=="image").map((e:any)=>e.imageId);
|
const imgIds = el.filter((e)=>e.type=="image").map((e:any)=>e.fileId);
|
||||||
const toDelete = Object.keys(st.files).filter((k)=>!imgIds.contains(k));
|
const toDelete = Object.keys(files).filter((k)=>!imgIds.contains(k));
|
||||||
toDelete.forEach((k)=>delete st.files[k]);
|
toDelete.forEach((k)=>delete files[k]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "excalidraw",
|
type: "excalidraw",
|
||||||
version: 2,
|
version: 2,
|
||||||
@@ -720,31 +813,61 @@ 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: st.files??{},
|
},
|
||||||
}
|
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};
|
||||||
let hoverPreviewTarget:EventTarget = null;
|
let hoverPreviewTarget:EventTarget = null;
|
||||||
@@ -775,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",
|
||||||
{
|
{
|
||||||
@@ -783,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;
|
||||||
@@ -802,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];
|
||||||
|
|
||||||
@@ -812,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,
|
||||||
@@ -838,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();
|
||||||
@@ -877,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;
|
||||||
|
|
||||||
@@ -905,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);
|
||||||
@@ -940,7 +1077,7 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
return true;
|
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;
|
||||||
@@ -975,7 +1112,9 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
switch(draggable?.type) {
|
switch(draggable?.type) {
|
||||||
case "file":
|
case "file":
|
||||||
if (!onDropHook("file",[draggable.file],null)) {
|
if (!onDropHook("file",[draggable.file],null)) {
|
||||||
if((event.ctrlKey || event.metaKey) && IMAGE_TYPES.contains(draggable.file.extension)) {
|
if((event.ctrlKey || event.metaKey)
|
||||||
|
&& (IMAGE_TYPES.contains(draggable.file.extension)
|
||||||
|
|| this.plugin.isExcalidrawFile(draggable.file))) {
|
||||||
const f = draggable.file;
|
const f = draggable.file;
|
||||||
const topX = currentPosition.x;
|
const topX = currentPosition.x;
|
||||||
const topY = currentPosition.y;
|
const topY = currentPosition.y;
|
||||||
@@ -1038,14 +1177,14 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
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;
|
||||||
@@ -1063,12 +1202,15 @@ 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
|
||||||
@@ -1077,37 +1219,4 @@ export default class ExcalidrawView extends TextFileView {
|
|||||||
current.zoomToFit(elements,2,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) => {
|
||||||
|
|||||||
264
src/Utils.ts
264
src/Utils.ts
@@ -1,9 +1,28 @@
|
|||||||
import { App, normalizePath, TAbstractFile, TFile, 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 { nanoid } from "nanoid";
|
||||||
import { IMAGE_TYPES } from "./constants";
|
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
|
||||||
@@ -105,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 },
|
||||||
{
|
{
|
||||||
@@ -127,32 +178,69 @@ export const viewportCoordsToSceneCoords = (
|
|||||||
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)
|
export const getObsidianImage = async (app: App, file: TFile)
|
||||||
:Promise<{
|
:Promise<{
|
||||||
imageId: string,
|
mimeType: MimeType,
|
||||||
dataURL: string,
|
fileId: FileId,
|
||||||
|
dataURL: DataURL,
|
||||||
|
created: number,
|
||||||
size: {height: number, width: number},
|
size: {height: number, width: number},
|
||||||
}> => {
|
}> => {
|
||||||
if(!app || !file) return null;
|
if(!app || !file) return null;
|
||||||
if (!IMAGE_TYPES.contains(file.extension)) 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 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 {
|
return {
|
||||||
imageId: await generateIdFromFile(ab),
|
mimeType: mimeType,
|
||||||
dataURL: file.extension==="svg" ? await getSVGData(app,file) : await getDataURL(ab),
|
fileId: await generateIdFromFile(ab),
|
||||||
size: await getImageSize(app,file)
|
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<string> => {
|
|
||||||
|
const getSVGData = async (app: App, file: TFile): Promise<DataURL> => {
|
||||||
const svg = await app.vault.read(file);
|
const svg = await app.vault.read(file);
|
||||||
return "data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.replaceAll(" "," "))))
|
return svgToBase64(svg) as DataURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDataURL = async (file: ArrayBuffer): Promise<string> => {
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = () => {
|
reader.onload = () => {
|
||||||
const dataURL = reader.result as string;
|
const dataURL = reader.result as DataURL;
|
||||||
resolve(dataURL);
|
resolve(dataURL);
|
||||||
};
|
};
|
||||||
reader.onerror = (error) => reject(error);
|
reader.onerror = (error) => reject(error);
|
||||||
@@ -160,8 +248,8 @@ const getDataURL = async (file: ArrayBuffer): Promise<string> => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateIdFromFile = async (file: ArrayBuffer):Promise<string> => {
|
const generateIdFromFile = async (file: ArrayBuffer):Promise<FileId> => {
|
||||||
let id: string;
|
let id: FileId;
|
||||||
try {
|
try {
|
||||||
const hashBuffer = await window.crypto.subtle.digest(
|
const hashBuffer = await window.crypto.subtle.digest(
|
||||||
"SHA-1",
|
"SHA-1",
|
||||||
@@ -172,19 +260,157 @@ const generateIdFromFile = async (file: ArrayBuffer):Promise<string> => {
|
|||||||
Array.from(new Uint8Array(hashBuffer))
|
Array.from(new Uint8Array(hashBuffer))
|
||||||
// convert to hex string
|
// convert to hex string
|
||||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||||
.join("");
|
.join("") as FileId;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
id = nanoid(40);
|
id = nanoid(40) as FileId;
|
||||||
}
|
}
|
||||||
return id;
|
return id;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getImageSize = async (app: App, file:TFile):Promise<{height:number, width:number}> => {
|
const getImageSize = async (app: App, src:string):Promise<{height:number, width:number}> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let img = new Image()
|
let img = new Image()
|
||||||
img.onload = () => resolve({height: img.height, width:img.width});
|
img.onload = () => resolve({height: img.height, width:img.width});
|
||||||
img.onerror = reject;
|
img.onerror = reject;
|
||||||
img.src = app.vault.getResourcePath(file);
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ 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 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 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";
|
||||||
|
|||||||
@@ -1,162 +1,170 @@
|
|||||||
import { FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS, FRONTMATTER_KEY_CUSTOM_PREFIX, FRONTMATTER_KEY_CUSTOM_URL_PREFIX } from "src/constants";
|
import { FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS, FRONTMATTER_KEY_CUSTOM_PREFIX, FRONTMATTER_KEY_CUSTOM_URL_PREFIX } from "src/constants";
|
||||||
|
|
||||||
// English
|
// English
|
||||||
export default {
|
export default {
|
||||||
// main.ts
|
// main.ts
|
||||||
OPEN_AS_EXCALIDRAW: "Open as Excalidraw Drawing",
|
OPEN_AS_EXCALIDRAW: "Open as Excalidraw Drawing",
|
||||||
TOGGLE_MODE: "Toggle between Excalidraw and Markdown mode",
|
TOGGLE_MODE: "Toggle between Excalidraw and Markdown mode",
|
||||||
CONVERT_NOTE_TO_EXCALIDRAW: "Convert empty note to Excalidraw Drawing",
|
CONVERT_NOTE_TO_EXCALIDRAW: "Convert empty note to Excalidraw Drawing",
|
||||||
CONVERT_EXCALIDRAW: "Convert *.excalidraw to *.md files",
|
CONVERT_EXCALIDRAW: "Convert *.excalidraw to *.md files",
|
||||||
CREATE_NEW : "New Excalidraw drawing",
|
CREATE_NEW : "New Excalidraw drawing",
|
||||||
CONVERT_FILE_KEEP_EXT: "*.excalidraw => *.excalidraw.md",
|
CONVERT_FILE_KEEP_EXT: "*.excalidraw => *.excalidraw.md",
|
||||||
CONVERT_FILE_REPLACE_EXT: "*.excalidraw => *.md (Logseq compatibility)",
|
CONVERT_FILE_REPLACE_EXT: "*.excalidraw => *.md (Logseq compatibility)",
|
||||||
DOWNLOAD_LIBRARY: "Export stencil library as an *.excalidrawlib file",
|
DOWNLOAD_LIBRARY: "Export stencil library as an *.excalidrawlib file",
|
||||||
OPEN_EXISTING_NEW_PANE: "Open an existing drawing - IN A NEW PANE",
|
OPEN_EXISTING_NEW_PANE: "Open an existing drawing - IN A NEW PANE",
|
||||||
OPEN_EXISTING_ACTIVE_PANE: "Open an existing drawing - IN THE CURRENT ACTIVE PANE",
|
OPEN_EXISTING_ACTIVE_PANE: "Open an existing drawing - IN THE CURRENT ACTIVE PANE",
|
||||||
TRANSCLUDE: "Transclude (embed) a drawing",
|
TRANSCLUDE: "Transclude (embed) a drawing",
|
||||||
TRANSCLUDE_MOST_RECENT: "Transclude (embed) the most recently edited drawing",
|
TRANSCLUDE_MOST_RECENT: "Transclude (embed) the most recently edited drawing",
|
||||||
NEW_IN_NEW_PANE: "Create a new drawing - IN A NEW PANE",
|
NEW_IN_NEW_PANE: "Create a new drawing - IN A NEW PANE",
|
||||||
NEW_IN_ACTIVE_PANE: "Create a new drawing - IN THE CURRENT ACTIVE PANE",
|
NEW_IN_ACTIVE_PANE: "Create a new drawing - IN THE CURRENT ACTIVE PANE",
|
||||||
NEW_IN_NEW_PANE_EMBED: "Create a new drawing - IN A NEW PANE - and embed into active document",
|
NEW_IN_NEW_PANE_EMBED: "Create a new drawing - IN A NEW PANE - and embed into active document",
|
||||||
NEW_IN_ACTIVE_PANE_EMBED: "Create a new drawing - IN THE CURRENT ACTIVE PANE - and embed into active document",
|
NEW_IN_ACTIVE_PANE_EMBED: "Create a new drawing - IN THE CURRENT ACTIVE PANE - and embed into active document",
|
||||||
EXPORT_SVG: "Save as SVG next to the current file",
|
EXPORT_SVG: "Save as SVG next to the current file",
|
||||||
EXPORT_PNG: "Save as PNG next to the current file",
|
EXPORT_PNG: "Save as PNG next to the current file",
|
||||||
TOGGLE_LOCK: "Toggle Text Element edit RAW/PREVIEW",
|
TOGGLE_LOCK: "Toggle Text Element edit RAW/PREVIEW",
|
||||||
INSERT_LINK: "Insert link to file",
|
INSERT_LINK: "Insert link to file",
|
||||||
INSERT_LATEX: "Insert LaTeX-symbol (e.g. $\\theta$)",
|
INSERT_LATEX: "Insert LaTeX-symbol (e.g. $\\theta$)",
|
||||||
ENTER_LATEX: "Enter a valid LaTeX expression",
|
ENTER_LATEX: "Enter a valid LaTeX expression",
|
||||||
|
|
||||||
//ExcalidrawView.ts
|
//ExcalidrawView.ts
|
||||||
OPEN_AS_MD: "Open as Markdown",
|
OPEN_AS_MD: "Open as Markdown",
|
||||||
SAVE_AS_PNG: "Save as PNG into Vault (CTRL/META+CLICK to export)",
|
SAVE_AS_PNG: "Save as PNG into Vault (CTRL/META+CLICK to export)",
|
||||||
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)",
|
||||||
RAW: "Change to PREVIEW mode (only effects text-elements with links or transclusions)",
|
RAW: "Change to PREVIEW mode (only effects text-elements with links or transclusions)",
|
||||||
PARSED: "Change to RAW mode (only effects text-elements with links or transclusions)",
|
PARSED: "Change to RAW mode (only effects text-elements with links or transclusions)",
|
||||||
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, " +
|
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. ",
|
"loading Markdown View may take a while. Please be patient. ",
|
||||||
|
|
||||||
//settings.ts
|
//settings.ts
|
||||||
FOLDER_NAME: "Excalidraw folder",
|
FOLDER_NAME: "Excalidraw folder",
|
||||||
FOLDER_DESC: "Default location for new drawings. If empty, drawings will be created in the Vault root.",
|
FOLDER_DESC: "Default location for new drawings. If empty, drawings will be created in the Vault root.",
|
||||||
TEMPLATE_NAME: "Excalidraw template file",
|
TEMPLATE_NAME: "Excalidraw template file",
|
||||||
TEMPLATE_DESC: "Full filepath to the Excalidraw template. " +
|
TEMPLATE_DESC: "Full filepath to the Excalidraw template. " +
|
||||||
"E.g.: If your template is in the default Excalidraw folder and it's name is " +
|
"E.g.: If your template is in the default Excalidraw folder and it's name is " +
|
||||||
"Template.md, the setting would be: Excalidraw/Template.md (or just Excalidraw/Template - you may ommit the .md file extension" +
|
"Template.md, the setting would be: Excalidraw/Template.md (or just Excalidraw/Template - you may ommit the .md file extension" +
|
||||||
"If you are using Excalidraw in compatibility mode, then your template must be a legacy excalidraw file as well " +
|
"If you are using Excalidraw in compatibility mode, then your template must be a legacy excalidraw file as well " +
|
||||||
"such as Excalidraw/Template.excalidraw.",
|
"such as Excalidraw/Template.excalidraw.",
|
||||||
AUTOSAVE_NAME: "Autosave",
|
AUTOSAVE_NAME: "Autosave",
|
||||||
AUTOSAVE_DESC: "Automatically save the active drawing every 30 seconds. Save normally happens when you close Excalidraw or Obsidian, or move "+
|
AUTOSAVE_DESC: "Automatically save the active drawing every 30 seconds. Save normally happens when you close Excalidraw or Obsidian, or move "+
|
||||||
"focus to another pane. In rare cases autosave may slightly disrupt your drawing flow. I created this feature with mobile " +
|
"focus to another pane. In rare cases autosave may slightly disrupt your drawing flow. I created this feature with mobile " +
|
||||||
"phones in mind (I only have experience with Android), where 'swiping out Obsidian to close it' led to some data loss, and because " +
|
"phones in mind (I only have experience with Android), where 'swiping out Obsidian to close it' led to some data loss, and because " +
|
||||||
"I wasn't able to force save on application termination on mobiles. If you use Excalidraw on a desktop this is likely not needed.",
|
"I wasn't able to force save on application termination on mobiles. If you use Excalidraw on a desktop this is likely not needed.",
|
||||||
FILENAME_HEAD: "Filename",
|
FILENAME_HEAD: "Filename",
|
||||||
FILENAME_DESC: "<p>The auto-generated filename consists of a prefix and a date. " +
|
FILENAME_DESC: "<p>The auto-generated filename consists of a prefix and a date. " +
|
||||||
"e.g.'Drawing 2021-05-24 12.58.07'.</p>"+
|
"e.g.'Drawing 2021-05-24 12.58.07'.</p>"+
|
||||||
"<p>Click this link for the <a href='https://momentjs.com/docs/#/displaying/format/'>"+
|
"<p>Click this link for the <a href='https://momentjs.com/docs/#/displaying/format/'>"+
|
||||||
"date and time format reference</a>.</p>",
|
"date and time format reference</a>.</p>",
|
||||||
FILENAME_SAMPLE: "The current file format is: <b>",
|
FILENAME_SAMPLE: "The current file format is: <b>",
|
||||||
FILENAME_PREFIX_NAME: "Filename prefix",
|
FILENAME_PREFIX_NAME: "Filename prefix",
|
||||||
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",
|
DISPLAY_HEAD: "Display",
|
||||||
ZOOM_TO_FIT_NAME: "Zoom to fit on view resize",
|
MATCH_THEME_NAME: "New drawing to match Obsidian theme",
|
||||||
ZOOM_TO_FIT_DESC: "Zoom to fit drawing when the pane is resized",
|
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. " +
|
||||||
LINKS_HEAD: "Links and transclusion",
|
"Also this will not effect when you open an existing drawing. Those will follow the theme of the template/drawing respectively.",
|
||||||
LINKS_DESC: "CTRL/META + CLICK on Text Elements to open them as links. " +
|
ZOOM_TO_FIT_NAME: "Zoom to fit on view resize",
|
||||||
"If the selected text has more than one [[valid Obsidian links]], only the first will be opened. " +
|
ZOOM_TO_FIT_DESC: "Zoom to fit drawing when the pane is resized",
|
||||||
"If the text starts as a valid web link (i.e. https:// or http://), then " +
|
LINKS_HEAD: "Links and transclusion",
|
||||||
"the plugin will open it in a browser. " +
|
LINKS_DESC: "CTRL/META + CLICK on Text Elements to open them as links. " +
|
||||||
"When Obsidian files change, the matching [[link]] in your drawings will also change. " +
|
"If the selected text has more than one [[valid Obsidian links]], only the first will be opened. " +
|
||||||
"If you don't want text accidentally changing in your drawings use [[links|with aliases]].",
|
"If the text starts as a valid web link (i.e. https:// or http://), then " +
|
||||||
LINK_BRACKETS_NAME: "Show [[brackets]] around links",
|
"the plugin will open it in a browser. " +
|
||||||
LINK_BRACKETS_DESC: "In PREVIEW mode, when parsing Text Elements, place brackets around links. " +
|
"When Obsidian files change, the matching [[link]] in your drawings will also change. " +
|
||||||
"You can override this setting for a specific drawing by adding '" + FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS +
|
"If you don't want text accidentally changing in your drawings use [[links|with aliases]].",
|
||||||
": true/false' to the file\'s frontmatter.",
|
ADJACENT_PANE_NAME: "Open in adjacent pane",
|
||||||
LINK_PREFIX_NAME:"Link prefix",
|
ADJACENT_PANE_DESC: "When CTRL+SHIFT clicking a link in Excalidraw by default the plugin will open the link in a new pane. " +
|
||||||
LINK_PREFIX_DESC:"In PREVIEW mode, if the Text Element contains a link, precede the text with these characters. " +
|
"Turning this setting on, Excalidraw will first look for an existing adjacent pane, and try to open the link there. " +
|
||||||
"You can override this setting for a specific drawing by adding \'" + FRONTMATTER_KEY_CUSTOM_PREFIX +
|
"Excalidraw will first look too the right, then to the left, then down, then up. If no pane is found, Excalidraw will open " +
|
||||||
': "📍 "\' to the file\'s frontmatter.',
|
"a new pane.",
|
||||||
URL_PREFIX_NAME:"URL prefix",
|
LINK_BRACKETS_NAME: "Show [[brackets]] around links",
|
||||||
URL_PREFIX_DESC:"In PREVIEW mode, if the Text Element contains a URL link, precede the text with these characters. " +
|
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_URL_PREFIX +
|
"You can override this setting for a specific drawing by adding '" + FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS +
|
||||||
': "🌐 "\' to the file\'s frontmatter.',
|
": true/false' to the file\'s frontmatter.",
|
||||||
LINK_CTRL_CLICK_NAME: "CTRL + CLICK on text to open them as links",
|
LINK_PREFIX_NAME:"Link prefix",
|
||||||
LINK_CTRL_CLICK_DESC: "You can turn this feature off if it interferes with default Excalidraw features you want to use. If " +
|
LINK_PREFIX_DESC:"In PREVIEW mode, if the Text Element contains a link, precede the text with these characters. " +
|
||||||
"this is turned off, only the link button in the title bar of the drawing pane will open links.",
|
"You can override this setting for a specific drawing by adding \'" + FRONTMATTER_KEY_CUSTOM_PREFIX +
|
||||||
TRANSCLUSION_WRAP_NAME: "Overflow wrap behavior of transcluded text",
|
': "📍 "\' to the file\'s frontmatter.',
|
||||||
TRANSCLUSION_WRAP_DESC: "Number specifies the character count where the text should be wrapped. " +
|
URL_PREFIX_NAME:"URL prefix",
|
||||||
"Set the text wrapping behavior of transcluded text. Turn this ON to force-wrap " +
|
URL_PREFIX_DESC:"In PREVIEW mode, if the Text Element contains a URL link, precede the text with these characters. " +
|
||||||
"text (i.e. no overflow), or OFF to soft-warp text (at the nearest whitespace).",
|
"You can override this setting for a specific drawing by adding \'" + FRONTMATTER_KEY_CUSTOM_URL_PREFIX +
|
||||||
PAGE_TRANSCLUSION_CHARCOUNT_NAME: "Page transclusion max char count",
|
': "🌐 "\' to the file\'s frontmatter.',
|
||||||
PAGE_TRANSCLUSION_CHARCOUNT_DESC: "The maximum number of characters to display from the page when transcluding an entire page with the "+
|
LINK_CTRL_CLICK_NAME: "CTRL + CLICK on text to open them as links",
|
||||||
"![[markdown page]] format.",
|
LINK_CTRL_CLICK_DESC: "You can turn this feature off if it interferes with default Excalidraw features you want to use. If " +
|
||||||
EMBED_HEAD: "Embed & Export",
|
"this is turned off, only the link button in the title bar of the drawing pane will open links.",
|
||||||
EMBED_PREVIEW_SVG_NAME: "Display SVG in markdown preview",
|
TRANSCLUSION_WRAP_NAME: "Overflow wrap behavior of transcluded text",
|
||||||
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.",
|
TRANSCLUSION_WRAP_DESC: "Number specifies the character count where the text should be wrapped. " +
|
||||||
EMBED_WIDTH_NAME: "Default width of embedded (transcluded) image",
|
"Set the text wrapping behavior of transcluded text. Turn this ON to force-wrap " +
|
||||||
EMBED_WIDTH_DESC: "Only relevant if embed type is excalidraw. Has no effect on PNG and SVG embeds. The default width of an embedded drawing. You can specify a custom " +
|
"text (i.e. no overflow), or OFF to soft-wrap text (at the nearest whitespace).",
|
||||||
"width when embedding an image using the ![[drawing.excalidraw|100]] or " +
|
PAGE_TRANSCLUSION_CHARCOUNT_NAME: "Page transclusion max char count",
|
||||||
"[[drawing.excalidraw|100x100]] format.",
|
PAGE_TRANSCLUSION_CHARCOUNT_DESC: "The maximum number of characters to display from the page when transcluding an entire page with the "+
|
||||||
EMBED_TYPE_NAME: "Type of file to insert into the document",
|
"![[markdown page]] format.",
|
||||||
EMBED_TYPE_DESC: "When you embed an image into a document using the command palette this setting will specify if Excalidraw should embed the original excalidraw file "+
|
EMBED_HEAD: "Embed & Export",
|
||||||
"or a PNG or an SVG copy. You need to enable auto-export PNG / SVG (see below under Export Settings) for those image types to be available in the dropdown. For drawings that do not have a " +
|
EMBED_PREVIEW_SVG_NAME: "Display SVG in markdown preview",
|
||||||
"a correspondign PNG or SVG readily available the command palette action will insert a broken link. You need to open the original drawing and initiate export manually. " +
|
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.",
|
||||||
"This option will not autogenerate PNG/SVG files, but will simply reference the already existing files.",
|
EMBED_WIDTH_NAME: "Default width of embedded (transcluded) image",
|
||||||
EXPORT_PNG_SCALE_NAME: "PNG export image scale",
|
EMBED_WIDTH_DESC: "Only relevant if embed type is excalidraw. Has no effect on PNG and SVG embeds. The default width of an embedded drawing. You can specify a custom " +
|
||||||
EXPORT_PNG_SCALE_DESC: "The size-scale of the exported PNG image",
|
"width when embedding an image using the ![[drawing.excalidraw|100]] or " +
|
||||||
EXPORT_BACKGROUND_NAME: "Export image with background",
|
"[[drawing.excalidraw|100x100]] format.",
|
||||||
EXPORT_BACKGROUND_DESC: "If turned off, the exported image will be transparent.",
|
EMBED_TYPE_NAME: "Type of file to insert into the document",
|
||||||
EXPORT_THEME_NAME: "Export image with theme",
|
EMBED_TYPE_DESC: "When you embed an image into a document using the command palette this setting will specify if Excalidraw should embed the original excalidraw file "+
|
||||||
EXPORT_THEME_DESC: "Export the image matching the dark/light theme of your drawing. If turned off, " +
|
"or a PNG or an SVG copy. You need to enable auto-export PNG / SVG (see below under Export Settings) for those image types to be available in the dropdown. For drawings that do not have a " +
|
||||||
"drawings created in drak mode will appear as they would in light mode.",
|
"a correspondign PNG or SVG readily available the command palette action will insert a broken link. You need to open the original drawing and initiate export manually. " +
|
||||||
EXPORT_HEAD: "Export Settings",
|
"This option will not autogenerate PNG/SVG files, but will simply reference the already existing files.",
|
||||||
EXPORT_SYNC_NAME:"Keep the .SVG and/or .PNG filenames in sync with the drawing file",
|
EXPORT_PNG_SCALE_NAME: "PNG export image scale",
|
||||||
EXPORT_SYNC_DESC:"When turned on, the plugin will automaticaly update the filename of the .SVG and/or .PNG files when the drawing in the same folder (and same name) is renamed. " +
|
EXPORT_PNG_SCALE_DESC: "The size-scale of the exported PNG image",
|
||||||
"The plugin will also automatically delete the .SVG and/or .PNG files when the drawing in the same folder (and same name) is deleted. ",
|
EXPORT_BACKGROUND_NAME: "Export image with background",
|
||||||
EXPORT_SVG_NAME: "Auto-export SVG",
|
EXPORT_BACKGROUND_DESC: "If turned off, the exported image will be transparent.",
|
||||||
EXPORT_SVG_DESC: "Automatically create an SVG export of your drawing matching the title of your file. " +
|
EXPORT_THEME_NAME: "Export image with theme",
|
||||||
"The plugin will save the *.SVG file in the same folder as the drawing. "+
|
EXPORT_THEME_DESC: "Export the image matching the dark/light theme of your drawing. If turned off, " +
|
||||||
"Embed the .svg file into your documents instead of excalidraw making you embeds platform independent. " +
|
"drawings created in drak mode will appear as they would in light mode.",
|
||||||
"While the auto-export switch is on, this file will get updated every time you edit the excalidraw drawing with the matching name.",
|
EXPORT_HEAD: "Export Settings",
|
||||||
EXPORT_PNG_NAME: "Auto-export PNG",
|
EXPORT_SYNC_NAME:"Keep the .SVG and/or .PNG filenames in sync with the drawing file",
|
||||||
EXPORT_PNG_DESC: "Same as the auto-export SVG, but for *.PNG",
|
EXPORT_SYNC_DESC:"When turned on, the plugin will automaticaly update the filename of the .SVG and/or .PNG files when the drawing in the same folder (and same name) is renamed. " +
|
||||||
COMPATIBILITY_HEAD: "Compatibility features",
|
"The plugin will also automatically delete the .SVG and/or .PNG files when the drawing in the same folder (and same name) is deleted. ",
|
||||||
EXPORT_EXCALIDRAW_NAME: "Auto-export Excalidraw",
|
EXPORT_SVG_NAME: "Auto-export SVG",
|
||||||
EXPORT_EXCALIDRAW_DESC: "Same as the auto-export SVG, but for *.Excalidraw",
|
EXPORT_SVG_DESC: "Automatically create an SVG export of your drawing matching the title of your file. " +
|
||||||
SYNC_EXCALIDRAW_NAME: "Sync *.excalidraw with *.md version of the same drawing",
|
"The plugin will save the *.SVG file in the same folder as the drawing. "+
|
||||||
SYNC_EXCALIDRAW_DESC: "If the modified date of the *.excalidraw file is more recent than the modified date of the *.md file " +
|
"Embed the .svg file into your documents instead of excalidraw making you embeds platform independent. " +
|
||||||
"then update the drawing in the .md file based on the .excalidraw file",
|
"While the auto-export switch is on, this file will get updated every time you edit the excalidraw drawing with the matching name.",
|
||||||
COMPATIBILITY_MODE_NAME: "New drawings as legacy files",
|
EXPORT_PNG_NAME: "Auto-export PNG",
|
||||||
COMPATIBILITY_MODE_DESC: "By enabling this feature drawings you create with the ribbon icon, the command palette actions, "+
|
EXPORT_PNG_DESC: "Same as the auto-export SVG, but for *.PNG",
|
||||||
"and the file explorer are going to be all legacy *.excalidraw files. This setting will also turn off the reminder message " +
|
COMPATIBILITY_HEAD: "Compatibility features",
|
||||||
"when you open a legacy file for editing.",
|
EXPORT_EXCALIDRAW_NAME: "Auto-export Excalidraw",
|
||||||
EXPERIMENTAL_HEAD: "Experimental features",
|
EXPORT_EXCALIDRAW_DESC: "Same as the auto-export SVG, but for *.Excalidraw",
|
||||||
EXPERIMENTAL_DESC: "These setting will not take effect immediately, only when the File Explorer is refreshed, or Obsidian restarted.",
|
SYNC_EXCALIDRAW_NAME: "Sync *.excalidraw with *.md version of the same drawing",
|
||||||
FILETYPE_NAME: "Display type (✏️) for excalidraw.md files in File Explorer",
|
SYNC_EXCALIDRAW_DESC: "If the modified date of the *.excalidraw file is more recent than the modified date of the *.md file " +
|
||||||
FILETYPE_DESC: "Excalidraw files will receive an indicator using the emojii or text defined in the next setting.",
|
"then update the drawing in the .md file based on the .excalidraw file",
|
||||||
FILETAG_NAME: "Set the type indicator for excalidraw.md files",
|
COMPATIBILITY_MODE_NAME: "New drawings as legacy files",
|
||||||
FILETAG_DESC: "The text or emojii to display as type indicator.",
|
COMPATIBILITY_MODE_DESC: "By enabling this feature drawings you create with the ribbon icon, the command palette actions, "+
|
||||||
INSERT_EMOJI: "Insert an emoji",
|
"and the file explorer are going to be all legacy *.excalidraw files. This setting will also turn off the reminder message " +
|
||||||
|
"when you open a legacy file for editing.",
|
||||||
|
EXPERIMENTAL_HEAD: "Experimental features",
|
||||||
//openDrawings.ts
|
EXPERIMENTAL_DESC: "These setting will not take effect immediately, only when the File Explorer is refreshed, or Obsidian restarted.",
|
||||||
SELECT_FILE: "Select a file then press enter.",
|
FILETYPE_NAME: "Display type (✏️) for excalidraw.md files in File Explorer",
|
||||||
NO_MATCH: "No file matches your query.",
|
FILETYPE_DESC: "Excalidraw files will receive an indicator using the emojii or text defined in the next setting.",
|
||||||
SELECT_FILE_TO_LINK: "Select the file you want to insert the link for.",
|
FILETAG_NAME: "Set the type indicator for excalidraw.md files",
|
||||||
TYPE_FILENAME: "Type name of drawing to select.",
|
FILETAG_DESC: "The text or emojii to display as type indicator.",
|
||||||
SELECT_FILE_OR_TYPE_NEW: "Select existing drawing or type name of a new drawing then press Enter.",
|
INSERT_EMOJI: "Insert an emoji",
|
||||||
SELECT_TO_EMBED: "Select the drawing to insert into active document.",
|
|
||||||
};
|
|
||||||
|
//openDrawings.ts
|
||||||
|
SELECT_FILE: "Select a file then press enter.",
|
||||||
|
NO_MATCH: "No file matches your query.",
|
||||||
|
SELECT_FILE_TO_LINK: "Select the file you want to insert the link for.",
|
||||||
|
TYPE_FILENAME: "Type name of drawing to select.",
|
||||||
|
SELECT_FILE_OR_TYPE_NEW: "Select existing drawing or type name of a new drawing then press Enter.",
|
||||||
|
SELECT_TO_EMBED: "Select the drawing to insert into active document.",
|
||||||
|
};
|
||||||
75
src/main.ts
75
src/main.ts
@@ -31,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,
|
||||||
@@ -55,15 +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";
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -222,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,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({
|
||||||
@@ -785,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;
|
||||||
@@ -1101,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%%';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1118,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;
|
||||||
@@ -1135,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(JSON.stringify(JSON_parse(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> {
|
||||||
@@ -1179,3 +1191,4 @@ export default class ExcalidrawPlugin extends Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,11 @@ 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,
|
zoomToFitOnResize: boolean,
|
||||||
|
openInAdjacentPane: boolean,
|
||||||
showLinkBrackets: boolean,
|
showLinkBrackets: boolean,
|
||||||
linkPrefix: string,
|
linkPrefix: string,
|
||||||
urlPrefix: string,
|
urlPrefix: string,
|
||||||
@@ -47,11 +49,13 @@ 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,
|
zoomToFitOnResize: true,
|
||||||
linkPrefix: "📍",
|
linkPrefix: "📍",
|
||||||
urlPrefix: "🌐",
|
urlPrefix: "🌐",
|
||||||
|
openInAdjacentPane: false,
|
||||||
showLinkBrackets: true,
|
showLinkBrackets: true,
|
||||||
allowCtrlClick: true,
|
allowCtrlClick: true,
|
||||||
forceWrap: false,
|
forceWrap: false,
|
||||||
@@ -189,6 +193,17 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
this.containerEl.createEl('h1', {text: t("DISPLAY_HEAD")});
|
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)
|
new Setting(containerEl)
|
||||||
.setName(t("ZOOM_TO_FIT_NAME"))
|
.setName(t("ZOOM_TO_FIT_NAME"))
|
||||||
.setDesc(t("ZOOM_TO_FIT_DESC"))
|
.setDesc(t("ZOOM_TO_FIT_DESC"))
|
||||||
@@ -203,6 +218,16 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
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"))
|
||||||
@@ -282,8 +307,8 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
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
|
||||||
@@ -291,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"))
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"1.3.16": "0.11.13"
|
"1.3.20": "0.11.13"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user