mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
7 Commits
1.9.6.1-be
...
1.9.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f726cbcd0 | ||
|
|
2f77988473 | ||
|
|
d00247029b | ||
|
|
1692d07b37 | ||
|
|
24a2d39e63 | ||
|
|
a9847ec864 | ||
|
|
791f98309d |
@@ -67,7 +67,7 @@ if(!isFirst) {
|
||||
ea.copyViewElementsToEAforEditing([fromElement]);
|
||||
|
||||
const previousTextElements = elements.filter((el)=>el.type==="text");
|
||||
const previousRectElements = elements.filter((el)=>el.type==="rectangle");
|
||||
const previousRectElements = elements.filter((el)=> ['ellipse', 'rectangle', 'diamond'].includes(el.type));
|
||||
if(previousTextElements.length>0) {
|
||||
const el = previousTextElements[0];
|
||||
ea.style.strokeColor = el.strokeColor;
|
||||
@@ -78,7 +78,7 @@ if(!isFirst) {
|
||||
textWidth = ea.measureText(text).width;
|
||||
|
||||
id = ea.addText(
|
||||
fixWidth
|
||||
fixWidth
|
||||
? fromElement.x+fromElement.width/2-width/2
|
||||
: fromElement.x+fromElement.width/2-textWidth/2-textPadding,
|
||||
fromElement.y+fromElement.height+gapBetweenElements,
|
||||
@@ -87,7 +87,7 @@ if(!isFirst) {
|
||||
wrapAt: wrapLineLen,
|
||||
textAlign: "center",
|
||||
textVerticalAlign: "middle",
|
||||
box: previousRectElements.length > 0 ? "rectangle" : false,
|
||||
box: previousRectElements.length > 0 ? previousRectElements[0].type : false,
|
||||
...fixWidth
|
||||
? {width: width, boxPadding:0}
|
||||
: {boxPadding: textPadding}
|
||||
|
||||
33
ea-scripts/Create DrawIO file.md
Normal file
33
ea-scripts/Create DrawIO file.md
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
Creates a new draw.io diagram file and opens the file in the [Diagram plugin](https://github.com/zapthedingbat/drawio-obsidian) in a new tab.
|
||||
```js*/
|
||||
|
||||
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.9.7")) {
|
||||
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
|
||||
return;
|
||||
}
|
||||
|
||||
const drawIO = app.plugins.plugins["drawio-obsidian"];
|
||||
if(!drawIO || !drawIO?._loaded) {
|
||||
new Notice("Can't find the draw.io diagram plugin");
|
||||
}
|
||||
|
||||
filename = await utils.inputPrompt("Diagram name?");
|
||||
if(!filename) return;
|
||||
filename = filename.toLowerCase().endsWith(".svg") ? filename : filename + ".svg";
|
||||
const filepath = await ea.getAttachmentFilepath(filename);
|
||||
if(!filepath) return;
|
||||
const leaf = app.workspace.getLeaf('tab')
|
||||
if(!leaf) return;
|
||||
|
||||
const file = await this.app.vault.create(filepath, `<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><!--${ea.generateElementId()}--><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="300px" height="300px" viewBox="-0.5 -0.5 1 1" content="<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel>"></svg>`);
|
||||
|
||||
await ea.addImage(0,0,file);
|
||||
await ea.addElementsToView(true,true);
|
||||
|
||||
leaf.setViewState({
|
||||
type: "diagram-edit",
|
||||
state: {
|
||||
file: filepath
|
||||
}
|
||||
});
|
||||
1
ea-scripts/Create DrawIO file.svg
Normal file
1
ea-scripts/Create DrawIO file.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="24 26 68 68" stroke="#000"><path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="3.553" d="m58.069 43.384-17.008 29.01"/><path fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="3.501" d="m58.068 43.384 17.008 29.01"/><path fill="#000" d="M52.773 77.084a3.564 3.564 0 0 1-3.553 3.553H36.999a3.564 3.564 0 0 1-3.553-3.553v-9.379a3.564 3.564 0 0 1 3.553-3.553h12.222a3.564 3.564 0 0 1 3.553 3.553v9.379zM67.762 48.074a3.564 3.564 0 0 1-3.553 3.553H51.988a3.564 3.564 0 0 1-3.553-3.553v-9.379a3.564 3.564 0 0 1 3.553-3.553H64.21a3.564 3.564 0 0 1 3.553 3.553v9.379zM82.752 77.084a3.564 3.564 0 0 1-3.553 3.553H66.977a3.564 3.564 0 0 1-3.553-3.553v-9.379a3.564 3.564 0 0 1 3.553-3.553h12.222a3.564 3.564 0 0 1 3.553 3.553v9.379z"/></svg>
|
||||
|
After Width: | Height: | Size: 830 B |
File diff suppressed because one or more lines are too long
@@ -41,6 +41,7 @@ I would love to include your contribution in the script library. If you have a s
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Convert%20selected%20text%20elements%20to%20sticky%20notes.svg"/></div>|[[#Convert selected text elements to sticky notes]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Convert%20text%20to%20link%20with%20folder%20and%20alias.svg"/></div>|[[#Convert text to link with folder and alias]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Copy%20Selected%20Element%20Styles%20to%20Global.svg"/></div>|[[#Copy Selected Element Styles to Global]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Create%20DrawIO%20file.svg"/></div>|[[#Create DrawIO file]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Create%20new%20markdown%20file%20and%20embed%20into%20active%20drawing.svg"/></div>|[[#Create new markdown file and embed into active drawing]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Darken%20background%20color.svg"/></div>|[[#Darken background color]]|
|
||||
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Deconstruct%20selected%20elements%20into%20new%20drawing.svg"/></div>|[[#Deconstruct selected elements into new drawing]]|
|
||||
@@ -177,6 +178,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/1-2-3'>@1-2-3</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Copy%20Selected%20Element%20Styles%20to%20Global.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">This script will copy styles of any selected element into Excalidraw's global styles.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-copy-selected-element-styles-to-global.png'></td></tr></table>
|
||||
|
||||
## Create DrawIO file
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Create%20DrawIO%20file.md
|
||||
```
|
||||
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Create%20DrawIO%20file.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">The script will prompt you for a filename, then create a new draw.io diagram file and open the file in the <a href='https://github.com/zapthedingbat/drawio-obsidian'>Diagram plugin</a>, in a new tab.</td></tr></table>
|
||||
|
||||
## Create new markdown file and embed into active drawing
|
||||
```excalidraw-script-install
|
||||
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Create%20new%20markdown%20file%20and%20embed%20into%20active%20drawing.md
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "1.9.6",
|
||||
"version": "1.9.7",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
fileid,
|
||||
GITHUB_RELEASES,
|
||||
} from "./Constants";
|
||||
import { getDrawingFilename, } from "./utils/FileUtils";
|
||||
import { getDrawingFilename, getNewUniqueFilepath, } from "./utils/FileUtils";
|
||||
import {
|
||||
//debug,
|
||||
embedFontsInSVG,
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
scaleLoadedImage,
|
||||
wrapTextAtCharLength,
|
||||
} from "./utils/Utils";
|
||||
import { getNewOrAdjacentLeaf, isObsidianThemeDark } from "./utils/ObsidianUtils";
|
||||
import { getAttachmentsFolderAndFilePath, getNewOrAdjacentLeaf, isObsidianThemeDark } from "./utils/ObsidianUtils";
|
||||
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/types";
|
||||
import { EmbeddedFile, EmbeddedFilesLoader, FileData } from "./EmbeddedFileLoader";
|
||||
import { tex2dataURL } from "./LaTeX";
|
||||
@@ -108,6 +108,16 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
get obsidian() {
|
||||
return obsidian_module;
|
||||
};
|
||||
|
||||
async getAttachmentFilepath(filename: string): Promise<string> {
|
||||
if (!this.targetView || !this.targetView?.file) {
|
||||
errorMessage("targetView not set", "getAttachmentFolderAndFilePath()");
|
||||
return null;
|
||||
}
|
||||
const folderAndPath = await getAttachmentsFolderAndFilePath(app,this.targetView.file.path, filename);
|
||||
return getNewUniqueFilepath(app.vault, filename, folderAndPath.folder);
|
||||
}
|
||||
|
||||
plugin: ExcalidrawPlugin;
|
||||
targetView: ExcalidrawView = null; //the view currently edited
|
||||
elementsDict: {[key:string]:any}; //contains the ExcalidrawElements currently edited in Automate indexed by el.id
|
||||
@@ -1049,6 +1059,7 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
topY: number,
|
||||
imageFile: TFile | string,
|
||||
scale: boolean = true, //true will scale the image to MAX_IMAGE_SIZE, false will insert image at 100% of its size
|
||||
|
||||
): Promise<string> {
|
||||
const id = nanoid();
|
||||
const loader = new EmbeddedFilesLoader(
|
||||
@@ -1867,6 +1878,17 @@ export class ExcalidrawAutomate implements ExcalidrawAutomateInterface {
|
||||
return elements.filter(el=>el.groupIds.some(id=>element.groupIds.includes(id)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the elements from elements[] that are contained in the frame.
|
||||
* @param element
|
||||
* @param elements - typically all the non-deleted elements in the scene
|
||||
* @returns
|
||||
*/
|
||||
getElementsInFrame(frameElement: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[] {
|
||||
if(!frameElement || !elements || frameElement.type !== "frame") return [];
|
||||
return elements.filter(el=>el.frameId === frameElement.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param element
|
||||
* @param a
|
||||
@@ -2304,6 +2326,13 @@ async function getTemplate(
|
||||
groupElements = plugin.ea.getElementsInTheSameGroupWithElement(el[0],scene.elements)
|
||||
}
|
||||
}
|
||||
if(filenameParts.hasFrameref) {
|
||||
const el = scene.elements.filter((el: ExcalidrawElement)=>el.id===filenameParts.blockref)
|
||||
if(el.length === 1) {
|
||||
groupElements = plugin.ea.getElementsInFrame(el[0],scene.elements)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(filenameParts.hasTaskbone) {
|
||||
groupElements = groupElements.filter( el =>
|
||||
@@ -2435,7 +2464,7 @@ export async function createSVG(
|
||||
);
|
||||
const filenameParts = getEmbeddedFilenameParts(templatePath);
|
||||
if(
|
||||
!filenameParts.hasGroupref &&
|
||||
!(filenameParts.hasGroupref || filenameParts.hasFrameref) &&
|
||||
(filenameParts.hasBlockref || filenameParts.hasSectionref)
|
||||
) {
|
||||
let el = filenameParts.hasSectionref
|
||||
@@ -2557,7 +2586,7 @@ export const search = async (view: ExcalidrawView) => {
|
||||
const ea = view.plugin.ea;
|
||||
ea.reset();
|
||||
ea.setView(view);
|
||||
const elements = ea.getViewElements().filter((el) => el.type === "text");
|
||||
const elements = ea.getViewElements().filter((el) => el.type === "text" || el.type === "frame");
|
||||
if (elements.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -2616,6 +2645,38 @@ export const getTextElementsMatchingQuery = (
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param elements
|
||||
* @param query
|
||||
* @param exactMatch - when searching for section header exactMatch should be set to true
|
||||
* @returns the elements matching the query
|
||||
*/
|
||||
export const getFrameElementsMatchingQuery = (
|
||||
elements: ExcalidrawElement[],
|
||||
query: string[],
|
||||
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
|
||||
): ExcalidrawElement[] => {
|
||||
if (!elements || elements.length === 0 || !query || query.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return elements.filter((el: any) =>
|
||||
el.type === "frame" &&
|
||||
query.some((q) => {
|
||||
if (exactMatch) {
|
||||
const text = el.name.toLowerCase().split("\n")[0].trim();
|
||||
const m = text.match(/^#*(# .*)/);
|
||||
if (!m || m.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
return m[1] === q.toLowerCase();
|
||||
}
|
||||
const text = el.name.toLowerCase().replaceAll("\n", " ").trim();
|
||||
return text.match(q.toLowerCase()); //to distinguish between "# frame" and "# frame 1" https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
|
||||
}));
|
||||
}
|
||||
|
||||
export const cloneElement = (el: ExcalidrawElement):any => {
|
||||
return {
|
||||
...el,
|
||||
|
||||
@@ -48,7 +48,13 @@ import {
|
||||
EXPORT_IMG_ICON_NAME,
|
||||
} from "./Constants";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import { repositionElementsToCursor, ExcalidrawAutomate, getTextElementsMatchingQuery, cloneElement } from "./ExcalidrawAutomate";
|
||||
import {
|
||||
repositionElementsToCursor,
|
||||
ExcalidrawAutomate,
|
||||
getTextElementsMatchingQuery,
|
||||
cloneElement,
|
||||
getFrameElementsMatchingQuery
|
||||
} from "./ExcalidrawAutomate";
|
||||
import { t } from "./lang/helpers";
|
||||
import {
|
||||
ExcalidrawData,
|
||||
@@ -90,7 +96,7 @@ import {
|
||||
} from "./utils/Utils";
|
||||
import { getLeaf, getParentOfClass } from "./utils/ObsidianUtils";
|
||||
import { splitFolderAndFilename } from "./utils/FileUtils";
|
||||
import { NewFileActions, Prompt } from "./dialogs/Prompt";
|
||||
import { ConfirmationPrompt, NewFileActions, Prompt } from "./dialogs/Prompt";
|
||||
import { ClipboardData } from "@zsviczian/excalidraw/types/clipboard";
|
||||
import { updateEquation } from "./LaTeX";
|
||||
import {
|
||||
@@ -105,16 +111,14 @@ import { ToolsPanel } from "./menu/ToolsPanel";
|
||||
import { ScriptEngine } from "./Scripts";
|
||||
import { getTextElementAtPointer, getImageElementAtPointer, getElementWithLinkAtPointer } from "./utils/GetElementAtPointer";
|
||||
import { ICONS, saveIcon } from "./menu/ActionIcons";
|
||||
//import { MainMenu } from "@zsviczian/excalidraw";
|
||||
//import {WelcomeScreen} from "@zsviczian/excalidraw";
|
||||
import { ExportDialog } from "./dialogs/ExportDialog";
|
||||
import { getEA } from "src";
|
||||
import { emulateCTRLClickForLinks, externalDragModifierType, internalDragModifierType, isALT, isCTRL, isMETA, isSHIFT, linkClickModifierType, mdPropModifier, ModifierKeys } from "./utils/ModifierkeyHelper";
|
||||
import { setDynamicStyle } from "./utils/DynamicStyling";
|
||||
import { MenuLinks } from "./menu/MenuLinks";
|
||||
import { InsertPDFModal } from "./dialogs/InsertPDFModal";
|
||||
import { CustomIFrame, renderWebView, useDefaultExcalidrawFrame } from "./customIFrame";
|
||||
import { insertIFrameToView, insertImageToView } from "./utils/ExcalidrawViewUtils";
|
||||
import { imageCache } from "./utils/ImageCache";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
|
||||
@@ -231,6 +235,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
public linksAlwaysOpenInANewPane: boolean = false; //override the need for SHIFT+CTRL+click (used by ExcaliBrain)
|
||||
private hookServer: ExcalidrawAutomate;
|
||||
public lastSaveTimestamp: number = 0; //used to validate if incoming file should sync with open file
|
||||
private lastLoadedFile: TFile = null;
|
||||
private onKeyUp: (e: KeyboardEvent) => void;
|
||||
private onKeyDown:(e: KeyboardEvent) => void;
|
||||
//store key state for view mode link resolution
|
||||
@@ -299,7 +304,6 @@ export default class ExcalidrawView extends TextFileView {
|
||||
private linkAction_Element: HTMLElement;
|
||||
public compatibilityMode: boolean = false;
|
||||
private obsidianMenu: ObsidianMenu;
|
||||
private menuLinks: MenuLinks;
|
||||
|
||||
//https://stackoverflow.com/questions/27132796/is-there-any-javascript-event-fired-when-the-on-screen-keyboard-on-mobile-safari
|
||||
private isEditingTextResetTimer: NodeJS.Timeout = null;
|
||||
@@ -618,6 +622,11 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
this.semaphores.preventReload = preventReload;
|
||||
await super.save();
|
||||
//saving to backup with a delay in case application closes in the meantime, I want to avoid both save and backup corrupted.
|
||||
const path = this.file.path;
|
||||
//@ts-ignore
|
||||
const data = this.lastSavedData;
|
||||
setTimeout(()=>imageCache.addBAKToCache(path,data),50);
|
||||
triggerReload = (this.lastSaveTimestamp === this.file.stat.mtime) &&
|
||||
!preventReload && forcesave;
|
||||
this.lastSaveTimestamp = this.file.stat.mtime;
|
||||
@@ -671,18 +680,20 @@ export default class ExcalidrawView extends TextFileView {
|
||||
// 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() {
|
||||
//debug({where:"getViewData",semaphores:this.semaphores});
|
||||
if (!this.getScene) {
|
||||
if (!this.getScene || !this.excalidrawData.loaded) {
|
||||
return this.data;
|
||||
}
|
||||
if (!this.excalidrawData.loaded) {
|
||||
|
||||
const scene = this.getScene();
|
||||
if(!scene) {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
//include deleted elements in save in case saving in markdown mode
|
||||
//deleted elements are only used if sync modifies files while Excalidraw is open
|
||||
//otherwise deleted elements are discarded when loading the scene
|
||||
const scene = this.getScene();
|
||||
if (!this.compatibilityMode) {
|
||||
let trimLocation = this.data.search(/(^%%\n)?# Text Elements\n/m);
|
||||
if (trimLocation == -1) {
|
||||
@@ -703,7 +714,11 @@ export default class ExcalidrawView extends TextFileView {
|
||||
: [
|
||||
[FRONTMATTER_KEY, this.textMode === TextMode.raw ? "raw" : "parsed"]
|
||||
];
|
||||
if(this.exportDialog?.dirty) this.exportDialog.dirty = false;
|
||||
|
||||
if(this.exportDialog?.dirty) {
|
||||
this.exportDialog.dirty = false;
|
||||
}
|
||||
|
||||
let header = updateFrontmatterInString(this.data.substring(0, trimLocation),keys);
|
||||
//this should be removed at a later time. Left it here to remediate 1.4.9 mistake
|
||||
const REG_IMG = /(^---[\w\W]*?---\n)(!\[\[.*?]]\n(%%\n)?)/m; //(%%\n)? because of 1.4.8-beta... to be backward compatible with anyone who installed that version
|
||||
@@ -715,15 +730,16 @@ export default class ExcalidrawView extends TextFileView {
|
||||
this.excalidrawData.disableCompression =
|
||||
this.isEditedAsMarkdownInOtherView();
|
||||
}
|
||||
const reuslt = header + this.excalidrawData.generateMD(
|
||||
const result = header + this.excalidrawData.generateMD(
|
||||
this.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted) //will be concatenated to scene.elements
|
||||
);
|
||||
this.excalidrawData.disableCompression = false;
|
||||
return reuslt;
|
||||
return result;
|
||||
}
|
||||
if (this.compatibilityMode) {
|
||||
return JSON.stringify(scene, null, "\t");
|
||||
}
|
||||
|
||||
return this.data;
|
||||
}
|
||||
|
||||
@@ -1024,6 +1040,28 @@ export default class ExcalidrawView extends TextFileView {
|
||||
keys.altKey = true;
|
||||
}
|
||||
const leaf = getLeaf(this.plugin,this.leaf,keys);
|
||||
|
||||
try {
|
||||
//@ts-ignore
|
||||
const drawIO = app.plugins.plugins["drawio-obsidian"];
|
||||
if(drawIO && drawIO._loaded) {
|
||||
if(file.extension === "svg") {
|
||||
const svg = await this.app.vault.cachedRead(file);
|
||||
if(/(<|\<)(mxfile|mxgraph)/i.test(svg)) {
|
||||
leaf.setViewState({
|
||||
type: "diagram-edit",
|
||||
state: {
|
||||
file: file.path
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
await leaf.openFile(file, subpath ? { active: !this.linksAlwaysOpenInANewPane, eState: { subpath } } : undefined); //if file exists open file and jump to reference
|
||||
//view.app.workspace.setActiveLeaf(leaf, true, true); //0.15.4 ExcaliBrain focus issue
|
||||
} catch (e) {
|
||||
@@ -1317,6 +1355,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
this.blockTextModeChange = false;
|
||||
}
|
||||
|
||||
public autosaveFunction: Function;
|
||||
public setupAutosaveTimer() {
|
||||
const timer = async () => {
|
||||
if(!this.isLoaded) {
|
||||
@@ -1368,6 +1407,8 @@ export default class ExcalidrawView extends TextFileView {
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
this.autosaveFunction = timer;
|
||||
if (this.autosaveTimer) {
|
||||
clearTimeout(this.autosaveTimer);
|
||||
this.autosaveTimer = null;
|
||||
@@ -1424,6 +1465,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
return;
|
||||
}
|
||||
if (this.semaphores.saving) return;
|
||||
this.lastLoadedFile = null;
|
||||
this.diskIcon.querySelector("svg").removeClass("excalidraw-dirty");
|
||||
if (this.compatibilityMode) {
|
||||
this.clearDirty();
|
||||
@@ -1582,6 +1624,10 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
public isLoaded: boolean = false;
|
||||
async setViewData(data: string, clear: boolean = false) {
|
||||
//I am using last loaded file to control when the view reloads.
|
||||
//It seems text file view gets the modified file event after sync before the modifyEventHandler in main.ts
|
||||
//reload can only be triggered via reload()
|
||||
if(this.lastLoadedFile === this.file) return;
|
||||
this.isLoaded = false;
|
||||
if(!this.file) return;
|
||||
if(this.plugin.settings.showNewVersionNotification) checkExcalidrawVersion(app);
|
||||
@@ -1589,6 +1635,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
this.clear();
|
||||
}
|
||||
this.lastSaveTimestamp = this.file.stat.mtime;
|
||||
this.lastLoadedFile = this.file;
|
||||
data = this.data = data.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
|
||||
app.workspace.onLayoutReady(async () => {
|
||||
this.compatibilityMode = this.file.extension === "excalidraw";
|
||||
@@ -1620,14 +1667,41 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
} catch (e) {
|
||||
errorlog({ where: "ExcalidrawView.setViewData", error: e });
|
||||
new Notice(
|
||||
`Error loading drawing:\n${e.message}${
|
||||
e.message === "Cannot read property 'index' of undefined"
|
||||
? "\n'# Drawing' section is likely missing"
|
||||
: ""
|
||||
}\n\nTry manually fixing the file or restoring an earlier version from sync history.`,
|
||||
10000,
|
||||
);
|
||||
const file = this.file;
|
||||
const plugin = this.plugin;
|
||||
const leaf = this.leaf;
|
||||
(async () => {
|
||||
let confirmation:boolean = true;
|
||||
while (!imageCache.isReady() && confirmation) {
|
||||
imageCache.initializationNotice = true;
|
||||
const confirmationPrompt = new ConfirmationPrompt(plugin,t("CACHE_NOT_READY"));
|
||||
confirmation = await confirmationPrompt.waitForClose
|
||||
}
|
||||
|
||||
const drawingBAK = await imageCache.getBAKFromCache(file.path);
|
||||
if (!drawingBAK) {
|
||||
new Notice(
|
||||
`Error loading drawing:\n${e.message}${
|
||||
e.message === "Cannot read property 'index' of undefined"
|
||||
? "\n'# Drawing' section is likely missing"
|
||||
: ""
|
||||
}\n\nTry manually fixing the file or restoring an earlier version from sync history.`,
|
||||
10000,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const confirmationPrompt = new ConfirmationPrompt(plugin,t("BACKUP_AVAILABLE"));
|
||||
confirmationPrompt.waitForClose.then(async (confirmed) => {
|
||||
if (confirmed) {
|
||||
await app.vault.modify(file, drawingBAK);
|
||||
//@ts-ignore
|
||||
plugin.excalidrawFileModes[leaf.id || file.path] = VIEW_TYPE_EXCALIDRAW;
|
||||
plugin.setExcalidrawView(leaf);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
})();
|
||||
this.setMarkdownView();
|
||||
return;
|
||||
}
|
||||
@@ -1736,7 +1810,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
try {
|
||||
const deletedIds = inData.deletedElements.map(el=>el.id);
|
||||
const sceneElements = this.excalidrawAPI.getSceneElements()
|
||||
const sceneElements = this.excalidrawAPI.getSceneElementsIncludingDeleted()
|
||||
//remove deleted elements
|
||||
.filter((el: ExcalidrawElement)=>!deletedIds.contains(el.id));
|
||||
const sceneElementIds = sceneElements.map((el:ExcalidrawElement)=>el.id);
|
||||
@@ -2208,7 +2282,6 @@ export default class ExcalidrawView extends TextFileView {
|
||||
const reactElement = React.createElement(() => {
|
||||
const excalidrawWrapperRef = React.useRef(null);
|
||||
const toolsPanelRef = React.useRef(null);
|
||||
const menuLinksRef = React.useRef(null);
|
||||
|
||||
const [dimensions, setDimensions] = React.useState({
|
||||
width: undefined,
|
||||
@@ -2224,7 +2297,6 @@ export default class ExcalidrawView extends TextFileView {
|
||||
|
||||
this.toolsPanelRef = toolsPanelRef;
|
||||
this.obsidianMenu = new ObsidianMenu(this.plugin, toolsPanelRef, this);
|
||||
this.menuLinks = new MenuLinks(this.plugin, menuLinksRef);
|
||||
|
||||
//excalidrawRef readypromise based on
|
||||
//https://codesandbox.io/s/eexcalidraw-resolvable-promise-d0qg3?file=/src/App.js:167-760
|
||||
@@ -4040,7 +4112,11 @@ export default class ExcalidrawView extends TextFileView {
|
||||
elements.filter((el: ExcalidrawElement) => el.type === "text"),
|
||||
query,
|
||||
exactMatch
|
||||
);
|
||||
).concat(getFrameElementsMatchingQuery(
|
||||
elements.filter((el: ExcalidrawElement) => el.type === "frame"),
|
||||
query,
|
||||
exactMatch
|
||||
));
|
||||
|
||||
if (match.length === 0) {
|
||||
new Notice("I could not find a matching text element");
|
||||
@@ -4135,6 +4211,51 @@ export default class ExcalidrawView extends TextFileView {
|
||||
: this.plugin.ea.getLargestElement(elements).id;
|
||||
}
|
||||
|
||||
const isFrame = elements.some(el=>el.id === elementId && el.type==="frame");
|
||||
|
||||
let buttons = [];
|
||||
if(isFrame) {
|
||||
switch(prefix) {
|
||||
case "area=":
|
||||
case "group=":
|
||||
case "frame=":
|
||||
buttons = [
|
||||
{caption: "Frame", action:()=>{prefix="frame="; return;}},
|
||||
{caption: "Link", action:()=>{prefix="";return}},
|
||||
];
|
||||
break;
|
||||
default:
|
||||
buttons = [
|
||||
{caption: "Link", action:()=>{prefix="";return}},
|
||||
{caption: "Frame", action:()=>{prefix="frame="; return;}},
|
||||
]
|
||||
}
|
||||
|
||||
} else {
|
||||
switch(prefix) {
|
||||
case "area=":
|
||||
buttons = [
|
||||
{caption: "Area", action:()=>{prefix="area="; return;}},
|
||||
{caption: "Link", action:()=>{prefix="";return}},
|
||||
{caption: "Group", action:()=>{prefix="group="; return;}},
|
||||
];
|
||||
break;
|
||||
case "group=":
|
||||
buttons = [
|
||||
{caption: "Group", action:()=>{prefix="group="; return;}},
|
||||
{caption: "Link", action:()=>{prefix="";return}},
|
||||
{caption: "Area", action:()=>{prefix="area="; return;}},
|
||||
];
|
||||
break;
|
||||
default:
|
||||
buttons = [
|
||||
{caption: "Link", action:()=>{prefix="";return}},
|
||||
{caption: "Area", action:()=>{prefix="area="; return;}},
|
||||
{caption: "Group", action:()=>{prefix="group="; return;}},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const alias = await ScriptEngine.inputPrompt(
|
||||
this,
|
||||
this.plugin,
|
||||
@@ -4142,11 +4263,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
"Set link alias",
|
||||
"Leave empty if you do not want to set an alias",
|
||||
"",
|
||||
[
|
||||
{caption: "Link", action:()=>{prefix="";return}},
|
||||
{caption: "Area", action:()=>{prefix="area="; return;}},
|
||||
{caption: "Group", action:()=>{prefix="group="; return;}}
|
||||
]
|
||||
buttons,
|
||||
);
|
||||
navigator.clipboard.writeText(
|
||||
`${prefix.length>0?"!":""}[[${this.file.path}#^${prefix}${elementId}${alias ? `|${alias}` : ``}]]`,
|
||||
|
||||
@@ -125,7 +125,7 @@ const getIMG = async (
|
||||
const cacheKey = {...filenameParts, isDark: theme==="dark", isSVG: false, scale};
|
||||
|
||||
if(cacheReady) {
|
||||
const src = await imageCache.get(cacheKey);
|
||||
const src = await imageCache.getImageFromCache(cacheKey);
|
||||
//In case of PNG I cannot change the viewBox to select the area of the element
|
||||
//being referenced. For PNG only the group reference works
|
||||
if(src) {
|
||||
@@ -134,7 +134,7 @@ const getIMG = async (
|
||||
}
|
||||
}
|
||||
|
||||
const quickPNG = !filenameParts.hasGroupref
|
||||
const quickPNG = !(filenameParts.hasGroupref || filenameParts.hasFrameref)
|
||||
? await getQuickImagePreview(plugin, file.path, "png")
|
||||
: undefined;
|
||||
|
||||
@@ -142,7 +142,7 @@ const getIMG = async (
|
||||
const png =
|
||||
quickPNG ??
|
||||
(await createPNG(
|
||||
filenameParts.hasGroupref
|
||||
(filenameParts.hasGroupref || filenameParts.hasFrameref)
|
||||
? filenameParts.filepath + filenameParts.linkpartReference
|
||||
: file.path,
|
||||
scale,
|
||||
@@ -159,13 +159,13 @@ const getIMG = async (
|
||||
return null;
|
||||
}
|
||||
img.src = URL.createObjectURL(png);
|
||||
cacheReady && imageCache.add(cacheKey, img.src);
|
||||
cacheReady && imageCache.addImageToCache(cacheKey, img.src);
|
||||
return img;
|
||||
}
|
||||
|
||||
const cacheKey = {...filenameParts, isDark: theme==="dark", isSVG: false, scale:1};
|
||||
const cacheKey = {...filenameParts, isDark: theme==="dark", isSVG: true, scale:1};
|
||||
if(cacheReady) {
|
||||
const src = await imageCache.get(cacheKey);
|
||||
const src = await imageCache.getImageFromCache(cacheKey);
|
||||
if(src) {
|
||||
img.setAttribute("src", src);
|
||||
return img;
|
||||
@@ -177,13 +177,13 @@ const getIMG = async (
|
||||
const quickSVG = await getQuickImagePreview(plugin, file.path, "svg");
|
||||
if (quickSVG) {
|
||||
img.setAttribute("src", svgToBase64(quickSVG));
|
||||
cacheReady && imageCache.add(cacheKey, img.src);
|
||||
cacheReady && imageCache.addImageToCache(cacheKey, img.src);
|
||||
return img;
|
||||
}
|
||||
}
|
||||
const svgSnapshot = (
|
||||
await createSVG(
|
||||
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref
|
||||
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref
|
||||
? filenameParts.filepath + filenameParts.linkpartReference
|
||||
: file.path,
|
||||
true,
|
||||
@@ -213,7 +213,7 @@ const getIMG = async (
|
||||
svg.removeAttribute("width");
|
||||
svg.removeAttribute("height");
|
||||
img.setAttribute("src", svgToBase64(svg.outerHTML));
|
||||
cacheReady && imageCache.add(cacheKey, img.src);
|
||||
cacheReady && imageCache.addImageToCache(cacheKey, img.src);
|
||||
return img;
|
||||
};
|
||||
|
||||
@@ -394,7 +394,7 @@ const isTextOnlyEmbed = (internalEmbedEl: Element):boolean => {
|
||||
const src = internalEmbedEl.getAttribute("src");
|
||||
if(!src) return true; //technically this does not mean this is a text only embed, but still should abort further processing
|
||||
const fnameParts = getEmbeddedFilenameParts(src);
|
||||
return !(fnameParts.hasArearef || fnameParts.hasGroupref) &&
|
||||
return !(fnameParts.hasArearef || fnameParts.hasGroupref || fnameParts.hasFrameref) &&
|
||||
(fnameParts.hasBlockref || fnameParts.hasSectionref)
|
||||
}
|
||||
|
||||
|
||||
@@ -196,7 +196,7 @@ export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
|
||||
// TODO: Figure out a better way to do this. Idea from Periodic Notes plugin
|
||||
this.app.keymap.pushScope(this.scope);
|
||||
|
||||
document.body.appendChild(this.suggestEl);
|
||||
this.inputEl.ownerDocument.body.appendChild(this.suggestEl);
|
||||
this.popper = createPopper(this.inputEl, this.suggestEl, {
|
||||
placement: "bottom-start",
|
||||
modifiers: [
|
||||
|
||||
@@ -17,6 +17,37 @@ I develop this plugin as a hobby, spending my free time doing this. If you find
|
||||
|
||||
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3" height=45></a></div>
|
||||
`,
|
||||
"1.9.7":`
|
||||
## Fixed:
|
||||
|
||||
- Fixed an issue where using the color picker shortcut would cause the UI to disappear in mobile view mode.
|
||||
- You can now add YouTube playlists to iframes.
|
||||
- Fixed a bug where the "Add any file" dropdown suggester opened in the main Obsidian workspace instead of the popout window when Excalidraw was running. ([#1179](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1191))
|
||||
- Made some improvements to the logic of opening in the adjacent pane, although it is still not perfect.
|
||||
- Fixed an issue where Obsidian sync would result in the loss of the last approximately 20 seconds of work. Excalidraw's handling of sync is now fixed. ([#1189](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1189))
|
||||
|
||||
## New:
|
||||
|
||||
- Introducing Image Cache: Excalidraw will now cache rendered images embedded in Markdown documents, which will enhance the markdown rendering experience.
|
||||
- Backup Cache: Excalidraw now stores a backup on your device when saving, in case the application is terminated during a save operation. If you are using sync, you can find the latest backup on the device you last used to edit your drawing.
|
||||
- Added ${String.fromCharCode(96)}frame=${String.fromCharCode(96)} parameter to image references. ([#1194](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1194)) For more details about this feature, check out this [YouTube video](https://youtu.be/yZQoJg2RCKI).
|
||||
- When an SVG image from Draw.io is embedded in Excalidraw, clicking the image will open the file in the [Diagram plugin](https://github.com/zapthedingbat/drawio-obsidian) (if available).
|
||||
- Added the [Create DrawIO file](https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Create%20DrawIO%20file.md) Excalidraw Automate Script to the library, which allows you to create a new draw.io drawing and add it to the current Excalidraw canvas.
|
||||
|
||||
## New in ExcalidrawAutomate
|
||||
|
||||
${String.fromCharCode(96,96,96)}typescript
|
||||
async getAttachmentFilepath(filename: string): Promise<string>
|
||||
${String.fromCharCode(96,96,96)}
|
||||
|
||||
This asynchronous function retrieves the filepath to a new file, taking into account the attachments preference settings in Obsidian. It creates the attachment folder if it doesn't already exist. The function returns the complete path to the file. If the provided filename already exists, the function will append '_[number]' before the extension to generate a unique filename.
|
||||
|
||||
${String.fromCharCode(96,96,96)}typescript
|
||||
getElementsInFrame(frameElement: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[];
|
||||
${String.fromCharCode(96,96,96)}
|
||||
|
||||
This function returns the elements contained within a frame.
|
||||
`,
|
||||
"1.9.6":`
|
||||
## Fixed
|
||||
- help shortcuts are really hard to see [#1176](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1179)
|
||||
|
||||
@@ -455,62 +455,6 @@ export class GenericSuggester extends FuzzySuggestModal<any> {
|
||||
}
|
||||
}
|
||||
|
||||
class MigrationPrompt extends Modal {
|
||||
private plugin: ExcalidrawPlugin;
|
||||
|
||||
constructor(app: App, plugin: ExcalidrawPlugin) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
this.titleEl.setText("Welcome to Excalidraw 1.2");
|
||||
this.createForm();
|
||||
}
|
||||
|
||||
onClose(): void {
|
||||
this.contentEl.empty();
|
||||
}
|
||||
|
||||
createForm(): void {
|
||||
const div = this.contentEl.createDiv();
|
||||
// div.addClass("excalidraw-prompt-div");
|
||||
// div.style.maxWidth = "600px";
|
||||
div.createEl("p", {
|
||||
text: "This version comes with tons of new features and possibilities. Please read the description in Community Plugins to find out more.",
|
||||
});
|
||||
div.createEl("p", { text: "" }, (el) => {
|
||||
el.innerHTML =
|
||||
"Drawings you've created with version 1.1.x need to be converted to take advantage of the new features. You can also continue to use them in compatibility mode. " +
|
||||
"During conversion your old *.excalidraw files will be replaced with new *.excalidraw.md files.";
|
||||
});
|
||||
div.createEl("p", { text: "" }, (el) => {
|
||||
//files manually follow one of two options:
|
||||
el.innerHTML =
|
||||
"To convert your drawings you have the following options:<br><ul>" +
|
||||
"<li>Click <code>CONVERT FILES</code> now to convert all of your *.excalidraw files, or if you prefer to make a backup first, then click <code>CANCEL</code>.</li>" +
|
||||
"<li>In the Command Palette select <code>Excalidraw: Convert *.excalidraw files to *.excalidraw.md files</code></li>" +
|
||||
"<li>Right click an <code>*.excalidraw</code> file in File Explorer and select one of the following options to convert files one by one: <ul>" +
|
||||
"<li><code>*.excalidraw => *.excalidraw.md</code></li>" +
|
||||
"<li><code>*.excalidraw => *.md (Logseq compatibility)</code>. This option will retain the original *.excalidraw file next to the new Obsidian format. " +
|
||||
"Make sure you also enable <code>Compatibility features</code> in Settings for a full solution.</li></ul></li>" +
|
||||
"<li>Open a drawing in compatibility mode and select <code>Convert to new format</code> from the <code>Options Menu</code></li></ul>";
|
||||
});
|
||||
div.createEl("p", {
|
||||
text: "This message will only appear maximum 3 times in case you have *.excalidraw files in your Vault.",
|
||||
});
|
||||
const bConvert = div.createEl("button", { text: "CONVERT FILES" });
|
||||
bConvert.onclick = () => {
|
||||
this.plugin.convertExcalidrawToMD();
|
||||
this.close();
|
||||
};
|
||||
const bCancel = div.createEl("button", { text: "CANCEL" });
|
||||
bCancel.onclick = () => {
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class NewFileActions extends Modal {
|
||||
constructor(
|
||||
private plugin: ExcalidrawPlugin,
|
||||
@@ -609,3 +553,74 @@ export class NewFileActions extends Modal {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfirmationPrompt extends Modal {
|
||||
public waitForClose: Promise<boolean>;
|
||||
private resolvePromise: (value: boolean) => void;
|
||||
private rejectPromise: (reason?: any) => void;
|
||||
private didConfirm: boolean = false;
|
||||
private readonly message: string;
|
||||
|
||||
constructor(private plugin: ExcalidrawPlugin, message: string) {
|
||||
super(plugin.app);
|
||||
this.message = message;
|
||||
this.waitForClose = new Promise<boolean>((resolve, reject) => {
|
||||
this.resolvePromise = resolve;
|
||||
this.rejectPromise = reject;
|
||||
});
|
||||
|
||||
this.display();
|
||||
this.open();
|
||||
}
|
||||
|
||||
private display() {
|
||||
this.contentEl.empty();
|
||||
this.titleEl.textContent = "Confirmation";
|
||||
|
||||
const messageEl = this.contentEl.createDiv();
|
||||
messageEl.style.marginBottom = "1rem";
|
||||
messageEl.innerHTML = this.message;
|
||||
|
||||
const buttonContainer = this.contentEl.createDiv();
|
||||
buttonContainer.style.display = "flex";
|
||||
buttonContainer.style.justifyContent = "flex-end";
|
||||
|
||||
const cancelButton = this.createButton(buttonContainer, "Cancel", this.cancelClickCallback);
|
||||
cancelButton.buttonEl.style.marginRight = "0.5rem";
|
||||
|
||||
const confirmButton = this.createButton(buttonContainer, "OK", this.confirmClickCallback);
|
||||
confirmButton.buttonEl.style.marginRight = "0";
|
||||
|
||||
cancelButton.buttonEl.focus();
|
||||
}
|
||||
|
||||
private createButton(container: HTMLElement, text: string, callback: (evt: MouseEvent) => void) {
|
||||
const button = new ButtonComponent(container);
|
||||
button.setButtonText(text).onClick(callback);
|
||||
return button;
|
||||
}
|
||||
|
||||
private cancelClickCallback = () => {
|
||||
this.didConfirm = false;
|
||||
this.close();
|
||||
};
|
||||
|
||||
private confirmClickCallback = () => {
|
||||
this.didConfirm = true;
|
||||
this.close();
|
||||
};
|
||||
|
||||
onOpen() {
|
||||
super.onOpen();
|
||||
this.contentEl.querySelector("button")?.focus();
|
||||
}
|
||||
|
||||
onClose() {
|
||||
super.onClose();
|
||||
if (!this.didConfirm) {
|
||||
this.resolvePromise(false);
|
||||
} else {
|
||||
this.resolvePromise(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,6 +516,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
desc: "Access functions and objects available on the <a onclick='window.open(\"https://github.com/obsidianmd/obsidian-api/blob/master/obsidian.d.ts\")'>Obsidian Module</a>",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "getAttachmentFilepath",
|
||||
code: "async getAttachmentFilepath(filename: string): Promise<string>",
|
||||
desc: "This asynchronous function should be awaited. It retrieves the filepath to a new file, taking into account the attachments preference settings in Obsidian. If the attachment folder doesn't exist, it creates it. The function returns the complete path to the file. If the provided filename already exists, the function will append '_[number]' before the extension to generate a unique filename.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "setViewModeEnabled",
|
||||
code: "setViewModeEnabled(enabled: boolean): void;",
|
||||
|
||||
@@ -50,6 +50,8 @@ export default {
|
||||
"Copy 'group=' markdown link for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_AREA:
|
||||
"Copy 'area=' markdown link for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_FRAME:
|
||||
"Copy 'frame=' markdown link for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_NORMAL:
|
||||
"Copy markdown link for selected element to clipboard.",
|
||||
INSERT_LINK_TO_ELEMENT_ERROR: "Select a single element in the scene",
|
||||
@@ -90,6 +92,14 @@ export default {
|
||||
COMPATIBILITY_MODE:
|
||||
"*.excalidraw file opened in compatibility mode. Convert to new format for full plugin functionality.",
|
||||
CONVERT_FILE: "Convert to new format",
|
||||
BACKUP_AVAILABLE: "There was an error loading your drawing. This could be because Obsidian terminated unexpectedly during a save operation (e.g. you 'swiped' Obsidian away on your mobile device in an unlucky moment). " +
|
||||
"<br><br><b>GOOD NEWS</b>: There is a local backup available. Note however, if the last time you modified this drawing you were using Obsidian on different device (i.e. now you are on your desktop and you modified it on your tablet), that other device will likely have a more recent backup available. " +
|
||||
"Try opening the drawing on your other device first, and restoring the backup from that local backup. " +
|
||||
"<br><br>Would you like to load the backup?",
|
||||
BACKUP_RESTORED: "Backup restored",
|
||||
CACHE_NOT_READY: "I am sorry, but there was an error loading your file. The plugin has a backup cache, but it looks like as if you've just started Obsidian. Initialization of the Backup Cache " +
|
||||
"can take up to a minute (or more depending on your device). You will be notified when the cache initialization is completed." +
|
||||
"<br><br>Press OK to try again, or Cancel to manually correct your file or to come back later.",
|
||||
|
||||
//settings.ts
|
||||
RELEASE_NOTES_NAME: "Display Release Notes after update",
|
||||
@@ -230,11 +240,11 @@ FILENAME_HEAD: "Filename",
|
||||
"the plugin will open it in a browser. " +
|
||||
"When Obsidian files change, the matching <code>[[link]]</code> in your drawings will also change. " +
|
||||
"If you don't want text accidentally changing in your drawings use <code>[[links|with aliases]]</code>.",
|
||||
ADJACENT_PANE_NAME: "Open in adjacent pane",
|
||||
ADJACENT_PANE_NAME: "Reuse adjacent pane",
|
||||
ADJACENT_PANE_DESC:
|
||||
`When ${labelCTRL()}+${labelSHIFT()} clicking a link in Excalidraw, by default the plugin will open the link in a new pane. ` +
|
||||
"Turning this setting on, Excalidraw will first look for an existing adjacent pane, and try to open the link there. " +
|
||||
"Excalidraw will look for the adjacent pane based on your focus/navigation history, i.e. the workpane that was active before you " +
|
||||
"Turning this setting on, Excalidraw will first look for an existing pane, and try to open the link there. " +
|
||||
"Excalidraw will look for the other workspace pane based on your focus/navigation history, i.e. the workpane that was active before you " +
|
||||
"activated Excalidraw.",
|
||||
MAINWORKSPACE_PANE_NAME: "Open in main workspace",
|
||||
MAINWORKSPACE_PANE_DESC:
|
||||
@@ -338,7 +348,9 @@ FILENAME_HEAD: "Filename",
|
||||
EMBED_IMAGE_CACHE_NAME: "Cache images for embedding in markdown",
|
||||
EMBED_IMAGE_CACHE_DESC: "Cache images for embedding in markdown. This will speed up the embedding process, but in case you compose images of several sub-component drawings, " +
|
||||
"the embedded image in Markdown won't update until you open the drawing and save it to trigger an update of the cache.",
|
||||
EMBED_IMAGE_CACHE_CLEAR: "Clear image cache",
|
||||
EMBED_IMAGE_CACHE_CLEAR: "Purge Cache",
|
||||
BACKUP_CACHE_CLEAR: "Purge Backups",
|
||||
BACKUP_CACHE_CLEAR_CONFIRMATION: "This action will delete all Excalidraw drawing backups. Backups are used as a safety measure in case your drawing file gets damaged. Each time you open Obsidian the plugin automatically deletes backups for files that no longer exist in your Vault. Are you sure you want to clear all backups?",
|
||||
EMBED_REUSE_EXPORTED_IMAGE_NAME:
|
||||
"If found, use the already exported image for preview",
|
||||
EMBED_REUSE_EXPORTED_IMAGE_DESC:
|
||||
|
||||
26
src/main.ts
26
src/main.ts
@@ -1172,6 +1172,22 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "insert-link-to-element-frame",
|
||||
name: t("INSERT_LINK_TO_ELEMENT_FRAME"),
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
||||
}
|
||||
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||
if (view) {
|
||||
view.copyLinkToSelectedElementToClipboard("frame=");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "insert-link-to-element-area",
|
||||
name: t("INSERT_LINK_TO_ELEMENT_AREA"),
|
||||
@@ -1699,6 +1715,14 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
const data = await app.vault.read(file);
|
||||
await inData.loadData(data,file,getTextMode(data));
|
||||
excalidrawView.synchronizeWithData(inData);
|
||||
if(excalidrawView.semaphores.dirty) {
|
||||
if(excalidrawView.autosaveTimer && excalidrawView.autosaveFunction) {
|
||||
clearTimeout(excalidrawView.autosaveTimer);
|
||||
}
|
||||
if(excalidrawView.autosaveFunction) {
|
||||
excalidrawView.autosaveFunction();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
excalidrawView.reload(true, excalidrawView.file);
|
||||
}
|
||||
@@ -2344,7 +2368,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
);
|
||||
}
|
||||
|
||||
private async setExcalidrawView(leaf: WorkspaceLeaf) {
|
||||
public async setExcalidrawView(leaf: WorkspaceLeaf) {
|
||||
await leaf.setViewState({
|
||||
type: VIEW_TYPE_EXCALIDRAW,
|
||||
state: leaf.view.getState(),
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { AppState } from "@zsviczian/excalidraw/types/types";
|
||||
import clsx from "clsx";
|
||||
import * as React from "react";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
|
||||
|
||||
export class MenuLinks {
|
||||
plugin: ExcalidrawPlugin;
|
||||
ref: React.MutableRefObject<any>;
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin, ref: React.MutableRefObject<any>) {
|
||||
this.plugin = plugin;
|
||||
this.ref = ref;
|
||||
}
|
||||
|
||||
render = (isMobile: boolean, appState: AppState) => {
|
||||
return (
|
||||
<div>Hello</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from "./utils/Utils";
|
||||
import { image } from "html2canvas/dist/types/css/types/image";
|
||||
import { imageCache } from "./utils/ImageCache";
|
||||
import { ConfirmationPrompt } from "./dialogs/Prompt";
|
||||
|
||||
export interface ExcalidrawSettings {
|
||||
folder: string;
|
||||
@@ -1154,7 +1155,19 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
button
|
||||
.setButtonText(t("EMBED_IMAGE_CACHE_CLEAR"))
|
||||
.onClick(() => {
|
||||
imageCache.clear();
|
||||
imageCache.clearImageCache();
|
||||
})
|
||||
)
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText(t("BACKUP_CACHE_CLEAR"))
|
||||
.onClick(() => {
|
||||
const confirmationPrompt = new ConfirmationPrompt(this.plugin,t("BACKUP_CACHE_CLEAR_CONFIRMATION"));
|
||||
confirmationPrompt.waitForClose.then((confirmed) => {
|
||||
if (confirmed) {
|
||||
imageCache.clearBackupCache();
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@ import ExcalidrawPlugin from "src/main";
|
||||
|
||||
//@ts-ignore
|
||||
const DB_NAME = "Excalidraw " + app.appId;
|
||||
const STORE_NAME = "imageCache";
|
||||
|
||||
const CACHE_STORE = "imageCache";
|
||||
const BACKUP_STORE = "drawingBAK";
|
||||
|
||||
type FileCacheData = { mtime: number; imageBase64: string };
|
||||
type BackupData = string;
|
||||
type BackupKey = string;
|
||||
|
||||
type ImageKey = {
|
||||
filepath: string;
|
||||
@@ -17,22 +19,26 @@ type ImageKey = {
|
||||
scale: number;
|
||||
};
|
||||
|
||||
const getKey = (key: ImageKey): string => `${key.filepath}#${key.blockref}#${key.sectionref}#${key.isDark?1:0}#${key.isSVG?1:0}#${key.scale}`;
|
||||
const getKey = (key: ImageKey): string =>
|
||||
`${key.filepath}#${key.blockref}#${key.sectionref}#${key.isDark ? 1 : 0}#${key.isSVG ? 1 : 0}#${key.scale}`;
|
||||
|
||||
class ImageCache {
|
||||
private dbName: string;
|
||||
private storeName: string;
|
||||
private cacheStoreName: string;
|
||||
private backupStoreName: string;
|
||||
private db: IDBDatabase | null;
|
||||
private isInitializing: boolean;
|
||||
public plugin: ExcalidrawPlugin;
|
||||
public initializationNotice: boolean = false;
|
||||
|
||||
constructor(dbName: string, storeName: string) {
|
||||
constructor(dbName: string, cacheStoreName: string, backupStoreName: string) {
|
||||
this.dbName = dbName;
|
||||
this.storeName = storeName;
|
||||
this.cacheStoreName = cacheStoreName;
|
||||
this.backupStoreName = backupStoreName;
|
||||
this.db = null;
|
||||
this.isInitializing = false;
|
||||
this.plugin = null;
|
||||
app.workspace.onLayoutReady(()=>this.initializeDB());
|
||||
app.workspace.onLayoutReady(() => this.initializeDB());
|
||||
}
|
||||
|
||||
private async initializeDB(): Promise<void> {
|
||||
@@ -47,8 +53,11 @@ class ImageCache {
|
||||
|
||||
request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
if (!db.objectStoreNames.contains(this.storeName)) {
|
||||
db.createObjectStore(this.storeName);
|
||||
if (!db.objectStoreNames.contains(this.cacheStoreName)) {
|
||||
db.createObjectStore(this.cacheStoreName);
|
||||
}
|
||||
if (!db.objectStoreNames.contains(this.backupStoreName)) {
|
||||
db.createObjectStore(this.backupStoreName);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -62,21 +71,29 @@ class ImageCache {
|
||||
reject(new Error(`Failed to open or create IndexedDB database: ${this.dbName}`));
|
||||
};
|
||||
});
|
||||
|
||||
// Pre-create the object store to reduce delay when accessing it later
|
||||
if (!this.db.objectStoreNames.contains(this.storeName)) {
|
||||
|
||||
// Pre-create the object stores to reduce delay when accessing them later
|
||||
if (
|
||||
!this.db.objectStoreNames.contains(this.cacheStoreName) ||
|
||||
!this.db.objectStoreNames.contains(this.backupStoreName)
|
||||
) {
|
||||
const version = this.db.version + 1;
|
||||
this.db.close();
|
||||
|
||||
const upgradeRequest = indexedDB.open(this.dbName, version);
|
||||
upgradeRequest.onupgradeneeded = (event: IDBVersionChangeEvent) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
db.createObjectStore(this.storeName);
|
||||
if (!db.objectStoreNames.contains(this.cacheStoreName)) {
|
||||
db.createObjectStore(this.cacheStoreName);
|
||||
}
|
||||
if (!db.objectStoreNames.contains(this.backupStoreName)) {
|
||||
db.createObjectStore(this.backupStoreName);
|
||||
}
|
||||
};
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
upgradeRequest.onsuccess = () => {
|
||||
const db = (upgradeRequest.result as IDBDatabase);
|
||||
const db = upgradeRequest.result as IDBDatabase;
|
||||
db.close();
|
||||
resolve();
|
||||
};
|
||||
@@ -89,7 +106,7 @@ class ImageCache {
|
||||
this.db = await new Promise<IDBDatabase>((resolve, reject) => {
|
||||
const openRequest = indexedDB.open(this.dbName);
|
||||
openRequest.onsuccess = () => {
|
||||
const db = (openRequest.result as IDBDatabase);
|
||||
const db = openRequest.result as IDBDatabase;
|
||||
resolve(db);
|
||||
};
|
||||
openRequest.onerror = () => {
|
||||
@@ -97,17 +114,22 @@ class ImageCache {
|
||||
};
|
||||
});
|
||||
}
|
||||
await this.purgeInvalidFiles();
|
||||
|
||||
await this.purgeInvalidCacheFiles();
|
||||
await this.purgeInvalidBackupFiles();
|
||||
} finally {
|
||||
this.isInitializing = false;
|
||||
if(this.initializationNotice) {
|
||||
new Notice("Excalidraw Image Cache is Initialized - You may now retry opening your damaged drawing.");
|
||||
this.initializationNotice = false;
|
||||
}
|
||||
console.log("Initialized Excalidraw Image Cache");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async purgeInvalidFiles(): Promise<void> {
|
||||
const transaction = this.db!.transaction(this.storeName, "readwrite");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
private async purgeInvalidCacheFiles(): Promise<void> {
|
||||
const transaction = this.db!.transaction(this.cacheStoreName, "readwrite");
|
||||
const store = transaction.objectStore(this.cacheStoreName);
|
||||
const files = app.vault.getFiles();
|
||||
|
||||
const deletePromises: Promise<void>[] = [];
|
||||
@@ -145,28 +167,51 @@ class ImageCache {
|
||||
});
|
||||
}
|
||||
|
||||
private async getObjectStore(mode: IDBTransactionMode): Promise<IDBObjectStore> {
|
||||
const transaction = this.db!.transaction(this.storeName, mode);
|
||||
return transaction.objectStore(this.storeName);
|
||||
}
|
||||
|
||||
public async openDB(): Promise<IDBDatabase> {
|
||||
return new Promise<IDBDatabase>((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName);
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error("Failed to open IndexedDB database."));
|
||||
private async purgeInvalidBackupFiles(): Promise<void> {
|
||||
const transaction = this.db!.transaction(this.backupStoreName, "readwrite");
|
||||
const store = transaction.objectStore(this.backupStoreName);
|
||||
const files = app.vault.getFiles();
|
||||
|
||||
const deletePromises: Promise<void>[] = [];
|
||||
|
||||
const request = store.openCursor();
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
request.onsuccess = (event: Event) => {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue | null>).result;
|
||||
if (cursor) {
|
||||
const key = cursor.key as BackupKey;
|
||||
const fileExists = files.some((f: TFile) => f.path === key);
|
||||
if (!fileExists) {
|
||||
deletePromises.push(
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const deleteRequest = store.delete(cursor.primaryKey);
|
||||
deleteRequest.onsuccess = () => resolve();
|
||||
deleteRequest.onerror = () =>
|
||||
reject(new Error(`Failed to delete backup file with key: ${key}`));
|
||||
})
|
||||
);
|
||||
}
|
||||
cursor.continue();
|
||||
} else {
|
||||
Promise.all(deletePromises)
|
||||
.then(() => resolve())
|
||||
.catch((error) => reject(error));
|
||||
}
|
||||
};
|
||||
|
||||
request.onsuccess = () => {
|
||||
const db = request.result as IDBDatabase;
|
||||
resolve(db);
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error("Failed to purge invalid backup files from IndexedDB."));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async getObjectStore(mode: IDBTransactionMode, storeName: string): Promise<IDBObjectStore> {
|
||||
const transaction = this.db!.transaction(storeName, mode);
|
||||
return transaction.objectStore(storeName);
|
||||
}
|
||||
|
||||
private async getCacheData(key: string): Promise<FileCacheData | null> {
|
||||
const store = await this.getObjectStore("readonly");
|
||||
const store = await this.getObjectStore("readonly", this.cacheStoreName);
|
||||
const request = store.get(key);
|
||||
|
||||
return new Promise<FileCacheData | null>((resolve, reject) => {
|
||||
@@ -181,25 +226,27 @@ class ImageCache {
|
||||
});
|
||||
}
|
||||
|
||||
public async isCached(key_: ImageKey): Promise<boolean> {
|
||||
const key = getKey(key_);
|
||||
return this.getCacheData(key).then((cachedData) => {
|
||||
if (cachedData) {
|
||||
const file = app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]);
|
||||
if (!file || !(file instanceof TFile)) return false;
|
||||
if (cachedData.mtime === file.stat.mtime) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
private async getBackupData(key: BackupKey): Promise<BackupData | null> {
|
||||
const store = await this.getObjectStore("readonly", this.backupStoreName);
|
||||
const request = store.get(key);
|
||||
|
||||
return new Promise<BackupData | null>((resolve, reject) => {
|
||||
request.onsuccess = () => {
|
||||
const result = request.result as BackupData;
|
||||
resolve(result || null);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error("Failed to retrieve backup data from IndexedDB."));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public isReady(): boolean {
|
||||
return !!this.db && !this.isInitializing && !!this.plugin && this.plugin.settings.allowImageCache;
|
||||
}
|
||||
}
|
||||
|
||||
public async get(key_: ImageKey): Promise<string | undefined> {
|
||||
public async getImageFromCache(key_: ImageKey): Promise<string | undefined> {
|
||||
if (!this.isReady()) {
|
||||
return null; // Database not initialized yet
|
||||
}
|
||||
@@ -215,42 +262,73 @@ class ImageCache {
|
||||
});
|
||||
}
|
||||
|
||||
public add(key_: ImageKey, imageBase64: string): void {
|
||||
public async getBAKFromCache(filepath: string): Promise<BackupData | null> {
|
||||
if (!this.isReady()) {
|
||||
return; // Database not initialized yet
|
||||
return null; // Database not initialized yet
|
||||
}
|
||||
|
||||
return this.getBackupData(filepath);
|
||||
}
|
||||
|
||||
public addImageToCache(key_: ImageKey, imageBase64: string): void {
|
||||
if (!this.isReady()) {
|
||||
return; // Database not initialized yet
|
||||
}
|
||||
|
||||
const file = app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]);
|
||||
if (!file || !(file instanceof TFile)) return;
|
||||
const data: FileCacheData = { mtime: file.stat.mtime, imageBase64 };
|
||||
|
||||
const transaction = this.db.transaction(this.storeName, "readwrite");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const key = getKey(key_)
|
||||
const transaction = this.db.transaction(this.cacheStoreName, "readwrite");
|
||||
const store = transaction.objectStore(this.cacheStoreName);
|
||||
const key = getKey(key_);
|
||||
store.put(data, key);
|
||||
}
|
||||
|
||||
public async clear(): Promise<void> {
|
||||
// deliberately not checking isReady() here
|
||||
if (!this.db || this.isInitializing) {
|
||||
public async addBAKToCache(filepath: string, data: BackupData): Promise<void> {
|
||||
if (!this.isReady()) {
|
||||
return; // Database not initialized yet
|
||||
}
|
||||
|
||||
const transaction = this.db.transaction(this.storeName, "readwrite");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.clear();
|
||||
const transaction = this.db.transaction(this.backupStoreName, "readwrite");
|
||||
const store = transaction.objectStore(this.backupStoreName);
|
||||
store.put(data, filepath);
|
||||
}
|
||||
|
||||
public async clearImageCache(): Promise<void> {
|
||||
if (!this.isReady()) {
|
||||
return; // Database not initialized yet
|
||||
}
|
||||
|
||||
return this.clear(this.cacheStoreName, "Image cache was cleared");
|
||||
}
|
||||
|
||||
public async clearBackupCache(): Promise<void> {
|
||||
if (!this.isReady()) {
|
||||
return; // Database not initialized yet
|
||||
}
|
||||
|
||||
return this.clear(this.backupStoreName, "All backups were cleared");
|
||||
}
|
||||
|
||||
private async clear(storeName: string, message: string): Promise<void> {
|
||||
if (!this.isReady()) {
|
||||
return; // Database not initialized yet
|
||||
}
|
||||
|
||||
const transaction = this.db.transaction([storeName], "readwrite");
|
||||
const store = transaction.objectStore(storeName);
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const request = store.clear();
|
||||
request.onsuccess = () => {
|
||||
new Notice("Image cache cleared.");
|
||||
new Notice(message);
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(new Error("Failed to clear data in IndexedDB."));
|
||||
};
|
||||
request.onerror = () => reject(new Error(`Failed to clear ${storeName}.`));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const imageCache = new ImageCache(DB_NAME, STORE_NAME);
|
||||
export const imageCache = new ImageCache(DB_NAME, CACHE_STORE, BACKUP_STORE);
|
||||
@@ -83,43 +83,47 @@ const getLeafLoc = (leaf: WorkspaceLeaf): ["main" | "popout" | "left" | "right"
|
||||
export const getNewOrAdjacentLeaf = (
|
||||
plugin: ExcalidrawPlugin,
|
||||
leaf: WorkspaceLeaf
|
||||
): WorkspaceLeaf => {
|
||||
): WorkspaceLeaf | null => {
|
||||
const [leafLoc, mainLeavesIds] = getLeafLoc(leaf);
|
||||
|
||||
const getMainLeaf = ():WorkspaceLeaf => {
|
||||
const getMostRecentOrAvailableLeafInMainWorkspace = (inDifferentTabGroup?: boolean):WorkspaceLeaf => {
|
||||
let mainLeaf = app.workspace.getMostRecentLeaf();
|
||||
if(mainLeaf && mainLeaf !== leaf && mainLeaf.view?.containerEl.ownerDocument === document) {
|
||||
//Found a leaf in the main workspace that is not the originating leaf
|
||||
return mainLeaf;
|
||||
}
|
||||
//Iterate all leaves in the main workspace and find the first one that is not the originating leaf
|
||||
mainLeaf = null;
|
||||
mainLeavesIds
|
||||
.forEach((id:any)=> {
|
||||
const l = app.workspace.getLeafById(id);
|
||||
if(mainLeaf ||
|
||||
!l.view?.navigation ||
|
||||
leaf === l
|
||||
leaf === l ||
|
||||
//@ts-ignore
|
||||
(inDifferentTabGroup && (l?.parent === leaf?.parent))
|
||||
) return;
|
||||
mainLeaf = l;
|
||||
})
|
||||
return mainLeaf;
|
||||
}
|
||||
|
||||
//1
|
||||
//1 - In Main Workspace
|
||||
if(plugin.settings.openInMainWorkspace || ["main","left","right"].contains(leafLoc)) {
|
||||
//1.1
|
||||
//1.1 - Create new leaf in main workspace
|
||||
if(!plugin.settings.openInAdjacentPane) {
|
||||
if(leafLoc === "main") {
|
||||
return app.workspace.createLeafBySplit(leaf);
|
||||
}
|
||||
const ml = getMainLeaf();
|
||||
const ml = getMostRecentOrAvailableLeafInMainWorkspace();
|
||||
return ml
|
||||
? (ml.view.getViewType() === "empty" ? ml : app.workspace.createLeafBySplit(ml))
|
||||
: app.workspace.getLeaf(true);
|
||||
}
|
||||
|
||||
//1.2
|
||||
const ml = getMainLeaf();
|
||||
return ml ?? app.workspace.getLeaf(true);
|
||||
//1.2 - Reuse leaf if it is adjacent
|
||||
const ml = getMostRecentOrAvailableLeafInMainWorkspace(true);
|
||||
return ml ?? app.workspace.createLeafBySplit(leaf); //app.workspace.getLeaf(true);
|
||||
}
|
||||
|
||||
//2
|
||||
|
||||
@@ -606,14 +606,15 @@ export const getEmbeddedFilenameParts = (fname:string):{
|
||||
hasGroupref: boolean,
|
||||
hasTaskbone: boolean,
|
||||
hasArearef: boolean,
|
||||
hasFrameref: boolean,
|
||||
blockref: string,
|
||||
hasSectionref: boolean,
|
||||
sectionref: string,
|
||||
linkpartReference: string,
|
||||
linkpartAlias: string
|
||||
} => {
|
||||
// 0 1 23 4 5 6 7 8 9
|
||||
const parts = fname?.match(/([^#\^]*)((#\^)(group=|area=|taskbone)?([^\|]*)|(#)(group=|area=|taskbone)?([^\^\|]*))(.*)/);
|
||||
// 0 1 23 4 5 6 7 8 9
|
||||
const parts = fname?.match(/([^#\^]*)((#\^)(group=|area=|frame=|taskbone)?([^\|]*)|(#)(group=|area=|frame=|taskbone)?([^\^\|]*))(.*)/);
|
||||
if(!parts) {
|
||||
return {
|
||||
filepath: fname,
|
||||
@@ -621,6 +622,7 @@ export const getEmbeddedFilenameParts = (fname:string):{
|
||||
hasGroupref: false,
|
||||
hasTaskbone: false,
|
||||
hasArearef: false,
|
||||
hasFrameref: false,
|
||||
blockref: "",
|
||||
hasSectionref: false,
|
||||
sectionref: "",
|
||||
@@ -634,6 +636,7 @@ export const getEmbeddedFilenameParts = (fname:string):{
|
||||
hasGroupref: (parts[4]==="group=") || (parts[7]==="group="),
|
||||
hasTaskbone: (parts[4]==="taskbone") || (parts[7]==="taskbone"),
|
||||
hasArearef: (parts[4]==="area=") || (parts[7]==="area="),
|
||||
hasFrameref: (parts[4]==="frame=") || (parts[7]==="frame="),
|
||||
blockref: parts[5],
|
||||
hasSectionref: Boolean(parts[6]),
|
||||
sectionref: parts[8],
|
||||
|
||||
Reference in New Issue
Block a user