This commit is contained in:
zsviczian
2024-04-22 20:10:21 +02:00
parent 37789f9907
commit f768548f60
14 changed files with 376 additions and 139 deletions

View File

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

View File

@@ -601,6 +601,7 @@ export class ExcalidrawAutomate {
"excalidraw-export-dark"?: boolean;
"excalidraw-export-padding"?: number;
"excalidraw-export-pngscale"?: number;
"excalidraw-export-embed-scene"?: boolean;
"excalidraw-default-mode"?: "view" | "zen";
"excalidraw-onload-script"?: string;
"excalidraw-linkbutton-opacity"?: number;

View File

@@ -51,6 +51,7 @@ import { BinaryFiles, DataURL, SceneData } from "@zsviczian/excalidraw/types/exc
import { EmbeddedFile, MimeType } from "./EmbeddedFileLoader";
import { ConfirmationPrompt } from "./dialogs/Prompt";
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
import { add } from "@zsviczian/excalidraw/types/excalidraw/ga";
type SceneDataWithFiles = SceneData & { files: BinaryFiles };
@@ -240,7 +241,11 @@ const estimateMaxLineLen = (text: string, originalText: string): number => {
const wrap = (text: string, lineLen: number) =>
lineLen ? wrapTextAtCharLength(text, lineLen, false, 0) : text;
const RE_TEXTELEMENTS = new RegExp(`^(%%\n)?${MD_TEXTELEMENTS}(?:\n|$)`, "m");
//WITHSECTION refers to back of the card note (see this.inputEl.onkeyup in SelectCard.ts)
const RE_TEXTELEMENTS_WITHSECTION_OK = new RegExp(`^#\n%%\n${MD_TEXTELEMENTS}(?:\n|$)`, "m");
const RE_TEXTELEMENTS_WITHSECTION_NOTOK = new RegExp(`#\n%%\n${MD_TEXTELEMENTS}(?:\n|$)`, "m");
const RE_TEXTELEMENTS_NOSECTION_OK = new RegExp(`^(%%\n)?${MD_TEXTELEMENTS}(?:\n|$)`, "m");
//The issue is that when editing in markdown embeds the user can delete the last enter causing two sections
//to collide. This is particularly problematic when the user is editing the lest section before # Text Elements
@@ -251,19 +256,70 @@ const RE_TEXTELEMENTS_FALLBACK_2 = new RegExp(`(.*)${MD_TEXTELEMENTS}(?:\n|$)`,
const RE_DRAWING = new RegExp(`(%%\n)?${MD_DRAWING}\n`);
export const getExcalidrawMarkdownHeaderSection = (data:string, keys?:[string,string][]):string => {
let trimLocation = data.search(RE_TEXTELEMENTS);
/* Expected markdown structure:
bla bla bla
#
%%
# Text Elements
*/
let trimLocation = data.search(RE_TEXTELEMENTS_WITHSECTION_OK);
let shouldFixTrailingHashtag = false;
if(trimLocation > 0) {
trimLocation += 2;
}
/* Expected markdown structure:
bla bla bla#
%%
# Text Elements
*/
if(trimLocation === -1) {
trimLocation = data.search(RE_TEXTELEMENTS_WITHSECTION_NOTOK);
if(trimLocation > 0) {
shouldFixTrailingHashtag = true;
}
}
/* Expected markdown structure
a)
bla bla bla
%%
# Text Elements
b)
bla bla bla
# Text Elements
*/
if(trimLocation === -1) {
trimLocation = data.search(RE_TEXTELEMENTS_NOSECTION_OK);
}
/* Expected markdown structure:
bla bla bla%%
# Text Elements
*/
if(trimLocation === -1) {
const res = data.match(RE_TEXTELEMENTS_FALLBACK_1);
if(res && Boolean(res[1])) {
trimLocation = res.index + res[1].length;
}
}
/* Expected markdown structure:
bla bla bla# Text Elements
*/
if(trimLocation === -1) {
const res = data.match(RE_TEXTELEMENTS_FALLBACK_2);
if(res && Boolean(res[1])) {
trimLocation = res.index + res[1].length;
}
}
}
/* Expected markdown structure:
a)
bla bla bla
# Drawing
b)
bla bla bla
%%
# Drawing
*/
if (trimLocation === -1) {
trimLocation = data.search(RE_DRAWING);
}
@@ -278,7 +334,9 @@ export const getExcalidrawMarkdownHeaderSection = (data:string, keys?:[string,st
header = header.replace(REG_IMG, "$1");
}
//end of remove
return header.endsWith("\n") ? header : (header + "\n");
return shouldFixTrailingHashtag
? header + "\n#\n"
: header.endsWith("\n") ? header : (header + "\n");
}
@@ -580,19 +638,36 @@ export class ExcalidrawData {
data = data.substring(0, sceneJSONandPOS.pos);
//The Markdown # Text Elements take priority over the JSON text elements. Assuming the scenario in which the link was updated due to filename changes
//The Markdown # Text Elements take priority over the JSON text elements. Assuming the scenario in which the
//link was updated due to filename changes
//The .excalidraw JSON is modified to reflect the MD in case of difference
//Read the text elements into the textElements Map
let position = data.search(RE_TEXTELEMENTS);
let position = data.search(RE_TEXTELEMENTS_NOSECTION_OK);
if (position === -1) {
//resillience in case back of the note was saved right on top of text elements
// # back of note section
// ....# Text Elements
// ....
// --------------
// instead of
// --------------
// # back of note section
// ....
// # Text Elements
position = data.search(RE_TEXTELEMENTS_FALLBACK_2);
}
if (position === -1) {
await this.setTextMode(textMode, false);
this.loaded = true;
return true; //Text Elements header does not exist
}
const textElementsMatch = data.match(new RegExp(`^((%%\n)?${MD_TEXTELEMENTS}(?:\n|$))`, "m"))[0]
position += textElementsMatch.length;
data = data.slice(position);
const normalMatch = data.match(new RegExp(`^((%%\n)?${MD_TEXTELEMENTS}(?:\n|$))`, "m"));
const textElementsMatch = normalMatch
? normalMatch[0]
: data.match(new RegExp(`(.*${MD_TEXTELEMENTS}(?:\n|$))`, "m"))[0];
data = data.slice(textElementsMatch.length);
this.textElementCommentedOut = textElementsMatch.startsWith("%%\n");
position = 0;
let parts;
@@ -1360,14 +1435,18 @@ export class ExcalidrawData {
const processedIds = new Set<string>();
fileIds.forEach((fileId,idx)=>{
if(processedIds.has(fileId)) {
const file = this.getFile(fileId);
const embeddedFile = this.getFile(fileId);
const equation = this.getEquation(fileId);
const mermaid = this.getMermaid(fileId);
//images should have a single reference, but equations, and markdown embeds should have as many as instances of the file in the scene
if(file && (file.isHyperLink || file.isLocalLink || (file.file && (file.file.extension !== "md" || this.plugin.isExcalidrawFile(file.file))))) {
if (embeddedFile &&
(embeddedFile.isHyperLink || embeddedFile.isLocalLink ||
(embeddedFile.file &&
(embeddedFile.file.extension !== "md" || this.plugin.isExcalidrawFile(embeddedFile.file))
)
)
) {
return;
}
if(mermaid) {
@@ -1379,6 +1458,11 @@ export class ExcalidrawData {
return;
}
if(!embeddedFile && !equation && !mermaid) {
//processing freshly pasted images from likely anotehr instance of excalidraw (e.g. Excalidraw.com, or another Obsidian instance)
return;
}
const newId = fileid();
(scene
.elements
@@ -1387,8 +1471,8 @@ export class ExcalidrawData {
.fileId = newId;
dirty = true;
processedIds.add(newId);
if(file) {
this.setFile(newId as FileId,new EmbeddedFile(this.plugin,this.file.path,file.linkParts.original));
if(embeddedFile) {
this.setFile(newId as FileId,new EmbeddedFile(this.plugin,this.file.path,embeddedFile.linkParts.original));
}
if(equation) {
this.setEquation(newId as FileId, {latex:equation.latex, isLoaded:false});

View File

@@ -9,14 +9,12 @@ import {
MarkdownView,
request,
requireApiVersion,
requestUrl,
} from "obsidian";
//import * as React from "react";
//import * as ReactDOM from "react-dom";
//import Excalidraw from "@zsviczian/excalidraw";
import {
ExcalidrawElement,
ExcalidrawGenericElement,
ExcalidrawImageElement,
ExcalidrawTextElement,
FileId,
@@ -82,12 +80,10 @@ import {
} from "./utils/FileUtils";
import {
checkExcalidrawVersion,
debug,
embedFontsInSVG,
errorlog,
getEmbeddedFilenameParts,
getExportTheme,
getLinkParts,
getPNG,
getPNGScale,
getSVG,
@@ -105,7 +101,7 @@ import {
shouldEmbedScene,
getContainerElement,
} from "./utils/Utils";
import { cleanSectionHeading, getLeaf, getParentOfClass, obsidianPDFQuoteWithRef, openLeaf } from "./utils/ObsidianUtils";
import { cleanBlockRef, cleanSectionHeading, getLeaf, getParentOfClass, obsidianPDFQuoteWithRef, openLeaf } from "./utils/ObsidianUtils";
import { splitFolderAndFilename } from "./utils/FileUtils";
import { ConfirmationPrompt, GenericInputPrompt, NewFileActions, Prompt, linkPrompt } from "./dialogs/Prompt";
import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard";
@@ -140,7 +136,9 @@ import { CustomMutationObserver, isDebugMode } from "./utils/DebugHelper";
import { extractCodeBlocks, postOpenAI } from "./utils/AIUtils";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { SelectCard } from "./dialogs/SelectCard";
import { link } from "fs";
const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000;
const PREVENT_RELOAD_TIMEOUT = 2000;
declare const PLUGIN_VERSION:string;
@@ -620,7 +618,7 @@ export default class ExcalidrawView extends TextFileView {
public setPreventReload() {
this.semaphores.preventReload = true;
const self = this;
this.preventReloadResetTimer = setTimeout(()=>self.semaphores.preventReload = false,2000);
this.preventReloadResetTimer = setTimeout(()=>self.semaphores.preventReload = false,PREVENT_RELOAD_TIMEOUT);
}
public clearPreventReloadTimer() {
@@ -647,7 +645,7 @@ export default class ExcalidrawView extends TextFileView {
public clearEmbeddableIsEditingSelf() {
const self = this;
this.clearEmbeddableIsEditingSelfTimer();
this.editingSelfResetTimer = setTimeout(()=>self.semaphores.embeddableIsEditingSelf = false,2000);
this.editingSelfResetTimer = setTimeout(()=>self.semaphores.embeddableIsEditingSelf = false,EMBEDDABLE_SEMAPHORE_TIMEOUT);
}
async save(preventReload: boolean = true, forcesave: boolean = false) {
@@ -681,30 +679,28 @@ export default class ExcalidrawView extends TextFileView {
}
try {
const allowSave = Boolean (
(this.semaphores.dirty !== null && this.semaphores.dirty) ||
this.semaphores.autosaving ||
forcesave
); //dirty == false when view.file == null;
const scene = this.getScene();
if (this.compatibilityMode) {
await this.excalidrawData.syncElements(scene);
} else if (
await this.excalidrawData.syncElements(scene, this.excalidrawAPI.getAppState().selectedElementIds)
&& !this.semaphores.popoutUnload //Obsidian going black after REACT 18 migration when closing last leaf on popout
) {
await this.loadDrawing(
false,
this.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted)
);
}
const allowSave = this.isDirty() || forcesave; //removed this.semaphores.autosaving
if(isDebugMode) console.log({allowSave, isDirty: this.isDirty(), autosaving: this.semaphores.autosaving, forcesave});
if (allowSave) {
const scene = this.getScene();
if (this.compatibilityMode) {
await this.excalidrawData.syncElements(scene);
} else if (
await this.excalidrawData.syncElements(scene, this.excalidrawAPI.getAppState().selectedElementIds)
&& !this.semaphores.popoutUnload //Obsidian going black after REACT 18 migration when closing last leaf on popout
) {
await this.loadDrawing(
false,
this.excalidrawAPI.getSceneElementsIncludingDeleted().filter((el:ExcalidrawElement)=>el.isDeleted)
);
}
//reload() is triggered indirectly when saving by the modifyEventHandler in main.ts
//prevent reload is set here to override reload when not wanted: typically when the user is editing
//and we do not want to interrupt the flow by reloading the drawing into the canvas.
this.clearDirty();
this.clearPreventReloadTimer();
this.semaphores.preventReload = preventReload;
@@ -717,7 +713,7 @@ export default class ExcalidrawView extends TextFileView {
triggerReload = (this.lastSaveTimestamp === this.file.stat.mtime) &&
!preventReload && forcesave;
this.lastSaveTimestamp = this.file.stat.mtime;
this.clearDirty();
//this.clearDirty(); //moved to right after allow save, to avoid autosave collision with load drawing
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/629
//there were odd cases when preventReload semaphore did not get cleared and consequently a synchronized image
@@ -942,6 +938,46 @@ export default class ExcalidrawView extends TextFileView {
return false;
}
private getLinkTextForElement(
selectedText:SelectedElementWithLink,
selectedElementWithLink?:SelectedElementWithLink
): {
linkText: string,
selectedElement: ExcalidrawElement,
} {
if (selectedText?.id || selectedElementWithLink?.id) {
const selectedTextElement: ExcalidrawTextElement = selectedText.id
? this.excalidrawAPI.getSceneElements().find((el:ExcalidrawElement)=>el.id === selectedText.id)
: null;
const selectedElement = selectedElementWithLink.id
? this.excalidrawAPI.getSceneElements().find((el:ExcalidrawElement)=>el.id === selectedElementWithLink.id)
: null;
let linkText =
selectedElementWithLink?.text ??
(this.textMode === TextMode.parsed
? this.excalidrawData.getRawText(selectedText.id)
: selectedText.text);
const partsArray = REGEX_LINK.getResList(linkText);
if (!linkText || partsArray.length === 0) {
//the container link takes precedence over the text link
if(selectedTextElement?.containerId) {
const container = getContainerElement(selectedTextElement, {elements: this.excalidrawAPI.getSceneElements()});
if(container) {
linkText = container.link;
}
}
if(!linkText || partsArray.length === 0) {
linkText = selectedTextElement?.link;
}
}
return {linkText, selectedElement: selectedTextElement ?? selectedElement};
}
return {linkText: null, selectedElement: null};
}
async linkClick(
ev: MouseEvent | null,
selectedText: SelectedElementWithLink,
@@ -959,41 +995,16 @@ export default class ExcalidrawView extends TextFileView {
let file = null;
let subpath: string = null;
let linkText: string = null;
if (selectedText?.id || selectedElementWithLink?.id) {
const selectedTextElement: ExcalidrawTextElement = selectedText.id
? this.excalidrawAPI.getSceneElements().find((el:ExcalidrawElement)=>el.id === selectedText.id)
: null;
linkText =
selectedElementWithLink?.text ??
(this.textMode === TextMode.parsed
? this.excalidrawData.getRawText(selectedText.id)
: selectedText.text);
const partsArray = REGEX_LINK.getResList(linkText);
if (!linkText || partsArray.length === 0) {
//the container link takes precedence over the text link
if(selectedTextElement?.containerId) {
const container = getContainerElement(selectedTextElement, {elements: this.excalidrawAPI.getSceneElements()});
if(container) {
linkText = container.link;
}
}
if(!linkText) {
linkText = selectedTextElement?.link;
}
}
let {linkText, selectedElement} = this.getLinkTextForElement(selectedText, selectedElementWithLink);
//if (selectedText?.id || selectedElementWithLink?.id) {
if (selectedElement) {
if (!linkText) {
return;
}
linkText = linkText.replaceAll("\n", ""); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/187
if(this.handleLinkHookCall(selectedTextElement,linkText,ev)) return;
if(this.handleLinkHookCall(selectedElement,linkText,ev)) return;
if(openExternalLink(linkText, this.app)) return;
const result = await linkPrompt(linkText, this.app, this);
@@ -1010,8 +1021,6 @@ export default class ExcalidrawView extends TextFileView {
selectedImage.fileId,
).latex;
GenericInputPrompt.Prompt(this,this.plugin,this.app,t("ENTER_LATEX"),undefined,equation, undefined, 3).then(async (formula: string) => {
// const prompt = new Prompt(this.app, t("ENTER_LATEX"), equation, "");
// prompt.openAndGetValue(async (formula: string) => {
if (!formula || formula === equation) {
return;
}
@@ -1182,7 +1191,7 @@ export default class ExcalidrawView extends TextFileView {
? null
: this.getSelectedImageElement();
const selectedElementWithLink =
selectedImage?.id || selectedText?.id
(selectedImage?.id || selectedText?.id)
? null
: this.getSelectedElementWithLink();
this.linkClick(
@@ -1336,7 +1345,7 @@ export default class ExcalidrawView extends TextFileView {
const self = this;
this.slidingPanesListner = () => {
if (self.excalidrawAPI) {
self.refresh();
self.refreshCanvasOffset();
}
};
let rootSplit = this.app.workspace.rootSplit as WorkspaceItem as WorkspaceItemExt;
@@ -1380,7 +1389,7 @@ export default class ExcalidrawView extends TextFileView {
const { offsetLeft, offsetTop } = target;
if (offsetLeft !== self.offsetLeft || offsetTop != self.offsetTop) {
if (self.excalidrawAPI) {
self.refresh();
self.refreshCanvasOffset();
}
self.offsetLeft = offsetLeft;
self.offsetTop = offsetTop;
@@ -1474,19 +1483,19 @@ export default class ExcalidrawView extends TextFileView {
return;
}
const st = api.getAppState();
const editing = st.editingElement !== null;
const isEditing = st.editingElement !== null;
const isDragging = st.draggingElement !== null;
//this will reset positioning of the cursor in case due to the popup keyboard,
//or the command palette, or some other unexpected reason the onResize would not fire...
this.refresh();
this.refreshCanvasOffset();
if (
this.semaphores.dirty &&
this.semaphores.dirty == this.file?.path &&
this.isDirty() &&
this.plugin.settings.autosave &&
!this.semaphores.forceSaving &&
!this.semaphores.autosaving &&
!this.semaphores.embeddableIsEditingSelf &&
!editing &&
st.draggingElement === null //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/630
!isEditing &&
!isDragging //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/630
) {
//console.log("autosave");
this.autosaveTimer = null;
@@ -1513,14 +1522,22 @@ export default class ExcalidrawView extends TextFileView {
};
this.autosaveFunction = timer;
this.resetAutosaveTimer();
}
private resetAutosaveTimer() {
if(!this.autosaveFunction) return;
if (this.autosaveTimer) {
clearTimeout(this.autosaveTimer);
this.autosaveTimer = null;
} // clear previous timer if one exists
this.autosaveTimer = setTimeout(
timer,
this.autosaveFunction,
this.plugin.settings.autosaveInterval,
);
}
//save current drawing when user closes workspace leaf
@@ -1683,10 +1700,26 @@ export default class ExcalidrawView extends TextFileView {
) await sleep(50); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/734
}
const filenameParts = getEmbeddedFilenameParts(state.subpath);
const filenameParts = getEmbeddedFilenameParts(
(state.subpath && state.subpath.startsWith("#^group") && !state.subpath.startsWith("#^group="))
? "#^group=" + state.subpath.substring(7)
: (state.subpath && state.subpath.startsWith("#^area") && !state.subpath.startsWith("#^area="))
? "#^area=" + state.subpath.substring(6)
: state.subpath
);
if(filenameParts.hasBlockref) {
setTimeout(async () => {
await waitForExcalidraw();
if(filenameParts.blockref && !filenameParts.hasGroupref) {
if(!self.getScene()?.elements.find(el=>el.id === filenameParts.blockref)) {
const cleanQuery = cleanSectionHeading(filenameParts.blockref).replaceAll(" ","");
const blocks = await self.getBackOfTheNoteBlocks();
if(blocks.includes(cleanQuery)) {
this.setMarkdownView(state);
return;
}
}
}
setTimeout(()=>self.zoomToElementId(filenameParts.blockref, filenameParts.hasGroupref));
});
}
@@ -1731,13 +1764,20 @@ export default class ExcalidrawView extends TextFileView {
}
}
self.selectElementsMatchingQuery(
if(!self.selectElementsMatchingQuery(
elements,
query,
!api.getAppState().viewModeEnabled,
filenameParts.hasSectionref,
filenameParts.hasGroupref
);
)) {
const cleanQuery = cleanSectionHeading(query[0]).replaceAll(" ","");
const sections = await self.getBackOfTheNoteSections();
if(sections.includes(cleanQuery)) {
self.setMarkdownView(state);
return;
}
}
});
}
@@ -2223,6 +2263,7 @@ export default class ExcalidrawView extends TextFileView {
}
public setDirty(debug?:number) {
if(this.semaphores.saving) return; //do not set dirty if saving
if(isDebugMode) console.log(debug);
this.semaphores.dirty = this.file?.path;
this.diskIcon.querySelector("svg").addClass("excalidraw-dirty");
@@ -2237,6 +2278,10 @@ export default class ExcalidrawView extends TextFileView {
}
}
public isDirty() {
return Boolean(this.semaphores.dirty) && (this.semaphores.dirty === this.file?.path);
}
public clearDirty() {
if(this.semaphores.viewunload) return;
const api = this.excalidrawAPI;
@@ -2302,17 +2347,17 @@ export default class ExcalidrawView extends TextFileView {
return ICON_NAME;
}
setMarkdownView() {
setMarkdownView(eState?: any) {
this.plugin.excalidrawFileModes[this.id || this.file.path] = "markdown";
this.plugin.setMarkdownView(this.leaf);
this.plugin.setMarkdownView(this.leaf, eState);
}
public async openAsMarkdown() {
public async openAsMarkdown(eState?: any) {
if (this.plugin.settings.compress === true) {
this.excalidrawData.disableCompression = true;
await this.save(true, true);
}
this.setMarkdownView();
this.setMarkdownView(eState);
}
public async convertExcalidrawToMD() {
@@ -2891,6 +2936,7 @@ export default class ExcalidrawView extends TextFileView {
currentStrokeOptions: st.currentStrokeOptions,
previousGridSize: st.previousGridSize,
frameRendering: st.frameRendering,
objectsSnapModeEnabled: st.objectsSnapModeEnabled,
},
prevTextMode: this.prevTextMode,
files,
@@ -2901,7 +2947,7 @@ export default class ExcalidrawView extends TextFileView {
* ExcalidrawAPI refreshes canvas offsets
* @returns
*/
private refresh() {
private refreshCanvasOffset() {
if(this.contentEl.clientWidth === 0 || this.contentEl.clientHeight === 0) return;
const api = this.excalidrawAPI;
if (!api) {
@@ -3002,41 +3048,52 @@ export default class ExcalidrawView extends TextFileView {
if (!linktext) {
if(!this.currentPosition) return;
linktext = "";
const selectedElement = getTextElementAtPointer(this.currentPosition, this);
if (!selectedElement || !selectedElement.text) {
const selectedEl = getTextElementAtPointer(this.currentPosition, this);
if (!selectedEl || !selectedEl.text) {
const selectedImgElement =
getImageElementAtPointer(this.currentPosition, this);
const selectedElementWithLink = (selectedImgElement?.id || selectedImgElement?.id)
? null
: getElementWithLinkAtPointer(this.currentPosition, this);
element = this.excalidrawAPI.getSceneElements().find((el:ExcalidrawElement)=>el.id === selectedImgElement.id);
if (!selectedImgElement || !selectedImgElement.fileId) {
if ((!selectedImgElement || !selectedImgElement.fileId) && !selectedElementWithLink?.id) {
return;
}
if (!this.excalidrawData.hasFile(selectedImgElement.fileId)) {
return;
if (selectedImgElement?.id) {
if (!this.excalidrawData.hasFile(selectedImgElement.fileId)) {
return;
}
const ef = this.excalidrawData.getFile(selectedImgElement.fileId);
if (
(ef.isHyperLink || ef.isLocalLink) || //web images don't have a preview
(IMAGE_TYPES.contains(ef.file.extension)) || //images don't have a preview
(ef.file.extension.toLowerCase() === "pdf") || //pdfs don't have a preview
(this.plugin.ea.isExcalidrawFile(ef.file))
) {//excalidraw files don't have a preview
linktext = getLinkTextFromLink(element.link);
if(!linktext) return;
} else {
const ref = ef.linkParts.ref
? `#${ef.linkParts.isBlockRef ? "^" : ""}${ef.linkParts.ref}`
: "";
linktext =
ef.file.path + ref;
}
}
const ef = this.excalidrawData.getFile(selectedImgElement.fileId);
if (
(ef.isHyperLink || ef.isLocalLink) || //web images don't have a preview
(IMAGE_TYPES.contains(ef.file.extension)) || //images don't have a preview
(ef.file.extension.toLowerCase() === "pdf") || //pdfs don't have a preview
(this.plugin.ea.isExcalidrawFile(ef.file))
) {//excalidraw files don't have a preview
linktext = getLinkTextFromLink(element.link);
if (selectedElementWithLink?.id) {
linktext = getLinkTextFromLink(selectedElementWithLink.text);
if(!linktext) return;
} else {
const ref = ef.linkParts.ref
? `#${ef.linkParts.isBlockRef ? "^" : ""}${ef.linkParts.ref}`
: "";
linktext =
ef.file.path + ref;
}
} else {
element = this.excalidrawAPI.getSceneElements().filter((el:ExcalidrawElement)=>el.id === selectedElement.id)[0];
const text: string =
this.textMode === TextMode.parsed
? this.excalidrawData.getRawText(selectedElement.id)
: selectedElement.text;
const {linkText, selectedElement} = this.getLinkTextForElement(selectedEl, selectedEl);
element = selectedElement;
/*this.excalidrawAPI.getSceneElements().filter((el:ExcalidrawElement)=>el.id === selectedElement.id)[0];
const text: string =
this.textMode === TextMode.parsed
? this.excalidrawData.getRawText(selectedElement.id)
: selectedElement.text;*/
linktext = getLinkTextFromLink(text);
linktext = getLinkTextFromLink(linkText);
if(!linktext) return;
}
}
@@ -3381,7 +3438,7 @@ export default class ExcalidrawView extends TextFileView {
}
if (data.elements) {
const self = this;
setTimeout(() => self.save(false), 300);
setTimeout(() => self.save(), 300); //removed prevent reload = false, as reload was triggered when pasted containers were processed and there was a conflict with the new elements
}
return true;
}
@@ -4052,6 +4109,19 @@ export default class ExcalidrawView extends TextFileView {
}
}
private async getBackOfTheNoteSections() {
return (await this.app.metadataCache.blockCache.getForFile({ isCancelled: () => false },this.file))
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
.filter((b: any) => !MD_EX_SECTIONS.includes(b.display))
.map((b: any) => cleanSectionHeading(b.display));
}
private async getBackOfTheNoteBlocks() {
return (await this.app.metadataCache.blockCache.getForFile({ isCancelled: () => false },this.file))
.blocks.filter((b:any) => b.display && b.node && b.node.hasOwnProperty("type") && b.node.hasOwnProperty("id"))
.map((b:any) => cleanBlockRef(b.node.id));
}
public getSingleSelectedImage(): {imageEl: ExcalidrawImageElement, embeddedFile: EmbeddedFile} {
if(!this.excalidrawAPI) return null;
const els = this.getViewSelectedElements().filter(el=>el.type==="image");
@@ -4064,13 +4134,9 @@ export default class ExcalidrawView extends TextFileView {
}
public async insertBackOfTheNoteCard() {
const sections = (await this.app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },this.file))
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
.filter((b: any) => !MD_EX_SECTIONS.includes(b.display))
.map((b: any) => cleanSectionHeading(b.display));
const sections = await this.getBackOfTheNoteSections();
const selectCardDialog = new SelectCard(this.app,this,sections);
selectCardDialog.start();
selectCardDialog.start();
}
public async convertImageElWithURLToLocalFile(data: {imageEl: ExcalidrawImageElement, embeddedFile: EmbeddedFile}) {
@@ -4176,6 +4242,7 @@ export default class ExcalidrawView extends TextFileView {
const onContextMenu = (elements: readonly ExcalidrawElement[], appState: AppState, onClose: (callback?: () => void) => void) => {
const contextMenuActions = [];
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
const areElementsSelected = Object.keys(api.getAppState().selectedElementIds).length>0
if(this.isLinkSelected()) {
contextMenuActions.push([
@@ -4290,6 +4357,17 @@ export default class ExcalidrawView extends TextFileView {
]);
}
if(areElementsSelected) {
contextMenuActions.push([
renderContextMenuAction(
t("COPY_ELEMENT_LINK"),
() => {
this.copyLinkToSelectedElementToClipboard("");
},
onClose
),
]);
}
contextMenuActions.push([
renderContextMenuAction(
t("INSERT_CARD"),
@@ -4670,7 +4748,7 @@ export default class ExcalidrawView extends TextFileView {
this.toolsPanelRef.current.updatePosition();
}
if(this.ownerDocument !== document) {
this.refresh(); //because resizeobserver in Excalidraw does not seem to work when in Obsidian Window
this.refreshCanvasOffset(); //because resizeobserver in Excalidraw does not seem to work when in Obsidian Window
}
} catch (err) {
errorlog({
@@ -4909,13 +4987,23 @@ export default class ExcalidrawView extends TextFileView {
this.plugin.saveSettings();
}
/**
*
* @param elements
* @param query
* @param selectResult
* @param exactMatch
* @param selectGroup
* @returns true if element found, false if no element is found.
*/
public selectElementsMatchingQuery(
elements: ExcalidrawElement[],
query: string[],
selectResult: boolean = true,
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
selectGroup: boolean = false,
) {
):boolean {
let match = getTextElementsMatchingQuery(
elements.filter((el: ExcalidrawElement) => el.type === "text"),
query,
@@ -4928,7 +5016,7 @@ export default class ExcalidrawView extends TextFileView {
if (match.length === 0) {
new Notice("I could not find a matching text element");
return;
return false;
}
if(selectGroup) {
@@ -4939,6 +5027,7 @@ export default class ExcalidrawView extends TextFileView {
}
this.zoomToElements(selectResult,match);
return true;
}
public zoomToElements(

View File

@@ -17,6 +17,39 @@ 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>
`,
"2.1.5":`
## New
- Save "Snap to objects" with the scene state. If this is the only change you make to the scene, force save it using CTRL+S (note, use CTRL on Mac as well).
- Added "Copy markdown link" to the context menu.
## Fixed
- Paste operation occasionally duplicated text elements.
- Pasting multiple instances of the same image from excalidraw.com or another instance of Obsidian, or pasting an image from anywhere and making copies with ALT/OPT + drag immediately after pasting (before autosave triggered) led to broken images when reopening the drawing.
- CTRL/CMD+Click on a Text Element with an element link did not work (previously, you had to click the top right link indicator). Now, you can click anywhere on the element.
- Hover preview for elements with a link only worked when hovering over the element link. Now, you can hover anywhere. If there are multiple elements with links, the top-level element will take precedence.
- Link navigation within drawing when the "Focus on Existing Tab" feature is enabled under "Links, transclusion and TODOs" in settings works again.
- If a link points to a back-of-the-card section or block the drawing will automatically switch to markdown view mode and navigate to the block or section.
- DynamicSytle, dark mode when canvas background is set to transparent.
- Scale to maintain the aspect ratio of a markdown notes embedded as images.
- You can now borrow interactive markdown embeds to tables, blockquotes, list elements and callouts - not just paragraphs.
- Back of the drawing cards:
- Leaving the Section Name empty when creating the first back of the card note resulted in an error.
- If you add the markdown comment (${String.fromCharCode(96)}%%${String.fromCharCode(96)}) directly before ${String.fromCharCode(96)}# Text Elements${String.fromCharCode(96)}, a trailing ${String.fromCharCode(96)}#${String.fromCharCode(96)} will be added to your document, when adding a back of the card note. This is to hide the markdown comment from the card. The trailing (empty) ${String.fromCharCode(96)}#${String.fromCharCode(96)} will not be visible in reading mode, pdf exports, and when publishing with Obsidian Publish.
Here's a sample markdown structure of your document:
${String.fromCharCode(96,96,96)}markdown
---
excalidraw-plugin: parsed
---
# Your back of the card section
bla bla bla
#
%%
# Text Elements
... the rest of the Excalidraw file
${String.fromCharCode(96,96,96)}
`,
"2.1.4":`
## Fixed
- Fixed the **aspect ratio** of an Excalidraw embedded within another Excalidraw **not updating**. [#1707](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1707)
@@ -33,7 +66,7 @@ I develop this plugin as a hobby, spending my free time doing this. If you find
- **Enhanced annotation and cropping** of images in Markdown documents:
- Newly embedded **links will now follow the style of the original link**. If the original format was a ${String.fromCharCode(96)}![markdown](link)${String.fromCharCode(96)}, the annotated file will follow this format. For ${String.fromCharCode(96)}[[wiki links]]${String.fromCharCode(96)}, it will follow that style. Additionally, if an alias was specified like ${String.fromCharCode(96)}[[link|alias]]${String.fromCharCode(96)}, the annotated or cropped image will retain the alias.
- Introduced a new setting under "Saving" titled **"Preserve image size when annotating"**. This setting is disabled by default. When enabled, the embed link replacing the annotated image will maintain the size of the original image.
- Option to **automaticaly embed the scene in exported PNG and SVG image files**. Including the scene will allow users to open the picture on Excalidraw.com or in another Obsidian Vault as an editable Excalidraw file.New setting is under the Export category. The new frontmatter tag is: ${String.fromCharCode(96)}excalidraw-export-embed-scene: true/false${String.fromCharCode(96)}.
- Option to **automatically embed the scene in exported PNG and SVG image files**. Including the scene will allow users to open the picture on Excalidraw.com or in another Obsidian Vault as an editable Excalidraw file.New setting is under the Export category. The new frontmatter tag is: ${String.fromCharCode(96)}excalidraw-export-embed-scene: true/false${String.fromCharCode(96)}.
`,
"2.1.3":`
This is a republish of 2.1.2 with a minor change. Sorry about the frequent releases. I will hold back for a few weeks now.

View File

@@ -5,7 +5,6 @@ import { getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { getExcalidrawMarkdownHeaderSection } from "src/ExcalidrawData";
import { MD_EX_SECTIONS } from "src/constants/constants";
import { ExcalidrawImageElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { cleanSectionHeading } from "src/utils/ObsidianUtils";
@@ -29,7 +28,7 @@ export class SelectCard extends FuzzySuggestModal<string> {
if (e.key == "Enter") {
if (this.containerEl.innerText.includes(t("EMPTY_SECTION_MESSAGE"))) {
const item = this.inputEl.value;
if(MD_EX_SECTIONS.includes(item)) {
if(item === "" || MD_EX_SECTIONS.includes(item)) {
new Notice(t("INVALID_SECTION_NAME"));
this.close();
return;
@@ -37,7 +36,13 @@ export class SelectCard extends FuzzySuggestModal<string> {
(async () => {
const data = view.data;
const header = getExcalidrawMarkdownHeaderSection(data);
view.data = data.replace(header, header + `\n# ${item}\n\n`);
const body = data.split(header)[1];
const shouldAddHashtag = body && body.startsWith("%%");
const shouldRemoveTrailingHashtag = header.endsWith("#\n");
view.data = data.replace(
header,
(shouldRemoveTrailingHashtag ? header.substring(0,header.length-2) : header) +
`\n# ${item}\n\n${shouldAddHashtag ? "#\n" : ""}`);
await view.forceSave(true);
let watchdog = 0;
await sleep(200);

View File

@@ -197,6 +197,7 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
' "excalidraw-export-dark"?: boolean;\n' +
' "excalidraw-export-padding"?: number;\n' +
' "excalidraw-export-pngscale"?: number;\n' +
' "excalidraw-export-embed-scene"?: boolean;\n' +
' "excalidraw-default-mode"?: "view" | "zen";\n' +
' "excalidraw-onload-script"?: string;\n' +
' "excalidraw-linkbutton-opacity"?: number;\n' +
@@ -821,6 +822,12 @@ export const FRONTMATTER_KEYS_INFO: SuggesterInfo[] = [
desc: "If this key is present it will override the default excalidraw embed and export setting. This only affects export to PNG. Specify the export scale for the image. The typical range is between 0.5 and 5, but you can experiment with other values as well.",
after: ": 1",
},
{
field: "excalidraw-export-embed-scene",
code: null,
desc: "If this key is present it will override the default excalidraw embed and export setting.",
after: ": false",
},
{
field: "open-md",
code: null,

View File

@@ -48,6 +48,7 @@ export default {
NEW_IN_POPOUT_WINDOW_EMBED: "Create new drawing - IN A POPOUT WINDOW - and embed into active document",
TOGGLE_LOCK: "Toggle Text Element between edit RAW and PREVIEW",
DELETE_FILE: "Delete selected image or Markdown file from Obsidian Vault",
COPY_ELEMENT_LINK: "Copy markdown link for selected element(s)",
INSERT_LINK_TO_ELEMENT:
`Copy markdown link for selected element to clipboard. ${labelCTRL()}+CLICK to copy 'group=' link. ${labelSHIFT()}+CLICK to copy an 'area=' link. ${labelALT()}+CLICK to watch a help video.`,
INSERT_LINK_TO_ELEMENT_GROUP:
@@ -684,7 +685,7 @@ FILENAME_HEAD: "Filename",
SELECT_SECTION_OR_TYPE_NEW:
"Select existing section or type name of a new section then press Enter.",
INVALID_SECTION_NAME: "Invalid section name.",
EMPTY_SECTION_MESSAGE: "Hit enter to create a new Section",
EMPTY_SECTION_MESSAGE: "Type the Section Name and hit enter to create a new Section",
//EmbeddedFileLoader.ts
INFINITE_LOOP_WARNING:

View File

@@ -3193,7 +3193,7 @@ export default class ExcalidrawPlugin extends Plugin {
return file.path;
}
public async setMarkdownView(leaf: WorkspaceLeaf) {
public async setMarkdownView(leaf: WorkspaceLeaf, eState?: any) {
const state = leaf.view.getState();
//Note v2.0.19: I have absolutely no idea why I thought this is necessary. Removing this.
@@ -3209,7 +3209,7 @@ export default class ExcalidrawPlugin extends Plugin {
state,
popstate: true,
} as ViewState,
{ focus: true },
eState ? eState : { focus: true },
);
}

View File

@@ -172,7 +172,9 @@ export class EmbeddableMenu {
view.updateScene({appState: {activeEmbeddable: null}});
const paragraphs = (await app.metadataCache.blockCache
.getForFile({ isCancelled: () => false },file))
.blocks.filter((b: any) => b.display && b.node?.type === "paragraph");
.blocks.filter((b: any) => b.display && b.node &&
(b.node.type === "paragraph" || b.node.type === "blockquote" || b.node.type === "listItem" || b.node.type === "table" || b.node.type === "callout")
);
const values = ["entire-file"].concat(paragraphs);
const display = [t("SHOW_ENTIRE_FILE")].concat(
paragraphs.map((b: any) => `${b.node?.id ? `#^${b.node.id}: ` : ``}${b.display.trim()}`));

View File

@@ -32,6 +32,10 @@ export const setDynamicStyle = (
view?.excalidrawAPI?.getAppState?.()?.theme === "light" ||
view?.excalidrawData?.scene?.appState?.theme === "light";
if (color==="transparent") {
color = "#ffffff";
}
const darker = "#101010";
const lighter = "#f0f0f0";
const step = 10;

View File

@@ -22,7 +22,7 @@ export const getElementsAtPointer = (
y <= pointer.y &&
y + h >= pointer.y
);
});
}).reverse();
};
export const getTextElementAtPointer = (pointer: any, view: ExcalidrawView) => {

View File

@@ -285,7 +285,13 @@ export const openLeaf = ({
leaf = l;
}
});
if(leaf) return {leaf, promise: Promise.resolve()};
if(leaf) {
if(openState) {
const promise = leaf.openFile(file, openState);
return {leaf, promise};
}
return {leaf, promise: Promise.resolve()};
}
}
leaf = fnGetLeaf();
const promise = leaf.openFile(file, openState);

View File

@@ -19,6 +19,7 @@ import {
IMAGE_TYPES,
FRONTMATTER_KEYS,
EXCALIDRAW_PLUGIN,
getCommonBoundingBox,
} from "../constants/constants";
import ExcalidrawPlugin from "../main";
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
@@ -157,6 +158,10 @@ const rotate = (
export const rotatedDimensions = (
element: ExcalidrawElement,
): [number, number, number, number] => {
const bb = getCommonBoundingBox([element]);
return [bb.minX, bb.minY, bb.maxX - bb.minX, bb.maxY - bb.minY];
//removed with 2.1.5... will delete later
if (element.angle === 0) {
return [element.x, element.y, element.width, element.height];
}
@@ -450,7 +455,7 @@ export const scaleLoadedImage = (
if(!ef) return false;
const file = EXCALIDRAW_PLUGIN.app.vault.getAbstractFileByPath(ef.path.replace(/#.*$/,"").replace(/\|.*$/,""));
if(!file || (file instanceof TFolder)) return false;
return EXCALIDRAW_PLUGIN.isExcalidrawFile(file as TFile)
return (file as TFile).extension==="md" || EXCALIDRAW_PLUGIN.isExcalidrawFile(file as TFile)
})) {
const [w_image, h_image] = [f.size.width, f.size.height];
const imageAspectRatio = f.size.width / f.size.height;