Compare commits

...

15 Commits

Author SHA1 Message Date
Zsolt Viczian
ebcf807501 1.2.0-alpha-2 2021-07-04 12:15:42 +02:00
Zsolt Viczian
bd155eced3 language file cleanup 2021-07-04 06:59:32 +02:00
Zsolt Viczian
2b7d0d5dc2 [[ encoding (incl. migration), template link. 2021-07-03 17:02:40 +02:00
Zsolt Viczian
2af2be2078 2.0 alpha release 2021-07-03 13:41:50 +02:00
Zsolt Viczian
6a2e010925 debugging 2021-07-03 08:15:48 +02:00
Zsolt Viczian
ea1b968d89 getViewData fixed 2021-07-02 23:33:32 +02:00
Zsolt Viczian
d1cf5d8c15 getViewData is not yet working 2021-07-02 22:33:38 +02:00
Zsolt Viczian
fc9088b251 TextElement lock / unlock 2021-07-01 06:23:24 +02:00
Zsolt Viczian
97a9a57685 Testing ExcalidrawData 2021-06-29 23:04:10 +02:00
Zsolt Viczian
47ad2da74b Excalidraw 2.0 very early draft 2021-06-27 23:14:35 +02:00
Zsolt Viczian
3551ce827a moved onLayoutReady to registerEventListenrs 2021-06-26 15:06:54 +02:00
Zsolt Viczian
d126b1ca1c minor cleanup 2021-06-26 14:34:30 +02:00
Zsolt Viczian
169d7b9919 minor refactor 2021-06-26 13:43:17 +02:00
Zsolt Viczian
b26f2f39b8 updated package.json dependencies 2021-06-26 13:27:57 +02:00
Zsolt Viczian
388f6ee92b 1.1.10 2021-06-21 20:41:15 +02:00
38 changed files with 6149 additions and 7799 deletions

View File

@@ -58,6 +58,9 @@ Part 6: Intro to Obsidian-Excalidraw: Embedding drawings (2:08)
[![Part 6: Intro to Obsidian-Excalidraw: Embedding drawings](https://user-images.githubusercontent.com/14358394/115983954-bbdd6380-a5a4-11eb-9243-f0151451afcd.jpg)](https://youtu.be/JQeJ-Hh-xAI)
# Release Notes
## 1.1.10
- When you CTRL-Click a grouped collection of objects Excalidraw will open the page based on the embedded text.
- I added a setting to disable the CTRL-click functionality should it interfere with default Excalidraw behavior for you. In my experience double-clicking achieves the same outcome as a CTRL-click on an element in a grouped collection of objects, but if you use the CTRL-click feature to select an element of a group frequently, and find the "CTRL-click to open a link" feature annoying, you can now disable it.
## 1.1.9
- I modified the behavior of Excalidraw text element links.

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "1.1.9",
"version": "1.1.10",
"minAppVersion": "0.11.13",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",

View File

@@ -1,6 +1,6 @@
{
"name": "obsidian-excalidraw-plugin",
"version": "1.0.4",
"version": "1.1.10",
"description": "This is an Obsidian.md plugin that lets you view and edit Excalidraw drawings",
"main": "main.js",
"scripts": {
@@ -11,33 +11,29 @@
"author": "",
"license": "MIT",
"dependencies": {
"@excalidraw/excalidraw": "0.8.0",
"aakansha-excalidraw": "0.8.0-bec34f2",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-scripts": "4.0.1"
"@excalidraw/excalidraw": "^0.8.0",
"monkey-around": "^2.2.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "^1.1.5"
},
"devDependencies": {
"@babel/core": "^7.3.3",
"@babel/core": "^7.14.6",
"@babel/preset-env": "^7.3.1",
"@babel/preset-react": "^7.0.0",
"@rollup/plugin-babel": "5.3.0",
"@babel/preset-react": "^7.14.5",
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-commonjs": "^15.1.0",
"@rollup/plugin-node-resolve": "^9.0.0",
"@rollup/plugin-typescript": "^6.0.0",
"@types/node": "^14.14.2",
"@types/react-dom": "^17.0.0",
"cross-env": "7.0.3",
"nanoid": "3.1.22",
"@rollup/plugin-node-resolve": "^13.0.0",
"@rollup/plugin-replace": "^2.4.2",
"@rollup/plugin-typescript": "^8.2.1",
"@types/node": "^15.12.4",
"@types/react-dom": "^17.0.8",
"cross-env": "^7.0.3",
"nanoid": "^3.1.23",
"obsidian": "https://github.com/obsidianmd/obsidian-api/tarball/master",
"postcss": "^8.2.6",
"rollup": "2.45.2",
"rollup-plugin-copy": "3.4.0",
"rollup-plugin-minify": "1.0.3",
"rollup-plugin-postcss": "^4.0.0",
"rollup-plugin-visualizer": "^5.4.1",
"tslib": "^2.0.3",
"typescript": "^4.0.3",
"webpack-bundle-analyzer": "^4.4.1"
"rollup": "^2.52.3",
"rollup-plugin-visualizer": "^5.5.0",
"tslib": "^2.3.0",
"typescript": "^4.3.4"
}
}

View File

@@ -1,7 +1,6 @@
import typescript from '@rollup/plugin-typescript';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
//import copy from 'rollup-plugin-copy';
import { env } from "process";
import babel from '@rollup/plugin-babel';
import replace from "@rollup/plugin-replace";

View File

@@ -1,18 +1,22 @@
import ExcalidrawPlugin from "./main";
import {
ExcalidrawElement,
FillStyle,
StrokeStyle,
StrokeSharpness,
FontFamily,
} from "@excalidraw/excalidraw/types/element/types";
import {customAlphabet} from "nanoid";
import {
normalizePath,
parseFrontMatterAliases,
TFile
} from "obsidian"
import ExcalidrawView from "./ExcalidrawView"
import ExcalidrawView from "./ExcalidrawView";
import { getJSON } from "./ExcalidrawData";
import {
FRONTMATTER,
nanoid,
JSON_stringify,
JSON_parse
} from "./constants";
declare type ConnectionPoint = "top"|"bottom"|"left"|"right";
@@ -58,12 +62,13 @@ export interface ExcalidrawAutomate extends Window {
connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?:{numberOfPoints: number,startArrowHead:string,endArrowHead:string, padding: number}):void;
clear(): void;
reset(): void;
isExcalidrawFile(f:TFile): boolean;
};
}
declare let window: ExcalidrawAutomate;
const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',8);
export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
export async function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
window.ExcalidrawAutomate = {
plugin: plugin,
elementIds: [],
@@ -158,7 +163,7 @@ export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
elements.push(this.elementsDict[this.elementIds[i]]);
}
navigator.clipboard.writeText(
JSON.stringify({
JSON_stringify({
"type":"excalidraw/clipboard",
"elements": elements,
}));
@@ -173,7 +178,8 @@ export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
params?.filename ? params.filename + '.excalidraw' : this.plugin.getNextDefaultFilename(),
params?.onNewPane ? params.onNewPane : false,
params?.foldername ? params.foldername : this.plugin.settings.folder,
JSON.stringify({
FRONTMATTER + exportSceneToMD(
JSON_stringify({
type: "excalidraw",
version: 2,
source: "https://excalidraw.com",
@@ -196,7 +202,7 @@ export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
currentItemEndArrowhead: template? template.appState.currentItemEndArrowhead : this.style.endArrowHead,
currentItemLinearStrokeSharpness: template? template.appState.currentItemLinearStrokeSharpness : this.style.strokeSharpness,
}
})
}))
);
},
async createSVG(templatePath?:string):Promise<SVGSVGElement> {
@@ -206,7 +212,7 @@ export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
elements.push(this.elementsDict[this.elementIds[i]]);
}
return ExcalidrawView.getSVG(
JSON.stringify({
JSON_stringify({
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
@@ -229,7 +235,7 @@ export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
elements.push(this.elementsDict[this.elementIds[i]]);
}
return ExcalidrawView.getPNG(
JSON.stringify({
JSON_stringify({
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
@@ -371,8 +377,12 @@ export function initExcalidrawAutomate(plugin: ExcalidrawPlugin) {
this.canvas.theme = "light";
this.canvas.viewBackgroundColor="#FFFFFF";
},
isExcalidrawFile(f:TFile) {
return this.plugin.isExcalidrawFile(f);
}
};
initFonts();
await initFonts();
}
export function destroyExcalidrawAutomate() {
@@ -431,11 +441,8 @@ function getFontFamily(id:number) {
}
async function initFonts () {
for (let i=0;i<3;i++) {
await (document as any).fonts.load(
window.ExcalidrawAutomate.style.fontSize.toString()+'px ' +
getFontFamily(window.ExcalidrawAutomate.style.fontFamily)
);
for (let i=1;i<=3;i++) {
await (document as any).fonts.load('20px ' + getFontFamily(i));
}
}
@@ -445,7 +452,6 @@ export function measureText (newText:string, fontSize:number, fontFamily:number)
line.style.position = "absolute";
line.style.whiteSpace = "pre";
line.style.font = fontSize.toString()+'px ' + getFontFamily(fontFamily);
// await (document as any).fonts.load(line.style.font);
body.appendChild(line);
line.innerText = newText
.split("\n")
@@ -474,7 +480,7 @@ async function getTemplate(fileWithPath: string):Promise<{elements: any,appState
const file = vault.getAbstractFileByPath(normalizePath(fileWithPath));
if(file && file instanceof TFile) {
const data = await vault.read(file);
const excalidrawData = JSON.parse(data);
const excalidrawData = JSON_parse(getJSON(data));
return {
elements: excalidrawData.elements,
appState: excalidrawData.appState,
@@ -485,3 +491,28 @@ async function getTemplate(fileWithPath: string):Promise<{elements: any,appState
appState: {},
}
}
/**
* Extracts the text elements from an Excalidraw scene into a string of ids as headers followed by the text contents
* @param {string} data - Excalidraw scene JSON string
* @returns {string} - Text starting with the "# Text Elements" header and followed by each "## id-value" and text
*/
export function exportSceneToMD(data:string): string {
if(!data) return "";
const excalidrawData = JSON_parse(data);
const textElements = excalidrawData.elements?.filter((el:any)=> el.type=="text")
let outString = '# Text Elements\n';
let id:string;
for (const te of textElements) {
id = te.id;
//replacing Excalidraw text IDs with my own, because default IDs may contain
//characters not recognized by Obsidian block references
//also Excalidraw IDs are inconveniently long
if(te.id.length>8) {
id=nanoid();
data = data.replaceAll(te.id,id); //brute force approach to replace all occurances.
}
outString += te.text+' ^'+id+'\n\n';
}
return outString + '# Drawing\n'+ data.replaceAll("[","&#91;");
}

334
src/ExcalidrawData.ts Normal file
View File

@@ -0,0 +1,334 @@
import { App, TFile } from "obsidian";
import {
nanoid,
FRONTMATTER_KEY_CUSTOM_PREFIX,
FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS,
} from "./constants";
import { measureText } from "./ExcalidrawAutomate";
import ExcalidrawPlugin from "./main";
import { ExcalidrawSettings } from "./settings";
import {
JSON_stringify,
JSON_parse
} from "./constants";
//![[link|alias]]![alias](link)
//1 2 3 4 5 6
export const REG_LINK_BACKETS = /(!)?\[\[([^|\]]+)\|?(.+)?]]|(!)?\[(.*)\]\((.*)\)/g;
export function getJSON(data:string):string {
const findJSON = /\n# Drawing\n(.*)/gm
const res = data.matchAll(findJSON);
const parts = res.next();
if(parts.value && parts.value.length>1) {
return parts.value[1];
}
return data;
}
export class ExcalidrawData {
private textElements:Map<string,{raw:string, parsed:string}> = null;
public scene:any = null;
private file:TFile = null;
private settings:ExcalidrawSettings;
private app:App;
private showLinkBrackets: boolean;
private linkPrefix: string;
private allowParse: boolean = false;
constructor(plugin: ExcalidrawPlugin) {
this.settings = plugin.settings;
this.app = plugin.app;
}
/**
* Loads a new drawing
* @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});
this.file = file;
this.textElements = new Map<string,{raw:string, parsed:string}>();
//I am storing these because if the settings change while a drawing is open parsing will run into errors during save
//The drawing will use these values until next drawing is loaded or this drawing is re-loaded
this.setShowLinkBrackets();
this.setLinkPrefix();
//Load scene: Read the JSON string after "# Drawing"
this.scene = null;
let parts = data.matchAll(/\n# Drawing\n(.*)/gm).next();
if(!(parts.value && parts.value.length>1)) return false; //JSON not found or invalid
this.scene = JSON_parse(parts.value[1]);
//Trim data to remove the JSON string
data = data.substring(0,parts.value.index);
//The Markdown # Text Elements take priority over the JSON text elements.
//i.e. if the JSON is modified to reflect the MD in case of difference
//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;
const BLOCKREF_LEN:number = " ^12345678\n\n".length;
const res = data.matchAll(/\s\^(.{8})\n/g);
while(!(parts = res.next()).done) {
const text = data.substring(position,parts.value.index);
this.textElements.set(parts.value[1],{raw: text, parsed: await this.parse(text)});
position = parts.value.index + BLOCKREF_LEN;
}
//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);
return true;
}
public async setAllowParse(allowParse:boolean,forceupdate:boolean=false) {
this.allowParse = allowParse;
await this.updateSceneTextElements(forceupdate);
}
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) {
const measure = measureText(newText,sceneTextElement.fontSize,sceneTextElement.fontFamily);
sceneTextElement.text = newText;
sceneTextElement.width = measure.w;
sceneTextElement.height = measure.h;
sceneTextElement.baseline = measure.baseline;
}
}
//update text in scene based on textElements Map
//first get scene text elements
const texts = this.scene.elements?.filter((el:any)=> el.type=="text")
for (const te of texts) {
update(te,await this.getText(te.id));
}
}
private async getText(id:string):Promise<string> {
if (this.allowParse) {
if(!this.textElements.get(id)?.parsed) {
const raw = this.textElements.get(id).raw;
this.textElements.set(id,{raw:raw, parsed: await this.parse(raw)})
}
//console.log("parsed",this.textElements.get(id).parsed);
return this.textElements.get(id).parsed;
}
//console.log("raw",this.textElements.get(id).raw);
return this.textElements.get(id).raw;
}
/**
* check for textElements in Scene missing from textElements Map
* @returns {boolean} - true if there were changes
*/
private findNewTextElementsInScene():boolean {
//console.log("Excalidraw.Data.findNewTextElementsInScene()");
//get scene text elements
const texts = this.scene.elements?.filter((el:any)=> el.type=="text")
let jsonString = JSON_stringify(this.scene);
let dirty:boolean = false; //to keep track if the json has changed
let id:string; //will be used to hold the new 8 char long ID for textelements that don't yet appear under # Text Elements
for (const te of texts) {
id = te.id;
//replacing Excalidraw text IDs with my own nanoid, because default IDs may contain
//characters not recognized by Obsidian block references
//also Excalidraw IDs are inconveniently long
if(te.id.length>8) {
dirty = true;
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)) {
dirty = true;
this.textElements.set(id,{raw: te.text, parsed: null});
this.parseasync(id,te.text);
}
}
if(dirty) { //reload scene json in case it has changed
this.scene = JSON_parse(jsonString);
}
return dirty;
}
/**
* update text element map by deleting entries that are no long in the scene
* and updating the textElement map based on the text updated in the scene
*/
private async updateTextElementsFromScene() {
//console.log("Excalidraw.Data.updateTextElementesFromScene()");
for(const key of this.textElements.keys()){
//find text element in the scene
const el = this.scene.elements?.filter((el:any)=> el.type=="text" && el.id==key);
if(el.length==0) {
this.textElements.delete(key); //if no longer in the scene, delete the text element
} else {
if(!this.textElements.has(key)) {
this.textElements.set(key,{raw: el[0].text,parsed: 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)});
}
}
}
}
}
/**
* update text element map by deleting entries that are no long in the scene
* and updating the textElement map based on the text updated in the scene
*/
private updateTextElementsFromSceneRawOnly() {
//console.log("Excalidraw.Data.updateTextElementsFromSceneRawOnly()");
for(const key of this.textElements.keys()){
//find text element in the scene
const el = this.scene.elements?.filter((el:any)=> el.type=="text" && el.id==key);
if(el.length==0) {
this.textElements.delete(key); //if no longer in the scene, delete the text element
} else {
if(!this.textElements.has(key)) {
this.textElements.set(key,{raw: el[0].text,parsed: null});
this.parseasync(key,el[0].text);
} else {
const text = this.allowParse ? this.textElements.get(key).parsed : this.textElements.get(key).raw;
if(text != el[0].text) {
this.textElements.set(key,{raw: el[0].text,parsed: null});
this.parseasync(key,el[0].text);
}
}
}
}
}
private async parseasync(key:string, raw:string) {
this.textElements.set(key,{raw:raw,parsed: await this.parse(raw)});
}
/**
* Process aliases and block embeds
* @param text
* @returns
*/
private async parse(text:string):Promise<string>{
const getTransclusion = async (text:string) => {
//file-name#^blockref
//1 2
const REG_FILE_BLOCKREF = /(.*)#\^(.*)/g;
const parts=text.matchAll(REG_FILE_BLOCKREF).next();
if(!parts.value[1] || !parts.value[2]) return text; //filename and/or blockref not found
const file = this.app.metadataCache.getFirstLinkpathDest(parts.value[1],this.file.path);
const contents = await this.app.vault.cachedRead(file);
//get transcluded line and take the part before ^blockref
const REG_TRANSCLUDE = new RegExp("(.*)\\s\\^" + parts.value[2]);
const res = contents.match(REG_TRANSCLUDE);
if(res) return res[1];
return text;//if blockref not found in file, return the input string
}
let outString = "";
let position = 0;
const res = text.matchAll(REG_LINK_BACKETS);
let linkIcon = false;
let parts;
while(!(parts=res.next()).done) {
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 ? "]]" : "");
}
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
*/
generateMD():string {
//console.log("Excalidraw.Data.generateMD()");
let outString = '# Text Elements\n';
for(const key of this.textElements.keys()){
outString += this.textElements.get(key).raw+' ^'+key+'\n\n';
}
return outString + '# Drawing\n' + JSON_stringify(this.scene);
}
public syncElements(newScene:any):boolean {
//console.log("Excalidraw.Data.syncElements()");
this.scene = JSON_parse(newScene);
const result = this.setLinkPrefix() || this.setShowLinkBrackets() || this.findNewTextElementsInScene();
this.updateTextElementsFromSceneRawOnly();
return result;
}
public async updateScene(newScene:any){
//console.log("Excalidraw.Data.updateScene()");
this.scene = JSON_parse(newScene);
const result = this.setLinkPrefix() || this.setShowLinkBrackets() || this.findNewTextElementsInScene();
await this.updateTextElementsFromScene();
if(result) {
await this.updateSceneTextElements();
return true;
};
return false;
}
public getRawText(id:string) {
return this.textElements.get(id)?.raw;
}
private setLinkPrefix():boolean {
const linkPrefix = this.linkPrefix;
const fileCache = this.app.metadataCache.getFileCache(this.file);
if (fileCache?.frontmatter && fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_PREFIX]!=null) {
this.linkPrefix=fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_PREFIX];
} else {
this.linkPrefix = this.settings.linkPrefix;
}
return linkPrefix != this.linkPrefix;
}
private setShowLinkBrackets():boolean {
const showLinkBrackets = this.showLinkBrackets;
const fileCache = this.app.metadataCache.getFileCache(this.file);
if (fileCache?.frontmatter && fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS]!=null) {
this.showLinkBrackets=fileCache.frontmatter[FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS]!=false;
} else {
this.showLinkBrackets = this.settings.showLinkBrackets;
}
return showLinkBrackets != this.showLinkBrackets;
}
}

View File

@@ -1,153 +0,0 @@
import {TFile,TAbstractFile, App} from 'obsidian';
import {EXCALIDRAW_FILE_EXTENSION, REG_LINKINDEX_BRACKETS, REG_LINKINDEX_HYPERLINK, REG_LINKINDEX_INVALIDCHARS } from './constants';
import ExcalidrawPlugin from './main';
export default class ExcalidrawLinkIndex {
private app: App;
private plugin: ExcalidrawPlugin;
public link2ex: Map<string, Set<string>>;
private ex2link: Map<string, Set<{link:string, text:string}>>;
private vaultEventHandlers:Map<string,any>;
constructor(plugin:ExcalidrawPlugin) {
this.app = plugin.app;
this.plugin = plugin;
this.link2ex = new Map<string,Set<string>>(); //file is referenced by set of excalidraw drawings
this.ex2link = new Map<string,Set<{link:string, text:string}>>(); //excalidraw drawing references these files
this.vaultEventHandlers = new Map();
}
async reloadIndex() {
this.initialize();
}
async initialize(): Promise<void> {
const link2ex = new Map<string,Set<string>>();
const ex2link = new Map<string,Set<{link:string,text:string}>>();
const timeStart = new Date().getTime();
let counter=0;
const files = this.app.vault.getFiles().filter((f)=>f.extension==EXCALIDRAW_FILE_EXTENSION);
for (const file of files) {
const links = await this.parseLinks(file);
if (links.size > 0) {
counter += links.size;
ex2link.set(file.path, links);
links.forEach((link)=>{
if(link2ex.has(link.link)) link2ex.set(link.link,link2ex.get(link.link).add(file.path));
else link2ex.set(link.link,(new Set<string>()).add(file.path));
});
}
}
this.link2ex = link2ex;
this.ex2link = ex2link;
const totalTimeMs = new Date().getTime() - timeStart;
console.log(
`Excalidraw: Parsed ${files.length} drawings and indexed ${counter} links
(${totalTimeMs / 1000.0}s)`,
);
this.registerEventHandlers();
}
public indexFile(file: TFile) {
if(file.extension != EXCALIDRAW_FILE_EXTENSION) return;
this.clearIndex(file);
this.parseLinks(file).then((links) => {
if(links.size == 0) return;
this.ex2link.set(file.path, links);
links.forEach((link)=>{
if(this.link2ex.has(link.link)) {
this.link2ex.set(link.link,this.link2ex.get(link.link).add(file.path));
}
else this.link2ex.set(link.link,(new Set<string>()).add(file.path));
});
});
//console.log("IndexFile: ", file.path, this.ex2link.get(file.path));
}
private clearIndex(file?: TFile,path?:string) {
if(file && file.extension != EXCALIDRAW_FILE_EXTENSION) return;
if(!path) path = file.path;
if(!this.ex2link.get(path)) return;
this.ex2link.get(path).forEach((ex)=> {
if(!this.link2ex.has(ex.link)) return;
const files = this.link2ex.get(ex.link);
files.delete(path);
if(files.size>0) this.link2ex.set(ex.link,files);
else this.link2ex.delete(ex.link);
});
this.ex2link.delete(path);
}
public static getLinks(textElements:any,filepath:string,app:App,validLinksOnly: boolean): Set<{link:string,text:string}>{
const links = new Set<{link:string,text:string}>();
if(!textElements) return links;
let parts, f, text;
for (const element of textElements) {
text = element.text;
parts = text?.matchAll(REG_LINKINDEX_BRACKETS).next();
if(validLinksOnly) text = ''; //clear text, if it is a valid link, parts.value[1] will hold a value
if(parts && parts.value) text = parts.value[1];
if(text!='' && !text?.match(REG_LINKINDEX_HYPERLINK) && !text?.match(REG_LINKINDEX_INVALIDCHARS)) { //not empty, not a hyperlink and not invalid filename
f = app.metadataCache.getFirstLinkpathDest(text,filepath);
if(f) {
links.add({link:f.path,text:text});
}
}
}
return links;
}
private async parseLinks(file: TFile): Promise<Set<{link:string, text:string}>> {
const fileContents = await this.app.vault.read(file);
const textElements = JSON.parse(fileContents)?.elements?.filter((el:any)=> el.type=="text");
return ExcalidrawLinkIndex.getLinks(textElements,file.path,this.app, this.plugin.settings.validLinksOnly);
}
public updateKey(oldpath:string, newpath:string) {
if (!this.link2ex.has(oldpath)) return;
this.link2ex.set(newpath,this.link2ex.get(oldpath));
this.link2ex.delete(oldpath); //old link2ex will be deleted when the .excalidraw updates trigger
}
public getLinkTextForDrawing(drawPath:string, link:string):string {
if(!this.ex2link.has(drawPath)) return;
for(const item of this.ex2link.get(drawPath)) {
if(item.link == link) return item.text;
}
return;
}
private registerEventHandlers() {
const indexAbstractFile = (file: TAbstractFile) => {
if (!(file instanceof TFile)) return;
if (file.extension != EXCALIDRAW_FILE_EXTENSION) return;
this.indexFile(file as TFile);
}
const clearIndex = (file: TFile) => {
this.clearIndex(file);
}
const rename = (file:TAbstractFile, oldPath: string) => {
if(!oldPath.endsWith("."+EXCALIDRAW_FILE_EXTENSION)) return;
this.clearIndex(null,oldPath);
indexAbstractFile(file);
}
this.app.vault.on('create', indexAbstractFile);
this.vaultEventHandlers.set("create",indexAbstractFile);
this.app.vault.on('modify', indexAbstractFile);
this.vaultEventHandlers.set("modify",indexAbstractFile);
this.app.vault.on('delete', clearIndex);
this.vaultEventHandlers.set("delete",clearIndex);
this.app.vault.on('rename', rename);
this.vaultEventHandlers.set("rename",rename);
}
public deregisterEventHandlers() {
for(const key of this.vaultEventHandlers.keys())
this.app.vault.off(key,this.vaultEventHandlers.get(key))
}
}

View File

@@ -4,12 +4,12 @@ import {
normalizePath,
TFile,
WorkspaceItem,
Notice
Notice,
Menu,
} from "obsidian";
import * as React from "react";
import * as ReactDOM from "react-dom";
import Excalidraw, {exportToSvg, getSceneVersion} from "@excalidraw/excalidraw";
//import Excalidraw, {exportToSvg, getSceneVersion} from "aakansha-excalidraw";
import { ExcalidrawElement } from "@excalidraw/excalidraw/types/element/types";
import {
AppState,
@@ -17,7 +17,6 @@ import {
} from "@excalidraw/excalidraw/types/types";
import {
VIEW_TYPE_EXCALIDRAW,
EXCALIDRAW_FILE_EXTENSION,
ICON_NAME,
EXCALIDRAW_LIB_HEADER,
VIRGIL_FONT,
@@ -25,12 +24,17 @@ import {
DISK_ICON_NAME,
PNG_ICON_NAME,
SVG_ICON_NAME,
REG_LINKINDEX_BRACKETS,
REG_LINKINDEX_HYPERLINK,
REG_LINKINDEX_INVALIDCHARS
FRONTMATTER_KEY,
UNLOCK_ICON_NAME,
LOCK_ICON_NAME,
JSON_stringify,
JSON_parse
} from './constants';
import ExcalidrawPlugin from './main';
import {ExcalidrawAutomate} from './ExcalidrawTemplate';
import {ExcalidrawAutomate} from './ExcalidrawAutomate';
import { t } from "./lang/helpers";
import { ExcalidrawData, REG_LINK_BACKETS } from "./ExcalidrawData";
declare let window: ExcalidrawAutomate;
interface WorkspaceItemExt extends WorkspaceItem {
@@ -42,31 +46,32 @@ export interface ExportSettings {
withTheme: boolean
}
const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;
const REG_LINKINDEX_INVALIDCHARS = /[<>:"\\|?*]/g;
export default class ExcalidrawView extends TextFileView {
private getScene: Function;
private getSelectedText: Function;
public addText:Function;
private refresh: Function;
private excalidrawRef: React.MutableRefObject<any>;
private justLoaded: boolean;
private excalidrawData: ExcalidrawData;
private getScene: Function = null;
private getSelectedText: Function = null;
private getSelectedId: Function = null;
public addText:Function = null;
private refresh: Function = null;
private excalidrawRef: React.MutableRefObject<any> = null;
private justLoaded: boolean = false;
private plugin: ExcalidrawPlugin;
private dirty: boolean;
private autosaveTimer: any;
private previousSceneVersion: number;
private dirty: boolean = false;
private autosaveTimer: any = null;
public isTextLocked:boolean = false;
private lockedElement:HTMLElement;
private unlockedElement:HTMLElement;
private preventReload:boolean = true;
id: string = (this.leaf as any).id;
constructor(leaf: WorkspaceLeaf, plugin: ExcalidrawPlugin) {
super(leaf);
this.getScene = null;
this.getSelectedText = null;
this.addText = null;
this.refresh = null;
this.excalidrawRef = null;
this.plugin = plugin;
this.justLoaded = false;
this.dirty = false;
this.autosaveTimer = null;
this.previousSceneVersion = 0;
this.excalidrawData = new ExcalidrawData(plugin);
}
public async saveSVG(data?: string) {
@@ -74,7 +79,7 @@ export default class ExcalidrawView extends TextFileView {
if (!this.getScene) return false;
data = this.getScene();
}
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf('.'+EXCALIDRAW_FILE_EXTENSION)) + '.svg';
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf('.md')) + '.svg';
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
const exportSettings: ExportSettings = {
withBackground: this.plugin.settings.exportWithBackground,
@@ -103,81 +108,113 @@ export default class ExcalidrawView extends TextFileView {
if (!this.getScene) return false;
data = this.getScene();
}
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf('.'+EXCALIDRAW_FILE_EXTENSION)) + '.png';
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
const exportSettings: ExportSettings = {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: this.plugin.settings.exportWithTheme
}
const png = await ExcalidrawView.getPNG(data,exportSettings);
if(!png) return;
const filepath = this.file.path.substring(0,this.file.path.lastIndexOf('.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 save(preventReload:boolean=true) {
this.preventReload = preventReload;
await super.save();
}
// get the new file content
// if drawing is in Text Element Edit Lock, then everything should be parsed and in sync
// 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) {
const scene = this.getScene();
if(this.plugin.settings.autoexportSVG) this.saveSVG(scene);
if(this.plugin.settings.autoexportPNG) this.savePNG(scene);
return scene;
if(this.excalidrawData.syncElements(scene)) {
this.loadDrawing(false);
}
let trimLocation = this.data.search("# Text Elements\n");
if(trimLocation == -1) trimLocation = this.data.search("# Drawing\n");
if(trimLocation == -1) return this.data;
const header = this.data.substring(0,trimLocation)
.replace(/excalidraw-plugin:\s.*\n/,FRONTMATTER_KEY+": " + (this.isTextLocked ? "locked\n" : "unlocked\n"));
return header + this.excalidrawData.generateMD();
}
else return this.data;
}
handleLinkClick(view: ExcalidrawView, ev:MouseEvent) {
let text = this.getSelectedText();
let text:string = this.isTextLocked
? this.excalidrawData.getRawText(this.getSelectedId())
: this.getSelectedText();
if(!text) {
new Notice('Select a text element.\n'+
'If it is a web link, it will open in a new browser window.\n'+
'Else, if it is a valid filename, Excalidraw will handle it as an Obsidian internal link.\n'+
'Use Shift+click to open it in a new pane.\n'+
'You can also ctrl/meta click on the text element in the drawing as a shortcut to using this button.',20000);
new Notice(t("LINK_BUTTON_CLICK_NO_TEXT"),20000);
return;
}
if(text.match(REG_LINKINDEX_HYPERLINK)) {
window.open(text,"_blank");
return; }
//![[link|alias]]![alias](link)
//1 2 3 4 5 6
const parts = text.matchAll(REG_LINK_BACKETS).next();
if(!parts.value) {
new Notice(t("TEXT_ELEMENT_EMPTY"),4000);
return;
}
const parts = text.matchAll(REG_LINKINDEX_BRACKETS).next();
if(view.plugin.settings.validLinksOnly) text = ''; //clear text, if it is a valid link, parts.value[1] will hold a value
if(parts.value) text = parts.value[1];
if(text=='') {
new Notice('Text element is empty, or [[valid links only]] setting is enabled in settings, and text does not contain a [[valid Obsidian link]]',4000);
text = parts.value[2] ? parts.value[2]:parts.value[6];
if(text.match(REG_LINKINDEX_HYPERLINK)) {
window.open(text,"_blank");
return;
}
if(text.search("#")>-1) text = text.substring(0,text.search("#"));
if(text.match(REG_LINKINDEX_INVALIDCHARS)) {
new Notice('File name cannot contain any of the following characters: * " \\  < > : | ?',4000);
new Notice(t("FILENAME_INVALID_CHARS"),4000);
return;
}
if (!ev.altKey) {
const file = view.app.metadataCache.getFirstLinkpathDest(text,view.file.path);
if (!file) {
new Notice("File does not exist. Hold down ALT (or ALT+SHIFT) and click link button to create a new file.", 4000);
new Notice(t("FILE_DOES_NOT_EXIST"), 4000);
return;
}
}
try {
const f = view.file;
view.app.workspace.openLinkText(text,view.file.path,ev.shiftKey).then( ()=> {
if(ev.altKey) //create new: need to reindex excalidraw file
view.plugin.linkIndex.indexFile(f);
});
view.app.workspace.openLinkText(text,view.file.path,ev.shiftKey);
} catch (e) {
new Notice(e,4000);
}
}
async onload() {
this.addAction(DISK_ICON_NAME,"Force-save now to update transclusion visible in adjacent workspace pane\n(Please note, that autosave is always on)",async (ev)=> {
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();
this.plugin.triggerEmbedUpdates();
});
this.addAction(PNG_ICON_NAME,"Export as PNG",async (ev)=>this.savePNG());
this.addAction(SVG_ICON_NAME,"Export as SVG",async (ev)=>this.saveSVG());
this.addAction("link","Open selected text as link\n(SHIFT+click to open in a new pane)", (ev)=>this.handleLinkClick(this,ev));
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.addAction("link",t("OPEN_LINK"), (ev)=>this.handleLinkClick(this,ev));
//this is to solve sliding panes bug
if (this.app.workspace.layoutReady) {
@@ -185,12 +222,27 @@ export default class ExcalidrawView extends TextFileView {
} 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.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();
} else {
this.unlockedElement.show();
this.lockedElement.hide();
}
if(reload) {
await this.save(false);
}
}
private setupAutosaveTimer() {
const timer = async () => {
//console.log("ExcalidrawView.autosaveTimer(), dirty", this.dirty);
if(this.dirty) {
this.dirty = false;
if(this.excalidrawRef) await this.save();
@@ -202,58 +254,69 @@ export default class ExcalidrawView extends TextFileView {
//save current drawing when user closes workspace leaf
async onunload() {
//console.log("ExcalidrawView.onunload()");
if(this.autosaveTimer) clearInterval(this.autosaveTimer);
if(this.excalidrawRef) await this.save();
//if(this.excalidrawRef) await this.save();
}
setViewData (data: string, clear: boolean) {
if (this.app.workspace.layoutReady) {
this.loadDrawing(data,clear);
} else {
this.registerEvent(this.app.workspace.on('layout-ready', async () => this.loadDrawing(data,clear)));
public async reload(fullreload:boolean = false, file?:TFile){
//console.log("ExcalidrawView.reload(), fullreload",fullreload,"preventReload",this.preventReload);
if(this.preventReload) {
this.preventReload = false;
return;
}
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);
this.loadDrawing(false);
}
// clear the view content
clear() {
/*if(this.excalidrawRef) {
this.excalidrawRef = null;
this.getScene = null;
this.refresh = null;
ReactDOM.unmountComponentAtNode(this.contentEl);
}*/
}
private async loadDrawing (data:string, clear:boolean) {
if(clear) this.clear();
this.justLoaded = true; //a flag to trigger zoom to fit after the drawing has been loaded
const excalidrawData = JSON.parse(data);
async setViewData (data: string, clear: boolean) {
this.app.workspace.onLayoutReady(async ()=>{
//console.log("ExcalidrawView.setViewData()");
this.lock(data.search("excalidraw-plugin: locked\n")>-1,false);
if(!(await this.excalidrawData.loadData(data, this.file,this.isTextLocked))) return;
if(clear) this.clear();
this.loadDrawing(true)
this.dirty = false;
});
}
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) {
this.excalidrawRef.current.updateScene({
elements: excalidrawData.elements,
appState: excalidrawData.appState,
});
} else {
this.instantiateExcalidraw({
elements: excalidrawData.elements,
appState: excalidrawData.appState,
scrollToContent: true,
libraryItems: await this.getLibrary(),
});
(async() => {
this.instantiateExcalidraw({
elements: excalidrawData.elements,
appState: excalidrawData.appState,
// scrollToContent: true,
libraryItems: await this.getLibrary(),
});
})();
}
}
// gets the title of the document
getDisplayText() {
if(this.file) return this.file.basename;
else return "Excalidraw (no file)";
else return t("NOFILE");
}
// confirms this view can accept csv extension
canAcceptExtension(extension: string) {
return extension == EXCALIDRAW_FILE_EXTENSION;
}
// the view type name
getViewType() {
return VIEW_TYPE_EXCALIDRAW;
@@ -264,16 +327,87 @@ export default class ExcalidrawView extends TextFileView {
return ICON_NAME;
}
onMoreOptionsMenu(menu: Menu) {
// Add a menu item to force the board to markdown view
menu
.addItem((item) => {
item
.setTitle(t("OPEN_AS_MD"))
.setIcon("document")
.onClick(async () => {
this.plugin.excalidrawFileModes[this.id || this.file.path] = "markdown";
this.plugin.setMarkdownView(this.leaf);
});
})
.addItem((item) => {
item
.setTitle(t("EXPORT_EXCALIDRAW"))
.setIcon(ICON_NAME)
.onClick( async (ev) => {
if(!this.getScene || !this.file) return;
this.download('data:text/plain;charset=utf-8',encodeURIComponent(this.getScene().replaceAll("&#91;","[")), this.file.basename+'.excalidraw');
});
})
.addItem((item) => {
item
.setTitle(t("SAVE_AS_PNG"))
.setIcon(PNG_ICON_NAME)
.onClick( async (ev)=> {
if(!this.getScene || !this.file) return;
if(ev.ctrlKey || ev.metaKey) {
const exportSettings: ExportSettings = {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: this.plugin.settings.exportWithTheme
}
const png = await ExcalidrawView.getPNG(this.getScene(),exportSettings);
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');
}
return;
}
this.savePNG();
});
})
.addItem((item) => {
item
.setTitle(t("SAVE_AS_SVG"))
.setIcon(SVG_ICON_NAME)
.onClick(async (ev)=> {
if(!this.getScene || !this.file) return;
if(ev.ctrlKey || ev.metaKey) {
const exportSettings: ExportSettings = {
withBackground: this.plugin.settings.exportWithBackground,
withTheme: this.plugin.settings.exportWithTheme
}
let svg = 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');
return;
}
this.saveSVG()
});
})
.addSeparator();
super.onMoreOptionsMenu(menu);
}
async getLibrary() {
const data = JSON.parse(this.plugin.settings.library);
const data = JSON_parse(this.plugin.settings.library);
return data?.library ? data.library : [];
}
private instantiateExcalidraw(initdata: any) {
//console.log("ExcalidrawView.instantiateExcalidraw()");
this.dirty = false;
this.previousSceneVersion = 0;
const reactElement = React.createElement(() => {
let previousSceneVersion = 0;
let currentPosition = {x:0, y:0};
const excalidrawRef = React.useRef(null);
const excalidrawWrapperRef = React.useRef(null);
@@ -301,12 +435,38 @@ export default class ExcalidrawView extends TextFileView {
return () => window.removeEventListener("resize", onResize);
}, [excalidrawWrapperRef]);
this.getSelectedText = ():string => {
this.getSelectedId = ():string => {
if(!excalidrawRef?.current) return null;
const selectedElement = excalidrawRef.current.getSceneElements().filter((el:any)=>el.id==Object.keys(excalidrawRef.current.getAppState().selectedElementIds)[0]);
if(selectedElement.length==0) return null;
if(selectedElement[0].type != "text") return null;
return selectedElement[0].text;
if(selectedElement[0].type == "text") return selectedElement[0].id; //a text element was selected. Retrun text
if(selectedElement[0].groupIds.length == 0) return null; //is the selected element part of a group?
const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of
const textElement = excalidrawRef
.current
.getSceneElements()
.filter((el:any)=>el.groupIds?.includes(group))
.filter((el:any)=>el.type=="text"); //filter for text elements of the group
if(textElement.length==0) return null; //the group had no text element member
return textElement[0].id; //return text element text
};
this.getSelectedText = (textonly:boolean=false):string => {
if(!excalidrawRef?.current) return null;
const selectedElement = excalidrawRef.current.getSceneElements().filter((el:any)=>el.id==Object.keys(excalidrawRef.current.getAppState().selectedElementIds)[0]);
if(selectedElement.length==0) return null;
if(selectedElement[0].type == "text") return selectedElement[0].text; //a text element was selected. Retrun text
if(textonly) return null;
if(selectedElement[0].groupIds.length == 0) return null; //is the selected element part of a group?
const group = selectedElement[0].groupIds[0]; //if yes, take the first group it is part of
const textElement = excalidrawRef
.current
.getSceneElements()
.filter((el:any)=>el.groupIds?.includes(group))
.filter((el:any)=>el.type=="text"); //filter for text elements of the group
if(textElement.length==0) return null; //the group had no text element member
return textElement[0].text; //return text element text
};
this.addText = (text:string, fontFamily?:1|2|3) => {
@@ -328,7 +488,6 @@ export default class ExcalidrawView extends TextFileView {
elements: el,
appState: st,
});
//console.log(currentPosition,el,st);
}
this.getScene = () => {
@@ -337,7 +496,7 @@ export default class ExcalidrawView extends TextFileView {
}
const el: ExcalidrawElement[] = excalidrawRef.current.getSceneElements();
const st: AppState = excalidrawRef.current.getAppState();
return JSON.stringify({
return JSON_stringify({
type: "excalidraw",
version: 2,
source: "https://excalidraw.com",
@@ -359,6 +518,7 @@ export default class ExcalidrawView extends TextFileView {
currentItemStartArrowhead: st.currentItemStartArrowhead,
currentItemEndArrowhead: st.currentItemEndArrowhead,
currentItemLinearStrokeSharpness: st.currentItemLinearStrokeSharpness,
gridSize: st.gridSize,
}
});
};
@@ -368,6 +528,8 @@ export default class ExcalidrawView extends TextFileView {
excalidrawRef.current.refresh();
};
let timestamp = (new Date()).getTime();
return React.createElement(
React.Fragment,
null,
@@ -378,10 +540,27 @@ 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
var 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;
}
if(!(e.ctrlKey||e.metaKey)) return;
if(!this.getSelectedText()) return;
if(!(this.plugin.settings.allowCtrlClick)) return;
if(!this.getSelectedId()) return;
this.handleLinkClick(this,e);
},
},
React.createElement(Excalidraw.default, {
ref: excalidrawRef,
@@ -403,6 +582,7 @@ 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);
}
@@ -410,15 +590,15 @@ export default class ExcalidrawView extends TextFileView {
st.draggingElement == null && st.editingGroupId == null &&
st.editingLinearElement == null ) {
const sceneVersion = Excalidraw.getSceneVersion(et);
if(sceneVersion != this.previousSceneVersion) {
this.previousSceneVersion = sceneVersion;
if(sceneVersion != previousSceneVersion) {
previousSceneVersion = sceneVersion;
this.dirty=true;
}
}
},
onLibraryChange: (items:LibraryItems) => {
(async () => {
this.plugin.settings.library = EXCALIDRAW_LIB_HEADER+JSON.stringify(items)+'}';
this.plugin.settings.library = EXCALIDRAW_LIB_HEADER+JSON_stringify(items)+'}';
await this.plugin.saveSettings();
})();
}
@@ -431,7 +611,7 @@ export default class ExcalidrawView extends TextFileView {
public static getSVG(data:string, exportSettings:ExportSettings):SVGSVGElement {
try {
const excalidrawData = JSON.parse(data);
const excalidrawData = JSON_parse(data);
return exportToSvg({
elements: excalidrawData.elements,
appState: {
@@ -448,7 +628,7 @@ export default class ExcalidrawView extends TextFileView {
public static async getPNG(data:string, exportSettings:ExportSettings) {
try {
const excalidrawData = JSON.parse(data);
const excalidrawData = JSON_parse(data);
return await Excalidraw.exportToBlob({
elements: excalidrawData.elements,
appState: {

View File

@@ -1,16 +1,24 @@
//This is to avoid brackets littering graph view with links
export function JSON_stringify(x:any):string {return JSON.stringify(x).replaceAll("[","&#91;");}
export function JSON_parse(x:string):any {return JSON.parse(x.replaceAll("&#91;","["));}
import {customAlphabet} from "nanoid";
export const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',8);
export const FRONTMATTER_KEY = "excalidraw-plugin";
export const FRONTMATTER_KEY_CUSTOM_PREFIX = "excalidraw-link-prefix";
export const FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS = "excalidraw-link-brackets";
export const VIEW_TYPE_EXCALIDRAW = "excalidraw";
export const EXCALIDRAW_FILE_EXTENSION = "excalidraw";
export const EXCALIDRAW_FILE_EXTENSION_LEN = EXCALIDRAW_FILE_EXTENSION.length;
export const ICON_NAME = "excalidraw-icon";
//export const CODEBLOCK_EXCALIDRAW = "excalidraw";
export const REG_LINKINDEX_BRACKETS = /\[\[(.+)]]/gm;
export const REG_LINKINDEX_HYPERLINK = /^\w+:\/\//;
export const REG_LINKINDEX_INVALIDCHARS = /[<>:"\\|?*]/g;
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 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 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";

62
src/lang/helpers.ts Normal file
View File

@@ -0,0 +1,62 @@
//Solution copied from obsidian-kanban: https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/lang/helpers.ts
import { moment } from "obsidian";
import ar from "./locale/ar";
import cz from "./locale/cz";
import da from "./locale/da";
import de from "./locale/de";
import en from "./locale/en";
import enGB from "./locale/en-gb";
import es from "./locale/es";
import fr from "./locale/fr";
import hi from "./locale/hi";
import id from "./locale/id";
import it from "./locale/it";
import ja from "./locale/ja";
import ko from "./locale/ko";
import nl from "./locale/nl";
import no from "./locale/no";
import pl from "./locale/pl";
import pt from "./locale/pt";
import ptBR from "./locale/pt-br";
import ro from "./locale/ro";
import ru from "./locale/ru";
import tr from "./locale/tr";
import zhCN from "./locale/zh-cn";
import zhTW from "./locale/zh-tw";
const localeMap: { [k: string]: Partial<typeof en> } = {
ar,
cs: cz,
da,
de,
en,
"en-gb": enGB,
es,
fr,
hi,
id,
it,
ja,
ko,
nl,
nn: no,
pl,
pt,
"pt-br": ptBR,
ro,
ru,
tr,
"zh-cn": zhCN,
"zh-tw": zhTW,
};
const locale = localeMap[moment.locale()];
export function t(str: keyof typeof en): string {
if (!locale) {
console.error("Error: Excalidraw locale not found", moment.locale());
}
return (locale && locale[str]) || en[str];
}

3
src/lang/locale/ar.ts Normal file
View File

@@ -0,0 +1,3 @@
// العربية
export default {};

3
src/lang/locale/cz.ts Normal file
View File

@@ -0,0 +1,3 @@
// čeština
export default {};

3
src/lang/locale/da.ts Normal file
View File

@@ -0,0 +1,3 @@
// Dansk
export default {};

3
src/lang/locale/de.ts Normal file
View File

@@ -0,0 +1,3 @@
// Deutsch
export default {};

3
src/lang/locale/en-gb.ts Normal file
View File

@@ -0,0 +1,3 @@
// British English
export default {};

107
src/lang/locale/en.ts Normal file
View File

@@ -0,0 +1,107 @@
import { FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS, FRONTMATTER_KEY_CUSTOM_PREFIX } from "src/constants";
// English
export default {
// main.ts
OPEN_AS_EXCALIDRAW: "Open as Excalidraw Drawing",
TOGGLE_MODE: "Toggle between Excalidraw and Markdown mode",
CONVERT_NOTE_TO_EXCALIDRAW: "Convert empty note to Excalidraw Drawing",
MIGRATE_TO_2: "MIGRATE to version 1.2: convert *.excalidraw to *.md files",
CREATE_NEW : "New Excalidraw drawing",
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",
TRANSCLUDE_MOST_RECENT: "Transclude (embed) the most recently edited drawing",
NEW_IN_NEW_PANE: "Create a new drawing - IN A NEW PANE",
NEW_IN_ACTIVE_PANE: "Create a new drawing - IN THE CURRENT ACTIVE PANE",
NEW_IN_NEW_PANE_EMBED: "Create a new drawing - IN A NEW PANE - and embed into active document",
NEW_IN_ACTIVE_PANE_EMBED: "Create a new drawing - IN THE CURRENT ACTIVE PANE - and embed into active document",
EXPORT_SVG: "Save as SVG next to the current file",
EXPORT_PNG: "Save as PNG next to the current file",
TOGGLE_LOCK: "Toggle Text Element edit LOCK/UNLOCK",
INSERT_LINK: "Insert link to file",
INSERT_LATEX: "Insert LaTeX-symbol (e.g. $\\theta$)",
ENTER_LATEX: "Enter a valid LaTeX expression",
//ExcalidrawView.ts
OPEN_AS_MD: "Open as Markdown",
SAVE_AS_PNG: "Save as PNG into Vault (CTRL/META+CLICK to export)",
SAVE_AS_SVG: "Save as SVG into Vault (CTRL/META+CLICK to export)",
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!',
TEXT_ELEMENT_EMPTY: "Text Element is empty, or [[valid-link|alias]] or [alias](valid-link) is not found",
FILENAME_INVALID_CHARS: 'File name cannot contain any of the following characters: * " \\  < > : | ?',
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.",
NOFILE: "Excalidraw (no file)",
//settings.ts
FOLDER_NAME: "Excalidraw folder",
FOLDER_DESC: "Default location for new drawings. If empty, drawings will be created in the Vault root.",
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.excalidraw, the setting would be: Excalidraw/Template.excalidraw",
FILENAME_HEAD: "Filenam for drawings",
FILENAME_DESC: "<p>The auto-generated filename consists of a prefix and a date. " +
"e.g.'Drawing 2021-05-24 12.58.07'.</p>"+
"<p>Click this link for the <a href='https://momentjs.com/docs/#/displaying/format/'>"+
"date and time format reference</a>.</p>",
FILENAME_SAMPLE: "The current file format is: <b>",
FILENAME_PREFIX_NAME: "Filename prefix",
FILENAME_PREFIX_DESC: "The first part of the filename",
FILENAME_DATE_NAME: "Filename date",
FILENAME_DATE_DESC: "The second part of the filename",
LINKS_HEAD: "Links in drawings",
LINKS_DESC: "CTRL/META + CLICK on Text Elements to open them as links. " +
"If the selected text has more than one [[valid Obsidian links]], only the first will be opened. " +
"If the text starts as a valid web link (i.e. https:// or http://), then " +
"the plugin will open it in a browser. " +
"When Obsidian files change, the matching [[link]] in your drawings will also change. " +
"If you don't want text accidentally changing in your drawings use [[links|with aliases]].",
LINK_BRACKETS_NAME: "Show [[brackets]] around links",
LINK_BRACKETS_DESC: "In preview (locked) mode, when parsing Text Elements, place brackets around links. " +
"You can override this setting for a specific drawing by adding '" + FRONTMATTER_KEY_CUSTOM_LINK_BRACKETS +
": true/false' to the file\'s frontmatter.",
LINK_PREFIX_NAME:"Link prefix",
LINK_PREFIX_DESC:"In preview (locked) mode, if the Text Element contains a link, precede the text with these characters. " +
"You can override this setting for a specific drawing by adding \'" + FRONTMATTER_KEY_CUSTOM_PREFIX +
': "👉 "\' to the file\'s frontmatter.',
LINK_CTRL_CLICK_NAME: "CTRL + CLICK on text to open them as links",
LINK_CTRL_CLICK_DESC: "You can turn this feature off if it interferes with default Excalidraw features you want to use. If " +
"this is turned off, only the link button in the title bar of the drawing pane will open links.",
EMBED_HEAD: "Embedded/Export image settings",
EMBED_WIDTH_NAME: "Default width of embedded (transcluded) image",
EMBED_WIDTH_DESC: "The default width of an embedded drawing. You can specify a custom " +
"width when embedding an image using the ![[drawing.excalidraw|100]] or " +
"[[drawing.excalidraw|100x100]] format.",
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",
EXPORT_THEME_DESC: "Export the image matching the dark/light theme of your drawing. If turned off, " +
"drawings created in drak mode will appear as they would in light mode.",
EXPORT_SVG_NAME: "Auto-export SVG",
EXPORT_SVG_DESC: "Automatically create an SVG export of your drawing matching the title of your file. " +
"The plugin will save the .SVG file in the same folder as the drawing. "+
"Embed the .svg file into your documents instead of excalidraw making you embeds platform independent. " +
"While the auto-export switch is on, this file will get updated every time you edit the excalidraw drawing with the matching name.",
EXPORT_PNG_NAME: "Auto-export PNG",
EXPORT_PNG_DESC: "Same as the auto-export SVG, but for PNG.",
EXPORT_SYNC_NAME:"Keep the .SVG and/or .PNG filenames in sync with the drawing file",
EXPORT_SYNC_DESC:"When turned on, the plugin will automaticaly update the filename of the .SVG and/or .PNG files when the drawing in the same folder (and same name) is renamed. " +
"The plugin will also automatically delete the .SVG and/or .PNG files when the drawing in the same folder (and same name) is deleted. ",
//openDrawings.ts
SELECT_FILE: "Select a file then press enter.",
NO_MATCH: "No file matches your query.",
SELECT_FILE_TO_LINK: "Select the file you want to insert the link for.",
TYPE_FILENAME: "Type name of drawing to select.",
SELECT_FILE_OR_TYPE_NEW: "Select existing drawing or type name of a new drawing then press Enter.",
SELECT_TO_EMBED: "Select the drawing to insert into active document.",
};

3
src/lang/locale/es.ts Normal file
View File

@@ -0,0 +1,3 @@
// Español
export default {};

3
src/lang/locale/fr.ts Normal file
View File

@@ -0,0 +1,3 @@
// français
export default {};

3
src/lang/locale/hi.ts Normal file
View File

@@ -0,0 +1,3 @@
// हिन्दी
export default {};

3
src/lang/locale/id.ts Normal file
View File

@@ -0,0 +1,3 @@
// Bahasa Indonesia
export default {};

3
src/lang/locale/it.ts Normal file
View File

@@ -0,0 +1,3 @@
// Italiano
export default {};

3
src/lang/locale/ja.ts Normal file
View File

@@ -0,0 +1,3 @@
// 日本語
export default {};

3
src/lang/locale/ko.ts Normal file
View File

@@ -0,0 +1,3 @@
// 한국어
export default {};

3
src/lang/locale/nl.ts Normal file
View File

@@ -0,0 +1,3 @@
// Nederlands
export default {};

3
src/lang/locale/no.ts Normal file
View File

@@ -0,0 +1,3 @@
// Norsk
export default {};

3
src/lang/locale/pl.ts Normal file
View File

@@ -0,0 +1,3 @@
// język polski
export default {};

4
src/lang/locale/pt-br.ts Normal file
View File

@@ -0,0 +1,4 @@
// Português do Brasil
// Brazilian Portuguese
export default {};

3
src/lang/locale/pt.ts Normal file
View File

@@ -0,0 +1,3 @@
// Português
export default {};

3
src/lang/locale/ro.ts Normal file
View File

@@ -0,0 +1,3 @@
// Română
export default {};

3
src/lang/locale/ru.ts Normal file
View File

@@ -0,0 +1,3 @@
// русский
export default {};

3
src/lang/locale/tr.ts Normal file
View File

@@ -0,0 +1,3 @@
// Türkçe
export default {};

3
src/lang/locale/zh-cn.ts Normal file
View File

@@ -0,0 +1,3 @@
// 简体中文
export default {};

3
src/lang/locale/zh-tw.ts Normal file
View File

@@ -0,0 +1,3 @@
// 繁體中文
export default {};

File diff suppressed because it is too large Load Diff

View File

@@ -6,8 +6,8 @@ import {
import ExcalidrawPlugin from './main';
import {
EMPTY_MESSAGE,
EXCALIDRAW_FILE_EXTENSION
} from './constants';
import {t} from './lang/helpers'
export enum openDialogAction {
openFile,
@@ -33,7 +33,7 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
this.inputEl.onkeyup = (e) => {
if(e.key=="Enter" && this.action == openDialogAction.openFile) {
if (this.containerEl.innerText.includes(EMPTY_MESSAGE)) {
this.plugin.createDrawing(this.plugin.settings.folder+'/'+this.inputEl.value+'.'+EXCALIDRAW_FILE_EXTENSION, this.onNewPane);
this.plugin.createDrawing(this.plugin.settings.folder+'/'+this.inputEl.value+'.excalidraw.md', this.onNewPane);
this.close();
}
}
@@ -42,11 +42,14 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
getItems(): TFile[] {
const excalidrawFiles = this.app.vault.getFiles();
return (excalidrawFiles || []).filter((f:TFile) => (this.action == openDialogAction.insertLink) || (f.extension==EXCALIDRAW_FILE_EXTENSION));
return (excalidrawFiles || []).filter((f:TFile) => {
if (this.action == openDialogAction.insertLink) return true;
return this.plugin.isExcalidrawFile(f);
});
}
getItemText(item: TFile): string {
return item.path; //this.action == openDialogAction.insertLink ? item.path : item.basename;
return item.path;
}
onChooseItem(item: TFile, _evt: MouseEvent | KeyboardEvent): void {
@@ -58,9 +61,9 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
this.plugin.embedDrawing(item.path);
break;
case(openDialogAction.insertLink):
//@ts-ignore
const filepath = this.app.metadataCache.getLinkpathDest(item.path,this.drawingPath)[0].path;
this.addText("[["+(filepath.endsWith(".md")?filepath.substr(0,filepath.length-3):filepath)+"]]"); //.md files don't need the extension
//TO-DO
const filepath = this.app.metadataCache.fileToLinktext(item,this.drawingPath,true);
this.addText("[["+filepath+"]]");
break;
}
}
@@ -70,17 +73,17 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
this.addText = addText;
this.drawingPath = drawingPath;
this.setInstructions([{
command: "Select a file then hit enter.",
command: t("SELECT_FILE"),
purpose: "",
}]);
this.emptyStateText = "No file matches your query.";
this.setPlaceholder("Select existing file to insert link into drawing.");
this.emptyStateText = t("NO_MATCH");
this.setPlaceholder(t("SELECT_FILE_TO_LINK"));
this.open();
}
public start(action:openDialogAction, onNewPane: boolean): void {
this.setInstructions([{
command: "Type name of drawing to select.",
command: t("TYPE_FILENAME"),
purpose: "",
}]);
this.action = action;
@@ -88,11 +91,11 @@ export class OpenFileDialog extends FuzzySuggestModal<TFile> {
switch(action) {
case (openDialogAction.openFile):
this.emptyStateText = EMPTY_MESSAGE;
this.setPlaceholder("Select existing drawing or type name of new and hit enter.");
this.setPlaceholder(t("SELECT_FILE_OR_TYPE_NEW"));
break;
case (openDialogAction.insertLinkToDrawing):
this.emptyStateText = "No file matches your query.";
this.setPlaceholder("Select existing drawing to insert into document.");
this.emptyStateText = t("NO_MATCH");
this.setPlaceholder(t("SELECT_TO_EMBED"));
break;
}
this.open();

View File

@@ -1,10 +1,11 @@
import {
App,
parseFrontMatterAliases,
PluginSettingTab,
Setting
} from 'obsidian';
import { EXCALIDRAW_FILE_EXTENSION } from './constants';
import { VIEW_TYPE_EXCALIDRAW } from './constants';
import ExcalidrawView from './ExcalidrawView';
import { t } from './lang/helpers';
import type ExcalidrawPlugin from "./main";
export interface ExcalidrawSettings {
@@ -13,17 +14,16 @@ export interface ExcalidrawSettings {
drawingFilenamePrefix: string,
drawingFilenameDateTime: string,
width: string,
validLinksOnly: boolean, //valid link as in [[valid Obsidian link]] - how to treat text elements in drawings
showLinkBrackets: boolean,
linkPrefix: string,
// validLinksOnly: boolean, //valid link as in [[valid Obsidian link]] - how to treat text elements in drawings
allowCtrlClick: boolean, //if disabled only the link button in the view header will open links
exportWithTheme: boolean,
exportWithBackground: boolean,
autoexportSVG: boolean,
autoexportPNG: boolean,
keepInSync: boolean,
library: string,
/*Excalidraw Sync Begin*/
syncFolder: string,
excalidrawSync: boolean,
/*Excalidraw Sync End*/
}
export const DEFAULT_SETTINGS: ExcalidrawSettings = {
@@ -32,17 +32,16 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
drawingFilenamePrefix: 'Drawing ',
drawingFilenameDateTime: 'YYYY-MM-DD HH.mm.ss',
width: '400',
validLinksOnly: false,
linkPrefix: ">> ",
showLinkBrackets: true,
// validLinksOnly: false,
allowCtrlClick: true,
exportWithTheme: true,
exportWithBackground: true,
autoexportSVG: false,
autoexportPNG: false,
keepInSync: false,
library: `{"type":"excalidrawlib","version":1,"library":[]}`,
/*Excalidraw Sync Begin*/
syncFolder: 'excalidraw_sync',
excalidrawSync: false,
/*Excalidraw Sync End*/
}
export class ExcalidrawSettingTab extends PluginSettingTab {
@@ -58,22 +57,8 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.containerEl.empty();
new Setting(containerEl)
.setName('Default width of embedded (transcluded) image')
.setDesc('The default width of an embedded drawing. You can specify a different ' +
'width when embedding an image using the ![[drawing.excalidraw|100]] or ' +
'[[drawing.excalidraw|100x100]] format.')
.addText(text => text
.setPlaceholder('400')
.setValue(this.plugin.settings.width)
.onChange(async (value) => {
this.plugin.settings.width = value;
await this.plugin.saveSettings();
this.plugin.triggerEmbedUpdates();
}));
new Setting(containerEl)
.setName('Excalidraw folder')
.setDesc('Default location for your Excalidraw drawings. Leaving this empty means drawings will be created in the Vault root.')
.setName(t("FOLDER_NAME"))
.setDesc(t("FOLDER_DESC"))
.addText(text => text
.setPlaceholder('Excalidraw')
.setValue(this.plugin.settings.folder)
@@ -83,40 +68,34 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName('Excalidraw template file')
.setDesc('Full path to file containing the file you want to use as the template for new Excalidraw drawings. '+
'Note that Excalidraw files will have the extension ".excalidraw". ' +
'Assuming your template is in the default Excalidraw folder, the setting would be: Excalidraw/Template.excalidraw')
.setName(t("TEMPLATE_NAME"))
.setDesc(t("TEMPLATE_DESC"))
.addText(text => text
.setPlaceholder('Excalidraw/Template.excalidraw')
.setPlaceholder('Excalidraw/Template')
.setValue(this.plugin.settings.templateFilePath)
.onChange(async (value) => {
this.plugin.settings.templateFilePath = value;
await this.plugin.saveSettings();
}));
this.containerEl.createEl('h1', {text: 'New drawing filename'});
this.containerEl.createEl('h1', {text: t("FILENAME_HEAD")});
containerEl.createDiv('',(el) => {
el.innerHTML = '<p>The automatically generated filename consists of a prefix and a date. ' +
'e.g."Drawing 2021-05-24 12.58.07".</p>'+
'<p>Click this link for the <a href="https://momentjs.com/docs/#/displaying/format/">'+
'date and time format reference</a>.</p>';
el.innerHTML = t("FILENAME_DESC");
});
const getFilenameSample = () => {
return 'The current file format is: <b>' +
return t("FILENAME_SAMPLE") +
this.plugin.settings.drawingFilenamePrefix +
window.moment().format(this.plugin.settings.drawingFilenameDateTime) +
'.' + EXCALIDRAW_FILE_EXTENSION + '</b>';
window.moment().format(this.plugin.settings.drawingFilenameDateTime) + '</b>';
};
const filenameEl = containerEl.createEl('p',{text: ''});
filenameEl.innerHTML = getFilenameSample();
new Setting(containerEl)
.setName('Filename prefix')
.setDesc('The first part of the filename')
.setName(t("FILENAME_PREFIX_NAME"))
.setDesc(t("FILENAME_PREFIX_DESC"))
.addText(text => text
.setPlaceholder('Drawing ')
.setValue(this.plugin.settings.drawingFilenamePrefix)
@@ -128,8 +107,8 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName('Filename date')
.setDesc('The second part of the filename')
.setName(t("FILENAME_DATE_NAME"))
.setDesc(t("FILENAME_DATE_DESC"))
.addText(text => text
.setPlaceholder('YYYY-MM-DD HH.mm.ss')
.setValue(this.plugin.settings.drawingFilenameDateTime)
@@ -140,33 +119,71 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
}));
this.containerEl.createEl('h1', {text: 'Links in drawings'});
this.containerEl.createEl('h1', {text: t("LINKS_HEAD")});
this.containerEl.createEl('p',{
text: 'You can CTRL/META + click on text elements in your drawings to open them as links. ' +
'By default the plugin will handle any text as a link, and will try to open it. ' +
'If the text element includes a [[valid Obsidian link]] then the rest of the text element will be ignored ' +
'and only the [[valid Obsidian link]] will be processed as a link. ' +
'If the text element starts as a valid web link (i.e. https:// or http://), then it will be treated as a web link ' +
'and the plugin will try to open it in a browser window. ' +
'The plugin indexes your drawings, and when Obsidian files change, the matching text in your drawings will also change. ' +
'If you don\'t want text accidentally changing in your drawings, you can set the below toggle to limit the link ' +
'feature to only [[valid Obsidian links]].'});
new Setting(containerEl)
.setName('Accept only [[valid Obsidian links]]')
.setDesc('If this is on, text in text elements will be ignored unless they contain a [[valid Obsidian link]]')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.validLinksOnly)
.onChange(async (value) => {
this.plugin.settings.validLinksOnly = value;
this.plugin.reloadIndex();
await this.plugin.saveSettings();
}));
this.containerEl.createEl('h1', {text: 'Embedded image settings'});
text: t("LINKS_DESC")});
const reloadDrawings = async () => {
const exs = this.plugin.app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW);
for(const v of exs) {
if(v.view instanceof ExcalidrawView) {
await v.view.save(false);
v.view.reload(true);
}
}
this.plugin.triggerEmbedUpdates();
}
new Setting(containerEl)
.setName('Export image with background')
.setDesc('If turned off, the exported image will be transparent.')
.setName(t("LINK_BRACKETS_NAME"))
.setDesc(t("LINK_BRACKETS_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.showLinkBrackets)
.onChange(async (value) => {
this.plugin.settings.showLinkBrackets = value;
await this.plugin.saveSettings();
reloadDrawings();
}));
new Setting(containerEl)
.setName(t("LINK_PREFIX_NAME"))
.setDesc(t("LINK_PREFIX_DESC"))
.addText(text => text
.setPlaceholder('>> ')
.setValue(this.plugin.settings.linkPrefix)
.onChange(async (value) => {
this.plugin.settings.linkPrefix = value;
await this.plugin.saveSettings();
reloadDrawings();
}));
new Setting(containerEl)
.setName(t("LINK_CTRL_CLICK_NAME"))
.setDesc(t("LINK_CTRL_CLICK_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.allowCtrlClick)
.onChange(async (value) => {
this.plugin.settings.allowCtrlClick = value;
await this.plugin.saveSettings();
}));
this.containerEl.createEl('h1', {text: t("EMBED_HEAD")});
new Setting(containerEl)
.setName(t("EMBED_WIDTH_NAME"))
.setDesc(t("EMBED_WIDTH_DESC"))
.addText(text => text
.setPlaceholder('400')
.setValue(this.plugin.settings.width)
.onChange(async (value) => {
this.plugin.settings.width = value;
await this.plugin.saveSettings();
this.plugin.triggerEmbedUpdates();
}));
new Setting(containerEl)
.setName(t("EXPORT_BACKGROUND_NAME"))
.setDesc(t("EXPORT_BACKGROUND_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.exportWithBackground)
.onChange(async (value) => {
@@ -176,9 +193,8 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName('Export image with theme')
.setDesc('Export the image matching the dark/light theme setting used for your drawing in Excalidraw. If turned off, ' +
'drawings created in drak mode will appear as they would in light mode.')
.setName(t("EXPORT_THEME_NAME"))
.setDesc(t("EXPORT_THEME_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.exportWithTheme)
.onChange(async (value) => {
@@ -188,11 +204,8 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName('Auto-export SVG')
.setDesc('Automatically create an SVG export of your drawing matching the title of your "my drawing.excalidraw" file. ' +
'The plugin will save the .SVG file in the same folder as the drawing. '+
'You can use this file ("my drawing.svg") to embed your drawing into documents in a platform independent way. ' +
'While the auto export switch is on, this file will get updated every time you edit the excalidraw drawing with the matching name.')
.setName(t("EXPORT_SVG_NAME"))
.setDesc(t("EXPORT_SVG_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.autoexportSVG)
.onChange(async (value) => {
@@ -201,8 +214,8 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}));
new Setting(containerEl)
.setName('Auto-export PNG')
.setDesc('Same as the auto-export SVG, but for PNG.')
.setName(t("EXPORT_PNG_NAME"))
.setDesc(t("EXPORT_PNG_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.autoexportPNG)
.onChange(async (value) => {
@@ -212,9 +225,8 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
new Setting(containerEl)
.setName('Keep the .SVG and/or .PNG filenames in sync with the .excalidraw file')
.setDesc('When turned on, the plugin will automaticaly update the filename of the .SVG and/or .PNG files when the .excalidraw file in the same folder (and same name) is renamed. ' +
'The plugin will also automatically delete the .SVG and/or .PNG files when the .excalidraw file in the same folder (and same name) is deleted. ')
.setName(t("EXPORT_SYNC_NAME"))
.setDesc(t("EXPORT_SYNC_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.keepInSync)
.onChange(async (value) => {
@@ -222,42 +234,5 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
}));
/*Excalidraw Sync Begin*/
this.containerEl.createEl('h1', {text: 'Excalidraw sync'});
this.containerEl.createEl('h3', {text: 'This is a hack and a temporary workaround. Turn it on only if you are comfortable with hacky solutions...'});
this.containerEl.createEl('p', {text: 'By enabling this feature Excalidraw will sync drawings to a sync folder where drawings are stored in an ".md" file. ' +
'This will allow Obsidian sync to synchronize Excalidraw drawings as well... ' +
'Whenever your drawing changes, the corresponding file in the sync folder will also get updated. Similarly, whenever a file is synchronized to the sync folder ' +
'by Obsidian sync, Excalidraw will sync it with the .excalidraw file in your vault.'});
this.containerEl.createEl('p', {text: 'Because this is a temporary workaround until Obsidian sync is ready, I didn\'t implement extensive application logic to manage sync. ' +
'Sync might get confused requiring some manual intervention.'});
new Setting(containerEl)
.setName('Excalidraw sync folder')
.setDesc('Configure the folder first, before activating the feature! ' +
'This is the root folder for your mirrored excalidraw drawings. ' +
'Don\'t save other files here, as my algorithm is not prepared to handle those... and I can\'t predict the outcome. ')
.addText(text => text
.setPlaceholder('.excalidraw_sync')
.setValue(this.plugin.settings.syncFolder)
.onChange(async (value) => {
this.plugin.settings.syncFolder = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Excalidraw sync')
.setDesc('Enable Excalidraw Sync')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.excalidrawSync)
.onChange(async (value) => {
this.plugin.settings.excalidrawSync = value;
await this.plugin.saveSettings();
this.plugin.initiateSync();
}));
/*Excalidraw Sync End*/
}
}

View File

@@ -1,3 +1,3 @@
{
"1.1.9": "0.11.13"
"1.1.10": "0.11.13"
}

11600
yarn.lock

File diff suppressed because it is too large Load Diff