mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54d4eb7ab4 | ||
|
|
e3242ebfb7 | ||
|
|
2e36d83abc | ||
|
|
6552e6bd7b | ||
|
|
21ff1833a8 | ||
|
|
1b931abd38 | ||
|
|
fe28098776 | ||
|
|
72c158e08b | ||
|
|
5133ab028b | ||
|
|
5fc0f70ded | ||
|
|
08f489f1c5 | ||
|
|
dd476b564a | ||
|
|
b7a7c9473e | ||
|
|
cc7dc16810 | ||
|
|
c45faef141 | ||
|
|
ae31ad0870 | ||
|
|
ba88ced2ba | ||
|
|
803fb9e234 | ||
|
|
d77249088f |
@@ -14,7 +14,7 @@ Returns the `id` of the object. The `id` is required when connecting objects wit
|
||||
|
||||
### addText()
|
||||
```typescript
|
||||
addText(topX:number, topY:number, text:string, formatting?:{width:number, height:number,textAlign: string, verticalAlign:string, box: boolean, boxPadding: number}):string
|
||||
addText(topX:number, topY:number, text:string, formatting?:{width?:number, height?:number,textAlign?: string, verticalAlign?:string, box?: boolean, boxPadding?: number},id?:string):string;
|
||||
```
|
||||
|
||||
Adds text to the drawing.
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# [◀ Excalidraw Automate How To](../readme.md)
|
||||
## Utility functions
|
||||
### isExcalidrawFile()
|
||||
```typescript
|
||||
isExcalidrawFile(f:TFile): boolean
|
||||
```
|
||||
Returns true if the file provided is a valid Excalidraw file (either a legacy `*.excalidraw` file or a markdown file with the excalidraw key in the front-matter).
|
||||
|
||||
### clear()
|
||||
`clear()` will clear objects from cache, but will retain element style settings.
|
||||
|
||||
@@ -38,6 +44,6 @@ Returns an HTML SVGSVGElement containing the generated drawing.
|
||||
|
||||
### createPNG()
|
||||
```typescript
|
||||
async createPNG(templatePath?:string)
|
||||
async createPNG(templatePath?:string, scale:number=1)
|
||||
```
|
||||
Returns a blob containing a PNG image of the generated drawing.
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "1.2.1",
|
||||
"minAppVersion": "0.11.13",
|
||||
"version": "1.2.11",
|
||||
"minAppVersion": "0.12.0",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
"authorUrl": "https://zsolt.blog",
|
||||
|
||||
@@ -11,11 +11,12 @@
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@excalidraw/excalidraw": "^0.9.0",
|
||||
"@zsviczian/excalidraw": "^0.9.0-onTextEditEvents-4",
|
||||
"monkey-around": "^2.2.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "^1.1.5"
|
||||
"react-scripts": "^1.1.5",
|
||||
"roughjs": "4.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.14.6",
|
||||
@@ -30,7 +31,7 @@
|
||||
"@types/react-dom": "^17.0.8",
|
||||
"cross-env": "^7.0.3",
|
||||
"nanoid": "^3.1.23",
|
||||
"obsidian": "https://github.com/obsidianmd/obsidian-api/tarball/master",
|
||||
"obsidian": "^0.12.11",
|
||||
"rollup": "^2.52.3",
|
||||
"rollup-plugin-visualizer": "^5.5.0",
|
||||
"tslib": "^2.3.0",
|
||||
|
||||
@@ -3,9 +3,10 @@ import {
|
||||
FillStyle,
|
||||
StrokeStyle,
|
||||
StrokeSharpness,
|
||||
} from "@excalidraw/excalidraw/types/element/types";
|
||||
} from "@zsviczian/excalidraw/types/element/types";
|
||||
import {
|
||||
normalizePath,
|
||||
Notice,
|
||||
TFile
|
||||
} from "obsidian"
|
||||
import ExcalidrawView from "./ExcalidrawView";
|
||||
@@ -54,7 +55,7 @@ export interface ExcalidrawAutomate extends Window {
|
||||
addRect(topX:number, topY:number, width:number, height:number):string;
|
||||
addDiamond(topX:number, topY:number, width:number, height:number):string;
|
||||
addEllipse(topX:number, topY:number, width:number, height:number):string;
|
||||
addText(topX:number, topY:number, text:string, formatting?:{width:number, height:number,textAlign: string, verticalAlign:string, box: boolean, boxPadding: number}):string;
|
||||
addText(topX:number, topY:number, text:string, formatting?:{width?:number, height?:number,textAlign?: string, verticalAlign?:string, box?: boolean, boxPadding?: number},id?:string):string;
|
||||
addLine(points: [[x:number,y:number]]):void;
|
||||
addArrow(points: [[x:number,y:number]],formatting?:{startArrowHead:string,endArrowHead:string,startObjectId:string,endObjectId:string}):void ;
|
||||
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints: number,startArrowHead:string,endArrowHead:string, padding: number}):void;
|
||||
@@ -226,7 +227,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
}
|
||||
)
|
||||
},
|
||||
async createPNG(templatePath?:string) {
|
||||
async createPNG(templatePath?:string, scale:number=1) {
|
||||
const template = templatePath ? (await getTemplate(templatePath)) : null;
|
||||
let elements = template ? template.elements : [];
|
||||
for (let i=0;i<this.elementIds.length;i++) {
|
||||
@@ -246,7 +247,8 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
{
|
||||
withBackground: plugin.settings.exportWithBackground,
|
||||
withTheme: plugin.settings.exportWithTheme
|
||||
}
|
||||
},
|
||||
scale
|
||||
)
|
||||
},
|
||||
addRect(topX:number, topY:number, width:number, height:number):string {
|
||||
@@ -267,8 +269,8 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
this.elementsDict[id] = boxedElement(id,"ellipse",topX,topY,width,height);
|
||||
return id;
|
||||
},
|
||||
addText(topX:number, topY:number, text:string, formatting?:{width:number, height:number,textAlign: string, verticalAlign:string, box: boolean, boxPadding: number}):string {
|
||||
const id = nanoid();
|
||||
addText(topX:number, topY:number, text:string, formatting?:{width?:number, height?:number,textAlign?: string, verticalAlign?:string, box?: boolean, boxPadding?: number},id?:string):string {
|
||||
if(!id) id = nanoid();
|
||||
const {w, h, baseline} = measureText(text, this.style.fontSize,this.style.fontFamily);
|
||||
const width = formatting?.width ? formatting.width : w;
|
||||
const height = formatting?.height ? formatting.height : h;
|
||||
@@ -375,7 +377,7 @@ export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
|
||||
this.canvas.theme = "light";
|
||||
this.canvas.viewBackgroundColor="#FFFFFF";
|
||||
},
|
||||
isExcalidrawFile(f:TFile) {
|
||||
isExcalidrawFile(f:TFile):boolean {
|
||||
return this.plugin.isExcalidrawFile(f);
|
||||
}
|
||||
|
||||
@@ -516,5 +518,4 @@ async function getTemplate(fileWithPath: string):Promise<{elements: any,appState
|
||||
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96)+'json\n'
|
||||
+ data + '\n'
|
||||
+ String.fromCharCode(96)+String.fromCharCode(96)+String.fromCharCode(96);
|
||||
//+data.replaceAll("[","[");
|
||||
}
|
||||
@@ -10,6 +10,10 @@ import { ExcalidrawSettings } from "./settings";
|
||||
import {
|
||||
JSON_parse
|
||||
} from "./constants";
|
||||
import { ExcalidrawTextElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
import { rawListeners } from "process";
|
||||
import { TextMode } from "./ExcalidrawView";
|
||||
import { link } from "fs";
|
||||
|
||||
const DRAWING_REG = /\n# Drawing\n(```json\n)?(.*)(```)?/gm;
|
||||
|
||||
@@ -18,11 +22,11 @@ const DRAWING_REG = /\n# Drawing\n(```json\n)?(.*)(```)?/gm;
|
||||
export const REG_LINK_BACKETS = /(!)?\[\[([^|\]]+)\|?(.+)?]]|(!)?\[(.*)\]\((.*)\)/g;
|
||||
|
||||
export function getJSON(data:string):string {
|
||||
const findJSON = DRAWING_REG;
|
||||
const res = data.matchAll(findJSON);
|
||||
const res = data.matchAll(DRAWING_REG);
|
||||
const parts = res.next();
|
||||
if(parts.value && parts.value.length>1) {
|
||||
return parts.value[2];
|
||||
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 data;
|
||||
}
|
||||
@@ -35,8 +39,7 @@ export class ExcalidrawData {
|
||||
private app:App;
|
||||
private showLinkBrackets: boolean;
|
||||
private linkPrefix: string;
|
||||
private allowParse: boolean = false;
|
||||
|
||||
private textMode: TextMode = TextMode.raw;
|
||||
constructor(plugin: ExcalidrawPlugin) {
|
||||
this.settings = plugin.settings;
|
||||
this.app = plugin.app;
|
||||
@@ -47,8 +50,7 @@ export class ExcalidrawData {
|
||||
* @param {TFile} file - the MD file containing the Excalidraw drawing
|
||||
* @returns {boolean} - true if file was loaded, false if there was an error
|
||||
*/
|
||||
public async loadData(data: string,file: TFile, allowParse:boolean):Promise<boolean> {
|
||||
//console.log("Excalidraw.Data.loadData()",{data:data,allowParse:allowParse,file:file});
|
||||
public async loadData(data: string,file: TFile, textMode:TextMode):Promise<boolean> {
|
||||
|
||||
this.file = file;
|
||||
this.textElements = new Map<string,{raw:string, parsed:string}>();
|
||||
@@ -72,7 +74,8 @@ export class ExcalidrawData {
|
||||
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
|
||||
this.scene = JSON_parse(parts.value[2]);
|
||||
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 ```
|
||||
}
|
||||
//Trim data to remove the JSON string
|
||||
data = data.substring(0,parts.value.index);
|
||||
@@ -95,7 +98,7 @@ export class ExcalidrawData {
|
||||
//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.
|
||||
this.findNewTextElementsInScene();
|
||||
await this.setAllowParse(allowParse,true);
|
||||
await this.setTextMode(textMode,true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -106,17 +109,23 @@ export class ExcalidrawData {
|
||||
this.setLinkPrefix();
|
||||
this.scene = JSON.parse(data);
|
||||
this.findNewTextElementsInScene();
|
||||
await this.setAllowParse(false,true);
|
||||
await this.setTextMode(TextMode.raw,true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async setAllowParse(allowParse:boolean,forceupdate:boolean=false) {
|
||||
this.allowParse = allowParse;
|
||||
public async setTextMode(textMode:TextMode,forceupdate:boolean=false) {
|
||||
this.textMode = textMode;
|
||||
await this.updateSceneTextElements(forceupdate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the TextElements in the Excalidraw scene based on textElements MAP in ExcalidrawData
|
||||
* Depending on textMode, TextElements will receive their raw or parsed values
|
||||
* @param forceupdate : will update text elements even if text contents has not changed, this will
|
||||
* correct sizing issues
|
||||
*/
|
||||
private async updateSceneTextElements(forceupdate:boolean=false) {
|
||||
//console.log("Excalidraw.Data.updateSceneTextElements(), forceupdate",forceupdate);
|
||||
|
||||
//update a single text element in the scene if the newText is different
|
||||
const update = (sceneTextElement:any, newText:string) => {
|
||||
if(forceupdate || newText!=sceneTextElement.text) {
|
||||
@@ -137,8 +146,8 @@ export class ExcalidrawData {
|
||||
}
|
||||
|
||||
private async getText(id:string):Promise<string> {
|
||||
if (this.allowParse) {
|
||||
if(!this.textElements.get(id)?.parsed) {
|
||||
if (this.textMode == TextMode.parsed) {
|
||||
if(!this.textElements.get(id).parsed) {
|
||||
const raw = this.textElements.get(id).raw;
|
||||
this.textElements.set(id,{raw:raw, parsed: await this.parse(raw)})
|
||||
}
|
||||
@@ -172,7 +181,12 @@ export class ExcalidrawData {
|
||||
id=nanoid();
|
||||
jsonString = jsonString.replaceAll(te.id,id); //brute force approach to replace all occurances (e.g. links, groups,etc.)
|
||||
}
|
||||
if(!this.textElements.has(id)) {
|
||||
if(te.id.length > 8 && this.textElements.has(te.id)) { //element was created with onBeforeTextSubmit
|
||||
const element = this.textElements.get(te.id);
|
||||
this.textElements.set(id,{raw: element.raw, parsed: element.parsed})
|
||||
this.textElements.delete(te.id);
|
||||
dirty = true;
|
||||
} else if(!this.textElements.has(id)) {
|
||||
dirty = true;
|
||||
this.textElements.set(id,{raw: te.text, parsed: null});
|
||||
this.parseasync(id,te.text);
|
||||
@@ -225,7 +239,7 @@ export class ExcalidrawData {
|
||||
this.textElements.set(key,{raw: el[0].text,parsed: null});
|
||||
this.parseasync(key,el[0].text);
|
||||
} else {
|
||||
const text = this.allowParse ? this.textElements.get(key).parsed : this.textElements.get(key).raw;
|
||||
const text = (this.textMode == TextMode.parsed) ? this.textElements.get(key).parsed : this.textElements.get(key).raw;
|
||||
if(text != el[0].text) {
|
||||
this.textElements.set(key,{raw: el[0].text,parsed: null});
|
||||
this.parseasync(key,el[0].text);
|
||||
@@ -239,6 +253,22 @@ export class ExcalidrawData {
|
||||
this.textElements.set(key,{raw:raw,parsed: await this.parse(raw)});
|
||||
}
|
||||
|
||||
private parseLinks(text:string, position:number, parts:any):string {
|
||||
let outString = null;
|
||||
if (parts.value[2]) {
|
||||
outString = text.substring(position,parts.value.index) +
|
||||
(this.showLinkBrackets ? "[[" : "") +
|
||||
(parts.value[3] ? parts.value[3]:parts.value[2]) + //insert alias or link text
|
||||
(this.showLinkBrackets ? "]]" : "");
|
||||
} else {
|
||||
outString = text.substring(position,parts.value.index) +
|
||||
(this.showLinkBrackets ? "[[" : "") +
|
||||
(parts.value[5] ? parts.value[5]:parts.value[6]) + //insert alias or link text
|
||||
(this.showLinkBrackets ? "]]" : "");
|
||||
}
|
||||
return outString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process aliases and block embeds
|
||||
* @param text
|
||||
@@ -269,19 +299,13 @@ export class ExcalidrawData {
|
||||
if (parts.value[1] || parts.value[4]) { //transclusion
|
||||
outString += text.substring(position,parts.value.index) +
|
||||
await getTransclusion(parts.value[1] ? parts.value[2] : parts.value[6]);
|
||||
} else if (parts.value[2]) {
|
||||
linkIcon = true;
|
||||
outString += text.substring(position,parts.value.index) +
|
||||
(this.showLinkBrackets ? "[[" : "") +
|
||||
(parts.value[3] ? parts.value[3]:parts.value[2]) + //insert alias or link text
|
||||
(this.showLinkBrackets ? "]]" : "");
|
||||
} else {
|
||||
linkIcon = true;
|
||||
outString += text.substring(position,parts.value.index) +
|
||||
(this.showLinkBrackets ? "[[" : "") +
|
||||
(parts.value[5] ? parts.value[5]:parts.value[6]) + //insert alias or link text
|
||||
(this.showLinkBrackets ? "]]" : "");
|
||||
}
|
||||
const parsedLink = this.parseLinks(text,position,parts);
|
||||
if(parsedLink) {
|
||||
linkIcon = true;
|
||||
outString += parsedLink;
|
||||
}
|
||||
}
|
||||
position = parts.value.index + parts.value[0].length;
|
||||
}
|
||||
outString += text.substring(position,text.length);
|
||||
@@ -292,6 +316,45 @@ export class ExcalidrawData {
|
||||
return outString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a quick parse of the raw text. Returns the parsed string if raw text does not include a transclusion.
|
||||
* Return null if raw text includes a transclusion.
|
||||
* This is implemented in a separate function, because by nature resolving a transclusion is an asynchronious
|
||||
* activity. Quick parse gets the job done synchronously if possible.
|
||||
* @param text
|
||||
*/
|
||||
private quickParse(text:string):string {
|
||||
const hasTransclusion = (text:string):boolean => {
|
||||
const res = text.matchAll(REG_LINK_BACKETS);
|
||||
let parts;
|
||||
while(!(parts=res.next()).done) {
|
||||
if (parts.value[1] || parts.value[4]) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (hasTransclusion(text)) return null;
|
||||
|
||||
let outString = "";
|
||||
let position = 0;
|
||||
const res = text.matchAll(REG_LINK_BACKETS);
|
||||
let linkIcon = false;
|
||||
let parts;
|
||||
while(!(parts=res.next()).done) {
|
||||
const parsedLink = this.parseLinks(text,position,parts);
|
||||
if(parsedLink) {
|
||||
linkIcon = true;
|
||||
outString += parsedLink;
|
||||
}
|
||||
position = parts.value.index + parts.value[0].length;
|
||||
}
|
||||
outString += text.substring(position,text.length);
|
||||
if (linkIcon) {
|
||||
outString = this.linkPrefix + outString;
|
||||
}
|
||||
return outString;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate markdown file representation of excalidraw drawing
|
||||
* @returns markdown string
|
||||
@@ -331,6 +394,28 @@ export class ExcalidrawData {
|
||||
public getRawText(id:string) {
|
||||
return this.textElements.get(id)?.raw;
|
||||
}
|
||||
|
||||
public getParsedText(id:string):string {
|
||||
return this.textElements.get(id)?.parsed;
|
||||
}
|
||||
|
||||
public setTextElement(elementID:string, rawText:string, updateScene:Function):string {
|
||||
const parseResult = this.quickParse(rawText); //will return the parsed result if raw text does not include transclusion
|
||||
if(parseResult) { //No transclusion
|
||||
this.textElements.set(elementID,{raw: rawText,parsed: parseResult});
|
||||
return parseResult;
|
||||
}
|
||||
//transclusion needs to be resolved asynchornously
|
||||
this.parse(rawText).then((parsedText:string)=> {
|
||||
this.textElements.set(elementID,{raw: rawText,parsed: parsedText});
|
||||
if(parsedText) updateScene(parsedText);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
public deleteTextElement(id:string) {
|
||||
this.textElements.delete(id);
|
||||
}
|
||||
|
||||
private setLinkPrefix():boolean {
|
||||
const linkPrefix = this.linkPrefix;
|
||||
@@ -354,4 +439,4 @@ export class ExcalidrawData {
|
||||
return showLinkBrackets != this.showLinkBrackets;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,16 @@ import {
|
||||
WorkspaceItem,
|
||||
Notice,
|
||||
Menu,
|
||||
TAbstractFile,
|
||||
} from "obsidian";
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import Excalidraw, {exportToSvg, getSceneVersion} from "@excalidraw/excalidraw";
|
||||
import { ExcalidrawElement } from "@excalidraw/excalidraw/types/element/types";
|
||||
import Excalidraw, {exportToSvg, getSceneVersion, loadLibraryFromBlob} from "@zsviczian/excalidraw";
|
||||
import { ExcalidrawElement,ExcalidrawTextElement } from "@zsviczian/excalidraw/types/element/types";
|
||||
import {
|
||||
AppState,
|
||||
LibraryItems
|
||||
} from "@excalidraw/excalidraw/types/types";
|
||||
} from "@zsviczian/excalidraw/types/types";
|
||||
import {
|
||||
VIEW_TYPE_EXCALIDRAW,
|
||||
ICON_NAME,
|
||||
@@ -25,17 +26,27 @@ import {
|
||||
PNG_ICON_NAME,
|
||||
SVG_ICON_NAME,
|
||||
FRONTMATTER_KEY,
|
||||
UNLOCK_ICON_NAME,
|
||||
LOCK_ICON_NAME,
|
||||
JSON_parse
|
||||
TEXT_DISPLAY_RAW_ICON_NAME,
|
||||
TEXT_DISPLAY_PARSED_ICON_NAME,
|
||||
EXIT_FULLSCREEN_ICON_NAME,
|
||||
FULLSCREEN_ICON_NAME,
|
||||
JSON_parse,
|
||||
nanoid
|
||||
} from './constants';
|
||||
import ExcalidrawPlugin from './main';
|
||||
import {ExcalidrawAutomate} from './ExcalidrawAutomate';
|
||||
import { t } from "./lang/helpers";
|
||||
import { ExcalidrawData, REG_LINK_BACKETS } from "./ExcalidrawData";
|
||||
import { checkAndCreateFolder, download, getNewUniqueFilepath, splitFolderAndFilename } from "./Utils";
|
||||
import { Prompt } from "./Prompt";
|
||||
|
||||
declare let window: ExcalidrawAutomate;
|
||||
|
||||
export enum TextMode {
|
||||
parsed,
|
||||
raw
|
||||
}
|
||||
|
||||
interface WorkspaceItemExt extends WorkspaceItem {
|
||||
containerEl: HTMLElement;
|
||||
}
|
||||
@@ -58,11 +69,14 @@ export default class ExcalidrawView extends TextFileView {
|
||||
private excalidrawRef: React.MutableRefObject<any> = null;
|
||||
private justLoaded: boolean = false;
|
||||
private plugin: ExcalidrawPlugin;
|
||||
private dirty: boolean = false;
|
||||
private dirty: string = null;
|
||||
public autosaveTimer: any = null;
|
||||
public isTextLocked:boolean = false;
|
||||
private lockedElement:HTMLElement;
|
||||
private unlockedElement:HTMLElement;
|
||||
public autosaving:boolean = false;
|
||||
public textMode:TextMode = TextMode.raw;
|
||||
private textIsParsed_Element:HTMLElement;
|
||||
private textIsRaw_Element:HTMLElement;
|
||||
private gotoFullscreen:HTMLElement;
|
||||
private exitFullscreen:HTMLElement;
|
||||
private preventReload:boolean = true;
|
||||
public compatibilityMode: boolean = false;
|
||||
|
||||
@@ -81,8 +95,8 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf('.md')) + '.excalidraw';
|
||||
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
|
||||
if(file && file instanceof TFile) this.app.vault.modify(file,JSON.stringify(scene));//data.replaceAll("[","["));
|
||||
else this.app.vault.create(filepath,JSON.stringify(scene));//.replaceAll("[","["));
|
||||
if(file && file instanceof TFile) this.app.vault.modify(file,JSON.stringify(scene));
|
||||
else this.app.vault.create(filepath,JSON.stringify(scene));
|
||||
}
|
||||
|
||||
public async saveSVG(scene?: any) {
|
||||
@@ -123,7 +137,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
withBackground: this.plugin.settings.exportWithBackground,
|
||||
withTheme: this.plugin.settings.exportWithTheme
|
||||
}
|
||||
const png = await ExcalidrawView.getPNG(scene,exportSettings);
|
||||
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));
|
||||
@@ -133,6 +147,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
async save(preventReload:boolean=true) {
|
||||
this.preventReload = preventReload;
|
||||
this.dirty = null;
|
||||
await super.save();
|
||||
}
|
||||
|
||||
@@ -141,9 +156,9 @@ export default class ExcalidrawView extends TextFileView {
|
||||
// if drawing is in Text Element Edit Unlock, then everything is raw and parse and so an async function is not required here
|
||||
getViewData () {
|
||||
//console.log("ExcalidrawView.getViewData()");
|
||||
if(this.getScene && !this.compatibilityMode) {
|
||||
|
||||
if(this.excalidrawData.syncElements(this.getScene())) {
|
||||
if(!this.getScene) return this.data;
|
||||
if(!this.compatibilityMode) {
|
||||
if(this.excalidrawData.syncElements(this.getScene()) && !this.autosaving) {
|
||||
this.loadDrawing(false);
|
||||
}
|
||||
let trimLocation = this.data.search("# Text Elements\n");
|
||||
@@ -151,26 +166,30 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if(trimLocation == -1) return this.data;
|
||||
|
||||
const scene = this.excalidrawData.scene;
|
||||
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
|
||||
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
|
||||
if(this.plugin.settings.autoexportExcalidraw) this.saveExcalidraw(scene);
|
||||
if(!this.autosaving) {
|
||||
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
|
||||
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
|
||||
if(this.plugin.settings.autoexportExcalidraw) this.saveExcalidraw(scene);
|
||||
}
|
||||
|
||||
const header = this.data.substring(0,trimLocation)
|
||||
.replace(/excalidraw-plugin:\s.*\n/,FRONTMATTER_KEY+": " + (this.isTextLocked ? "locked\n" : "unlocked\n"));
|
||||
.replace(/excalidraw-plugin:\s.*\n/,FRONTMATTER_KEY+": " + ( (this.textMode == TextMode.raw) ? "raw\n" : "parsed\n"));
|
||||
return header + this.excalidrawData.generateMD();
|
||||
}
|
||||
if(this.getScene && this.compatibilityMode) {
|
||||
if(this.compatibilityMode) {
|
||||
this.excalidrawData.syncElements(this.getScene());
|
||||
const scene = this.excalidrawData.scene;
|
||||
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
|
||||
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
|
||||
if(!this.autosaving) {
|
||||
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
|
||||
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
|
||||
}
|
||||
return JSON.stringify(scene);
|
||||
}
|
||||
else return this.data;
|
||||
return this.data;
|
||||
}
|
||||
|
||||
handleLinkClick(view: ExcalidrawView, ev:MouseEvent) {
|
||||
let text:string = this.isTextLocked
|
||||
let text:string = (this.textMode == TextMode.parsed)
|
||||
? this.excalidrawData.getRawText(this.getSelectedId())
|
||||
: this.getSelectedText();
|
||||
if(!text) {
|
||||
@@ -185,7 +204,17 @@ export default class ExcalidrawView extends TextFileView {
|
||||
//1 2 3 4 5 6
|
||||
const parts = text.matchAll(REG_LINK_BACKETS).next();
|
||||
if(!parts.value) {
|
||||
new Notice(t("TEXT_ELEMENT_EMPTY"),4000);
|
||||
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();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -210,77 +239,95 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
try {
|
||||
const f = view.file;
|
||||
if(ev.shiftKey && this.gotoFullscreen.style.display=="none") this.toggleFullscreen();
|
||||
view.app.workspace.openLinkText(text,view.file.path,ev.shiftKey);
|
||||
} catch (e) {
|
||||
new Notice(e,4000);
|
||||
}
|
||||
}
|
||||
|
||||
download(encoding:string,data:any,filename:string) {
|
||||
let element = document.createElement('a');
|
||||
element.setAttribute('href', (encoding ? encoding + ',' : '') + data);
|
||||
element.setAttribute('download', filename);
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
onload() {
|
||||
//console.log("ExcalidrawView.onload()");
|
||||
this.addAction(DISK_ICON_NAME,t("FORCE_SAVE"),async (ev)=> {
|
||||
await this.save();
|
||||
await this.save(false);
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
});
|
||||
|
||||
this.unlockedElement = this.addAction(UNLOCK_ICON_NAME,t("LOCK"), (ev) => this.lock(true));
|
||||
this.lockedElement = this.addAction(LOCK_ICON_NAME,t("UNLOCK"), (ev) => this.lock(false));
|
||||
this.textIsRaw_Element = this.addAction(TEXT_DISPLAY_RAW_ICON_NAME,t("RAW"), (ev) => this.changeTextMode(TextMode.parsed));
|
||||
this.textIsParsed_Element = this.addAction(TEXT_DISPLAY_PARSED_ICON_NAME,t("PARSED"), (ev) => this.changeTextMode(TextMode.raw));
|
||||
|
||||
this.addAction("link",t("OPEN_LINK"), (ev)=>this.handleLinkClick(this,ev));
|
||||
|
||||
|
||||
this.gotoFullscreen = this.addAction(FULLSCREEN_ICON_NAME,"",()=>this.toggleFullscreen());
|
||||
this.exitFullscreen = this.addAction(EXIT_FULLSCREEN_ICON_NAME,"",()=>this.toggleFullscreen());
|
||||
this.exitFullscreen.hide();
|
||||
//@ts-ignore
|
||||
if(this.app.isMobile) this.gotoFullscreen.hide();
|
||||
|
||||
//this is to solve sliding panes bug
|
||||
if (this.app.workspace.layoutReady) {
|
||||
(this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt).containerEl.addEventListener('scroll',(e)=>{if(this.refresh) this.refresh();});
|
||||
} else {
|
||||
this.registerEvent(this.app.workspace.on('layout-ready', async () => (this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt).containerEl.addEventListener('scroll',(e)=>{if(this.refresh) this.refresh();})));
|
||||
this.app.workspace.onLayoutReady(
|
||||
async () => (this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt).containerEl.addEventListener('scroll',(e)=>{if(this.refresh) this.refresh();})
|
||||
);
|
||||
}
|
||||
this.setupAutosaveTimer();
|
||||
}
|
||||
|
||||
public async lock(locked:boolean,reload:boolean=true) {
|
||||
//console.log("ExcalidrawView.lock(), locked",locked, "reload",reload);
|
||||
this.isTextLocked = locked;
|
||||
if(locked) {
|
||||
this.unlockedElement.hide();
|
||||
this.lockedElement.show();
|
||||
private toggleFullscreen() {
|
||||
//@ts-ignore
|
||||
if(this.app.isMobile) return;
|
||||
if(this.exitFullscreen.style.display=="none") {
|
||||
this.containerEl.requestFullscreen();
|
||||
this.gotoFullscreen.hide();
|
||||
this.exitFullscreen.show();
|
||||
} else {
|
||||
this.unlockedElement.show();
|
||||
this.lockedElement.hide();
|
||||
document.exitFullscreen();
|
||||
this.gotoFullscreen.show();
|
||||
this.exitFullscreen.hide();
|
||||
}
|
||||
this.zoomToFit();
|
||||
}
|
||||
|
||||
public async changeTextMode(textMode:TextMode,reload:boolean=true) {
|
||||
this.textMode = textMode;
|
||||
if(textMode == TextMode.parsed) {
|
||||
this.textIsRaw_Element.hide();
|
||||
this.textIsParsed_Element.show();
|
||||
} else {
|
||||
this.textIsRaw_Element.show();
|
||||
this.textIsParsed_Element.hide();
|
||||
}
|
||||
if(reload) {
|
||||
await this.save(false);
|
||||
this.excalidrawRef.current.history.clear(); //to avoid undo replacing links with parsed text
|
||||
}
|
||||
}
|
||||
|
||||
public setupAutosaveTimer() {
|
||||
const timer = async () => {
|
||||
//console.log("ExcalidrawView.autosaveTimer(), dirty", this.dirty);
|
||||
if(this.dirty) {
|
||||
console.log("autosave",Date.now());
|
||||
this.dirty = false;
|
||||
if(this.dirty && (this.dirty == this.file?.path)) {
|
||||
this.dirty = null;
|
||||
this.autosaving=true;
|
||||
if(this.excalidrawRef) await this.save();
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
this.autosaving=false;
|
||||
}
|
||||
}
|
||||
if(this.plugin.settings.autosave) {
|
||||
this.autosaveTimer = setInterval(timer,30000);
|
||||
}
|
||||
if(this.autosaveTimer) clearInterval(this.autosaveTimer); // clear previous timer if one exists
|
||||
//if(this.plugin.settings.autosave) {
|
||||
this.autosaveTimer = setInterval(timer,20000);
|
||||
//}
|
||||
}
|
||||
|
||||
//save current drawing when user closes workspace leaf
|
||||
async onunload() {
|
||||
//console.log("ExcalidrawView.onunload()");
|
||||
if(this.autosaveTimer) clearInterval(this.autosaveTimer);
|
||||
if(this.autosaveTimer) {
|
||||
clearInterval(this.autosaveTimer);
|
||||
this.autosaveTimer = null;
|
||||
}
|
||||
//if(this.excalidrawRef) await this.save();
|
||||
}
|
||||
|
||||
@@ -293,9 +340,10 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if(!this.excalidrawRef) return;
|
||||
if(!this.file) return;
|
||||
if(file) this.data = await this.app.vault.read(file);
|
||||
if(fullreload) await this.excalidrawData.loadData(this.data, this.file,this.isTextLocked);
|
||||
else await this.excalidrawData.setAllowParse(this.isTextLocked);
|
||||
if(fullreload) await this.excalidrawData.loadData(this.data, this.file,this.textMode);
|
||||
else await this.excalidrawData.setTextMode(this.textMode);
|
||||
this.loadDrawing(false);
|
||||
this.dirty = null;
|
||||
}
|
||||
|
||||
// clear the view content
|
||||
@@ -306,41 +354,47 @@ export default class ExcalidrawView extends TextFileView {
|
||||
async setViewData (data: string, clear: boolean = false) {
|
||||
this.app.workspace.onLayoutReady(async ()=>{
|
||||
//console.log("ExcalidrawView.setViewData()");
|
||||
this.dirty = null;
|
||||
this.compatibilityMode = this.file.extension == "excalidraw";
|
||||
this.plugin.settings.drawingOpenCount++;
|
||||
this.plugin.saveSettings();
|
||||
await this.plugin.loadSettings();
|
||||
this.plugin.opencount++;
|
||||
if(this.compatibilityMode) {
|
||||
this.unlockedElement.hide();
|
||||
this.lockedElement.hide();
|
||||
this.textIsRaw_Element.hide();
|
||||
this.textIsParsed_Element.hide();
|
||||
await this.excalidrawData.loadLegacyData(data,this.file);
|
||||
if (!this.plugin.settings.compatibilityMode) {
|
||||
new Notice(t("COMPATIBILITY_MODE"),4000);
|
||||
}
|
||||
} else {
|
||||
this.lock(data.search("excalidraw-plugin: locked\n")>-1,false);
|
||||
if(!(await this.excalidrawData.loadData(data, this.file,this.isTextLocked))) return;
|
||||
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;
|
||||
}
|
||||
if(clear) this.clear();
|
||||
this.loadDrawing(true)
|
||||
this.dirty = false;
|
||||
});
|
||||
}
|
||||
|
||||
private loadDrawing (justloaded:boolean) {
|
||||
private loadDrawing(justloaded:boolean) {
|
||||
//console.log("ExcalidrawView.loadDrawing, justloaded", justloaded);
|
||||
this.justLoaded = justloaded; //a flag to trigger zoom to fit after the drawing has been loaded
|
||||
const excalidrawData = this.excalidrawData.scene;
|
||||
if(this.excalidrawRef) {
|
||||
if(justloaded) {
|
||||
this.excalidrawRef.current.resetScene();
|
||||
this.excalidrawRef.current.history.clear();
|
||||
this.justLoaded = justloaded; //reset screen will clear justLoaded, so need to set it again
|
||||
}
|
||||
this.excalidrawRef.current.updateScene({
|
||||
elements: excalidrawData.elements,
|
||||
appState: excalidrawData.appState,
|
||||
commitToHistory: true,
|
||||
});
|
||||
} else {
|
||||
(async() => {
|
||||
this.instantiateExcalidraw({
|
||||
elements: excalidrawData.elements,
|
||||
appState: excalidrawData.appState,
|
||||
// scrollToContent: true,
|
||||
libraryItems: await this.getLibrary(),
|
||||
});
|
||||
})();
|
||||
@@ -387,7 +441,21 @@ export default class ExcalidrawView extends TextFileView {
|
||||
.setIcon(ICON_NAME)
|
||||
.onClick( async (ev) => {
|
||||
if(!this.getScene || !this.file) return;
|
||||
this.download('data:text/plain;charset=utf-8',encodeURIComponent(JSON.stringify(this.getScene())), this.file.basename+'.excalidraw');
|
||||
//@ts-ignore
|
||||
if(this.app.isMobile) {
|
||||
const prompt = new Prompt(this.app, "Please provide filename",this.file.basename,'filename, leave blank to cancel action');
|
||||
prompt.openAndGetValue( async (filename:string)=> {
|
||||
if(!filename) return;
|
||||
filename = filename + ".excalidraw";
|
||||
const folderpath = splitFolderAndFilename(this.file.path).folderpath;
|
||||
await checkAndCreateFolder(this.app.vault,folderpath); //create folder if it does not exist
|
||||
const fname = getNewUniqueFilepath(this.app.vault,filename,folderpath);
|
||||
this.app.vault.create(fname,JSON.stringify(this.getScene()));
|
||||
new Notice("Exported to " + fname,6000);
|
||||
});
|
||||
return;
|
||||
}
|
||||
download('data:text/plain;charset=utf-8',encodeURIComponent(JSON.stringify(this.getScene())), this.file.basename+'.excalidraw');
|
||||
});
|
||||
});
|
||||
} else {
|
||||
@@ -413,14 +481,14 @@ export default class ExcalidrawView extends TextFileView {
|
||||
withBackground: this.plugin.settings.exportWithBackground,
|
||||
withTheme: this.plugin.settings.exportWithTheme
|
||||
}
|
||||
const png = await ExcalidrawView.getPNG(this.getScene(),exportSettings);
|
||||
const png = await ExcalidrawView.getPNG(this.getScene(),exportSettings,this.plugin.settings.pngExportScale);
|
||||
if(!png) return;
|
||||
let reader = new FileReader();
|
||||
reader.readAsDataURL(png);
|
||||
const self = this;
|
||||
reader.onloadend = function() {
|
||||
let base64data = reader.result;
|
||||
self.download(null,base64data,self.file.basename+'.png');
|
||||
download(null,base64data,self.file.basename+'.png');
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -441,7 +509,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
let svg = await ExcalidrawView.getSVG(this.getScene(),exportSettings);
|
||||
if(!svg) return null;
|
||||
svg = ExcalidrawView.embedFontsInSVG(svg);
|
||||
this.download("data:image/svg+xml;base64",btoa(unescape(encodeURIComponent(svg.outerHTML))),this.file.basename+'.svg');
|
||||
download("data:image/svg+xml;base64",btoa(unescape(encodeURIComponent(svg.outerHTML))),this.file.basename+'.svg');
|
||||
return;
|
||||
}
|
||||
this.saveSVG()
|
||||
@@ -452,14 +520,14 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
|
||||
async getLibrary() {
|
||||
const data = JSON_parse(this.plugin.settings.library);
|
||||
const data = JSON_parse(this.plugin.getStencilLibrary());
|
||||
return data?.library ? data.library : [];
|
||||
}
|
||||
|
||||
|
||||
private instantiateExcalidraw(initdata: any) {
|
||||
//console.log("ExcalidrawView.instantiateExcalidraw()");
|
||||
this.dirty = false;
|
||||
this.dirty = null;
|
||||
const reactElement = React.createElement(() => {
|
||||
let previousSceneVersion = 0;
|
||||
let currentPosition = {x:0, y:0};
|
||||
@@ -529,19 +597,33 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
|
||||
const st: AppState = excalidrawRef.current.getAppState();
|
||||
const id = nanoid();
|
||||
window.ExcalidrawAutomate.reset();
|
||||
window.ExcalidrawAutomate.style.strokeColor = st.currentItemStrokeColor;
|
||||
window.ExcalidrawAutomate.style.opacity = st.currentItemOpacity;
|
||||
window.ExcalidrawAutomate.style.fontFamily = fontFamily ? fontFamily: st.currentItemFontFamily;
|
||||
window.ExcalidrawAutomate.style.fontSize = st.currentItemFontSize;
|
||||
window.ExcalidrawAutomate.style.textAlign = st.currentItemTextAlign;
|
||||
const id = window.ExcalidrawAutomate.addText(currentPosition.x, currentPosition.y, text);
|
||||
//@ts-ignore
|
||||
el.push(window.ExcalidrawAutomate.elementsDict[id]);
|
||||
excalidrawRef.current.updateScene({
|
||||
elements: el,
|
||||
appState: st,
|
||||
|
||||
const addText = (text:string) => {
|
||||
window.ExcalidrawAutomate.addText(currentPosition.x, currentPosition.y, text,null,id);
|
||||
//@ts-ignore
|
||||
const textElement = window.ExcalidrawAutomate.elementsDict[id];
|
||||
el.push(textElement);
|
||||
excalidrawRef.current.updateScene({
|
||||
elements: el,
|
||||
appState: st,
|
||||
});
|
||||
this.save(false);
|
||||
}
|
||||
const self = this;
|
||||
//setTextElement will attempt a quick parse (without processing transclusions)
|
||||
const parseResult = this.excalidrawData.setTextElement(id, text,async (parsedText:string)=>{
|
||||
addText(self.textMode==TextMode.parsed?parsedText:text);
|
||||
});
|
||||
if(parseResult) { //there were no transclusions in the raw text, quick parse was successful
|
||||
addText(self.textMode==TextMode.parsed?parseResult:text);
|
||||
}
|
||||
}
|
||||
|
||||
this.getScene = () => {
|
||||
@@ -581,9 +663,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if(!excalidrawRef?.current) return;
|
||||
excalidrawRef.current.refresh();
|
||||
};
|
||||
|
||||
let timestamp = (new Date()).getTime();
|
||||
|
||||
|
||||
return React.createElement(
|
||||
React.Fragment,
|
||||
null,
|
||||
@@ -594,40 +674,12 @@ export default class ExcalidrawView extends TextFileView {
|
||||
ref: excalidrawWrapperRef,
|
||||
key: "abc",
|
||||
onClick: (e:MouseEvent):any => {
|
||||
if(this.isTextLocked && (e.target instanceof HTMLCanvasElement) && this.getSelectedText(true)) { //text element is selected
|
||||
const now = (new Date()).getTime();
|
||||
if(now-timestamp < 600) { //double click
|
||||
let event = new MouseEvent('dblclick', {
|
||||
'view': window,
|
||||
'bubbles': true,
|
||||
'cancelable': true,
|
||||
});
|
||||
e.target.dispatchEvent(event);
|
||||
new Notice(t("UNLOCK_TO_EDIT"));
|
||||
timestamp = now;
|
||||
return;
|
||||
}
|
||||
timestamp = now;
|
||||
}
|
||||
//@ts-ignore
|
||||
if(!(e.ctrlKey||e.metaKey)) return;
|
||||
if(!(this.plugin.settings.allowCtrlClick)) return;
|
||||
if(!this.getSelectedId()) return;
|
||||
this.handleLinkClick(this,e);
|
||||
},
|
||||
onKeyDown: (ev:any) => {
|
||||
if(!this.isTextLocked) return; //text is not locked
|
||||
if(ev.keyCode!=13) return; //not an enter
|
||||
if(!(ev.target instanceof HTMLDivElement)) return;
|
||||
if(!this.getSelectedId()) return;
|
||||
const event = new MouseEvent('dblclick', {
|
||||
'view': window,
|
||||
'bubbles': true,
|
||||
'cancelable': true,
|
||||
});
|
||||
ev.target.querySelector("canvas").dispatchEvent(event);
|
||||
new Notice(t("UNLOCK_TO_EDIT"));
|
||||
},
|
||||
|
||||
}
|
||||
},
|
||||
React.createElement(Excalidraw.default, {
|
||||
ref: excalidrawRef,
|
||||
@@ -651,25 +703,67 @@ export default class ExcalidrawView extends TextFileView {
|
||||
onChange: (et:ExcalidrawElement[],st:AppState) => {
|
||||
if(this.justLoaded) {
|
||||
this.justLoaded = false;
|
||||
previousSceneVersion = Excalidraw.getSceneVersion(et);
|
||||
const e = new KeyboardEvent("keydown", {bubbles : true, cancelable : true, shiftKey : true, code:"Digit1"});
|
||||
this.contentEl.querySelector("canvas")?.dispatchEvent(e);
|
||||
this.zoomToFit();
|
||||
previousSceneVersion = getSceneVersion(et);
|
||||
return;
|
||||
}
|
||||
if (st.editingElement == null && st.resizingElement == null &&
|
||||
st.draggingElement == null && st.editingGroupId == null &&
|
||||
st.editingLinearElement == null ) {
|
||||
const sceneVersion = Excalidraw.getSceneVersion(et);
|
||||
const sceneVersion = getSceneVersion(et);
|
||||
if(sceneVersion != previousSceneVersion) {
|
||||
previousSceneVersion = sceneVersion;
|
||||
this.dirty=true;
|
||||
this.dirty=this.file?.path;
|
||||
}
|
||||
}
|
||||
},
|
||||
onLibraryChange: (items:LibraryItems) => {
|
||||
(async () => {
|
||||
this.plugin.settings.library = EXCALIDRAW_LIB_HEADER+JSON.stringify(items)+'}';
|
||||
this.plugin.setStencilLibrary(EXCALIDRAW_LIB_HEADER+JSON.stringify(items)+'}');
|
||||
await this.plugin.saveSettings();
|
||||
})();
|
||||
},
|
||||
/*onPaste: (data: ClipboardData, event: ClipboardEvent | null) => {
|
||||
console.log(data,event);
|
||||
return true;
|
||||
},*/
|
||||
onBeforeTextEdit: (textElement: ExcalidrawTextElement) => {
|
||||
if(this.autosaveTimer) { //stopping autosave to avoid autosave overwriting text while the user edits it
|
||||
clearInterval(this.autosaveTimer);
|
||||
this.autosaveTimer = null;
|
||||
}
|
||||
if(this.textMode==TextMode.parsed) return this.excalidrawData.getRawText(textElement.id);
|
||||
return null;
|
||||
},
|
||||
onBeforeTextSubmit: (textElement: ExcalidrawTextElement, text:string, isDeleted:boolean) => {
|
||||
if(isDeleted) {
|
||||
this.excalidrawData.deleteTextElement(textElement.id);
|
||||
this.dirty=this.file?.path;
|
||||
this.setupAutosaveTimer();
|
||||
return;
|
||||
}
|
||||
//If the parsed text is different than the raw text, and if View is in TextMode.parsed
|
||||
//Then I need to clear the undo history to avoid overwriting raw text with parsed text and losing links
|
||||
if(text!=textElement.text) { //the user made changes to the text
|
||||
//setTextElement will attempt a quick parse (without processing transclusions)
|
||||
const parseResult = this.excalidrawData.setTextElement(textElement.id, text,async ()=>{
|
||||
await this.save(false);
|
||||
//this callback function will only be invoked if quick parse fails, i.e. there is a transclusion in the raw text
|
||||
//thus I only check if TextMode.parsed, text is always != with parseResult
|
||||
if(this.textMode == TextMode.parsed) this.excalidrawRef.current.history.clear();
|
||||
this.setupAutosaveTimer();
|
||||
});
|
||||
if(parseResult) { //there were no transclusions in the raw text, quick parse was successful
|
||||
this.setupAutosaveTimer();
|
||||
if(this.textMode == TextMode.raw) return; //text is displayed in raw, no need to clear the history, undo will not create problems
|
||||
if(text == parseResult) return; //There were no links to parse, raw text and parsed text are equivalent
|
||||
this.excalidrawRef.current.history.clear();
|
||||
return parseResult;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.setupAutosaveTimer();
|
||||
if(this.textMode==TextMode.parsed) return this.excalidrawData.getParsedText(textElement.id);
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -678,6 +772,14 @@ export default class ExcalidrawView extends TextFileView {
|
||||
ReactDOM.render(reactElement,(this as any).contentEl);
|
||||
}
|
||||
|
||||
private zoomToFit() {
|
||||
const el = this.containerEl;
|
||||
setTimeout(()=>{
|
||||
const e = new KeyboardEvent("keydown", {bubbles : true, cancelable : true, shiftKey : true, code:"Digit1"});
|
||||
el.querySelector("canvas")?.dispatchEvent(e);
|
||||
},400)
|
||||
}
|
||||
|
||||
public static async getSVG(scene:any, exportSettings:ExportSettings):Promise<SVGSVGElement> {
|
||||
try {
|
||||
return exportToSvg({
|
||||
@@ -687,14 +789,13 @@ export default class ExcalidrawView extends TextFileView {
|
||||
exportWithDarkMode: exportSettings.withTheme ? (scene.appState?.theme=="light" ? false : true) : false,
|
||||
... scene.appState,},
|
||||
exportPadding:10,
|
||||
//metadata: "Generated by Excalidraw-Obsidian plugin",
|
||||
});
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static async getPNG(scene:any, exportSettings:ExportSettings) {
|
||||
public static async getPNG(scene:any, exportSettings:ExportSettings, scale:number = 1) {
|
||||
try {
|
||||
return await Excalidraw.exportToBlob({
|
||||
elements: scene.elements,
|
||||
@@ -705,9 +806,10 @@ export default class ExcalidrawView extends TextFileView {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ export class Prompt extends Modal {
|
||||
private promptEl: HTMLInputElement;
|
||||
private resolve: (value: string) => void;
|
||||
|
||||
constructor(app: App, private prompt_text: string, private default_value: string) {
|
||||
constructor(app: App, private prompt_text: string, private default_value: string, private placeholder:string) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export class Prompt extends Modal {
|
||||
|
||||
this.promptEl = form.createEl("input");
|
||||
this.promptEl.type = "text";
|
||||
this.promptEl.placeholder = "$\\theta$";
|
||||
this.promptEl.placeholder = this.placeholder;
|
||||
this.promptEl.value = this.default_value ?? "";
|
||||
this.promptEl.addClass("excalidraw-prompt-input")
|
||||
this.promptEl.select();
|
||||
|
||||
74
src/Utils.ts
Normal file
74
src/Utils.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Modal, normalizePath, TAbstractFile, TFolder, Vault } from "obsidian";
|
||||
import { Random } from "roughjs/bin/math";
|
||||
|
||||
/**
|
||||
* Splits a full path including a folderpath and a filename into separate folderpath and filename components
|
||||
* @param filepath
|
||||
*/
|
||||
export function splitFolderAndFilename(filepath: string):{folderpath: string, filename: string} {
|
||||
let folderpath: string, filename:string;
|
||||
const lastIndex = filepath.lastIndexOf("/");
|
||||
return {
|
||||
folderpath: normalizePath(filepath.substr(0,lastIndex)),
|
||||
filename: lastIndex==-1 ? filepath : filepath.substr(lastIndex+1),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Download data as file from Obsidian, to store on local device
|
||||
* @param encoding
|
||||
* @param data
|
||||
* @param filename
|
||||
*/
|
||||
export function download(encoding:string,data:any,filename:string) {
|
||||
let element = document.createElement('a');
|
||||
element.setAttribute('href', (encoding ? encoding + ',' : '') + data);
|
||||
element.setAttribute('download', filename);
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the image filename based on the excalidraw filename
|
||||
* @param excalidrawPath - Full filepath of ExclidrawFile
|
||||
* @param newExtension - extension of IMG file in ".extension" format
|
||||
* @returns
|
||||
*/
|
||||
export function getIMGPathFromExcalidrawFile (excalidrawPath:string,newExtension:string):string {
|
||||
const isLegacyFile:boolean = excalidrawPath.endsWith(".excalidraw");
|
||||
const replaceExtension:string = isLegacyFile ? ".excalidraw" : ".md";
|
||||
return excalidrawPath.substring(0,excalidrawPath.lastIndexOf(replaceExtension)) + newExtension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new file, if file already exists find first unique filename by adding a number to the end of the filename
|
||||
* @param filename
|
||||
* @param folderpath
|
||||
* @returns
|
||||
*/
|
||||
export function getNewUniqueFilepath(vault:Vault, filename:string, folderpath:string):string {
|
||||
let fname = normalizePath(folderpath +'/'+ filename);
|
||||
let file:TAbstractFile = vault.getAbstractFileByPath(fname);
|
||||
let i = 0;
|
||||
while(file) {
|
||||
fname = normalizePath(folderpath + '/' + filename.slice(0,filename.lastIndexOf("."))+"_"+i+filename.slice(filename.lastIndexOf(".")));
|
||||
i++;
|
||||
file = vault.getAbstractFileByPath(fname);
|
||||
}
|
||||
return fname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open or create a folderpath if it does not exist
|
||||
* @param folderpath
|
||||
*/
|
||||
export async function checkAndCreateFolder(vault:Vault,folderpath:string) {
|
||||
let folder = vault.getAbstractFileByPath(folderpath);
|
||||
if(folder && folder instanceof TFolder) return;
|
||||
await vault.createFolder(folderpath);
|
||||
}
|
||||
|
||||
let random = new Random(Date.now());
|
||||
export const randomInteger = () => Math.floor(random.next() * 2 ** 31);
|
||||
@@ -1,5 +1,4 @@
|
||||
//This is to avoid brackets littering graph view with links
|
||||
//export function JSON_stringify(x:any):string {return JSON.stringify(x).replaceAll("[","[");}
|
||||
//This is only for backward compatibility because an early version of obsidian included an encoding to avoid fantom links from littering Obsidian graph view
|
||||
export function JSON_parse(x:string):any {return JSON.parse(x.replaceAll("[","["));}
|
||||
|
||||
import {customAlphabet} from "nanoid";
|
||||
@@ -13,12 +12,12 @@ 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`,"","---", "", ""].join("\n");
|
||||
export const FRONTMATTER = ["---","",`${FRONTMATTER_KEY}: unlocked`,"","---", "==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠==", "",""].join("\n");
|
||||
export const EMPTY_MESSAGE = "Hit enter to create a new drawing";
|
||||
export const LOCK_ICON_NAME = "lock";
|
||||
export const LOCK_ICON = `<path fill="currentColor" stroke="currentColor" d="M35.715 42.855h28.57v-10.71c0-3.946-1.394-7.313-4.183-10.102-2.793-2.79-6.157-4.188-10.102-4.188-3.945 0-7.309 1.399-10.102 4.188-2.789 2.789-4.183 6.156-4.183 10.102zm46.43 5.36v32.14c0 1.489-.524 2.754-1.563 3.797-1.043 1.043-2.309 1.563-3.797 1.563h-53.57c-1.488 0-2.754-.52-3.797-1.563-1.04-1.043-1.563-2.308-1.563-3.797v-32.14c0-1.488.524-2.754 1.563-3.797 1.043-1.04 2.309-1.563 3.797-1.563H25v-10.71c0-6.848 2.457-12.727 7.367-17.637S43.157 7.145 50 7.145c6.844 0 12.723 2.453 17.633 7.363C72.543 19.418 75 25.297 75 32.145v10.71h1.785c1.488 0 2.754.524 3.797 1.563 1.04 1.043 1.563 2.309 1.563 3.797zm0 0"/>`
|
||||
export const UNLOCK_ICON_NAME = "unlock";
|
||||
export const UNLOCK_ICON = `<path fill="currentColor" stroke="currentColor" d="M96.43 32.145V46.43c0 .965-.356 1.804-1.063 2.511-.707.707-1.543 1.059-2.512 1.059h-3.57c-.965 0-1.805-.352-2.512-1.059-.707-.707-1.058-1.546-1.058-2.511V32.145c0-3.946-1.395-7.313-4.188-10.102-2.789-2.79-6.156-4.188-10.097-4.188-3.946 0-7.313 1.399-10.102 4.188-2.789 2.789-4.183 6.156-4.183 10.102v10.71H62.5c1.488 0 2.754.524 3.793 1.563 1.043 1.043 1.562 2.309 1.562 3.797v32.14c0 1.489-.52 2.754-1.562 3.797-1.04 1.043-2.305 1.563-3.793 1.563H8.93c-1.489 0-2.754-.52-3.797-1.563-1.04-1.043-1.563-2.308-1.563-3.797v-32.14c0-1.488.524-2.754 1.563-3.797 1.043-1.04 2.308-1.563 3.797-1.563h37.5v-10.71c0-6.883 2.445-12.77 7.336-17.665 4.894-4.89 10.78-7.335 17.664-7.335 6.882 0 12.77 2.445 17.66 7.335 4.894 4.895 7.34 10.782 7.34 17.665zm0 0"/>`;
|
||||
export const TEXT_DISPLAY_PARSED_ICON_NAME = "presentation";
|
||||
export const TEXT_DISPLAY_RAW_ICON_NAME = "quote-glyph";
|
||||
export const FULLSCREEN_ICON_NAME="fullscreen";
|
||||
export const EXIT_FULLSCREEN_ICON_NAME = "exit-fullscreen";
|
||||
export const DISK_ICON_NAME = "disk";
|
||||
export const DISK_ICON = `<path fill="none" stroke="currentColor" fill="#fff" d="M0 0h100v100H0z"/><path fill="none" stroke="currentColor" d="M20.832 4.168c21.824.145 43.645.289 74.68.5m-74.68-.5c17.09.113 34.176.227 74.68.5m0 0c.094 27.3.191 54.602.32 91.164m-.32-91.164c.113 32.633.23 65.27.32 91.164m0 0H4.168m91.664 0H4.168m0 0v-75m0 75v-75m0 0L20.832 4.168M4.168 20.832L20.832 4.168M20.832 4.168h58.336m-58.336 0h58.336m0 0v25m0-25v25m0 0H20.832m58.336 0H20.832m0 0v-25m0 25v-25" stroke-width="1.66668" /><path fill="none" stroke="currentColor" d="M29.168 4.168h16.664v16.664H29.168"/><path fill="none" stroke="currentColor" d="M29.168 4.168h16.664m-16.664 0h16.664m0 0v16.664m0-16.664v16.664m0 0H29.168m16.664 0H29.168m0 0V4.168m0 16.664V4.168M12.5 54.168h75m-75 0h75m0 0v41.664m0-41.664v41.664m0 0h-75m75 0h-75m0 0V54.168m0 41.664V54.168M20.832 62.5c20.11-.18 40.219-.36 55.68-.5m-55.68.5c14.656-.133 29.313-.262 55.68-.5M20.832 71.332c13.098-.117 26.2-.234 55.68-.5m-55.68.5l55.68-.5M21.117 79.582c20.645-.184 41.285-.371 55.68-.5m-55.68.5c18.153-.16 36.301-.324 55.68-.5" stroke-width="1.66668"/>`;
|
||||
export const PNG_ICON_NAME = "save-png";
|
||||
|
||||
@@ -10,6 +10,7 @@ export default {
|
||||
CREATE_NEW : "New Excalidraw drawing",
|
||||
CONVERT_FILE_KEEP_EXT: "*.excalidraw => *.excalidraw.md",
|
||||
CONVERT_FILE_REPLACE_EXT: "*.excalidraw => *.md (Logseq compatibility)",
|
||||
DOWNLOAD_LIBRARY: "Export stencil library as an *.excalidrawlib file",
|
||||
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",
|
||||
TRANSCLUDE: "Transclude (embed) a drawing",
|
||||
@@ -31,7 +32,6 @@ export default {
|
||||
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)",
|
||||
EXPORT_EXCALIDRAW: "Export to an .Excalidraw file",
|
||||
UNLOCK_TO_EDIT: "UNLOCK Text Elements to edit",
|
||||
LINK_BUTTON_CLICK_NO_TEXT: 'Select a Text Element containing an internal or external link.\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!',
|
||||
@@ -39,8 +39,8 @@ export default {
|
||||
FILENAME_INVALID_CHARS: 'File name cannot contain any of the following characters: * " \\ < > : | ?',
|
||||
FILE_DOES_NOT_EXIST: "File does not exist. Hold down ALT (or ALT+SHIFT) and CLICK link button to create a new file.",
|
||||
FORCE_SAVE: "Force-save to update transclusions in adjacent panes.\n(Please note, that autosave is always on)",
|
||||
LOCK: "Text Elements are unlocked. Click to LOCK.",
|
||||
UNLOCK: "Text Elements are locked. Click to UNLOCK.",
|
||||
RAW: "Text-elements are displayed in RAW mode. Click button to change to PREVIEW mode.",
|
||||
PARSED: "Text-elements are displayed in PREVIEW mode. Click button to change to RAW mode.",
|
||||
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",
|
||||
@@ -92,6 +92,8 @@ export default {
|
||||
EMBED_WIDTH_DESC: "The default width of an embedded drawing. You can specify a custom " +
|
||||
"width when embedding an image using the ![[drawing.excalidraw|100]] or " +
|
||||
"[[drawing.excalidraw|100x100]] format.",
|
||||
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",
|
||||
EXPORT_BACKGROUND_DESC: "If turned off, the exported image will be transparent.",
|
||||
EXPORT_THEME_NAME: "Export image with theme",
|
||||
|
||||
@@ -1,3 +1,140 @@
|
||||
// 简体中文
|
||||
|
||||
export default {};
|
||||
import { FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS, FRONTMATTER_KEY_CUSTOM_PREFIX } from "src/constants";
|
||||
|
||||
export default {
|
||||
// main.ts
|
||||
OPEN_AS_EXCALIDRAW: "打开为 Excalidraw 绘图",
|
||||
TOGGLE_MODE: "在 Excalidraw 和 Markdown 模式之间切换",
|
||||
CONVERT_NOTE_TO_EXCALIDRAW: "转换空白笔记为 Excalidraw 绘图",
|
||||
CONVERT_EXCALIDRAW: "转换 *.excalidraw 为 *.md 文件",
|
||||
CREATE_NEW : "新建 Excalidraw 绘图",
|
||||
CONVERT_FILE_KEEP_EXT: "*.excalidraw 格式 => *.excalidraw.md 格式",
|
||||
CONVERT_FILE_REPLACE_EXT: "*.excalidraw 格式 => *.md (Logseq compatibility) 格式",
|
||||
DOWNLOAD_LIBRARY: "导出 stencil 库为 *.excalidrawlib 文件",
|
||||
OPEN_EXISTING_NEW_PANE: "在新面板中打开已存在的绘图",
|
||||
OPEN_EXISTING_ACTIVE_PANE: "在当前面板中打开已存在的绘图",
|
||||
TRANSCLUDE: "嵌入绘图",
|
||||
TRANSCLUDE_MOST_RECENT: "嵌入最近编辑的绘图",
|
||||
NEW_IN_NEW_PANE: "在新面板中创建已存在的绘图",
|
||||
NEW_IN_ACTIVE_PANE: "在当前面板中创建已存在的绘图",
|
||||
NEW_IN_NEW_PANE_EMBED: "在新面板中创建已存在的绘图且嵌入到当前笔记中",
|
||||
NEW_IN_ACTIVE_PANE_EMBED: "在当前面板中创建已存在的绘图且嵌入到当前笔记中",
|
||||
EXPORT_SVG: "导出 SVG 文件到当前文件的目录中",
|
||||
EXPORT_PNG: "导出 PNG 文件到当前文件的目录中",
|
||||
TOGGLE_LOCK: "切换文本元素锁定模式",
|
||||
INSERT_LINK: "在文件中插入链接",
|
||||
INSERT_LATEX: "在文件中插入 LaTeX 符号 (e.g. $\\theta$)",
|
||||
ENTER_LATEX: "输入一个 LaTeX 表达式",
|
||||
|
||||
//ExcalidrawView.ts
|
||||
OPEN_AS_MD: "打开为 Markdown 文件",
|
||||
SAVE_AS_PNG: "保存成 PNG 文件到库里(CTRL/META 加左键点击来指定导出位置)",
|
||||
SAVE_AS_SVG: "保存成 SVG 文件到库里(CTRL/META 加左键点击来指定导出位置)",
|
||||
OPEN_LINK: "以链接的方式打开文本 \n(按住 SHIFT 来在新面板中打开)",
|
||||
EXPORT_EXCALIDRAW: "导出为 .Excalidraw 文件",
|
||||
LINK_BUTTON_CLICK_NO_TEXT: '选择带有外部链接或内部链接的文本。\n'+
|
||||
'SHIFT 加左键点击按钮来在新面板中打开链接。\n'+
|
||||
'CTRL/META 加左键在画布中点击文本元素也可以打开对应的链接。',
|
||||
TEXT_ELEMENT_EMPTY: "文本元素没有链接任何东西.",
|
||||
FILENAME_INVALID_CHARS: '文件名不能包含以下符号: * " \\ < > : | ?',
|
||||
FILE_DOES_NOT_EXIST: "文件不存在。按住 ALT(或者 ALT + SHIFT)加左键点击来创建新文件。",
|
||||
FORCE_SAVE: "强制保存以更新相邻面板中的嵌入。\n(请注意,自动保存始终处于开启状态)",
|
||||
RAW: "文本元素正以原文模式显示。 单击按钮更改为预览模式。",
|
||||
PARSED: "文本元素正以预览模式显示。 单击按钮更改为原文模式。",
|
||||
NOFILE: "Excalidraw (没有文件)",
|
||||
COMPATIBILITY_MODE: "*.excalidraw 文件以兼容模式打开。转换为新格式以获得完整的插件功能。",
|
||||
CONVERT_FILE: "转换为新格式",
|
||||
|
||||
//settings.ts
|
||||
FOLDER_NAME: "Excalidraw 文件夹",
|
||||
FOLDER_DESC: "新绘图的默认位置。如果此处为空,将在 Vault 根目录中创建绘图。",
|
||||
TEMPLATE_NAME: "Excalidraw 模板文件",
|
||||
TEMPLATE_DESC: "Excalidraw 模板的完整文件路径。" +
|
||||
"例如:如果您的模板在默认的 Excalidraw 文件夹中并且它的名称是" +
|
||||
"Template.md,设置为:Excalidraw/Template.md" +
|
||||
"如果您在兼容模式下使用 Excalidraw,那么您的模板也必须是旧的 excalidraw 文件" +
|
||||
"例如 Excalidraw/Template.excalidraw。",
|
||||
AUTOSAVE_NAME: "自动保存",
|
||||
AUTOSAVE_DESC: "每 30 秒自动保存编辑中的绘图。当您关闭 Excalidraw 或 Obsidian 或焦点移动到另一个面板时,通常会引发保存"+
|
||||
"在极少数情况下自动保存可能会稍微扰乱绘图流程。我在创建此功能时考虑到了手机端(安卓)," +
|
||||
"其中“滑到另一个应用程序”会导致一些数据丢失,并且因为我无法在手机上的应用程序" +
|
||||
" 终止时强制保存。如果您在桌面上使用 Excalidraw,这你可以关掉它。",
|
||||
FILENAME_HEAD: "文件名",
|
||||
FILENAME_DESC: "<p>自动生成的文件名包括一个前缀和一个日期." +
|
||||
"例如 'Drawing 2021-05-24 12.58.07'。</p>"+
|
||||
"<p>点击<a href='https://momentjs.com/docs/#/displaying/format/'>"+
|
||||
"日期和时间格式参考</a>来查看如何修改。</p>",
|
||||
FILENAME_SAMPLE: "当前文件名的格式为:<b>",
|
||||
FILENAME_PREFIX_NAME: "文件名前缀",
|
||||
FILENAME_PREFIX_DESC: "文件名的第一部分",
|
||||
FILENAME_DATE_NAME: "文件名日期",
|
||||
FILENAME_DATE_DESC: "文件名的第二部分",
|
||||
LINKS_HEAD: "链接",
|
||||
LINKS_DESC: "CTRL/META 加左键点击文本元素来打开链接。" +
|
||||
"如果选中的文本指向多个双链,只会打开第一个" +
|
||||
"如果选中的文本为超链接 (i.e. https:// or http://),然后" +
|
||||
"插件会在浏览器中打开。" +
|
||||
"当对应的文件名修改时,匹配的链接也会修改" +
|
||||
"如果你不希望你自己的链接文本突然修改,用别名",
|
||||
LINK_BRACKETS_NAME: "在链接上显示双链符号[[",
|
||||
LINK_BRACKETS_DESC: "在预览(锁定)模式,当解析文本元素,在链接左右展示中括号。" +
|
||||
"你可以在文件的 Frontmatter 中加入'" + FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS +
|
||||
": true/false' 来单独控制某个文件",
|
||||
LINK_PREFIX_NAME:"链接前缀",
|
||||
LINK_PREFIX_DESC:"在预览(锁定)模式,如果文本元素包含链接,在文本之前加上这些字符。" +
|
||||
"你可以在文件的 Frontmatter 中加入 \'" + FRONTMATTER_KEY_CUSTOM_PREFIX +
|
||||
': "👉 "\' 中单独更改',
|
||||
LINK_CTRL_CLICK_NAME: "CTRL 加左键点击文本来打开链接",
|
||||
LINK_CTRL_CLICK_DESC: "如果此功能干扰了您要使用的默认 Excalidraw 功能,您可以将其关闭. 如果" +
|
||||
"关闭此选项,则只有绘图标题栏中的链接按钮可以让你打开链接。",
|
||||
EMBED_HEAD: "嵌入 & 导出",
|
||||
EMBED_WIDTH_NAME: "嵌入图像的默认宽度",
|
||||
EMBED_WIDTH_DESC: "嵌入图形的默认宽度。您可以在使用" +
|
||||
"![[drawing.excalidraw|100]] 或 [[drawing.excalidraw|100x100]]" +
|
||||
"格式嵌入图像时指定自定义宽度。",
|
||||
EXPORT_PNG_SCALE_NAME: "PNG 导出图像比例",
|
||||
EXPORT_PNG_SCALE_DESC: "导出的 PNG 图像的大小比例",
|
||||
EXPORT_BACKGROUND_NAME: "导出带有背景的图像",
|
||||
EXPORT_BACKGROUND_DESC: "如果关闭,导出的图像的背景将是透明的。",
|
||||
EXPORT_THEME_NAME: "导出带有主题的图像",
|
||||
EXPORT_THEME_DESC: "导出与绘图的暗/亮主题匹配的图像。" +
|
||||
"如果关闭,在深色模式下导出的绘图将和浅色模式下导出的图像一样",
|
||||
EXPORT_HEAD: "导出设置",
|
||||
EXPORT_SYNC_NAME:"保持 .SVG 和/或 .PNG 文件名与绘图文件同步",
|
||||
EXPORT_SYNC_DESC:"打开后,当同一文件夹且同名的绘图被重命名时,插件将自动更新对应的 .SVG 和/或 .PNG 文件的文件名。" +
|
||||
"当同一文件夹的同一名称的绘图被删除时,该插件还将自动删除对应的 .SVG 和/或 .PNG 文件。",
|
||||
EXPORT_SVG_NAME: "自动导出 SVG",
|
||||
EXPORT_SVG_DESC: "自动导出和你文件同名的 SVG 文件" +
|
||||
"插件会将 SVG 文件保存到对应的 Excalidraw 所在的文件夹中"+
|
||||
"将 .svg 文件嵌入到文档中,而不是 excalidraw,使您嵌入的页面独立开来" +
|
||||
"当自动导出开关打开时,每次您编辑对应的 excalidraw 绘图时,此文件都会更新。",
|
||||
EXPORT_PNG_NAME: "自动导出 PNG",
|
||||
EXPORT_PNG_DESC: "和自动导出 SVG 一样,但面向 *.PNG",
|
||||
COMPATIBILITY_HEAD: "兼容特性",
|
||||
EXPORT_EXCALIDRAW_NAME: "自动导出 Excalidraw 文件",
|
||||
EXPORT_EXCALIDRAW_DESC: "和自动导出 SVG 一样,但面向 *.Excalidraw",
|
||||
SYNC_EXCALIDRAW_NAME: "同步 .md 格式以及 .excalidraw 格式",
|
||||
SYNC_EXCALIDRAW_DESC: "如果 *.excalidraw 文件的修改比 *.md 文件的修改更新" +
|
||||
",会根据 .excalidraw 文件更新 .md 文件中的绘图",
|
||||
COMPATIBILITY_MODE_NAME: "以旧格式创建新绘图",
|
||||
COMPATIBILITY_MODE_DESC: "通过启用此功能图形,您可以使用功能区图标、命令面板操作、 "+
|
||||
"并且文件浏览器将仍旧保留 *.excalidraw 文件。 此设置还将" +
|
||||
"关闭你打开旧格式绘图时的提醒消息",
|
||||
EXPERIMENTAL_HEAD: "实验性特性",
|
||||
EXPERIMENTAL_DESC: "这些设置不会立即生效,只有在刷新文件资源管理器或重新启动 Obsidian 时才会生效。",
|
||||
FILETYPE_NAME: "在文件浏览器中给所有的 Excalidraw 文件加上 ✏️ 标识符",
|
||||
FILETYPE_DESC: "Excalidraw 文件将使用下一个设置中定义的表情符号或文本来做标识。",
|
||||
FILETAG_NAME: "给 Excalidraw 文件设置标识符",
|
||||
FILETAG_DESC: "要显示为标识符的文本或表情符号。",
|
||||
|
||||
|
||||
|
||||
//openDrawings.ts
|
||||
SELECT_FILE: "选择一个文件后按回车。",
|
||||
NO_MATCH: "没有文件匹配你的索引。",
|
||||
SELECT_FILE_TO_LINK: "选择要为其插入链接的文件。",
|
||||
TYPE_FILENAME: "键入要选择的绘图名称。",
|
||||
SELECT_FILE_OR_TYPE_NEW: "选择现有绘图或新绘图的类型名称,然后按回车。",
|
||||
SELECT_TO_EMBED: "选择要插入到当前文档中的绘图。",
|
||||
};
|
||||
|
||||
129
src/main.ts
129
src/main.ts
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
TFile,
|
||||
TFolder,
|
||||
Plugin,
|
||||
WorkspaceLeaf,
|
||||
addIcon,
|
||||
@@ -16,6 +15,8 @@ import {
|
||||
MarkdownRenderer,
|
||||
ViewState,
|
||||
Notice,
|
||||
TFolder,
|
||||
Modal,
|
||||
} from "obsidian";
|
||||
|
||||
import {
|
||||
@@ -32,13 +33,13 @@ import {
|
||||
RERENDER_EVENT,
|
||||
FRONTMATTER_KEY,
|
||||
FRONTMATTER,
|
||||
LOCK_ICON,
|
||||
LOCK_ICON_NAME,
|
||||
UNLOCK_ICON_NAME,
|
||||
UNLOCK_ICON,
|
||||
//LOCK_ICON,
|
||||
TEXT_DISPLAY_PARSED_ICON_NAME,
|
||||
TEXT_DISPLAY_RAW_ICON_NAME,
|
||||
//UNLOCK_ICON,
|
||||
JSON_parse
|
||||
} from "./constants";
|
||||
import ExcalidrawView, {ExportSettings} from "./ExcalidrawView";
|
||||
import ExcalidrawView, {ExportSettings, TextMode} from "./ExcalidrawView";
|
||||
import {getJSON} from "./ExcalidrawData";
|
||||
import {
|
||||
ExcalidrawSettings,
|
||||
@@ -58,17 +59,20 @@ import { Prompt } from "./Prompt";
|
||||
import { around } from "monkey-around";
|
||||
import { t } from "./lang/helpers";
|
||||
import { MigrationPrompt } from "./MigrationPrompt";
|
||||
import { checkAndCreateFolder, download, getIMGPathFromExcalidrawFile, getNewUniqueFilepath } from "./Utils";
|
||||
|
||||
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 activeExcalidrawView: ExcalidrawView = null;
|
||||
public lastActiveExcalidrawFilePath: string = null;
|
||||
private hover: {linkText: string, sourcePath: string} = {linkText: null, sourcePath: null};
|
||||
private observer: MutationObserver;
|
||||
private fileExplorerObserver: MutationObserver;
|
||||
public opencount:number = 0;
|
||||
|
||||
constructor(app: App, manifest: PluginManifest) {
|
||||
super(app, manifest);
|
||||
@@ -79,8 +83,8 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
addIcon(DISK_ICON_NAME,DISK_ICON);
|
||||
addIcon(PNG_ICON_NAME,PNG_ICON);
|
||||
addIcon(SVG_ICON_NAME,SVG_ICON);
|
||||
addIcon(LOCK_ICON_NAME,LOCK_ICON);
|
||||
addIcon(UNLOCK_ICON_NAME,UNLOCK_ICON);
|
||||
//addIcon(TEXT_DISPLAY_PARSED_ICON_NAME,LOCK_ICON);
|
||||
//addIcon(TEXT_DISPLAY_RAW_ICON_NAME,UNLOCK_ICON);
|
||||
|
||||
await this.loadSettings();
|
||||
this.addSettingTab(new ExcalidrawSettingTab(this.app, this));
|
||||
@@ -102,7 +106,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
//inspiration taken from kanban:
|
||||
//https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/main.ts#L267
|
||||
this.registerMonkeyPatches();
|
||||
if(this.settings.loadCount<3) this.migrationNotice();
|
||||
if(this.settings.loadCount<1) this.migrationNotice();
|
||||
|
||||
}
|
||||
|
||||
@@ -156,7 +160,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
|
||||
if(parts.fheight) img.setAttribute("height",parts.fheight);
|
||||
img.addClass(parts.style);
|
||||
img.setAttribute("src","data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.outerHTML))));
|
||||
img.setAttribute("src","data:image/svg+xml;base64,"+btoa(unescape(encodeURIComponent(svg.outerHTML.replaceAll(" "," ")))));
|
||||
return img;
|
||||
}
|
||||
|
||||
@@ -239,7 +243,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
//@ts-ignore
|
||||
this.app.workspace.on('hover-link',hoverEvent)
|
||||
);
|
||||
|
||||
|
||||
//monitoring for div.popover.hover-popover.file-embed.is-loaded to be added to the DOM tree
|
||||
this.observer = new MutationObserver((m)=>{
|
||||
if(m.length == 0) return;
|
||||
@@ -411,6 +415,28 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
this.app.workspace.on("file-menu", fileMenuHandlerConvertReplaceExtension)
|
||||
);
|
||||
|
||||
this.addCommand({
|
||||
id: "excalidraw-download-lib",
|
||||
name: t("DOWNLOAD_LIBRARY"),
|
||||
callback: async () => {
|
||||
//@ts-ignore
|
||||
if(this.app.isMobile) {
|
||||
const prompt = new Prompt(this.app, "Please provide a filename",'my-library','filename, leave blank to cancel action');
|
||||
prompt.openAndGetValue( async (filename:string)=> {
|
||||
if(!filename) return;
|
||||
filename = filename + ".excalidrawlib";
|
||||
const folderpath = normalizePath(this.settings.folder);
|
||||
await checkAndCreateFolder(this.app.vault,folderpath); //create folder if it does not exist
|
||||
const fname = getNewUniqueFilepath(this.app.vault,filename,folderpath);
|
||||
this.app.vault.create(fname,this.settings.library);
|
||||
new Notice("Exported library to " + fname,6000);
|
||||
});
|
||||
return;
|
||||
}
|
||||
download('data:text/plain;charset=utf-8',encodeURIComponent(this.settings.library), 'my-obsidian-library.excalidrawlib');
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "excalidraw-open",
|
||||
name: t("OPEN_EXISTING_NEW_PANE"),
|
||||
@@ -505,10 +531,6 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return this.app.workspace.activeLeaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW;
|
||||
/* if(this.app.workspace.activeLeaf.view.getViewType() == VIEW_TYPE_EXCALIDRAW) {
|
||||
return !(this.app.workspace.activeLeaf.view as ExcalidrawView).compatibilityMode;
|
||||
}
|
||||
return false;*/
|
||||
} else {
|
||||
const view = this.app.workspace.activeLeaf.view;
|
||||
if (view instanceof ExcalidrawView) {
|
||||
@@ -550,7 +572,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
} else {
|
||||
const view = this.app.workspace.activeLeaf.view;
|
||||
if (view instanceof ExcalidrawView) {
|
||||
view.lock(!view.isTextLocked);
|
||||
view.changeTextMode((view.textMode==TextMode.parsed)?TextMode.raw:TextMode.parsed);
|
||||
return true;
|
||||
}
|
||||
else return false;
|
||||
@@ -586,7 +608,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
} else {
|
||||
const view = this.app.workspace.activeLeaf.view;
|
||||
if (view instanceof ExcalidrawView) {
|
||||
const prompt = new Prompt(this.app, t("ENTER_LATEX"),'');
|
||||
const prompt = new Prompt(this.app, t("ENTER_LATEX"),'','$\\theta$');
|
||||
prompt.openAndGetValue( async (formula:string)=> {
|
||||
if(!formula) return;
|
||||
const el = createEl('p');
|
||||
@@ -668,9 +690,19 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
public async convertSingleExcalidrawToMD(file: TFile, replaceExtension:boolean = false, keepOriginal:boolean = false):Promise<TFile> {
|
||||
const data = await this.app.vault.read(file);
|
||||
const filename = file.name.substr(0,file.name.lastIndexOf(".excalidraw")) + (replaceExtension ? ".md" : ".excalidraw.md");
|
||||
const fname = this.getNewUniqueFilepath(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);
|
||||
const result = await this.app.vault.create(fname,FRONTMATTER + exportSceneToMD(data));
|
||||
if (this.settings.keepInSync) {
|
||||
['.svg','.png'].forEach( (ext:string)=>{
|
||||
const oldIMGpath = file.path.substring(0,file.path.lastIndexOf(".excalidraw")) + ext;
|
||||
const imgFile = this.app.vault.getAbstractFileByPath(normalizePath(oldIMGpath));
|
||||
if(imgFile && imgFile instanceof TFile) {
|
||||
const newIMGpath = fname.substr(0,fname.lastIndexOf(".md")) + ext;
|
||||
this.app.vault.rename(imgFile,newIMGpath);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!keepOriginal) this.app.vault.delete(file);
|
||||
return result;
|
||||
}
|
||||
@@ -778,16 +810,18 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
const self = this;
|
||||
this.app.workspace.onLayoutReady(async () => {
|
||||
|
||||
|
||||
|
||||
//watch filename change to rename .svg, .png; to sync to .md; to update links
|
||||
const renameEventHandler = async (file:TAbstractFile,oldPath:string) => {
|
||||
if(!(file instanceof TFile)) return;
|
||||
if (!self.isExcalidrawFile(file)) return;
|
||||
if (!self.settings.keepInSync) return;
|
||||
if(!self.isExcalidrawFile(file)) return;
|
||||
if(!self.settings.keepInSync) return;
|
||||
['.svg','.png','.excalidraw'].forEach(async (ext:string)=>{
|
||||
const oldIMGpath = oldPath.substring(0,oldPath.lastIndexOf('.md')) + ext;
|
||||
const oldIMGpath = getIMGPathFromExcalidrawFile(oldPath,ext);
|
||||
const imgFile = self.app.vault.getAbstractFileByPath(normalizePath(oldIMGpath));
|
||||
if(imgFile && imgFile instanceof TFile) {
|
||||
const newIMGpath = file.path.substring(0,file.path.lastIndexOf('.md')) + ext;
|
||||
const newIMGpath = getIMGPathFromExcalidrawFile(file.path,ext);
|
||||
await self.app.vault.rename(imgFile,newIMGpath);
|
||||
}
|
||||
});
|
||||
@@ -816,7 +850,9 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
const deleteEventHandler = async (file:TFile) => {
|
||||
if (!(file instanceof TFile)) return;
|
||||
//@ts-ignore
|
||||
if (file.unsaveCachedData && !file.unsafeCachedData.search(/---\n[\s\S]*excalidraw-plugin:\s*(locked|unlocked)\n[\s\S]*---/gm)==-1) return;
|
||||
const isExcalidarwFile = (file.unsafeCachedData && file.unsafeCachedData.search(/---\n[\s\S]*excalidraw-plugin:\s*(locked|unlocked)\n[\s\S]*---/gm)>-1)
|
||||
|| (file.extension=="excalidraw");
|
||||
if(!isExcalidarwFile) return;
|
||||
|
||||
//close excalidraw view where this file is open
|
||||
const leaves = self.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
|
||||
@@ -829,7 +865,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
//delete PNG and SVG files as well
|
||||
if (self.settings.keepInSync) {
|
||||
['.svg','.png','.excalidraw'].forEach(async (ext:string) => {
|
||||
const imgPath = file.path.substring(0,file.path.lastIndexOf('.md')) + ext;
|
||||
const imgPath = getIMGPathFromExcalidrawFile(file.path,ext);
|
||||
const imgFile = self.app.vault.getAbstractFileByPath(normalizePath(imgPath));
|
||||
if(imgFile && imgFile instanceof TFile) {
|
||||
await self.app.vault.delete(imgFile);
|
||||
@@ -847,6 +883,9 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
for (let i=0;i<leaves.length;i++) {
|
||||
(leaves[i].view as ExcalidrawView).save();
|
||||
}
|
||||
this.settings.drawingOpenCount += this.opencount;
|
||||
this.settings.loadCount++;
|
||||
//this.saveSettings();
|
||||
}
|
||||
self.registerEvent(
|
||||
self.app.workspace.on("quit",quitEventHandler)
|
||||
@@ -881,7 +920,9 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
excalidrawLeaves.forEach((leaf) => {
|
||||
this.setMarkdownView(leaf);
|
||||
});
|
||||
|
||||
this.settings.drawingOpenCount += this.opencount;
|
||||
this.settings.loadCount++;
|
||||
//this.saveSettings();
|
||||
}
|
||||
|
||||
public embedDrawing(data:string) {
|
||||
@@ -894,7 +935,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
|
||||
}
|
||||
|
||||
private async loadSettings() {
|
||||
public async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
|
||||
@@ -902,6 +943,14 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
public getStencilLibrary():string {
|
||||
return this.settings.library;
|
||||
}
|
||||
|
||||
public setStencilLibrary(library:string) {
|
||||
this.settings.library = library;
|
||||
}
|
||||
|
||||
public triggerEmbedUpdates(filepath?:string){
|
||||
const e = document.createEvent("Event")
|
||||
e.initEvent(RERENDER_EVENT,true,false);
|
||||
@@ -961,12 +1010,9 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
|
||||
public async createDrawing(filename: string, onNewPane: boolean, foldername?: string, initData?:string) {
|
||||
const folderpath = normalizePath(foldername ? foldername: this.settings.folder);
|
||||
const folder = this.app.vault.getAbstractFileByPath(folderpath);
|
||||
if (!(folder && folder instanceof TFolder)) {
|
||||
await this.app.vault.createFolder(folderpath);
|
||||
}
|
||||
await checkAndCreateFolder(this.app.vault,folderpath); //create folder if it does not exist
|
||||
|
||||
const fname = this.getNewUniqueFilepath(filename,folderpath);
|
||||
const fname = getNewUniqueFilepath(this.app.vault,filename,folderpath);
|
||||
|
||||
if(initData) {
|
||||
this.openDrawing(await this.app.vault.create(fname,initData),onNewPane);
|
||||
@@ -995,27 +1041,10 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
} as ViewState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new file, if file already exists find first unique filename by adding a number to the end of the filename
|
||||
* @param filename
|
||||
* @param folderpath
|
||||
* @returns
|
||||
*/
|
||||
getNewUniqueFilepath(filename:string, folderpath:string):string {
|
||||
let fname = normalizePath(folderpath +'/'+ filename);
|
||||
let file:TAbstractFile = this.app.vault.getAbstractFileByPath(fname);
|
||||
let i = 0;
|
||||
while(file) {
|
||||
fname = normalizePath(folderpath + '/' + filename.slice(0,filename.lastIndexOf("."))+"_"+i+filename.slice(filename.lastIndexOf(".")));
|
||||
i++;
|
||||
file = this.app.vault.getAbstractFileByPath(fname);
|
||||
}
|
||||
return fname;
|
||||
}
|
||||
|
||||
isExcalidrawFile(f:TFile) {
|
||||
if(f.extension=="excalidraw") return true;
|
||||
const fileCache = this.app.metadataCache.getFileCache(f);
|
||||
return !!fileCache?.frontmatter && !!fileCache.frontmatter[FRONTMATTER_KEY];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import {
|
||||
App,
|
||||
PluginSettingTab,
|
||||
Setting
|
||||
Setting,
|
||||
TFile
|
||||
} from 'obsidian';
|
||||
import { VIEW_TYPE_EXCALIDRAW } from './constants';
|
||||
import ExcalidrawView from './ExcalidrawView';
|
||||
@@ -16,8 +17,9 @@ export interface ExcalidrawSettings {
|
||||
width: string,
|
||||
showLinkBrackets: boolean,
|
||||
linkPrefix: string,
|
||||
autosave: boolean;
|
||||
//autosave: boolean;
|
||||
allowCtrlClick: boolean, //if disabled only the link button in the view header will open links
|
||||
pngExportScale: number,
|
||||
exportWithTheme: boolean,
|
||||
exportWithBackground: boolean,
|
||||
keepInSync: boolean,
|
||||
@@ -25,12 +27,12 @@ export interface ExcalidrawSettings {
|
||||
autoexportPNG: boolean,
|
||||
autoexportExcalidraw: boolean,
|
||||
syncExcalidraw: boolean,
|
||||
library: string,
|
||||
compatibilityMode: boolean,
|
||||
experimentalFileType: boolean,
|
||||
experimentalFileTag: string,
|
||||
loadCount: number, //version 1.2 migration counter
|
||||
drawingOpenCount: number,
|
||||
library: string,
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
@@ -39,10 +41,11 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
drawingFilenamePrefix: 'Drawing ',
|
||||
drawingFilenameDateTime: 'YYYY-MM-DD HH.mm.ss',
|
||||
width: '400',
|
||||
linkPrefix: ">> ",
|
||||
linkPrefix: "📍",
|
||||
showLinkBrackets: true,
|
||||
autosave: false,
|
||||
//autosave: false,
|
||||
allowCtrlClick: true,
|
||||
pngExportScale: 1,
|
||||
exportWithTheme: true,
|
||||
exportWithBackground: true,
|
||||
keepInSync: false,
|
||||
@@ -50,12 +53,12 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
autoexportPNG: false,
|
||||
autoexportExcalidraw: false,
|
||||
syncExcalidraw: false,
|
||||
library: `{"type":"excalidrawlib","version":1,"library":[]}`,
|
||||
experimentalFileType: false,
|
||||
experimentalFileTag: "✏️",
|
||||
compatibilityMode: false,
|
||||
loadCount: 0,
|
||||
drawingOpenCount: 0,
|
||||
library: `{"type":"excalidrawlib","version":1,"library":[]}`,
|
||||
}
|
||||
|
||||
export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
@@ -92,7 +95,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
/* new Setting(containerEl)
|
||||
.setName(t("AUTOSAVE_NAME"))
|
||||
.setDesc(t("AUTOSAVE_DESC"))
|
||||
.addToggle(toggle => toggle
|
||||
@@ -112,7 +115,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}));*/
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("FILENAME_HEAD")});
|
||||
containerEl.createDiv('',(el) => {
|
||||
@@ -185,7 +188,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
.setName(t("LINK_PREFIX_NAME"))
|
||||
.setDesc(t("LINK_PREFIX_DESC"))
|
||||
.addText(text => text
|
||||
.setPlaceholder('>> ')
|
||||
.setPlaceholder('📍')
|
||||
.setValue(this.plugin.settings.linkPrefix)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.linkPrefix = value;
|
||||
@@ -217,6 +220,26 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
this.plugin.triggerEmbedUpdates();
|
||||
}));
|
||||
|
||||
let scaleText:HTMLDivElement;
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EXPORT_PNG_SCALE_NAME"))
|
||||
.setDesc(t("EXPORT_PNG_SCALE_DESC"))
|
||||
.addSlider(slider => slider
|
||||
.setLimits(1,5,0.5)
|
||||
.setValue(this.plugin.settings.pngExportScale)
|
||||
.onChange(async (value)=> {
|
||||
scaleText.innerText = " " + value.toString();
|
||||
this.plugin.settings.pngExportScale = value;
|
||||
this.plugin.saveSettings();
|
||||
}))
|
||||
.settingEl.createDiv('',(el)=>{
|
||||
scaleText = el;
|
||||
el.style.minWidth = "2.3em";
|
||||
el.style.textAlign = "right";
|
||||
el.innerText = " " + this.plugin.settings.pngExportScale.toString();
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("EXPORT_BACKGROUND_NAME"))
|
||||
.setDesc(t("EXPORT_BACKGROUND_DESC"))
|
||||
@@ -271,7 +294,7 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
this.plugin.settings.autoexportPNG = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
|
||||
this.containerEl.createEl('h1', {text: t("COMPATIBILITY_HEAD")});
|
||||
|
||||
new Setting(containerEl)
|
||||
|
||||
20
styles.css
20
styles.css
@@ -63,7 +63,25 @@ button.ToolIcon_type_button[title="Export"] {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
li[data-testid] {
|
||||
border: 0 !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.excalidraw .context-menu-option-separator {
|
||||
margin: 4px !important;
|
||||
}
|
||||
|
||||
.excalidraw .popover {
|
||||
padding: 0 !important;
|
||||
border-color: transparent !important;
|
||||
border: 0 !important;
|
||||
box-shadow: 0 !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
/*
|
||||
@font-face {
|
||||
font-family: "Virgil";
|
||||
src: url("https://excalidraw.com/Virgil.woff2");
|
||||
@@ -71,4 +89,4 @@ button.ToolIcon_type_button[title="Export"] {
|
||||
@font-face {
|
||||
font-family: "Cascadia";
|
||||
src: url("https://excalidraw.com/Cascadia.woff2");
|
||||
}
|
||||
}*/
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
"esnext",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"jsx": "react",
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx", "src/openDrawing.ts",
|
||||
"**/*.tsx", "src/openDrawing.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"1.1.10": "0.11.13"
|
||||
"1.2.11": "0.11.13"
|
||||
}
|
||||
|
||||
51
yarn.lock
51
yarn.lock
@@ -882,11 +882,6 @@
|
||||
"@babel/helper-validator-identifier" "^7.14.5"
|
||||
"to-fast-properties" "^2.0.0"
|
||||
|
||||
"@excalidraw/excalidraw@^0.9.0":
|
||||
"integrity" "sha512-/LrmrZnrI7LLjT6+UOpxYOPVaR9sEEPJT4afodTGFk07ZedpmWbcLk59WekurDhwuiU2vyZW3Pc/fy3pPkFpxw=="
|
||||
"resolved" "https://registry.npmjs.org/@excalidraw/excalidraw/-/excalidraw-0.9.0.tgz"
|
||||
"version" "0.9.0"
|
||||
|
||||
"@rollup/plugin-babel@^5.3.0":
|
||||
"integrity" "sha512-9uIC8HZOnVLrLHxayq/PTzw+uS25E14KPUBh5ktF+18Mjo5yK0ToMMx6epY0uEgkjwJw0aBW4x2horYXh8juWw=="
|
||||
"resolved" "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz"
|
||||
@@ -1029,6 +1024,11 @@
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
|
||||
"@zsviczian/excalidraw@^0.9.0-onTextEditEvents-4":
|
||||
"integrity" "sha512-4W/s1gbsOCpSerqgog6Nu38doy6n1h+aEy5q7DS/nodJVca5bc6wj+OcUJJeajw7NabkKofOI2RYOVw3oMgJcw=="
|
||||
"resolved" "https://registry.npmjs.org/@zsviczian/excalidraw/-/excalidraw-0.9.0-onTextEditEvents-4.tgz"
|
||||
"version" "0.9.0-onTextEditEvents-4"
|
||||
|
||||
"abab@^1.0.3":
|
||||
"integrity" "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4="
|
||||
"resolved" "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz"
|
||||
@@ -6897,14 +6897,13 @@
|
||||
dependencies:
|
||||
"isobject" "^3.0.1"
|
||||
|
||||
"obsidian@https://github.com/obsidianmd/obsidian-api/tarball/master":
|
||||
"integrity" "sha512-vXWcdTZ6XFfbzBJUOJ8axQq58KoIxkXQln0yyloUO8DhWSe1xt0xQNZIa0RxOTfsGfK5bgKk/0nOex4pJiW8bw=="
|
||||
"resolved" "https://github.com/obsidianmd/obsidian-api/tarball/master"
|
||||
"version" "0.11.13"
|
||||
"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"
|
||||
dependencies:
|
||||
"@types/codemirror" "0.0.108"
|
||||
"moment" "2.29.1"
|
||||
"yaml" "2.0.0-4"
|
||||
|
||||
"obuf@^1.0.0", "obuf@^1.1.1":
|
||||
"integrity" "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg=="
|
||||
@@ -7106,6 +7105,11 @@
|
||||
"resolved" "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz"
|
||||
"version" "0.0.1"
|
||||
|
||||
"path-data-parser@^0.1.0", "path-data-parser@0.1.0":
|
||||
"integrity" "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="
|
||||
"resolved" "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz"
|
||||
"version" "0.1.0"
|
||||
|
||||
"path-dirname@^1.0.0":
|
||||
"integrity" "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA="
|
||||
"resolved" "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz"
|
||||
@@ -7238,6 +7242,19 @@
|
||||
"resolved" "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz"
|
||||
"version" "7.0.0"
|
||||
|
||||
"points-on-curve@^0.2.0", "points-on-curve@0.2.0":
|
||||
"integrity" "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="
|
||||
"resolved" "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz"
|
||||
"version" "0.2.0"
|
||||
|
||||
"points-on-path@^0.2.1":
|
||||
"integrity" "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="
|
||||
"resolved" "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz"
|
||||
"version" "0.2.1"
|
||||
dependencies:
|
||||
"path-data-parser" "0.1.0"
|
||||
"points-on-curve" "0.2.0"
|
||||
|
||||
"portfinder@^1.0.9":
|
||||
"integrity" "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA=="
|
||||
"resolved" "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz"
|
||||
@@ -8412,6 +8429,15 @@
|
||||
optionalDependencies:
|
||||
"fsevents" "~2.3.2"
|
||||
|
||||
"roughjs@4.4.1":
|
||||
"integrity" "sha512-/RZvyVquID319VDc9HsF8wn8VPpbMBVdr4NMCi7mta9UeBBeqP6h5Hg4GZXG29DL6jwTkfMjyth/MF7Hn6Sq/w=="
|
||||
"resolved" "https://registry.npmjs.org/roughjs/-/roughjs-4.4.1.tgz"
|
||||
"version" "4.4.1"
|
||||
dependencies:
|
||||
"path-data-parser" "^0.1.0"
|
||||
"points-on-curve" "^0.2.0"
|
||||
"points-on-path" "^0.2.1"
|
||||
|
||||
"run-async@^2.2.0":
|
||||
"integrity" "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ=="
|
||||
"resolved" "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz"
|
||||
@@ -9961,11 +9987,6 @@
|
||||
"resolved" "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz"
|
||||
"version" "2.1.2"
|
||||
|
||||
"yaml@2.0.0-4":
|
||||
"integrity" "sha512-MoQoNhTFI400tkaeod+X0Vety1KD2L9dUa6pa1CVcyfcATjC/iDxoMLvqZ6U3D8c5KzxBrU2HnJH+PfaXOqI7w=="
|
||||
"resolved" "https://registry.npmjs.org/yaml/-/yaml-2.0.0-4.tgz"
|
||||
"version" "2.0.0-4"
|
||||
|
||||
"yargs-parser@^20.2.2":
|
||||
"integrity" "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw=="
|
||||
"resolved" "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz"
|
||||
|
||||
Reference in New Issue
Block a user