This commit is contained in:
Zsolt Viczian
2021-07-11 23:28:46 +02:00
parent 803fb9e234
commit ba88ced2ba
7 changed files with 196 additions and 47 deletions

View File

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

View File

@@ -9,7 +9,7 @@ import {
} from "obsidian";
import * as React from "react";
import * as ReactDOM from "react-dom";
import Excalidraw, {exportToSvg, getSceneVersion} from "@excalidraw/excalidraw";
import Excalidraw, {exportToSvg, getSceneVersion, loadLibraryFromBlob} from "@excalidraw/excalidraw";
import { ExcalidrawElement } from "@excalidraw/excalidraw/types/element/types";
import {
AppState,
@@ -33,6 +33,7 @@ import ExcalidrawPlugin from './main';
import {ExcalidrawAutomate} from './ExcalidrawAutomate';
import { t } from "./lang/helpers";
import { ExcalidrawData, REG_LINK_BACKETS } from "./ExcalidrawData";
import { download } from "./Utils";
declare let window: ExcalidrawAutomate;
@@ -216,16 +217,6 @@ export default class ExcalidrawView extends TextFileView {
}
}
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)=> {
@@ -307,8 +298,8 @@ export default class ExcalidrawView extends TextFileView {
this.app.workspace.onLayoutReady(async ()=>{
//console.log("ExcalidrawView.setViewData()");
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();
@@ -390,7 +381,7 @@ 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');
download('data:text/plain;charset=utf-8',encodeURIComponent(JSON.stringify(this.getScene())), this.file.basename+'.excalidraw');
});
});
} else {
@@ -423,7 +414,7 @@ export default class ExcalidrawView extends TextFileView {
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;
}
@@ -444,7 +435,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()
@@ -455,7 +446,7 @@ 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 : [];
}
@@ -640,7 +631,25 @@ export default class ExcalidrawView extends TextFileView {
}
}*/
let timestamp = (new Date()).getTime();
let timestamp = 0;
const dblclickEvent = (e: Event):boolean => {
if((e.target instanceof HTMLCanvasElement) && this.getSelectedText(true)) { //text element is selected
const now = (new Date()).getTime();
if(now-timestamp < 600) { //double click
if(this.isTextLocked) {
e.preventDefault();
e.stopPropagation();
new Notice(t("UNLOCK_TO_EDIT"));
}
timestamp = 0;
this.lock(false);
return true;
}
timestamp = now;
}
return false;
}
return React.createElement(
React.Fragment,
@@ -651,19 +660,13 @@ export default class ExcalidrawView extends TextFileView {
className: "excalidraw-wrapper",
ref: excalidrawWrapperRef,
key: "abc",
onTouchEnd: (e: TouchEvent) => {
if (dblclickEvent(e)) return;
},
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
e.preventDefault();
e.stopPropagation();
this.lock(false);
new Notice(t("UNLOCK_TO_EDIT"));
timestamp = now;
return;
}
timestamp = now;
}
//@ts-ignore
if(this.app.isMobile) return;
if(this.isTextLocked && dblclickEvent(e)) return;
if(!(e.ctrlKey||e.metaKey)) return;
if(!(this.plugin.settings.allowCtrlClick)) return;
if(!this.getSelectedId()) return;
@@ -735,7 +738,7 @@ export default class ExcalidrawView extends TextFileView {
},
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();
})();
}

30
src/Utils.ts Normal file
View File

