mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
10 Commits
1.3.10
...
ImageEleme
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78fb37b173 | ||
|
|
a17638717f | ||
|
|
70de8ba2f8 | ||
|
|
e8a29a2715 | ||
|
|
7b1f13391c | ||
|
|
33081b1a84 | ||
|
|
aafd9f17f8 | ||
|
|
a27da5f5f5 | ||
|
|
472b58a417 | ||
|
|
1bba254eaf |
@@ -46,6 +46,7 @@ To convert files you have the following options:
|
||||
- `![[myfile#section]]` also works, this will transclude the section
|
||||
- you can also specify word wrapping for transcluded text by adding the max character count in curly brackets right after the transclusion e.g. `![[myfile#^blockref]]{40}` will wrap text at 40 characters.
|
||||
- For convenience you can also use the command palette to insert links into drawings
|
||||
- CTRL/META + hover to bring up the Obsidian quick preview for the link. (On Mac it is CTRL+CMD+hover).
|
||||
- CTRL/META + CLICK a text element to open it as a link.
|
||||
- CTRL/META + ALT + CLICK to create the file (if it does not yet exist) and open it
|
||||
- CTRL/META + SHIFT + CLICK to open the file in a new pane
|
||||
|
||||
@@ -102,6 +102,8 @@ export interface ExcalidrawAutomate extends Window {
|
||||
targetView: ExcalidrawView;
|
||||
setView (view:ExcalidrawView|"first"|"active"):ExcalidrawView;
|
||||
getExcalidrawAPI ():any;
|
||||
getViewElements ():ExcalidrawElement[];
|
||||
deleteViewElements (el: ExcalidrawElement[]):boolean;
|
||||
getViewSelectedElement( ):ExcalidrawElement;
|
||||
getViewSelectedElements ():ExcalidrawElement[];
|
||||
viewToggleFullScreen (forceViewMode?:boolean):void;
|
||||
@@ -117,6 +119,19 @@ export interface ExcalidrawAutomate extends Window {
|
||||
}
|
||||
):boolean;
|
||||
addElementsToView (repositionToCursor:boolean, save:boolean):Promise<boolean>;
|
||||
onDropHook (data: {
|
||||
ea: ExcalidrawAutomate,
|
||||
event: React.DragEvent<HTMLDivElement>,
|
||||
draggable: any, //Obsidian draggable object
|
||||
type: "file"|"text"|"unknown",
|
||||
payload: {
|
||||
files: TFile[], //TFile[] array of dropped files
|
||||
text: string, //string
|
||||
},
|
||||
excalidrawFile: TFile, //the file receiving the drop event
|
||||
view: ExcalidrawView, //the excalidraw view receiving the drop
|
||||
pointerPosition: {x:number, y:number} //the pointer position on canvas at the time of drop
|
||||
}):boolean;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
@@ -114,6 +114,28 @@ getExcalidrawAPI():any
|
||||
Returns the native Excalidraw API (ref.current) for the active drawing specified in `targetView`.
|
||||
See Excalidraw documentation here: https://www.npmjs.com/package/@excalidraw/excalidraw#ref
|
||||
|
||||
#### getViewElements()
|
||||
```typescript
|
||||
getViewElements():ExcalidrawElement[]
|
||||
```
|
||||
|
||||
Returns all the elements from the view.
|
||||
|
||||
#### deleteViewElements()
|
||||
```typescript
|
||||
deleteViewElements(elToDelete: ExcalidrawElement[]):boolean
|
||||
```
|
||||
|
||||
Deletes those elements from the view that match the elements provided as the input parameter.
|
||||
|
||||
Example to delete the selected elements from the view:
|
||||
```typescript
|
||||
ea = ExcalidrawAutomate;
|
||||
ea.setView("active");
|
||||
el = ea.getViewSelectedElements();
|
||||
ea.deleteViewElements();
|
||||
```
|
||||
|
||||
#### getViewSelectedElement()
|
||||
```typescript
|
||||
getViewSelectedElement():ExcalidrawElement
|
||||
@@ -162,4 +184,38 @@ Adds elements created with ExcalidrawAutomate to the target ExcalidrawView.
|
||||
|
||||
`save` default is false
|
||||
- true: the drawing will be saved after the elements were added.
|
||||
- false: the drawing will be saved at the next autosave cycle. Use false when adding multiple elements one after the other. Else, best to use true, to minimize risk of data loss.
|
||||
- false: the drawing will be saved at the next autosave cycle. Use false when adding multiple elements one after the other. Else, best to use true, to minimize risk of data loss.
|
||||
|
||||
### onDropHook
|
||||
```typescript
|
||||
onDropHook (data: {
|
||||
ea: ExcalidrawAutomate,
|
||||
event: React.DragEvent<HTMLDivElement>,
|
||||
draggable: any, //Obsidian draggable object
|
||||
type: "file"|"text"|"unknown",
|
||||
payload: {
|
||||
files: TFile[], //TFile[] array of dropped files
|
||||
text: string, //string
|
||||
},
|
||||
excalidrawFile: TFile, //the file receiving the drop event
|
||||
view: ExcalidrawView, //the excalidraw view receiving the drop
|
||||
pointerPosition: {x:number, y:number} //the pointer position on canvas at the time of drop
|
||||
}):boolean;
|
||||
```
|
||||
|
||||
Callback function triggered when an draggable item is dropped on Excalidraw.
|
||||
The function should return a boolean value. True if the drop was handled by the hook and futher native processing should be stopped, and false if Excalidraw should continue with the processing of the drop.
|
||||
type of drop can be one of:
|
||||
- "file" if a file from Obsidian file explorer is dropped onto Excalidraw. In this case payload.files will contain the list of files dropped.
|
||||
- "text" if a link (e.g. url, or wiki link) or other text is dropped. In this case payload.text will contain the received string
|
||||
- "unknown" if Excalidraw plugin does not recognize the type of dropped object. In this case you can use React.DragEvent to analysed the dropped object.
|
||||
|
||||
Use Templater startup templates or similar to set the Hook function.
|
||||
|
||||
```typescript
|
||||
ea = ExcalidrawAutomate;
|
||||
ea.onDropHook = (data) => {
|
||||
console.log(data);
|
||||
return false;
|
||||
}
|
||||
```
|
||||
@@ -59,9 +59,5 @@ function buildMindmap(subtasks, depth, offset, parentObjectID) {
|
||||
tasks["objectID"] = ea.addText(width*1.5,width,tasks.text,{box:true, textAlign:"center"});
|
||||
buildMindmap(tasks.subtasks, 2, 0, tasks.objectID);
|
||||
|
||||
(async ()=> {
|
||||
const svg = await ea.createSVG();
|
||||
const el=document.querySelector("div.block-language-dataviewjs");
|
||||
el.appendChild(svg);
|
||||
})();
|
||||
ea.createSVG().then((svg)=>dv.span(svg.outerHTML));
|
||||
```
|
||||
@@ -52,9 +52,5 @@ function buildMindmap(subtasks, depth, offset, parentObjectID) {
|
||||
tasks["objectID"] = ea.addText(0,(tasks.size/2)*height,tasks.text,{box:true});
|
||||
buildMindmap(tasks.subtasks, 1, 0, tasks.objectID);
|
||||
|
||||
(async ()=> {
|
||||
const svg = await ea.createSVG();
|
||||
const el=document.querySelector("div.block-language-dataviewjs");
|
||||
el.appendChild(svg);
|
||||
})();
|
||||
ea.createSVG().then((svg)=>dv.span(svg.outerHTML));
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "1.3.10",
|
||||
"version": "1.3.16",
|
||||
"minAppVersion": "0.12.0",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@zsviczian/excalidraw": "0.9.0-obsidian-8",
|
||||
"@zsviczian/excalidraw": "0.9.0-obsidian-image-support-3",
|
||||
"monkey-around": "^2.2.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
@@ -31,7 +31,7 @@
|
||||
"@types/react-dom": "^17.0.8",
|
||||
"cross-env": "^7.0.3",
|
||||
"nanoid": "^3.1.23",
|
||||
"obsidian": "^0.12.11",
|
||||
"obsidian": "^0.12.16",
|
||||
"rollup": "^2.52.3",
|
||||
"rollup-plugin-visualizer": "^5.5.0",
|
||||
"tslib": "^2.3.0",
|
||||
|
||||
@@ -15,9 +15,11 @@ import {
|
||||
FRONTMATTER,
|
||||
nanoid,
|
||||
JSON_parse,
|
||||
VIEW_TYPE_EXCALIDRAW
|
||||
VIEW_TYPE_EXCALIDRAW,
|
||||
MAX_IMAGE_SIZE
|
||||
} from "./constants";
|
||||
import { wrapText } from "./Utils";
|
||||
import { getObsidianImage, wrapText } from "./Utils";
|
||||
import { AppState } from "@zsviczian/excalidraw/types/types";
|
||||
|
||||
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
|
||||
|
||||
@@ -25,6 +27,7 @@ export interface ExcalidrawAutomate extends Window {
|
||||
ExcalidrawAutomate: {
|
||||
plugin: ExcalidrawPlugin;
|
||||
elementsDict: {};
|
||||
imagesDict: {};
|
||||
style: {
|
||||
strokeColor: string;
|
||||
backgroundColor: string;
|
||||
@@ -101,6 +104,7 @@ export interface ExcalidrawAutomate extends Window {
|
||||
endObjectId?:string
|
||||
}
|
||||
):string ;
|
||||
addImage(topX:number, topY:number, imageFile: TFile):Promise<string>;
|
||||
connectObjects (
|
||||
objectA: string,
|
||||
connectionA: ConnectionPoint,
|
||||
@@ -120,7 +124,9 @@ export interface ExcalidrawAutomate extends Window {
|
||||
targetView: ExcalidrawView;
|
||||
setView (view:ExcalidrawView|"first"|"active"):ExcalidrawView;
|
||||
getExcalidrawAPI ():any;
|
||||
getViewSelectedElement( ):ExcalidrawElement;
|
||||
getViewElements ():ExcalidrawElement[];
|
||||
deleteViewElements (el: ExcalidrawElement[]):boolean;
|
||||
getViewSelectedElement ():ExcalidrawElement;
|
||||
getViewSelectedElements ():ExcalidrawElement[];
|
||||
viewToggleFullScreen (forceViewMode?:boolean):void;
|
||||
connectObjectWithViewSelectedElement (
|
||||
@@ -135,6 +141,19 @@ export interface ExcalidrawAutomate extends Window {
|
||||
}
|
||||
):boolean;
|
||||
addElementsToView (repositionToCursor:boolean, save:boolean):Promise<boolean>;
|
||||
onDropHook (data: {
|
||||
ea: ExcalidrawAutomate,
|
||||
event: React.DragEvent<HTMLDivElement>,
|
||||
draggable: any, //Obsidian draggable object
|
||||
type: "file"|"text"|"unknown",
|
||||
payload: {
|
||||
files: TFile[], //TFile[] array of dropped files
|
||||
text: string, //string
|
||||
},
|
||||
excalidrawFile: TFile, //the file receiving the drop event
|
||||
view: ExcalidrawView, //the excalidraw view receiving the drop
|
||||
pointerPosition: {x:number, y:number} //the pointer position on canvas at the time of drop
|
||||
}):boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -144,6 +163,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
window.ExcalidrawAutomate = {
|
||||
plugin: plugin,
|
||||
elementsDict: {},
|
||||
imagesDict: {},
|
||||
style: {
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "transparent",
|
||||
@@ -231,7 +251,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
async toClipboard(templatePath?:string) {
|
||||
const template = templatePath ? (await getTemplate(templatePath)) : null;
|
||||
let elements = template ? template.elements : [];
|
||||
elements.concat(this.getElements());
|
||||
elements = elements.concat(this.getElements());
|
||||
navigator.clipboard.writeText(
|
||||
JSON.stringify({
|
||||
"type":"excalidraw/clipboard",
|
||||
@@ -316,7 +336,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
async createSVG(templatePath?:string):Promise<SVGSVGElement> {
|
||||
const template = templatePath ? (await getTemplate(templatePath)) : null;
|
||||
let elements = template ? template.elements : [];
|
||||
elements.concat(this.getElements());
|
||||
elements = elements.concat(this.getElements());
|
||||
return await ExcalidrawView.getSVG(
|
||||
{//createDrawing
|
||||
"type": "excalidraw",
|
||||
@@ -337,7 +357,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
async createPNG(templatePath?:string, scale:number=1) {
|
||||
const template = templatePath ? (await getTemplate(templatePath)) : null;
|
||||
let elements = template ? template.elements : [];
|
||||
elements.concat(this.getElements());
|
||||
elements = elements.concat(this.getElements());
|
||||
return ExcalidrawView.getPNG(
|
||||
{
|
||||
"type": "excalidraw",
|
||||
@@ -497,6 +517,25 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
}
|
||||
return id;
|
||||
},
|
||||
async addImage(topX:number, topY:number, imageFile: TFile):Promise<string> {
|
||||
const id = nanoid();
|
||||
const image = await getObsidianImage(this.plugin.app,imageFile);
|
||||
if(!image) return null;
|
||||
this.imagesDict[image.imageId] = {
|
||||
type:"image",
|
||||
id: image.imageId,
|
||||
dataURL: image.dataURL
|
||||
}
|
||||
if (Math.max(image.size.width,image.size.height) > MAX_IMAGE_SIZE) {
|
||||
const scale = MAX_IMAGE_SIZE/Math.max(image.size.width,image.size.height);
|
||||
image.size.width = scale*image.size.width;
|
||||
image.size.height = scale*image.size.height;
|
||||
}
|
||||
this.elementsDict[id] = boxedElement(id,"image",topX,topY,image.size.width,image.size.height);
|
||||
this.elementsDict[id].imageId = image.imageId;
|
||||
this.elementsDict[id].scale = [1,1];
|
||||
return id;
|
||||
},
|
||||
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints?: number,startArrowHead?:string,endArrowHead?:string, padding?: number}):void {
|
||||
if(!(this.elementsDict[objectA] && this.elementsDict[objectB])) {
|
||||
return;
|
||||
@@ -536,6 +575,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
},
|
||||
clear() {
|
||||
this.elementsDict = {};
|
||||
this.imagesDict = {};
|
||||
},
|
||||
reset() {
|
||||
this.clear();
|
||||
@@ -583,6 +623,32 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
}
|
||||
return (this.targetView as ExcalidrawView).excalidrawRef.current;
|
||||
},
|
||||
getViewElements ():ExcalidrawElement[] {
|
||||
if (!this.targetView || !this.targetView?._loaded) {
|
||||
errorMessage("targetView not set", "getViewSelectedElements()");
|
||||
return [];
|
||||
}
|
||||
const current = this.targetView?.excalidrawRef?.current;
|
||||
if(!current) return [];
|
||||
return current?.getSceneElements();
|
||||
},
|
||||
deleteViewElements (elToDelete: ExcalidrawElement[]):boolean {
|
||||
if (!this.targetView || !this.targetView?._loaded) {
|
||||
errorMessage("targetView not set", "getViewSelectedElements()");
|
||||
return false;
|
||||
}
|
||||
const current = this.targetView?.excalidrawRef?.current;
|
||||
if(!current) return false;
|
||||
const el: ExcalidrawElement[] = current.getSceneElements();
|
||||
const st: AppState = current.getAppState();
|
||||
current.updateScene({
|
||||
elements: el.filter((e:ExcalidrawElement)=>!elToDelete.includes(e)),
|
||||
appState: st,
|
||||
commitToHistory: true,
|
||||
});
|
||||
this.targetView.save();
|
||||
return true;
|
||||
},
|
||||
getViewSelectedElement():any {
|
||||
const elements = this.getViewSelectedElements();
|
||||
return elements ? elements[0] : null;
|
||||
@@ -640,9 +706,9 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
return false;
|
||||
}
|
||||
const elements = this.getElements();
|
||||
return await this.targetView.addElements(elements,repositionToCursor,save);
|
||||
return await this.targetView.addElements(elements,repositionToCursor,save,this.imagesDict);
|
||||
},
|
||||
|
||||
onDropHook:null,
|
||||
};
|
||||
await initFonts();
|
||||
}
|
||||
@@ -748,7 +814,7 @@ async function getTemplate(fileWithPath: string):Promise<{elements: any,appState
|
||||
let trimLocation = data.search("# Text Elements\n");
|
||||
if(trimLocation == -1) trimLocation = data.search("# Drawing\n");
|
||||
|
||||
const excalidrawData = JSON_parse(getJSON(data));
|
||||
const excalidrawData = JSON_parse(getJSON(data)[0]);
|
||||
return {
|
||||
elements: excalidrawData.elements,
|
||||
appState: excalidrawData.appState,
|
||||
|
||||
@@ -22,12 +22,9 @@ declare module "obsidian" {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const DRAWING_REG = /[\r\n]# Drawing[\r\n](```json[\r\n])?(.*)(```)?(%%)?/gm;
|
||||
|
||||
export const REGEX_LINK = {
|
||||
//![[link|alias]] [alias](link){num}
|
||||
//12 3 4 5 6 7 8
|
||||
// 1 2 3 4 5 6 7 8 9
|
||||
EXPR: /(!)?(\[\[([^|\]]+)\|?(.+)?]]|\[(.*)\]\((.*)\))(\{(\d+)\})?/g,
|
||||
isTransclusion: (parts: IteratorResult<RegExpMatchArray, any>):boolean => {
|
||||
return parts.value[1] ? true:false;
|
||||
@@ -51,14 +48,23 @@ export const REGEX_LINK = {
|
||||
|
||||
export const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;
|
||||
|
||||
export function getJSON(data:string):string {
|
||||
const res = data.matchAll(DRAWING_REG);
|
||||
const parts = res.next();
|
||||
const DRAWING_REG = /\n%%\n# Drawing\n(```json\n)(.*)\n```%%/gm;
|
||||
const DRAWING_REG_FALLBACK = /\n# Drawing\n(```json\n)?(.*)(```)?(%%)?/gm;
|
||||
export function getJSON(data:string):[string,number] {
|
||||
let res = data.matchAll(DRAWING_REG);
|
||||
|
||||
//In case the user adds a text element with the contents "# Drawing\n"
|
||||
let parts;
|
||||
parts = res.next();
|
||||
if(parts.done) { //did not find a match
|
||||
res = data.matchAll(DRAWING_REG_FALLBACK);
|
||||
parts = res.next();
|
||||
}
|
||||
if(parts.value && parts.value.length>1) {
|
||||
const result = parts.value[2];
|
||||
return result.substr(0,result.lastIndexOf("}")+1); //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;
|
||||
return [data,parts.value.index];
|
||||
}
|
||||
|
||||
export class ExcalidrawData {
|
||||
@@ -71,6 +77,7 @@ export class ExcalidrawData {
|
||||
private urlPrefix: string;
|
||||
private textMode: TextMode = TextMode.raw;
|
||||
private plugin: ExcalidrawPlugin;
|
||||
public loaded: boolean = false;
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin) {
|
||||
this.plugin = plugin;
|
||||
@@ -83,7 +90,7 @@ export class ExcalidrawData {
|
||||
* @returns {boolean} - true if file was loaded, false if there was an error
|
||||
*/
|
||||
public async loadData(data: string,file: TFile, textMode:TextMode):Promise<boolean> {
|
||||
|
||||
this.loaded = false;
|
||||
this.file = file;
|
||||
this.textElements = new Map<string,{raw:string, parsed:string}>();
|
||||
|
||||
@@ -109,30 +116,39 @@ export class ExcalidrawData {
|
||||
}
|
||||
|
||||
//Load scene: Read the JSON string after "# Drawing"
|
||||
let parts = data.matchAll(DRAWING_REG).next();
|
||||
if(!(parts.value && parts.value.length>1)) return false; //JSON not found or invalid
|
||||
if(!this.scene) { //scene was not loaded from .excalidraw
|
||||
const scene = parts.value[2];
|
||||
this.scene = JSON_parse(scene.substr(0,scene.lastIndexOf("}")+1)); //this is a workaround to address when files are mereged by sync and one version is still an old markdown without the codeblock ```
|
||||
//using JSON_parse for legacy compatibiltiy. In an earlier version Excalidraw JSON was not enclosed in a codeblock
|
||||
const [scene,pos] = getJSON(data);
|
||||
if (pos === -1) {
|
||||
return false; //JSON not found
|
||||
}
|
||||
//Trim data to remove the JSON string
|
||||
data = data.substring(0,parts.value.index);
|
||||
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 ```
|
||||
}
|
||||
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 .excalidraw JSON is modified to reflect the MD in case of difference
|
||||
//Read the text elements into the textElements Map
|
||||
let position = data.search("# Text Elements");
|
||||
if(position==-1) return true; //Text Elements header does not exist
|
||||
position += "# Text Elements\n".length;
|
||||
let position = data.search(/(^%%\n)?# Text Elements\n/m);
|
||||
if(position==-1) {
|
||||
await this.setTextMode(textMode,false);
|
||||
this.loaded = true;
|
||||
return true; //Text Elements header does not exist
|
||||
}
|
||||
position += data.match(/((^%%\n)?# Text Elements\n)/m)[0].length
|
||||
|
||||
//iterating through all the text elements in .md
|
||||
//Text elements always contain the raw value
|
||||
const BLOCKREF_LEN:number = " ^12345678\n\n".length;
|
||||
const res = data.matchAll(/\s\^(.{8})[\r\n]/g);
|
||||
const res = data.matchAll(/\s\^(.{8})[\n]+/g);
|
||||
let parts;
|
||||
while(!(parts = res.next()).done) {
|
||||
const text = data.substring(position,parts.value.index);
|
||||
this.textElements.set(parts.value[1],{raw: text, parsed: await this.parse(text)});
|
||||
const id:string = parts.value[1];
|
||||
this.textElements.set(id,{raw: text, parsed: await this.parse(text)});
|
||||
//this will set the rawText field of text elements imported from files before 1.3.14, and from other instances of Excalidraw
|
||||
const textEl = this.scene.elements.filter((el:any)=>el.id===id)[0];
|
||||
if(textEl && (!textEl.rawText || textEl.rawText === "")) textEl.rawText = text;
|
||||
|
||||
position = parts.value.index + BLOCKREF_LEN;
|
||||
}
|
||||
|
||||
@@ -140,6 +156,7 @@ export class ExcalidrawData {
|
||||
//e.g. if the entire text elements section was deleted.
|
||||
this.findNewTextElementsInScene();
|
||||
await this.setTextMode(textMode,true);
|
||||
this.loaded = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -229,8 +246,9 @@ export class ExcalidrawData {
|
||||
dirty = true;
|
||||
} else if(!this.textElements.has(id)) {
|
||||
dirty = true;
|
||||
this.textElements.set(id,{raw: te.text, parsed: null});
|
||||
this.parseasync(id,te.text);
|
||||
const raw = (te.rawText && te.rawText!==""?te.rawText:te.text); //this is for compatibility with drawings created before the rawText change on ExcalidrawTextElement
|
||||
this.textElements.set(id,{raw: raw, parsed: null});
|
||||
this.parseasync(id,raw);
|
||||
}
|
||||
}
|
||||
if(dirty) { //reload scene json in case it has changed
|
||||
@@ -251,13 +269,9 @@ export class ExcalidrawData {
|
||||
if(el.length==0) {
|
||||
this.textElements.delete(key); //if no longer in the scene, delete the text element
|
||||
} else {
|
||||
if(!this.textElements.has(key)) {
|
||||
const text = await this.getText(key);
|
||||
if(text != el[0].text) {
|
||||
this.textElements.set(key,{raw: el[0].text,parsed: await this.parse(el[0].text)});
|
||||
} else {
|
||||
const text = await this.getText(key);
|
||||
if(text != el[0].text) {
|
||||
this.textElements.set(key,{raw: el[0].text,parsed: await this.parse(el[0].text)});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -274,14 +288,24 @@ export class ExcalidrawData {
|
||||
(this.showLinkBrackets ? "]]" : "");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param text
|
||||
* @returns [string,number] - the transcluded text, and the line number for the location of the text
|
||||
*/
|
||||
public async getTransclusion (text:string):Promise<[string,number]> {
|
||||
//file-name#^blockref
|
||||
//1 2 3
|
||||
const REG_FILE_BLOCKREF = /(.*)#(\^)?(.*)/g;
|
||||
const parts=text.matchAll(REG_FILE_BLOCKREF).next();
|
||||
if(parts.done || !parts.value[1] || !parts.value[3]) return [text,0]; //filename and/or blockref not found
|
||||
const file = this.app.metadataCache.getFirstLinkpathDest(parts.value[1],this.file.path);
|
||||
if(!parts.done && !parts.value[1]) return [text,0]; //filename not found
|
||||
const filename = parts.done ? text : parts.value[1];
|
||||
const file = this.app.metadataCache.getFirstLinkpathDest(filename,this.file.path);
|
||||
if(!file || !(file instanceof TFile)) return [text,0];
|
||||
const contents = await this.app.vault.cachedRead(file);
|
||||
if(parts.done) { //no blockreference
|
||||
return([contents.substr(0,this.plugin.settings.pageTransclusionCharLimit),0]);
|
||||
}
|
||||
const isParagraphRef = parts.value[2] ? true : false; //does the reference contain a ^ character?
|
||||
const id = parts.value[3]; //the block ID or heading text
|
||||
const blocks = (await this.app.metadataCache.blockCache.getForFile({isCancelled: ()=>false},file)).blocks.filter((block:any)=>block.node.type!="comment");
|
||||
@@ -414,17 +438,17 @@ export class ExcalidrawData {
|
||||
public async syncElements(newScene:any):Promise<boolean> {
|
||||
//console.log("Excalidraw.Data.syncElements()");
|
||||
this.scene = newScene;//JSON_parse(newScene);
|
||||
const result = this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets() || this.findNewTextElementsInScene();
|
||||
const result = this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets();
|
||||
await this.updateTextElementsFromScene();
|
||||
return result;
|
||||
return result || this.findNewTextElementsInScene();
|
||||
}
|
||||
|
||||
public async updateScene(newScene:any){
|
||||
//console.log("Excalidraw.Data.updateScene()");
|
||||
this.scene = JSON_parse(newScene);
|
||||
const result = this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets() || this.findNewTextElementsInScene();
|
||||
const result = this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets();
|
||||
await this.updateTextElementsFromScene();
|
||||
if(result) {
|
||||
if(result || this.findNewTextElementsInScene()) {
|
||||
await this.updateSceneTextElements();
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -28,7 +28,8 @@ import {
|
||||
TEXT_DISPLAY_RAW_ICON_NAME,
|
||||
TEXT_DISPLAY_PARSED_ICON_NAME,
|
||||
FULLSCREEN_ICON_NAME,
|
||||
JSON_parse
|
||||
JSON_parse,
|
||||
IMAGE_TYPES
|
||||
} from './constants';
|
||||
import ExcalidrawPlugin from './main';
|
||||
import {estimateBounds, ExcalidrawAutomate, repositionElementsToCursor} from './ExcalidrawAutomate';
|
||||
@@ -36,6 +37,7 @@ import { t } from "./lang/helpers";
|
||||
import { ExcalidrawData, REG_LINKINDEX_HYPERLINK, REGEX_LINK } from "./ExcalidrawData";
|
||||
import { checkAndCreateFolder, download, getNewUniqueFilepath, splitFolderAndFilename, viewportCoordsToSceneCoords } from "./Utils";
|
||||
import { Prompt } from "./Prompt";
|
||||
import { ClipboardData } from "@zsviczian/excalidraw/types/clipboard";
|
||||
|
||||
declare let window: ExcalidrawAutomate;
|
||||
|
||||
@@ -99,22 +101,25 @@ export default class ExcalidrawView extends TextFileView {
|
||||
else this.app.vault.create(filepath,JSON.stringify(scene));
|
||||
}
|
||||
|
||||
public async saveSVG(scene?: any) {
|
||||
public saveSVG(scene?: any) {
|
||||
if(!scene) {
|
||||
if (!this.getScene) return false;
|
||||
scene = this.getScene();
|
||||
}
|
||||
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf(this.compatibilityMode ? '.excalidraw':'.md')) + '.svg';
|
||||
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
|
||||
const exportSettings: ExportSettings = {
|
||||
withBackground: this.plugin.settings.exportWithBackground,
|
||||
withTheme: this.plugin.settings.exportWithTheme
|
||||
}
|
||||
const svg = await ExcalidrawView.getSVG(scene,exportSettings);
|
||||
if(!svg) return;
|
||||
const svgString = ExcalidrawView.embedFontsInSVG(svg).outerHTML;
|
||||
if(file && file instanceof TFile) await this.app.vault.modify(file,svgString);
|
||||
else await this.app.vault.create(filepath,svgString);
|
||||
(async () => {
|
||||
const exportSettings: ExportSettings = {
|
||||
withBackground: this.plugin.settings.exportWithBackground,
|
||||
withTheme: this.plugin.settings.exportWithTheme
|
||||
}
|
||||
const svg = await ExcalidrawView.getSVG(scene,exportSettings);
|
||||
if(!svg) return;
|
||||
let serializer =new XMLSerializer();
|
||||
const svgString = serializer.serializeToString(ExcalidrawView.embedFontsInSVG(svg));
|
||||
if(file && file instanceof TFile) await this.app.vault.modify(file,svgString);
|
||||
else await this.app.vault.create(filepath,svgString);
|
||||
})();
|
||||
}
|
||||
|
||||
public static embedFontsInSVG(svg:SVGSVGElement):SVGSVGElement {
|
||||
@@ -128,21 +133,25 @@ export default class ExcalidrawView extends TextFileView {
|
||||
return svg;
|
||||
}
|
||||
|
||||
public async savePNG(scene?: any) {
|
||||
public savePNG(scene?: any) {
|
||||
if(!scene) {
|
||||
if (!this.getScene) return false;
|
||||
scene = this.getScene();
|
||||
}
|
||||
const exportSettings: ExportSettings = {
|
||||
withBackground: this.plugin.settings.exportWithBackground,
|
||||
withTheme: this.plugin.settings.exportWithTheme
|
||||
}
|
||||
const png = await ExcalidrawView.getPNG(scene,exportSettings,this.plugin.settings.pngExportScale);
|
||||
if(!png) return;
|
||||
|
||||
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf(this.compatibilityMode ? '.excalidraw':'.md')) + '.png';
|
||||
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
|
||||
if(file && file instanceof TFile) await this.app.vault.modifyBinary(file,await png.arrayBuffer());
|
||||
else await this.app.vault.createBinary(filepath,await png.arrayBuffer());
|
||||
|
||||
(async () => {
|
||||
const exportSettings: ExportSettings = {
|
||||
withBackground: this.plugin.settings.exportWithBackground,
|
||||
withTheme: this.plugin.settings.exportWithTheme
|
||||
}
|
||||
const png = await ExcalidrawView.getPNG(scene,exportSettings,this.plugin.settings.pngExportScale);
|
||||
if(!png) return;
|
||||
if(file && file instanceof TFile) await this.app.vault.modifyBinary(file,await png.arrayBuffer());
|
||||
else await this.app.vault.createBinary(filepath,await png.arrayBuffer());
|
||||
})();
|
||||
}
|
||||
|
||||
async save(preventReload:boolean=true) {
|
||||
@@ -166,9 +175,10 @@ export default class ExcalidrawView extends TextFileView {
|
||||
getViewData () {
|
||||
//console.log("ExcalidrawView.getViewData()");
|
||||
if(!this.getScene) return this.data;
|
||||
if(!this.excalidrawData.loaded) return this.data;
|
||||
if(!this.compatibilityMode) {
|
||||
let trimLocation = this.data.search("# Text Elements\n");
|
||||
if(trimLocation == -1) trimLocation = this.data.search("# Drawing\n");
|
||||
let trimLocation = this.data.search(/(^%%\n)?# Text Elements\n/m);
|
||||
if(trimLocation == -1) trimLocation = this.data.search(/(%%\n)?# Drawing\n/);
|
||||
if(trimLocation == -1) return this.data;
|
||||
|
||||
const scene = this.excalidrawData.scene;
|
||||
@@ -266,6 +276,12 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
}
|
||||
|
||||
onResize() {
|
||||
if(!this.plugin.settings.zoomToFitOnResize) return;
|
||||
if(!this.excalidrawRef) return;
|
||||
this.zoomToFit(false);
|
||||
}
|
||||
|
||||
onload() {
|
||||
//console.log("ExcalidrawView.onload()");
|
||||
this.addAction(DISK_ICON_NAME,t("FORCE_SAVE"),async (ev)=> {
|
||||
@@ -351,12 +367,15 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
// clear the view content
|
||||
clear() {
|
||||
|
||||
if(!this.excalidrawRef) return;
|
||||
this.excalidrawRef.current.resetScene();
|
||||
this.excalidrawRef.current.history.clear();
|
||||
}
|
||||
|
||||
async setViewData (data: string, clear: boolean = false) {
|
||||
if(clear) this.clear();
|
||||
data = this.data = data.replaceAll("\r\n","\n").replaceAll("\r","\n");
|
||||
this.app.workspace.onLayoutReady(async ()=>{
|
||||
//console.log("ExcalidrawView.setViewData()");
|
||||
this.dirty = null;
|
||||
this.compatibilityMode = this.file.extension == "excalidraw";
|
||||
await this.plugin.loadSettings();
|
||||
@@ -371,9 +390,19 @@ export default class ExcalidrawView extends TextFileView {
|
||||
} else {
|
||||
const parsed = data.search("excalidraw-plugin: parsed\n")>-1 || data.search("excalidraw-plugin: locked\n")>-1; //locked for backward compatibility
|
||||
this.changeTextMode(parsed ? TextMode.parsed : TextMode.raw,false);
|
||||
if(!(await this.excalidrawData.loadData(data, this.file,this.textMode))) return;
|
||||
try {
|
||||
if(!(await this.excalidrawData.loadData(data, this.file,this.textMode))) return;
|
||||
} catch(e) {
|
||||
new Notice( "Error loading drawing:\n"
|
||||
+ e.message
|
||||
+ ((e.message === "Cannot read property 'index' of undefined")
|
||||
? "\n'# Drawing' section is likely missing"
|
||||
: "")
|
||||
+ "\nTry manually fixing the file or restoring an earlier version from sync history" ,8000);
|
||||
this.setMarkdownView();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(clear) this.clear();
|
||||
await this.loadDrawing(true)
|
||||
});
|
||||
}
|
||||
@@ -384,14 +413,10 @@ export default class ExcalidrawView extends TextFileView {
|
||||
*/
|
||||
private async loadDrawing(justloaded:boolean) {
|
||||
const excalidrawData = this.excalidrawData.scene;
|
||||
this.justLoaded = justloaded;
|
||||
if(this.excalidrawRef) {
|
||||
const viewModeEnabled = this.excalidrawRef.current.getAppState().viewModeEnabled;
|
||||
const zenModeEnabled = this.excalidrawRef.current.getAppState().zenModeEnabled;
|
||||
if(justloaded) {
|
||||
this.excalidrawRef.current.resetScene();
|
||||
this.excalidrawRef.current.history.clear();
|
||||
this.justLoaded = justloaded; //reset screen will clear justLoaded, so need to set it here
|
||||
}
|
||||
this.excalidrawRef.current.updateScene({
|
||||
elements: excalidrawData.elements,
|
||||
appState: {
|
||||
@@ -405,7 +430,6 @@ export default class ExcalidrawView extends TextFileView {
|
||||
this.excalidrawWrapperRef.current.focus();
|
||||
}
|
||||
} else {
|
||||
this.justLoaded = justloaded;
|
||||
this.instantiateExcalidraw({
|
||||
elements: excalidrawData.elements,
|
||||
appState: excalidrawData.appState,
|
||||
@@ -435,6 +459,17 @@ export default class ExcalidrawView extends TextFileView {
|
||||
return ICON_NAME;
|
||||
}
|
||||
|
||||
setMarkdownView() {
|
||||
if(this.excalidrawRef) {
|
||||
const el = this.excalidrawRef.current.getSceneElements();
|
||||
if(el.filter((e:any)=>e.type==="image").length>0) {
|
||||
new Notice(t("DRAWING_CONTAINS_IMAGE"),6000);
|
||||
}
|
||||
}
|
||||
this.plugin.excalidrawFileModes[this.id || this.file.path] = "markdown";
|
||||
this.plugin.setMarkdownView(this.leaf);
|
||||
}
|
||||
|
||||
onMoreOptionsMenu(menu: Menu) {
|
||||
// Add a menu item to force the board to markdown view
|
||||
if(!this.compatibilityMode) {
|
||||
@@ -444,8 +479,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
.setTitle(t("OPEN_AS_MD"))
|
||||
.setIcon("document")
|
||||
.onClick(async () => {
|
||||
this.plugin.excalidrawFileModes[this.id || this.file.path] = "markdown";
|
||||
this.plugin.setMarkdownView(this.leaf);
|
||||
this.setMarkdownView();
|
||||
});
|
||||
})
|
||||
.addItem((item) => {
|
||||
@@ -613,7 +647,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
this.addElements(window.ExcalidrawAutomate.getElements(),false,true);
|
||||
}
|
||||
|
||||
this.addElements = async (newElements:ExcalidrawElement[],repositionToCursor:boolean = false, save:boolean=false):Promise<boolean> => {
|
||||
this.addElements = async (newElements:ExcalidrawElement[],repositionToCursor:boolean = false, save:boolean=false, images:any):Promise<boolean> => {
|
||||
if(!excalidrawRef?.current) return false;
|
||||
|
||||
const textElements = newElements.filter((el)=>el.type=="text");
|
||||
@@ -626,7 +660,20 @@ export default class ExcalidrawView extends TextFileView {
|
||||
};
|
||||
|
||||
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
|
||||
const st: AppState = excalidrawRef.current.getAppState();
|
||||
let st: AppState = excalidrawRef.current.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);
|
||||
this.excalidrawRef.current.updateScene({
|
||||
elements: el.concat(newElements),
|
||||
@@ -643,6 +690,13 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
|
||||
const st: AppState = excalidrawRef.current.getAppState();
|
||||
|
||||
if(st.files) {
|
||||
const imgIds = el.filter((e)=>e.type=="image").map((e:any)=>e.imageId);
|
||||
const toDelete = Object.keys(st.files).filter((k)=>!imgIds.contains(k));
|
||||
toDelete.forEach((k)=>delete st.files[k]);
|
||||
}
|
||||
|
||||
return {
|
||||
type: "excalidraw",
|
||||
version: 2,
|
||||
@@ -666,6 +720,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
currentItemEndArrowhead: st.currentItemEndArrowhead,
|
||||
currentItemLinearStrokeSharpness: st.currentItemLinearStrokeSharpness,
|
||||
gridSize: st.gridSize,
|
||||
files: st.files??{},
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -732,11 +787,11 @@ export default class ExcalidrawView extends TextFileView {
|
||||
document.exitFullscreen();
|
||||
this.zoomToFit();
|
||||
}
|
||||
this.ctrlKeyDown = e.ctrlKey;
|
||||
this.ctrlKeyDown = e.ctrlKey || e.metaKey;
|
||||
this.shiftKeyDown = e.shiftKey;
|
||||
this.altKeyDown = e.altKey;
|
||||
|
||||
if(e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
|
||||
|
||||
if(e.ctrlKey && !e.shiftKey && !e.altKey) { // && !e.metaKey) {
|
||||
const selectedElement = getTextElementAtPointer(currentPosition);
|
||||
if(!selectedElement) return;
|
||||
|
||||
@@ -775,7 +830,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
},
|
||||
onKeyUp: (e:any) => {
|
||||
this.ctrlKeyDown = e.ctrlKey;
|
||||
this.ctrlKeyDown = e.ctrlKey || e.metaKey;
|
||||
this.shiftKeyDown = e.shiftKey;
|
||||
this.altKeyDown = e.altKey;
|
||||
},
|
||||
@@ -877,32 +932,83 @@ export default class ExcalidrawView extends TextFileView {
|
||||
await this.plugin.saveSettings();
|
||||
})();
|
||||
},
|
||||
/*onPaste: (data: ClipboardData, event: ClipboardEvent | null) => {
|
||||
console.log(data,event);
|
||||
return false;
|
||||
},*/
|
||||
onPaste: (data: ClipboardData, event: ClipboardEvent | null) => {
|
||||
if(data.elements) {
|
||||
const self = this;
|
||||
setTimeout(()=>self.save(false),300);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
onDrop: (event: React.DragEvent<HTMLDivElement>):boolean => {
|
||||
const st: AppState = excalidrawRef.current.getAppState();
|
||||
currentPosition = viewportCoordsToSceneCoords({ clientX: event.clientX, clientY: event.clientY },st);
|
||||
|
||||
const draggable = (this.app as any).dragManager.draggable;
|
||||
const onDropHook = (type:"file"|"text"|"unknown", files:TFile[], text:string):boolean => {
|
||||
if (window.ExcalidrawAutomate.onDropHook) {
|
||||
try {
|
||||
return window.ExcalidrawAutomate.onDropHook({
|
||||
//@ts-ignore
|
||||
ea: window.ExcalidrawAutomate, //the Excalidraw Automate object
|
||||
event: event, //React.DragEvent<HTMLDivElement>
|
||||
draggable: draggable, //Obsidian draggable object
|
||||
type: type, //"file"|"text"
|
||||
payload: {
|
||||
files: files, //TFile[] array of dropped files
|
||||
text: text, //string
|
||||
},
|
||||
excalidrawFile: this.file, //the file receiving the drop event
|
||||
view: this, //the excalidraw view receiving the drop
|
||||
pointerPosition: currentPosition //the pointer position on canvas at the time of drop
|
||||
});
|
||||
} catch (e) {
|
||||
new Notice("on drop hook error. See console log for details");
|
||||
console.log(e);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
switch(draggable?.type) {
|
||||
case "file":
|
||||
this.addText(`[[${this.app.metadataCache.fileToLinktext(draggable.file,this.file.path,true)}]]`);
|
||||
if (!onDropHook("file",[draggable.file],null)) {
|
||||
if((event.ctrlKey || event.metaKey) && IMAGE_TYPES.contains(draggable.file.extension)) {
|
||||
const f = draggable.file;
|
||||
const topX = currentPosition.x;
|
||||
const topY = currentPosition.y;
|
||||
const ea = window.ExcalidrawAutomate;
|
||||
ea.reset();
|
||||
ea.setView(this);
|
||||
(async () => {
|
||||
await ea.addImage(currentPosition.x,currentPosition.y,draggable.file);
|
||||
ea.addElementsToView(false,false);
|
||||
})();
|
||||
return false;
|
||||
}
|
||||
this.addText(`[[${this.app.metadataCache.fileToLinktext(draggable.file,this.file.path,true)}]]`);
|
||||
}
|
||||
return false;
|
||||
case "files":
|
||||
for(const f of draggable.files) {
|
||||
this.addText(`[[${this.app.metadataCache.fileToLinktext(f,this.file.path,true)}]]`);
|
||||
currentPosition.y+=st.currentItemFontSize*2;
|
||||
if (!onDropHook("file",draggable.files,null)) {
|
||||
for(const f of draggable.files) {
|
||||
this.addText(`[[${this.app.metadataCache.fileToLinktext(f,this.file.path,true)}]]`);
|
||||
currentPosition.y+=st.currentItemFontSize*2;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (event.dataTransfer.types.includes("text/plain")) {
|
||||
const text:string = event.dataTransfer.getData("text");
|
||||
if(!text) return true;
|
||||
this.addText(text.replace(/(!\[\[.*#[^\]]*\]\])/g,"$1{40}"));
|
||||
if (!onDropHook("text",null,text)) {
|
||||
this.addText(text.replace(/(!\[\[.*#[^\]]*\]\])/g,"$1{40}"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if(onDropHook("unknown",null,null)) return false;
|
||||
return true;
|
||||
},
|
||||
onBeforeTextEdit: (textElement: ExcalidrawTextElement) => {
|
||||
@@ -910,8 +1016,12 @@ export default class ExcalidrawView extends TextFileView {
|
||||
clearInterval(this.autosaveTimer);
|
||||
this.autosaveTimer = null;
|
||||
}
|
||||
if(this.textMode==TextMode.parsed) return this.excalidrawData.getRawText(textElement.id);
|
||||
return null;
|
||||
//if(this.textMode==TextMode.parsed) {
|
||||
const raw = this.excalidrawData.getRawText(textElement.id);
|
||||
if(!raw) return textElement.rawText;
|
||||
return raw;
|
||||
/*}
|
||||
return null;*/
|
||||
},
|
||||
onBeforeTextSubmit: (textElement: ExcalidrawTextElement, text:string, isDeleted:boolean) => {
|
||||
if(isDeleted) {
|
||||
@@ -922,7 +1032,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
//If the parsed text is different than the raw text, and if View is in TextMode.parsed
|
||||
//Then I need to clear the undo history to avoid overwriting raw text with parsed text and losing links
|
||||
if(text!=textElement.text) { //the user made changes to the text
|
||||
if(text!=textElement.text || !this.excalidrawData.getRawText(textElement.id)) { //the user made changes to the text or the text is missing from Excalidraw Data (recently copy/pasted)
|
||||
//setTextElement will attempt a quick parse (without processing transclusions)
|
||||
const parseResult = this.excalidrawData.setTextElement(textElement.id, text,async ()=>{
|
||||
await this.save(false);
|
||||
@@ -962,9 +1072,9 @@ export default class ExcalidrawView extends TextFileView {
|
||||
const fullscreen = (document.fullscreenElement==this.contentEl);
|
||||
const elements = current.getSceneElements();
|
||||
if(delay) { //time for the DOM to render, I am sure there is a more elegant solution
|
||||
setTimeout(() => current.zoomToFit(elements,10,fullscreen?0:0.05),100);
|
||||
setTimeout(() => current.zoomToFit(elements,2,fullscreen?0:0.05),100);
|
||||
} else {
|
||||
current.zoomToFit(elements,10,fullscreen?0:0.05);
|
||||
current.zoomToFit(elements,2,fullscreen?0:0.05);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
43
src/InsertLinkDialog.ts
Normal file
43
src/InsertLinkDialog.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
App,
|
||||
FuzzySuggestModal,
|
||||
TFile
|
||||
} from "obsidian";
|
||||
import {t} from './lang/helpers'
|
||||
|
||||
export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
|
||||
public app: App;
|
||||
private addText: Function;
|
||||
private drawingPath: string;
|
||||
|
||||
constructor(app: App) {
|
||||
super(app);
|
||||
this.app = app;
|
||||
this.limit = 20;
|
||||
this.setInstructions([{
|
||||
command: t("SELECT_FILE"),
|
||||
purpose: "",
|
||||
}]);
|
||||
this.setPlaceholder(t("SELECT_FILE_TO_LINK"));
|
||||
this.emptyStateText = t("NO_MATCH");
|
||||
}
|
||||
|
||||
getItems(): TFile[] {
|
||||
return this.app.vault.getFiles();
|
||||
}
|
||||
|
||||
getItemText(item: TFile): string {
|
||||
return item.path;
|
||||
}
|
||||
|
||||
onChooseItem(item: TFile, _evt: MouseEvent | KeyboardEvent): void {
|
||||
const filepath = this.app.metadataCache.fileToLinktext(item,this.drawingPath,true);
|
||||
this.addText("[["+filepath+"]]");
|
||||
}
|
||||
|
||||
public start(drawingPath:string, addText: Function) {
|
||||
this.addText = addText;
|
||||
this.drawingPath = drawingPath;
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
69
src/Utils.ts
69
src/Utils.ts
@@ -1,6 +1,9 @@
|
||||
import { normalizePath, TAbstractFile, TFolder, Vault } from "obsidian";
|
||||
import { App, normalizePath, TAbstractFile, TFile, TFolder, Vault } from "obsidian";
|
||||
import { Random } from "roughjs/bin/math";
|
||||
import { Zoom } from "@zsviczian/excalidraw/types/types";
|
||||
import { nanoid } from "nanoid";
|
||||
import { IMAGE_TYPES } from "./constants";
|
||||
|
||||
|
||||
/**
|
||||
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
|
||||
@@ -122,4 +125,66 @@ export const viewportCoordsToSceneCoords = (
|
||||
const x = (clientX - zoom.translation.x - offsetLeft) * invScale - scrollX;
|
||||
const y = (clientY - zoom.translation.y - offsetTop) * invScale - scrollY;
|
||||
return { x, y };
|
||||
};
|
||||
};
|
||||
|
||||
export const getObsidianImage = async (app: App, file: TFile)
|
||||
:Promise<{
|
||||
imageId: string,
|
||||
dataURL: string,
|
||||
size: {height: number, width: number},
|
||||
}> => {
|
||||
if(!app || !file) return null;
|
||||
if (!IMAGE_TYPES.contains(file.extension)) return null;
|
||||
const ab = await app.vault.readBinary(file);
|
||||
return {
|
||||
imageId: await generateIdFromFile(ab),
|
||||
dataURL: file.extension==="svg" ? await getSVGData(app,file) : await getDataURL(ab),
|
||||
size: await getImageSize(app,file)
|
||||
}
|
||||
}
|
||||
|
||||
const getSVGData = async (app: App, file: TFile): Promise<string> => {
|
||||
const svg = await app.vault.read(file);
|
||||
return "data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.replaceAll(" "," "))))
|
||||
}
|
||||
|
||||
const getDataURL = async (file: ArrayBuffer): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const dataURL = reader.result as string;
|
||||
resolve(dataURL);
|
||||
};
|
||||
reader.onerror = (error) => reject(error);
|
||||
reader.readAsDataURL(new Blob([new Uint8Array(file)]));
|
||||
});
|
||||
};
|
||||
|
||||
const generateIdFromFile = async (file: ArrayBuffer):Promise<string> => {
|
||||
let id: string;
|
||||
try {
|
||||
const hashBuffer = await window.crypto.subtle.digest(
|
||||
"SHA-1",
|
||||
file,
|
||||
);
|
||||
id =
|
||||
// convert buffer to byte array
|
||||
Array.from(new Uint8Array(hashBuffer))
|
||||
// convert to hex string
|
||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
id = nanoid(40);
|
||||
}
|
||||
return id;
|
||||
};
|
||||
|
||||
const getImageSize = async (app: App, file:TFile):Promise<{height:number, width:number}> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let img = new Image()
|
||||
img.onload = () => resolve({height: img.height, width:img.width});
|
||||
img.onerror = reject;
|
||||
img.src = app.vault.getResourcePath(file);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ export function JSON_parse(x:string):any {return JSON.parse(x.replaceAll("["
|
||||
|
||||
import {customAlphabet} from "nanoid";
|
||||
export const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',8);
|
||||
export const IMAGE_TYPES = ['jpeg', 'jpg', 'png', 'gif', 'svg', 'bmp'];
|
||||
export const MAX_IMAGE_SIZE = 600;
|
||||
export const FRONTMATTER_KEY = "excalidraw-plugin";
|
||||
export const FRONTMATTER_KEY_CUSTOM_PREFIX = "excalidraw-link-prefix";
|
||||
export const FRONTMATTER_KEY_CUSTOM_URL_PREFIX = "excalidraw-url-prefix";
|
||||
@@ -13,7 +15,7 @@ export const MAX_COLORS = 5;
|
||||
export const COLOR_FREQ = 6;
|
||||
export const RERENDER_EVENT = "excalidraw-embed-rerender";
|
||||
export const BLANK_DRAWING = '{"type":"excalidraw","version":2,"source":"https://excalidraw.com","elements":[],"appState":{"gridSize":null,"viewBackgroundColor":"#ffffff"}}';
|
||||
export const FRONTMATTER = ["---","",`${FRONTMATTER_KEY}: unlocked`,"","---", "==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠==", "",""].join("\n");
|
||||
export const 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 TEXT_DISPLAY_PARSED_ICON_NAME = "quote-glyph";
|
||||
export const TEXT_DISPLAY_RAW_ICON_NAME = "presentation";
|
||||
|
||||
@@ -44,6 +44,8 @@ export default {
|
||||
NOFILE: "Excalidraw (no file)",
|
||||
COMPATIBILITY_MODE: "*.excalidraw file opened in compatibility mode. Convert to new format for full plugin functionality.",
|
||||
CONVERT_FILE: "Convert to new format",
|
||||
DRAWING_CONTAINS_IMAGE: "Warning! The drawing contains image elements. Depending on the number and size of the images, " +
|
||||
"loading Markdown View may take a while. Please be patient. ",
|
||||
|
||||
//settings.ts
|
||||
FOLDER_NAME: "Excalidraw folder",
|
||||
@@ -51,7 +53,7 @@ export default {
|
||||
TEMPLATE_NAME: "Excalidraw template file",
|
||||
TEMPLATE_DESC: "Full filepath to the Excalidraw template. " +
|
||||
"E.g.: If your template is in the default Excalidraw folder and it's name is " +
|
||||
"Template.md, the setting would be: Excalidraw/Template.md " +
|
||||
"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 " +
|
||||
"such as Excalidraw/Template.excalidraw.",
|
||||
AUTOSAVE_NAME: "Autosave",
|
||||
@@ -69,6 +71,9 @@ export default {
|
||||
FILENAME_PREFIX_DESC: "The first part of the filename",
|
||||
FILENAME_DATE_NAME: "Filename date",
|
||||
FILENAME_DATE_DESC: "The second part of the filename",
|
||||
DISPLAY_HEAD: "Display",
|
||||
ZOOM_TO_FIT_NAME: "Zoom to fit on view resize",
|
||||
ZOOM_TO_FIT_DESC: "Zoom to fit drawing when the pane is resized",
|
||||
LINKS_HEAD: "Links and transclusion",
|
||||
LINKS_DESC: "CTRL/META + CLICK on Text Elements to open them as links. " +
|
||||
"If the selected text has more than one [[valid Obsidian links]], only the first will be opened. " +
|
||||
@@ -95,13 +100,21 @@ export default {
|
||||
TRANSCLUSION_WRAP_DESC: "Number specifies the character count where the text should be wrapped. " +
|
||||
"Set the text wrapping behavior of transcluded text. Turn this ON to force-wrap " +
|
||||
"text (i.e. no overflow), or OFF to soft-warp text (at the nearest whitespace).",
|
||||
PAGE_TRANSCLUSION_CHARCOUNT_NAME: "Page transclusion max char count",
|
||||
PAGE_TRANSCLUSION_CHARCOUNT_DESC: "The maximum number of characters to display from the page when transcluding an entire page with the "+
|
||||
"![[markdown page]] format.",
|
||||
EMBED_HEAD: "Embed & Export",
|
||||
EMBED_PREVIEW_SVG_NAME: "Display SVG in markdown preview",
|
||||
EMBED_PREVIEW_SVG_DESC: "The default is to display drawings as SVG images in the markdown preview. Turning this feature off, the markdown preview will display the drawing as an embedded PNG image.",
|
||||
EMBED_WIDTH_NAME: "Default width of embedded (transcluded) image",
|
||||
EMBED_WIDTH_DESC: "The default width of an embedded drawing. You can specify a custom " +
|
||||
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 " +
|
||||
"width when embedding an image using the ![[drawing.excalidraw|100]] or " +
|
||||
"[[drawing.excalidraw|100x100]] format.",
|
||||
EMBED_TYPE_NAME: "Type of file to insert into the document",
|
||||
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 "+
|
||||
"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 " +
|
||||
"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. " +
|
||||
"This option will not autogenerate PNG/SVG files, but will simply reference the already existing files.",
|
||||
EXPORT_PNG_SCALE_NAME: "PNG export image scale",
|
||||
EXPORT_PNG_SCALE_DESC: "The size-scale of the exported PNG image",
|
||||
EXPORT_BACKGROUND_NAME: "Export image with background",
|
||||
|
||||
68
src/main.ts
68
src/main.ts
@@ -15,9 +15,7 @@ import {
|
||||
MarkdownRenderer,
|
||||
ViewState,
|
||||
Notice,
|
||||
request,
|
||||
} from "obsidian";
|
||||
|
||||
import {
|
||||
BLANK_DRAWING,
|
||||
VIEW_TYPE_EXCALIDRAW,
|
||||
@@ -46,6 +44,9 @@ import {
|
||||
openDialogAction,
|
||||
OpenFileDialog
|
||||
} from "./openDrawing";
|
||||
import {
|
||||
InsertLinkDialog
|
||||
} from "./InsertLinkDialog";
|
||||
import {
|
||||
initExcalidrawAutomate,
|
||||
destroyExcalidrawAutomate
|
||||
@@ -72,8 +73,8 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
public excalidrawFileModes: { [file: string]: string } = {};
|
||||
private _loaded: boolean = false;
|
||||
public settings: ExcalidrawSettings;
|
||||
//public stencilLibrary: any = null;
|
||||
private openDialog: OpenFileDialog;
|
||||
private insertLinkDialog: InsertLinkDialog;
|
||||
private activeExcalidrawView: ExcalidrawView = null;
|
||||
public lastActiveExcalidrawFilePath: string = null;
|
||||
public hover: {linkText: string, sourcePath: string} = {linkText: null, sourcePath: null};
|
||||
@@ -117,6 +118,43 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
new Notice(`You are running an older version of the electron Browser (${electron}). If Excalidraw does not start up, please reinstall Obsidian with the latest installer and try again.`,10000);
|
||||
}
|
||||
this.switchToExcalidarwAfterLoad()
|
||||
|
||||
//This is a once off cleanup process to remediate incorrectly placed comment %% before # Text Elements
|
||||
if(this.settings.patchCommentBlock) {
|
||||
const self = this;
|
||||
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw will patch drawings in 5 minutes");
|
||||
setTimeout(async ()=>{
|
||||
await self.loadSettings();
|
||||
if (!self.settings.patchCommentBlock) {
|
||||
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw patching aborted because synched data.json is already patched");
|
||||
return;
|
||||
}
|
||||
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw is starting the patching process");
|
||||
let i = 0;
|
||||
const excalidrawFiles = this.app.vault.getFiles();
|
||||
for (const f of (excalidrawFiles || []).filter((f:TFile) => self.isExcalidrawFile(f))) {
|
||||
if ( (f.extension !== "excalidraw") //legacy files do not need to be touched
|
||||
&& (self.app.workspace.getActiveFile() !== f)) { //file is currently being edited
|
||||
let drawing = await self.app.vault.read(f);
|
||||
const orig_drawing = drawing;
|
||||
drawing = drawing.replaceAll("\r\n","\n").replaceAll("\r","\n"); //Win, Mac, Linux compatibility
|
||||
drawing = drawing.replace("\n%%\n# Text Elements\n","\n# Text Elements\n");
|
||||
if (drawing.search("\n%%\n# Drawing\n") === -1) {
|
||||
const [json,pos] = getJSON(drawing);
|
||||
drawing = drawing.substr(0,pos)+"\n%%\n# Drawing\n```json\n"+json+"\n```%%";
|
||||
};
|
||||
if (drawing !== orig_drawing) {
|
||||
i++;
|
||||
console.log("Excalidraw patched: " + f.path);
|
||||
await self.app.vault.modify(f,drawing);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.settings.patchCommentBlock = false;
|
||||
self.saveSettings();
|
||||
console.log(window.moment().format("HH:mm:ss") + ": Excalidraw patched in total " + i + " files");
|
||||
},300000) //5 minutes
|
||||
}
|
||||
}
|
||||
|
||||
private switchToExcalidarwAfterLoad() {
|
||||
@@ -191,12 +229,12 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
if(width>=800) scale = 2;
|
||||
if(width>=1600) scale = 3;
|
||||
if(width>=2400) scale = 4;
|
||||
const png = await ExcalidrawView.getPNG(JSON_parse(getJSON(content)),exportSettings, scale);
|
||||
const png = await ExcalidrawView.getPNG(JSON_parse(getJSON(content)[0]),exportSettings, scale);
|
||||
if(!png) return null;
|
||||
img.src = URL.createObjectURL(png);
|
||||
return img;
|
||||
}
|
||||
let svg = await ExcalidrawView.getSVG(JSON_parse(getJSON(content)),exportSettings);
|
||||
let svg = await ExcalidrawView.getSVG(JSON_parse(getJSON(content)[0]),exportSettings);
|
||||
if(!svg) return null;
|
||||
svg = ExcalidrawView.embedFontsInSVG(svg);
|
||||
svg.removeAttribute('width');
|
||||
@@ -401,6 +439,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
|
||||
private registerCommands() {
|
||||
this.openDialog = new OpenFileDialog(this.app, this);
|
||||
this.insertLinkDialog = new InsertLinkDialog(this.app);
|
||||
|
||||
this.addRibbonIcon(ICON_NAME, t("CREATE_NEW"), async (e) => {
|
||||
this.createDrawing(this.getNextDefaultFilename(), e.ctrlKey||e.metaKey);
|
||||
@@ -645,7 +684,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
} else {
|
||||
const view = this.app.workspace.activeLeaf.view;
|
||||
if (view instanceof ExcalidrawView) {
|
||||
this.openDialog.insertLink(view.file.path,view.addText);
|
||||
this.insertLinkDialog.start(view.file.path,view.addText);
|
||||
return true;
|
||||
}
|
||||
else return false;
|
||||
@@ -904,7 +943,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
const deleteEventHandler = async (file:TFile) => {
|
||||
if (!(file instanceof TFile)) return;
|
||||
//@ts-ignore
|
||||
const isExcalidarwFile = (file.unsafeCachedData && file.unsafeCachedData.search(/---[\r\n][\s\S]*excalidraw-plugin:\s*(locked|unlocked)[\r\n][\s\S]*---/gm)>-1)
|
||||
const isExcalidarwFile = (file.unsafeCachedData && file.unsafeCachedData.search(/---[\r\n]+[\s\S]*excalidraw-plugin:\s*\w+[\r\n]+[\s\S]*---/gm)>-1)
|
||||
|| (file.extension=="excalidraw");
|
||||
if(!isExcalidarwFile) return;
|
||||
|
||||
@@ -980,7 +1019,18 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if(activeView) {
|
||||
const editor = activeView.editor;
|
||||
editor.replaceSelection("![["+data+"]]");
|
||||
switch (this.settings.embedType) {
|
||||
case "excalidraw":
|
||||
editor.replaceSelection("![["+data+"]]");
|
||||
break;
|
||||
case "PNG":
|
||||
editor.replaceSelection("![["+data.substr(0,data.lastIndexOf("."))+".png]] ([["+data+"|*]])");
|
||||
break;
|
||||
case "SVG":
|
||||
editor.replaceSelection("![["+data.substr(0,data.lastIndexOf("."))+".svg]] ([["+data+"|*]])");
|
||||
break;
|
||||
}
|
||||
|
||||
editor.focus();
|
||||
}
|
||||
|
||||
@@ -1085,7 +1135,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
}
|
||||
outString += te.text+' ^'+id+'\n\n';
|
||||
}
|
||||
return outString + this.getMarkdownDrawingSection(data);
|
||||
return outString + this.getMarkdownDrawingSection(JSON.stringify(JSON_parse(data)));
|
||||
}
|
||||
|
||||
public async createDrawing(filename: string, onNewPane: boolean, foldername?: string, initData?:string):Promise<string> {
|
||||
|
||||
@@ -12,7 +12,6 @@ import {t} from './lang/helpers'
|
||||
export enum openDialogAction {
|
||||
openFile,
|
||||
insertLinkToDrawing,
|
||||
insertLink
|
||||
}
|
||||
|
||||
export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
||||
@@ -20,8 +19,6 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
||||
private plugin: ExcalidrawPlugin;
|
||||
private action: openDialogAction;
|
||||
private onNewPane: boolean;
|
||||
private addText: Function;
|
||||
private drawingPath: string;
|
||||
|
||||
constructor(app: App, plugin: ExcalidrawPlugin) {
|
||||
super(app);
|
||||
@@ -29,6 +26,11 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
||||
this.action = openDialogAction.openFile;
|
||||
this.plugin = plugin;
|
||||
this.onNewPane = false;
|
||||
this.limit = 20;
|
||||
this.setInstructions([{
|
||||
command: t("TYPE_FILENAME"),
|
||||
purpose: "",
|
||||
}]);
|
||||
|
||||
this.inputEl.onkeyup = (e) => {
|
||||
if(e.key=="Enter" && this.action == openDialogAction.openFile) {
|
||||
@@ -42,10 +44,7 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
||||
|
||||
getItems(): TFile[] {
|
||||
const excalidrawFiles = this.app.vault.getFiles();
|
||||
return (excalidrawFiles || []).filter((f:TFile) => {
|
||||
if (this.action == openDialogAction.insertLink) return true;
|
||||
return this.plugin.isExcalidrawFile(f);
|
||||
});
|
||||
return (excalidrawFiles || []).filter((f:TFile) => this.plugin.isExcalidrawFile(f));
|
||||
}
|
||||
|
||||
getItemText(item: TFile): string {
|
||||
@@ -60,32 +59,10 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
|
||||
case(openDialogAction.insertLinkToDrawing):
|
||||
this.plugin.embedDrawing(item.path);
|
||||
break;
|
||||
case(openDialogAction.insertLink):
|
||||
//TO-DO
|
||||
const filepath = this.app.metadataCache.fileToLinktext(item,this.drawingPath,true);
|
||||
this.addText("[["+filepath+"]]");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public insertLink(drawingPath:string, addText: Function) {
|
||||
this.action = openDialogAction.insertLink;
|
||||
this.addText = addText;
|
||||
this.drawingPath = drawingPath;
|
||||
this.setInstructions([{
|
||||
command: t("SELECT_FILE"),
|
||||
purpose: "",
|
||||
}]);
|
||||
this.emptyStateText = t("NO_MATCH");
|
||||
this.setPlaceholder(t("SELECT_FILE_TO_LINK"));
|
||||
this.open();
|
||||
}
|
||||
|
||||
public start(action:openDialogAction, onNewPane: boolean): void {
|
||||
this.setInstructions([{
|
||||
command: t("TYPE_FILENAME"),
|
||||
purpose: "",
|
||||
}]);
|
||||
this.action = action;
|
||||
this.onNewPane = onNewPane;
|
||||
switch(action) {
|
||||
|
||||
163
src/settings.ts
163
src/settings.ts
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
App,
|
||||
DropdownComponent,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
TFile
|
||||
@@ -16,11 +17,13 @@ export interface ExcalidrawSettings {
|
||||
drawingFilenameDateTime: string,
|
||||
displaySVGInPreview: boolean,
|
||||
width: string,
|
||||
zoomToFitOnResize: boolean,
|
||||
showLinkBrackets: boolean,
|
||||
linkPrefix: string,
|
||||
urlPrefix: string,
|
||||
allowCtrlClick: boolean, //if disabled only the link button in the view header will open links
|
||||
forceWrap: boolean,
|
||||
pageTransclusionCharLimit: number,
|
||||
pngExportScale: number,
|
||||
exportWithTheme: boolean,
|
||||
exportWithBackground: boolean,
|
||||
@@ -28,6 +31,7 @@ export interface ExcalidrawSettings {
|
||||
autoexportSVG: boolean,
|
||||
autoexportPNG: boolean,
|
||||
autoexportExcalidraw: boolean,
|
||||
embedType: "excalidraw"|"PNG"|"SVG",
|
||||
syncExcalidraw: boolean,
|
||||
compatibilityMode: boolean,
|
||||
experimentalFileType: boolean,
|
||||
@@ -35,6 +39,7 @@ export interface ExcalidrawSettings {
|
||||
loadCount: number, //version 1.2 migration counter
|
||||
drawingOpenCount: number,
|
||||
library: string,
|
||||
patchCommentBlock: boolean, //1.3.12
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
@@ -44,11 +49,13 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
drawingFilenameDateTime: 'YYYY-MM-DD HH.mm.ss',
|
||||
displaySVGInPreview: true,
|
||||
width: '400',
|
||||
zoomToFitOnResize: true,
|
||||
linkPrefix: "📍",
|
||||
urlPrefix: "🌐",
|
||||
showLinkBrackets: true,
|
||||
allowCtrlClick: true,
|
||||
forceWrap: false,
|
||||
pageTransclusionCharLimit: 200,
|
||||
pngExportScale: 1,
|
||||
exportWithTheme: true,
|
||||
exportWithBackground: true,
|
||||
@@ -56,6 +63,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
autoexportSVG: false,
|
||||
autoexportPNG: false,
|
||||
autoexportExcalidraw: false,
|
||||
embedType: "excalidraw",
|
||||
syncExcalidraw: false,
|
||||
experimentalFileType: false,
|
||||
experimentalFileTag: "✏️",
|
||||
@@ -63,18 +71,29 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
loadCount: 0,
|
||||
drawingOpenCount: 0,
|
||||
library: `{"type":"excalidrawlib","version":1,"library":[]}`,
|
||||
patchCommentBlock: true,
|
||||
}
|
||||
|
||||
export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
plugin: ExcalidrawPlugin;
|
||||
private requestEmbedUpdate:boolean = false;
|
||||
private requestReloadDrawings:boolean = false;
|
||||
private applyDebounceTimer: number = 0;
|
||||
|
||||
constructor(app: App, plugin: ExcalidrawPlugin) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
applySettingsUpdate(requestReloadDrawings:boolean = false) {
|
||||
clearTimeout(this.applyDebounceTimer);
|
||||
const plugin = this.plugin;
|
||||
this.applyDebounceTimer = window.setTimeout(() => {
|
||||
plugin.saveSettings();
|
||||
}, 200);
|
||||
if(requestReloadDrawings) this.requestReloadDrawings = true;
|
||||
}
|
||||
|
||||
async hide() {
|
||||
if(this.requestReloadDrawings) {
|
||||
const exs = this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
@@ -114,7 +133,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.setValue(this.plugin.settings.folder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.folder = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
@@ -125,7 +144,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.setValue(this.plugin.settings.templateFilePath)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.templateFilePath = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("FILENAME_HEAD")});
|
||||
@@ -153,7 +172,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
this.plugin.settings.drawingFilenamePrefix = value.replaceAll(/[<>:"/\\|?*]/g,'_');
|
||||
text.setValue(this.plugin.settings.drawingFilenamePrefix);
|
||||
filenameEl.innerHTML = getFilenameSample();
|
||||
await this.plugin.saveSettings();
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
@@ -166,7 +185,18 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
this.plugin.settings.drawingFilenameDateTime = value.replaceAll(/[<>:"/\\|?*]/g,'_');
|
||||
text.setValue(this.plugin.settings.drawingFilenameDateTime);
|
||||
filenameEl.innerHTML = getFilenameSample();
|
||||
await this.plugin.saveSettings();
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("DISPLAY_HEAD")});
|
||||
new Setting(containerEl)
|
||||
.setName(t("ZOOM_TO_FIT_NAME"))
|
||||
.setDesc(t("ZOOM_TO_FIT_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.zoomToFitOnResize)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.zoomToFitOnResize = value;
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("LINKS_HEAD")});
|
||||
@@ -180,8 +210,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.setValue(this.plugin.settings.showLinkBrackets)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.showLinkBrackets = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.requestReloadDrawings = true;
|
||||
this.applySettingsUpdate(true);
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
@@ -193,8 +222,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.onChange((value) => {
|
||||
console.log(value);
|
||||
this.plugin.settings.linkPrefix = value;
|
||||
this.plugin.saveSettings();
|
||||
this.requestReloadDrawings = true;
|
||||
this.applySettingsUpdate(true);
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
@@ -205,8 +233,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.setValue(this.plugin.settings.urlPrefix)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.urlPrefix = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.requestReloadDrawings = true;
|
||||
this.applySettingsUpdate(true);
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
@@ -216,7 +243,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.setValue(this.plugin.settings.allowCtrlClick)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.allowCtrlClick = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
const s = new Setting(containerEl)
|
||||
@@ -226,11 +253,33 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.setValue(this.plugin.settings.forceWrap)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.forceWrap = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.requestReloadDrawings = true;
|
||||
this.applySettingsUpdate(true);
|
||||
}));
|
||||
s.descEl.innerHTML="<code>![[doc#^ref]]{number}</code> "+t("TRANSCLUSION_WRAP_DESC");
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("PAGE_TRANSCLUSION_CHARCOUNT_NAME"))
|
||||
.setDesc(t("PAGE_TRANSCLUSION_CHARCOUNT_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder('Enter a number')
|
||||
.setValue(this.plugin.settings.pageTransclusionCharLimit.toString())
|
||||
.onChange(async (value) => {
|
||||
const intVal = parseInt(value);
|
||||
if(isNaN(intVal) && value!=="") {
|
||||
text.setValue(this.plugin.settings.pageTransclusionCharLimit.toString());
|
||||
return;
|
||||
}
|
||||
this.requestEmbedUpdate = true;
|
||||
if(value === "") {
|
||||
this.plugin.settings.pageTransclusionCharLimit = 10;
|
||||
this.applySettingsUpdate(true);
|
||||
return;
|
||||
}
|
||||
this.plugin.settings.pageTransclusionCharLimit = intVal;
|
||||
text.setValue(this.plugin.settings.pageTransclusionCharLimit.toString());
|
||||
this.applySettingsUpdate(true);
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("EMBED_HEAD")});
|
||||
|
||||
|
||||
@@ -241,7 +290,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.setValue(this.plugin.settings.displaySVGInPreview)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.displaySVGInPreview = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
|
||||
@@ -253,9 +302,42 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.setValue(this.plugin.settings.width)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.width = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.applySettingsUpdate();
|
||||
this.requestEmbedUpdate = true;
|
||||
}));
|
||||
}));
|
||||
|
||||
let dropdown: DropdownComponent;
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EMBED_TYPE_NAME"))
|
||||
.setDesc(t("EMBED_TYPE_DESC"))
|
||||
.addDropdown(async (d:DropdownComponent) => {
|
||||
dropdown = d;
|
||||
dropdown.addOption("excalidraw","excalidraw")
|
||||
if(this.plugin.settings.autoexportPNG) {
|
||||
dropdown.addOption("PNG","PNG");
|
||||
} else {
|
||||
if(this.plugin.settings.embedType === "PNG") {
|
||||
this.plugin.settings.embedType = "excalidraw";
|
||||
this.applySettingsUpdate();
|
||||
}
|
||||
}
|
||||
if(this.plugin.settings.autoexportSVG) {
|
||||
dropdown.addOption("SVG","SVG");
|
||||
} else {
|
||||
if(this.plugin.settings.embedType === "SVG") {
|
||||
this.plugin.settings.embedType = "excalidraw";
|
||||
this.applySettingsUpdate();
|
||||
}
|
||||
}
|
||||
dropdown
|
||||
.setValue(this.plugin.settings.embedType)
|
||||
.onChange(async (value)=>{
|
||||
//@ts-ignore
|
||||
this.plugin.settings.embedType = value;
|
||||
this.applySettingsUpdate();
|
||||
});
|
||||
});
|
||||
|
||||
let scaleText:HTMLDivElement;
|
||||
|
||||
@@ -268,7 +350,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.onChange(async (value)=> {
|
||||
scaleText.innerText = " " + value.toString();
|
||||
this.plugin.settings.pngExportScale = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.applySettingsUpdate();
|
||||
}))
|
||||
.settingEl.createDiv('',(el)=>{
|
||||
scaleText = el;
|
||||
@@ -284,7 +366,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.setValue(this.plugin.settings.exportWithBackground)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.exportWithBackground = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.applySettingsUpdate();
|
||||
this.requestEmbedUpdate = true;
|
||||
}));
|
||||
|
||||
@@ -295,7 +377,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.setValue(this.plugin.settings.exportWithTheme)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.exportWithTheme = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.applySettingsUpdate();
|
||||
this.requestEmbedUpdate = true;
|
||||
}));
|
||||
|
||||
@@ -308,17 +390,35 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.setValue(this.plugin.settings.keepInSync)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.keepInSync = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
const removeDropdownOption = (opt: string) => {
|
||||
let i=0;
|
||||
for(i=0;i<dropdown.selectEl.options.length;i++) {
|
||||
if((dropdown.selectEl.item(i) as HTMLOptionElement).label===opt) {
|
||||
dropdown.selectEl.item(i).remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EXPORT_SVG_NAME"))
|
||||
.setDesc(t("EXPORT_SVG_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autoexportSVG)
|
||||
.onChange(async (value) => {
|
||||
if(value) {
|
||||
dropdown.addOption("SVG","SVG");
|
||||
} else {
|
||||
if (this.plugin.settings.embedType === "SVG") {
|
||||
dropdown.setValue("excalidraw");
|
||||
this.plugin.settings.embedType = "excalidraw";
|
||||
}
|
||||
removeDropdownOption("SVG");
|
||||
}
|
||||
this.plugin.settings.autoexportSVG = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
|
||||
@@ -328,8 +428,17 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.autoexportPNG)
|
||||
.onChange(async (value) => {
|
||||
if(value) {
|
||||
dropdown.addOption("PNG","PNG");
|
||||
} else {
|
||||
if (this.plugin.settings.embedType === "PNG") {
|
||||
dropdown.setValue("excalidraw");
|
||||
this.plugin.settings.embedType = "excalidraw";
|
||||
}
|
||||
removeDropdownOption("PNG");
|
||||
}
|
||||
this.plugin.settings.autoexportPNG = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("COMPATIBILITY_HEAD")});
|
||||
@@ -341,7 +450,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.setValue(this.plugin.settings.compatibilityMode)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.compatibilityMode = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
@@ -351,7 +460,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.setValue(this.plugin.settings.autoexportExcalidraw)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.autoexportExcalidraw = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
@@ -361,7 +470,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.setValue(this.plugin.settings.syncExcalidraw)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.syncExcalidraw = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("EXPERIMENTAL_HEAD")});
|
||||
@@ -375,7 +484,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.experimentalFileType = value;
|
||||
this.plugin.experimentalFileTypeDisplayToggle(value);
|
||||
await this.plugin.saveSettings();
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
@@ -386,7 +495,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.setValue(this.plugin.settings.experimentalFileTag)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.experimentalFileTag = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.applySettingsUpdate();
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"1.3.10": "0.11.13"
|
||||
"1.3.16": "0.11.13"
|
||||
}
|
||||
|
||||
16
yarn.lock
16
yarn.lock
@@ -1024,10 +1024,10 @@
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
|
||||
"@zsviczian/excalidraw@0.9.0-obsidian-8":
|
||||
"integrity" "sha512-RXKtaXn8i94qitblEwKvKPVqjWzUwrqkZaPaCdIQnnSMllTkJtZ0fE7X6nqK6BNSpGQeGNkjz3kL8VYIbFQZlA=="
|
||||
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.9.0-obsidian-8.tgz"
|
||||
"version" "0.9.0-obsidian-8"
|
||||
"@zsviczian/excalidraw@0.9.0-obsidian-image-support-3":
|
||||
"integrity" "sha512-Y+hIhIxoNsoyYS2AmhE9I8G0M6Q6KaQ3LxSn8p3JZbUGFqoAmDg+26fGHXhMsboaVXhKqonkw5UN1UlZYL5k1A=="
|
||||
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.9.0-obsidian-image-support-3.tgz"
|
||||
"version" "0.9.0-obsidian-image-support-3"
|
||||
|
||||
"abab@^1.0.3":
|
||||
"integrity" "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4="
|
||||
@@ -6891,10 +6891,10 @@
|
||||
dependencies:
|
||||
"isobject" "^3.0.1"
|
||||
|
||||
"obsidian@^0.12.11":
|
||||
"integrity" "sha512-Kv4m1n4nfd17FzpqHZfqFS2YZAyY+cxAUM7/5jqh1bmbPlmKoNd1XJZC7o9KvkXfTCxALiXfGRdrjHB+GUFAEA=="
|
||||
"resolved" "https://registry.npmjs.org/obsidian/-/obsidian-0.12.11.tgz"
|
||||
"version" "0.12.11"
|
||||
"obsidian@^0.12.16":
|
||||
"integrity" "sha512-YwwhYrmlv71A/ntTiGOyGaqr9ONhKPE1OKN1tcHovLJC0N/BRZ7XlGpzaN1T3BnAE5H8DmYLsAI3h67u6eJPGQ=="
|
||||
"resolved" "https://registry.npmjs.org/obsidian/-/obsidian-0.12.16.tgz"
|
||||
"version" "0.12.16"
|
||||
dependencies:
|
||||
"@types/codemirror" "0.0.108"
|
||||
"moment" "2.29.1"
|
||||
|
||||
Reference in New Issue
Block a user