@@ -0,0 +1,30 @@
import { normalizePath, TFolder } from "obsidian";
/**
* 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);
}

View File

@@ -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",
@@ -108,6 +109,17 @@ export default {
"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",
/* STENCIL_HEAD: "Stencil Library",
STENCIL_INVAULT_NAME: "Store as FILE",
STENCIL_INVAULT_DESC: "By enabling this feature, the stencil library will be stored in a file specified in the next setting. " +
"Obsidan Sync now synchronizes all filetypes. By storing your stencil library in a file you can synchronize your library between devices. " +
"When enabling this setting, if the file you specified is empty, your existing stencils in settings will be copied to the file. " +
"When disabling this setting, your current stencil library will not overwirte stencils in your settings. " +
"You need to close all Excalidraw views and reopen them, for this change to take effect. " +
"The default filename is Excalidraw/stencils.excalidrawlib " ,
STENCIL_PATH_NAME: "Filepath",
STENCIL_PATH_DESC: "This can only be edited when \"Store as FILE\" is turned off. " +
"The filepath of the stencil library. Enter the filename without the extension. ",*/
COMPATIBILITY_HEAD: "Compatibility features",
EXPORT_EXCALIDRAW_NAME: "Auto-export Excalidraw",
EXPORT_EXCALIDRAW_DESC: "Same as the auto-export SVG, but for *.Excalidraw",

View File

@@ -1,6 +1,5 @@
import {
TFile,
TFolder,
Plugin,
WorkspaceLeaf,
addIcon,
@@ -16,6 +15,7 @@ import {
MarkdownRenderer,
ViewState,
Notice,
TFolder,
} from "obsidian";
import {
@@ -58,17 +58,20 @@ import { Prompt } from "./Prompt";
import { around } from "monkey-around";
import { t } from "./lang/helpers";
import { MigrationPrompt } from "./MigrationPrompt";
import { download, splitFolderAndFilename } 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);
@@ -102,7 +105,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();
}
@@ -110,7 +113,7 @@ export default class ExcalidrawPlugin extends Plugin {
const self = this;
this.app.workspace.onLayoutReady(async () => {
self.settings.loadCount++;
self.saveSettings();
//self.saveSettings();
const files = this.app.vault.getFiles().filter((f)=>f.extension=="excalidraw");
if(files.length>0) {
const prompt = new MigrationPrompt(self.app, self);
@@ -239,7 +242,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 +414,14 @@ export default class ExcalidrawPlugin extends Plugin {
this.app.workspace.on("file-menu", fileMenuHandlerConvertReplaceExtension)
);
this.addCommand({
id: "excalidraw-download-lib",
name: t("DOWNLOAD_LIBRARY"),
callback: () => {
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"),
@@ -847,6 +858,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 +895,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,12 +910,45 @@ export default class ExcalidrawPlugin extends Plugin {
}
private async loadSettings() {
public async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
/* if(this.settings.libraryInVault) {
const filepath = this.settings.libraryLocation+".excalidrawlib";
const file = this.app.vault.getAbstractFileByPath(filepath);
if(file && file instanceof TFile) {
this.stencilLibrary = await this.app.vault.read(file);
} else {
this.stencilLibrary = this.settings.library;
}
}*/
}
async saveSettings() {
await this.saveData(this.settings);
/* if(this.settings.libraryInVault) {
const filepath = this.settings.libraryLocation+".excalidrawlib";
const f = splitFolderAndFilename(filepath);
await this.checkAndCreateFolder(f.folderpath);
const file = this.app.vault.getAbstractFileByPath(filepath);
if(file && file instanceof TFile) {
await this.app.vault.modify(file,this.stencilLibrary ? this.stencilLibrary : this.settings.library)
} else {
await this.app.vault.create(filepath,JSON.stringify(this.stencilLibrary ? this.stencilLibrary : this.settings.library));
}
}*/
}
public getStencilLibrary():string {
//if(this.settings.libraryInVault) return this.stencilLibrary;
return this.settings.library;
}
public setStencilLibrary(library:string) {
/* if(this.settings.libraryInVault) {
this.stencilLibrary = library;
} else {*/
this.settings.library = library;
//}
}
public triggerEmbedUpdates(filepath?:string){
@@ -961,10 +1010,7 @@ 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 this.checkAndCreateFolder(folderpath); //create folder if it does not exist
const fname = this.getNewUniqueFilepath(filename,folderpath);
@@ -1018,4 +1064,14 @@ export default class ExcalidrawPlugin extends Plugin {
const fileCache = this.app.metadataCache.getFileCache(f);
return !!fileCache?.frontmatter && !!fileCache.frontmatter[FRONTMATTER_KEY];
}
/**
* Open or create a folderpath if it does not exist
* @param folderpath
*/
public async checkAndCreateFolder(folderpath:string) {
let folder = this.app.vault.getAbstractFileByPath(folderpath);
if(folder && folder instanceof TFolder) return;
await this.app.vault.createFolder(folderpath);
}
}

View File

@@ -1,12 +1,14 @@
import {
App,
PluginSettingTab,
Setting
Setting,
TFile
} from 'obsidian';
import { VIEW_TYPE_EXCALIDRAW } from './constants';
import ExcalidrawView from './ExcalidrawView';
import { t } from './lang/helpers';
import type ExcalidrawPlugin from "./main";
import { splitFolderAndFilename } from './Utils';
export interface ExcalidrawSettings {
folder: string,
@@ -25,12 +27,14 @@ 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,
// libraryInVault: boolean, //if true, library is stored in the vault in a file
// libraryLocation: string, //full path to the library file
library: string,
}
export const DEFAULT_SETTINGS: ExcalidrawSettings = {
@@ -50,12 +54,14 @@ 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,
// libraryInVault: false,
// libraryLocation: "Excalidraw/library",
library: `{"type":"excalidrawlib","version":1,"library":[]}`,
}
export class ExcalidrawSettingTab extends PluginSettingTab {
@@ -271,6 +277,48 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
this.plugin.settings.autoexportPNG = value;
await this.plugin.saveSettings();
}));
/*
this.containerEl.createEl('h1', {text: t("STENCIL_HEAD")});
const changeLibrary = async () => {
if(!this.plugin.settings.libraryInVault) return;
const filepath = this.plugin.settings.libraryLocation+".excalidrawlib";
const f = splitFolderAndFilename(filepath);
await this.plugin.checkAndCreateFolder(f.folderpath);
const file = this.app.vault.getAbstractFileByPath(filepath);
if(file && file instanceof TFile) {
this.plugin.stencilLibrary = await this.app.vault.read(file);
} else {
this.plugin.stencilLibrary = this.plugin.settings.library;
}
}
new Setting(containerEl)
.setName(t("STENCIL_INVAULT_NAME"))
.setDesc(t("STENCIL_INVAULT_DESC"))
.addToggle(toggle => toggle
.setValue(this.plugin.settings.libraryInVault)
.onChange(async (value) => {
this.plugin.settings.libraryInVault = value;
if(value) stencilLib.setDisabled(true);
await changeLibrary();
await this.plugin.saveSettings();
}));
const stencilLib = new Setting(containerEl)
.setName(t("STENCIL_PATH_NAME"))
.setDesc(t("STENCIL_PATH_DESC"))
.addText(text => text
.setPlaceholder('Excalidraw/library')
.setValue(this.plugin.settings.libraryLocation)
.onChange(async (value) => {
this.plugin.settings.libraryInVault = false;
this.plugin.stencilLibrary = null;
this.plugin.settings.libraryLocation = value;
await this.plugin.saveSettings();
})); */
this.containerEl.createEl('h1', {text: t("COMPATIBILITY_HEAD")});

View File

@@ -63,7 +63,7 @@ button.ToolIcon_type_button[title="Export"] {
flex-grow: 1;
}
/*
@font-face {
font-family: "Virgil";
src: url("https://excalidraw.com/Virgil.woff2");
@@ -71,4 +71,4 @@ button.ToolIcon_type_button[title="Export"] {
@font-face {
font-family: "Cascadia";
src: url("https://excalidraw.com/Cascadia.woff2");
}
}*/