mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
2.0.26
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.0.25",
|
||||
"version": "2.0.26",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@zsviczian/excalidraw": "0.17.1-obsidian-17",
|
||||
"@zsviczian/excalidraw": "0.17.1-obsidian-18",
|
||||
"chroma-js": "^2.4.2",
|
||||
"clsx": "^2.0.0",
|
||||
"colormaster": "^1.2.1",
|
||||
@@ -31,7 +31,8 @@
|
||||
"polybooljs": "^1.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"roughjs": "^4.5.2"
|
||||
"roughjs": "^4.5.2",
|
||||
"js-yaml": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.9",
|
||||
@@ -49,6 +50,7 @@
|
||||
"@types/node": "^20.10.5",
|
||||
"@types/react": "^18.2.45",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@zerollup/ts-transform-paths": "^1.7.18",
|
||||
"cross-env": "^7.0.3",
|
||||
"cssnano": "^6.0.2",
|
||||
|
||||
@@ -35,6 +35,8 @@ import {
|
||||
REG_LINKINDEX_INVALIDCHARS,
|
||||
THEME_FILTER,
|
||||
mermaidToExcalidraw,
|
||||
MD_TEXTELEMENTS,
|
||||
MD_DRAWING,
|
||||
} from "src/constants/constants";
|
||||
import { blobToBase64, checkAndCreateFolder, getDrawingFilename, getListOfTemplateFiles, getNewUniqueFilepath, } from "src/utils/FileUtils";
|
||||
import {
|
||||
@@ -53,7 +55,7 @@ import {
|
||||
scaleLoadedImage,
|
||||
wrapTextAtCharLength,
|
||||
} from "src/utils/Utils";
|
||||
import { getAttachmentsFolderAndFilePath, getLeaf, getNewOrAdjacentLeaf, isObsidianThemeDark, openLeaf } from "src/utils/ObsidianUtils";
|
||||
import { getAttachmentsFolderAndFilePath, getLeaf, getNewOrAdjacentLeaf, isObsidianThemeDark, mergeMarkdownFiles, openLeaf } from "src/utils/ObsidianUtils";
|
||||
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI, Point } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { EmbeddedFile, EmbeddedFilesLoader, FileData } from "src/EmbeddedFileLoader";
|
||||
import { tex2dataURL } from "src/LaTeX";
|
||||
@@ -91,6 +93,7 @@ import {
|
||||
import { EXCALIDRAW_AUTOMATE_INFO, EXCALIDRAW_SCRIPTENGINE_INFO } from "./dialogs/SuggesterInfo";
|
||||
import { CropImage } from "./utils/CropImage";
|
||||
import { has } from "./svgToExcalidraw/attributes";
|
||||
import { getFrameBasedOnFrameNameOrId } from "./utils/ExcalidrawViewUtils";
|
||||
|
||||
extendPlugins([
|
||||
HarmonyPlugin,
|
||||
@@ -603,6 +606,7 @@ export class ExcalidrawAutomate {
|
||||
"excalidraw-linkbutton-opacity"?: number;
|
||||
"excalidraw-autoexport"?: boolean;
|
||||
"excalidraw-mask"?: boolean;
|
||||
"excalidraw-open-md"?: boolean;
|
||||
"cssclasses"?: string;
|
||||
};
|
||||
plaintext?: string; //text to insert above the `# Text Elements` section
|
||||
@@ -642,6 +646,12 @@ export class ExcalidrawAutomate {
|
||||
: FRONTMATTER;
|
||||
}
|
||||
|
||||
frontmatter += params.plaintext ? params.plaintext + "\n\n" : "";
|
||||
if(template?.frontmatter && params?.frontmatterKeys) {
|
||||
//the frontmatter tags supplyed to create take priority
|
||||
frontmatter = mergeMarkdownFiles(template.frontmatter,frontmatter);
|
||||
}
|
||||
|
||||
const scene = {
|
||||
type: "excalidraw",
|
||||
version: 2,
|
||||
@@ -694,9 +704,8 @@ export class ExcalidrawAutomate {
|
||||
};
|
||||
|
||||
const generateMD = ():string => {
|
||||
let outString = params.plaintext ? params.plaintext + "\n\n" : "";
|
||||
const textElements = this.getElements().filter(el => el.type === "text") as ExcalidrawTextElement[];
|
||||
outString += "# Text Elements\n";
|
||||
let outString = `${MD_TEXTELEMENTS}\n`;
|
||||
textElements.forEach(te=> {
|
||||
outString += `${te.rawText ?? (te.originalText ?? te.text)} ^${te.id}\n\n`;
|
||||
});
|
||||
@@ -735,7 +744,7 @@ export class ExcalidrawAutomate {
|
||||
}
|
||||
}
|
||||
})
|
||||
return outString;
|
||||
return outString + "\n%%\n";
|
||||
}
|
||||
|
||||
const filename = params?.filename
|
||||
@@ -759,14 +768,6 @@ export class ExcalidrawAutomate {
|
||||
}
|
||||
};
|
||||
|
||||
/* getCropImageObject(): CropImage {
|
||||
const scene = this.targetView.getScene();
|
||||
return new CropImage(
|
||||
scene.elements,
|
||||
scene.files,
|
||||
);
|
||||
}*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param templatePath
|
||||
@@ -2760,9 +2761,9 @@ async function getTemplate(
|
||||
textMode,
|
||||
);
|
||||
|
||||
let trimLocation = data.search("# Text Elements\n");
|
||||
let trimLocation = data.search(new RegExp(`^${MD_TEXTELEMENTS}$`,"m"));
|
||||
if (trimLocation == -1) {
|
||||
trimLocation = data.search("# Drawing\n");
|
||||
trimLocation = data.search(`${MD_DRAWING}\n`);
|
||||
}
|
||||
|
||||
let scene = excalidrawData.scene;
|
||||
@@ -2798,9 +2799,10 @@ async function getTemplate(
|
||||
}
|
||||
}
|
||||
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)
|
||||
const el = getFrameBasedOnFrameNameOrId(filenameParts.blockref,scene.elements);
|
||||
|
||||
if(el) {
|
||||
groupElements = plugin.ea.getElementsInFrame(el,scene.elements)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@ import {
|
||||
ERROR_IFRAME_CONVERSION_CANCELED,
|
||||
JSON_parse,
|
||||
FRONTMATTER_KEYS,
|
||||
MD_TEXTELEMENTS,
|
||||
MD_DRAWING,
|
||||
MD_ELEMENTLINKS,
|
||||
} from "./constants/constants";
|
||||
import { _measureText } from "./ExcalidrawAutomate";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
@@ -201,10 +204,10 @@ export function getMarkdownDrawingSection(
|
||||
compressed: boolean,
|
||||
) {
|
||||
return compressed
|
||||
? `%%\n# Drawing\n\x60\x60\x60compressed-json\n${compress(
|
||||
? `# Drawing\n\x60\x60\x60compressed-json\n${compress(
|
||||
jsonString,
|
||||
)}\n\x60\x60\x60\n%%`
|
||||
: `%%\n# Drawing\n\x60\x60\x60json\n${jsonString}\n\x60\x60\x60\n%%`;
|
||||
: `# Drawing\n\x60\x60\x60json\n${jsonString}\n\x60\x60\x60\n%%`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -237,13 +240,35 @@ const estimateMaxLineLen = (text: string, originalText: string): number => {
|
||||
const wrap = (text: string, lineLen: number) =>
|
||||
lineLen ? wrapTextAtCharLength(text, lineLen, false, 0) : text;
|
||||
|
||||
export const getExcalidrawMarkdownHeaderSection = (data:string, keys:[string,string][]):string => {
|
||||
let trimLocation = data.search(/(^%%\n)?# Text Elements\n/m);
|
||||
if (trimLocation == -1) {
|
||||
trimLocation = data.search(/(%%\n)?# Drawing\n/);
|
||||
const RE_TEXTELEMENTS = 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
|
||||
const RE_TEXTELEMENTS_FALLBACK_1 = new RegExp(`(.*)%%\n${MD_TEXTELEMENTS}(?:\n|$)`, "m");
|
||||
const RE_TEXTELEMENTS_FALLBACK_2 = new RegExp(`(.*)${MD_TEXTELEMENTS}(?:\n|$)`, "m");
|
||||
|
||||
|
||||
const RE_DRAWING = new RegExp(`(%%\n)?${MD_DRAWING}\n`);
|
||||
|
||||
export const getExcalidrawMarkdownHeaderSection = (data:string, keys?:[string,string][]):string => {
|
||||
let trimLocation = data.search(RE_TEXTELEMENTS);
|
||||
if(trimLocation === -1) {
|
||||
const res = data.match(RE_TEXTELEMENTS_FALLBACK_1);
|
||||
if(res && Boolean(res[1])) {
|
||||
trimLocation = res.index + res[1].length;
|
||||
}
|
||||
}
|
||||
if (trimLocation == -1) {
|
||||
return data;
|
||||
if(trimLocation === -1) {
|
||||
const res = data.match(RE_TEXTELEMENTS_FALLBACK_2);
|
||||
if(res && Boolean(res[1])) {
|
||||
trimLocation = res.index + res[1].length;
|
||||
}
|
||||
}
|
||||
if (trimLocation === -1) {
|
||||
trimLocation = data.search(RE_DRAWING);
|
||||
}
|
||||
if (trimLocation === -1) {
|
||||
return data.endsWith("\n") ? data : (data + "\n");
|
||||
}
|
||||
|
||||
let header = updateFrontmatterInString(data.substring(0, trimLocation),keys);
|
||||
@@ -253,7 +278,7 @@ export const getExcalidrawMarkdownHeaderSection = (data:string, keys:[string,str
|
||||
header = header.replace(REG_IMG, "$1");
|
||||
}
|
||||
//end of remove
|
||||
return header;
|
||||
return header.endsWith("\n") ? header : (header + "\n");
|
||||
}
|
||||
|
||||
|
||||
@@ -278,6 +303,7 @@ export class ExcalidrawData {
|
||||
private equations: Map<FileId, { latex: string; isLoaded: boolean }> = null; //fileId, path
|
||||
private mermaids: Map<FileId, { mermaid: string; isLoaded: boolean }> = null; //fileId, path
|
||||
private compatibilityMode: boolean = false;
|
||||
private textElementCommentedOut: boolean = false;
|
||||
selectedElementIds: {[key:string]:boolean} = {}; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/609
|
||||
|
||||
constructor(
|
||||
@@ -557,22 +583,37 @@ export class ExcalidrawData {
|
||||
//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(/(^%%\n)?# Text Elements\n/m);
|
||||
let position = data.search(RE_TEXTELEMENTS);
|
||||
if (position === -1) {
|
||||
await this.setTextMode(textMode, false);
|
||||
this.loaded = true;
|
||||
return true; //Text Elements header does not exist
|
||||
}
|
||||
position += data.match(/((^%%\n)?# Text Elements\n)/m)[0].length;
|
||||
|
||||
data = data.substring(position);
|
||||
const textElementsMatch = data.match(new RegExp(`^((%%\n)?${MD_TEXTELEMENTS}(?:\n|$))`, "m"))[0]
|
||||
position += textElementsMatch.length;
|
||||
|
||||
data = data.slice(position);
|
||||
this.textElementCommentedOut = textElementsMatch.startsWith("%%\n");
|
||||
position = 0;
|
||||
let parts;
|
||||
|
||||
//load element links
|
||||
const elementLinkMap = new Map<string,string>();
|
||||
const elementLinksData = data.substring(
|
||||
data.indexOf(`${MD_ELEMENTLINKS}\n`) + `${MD_ELEMENTLINKS}\n`.length,
|
||||
);
|
||||
//Load Embedded files
|
||||
const RE_ELEMENT_LINKS = /^(.{8}):\s*(\[\[[^\]]*]])$/gm;
|
||||
const linksRes = elementLinksData.matchAll(RE_ELEMENT_LINKS);
|
||||
while (!(parts = linksRes.next()).done) {
|
||||
elementLinkMap.set(parts.value[1], parts.value[2]);
|
||||
}
|
||||
|
||||
//iterating through all the text elements in .md
|
||||
//Text elements always contain the raw value
|
||||
const BLOCKREF_LEN: number = " ^12345678\n\n".length;
|
||||
const RE_TEXT_ELEMENT_LINK = /^%%\*\*\*>>>text element-link:(\[\[[^<*\]]*]])<<<\*\*\*%%/gm;
|
||||
let res = data.matchAll(/\s\^(.{8})[\n]+/g);
|
||||
let parts;
|
||||
while (!(parts = res.next()).done) {
|
||||
let text = data.substring(position, parts.value.index);
|
||||
const id: string = parts.value[1];
|
||||
@@ -580,6 +621,7 @@ export class ExcalidrawData {
|
||||
if (textEl) {
|
||||
if (textEl.type !== "text") {
|
||||
//markdown link attached to elements
|
||||
//legacy fileformat support as of 2.0.26
|
||||
if (textEl.link !== text) {
|
||||
textEl.link = text;
|
||||
textEl.version++;
|
||||
@@ -589,12 +631,16 @@ export class ExcalidrawData {
|
||||
} else {
|
||||
const wrapAt = estimateMaxLineLen(textEl.text, textEl.originalText);
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/566
|
||||
const elementLinkRes = text.matchAll(/^%%\*\*\*>>>text element-link:(\[\[[^<*\]]*]])<<<\*\*\*%%/gm);
|
||||
const elementLinkRes = text.matchAll(RE_TEXT_ELEMENT_LINK);
|
||||
const elementLink = elementLinkRes.next();
|
||||
if(!elementLink.done) {
|
||||
text = text.replace(/^%%\*\*\*>>>text element-link:\[\[[^<*\]]*]]<<<\*\*\*%%/gm,"");
|
||||
text = text.replace(RE_TEXT_ELEMENT_LINK,"");
|
||||
textEl.link = elementLink.value[1];
|
||||
}
|
||||
}
|
||||
if(elementLinkMap.has(id)) {
|
||||
textEl.link = elementLinkMap.get(id);
|
||||
elementLinkMap.delete(id);
|
||||
}
|
||||
const parseRes = await this.parse(text);
|
||||
textEl.rawText = text;
|
||||
this.textElements.set(id, {
|
||||
@@ -614,54 +660,68 @@ export class ExcalidrawData {
|
||||
position = parts.value.index + BLOCKREF_LEN;
|
||||
}
|
||||
|
||||
data = data.substring(
|
||||
data.indexOf("# Embedded files\n") + "# Embedded files\n".length,
|
||||
);
|
||||
//Load Embedded files
|
||||
const REG_FILEID_FILEPATH = /([\w\d]*):\s*\[\[([^\]]*)]]\s?(\{[^}]*})?\n/gm;
|
||||
res = data.matchAll(REG_FILEID_FILEPATH);
|
||||
while (!(parts = res.next()).done) {
|
||||
const embeddedFile = new EmbeddedFile(
|
||||
this.plugin,
|
||||
this.file.path,
|
||||
parts.value[2],
|
||||
parts.value[3],
|
||||
//In theory only non-text elements should be left in the elementLinkMap
|
||||
//new file format from 2.0.26
|
||||
for (const [id, link] of elementLinkMap) {
|
||||
const textEl = this.scene.elements.filter((el: any) => el.id === id)[0];
|
||||
if (textEl) {
|
||||
textEl.link = link;
|
||||
textEl.version++;
|
||||
textEl.versionNonce++;
|
||||
this.elementLinks.set(id, link);
|
||||
}
|
||||
}
|
||||
|
||||
const indexOfEmbeddedFiles = data.indexOf("# Embedded files\n");
|
||||
if(indexOfEmbeddedFiles>-1) {
|
||||
data = data.substring(
|
||||
indexOfEmbeddedFiles + "# Embedded files\n".length,
|
||||
);
|
||||
this.setFile(parts.value[1] as FileId, embeddedFile);
|
||||
}
|
||||
//Load Embedded files
|
||||
const REG_FILEID_FILEPATH = /([\w\d]*):\s*\[\[([^\]]*)]]\s?(\{[^}]*})?\n/gm;
|
||||
res = data.matchAll(REG_FILEID_FILEPATH);
|
||||
while (!(parts = res.next()).done) {
|
||||
const embeddedFile = new EmbeddedFile(
|
||||
this.plugin,
|
||||
this.file.path,
|
||||
parts.value[2],
|
||||
parts.value[3],
|
||||
);
|
||||
this.setFile(parts.value[1] as FileId, embeddedFile);
|
||||
}
|
||||
|
||||
//Load links
|
||||
const REG_LINKID_FILEPATH = /([\w\d]*):\s*((?:https?|file|ftps?):\/\/[^\s]*)\n/gm;
|
||||
res = data.matchAll(REG_LINKID_FILEPATH);
|
||||
while (!(parts = res.next()).done) {
|
||||
const embeddedFile = new EmbeddedFile(
|
||||
this.plugin,
|
||||
null,
|
||||
parts.value[2],
|
||||
);
|
||||
this.setFile(parts.value[1] as FileId, embeddedFile);
|
||||
}
|
||||
//Load links
|
||||
const REG_LINKID_FILEPATH = /([\w\d]*):\s*((?:https?|file|ftps?):\/\/[^\s]*)\n/gm;
|
||||
res = data.matchAll(REG_LINKID_FILEPATH);
|
||||
while (!(parts = res.next()).done) {
|
||||
const embeddedFile = new EmbeddedFile(
|
||||
this.plugin,
|
||||
null,
|
||||
parts.value[2],
|
||||
);
|
||||
this.setFile(parts.value[1] as FileId, embeddedFile);
|
||||
}
|
||||
|
||||
//Load Equations
|
||||
const REG_FILEID_EQUATION = /([\w\d]*):\s*\$\$([\s\S]*?)(\$\$\s*\n)/gm;
|
||||
res = data.matchAll(REG_FILEID_EQUATION);
|
||||
while (!(parts = res.next()).done) {
|
||||
this.setEquation(parts.value[1] as FileId, {
|
||||
latex: parts.value[2],
|
||||
isLoaded: false,
|
||||
});
|
||||
}
|
||||
//Load Equations
|
||||
const REG_FILEID_EQUATION = /([\w\d]*):\s*\$\$([\s\S]*?)(\$\$\s*\n)/gm;
|
||||
res = data.matchAll(REG_FILEID_EQUATION);
|
||||
while (!(parts = res.next()).done) {
|
||||
this.setEquation(parts.value[1] as FileId, {
|
||||
latex: parts.value[2],
|
||||
isLoaded: false,
|
||||
});
|
||||
}
|
||||
|
||||
//Load Mermaids
|
||||
const mermaidElements = getMermaidImageElements(this.scene.elements);
|
||||
if(mermaidElements.length>0 && !shouldRenderMermaid()) {
|
||||
new Notice ("Mermaid images are only supported in Obsidian 1.4.14 and above. Please update Obsidian to see the mermaid images in this drawing. Obsidian mobile 1.4.14 currently only avaiable to Obsidian insiders", 5000);
|
||||
} else {
|
||||
mermaidElements.forEach(el =>
|
||||
this.setMermaid(el.fileId, {mermaid: getMermaidText(el), isLoaded: false})
|
||||
);
|
||||
//Load Mermaids
|
||||
const mermaidElements = getMermaidImageElements(this.scene.elements);
|
||||
if(mermaidElements.length>0 && !shouldRenderMermaid()) {
|
||||
new Notice ("Mermaid images are only supported in Obsidian 1.4.14 and above. Please update Obsidian to see the mermaid images in this drawing. Obsidian mobile 1.4.14 currently only avaiable to Obsidian insiders", 5000);
|
||||
} else {
|
||||
mermaidElements.forEach(el =>
|
||||
this.setMermaid(el.fileId, {mermaid: getMermaidText(el), isLoaded: false})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//Check to see if there are text elements in the JSON that were missed from the # Text Elements section
|
||||
//e.g. if the entire text elements section was deleted.
|
||||
this.findNewTextElementsInScene();
|
||||
@@ -1130,27 +1190,37 @@ export class ExcalidrawData {
|
||||
*/
|
||||
disableCompression: boolean = false;
|
||||
generateMD(deletedElements: ExcalidrawElement[] = []): string {
|
||||
let outString = "# Text Elements\n";
|
||||
let outString = this.textElementCommentedOut ? "%%\n" : "";
|
||||
outString += `${MD_TEXTELEMENTS}\n`;
|
||||
const textElementLinks = new Map<string, string>();
|
||||
for (const key of this.textElements.keys()) {
|
||||
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/566
|
||||
const element = this.scene.elements.filter((el:any)=>el.id===key);
|
||||
let elementString = this.textElements.get(key).raw;
|
||||
if(element && element.length===1 && element[0].link && element[0].rawText === element[0].originalText) {
|
||||
if(element[0].link.match(/^\[\[[^\]]*]]$/g)) { //apply this only to markdown links
|
||||
elementString = `%%***>>>text element-link:${element[0].link}<<<***%%` + elementString;
|
||||
textElementLinks.set(key, element[0].link);
|
||||
//elementString = `%%***>>>text element-link:${element[0].link}<<<***%%` + elementString;
|
||||
}
|
||||
}
|
||||
outString += `${elementString} ^${key}\n\n`;
|
||||
}
|
||||
|
||||
for (const key of this.elementLinks.keys()) {
|
||||
outString += `${this.elementLinks.get(key)} ^${key}\n\n`;
|
||||
if (this.elementLinks.size > 0 || textElementLinks.size > 0) {
|
||||
outString += `${MD_ELEMENTLINKS}\n`;
|
||||
for (const key of this.elementLinks.keys()) {
|
||||
outString += `${key}: ${this.elementLinks.get(key)}\n`;
|
||||
}
|
||||
for (const key of textElementLinks.keys()) {
|
||||
outString += `${key}: ${textElementLinks.get(key)}\n`;
|
||||
}
|
||||
outString += "\n";
|
||||
}
|
||||
|
||||
// deliberately not adding mermaids to here. It is enough to have the mermaidText in the image element's customData
|
||||
outString +=
|
||||
this.equations.size > 0 || this.files.size > 0
|
||||
? "\n# Embedded files\n"
|
||||
? "# Embedded files\n"
|
||||
: "";
|
||||
if (this.equations.size > 0) {
|
||||
for (const key of this.equations.keys()) {
|
||||
@@ -1185,6 +1255,7 @@ export class ExcalidrawData {
|
||||
}, null, "\t");
|
||||
return (
|
||||
outString +
|
||||
(this.textElementCommentedOut ? "" : "%%\n") +
|
||||
getMarkdownDrawingSection(
|
||||
sceneJSONstring,
|
||||
this.disableCompression ? false : this.plugin.settings.compress,
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
MAX_IMAGE_SIZE,
|
||||
fileid,
|
||||
sceneCoordsToViewportCoords,
|
||||
MD_EX_SECTIONS,
|
||||
} from "./constants/constants";
|
||||
import ExcalidrawPlugin from "./main";
|
||||
import {
|
||||
@@ -101,7 +102,7 @@ import {
|
||||
fragWithHTML,
|
||||
isMaskFile,
|
||||
} from "./utils/Utils";
|
||||
import { getLeaf, getParentOfClass, obsidianPDFQuoteWithRef, openLeaf } from "./utils/ObsidianUtils";
|
||||
import { 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";
|
||||
@@ -124,7 +125,7 @@ import { anyModifierKeysPressed, emulateKeysForLinkClick, webbrowserDragModifier
|
||||
import { setDynamicStyle } from "./utils/DynamicStyling";
|
||||
import { InsertPDFModal } from "./dialogs/InsertPDFModal";
|
||||
import { CustomEmbeddable, renderWebView } from "./customEmbeddable";
|
||||
import { getExcalidrawFileForwardLinks, getLinkTextFromLink, insertEmbeddableToView, insertImageToView, openExternalLink, openTagSearch } from "./utils/ExcalidrawViewUtils";
|
||||
import { getExcalidrawFileForwardLinks, getFrameBasedOnFrameNameOrId, getLinkTextFromLink, insertEmbeddableToView, insertImageToView, openExternalLink, openTagSearch } from "./utils/ExcalidrawViewUtils";
|
||||
import { imageCache } from "./utils/ImageCache";
|
||||
import { CanvasNodeFactory, ObsidianCanvasNode } from "./utils/CanvasNodeFactory";
|
||||
import { EmbeddableMenu } from "./menu/EmbeddableActionsMenu";
|
||||
@@ -135,6 +136,7 @@ import { nanoid } from "nanoid";
|
||||
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";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
|
||||
@@ -263,6 +265,8 @@ export default class ExcalidrawView extends TextFileView {
|
||||
// private scrollYBeforeKeyboard: number = null;
|
||||
|
||||
public semaphores: {
|
||||
//flag to prevent overwriting the changes the user makes in an embeddable view editing the back side of the drawing
|
||||
embeddableIsEditingSelf: boolean;
|
||||
popoutUnload: boolean; //the unloaded Excalidraw view was the last leaf in the popout window
|
||||
viewunload: boolean;
|
||||
//first time initialization of the view
|
||||
@@ -297,6 +301,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
hoverSleep: boolean; //flag with timer to prevent hover preview from being triggered dozens of times
|
||||
wheelTimeout:NodeJS.Timeout; //used to avoid hover preview while zooming
|
||||
} = {
|
||||
embeddableIsEditingSelf: false,
|
||||
popoutUnload: false,
|
||||
viewunload: false,
|
||||
scriptsReady: false,
|
||||
@@ -598,10 +603,48 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
|
||||
private preventReloadResetTimer: NodeJS.Timeout = null;
|
||||
|
||||
public setPreventReload() {
|
||||
this.semaphores.preventReload = true;
|
||||
const self = this;
|
||||
this.preventReloadResetTimer = setTimeout(()=>self.semaphores.preventReload = false,2000);
|
||||
}
|
||||
|
||||
public clearPreventReloadTimer() {
|
||||
if(this.preventReloadResetTimer) {
|
||||
clearTimeout(this.preventReloadResetTimer);
|
||||
this.preventReloadResetTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private editingSelfResetTimer: NodeJS.Timeout = null;
|
||||
public async setEmbeddableIsEditingSelf() {
|
||||
this.clearEmbeddableIsEditingSelfTimer();
|
||||
await this.forceSave(true);
|
||||
this.semaphores.embeddableIsEditingSelf = true;
|
||||
}
|
||||
|
||||
public clearEmbeddableIsEditingSelfTimer () {
|
||||
if(this.editingSelfResetTimer) {
|
||||
clearTimeout(this.editingSelfResetTimer);
|
||||
this.editingSelfResetTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public clearEmbeddableIsEditingSelf() {
|
||||
const self = this;
|
||||
this.clearEmbeddableIsEditingSelfTimer();
|
||||
this.editingSelfResetTimer = setTimeout(()=>self.semaphores.embeddableIsEditingSelf = false,2000);
|
||||
}
|
||||
|
||||
async save(preventReload: boolean = true, forcesave: boolean = false) {
|
||||
if(!this.isLoaded) {
|
||||
return;
|
||||
}
|
||||
if (this.semaphores.embeddableIsEditingSelf) {
|
||||
return;
|
||||
}
|
||||
//console.log("saving - embeddable not editing")
|
||||
//debug({where:"save", preventReload, forcesave, semaphores:this.semaphores});
|
||||
if (this.semaphores.saving) {
|
||||
return;
|
||||
@@ -648,10 +691,8 @@ export default class ExcalidrawView extends TextFileView {
|
||||
//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.
|
||||
if(this.preventReloadResetTimer) {
|
||||
clearTimeout(this.preventReloadResetTimer);
|
||||
this.preventReloadResetTimer = null;
|
||||
}
|
||||
|
||||
this.clearPreventReloadTimer();
|
||||
|
||||
this.semaphores.preventReload = preventReload;
|
||||
await super.save();
|
||||
@@ -669,8 +710,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
//there were odd cases when preventReload semaphore did not get cleared and consequently a synchronized image
|
||||
//did not update the open drawing
|
||||
if(preventReload) {
|
||||
const self = this;
|
||||
this.preventReloadResetTimer = setTimeout(()=>self.semaphores.preventReload = false,2000);
|
||||
this.setPreventReload();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -729,13 +769,14 @@ export default class ExcalidrawView extends TextFileView {
|
||||
//deleted elements are only used if sync modifies files while Excalidraw is open
|
||||
//otherwise deleted elements are discarded when loading the scene
|
||||
if (!this.compatibilityMode) {
|
||||
|
||||
const keys:[string,string][] = this.exportDialog?.dirty && this.exportDialog?.saveSettings
|
||||
? [
|
||||
[FRONTMATTER_KEYS["export-padding"].name, this.exportDialog.padding.toString()],
|
||||
[FRONTMATTER_KEYS["export-pngscale"].name, this.exportDialog.scale.toString()],
|
||||
[FRONTMATTER_KEYS["export-dark"].name, this.exportDialog.theme === "dark" ? "true" : "false"],
|
||||
[FRONTMATTER_KEYS["export-transparent"].name, this.exportDialog.transparent ? "true" : "false"],
|
||||
[FRONTMATTER_KEYS["plugin"].name, this.textMode === TextMode.raw ? "raw" : "parsed"]
|
||||
[FRONTMATTER_KEYS["plugin"].name, this.textMode === TextMode.raw ? "raw" : "parsed"],
|
||||
]
|
||||
: [
|
||||
[FRONTMATTER_KEYS["plugin"].name, this.textMode === TextMode.raw ? "raw" : "parsed"]
|
||||
@@ -1411,9 +1452,11 @@ export default class ExcalidrawView extends TextFileView {
|
||||
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
|
||||
) {
|
||||
//console.log("autosave");
|
||||
this.autosaveTimer = null;
|
||||
if (this.excalidrawAPI) {
|
||||
this.semaphores.autosaving = true;
|
||||
@@ -1486,7 +1529,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
|
||||
/**
|
||||
* reload is triggered by the modifyEventHandler in main.ts when ever an excalidraw drawing that is currently open
|
||||
* reload is triggered by the modifyEventHandler in main.ts whenever an excalidraw drawing that is currently open
|
||||
* in a workspace leaf is modified. There can be two reasons for the file change:
|
||||
* - The user saves the drawing in the active view (either force-save or autosave)
|
||||
* - The file is modified by some other process, typically as a result of background sync, or because the drawing is open
|
||||
@@ -1496,6 +1539,26 @@ export default class ExcalidrawView extends TextFileView {
|
||||
* @returns
|
||||
*/
|
||||
public async reload(fullreload: boolean = false, file?: TFile) {
|
||||
const loadOnModifyTrigger = file && file === this.file;
|
||||
|
||||
//once you've finished editing the embeddable, the first time the file
|
||||
//reloads will be because of the embeddable changed the file,
|
||||
//there is a 2000 ms time window allowed for this, but typically this will
|
||||
//happen within 100 ms. When this happens the timer is cleared and the
|
||||
//next time reload triggers the file will be reloaded as normal.
|
||||
if (this.semaphores.embeddableIsEditingSelf) {
|
||||
//console.log("reload - embeddable is editing")
|
||||
if(this.editingSelfResetTimer) {
|
||||
this.clearEmbeddableIsEditingSelfTimer();
|
||||
this.semaphores.embeddableIsEditingSelf = false;
|
||||
}
|
||||
if(loadOnModifyTrigger) {
|
||||
this.data = await this.app.vault.read(this.file);
|
||||
}
|
||||
return;
|
||||
}
|
||||
//console.log("reload - embeddable is not editing")
|
||||
|
||||
if (this.semaphores.preventReload) {
|
||||
this.semaphores.preventReload = false;
|
||||
return;
|
||||
@@ -1511,9 +1574,9 @@ export default class ExcalidrawView extends TextFileView {
|
||||
if (!this.file || !api) {
|
||||
return;
|
||||
}
|
||||
const loadOnModifyTrigger = file && file === this.file;
|
||||
|
||||
if (loadOnModifyTrigger) {
|
||||
this.data = await app.vault.read(file);
|
||||
this.data = await this.app.vault.read(file);
|
||||
this.preventAutozoom();
|
||||
}
|
||||
if (fullreload) {
|
||||
@@ -1536,7 +1599,14 @@ export default class ExcalidrawView extends TextFileView {
|
||||
const sceneElements = api.getSceneElements();
|
||||
|
||||
let elements = sceneElements.filter((el: ExcalidrawElement) => el.id === id);
|
||||
if(elements.length === 0) return;
|
||||
if(elements.length === 0) {
|
||||
const frame = getFrameBasedOnFrameNameOrId(id, sceneElements);
|
||||
if (frame) {
|
||||
elements = [frame];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(hasGroupref) {
|
||||
const groupElements = this.plugin.ea.getElementsInTheSameGroupWithElement(elements[0],sceneElements)
|
||||
if(groupElements.length>0) {
|
||||
@@ -1865,6 +1935,10 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
|
||||
public async synchronizeWithData(inData: ExcalidrawData) {
|
||||
if(this.semaphores.embeddableIsEditingSelf) {
|
||||
return;
|
||||
}
|
||||
//console.log("synchronizeWithData - embeddable is not editing");
|
||||
//check if saving, wait until not
|
||||
let counter = 0;
|
||||
while(this.semaphores.saving && counter++<30) {
|
||||
@@ -3957,6 +4031,16 @@ export default class ExcalidrawView extends TextFileView {
|
||||
return {imageEl: el, embeddedFile: imageFile};
|
||||
}
|
||||
|
||||
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 selectCardDialog = new SelectCard(this.app,this,sections);
|
||||
selectCardDialog.start();
|
||||
}
|
||||
|
||||
public async convertImageElWithURLToLocalFile(data: {imageEl: ExcalidrawImageElement, embeddedFile: EmbeddedFile}) {
|
||||
const {imageEl, embeddedFile} = data;
|
||||
const imageDataURL = embeddedFile.getImage(false);
|
||||
@@ -4174,6 +4258,16 @@ export default class ExcalidrawView extends TextFileView {
|
||||
]);
|
||||
}
|
||||
|
||||
contextMenuActions.push([
|
||||
renderContextMenuAction(
|
||||
t("INSERT_CARD"),
|
||||
() => {
|
||||
this.insertBackOfTheNoteCard();
|
||||
},
|
||||
onClose
|
||||
),
|
||||
]);
|
||||
|
||||
contextMenuActions.push([
|
||||
renderContextMenuAction(
|
||||
t("UNIVERSAL_ADD_FILE"),
|
||||
@@ -4514,7 +4608,7 @@ export default class ExcalidrawView extends TextFileView {
|
||||
const delta = editingElViewY - scrollViewY;
|
||||
const isElementAboveKeyboard = height > (delta + appToolHeight*2)
|
||||
const excalidrawWrapper = this.excalidrawWrapperRef.current;
|
||||
console.log({isElementAboveKeyboard});
|
||||
//console.log({isElementAboveKeyboard});
|
||||
if(excalidrawWrapper && !isElementAboveKeyboard) {
|
||||
excalidrawWrapper.style.top = `${-(st.height - height)}px`;
|
||||
excalidrawWrapper.style.height = `${st.height}px`;
|
||||
@@ -5024,15 +5118,15 @@ export default class ExcalidrawView extends TextFileView {
|
||||
}
|
||||
}
|
||||
|
||||
public getEmbeddableLeafElementById(id: string): {leaf: WorkspaceLeaf; node?: ObsidianCanvasNode} | null {
|
||||
public getEmbeddableLeafElementById(id: string): {leaf: WorkspaceLeaf; node?: ObsidianCanvasNode; editNode?: Function} | null {
|
||||
const ref = this.embeddableLeafRefs.get(id);
|
||||
if(!ref) {
|
||||
return null;
|
||||
}
|
||||
return ref as {leaf: WorkspaceLeaf; node?: ObsidianCanvasNode};
|
||||
return ref as {leaf: WorkspaceLeaf; node?: ObsidianCanvasNode; editNode?: Function};
|
||||
}
|
||||
|
||||
getActiveEmbeddable = ():{leaf: WorkspaceLeaf; node?: ObsidianCanvasNode}|null => {
|
||||
getActiveEmbeddable = ():{leaf: WorkspaceLeaf; node?: ObsidianCanvasNode; editNode?: Function}|null => {
|
||||
if(!this.excalidrawAPI) return null;
|
||||
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
const st = api.getAppState();
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
TFile,
|
||||
Vault,
|
||||
} from "obsidian";
|
||||
import { RERENDER_EVENT } from "./constants/constants";
|
||||
import { DEVICE, RERENDER_EVENT } from "./constants/constants";
|
||||
import { EmbeddedFilesLoader } from "./EmbeddedFileLoader";
|
||||
import { createPNG, createSVG } from "./ExcalidrawAutomate";
|
||||
import { ExportSettings } from "./ExcalidrawView";
|
||||
@@ -417,7 +417,10 @@ const createImgElement = async (
|
||||
});
|
||||
eventElement.addEventListener("pointerdown",(ev)=>{
|
||||
if(imgOrDiv?.parentElement?.hasClass("canvas-node-content")) return;
|
||||
timer = setTimeout(()=>clickEvent(ev),500);
|
||||
//@ts-ignore
|
||||
const PLUGIN = app.plugins.plugins["obsidian-excalidraw-plugin"] as ExcalidrawPlugin;
|
||||
const timeoutValue = DEVICE.isDesktop ? PLUGIN.settings.longPressDesktop : PLUGIN.settings.longPressMobile;
|
||||
timer = setTimeout(()=>clickEvent(ev),timeoutValue);
|
||||
pointerDownEvent = ev;
|
||||
});
|
||||
eventElement.addEventListener("pointerup",()=>{
|
||||
|
||||
@@ -4,6 +4,13 @@ import { ExcalidrawLib } from "../ExcalidrawLib";
|
||||
import { moment } from "obsidian";
|
||||
//This is only for backward compatibility because an early version of obsidian included an encoding to avoid fantom links from littering Obsidian graph view
|
||||
declare const PLUGIN_VERSION:string;
|
||||
export const MD_TEXTELEMENTS = "# Text Elements";
|
||||
export const MD_JSON_START = "```json\n";
|
||||
export const MD_JSON_END = "```";
|
||||
export const MD_DRAWING = "# Drawing";
|
||||
export const MD_ELEMENTLINKS = "# Element Links";
|
||||
export const MD_EMBEDFILES = "# Embed files";
|
||||
export const MD_EX_SECTIONS = [MD_TEXTELEMENTS, MD_DRAWING, MD_ELEMENTLINKS, MD_EMBEDFILES];
|
||||
|
||||
export const ERROR_IFRAME_CONVERSION_CANCELED = "iframe conversion canceled";
|
||||
|
||||
@@ -162,8 +169,9 @@ export const FRONTMATTER_KEYS:{[key:string]: {name: string, type: string, depric
|
||||
"font-color": {name: "excalidraw-font-color", type: "text"},
|
||||
"border-color": {name: "excalidraw-border-color", type: "text"},
|
||||
"md-css": {name: "excalidraw-css", type: "text"},
|
||||
"autoexport": {name: "excalidraw-autoexport", type: "checkbox"},
|
||||
"iframe-theme": {name: "excalidraw-iframe-theme", type: "text"},
|
||||
"autoexport": {name: "excalidraw-autoexport", type: "text"},
|
||||
"iframe-theme": {name: "excalidraw-iframe-theme", type: "text"},
|
||||
"open-as-markdown": {name: "excalidraw-open-md", type: "checkbox"},
|
||||
};
|
||||
|
||||
export const EMBEDDABLE_THEME_FRONTMATTER_VALUES = ["light", "dark", "auto", "dafault"];
|
||||
|
||||
@@ -89,7 +89,7 @@ function RenderObsidianView(
|
||||
const react = view.plugin.getPackage(view.ownerWindow).react;
|
||||
|
||||
//@ts-ignore
|
||||
const leafRef = react.useRef<{leaf: WorkspaceLeaf; node?: ObsidianCanvasNode} | null>(null);
|
||||
const leafRef = react.useRef<{leaf: WorkspaceLeaf; node?: ObsidianCanvasNode, editNode?: Function} | null>(null);
|
||||
const isEditingRef = react.useRef(false);
|
||||
const isActiveRef = react.useRef(false);
|
||||
const themeRef = react.useRef(theme);
|
||||
@@ -154,7 +154,7 @@ function RenderObsidianView(
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
//mount the workspace leaf or the canvas node depending on subpath
|
||||
//Mount the workspace leaf or the canvas node depending on subpath
|
||||
//--------------------------------------------------------------------------------
|
||||
react.useEffect(() => {
|
||||
if(!containerRef?.current) {
|
||||
@@ -176,7 +176,8 @@ function RenderObsidianView(
|
||||
rootSplit.containerEl.style.borderRadius = "var(--embeddable-radius)";
|
||||
leafRef.current = {
|
||||
leaf: app.workspace.createLeafInParent(rootSplit, 0),
|
||||
node: null
|
||||
node: null,
|
||||
editNode: null,
|
||||
};
|
||||
|
||||
const setKeepOnTop = () => {
|
||||
@@ -230,6 +231,9 @@ function RenderObsidianView(
|
||||
return () => {}; //cleanup on unmount
|
||||
}, [linkText, subpath, containerRef]);
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
//Set colors of the canvas node
|
||||
//--------------------------------------------------------------------------------
|
||||
const setColors = (canvasNode: HTMLDivElement, element: NonDeletedExcalidrawElement, mdProps: EmbeddableMDCustomProps, canvasColor: string) => {
|
||||
if(!mdProps) return;
|
||||
if (!leafRef.current?.hasOwnProperty("node")) return;
|
||||
@@ -289,6 +293,9 @@ function RenderObsidianView(
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
//Set colors of the canvas node
|
||||
//--------------------------------------------------------------------------------
|
||||
react.useEffect(() => {
|
||||
if(!containerRef.current) {
|
||||
return;
|
||||
@@ -304,6 +311,9 @@ function RenderObsidianView(
|
||||
canvasColor,
|
||||
])
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
//Switch to preview mode when the iframe is not active
|
||||
//--------------------------------------------------------------------------------
|
||||
react.useEffect(() => {
|
||||
if(isEditingRef.current) {
|
||||
if(leafRef.current?.node) {
|
||||
@@ -318,9 +328,9 @@ function RenderObsidianView(
|
||||
//--------------------------------------------------------------------------------
|
||||
//Switch to edit mode when markdown view is clicked
|
||||
//--------------------------------------------------------------------------------
|
||||
const handleClick = react.useCallback((event: React.PointerEvent<HTMLElement>) => {
|
||||
const handleClick = react.useCallback((event?: React.PointerEvent<HTMLElement>) => {
|
||||
if(isActiveRef.current) {
|
||||
event.stopPropagation();
|
||||
event?.stopPropagation();
|
||||
}
|
||||
|
||||
if (isActiveRef.current && !isEditingRef.current && leafRef.current?.leaf) {
|
||||
@@ -349,6 +359,22 @@ function RenderObsidianView(
|
||||
}
|
||||
}, [leafRef.current?.leaf, element.id, view, themeRef.current]);
|
||||
|
||||
if(leafRef.current) leafRef.current.editNode = handleClick;
|
||||
// Event listener for key press
|
||||
react.useEffect(() => {
|
||||
const handleKeyPress = (event: KeyboardEvent) => {
|
||||
if (event.key === "Enter") {
|
||||
handleClick(event); // Call handleClick function when Enter key is pressed
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyPress); // Add event listener for key press
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyPress); // Remove event listener when component unmounts
|
||||
};
|
||||
}, [handleClick]);
|
||||
|
||||
//--------------------------------------------------------------------------------
|
||||
// Set isActiveRef and switch to preview mode when the iframe is not active
|
||||
//--------------------------------------------------------------------------------
|
||||
@@ -404,7 +430,6 @@ export const CustomEmbeddable: React.FC<{element: NonDeletedExcalidrawElement; v
|
||||
const containerRef: React.RefObject<HTMLDivElement> = react.useRef(null);
|
||||
const theme = getTheme(view, appState.theme);
|
||||
const mdProps: EmbeddableMDCustomProps = element.customData?.mdProps || null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
|
||||
@@ -49,7 +49,7 @@ export class EmbeddableSettings extends Modal {
|
||||
this.zoomValue = element.scale[0];
|
||||
this.isYouTube = isYouTube(this.element.link);
|
||||
this.notExcalidrawIsInternal = this.file && !this.view.plugin.isExcalidrawFile(this.file)
|
||||
this.isMDFile = this.file && this.file.extension === "md" && !this.view.plugin.isExcalidrawFile(this.file);
|
||||
this.isMDFile = this.file && this.file.extension === "md"; // && !this.view.plugin.isExcalidrawFile(this.file);
|
||||
this.isLocalURI = this.element.link.startsWith("file://");
|
||||
if(isYouTube) this.youtubeStart = getYouTubeStartAt(this.element.link);
|
||||
|
||||
@@ -212,7 +212,12 @@ export class EmbeddableSettings extends Modal {
|
||||
el.scale = [this.zoomValue,this.zoomValue];
|
||||
}
|
||||
if(dirty) {
|
||||
this.ea.addElementsToView();
|
||||
(async() => {
|
||||
await this.ea.addElementsToView();
|
||||
//@ts-ignore
|
||||
this.ea.viewUpdateScene({appState: {}});
|
||||
})();
|
||||
|
||||
}
|
||||
this.close();
|
||||
};
|
||||
|
||||
@@ -17,6 +17,41 @@ 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.0.26":`
|
||||
<div class="excalidraw-videoWrapper"><div>
|
||||
<iframe src="https://www.youtube.com/embed/tHUcD4rWIuY" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
</div></div>
|
||||
|
||||
## New
|
||||
- Minor updates from [Excalidraw.com](https://excalidraw.com). The key change is text measurements that should result in consistent text sizing between desktop and mobile devices.
|
||||
- Now you can embed the markdown section of an Excalidraw note to your drawing. Simply select ${String.fromCharCode(96)}Insert ANY file${String.fromCharCode(96)}, choose the drawing, and select the relevant heading section to embed.
|
||||
- This also works with "back-of-the-drawing" markdown sections. Use the context menu ${String.fromCharCode(96)}Add back-of-note Card${String.fromCharCode(96)}. The action is also available on the Command Palette and in the Excalidraw-Obsidian Tools Panel.
|
||||
- Editing an embedded markdown note is now easier. Just press Enter when the element is selected. [#1650](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1650)
|
||||
- The crosshair cursor is now hidden when the freedraw tool is active and using a pen. [#1659](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1659)
|
||||
- ${String.fromCharCode(96)}Convert markdown note to Excalidraw Drawing${String.fromCharCode(96)} now converts an existing markdown note (not just empty notes) into a drawing. The original markdown note will be on the "back side of the drawing".
|
||||
- Introducing ${String.fromCharCode(96)}Annotate image in Excalidraw${String.fromCharCode(96)}, which works very similarly to ${String.fromCharCode(96)}Crop and mask image${String.fromCharCode(96)}. You can replace an image in a markdown note or on the Obsidian Canvas with an Excalidraw drawing containing that image. You will be able to annotate the image in Excalidraw.
|
||||
- Now you can reference frames in images embedded in markdown and canvas with frame names e.g. ${String.fromCharCode(96)}![[drawing#^frame=Frame 01]]${String.fromCharCode(96)}
|
||||
- Excalidraw file format change:
|
||||
- New frontmatter switch ${String.fromCharCode(96)}excalidraw-open-md${String.fromCharCode(96)}: If set to true, the file by default will open as a markdown file. You can switch to Excalidraw View Mode via the command palette action or by right-clicking the tab.
|
||||
- Easter Egg: If you add a comment in front of ${String.fromCharCode(96)}# Text Elements${String.fromCharCode(96)}, then the entire Excalidraw data: markdown and JSON will be commented out, thus invisible when exporting to the web. If you remove the comment from before ${String.fromCharCode(96)}# Text Elements${String.fromCharCode(96)}, then only the JSON will be commented out.
|
||||
|
||||
Before:
|
||||
${String.fromCharCode(96,96,96)}markdown
|
||||
#1657
|
||||
%%
|
||||
# Text Elements
|
||||
...
|
||||
# Drawing
|
||||
${String.fromCharCode(96,96,96)}
|
||||
|
||||
After:
|
||||
${String.fromCharCode(96,96,96)}markdown
|
||||
# Text Elements
|
||||
....
|
||||
%%
|
||||
# Drawing
|
||||
${String.fromCharCode(96,96,96)}
|
||||
`,
|
||||
"2.0.25":`
|
||||
# New - a small change that opens big opportunities
|
||||
- You can now set a folder as the Excalidraw Template in settings (See under Basic). If a folder is provided, Excalidraw will treat drawings in that folder as templates and will prompt you to select the template to use for new drawings.
|
||||
|
||||
103
src/dialogs/SelectCard.ts
Normal file
103
src/dialogs/SelectCard.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { App, FuzzySuggestModal, Notice, TFile } from "obsidian";
|
||||
import { t } from "../lang/helpers";
|
||||
import ExcalidrawView from "src/ExcalidrawView";
|
||||
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";
|
||||
|
||||
export class SelectCard extends FuzzySuggestModal<string> {
|
||||
|
||||
constructor(
|
||||
public app: App,
|
||||
private view: ExcalidrawView,
|
||||
private sections: string[]
|
||||
) {
|
||||
super(app);
|
||||
this.limit = 20;
|
||||
this.setInstructions([
|
||||
{
|
||||
command: t("TYPE_SECTION"),
|
||||
purpose: "",
|
||||
},
|
||||
]);
|
||||
|
||||
this.inputEl.onkeyup = (e) => {
|
||||
if (e.key == "Enter") {
|
||||
if (this.containerEl.innerText.includes(t("EMPTY_SECTION_MESSAGE"))) {
|
||||
const item = this.inputEl.value;
|
||||
if(MD_EX_SECTIONS.includes(item)) {
|
||||
new Notice(t("INVALID_SECTION_NAME"));
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
(async () => {
|
||||
const data = view.data;
|
||||
const header = getExcalidrawMarkdownHeaderSection(data);
|
||||
view.data = data.replace(header, header + `\n# ${item}\n\n`);
|
||||
await view.forceSave(true);
|
||||
let watchdog = 0;
|
||||
await sleep(200);
|
||||
let found:string;
|
||||
while (watchdog++ < 10 && !(found=(await this.app.metadataCache.blockCache
|
||||
.getForFile({ isCancelled: () => false },view.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))
|
||||
.find((b: any) => b === item))) {
|
||||
await sleep(200);
|
||||
}
|
||||
|
||||
const ea = getEA(this.view) as ExcalidrawAutomate;
|
||||
const id = ea.addEmbeddable(
|
||||
0,0,400,500,
|
||||
`[[${this.view.file.path}#${item}]]`
|
||||
);
|
||||
await ea.addElementsToView(true, false, true);
|
||||
|
||||
const api = view.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
const el = ea.getViewElements().find(el=>el.id === id);
|
||||
api.selectElements([el]);
|
||||
setTimeout(()=>{
|
||||
api.updateScene({appState: {activeEmbeddable: {element: el, state: "active"}}});
|
||||
if(found) view.getEmbeddableLeafElementById(el.id)?.editNode?.();
|
||||
});
|
||||
})();
|
||||
//create new section
|
||||
//`# ${this.inputEl.value}\n\n`;
|
||||
//Do not allow MD_EX_SECTIONS
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getItems(): string[] {
|
||||
return this.sections;
|
||||
}
|
||||
|
||||
getItemText(item: string): string {
|
||||
return item;
|
||||
}
|
||||
|
||||
onChooseItem(item: string): void {
|
||||
const ea = getEA(this.view) as ExcalidrawAutomate;
|
||||
const id = ea.addEmbeddable(
|
||||
0,0,400,500,
|
||||
`[[${this.view.file.path}#${item}]]`
|
||||
);
|
||||
(async () => {
|
||||
await ea.addElementsToView(true, false, true);
|
||||
ea.selectElementsInView([id]);
|
||||
})();
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
this.emptyStateText = t("EMPTY_SECTION_MESSAGE");
|
||||
this.setPlaceholder(t("SELECT_SECTION_OR_TYPE_NEW"));
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
@@ -821,6 +821,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: "open-md",
|
||||
code: null,
|
||||
desc: "If this key is present the file will be opened as a markdown file in the editor",
|
||||
after: ": true",
|
||||
},
|
||||
{
|
||||
field: "autoexport",
|
||||
code: null,
|
||||
|
||||
@@ -3,7 +3,7 @@ import ExcalidrawView from "../ExcalidrawView";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { Modal, Setting, TextComponent } from "obsidian";
|
||||
import { FileSuggestionModal } from "./FolderSuggester";
|
||||
import { IMAGE_TYPES, sceneCoordsToViewportCoords, viewportCoordsToSceneCoords, MAX_IMAGE_SIZE, ANIMATED_IMAGE_TYPES } from "src/constants/constants";
|
||||
import { IMAGE_TYPES, sceneCoordsToViewportCoords, viewportCoordsToSceneCoords, MAX_IMAGE_SIZE, ANIMATED_IMAGE_TYPES, MD_EX_SECTIONS } from "src/constants/constants";
|
||||
import { insertEmbeddableToView, insertImageToView } from "src/utils/ExcalidrawViewUtils";
|
||||
import { getEA } from "src";
|
||||
import { InsertPDFModal } from "./InsertPDFModal";
|
||||
@@ -78,6 +78,7 @@ export class UniversalInsertFileModal extends Modal {
|
||||
|
||||
const updateForm = async () => {
|
||||
const ea = this.plugin.ea;
|
||||
const isSelf = file === this.view.file;
|
||||
const isMarkdown = file && file.extension === "md" && !ea.isExcalidrawFile(file);
|
||||
const isImage = file && (IMAGE_TYPES.contains(file.extension) || ea.isExcalidrawFile(file));
|
||||
const isAnimatedImage = file && ANIMATED_IMAGE_TYPES.contains(file.extension);
|
||||
@@ -85,39 +86,43 @@ export class UniversalInsertFileModal extends Modal {
|
||||
const isPDF = file && file.extension === "pdf";
|
||||
const isExcalidraw = file && ea.isExcalidrawFile(file);
|
||||
|
||||
if (isMarkdown) {
|
||||
const sections = file && file.extension === "md"
|
||||
? (await this.plugin.app.metadataCache.blockCache
|
||||
.getForFile({ isCancelled: () => false },file))
|
||||
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
|
||||
.filter((b: any) => !isExcalidraw || !MD_EX_SECTIONS.includes(b.display))
|
||||
: null;
|
||||
|
||||
if (isMarkdown || (isExcalidraw && sections?.length > 0)) {
|
||||
sectionPickerSetting.settingEl.style.display = "";
|
||||
sectionPicker.selectEl.style.display = "block";
|
||||
while(sectionPicker.selectEl.options.length > 0) {
|
||||
sectionPicker.selectEl.remove(0);
|
||||
}
|
||||
sectionPicker.addOption("","");
|
||||
(await app.metadataCache.blockCache
|
||||
.getForFile({ isCancelled: () => false },file))
|
||||
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
|
||||
.forEach((b: any) => {
|
||||
sectionPicker.addOption(
|
||||
`#${cleanSectionHeading(b.display)}`,
|
||||
b.display)
|
||||
});
|
||||
if(!isExcalidraw) sectionPicker.addOption("","");
|
||||
sections.forEach((b: any) => {
|
||||
sectionPicker.addOption(
|
||||
`#${cleanSectionHeading(b.display)}`,
|
||||
b.display)
|
||||
});
|
||||
} else {
|
||||
sectionPickerSetting.settingEl.style.display = "none";
|
||||
sectionPicker.selectEl.style.display = "none";
|
||||
}
|
||||
|
||||
if (isExcalidraw) {
|
||||
if (isExcalidraw && !isSelf) {
|
||||
sizeToggleSetting.settingEl.style.display = "";
|
||||
} else {
|
||||
sizeToggleSetting.settingEl.style.display = "none";
|
||||
}
|
||||
|
||||
if (isImage || (file?.extension === "md")) {
|
||||
if (!isSelf && (isImage || (file?.extension === "md"))) {
|
||||
actionImage.buttonEl.style.display = "block";
|
||||
} else {
|
||||
actionImage.buttonEl.style.display = "none";
|
||||
}
|
||||
|
||||
if (isIFrame || isAnimatedImage) {
|
||||
if (isIFrame || isAnimatedImage || (isExcalidraw && sections?.length > 0)) {
|
||||
actionIFrame.buttonEl.style.display = "block";
|
||||
} else {
|
||||
actionIFrame.buttonEl.style.display = "none";
|
||||
@@ -131,9 +136,17 @@ export class UniversalInsertFileModal extends Modal {
|
||||
|
||||
}
|
||||
|
||||
const sections = (await this.plugin.app.metadataCache.blockCache
|
||||
.getForFile({ isCancelled: () => false },this.view.file))
|
||||
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
|
||||
.filter((b: any) => !MD_EX_SECTIONS.includes(b.display));
|
||||
|
||||
const search = new TextComponent(ce);
|
||||
search.inputEl.style.width = "100%";
|
||||
const suggester = new FileSuggestionModal(this.app, search,app.vault.getFiles().filter((f: TFile) => f!==this.view.file));
|
||||
const suggester = new FileSuggestionModal(
|
||||
this.app,
|
||||
search,
|
||||
this.app.vault.getFiles().filter((f: TFile) => sections?.length > 0 || f!==this.view.file));
|
||||
search.onChange(() => {
|
||||
file = suggester.getSelectedItem();
|
||||
updateForm();
|
||||
|
||||
@@ -23,7 +23,7 @@ export default {
|
||||
"Script is up to date - Click to reinstall",
|
||||
OPEN_AS_EXCALIDRAW: "Open as Excalidraw Drawing",
|
||||
TOGGLE_MODE: "Toggle between Excalidraw and Markdown mode",
|
||||
CONVERT_NOTE_TO_EXCALIDRAW: "Convert empty note to Excalidraw Drawing",
|
||||
CONVERT_NOTE_TO_EXCALIDRAW: "Convert markdown note to Excalidraw Drawing",
|
||||
CONVERT_EXCALIDRAW: "Convert *.excalidraw to *.md files",
|
||||
CREATE_NEW: "Create new drawing",
|
||||
CONVERT_FILE_KEEP_EXT: "*.excalidraw => *.excalidraw.md",
|
||||
@@ -68,6 +68,7 @@ export default {
|
||||
INSERT_MD: "Insert markdown file from vault",
|
||||
INSERT_PDF: "Insert PDF file from vault",
|
||||
UNIVERSAL_ADD_FILE: "Insert ANY file",
|
||||
INSERT_CARD: "Add back-of-note card",
|
||||
INSERT_LATEX:
|
||||
`Insert LaTeX formula (e.g. \\binom{n}{k} = \\frac{n!}{k!(n-k)!}). ${labelALT()}+CLICK to watch a help video.`,
|
||||
ENTER_LATEX: "Enter a valid LaTeX expression",
|
||||
@@ -76,6 +77,7 @@ export default {
|
||||
TRAY_MODE: "Toggle property-panel tray-mode",
|
||||
SEARCH: "Search for text in drawing",
|
||||
CROP_IMAGE: "Crop and mask image",
|
||||
ANNOTATE_IMAGE : "Annotate image in Excalidraw",
|
||||
INSERT_ACTIVE_PDF_PAGE_AS_IMAGE: "Insert active PDF page as image",
|
||||
RESET_IMG_TO_100: "Set selected image element size to 100% of original",
|
||||
TEMPORARY_DISABLE_AUTOSAVE: "Disable autosave until next time Obsidian starts (only set this if you know what you are doing)",
|
||||
@@ -133,9 +135,16 @@ export default {
|
||||
CROP_PREFIX_DESC:
|
||||
"The first part of the filename for new drawings created when cropping an image. " +
|
||||
"If empty the default 'cropped_' will be used.",
|
||||
ANNOTATE_PREFIX_NAME: "Annotation file prefix",
|
||||
ANNOTATE_PREFIX_DESC:
|
||||
"The first part of the filename for new drawings created when annotating an image. " +
|
||||
"If empty the default 'annotated_' will be used.",
|
||||
CROP_FOLDER_NAME: "Crop file folder",
|
||||
CROP_FOLDER_DESC:
|
||||
"Default location for new drawings created when cropping an image. If empty, drawings will be created following the Vault attachments settings.",
|
||||
ANNOTATE_FOLDER_NAME: "Image annotation file folder",
|
||||
ANNOTATE_FOLDER_DESC:
|
||||
"Default location for new drawings created when annotating an image. If empty, drawings will be created following the Vault attachments settings.",
|
||||
FOLDER_EMBED_NAME:
|
||||
"Use Excalidraw folder when embedding a drawing into the active document",
|
||||
FOLDER_EMBED_DESC:
|
||||
@@ -313,6 +322,11 @@ FILENAME_HEAD: "Filename",
|
||||
"These settings are different for Apple and non-Apple. If you use Obsidian on multiple platforms, you'll need to make the settings separately. "+
|
||||
"The toggles follow the order of " +
|
||||
(DEVICE.isIOS || DEVICE.isMacOS ? "SHIFT, CMD, OPT, CONTROL." : "SHIFT, CTRL, ALT, META (Windows key)."),
|
||||
LONG_PRESS_DESKTOP_NAME: "Long press to open desktop",
|
||||
LONG_PRESS_DESKTOP_DESC: "Long press delay in milliseconds to open an Excalidraw Drawing embedded in a Markdown file. ",
|
||||
LONG_PRESS_MOBILE_NAME: "Long press to open mobile",
|
||||
LONG_PRESS_MOBILE_DESC: "Long press delay in milliseconds to open an Excalidraw Drawing embedded in a Markdown file. ",
|
||||
|
||||
FOCUS_ON_EXISTING_TAB_NAME: "Focus on Existing Tab",
|
||||
FOCUS_ON_EXISTING_TAB_DESC: "When opening a link, Excalidraw will focus on the existing tab if the file is already open. " +
|
||||
"Enabling this setting overrides 'Reuse Adjacent Pane' when the file is already open.",
|
||||
@@ -638,6 +652,13 @@ FILENAME_HEAD: "Filename",
|
||||
PDF_PAGES_HEADER: "Pages to load?",
|
||||
PDF_PAGES_DESC: "Format: 1, 3-5, 7, 9-11",
|
||||
|
||||
//SelectCard.ts
|
||||
TYPE_SECTION: "Type section name to select.",
|
||||
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",
|
||||
|
||||
//EmbeddedFileLoader.ts
|
||||
INFINITE_LOOP_WARNING:
|
||||
"EXCALIDRAW WARNING\nAborted loading embedded images due to infinite loop in file:\n",
|
||||
|
||||
211
src/main.ts
211
src/main.ts
@@ -41,6 +41,7 @@ import {
|
||||
EXPORT_IMG_ICON,
|
||||
LOCALE,
|
||||
IMAGE_TYPES,
|
||||
MD_TEXTELEMENTS,
|
||||
} from "./constants/constants";
|
||||
import {
|
||||
VIRGIL_FONT,
|
||||
@@ -78,6 +79,8 @@ import { t } from "./lang/helpers";
|
||||
import {
|
||||
checkAndCreateFolder,
|
||||
download,
|
||||
fileShouldDefaultAsExcalidraw,
|
||||
getAnnotationFileNameAndFolder,
|
||||
getCropFileNameAndFolder,
|
||||
getDrawingFilename,
|
||||
getEmbedFilename,
|
||||
@@ -99,7 +102,7 @@ import {
|
||||
isCallerFromTemplaterPlugin,
|
||||
decompress,
|
||||
} from "./utils/Utils";
|
||||
import { extractSVGPNGFileName, getActivePDFPageNumberFromPDFView, getAttachmentsFolderAndFilePath, getNewOrAdjacentLeaf, getParentOfClass, isObsidianThemeDark, openLeaf } from "./utils/ObsidianUtils";
|
||||
import { extractSVGPNGFileName, getActivePDFPageNumberFromPDFView, getAttachmentsFolderAndFilePath, getNewOrAdjacentLeaf, getParentOfClass, isObsidianThemeDark, mergeMarkdownFiles, openLeaf } from "./utils/ObsidianUtils";
|
||||
import { ExcalidrawElement, ExcalidrawEmbeddableElement, ExcalidrawImageElement, ExcalidrawTextElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { ScriptEngine } from "./Scripts";
|
||||
import {
|
||||
@@ -383,10 +386,11 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
const self = this;
|
||||
this.app.workspace.onLayoutReady(() => {
|
||||
let leaf: WorkspaceLeaf;
|
||||
for (leaf of app.workspace.getLeavesOfType("markdown")) {
|
||||
for (leaf of this.app.workspace.getLeavesOfType("markdown")) {
|
||||
if (
|
||||
leaf.view instanceof MarkdownView &&
|
||||
self.isExcalidrawFile(leaf.view.file)
|
||||
self.isExcalidrawFile(leaf.view.file) &&
|
||||
fileShouldDefaultAsExcalidraw(leaf.view.file?.path, self.app)
|
||||
) {
|
||||
self.excalidrawFileModes[(leaf as any).id || leaf.view.file.path] =
|
||||
VIEW_TYPE_EXCALIDRAW;
|
||||
@@ -1759,6 +1763,154 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
}
|
||||
})
|
||||
|
||||
this.addCommand({
|
||||
id: "annotate-image",
|
||||
name: t("ANNOTATE_IMAGE"),
|
||||
checkCallback: (checking:boolean) => {
|
||||
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
const canvasView:any = this.app.workspace.activeLeaf?.view;
|
||||
const isCanvas = canvasView && canvasView.getViewType() === "canvas";
|
||||
if(!markdownView && !isCanvas) return false;
|
||||
|
||||
const carveout = async (isFile: boolean, sourceFile: TFile, imageFile: TFile, imageURL: string, replacer: Function, ref?: string) => {
|
||||
const ea = getEA() as ExcalidrawAutomate;
|
||||
const imageID = await ea.addImage(
|
||||
0, 0,
|
||||
isFile
|
||||
? ((isFile && imageFile.extension === "pdf" && ref) ? `${imageFile.path}#${ref}` : imageFile)
|
||||
: imageURL,
|
||||
false, false
|
||||
);
|
||||
if(!imageID) {
|
||||
new Notice(`Can't load image\n\n${imageURL}`);
|
||||
return;
|
||||
}
|
||||
ea.getElement(imageID).locked = true;
|
||||
|
||||
let fnBase = "";
|
||||
let imageLink = "";
|
||||
if(isFile) {
|
||||
fnBase = imageFile.basename;
|
||||
imageLink = ref
|
||||
? `[[${imageFile.path}#${ref}]]`
|
||||
: `[[${imageFile.path}]]`;
|
||||
} else {
|
||||
imageLink = imageURL;
|
||||
const imagename = imageURL.match(/^.*\/([^?]*)\??.*$/)?.[1];
|
||||
fnBase = imagename.substring(0,imagename.lastIndexOf("."));
|
||||
}
|
||||
|
||||
let template:TFile;
|
||||
const templates = getListOfTemplateFiles(this);
|
||||
if(templates) {
|
||||
template = await templatePromt(templates, this.app);
|
||||
}
|
||||
|
||||
const {folderpath, filename} = await getAnnotationFileNameAndFolder(this,sourceFile.path,fnBase)
|
||||
const newPath = await ea.create ({
|
||||
templatePath: template?.path,
|
||||
filename,
|
||||
foldername: folderpath,
|
||||
onNewPane: true,
|
||||
frontmatterKeys: {
|
||||
...this.settings.matchTheme ? {"excalidraw-export-dark": isObsidianThemeDark()} : {},
|
||||
...(imageFile.extension === "pdf") ? {"cssclasses": "excalidraw-cropped-pdfpage"} : {},
|
||||
}
|
||||
});
|
||||
|
||||
//wait for file to be created/indexed by Obsidian
|
||||
let newFile = this.app.vault.getAbstractFileByPath(newPath);
|
||||
let counter = 0;
|
||||
while((!newFile || !this.isExcalidrawFile(newFile as TFile)) && counter < 50) {
|
||||
await sleep(100);
|
||||
newFile = this.app.vault.getAbstractFileByPath(newPath);
|
||||
counter++;
|
||||
}
|
||||
//console.log({counter, file});
|
||||
if(!newFile || !(newFile instanceof TFile)) {
|
||||
new Notice("File not found. NewExcalidraw Drawing is taking too long to create. Please try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!newFile) return;
|
||||
const link = this.app.metadataCache.fileToLinktext(newFile,sourceFile.path, true);
|
||||
replacer(link, newFile);
|
||||
}
|
||||
|
||||
if(isCanvas) {
|
||||
const selectedNodes:any = [];
|
||||
canvasView.canvas.nodes.forEach((node:any) => {
|
||||
if(node.nodeEl.hasClass("is-focused")) selectedNodes.push(node);
|
||||
})
|
||||
if(selectedNodes.length !== 1) return false;
|
||||
const node = selectedNodes[0];
|
||||
let extension = "";
|
||||
let isExcalidraw = false;
|
||||
if(node.file) {
|
||||
extension = node.file.extension;
|
||||
isExcalidraw = this.isExcalidrawFile(node.file);
|
||||
}
|
||||
if(node.url) {
|
||||
extension = getURLImageExtension(node.url);
|
||||
}
|
||||
const page = extension === "pdf" ? getActivePDFPageNumberFromPDFView(node?.child) : undefined;
|
||||
if(!page && !IMAGE_TYPES.contains(extension) && !isExcalidraw) return false;
|
||||
if(checking) return true;
|
||||
|
||||
const replacer = (link:string, file: TFile) => {
|
||||
if(node.file) {
|
||||
(node.file.extension === "pdf")
|
||||
? node.canvas.createFileNode({pos:{x:node.x + node.width + 10,y: node.y}, file})
|
||||
: node.setFile(file);
|
||||
}
|
||||
if(node.url) {
|
||||
node.canvas.createFileNode({pos:{x:node.x + 20,y: node.y+20}, file});
|
||||
}
|
||||
}
|
||||
carveout(Boolean(node.file), canvasView.file, node.file, node.url, replacer, page ? `page=${page}` : undefined);
|
||||
}
|
||||
|
||||
if (markdownView) {
|
||||
const editor = markdownView.editor;
|
||||
const cursor = editor.getCursor();
|
||||
const line = editor.getLine(cursor.line);
|
||||
const parts = REGEX_LINK.getResList(line);
|
||||
if(parts.length === 0) return false;
|
||||
const imgpath = REGEX_LINK.getLink(parts[0]);
|
||||
const imagePathParts = imgpath.split("#");
|
||||
const hasRef = imagePathParts.length === 2;
|
||||
const imageFile = this.app.metadataCache.getFirstLinkpathDest(
|
||||
hasRef ? imagePathParts[0] : imgpath,
|
||||
markdownView.file.path
|
||||
);
|
||||
const isFile = (imageFile && imageFile instanceof TFile);
|
||||
const isExcalidraw = isFile ? this.isExcalidrawFile(imageFile) : false;
|
||||
let imagepath = isFile ? imageFile.path : "";
|
||||
let extension = isFile ? imageFile.extension : "";
|
||||
if(imgpath.match(/^https?|file/)) {
|
||||
imagepath = imgpath;
|
||||
extension = getURLImageExtension(imgpath);
|
||||
}
|
||||
if(imagepath === "") return false;
|
||||
if(extension !== "pdf" && !IMAGE_TYPES.contains(extension) && !isExcalidraw) return false;
|
||||
if(checking) return true;
|
||||
const ref = imagePathParts[1];
|
||||
const replacer = (link:string) => {
|
||||
const lineparts = line.split(parts[0].value[0])
|
||||
const pdfLink = isFile && ref
|
||||
? "\n" + getLink(this ,{
|
||||
embed: false,
|
||||
alias: `${imageFile.basename}, ${ref.replace("="," ")}`,
|
||||
path:`${imageFile.path}#${ref}`
|
||||
})
|
||||
: "";
|
||||
editor.setLine(cursor.line,lineparts[0] + getLink(this ,{embed: true, path:link}) + pdfLink + lineparts[1]);
|
||||
}
|
||||
carveout(isFile, markdownView.file, imageFile, imagepath, replacer, ref);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.addCommand({
|
||||
id: "insert-image",
|
||||
name: t("INSERT_IMAGE"),
|
||||
@@ -1877,6 +2029,22 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "universal-add-file",
|
||||
name: t("INSERT_CARD"),
|
||||
checkCallback: (checking: boolean) => {
|
||||
if (checking) {
|
||||
return Boolean(this.app.workspace.getActiveViewOfType(ExcalidrawView))
|
||||
}
|
||||
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
|
||||
if (view) {
|
||||
view.insertBackOfTheNoteCard();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: "insert-LaTeX-symbol",
|
||||
name: t("INSERT_LATEX"),
|
||||
@@ -1941,21 +2109,24 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isFileEmpty = activeFile.stat.size === 0;
|
||||
|
||||
if (checking) {
|
||||
return isFileEmpty;
|
||||
if(this.isExcalidrawFile(activeFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isFileEmpty) {
|
||||
(async () => {
|
||||
await this.app.vault.modify(
|
||||
activeFile,
|
||||
await this.getBlankDrawing(),
|
||||
);
|
||||
this.setExcalidrawView(activeView.leaf);
|
||||
})();
|
||||
if(checking) {
|
||||
return true;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const template = await this.getBlankDrawing();
|
||||
const target = await this.app.vault.read(activeFile);
|
||||
const mergedTarget = mergeMarkdownFiles(template, target);
|
||||
await this.app.vault.modify(
|
||||
activeFile,
|
||||
mergedTarget,
|
||||
);
|
||||
this.setExcalidrawView(activeView.leaf);
|
||||
})();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2139,10 +2310,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
self.excalidrawFileModes[this.id || state.state.file] !==
|
||||
"markdown"
|
||||
) {
|
||||
// Then check for the excalidraw frontMatterKey
|
||||
const cache = app.metadataCache.getCache(state.state.file);
|
||||
|
||||
if (cache?.frontmatter && cache.frontmatter[FRONTMATTER_KEYS["plugin"].name]) {
|
||||
if (fileShouldDefaultAsExcalidraw(state.state.file,this.app)) {
|
||||
// If we have it, force the view type to excalidraw
|
||||
const newState = {
|
||||
...state,
|
||||
@@ -2288,8 +2456,9 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
return;
|
||||
}
|
||||
if(file.extension==="md") {
|
||||
if(excalidrawView.semaphores.embeddableIsEditingSelf) return;
|
||||
const inData = new ExcalidrawData(self);
|
||||
const data = await app.vault.read(file);
|
||||
const data = await this.app.vault.read(file);
|
||||
await inData.loadData(data,file,getTextMode(data));
|
||||
excalidrawView.synchronizeWithData(inData);
|
||||
if(excalidrawView.semaphores.dirty) {
|
||||
@@ -2932,7 +3101,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
const textElements = excalidrawData.elements?.filter(
|
||||
(el: any) => el.type == "text",
|
||||
);
|
||||
let outString = "# Text Elements\n";
|
||||
let outString = `${MD_TEXTELEMENTS}\n`;
|
||||
let id: string;
|
||||
for (const te of textElements) {
|
||||
id = te.id;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Copy, Crop, Globe, RotateCcw, Scan, Settings } from "lucide-react";
|
||||
import { Copy, Crop, Globe, RotateCcw, Scan, Settings, TextSelect } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { PenStyle } from "src/PenTypes";
|
||||
|
||||
@@ -26,6 +26,7 @@ export const ICONS = {
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
BackOfNote: (<TextSelect />),
|
||||
Reload: (<RotateCcw />),
|
||||
Copy: (<Copy /> ),
|
||||
Globe: (<Globe />),
|
||||
|
||||
@@ -7,12 +7,14 @@ import { ActionButton } from "./ActionButton";
|
||||
import { ICONS } from "./ActionIcons";
|
||||
import { t } from "src/lang/helpers";
|
||||
import { ScriptEngine } from "src/Scripts";
|
||||
import { ROOTELEMENTSIZE, mutateElement, nanoid, sceneCoordsToViewportCoords } from "src/constants/constants";
|
||||
import { MD_EX_SECTIONS, ROOTELEMENTSIZE, mutateElement, nanoid, sceneCoordsToViewportCoords } from "src/constants/constants";
|
||||
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "src/ExcalidrawData";
|
||||
import { processLinkText, useDefaultExcalidrawFrame } from "src/utils/CustomEmbeddableUtils";
|
||||
import { cleanSectionHeading } from "src/utils/ObsidianUtils";
|
||||
import { EmbeddableSettings } from "src/dialogs/EmbeddableSettings";
|
||||
import { openExternalLink } from "src/utils/ExcalidrawViewUtils";
|
||||
import { getEA } from "src";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
|
||||
export class EmbeddableMenu {
|
||||
|
||||
@@ -34,10 +36,13 @@ export class EmbeddableMenu {
|
||||
file.extension === "md",
|
||||
)
|
||||
const link = `[[${path}${subpath}]]`;
|
||||
mutateElement (element,{link});
|
||||
const ea = getEA(view) as ExcalidrawAutomate;
|
||||
ea.copyViewElementsToEAforEditing([element]);
|
||||
ea.getElement(element.id).link = link;
|
||||
//mutateElement (element,{link});
|
||||
//view.setDirty(99);
|
||||
view.excalidrawData.elementLinks.set(element.id, link);
|
||||
view.setDirty(99);
|
||||
view.updateScene({appState: {activeEmbeddable: null}});
|
||||
ea.addElementsToView(false, true, true);
|
||||
}
|
||||
|
||||
private menuFadeTimeout: number = 0;
|
||||
@@ -99,6 +104,7 @@ export class EmbeddableMenu {
|
||||
const { subpath, file } = processLinkText(link, view);
|
||||
if(!file) return;
|
||||
const isMD = file.extension==="md";
|
||||
const isExcalidrawFile = view.plugin.isExcalidrawFile(file);
|
||||
const { x, y } = sceneCoordsToViewportCoords( { sceneX: element.x, sceneY: element.y }, appState);
|
||||
const top = `${y-2.5*ROOTELEMENTSIZE-appState.offsetTop}px`;
|
||||
const left = `${x-appState.offsetLeft}px`;
|
||||
@@ -128,15 +134,23 @@ export class EmbeddableMenu {
|
||||
key={"MarkdownSection"}
|
||||
title={t("NARROW_TO_HEADING")}
|
||||
action={async () => {
|
||||
view.updateScene({appState: {activeEmbeddable: null}});
|
||||
const sections = (await app.metadataCache.blockCache
|
||||
.getForFile({ isCancelled: () => false },file))
|
||||
.blocks.filter((b: any) => b.display && b.node?.type === "heading");
|
||||
const values = [""].concat(
|
||||
sections.map((b: any) => `#${cleanSectionHeading(b.display)}`)
|
||||
);
|
||||
const display = [t("SHOW_ENTIRE_FILE")].concat(
|
||||
sections.map((b: any) => b.display)
|
||||
);
|
||||
.blocks.filter((b: any) => b.display && b.node?.type === "heading")
|
||||
.filter((b: any) => !isExcalidrawFile || !MD_EX_SECTIONS.includes(b.display));
|
||||
let values, display;
|
||||
if(isExcalidrawFile) {
|
||||
values = sections.map((b: any) => `#${cleanSectionHeading(b.display)}`);
|
||||
display = sections.map((b: any) => b.display);
|
||||
} else {
|
||||
values = [""].concat(
|
||||
sections.map((b: any) => `#${cleanSectionHeading(b.display)}`)
|
||||
);
|
||||
display = [t("SHOW_ENTIRE_FILE")].concat(
|
||||
sections.map((b: any) => b.display)
|
||||
);
|
||||
}
|
||||
const newSubpath = await ScriptEngine.suggester(
|
||||
app, display, values, "Select section from document"
|
||||
);
|
||||
@@ -149,12 +163,13 @@ export class EmbeddableMenu {
|
||||
view={view}
|
||||
/>
|
||||
)}
|
||||
{isMD && (
|
||||
{isMD && !isExcalidrawFile && (
|
||||
<ActionButton
|
||||
key={"MarkdownBlock"}
|
||||
title={t("NARROW_TO_BLOCK")}
|
||||
action={async () => {
|
||||
if(!file) return;
|
||||
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");
|
||||
|
||||
@@ -15,6 +15,7 @@ import { isWinALTorMacOPT, isWinCTRLorMacCMD, isSHIFT } from "src/utils/Modifier
|
||||
import { InsertPDFModal } from "src/dialogs/InsertPDFModal";
|
||||
import { ExportDialog } from "src/dialogs/ExportDialog";
|
||||
import { openExternalLink } from "src/utils/ExcalidrawViewUtils";
|
||||
import { UniversalInsertFileModal } from "src/dialogs/UniversalInsertFileModal";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
const dark = '<svg style="stroke:#ced4da;#212529;color:#ced4da;fill:#ced4da" ';
|
||||
@@ -461,11 +462,37 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
icon={ICONS.switchToMarkdown}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"link-to-element"}
|
||||
title={t("INSERT_LINK_TO_ELEMENT")}
|
||||
action={(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
if(isWinALTorMacOPT(e)) {
|
||||
openExternalLink("https://youtu.be/yZQoJg2RCKI", this.props.view.app);
|
||||
return;
|
||||
}
|
||||
this.props.view.copyLinkToSelectedElementToClipboard(
|
||||
isWinCTRLorMacCMD(e) ? "group=" : (isSHIFT(e) ? "area=" : "")
|
||||
);
|
||||
}}
|
||||
icon={ICONS.copyElementLink}
|
||||
view={this.props.view}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Insert actions</legend>
|
||||
<div className="buttonList buttonListIcon">
|
||||
<ActionButton
|
||||
key={"anyfile"}
|
||||
title={t("UNIVERSAL_ADD_FILE")}
|
||||
action={() => {
|
||||
this.props.centerPointer();
|
||||
const insertFileModal = new UniversalInsertFileModal(this.props.view.plugin, this.props.view);
|
||||
insertFileModal.open();
|
||||
}}
|
||||
icon={ICONS["add-file"]}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"image"}
|
||||
title={t("INSERT_IMAGE")}
|
||||
@@ -501,6 +528,16 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
icon={ICONS.insertMD}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"insertBackOfNote"}
|
||||
title={t("INSERT_CARD")}
|
||||
action={() => {
|
||||
this.props.centerPointer();
|
||||
this.props.view.insertBackOfTheNoteCard();
|
||||
}}
|
||||
icon={ICONS.BackOfNote}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"latex"}
|
||||
title={t("INSERT_LATEX")}
|
||||
@@ -528,21 +565,6 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
|
||||
icon={ICONS.insertLink}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"link-to-element"}
|
||||
title={t("INSERT_LINK_TO_ELEMENT")}
|
||||
action={(e:React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
if(isWinALTorMacOPT(e)) {
|
||||
openExternalLink("https://youtu.be/yZQoJg2RCKI", this.props.view.app);
|
||||
return;
|
||||
}
|
||||
this.props.view.copyLinkToSelectedElementToClipboard(
|
||||
isWinCTRLorMacCMD(e) ? "group=" : (isSHIFT(e) ? "area=" : "")
|
||||
);
|
||||
}}
|
||||
icon={ICONS.copyElementLink}
|
||||
view={this.props.view}
|
||||
/>
|
||||
<ActionButton
|
||||
key={"import-svg"}
|
||||
title={t("IMPORT_SVG")}
|
||||
|
||||
@@ -33,11 +33,12 @@ import { EmbeddalbeMDFileCustomDataSettingsComponent } from "./dialogs/Embeddabl
|
||||
import { startupScript } from "./constants/starutpscript";
|
||||
import { ModifierKeySet, ModifierSetType } from "./utils/ModifierkeyHelper";
|
||||
import { ModifierKeySettingsComponent } from "./dialogs/ModifierKeySettings";
|
||||
import { CROPPED_PREFIX } from "./utils/CarveOut";
|
||||
import { ANNOTATED_PREFIX, CROPPED_PREFIX } from "./utils/CarveOut";
|
||||
|
||||
export interface ExcalidrawSettings {
|
||||
folder: string;
|
||||
cropFolder: string;
|
||||
annotateFolder: string;
|
||||
embedUseExcalidrawFolder: boolean;
|
||||
templateFilePath: string;
|
||||
scriptFolderPath: string;
|
||||
@@ -52,6 +53,7 @@ export interface ExcalidrawSettings {
|
||||
drawingFilenameDateTime: string;
|
||||
useExcalidrawExtension: boolean;
|
||||
cropPrefix: string;
|
||||
annotatePrefix: string;
|
||||
displaySVGInPreview: boolean; //No longer used since 1.9.13
|
||||
previewImageType: PreviewImageType; //Introduced with 1.9.13
|
||||
allowImageCache: boolean;
|
||||
@@ -177,6 +179,8 @@ export interface ExcalidrawSettings {
|
||||
},
|
||||
slidingPanesSupport: boolean;
|
||||
areaZoomLimit: number;
|
||||
longPressDesktop: number;
|
||||
longPressMobile: number;
|
||||
}
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
@@ -184,6 +188,7 @@ declare const PLUGIN_VERSION:string;
|
||||
export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
folder: "Excalidraw",
|
||||
cropFolder: "",
|
||||
annotateFolder: "",
|
||||
embedUseExcalidrawFolder: false,
|
||||
templateFilePath: "Excalidraw/Template.excalidraw",
|
||||
scriptFolderPath: "Excalidraw/Scripts",
|
||||
@@ -198,6 +203,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
drawingFilenameDateTime: "YYYY-MM-DD HH.mm.ss",
|
||||
useExcalidrawExtension: true,
|
||||
cropPrefix: CROPPED_PREFIX,
|
||||
annotatePrefix: ANNOTATED_PREFIX,
|
||||
displaySVGInPreview: undefined,
|
||||
previewImageType: undefined,
|
||||
allowImageCache: true,
|
||||
@@ -415,6 +421,8 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
|
||||
},
|
||||
slidingPanesSupport: false,
|
||||
areaZoomLimit: 1,
|
||||
longPressDesktop: 500,
|
||||
longPressMobile: 500,
|
||||
};
|
||||
|
||||
export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
@@ -576,6 +584,19 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("ANNOTATE_FOLDER_NAME"))
|
||||
.setDesc(fragWithHTML(t("ANNOTATE_FOLDER_DESC")))
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("e.g.: Excalidraw/Annotations")
|
||||
.setValue(this.plugin.settings.annotateFolder)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.annotateFolder = value;
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("TEMPLATE_NAME"))
|
||||
.setDesc(fragWithHTML(t("TEMPLATE_DESC")))
|
||||
@@ -772,7 +793,6 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("CROP_PREFIX_NAME"))
|
||||
.setDesc(fragWithHTML(t("CROP_PREFIX_DESC")))
|
||||
@@ -789,6 +809,23 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(detailsEl)
|
||||
.setName(t("ANNOTATE_PREFIX_NAME"))
|
||||
.setDesc(fragWithHTML(t("ANNOTATE_PREFIX_DESC")))
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("e.g.: Annotated_ ")
|
||||
.setValue(this.plugin.settings.annotatePrefix)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.annotatePrefix = value.replaceAll(
|
||||
/[<>:"/\\|?*]/g,
|
||||
"_",
|
||||
);
|
||||
text.setValue(this.plugin.settings.annotatePrefix);
|
||||
this.applySettingsUpdate();
|
||||
}),
|
||||
);
|
||||
//------------------------------------------------
|
||||
// AI Settings
|
||||
//------------------------------------------------
|
||||
@@ -1151,6 +1188,48 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
});
|
||||
detailsEl.createDiv({ text: t("DRAG_MODIFIER_DESC"), cls: "setting-item-description" });
|
||||
|
||||
let longPressDesktop: HTMLDivElement;
|
||||
new Setting(detailsEl)
|
||||
.setName(t("LONG_PRESS_DESKTOP_NAME"))
|
||||
.setDesc(fragWithHTML(t("LONG_PRESS_DESKTOP_DESC")))
|
||||
.addSlider((slider) =>
|
||||
slider
|
||||
.setLimits(300, 100, 3000)
|
||||
.setValue(this.plugin.settings.longPressDesktop)
|
||||
.onChange(async (value) => {
|
||||
longPressDesktop.innerText = ` ${value.toString()}`;
|
||||
this.plugin.settings.longPressDesktop = value;
|
||||
this.applySettingsUpdate(true);
|
||||
}),
|
||||
)
|
||||
.settingEl.createDiv("", (el) => {
|
||||
longPressDesktop = el;
|
||||
el.style.minWidth = "2.3em";
|
||||
el.style.textAlign = "right";
|
||||
el.innerText = ` ${this.plugin.settings.longPressDesktop.toString()}`;
|
||||
});
|
||||
|
||||
let longPressMobile: HTMLDivElement;
|
||||
new Setting(detailsEl)
|
||||
.setName(t("LONG_PRESS_MOBILE_NAME"))
|
||||
.setDesc(fragWithHTML(t("LONG_PRESS_MOBILE_DESC")))
|
||||
.addSlider((slider) =>
|
||||
slider
|
||||
.setLimits(300, 100, 3000)
|
||||
.setValue(this.plugin.settings.longPressMobile)
|
||||
.onChange(async (value) => {
|
||||
longPressDesktop.innerText = ` ${value.toString()}`;
|
||||
this.plugin.settings.longPressMobile = value;
|
||||
this.applySettingsUpdate(true);
|
||||
}),
|
||||
)
|
||||
.settingEl.createDiv("", (el) => {
|
||||
longPressMobile = el;
|
||||
el.style.minWidth = "2.3em";
|
||||
el.style.textAlign = "right";
|
||||
el.innerText = ` ${this.plugin.settings.longPressMobile.toString()}`;
|
||||
});
|
||||
|
||||
new ModifierKeySettingsComponent(
|
||||
detailsEl,
|
||||
this.plugin.settings.modifierKeyConfig,
|
||||
|
||||
@@ -80,8 +80,11 @@ export class CanvasNodeFactory {
|
||||
return node;
|
||||
}
|
||||
|
||||
public startEditing(node: ObsidianCanvasNode, theme: string) {
|
||||
public async startEditing(node: ObsidianCanvasNode, theme: string) {
|
||||
if (!this.initialized || !node) return;
|
||||
if (node.file === this.view.file) {
|
||||
await this.view.setEmbeddableIsEditingSelf();
|
||||
}
|
||||
node.startEditing();
|
||||
|
||||
const obsidianTheme = isObsidianThemeDark() ? "theme-dark" : "theme-light";
|
||||
@@ -118,6 +121,9 @@ export class CanvasNodeFactory {
|
||||
public stopEditing(node: ObsidianCanvasNode) {
|
||||
if(!this.initialized || !node) return;
|
||||
if(!node.child.editMode) return;
|
||||
if(node.file === this.view.file) {
|
||||
this.view.clearEmbeddableIsEditingSelf();
|
||||
}
|
||||
node.child.showPreview();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,9 @@ import { getEA } from "src";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import { getCropFileNameAndFolder, getListOfTemplateFiles, splitFolderAndFilename } from "./FileUtils";
|
||||
import { Notice, TFile } from "obsidian";
|
||||
import ExcalidrawView from "src/ExcalidrawView";
|
||||
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
|
||||
export const CROPPED_PREFIX = "cropped_";
|
||||
export const ANNOTATED_PREFIX = "annotated_";
|
||||
|
||||
export const carveOutImage = async (sourceEA: ExcalidrawAutomate, viewImageEl: ExcalidrawImageElement) => {
|
||||
if(!viewImageEl?.fileId) return;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { App, TFile } from "obsidian";
|
||||
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
|
||||
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "src/ExcalidrawData";
|
||||
import ExcalidrawView from "src/ExcalidrawView";
|
||||
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { ExcalidrawElement, ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { getLinkParts } from "./Utils";
|
||||
|
||||
export const insertImageToView = async (
|
||||
@@ -118,4 +118,15 @@ export const getExcalidrawFileForwardLinks = (app: App, excalidrawFile: TFile, s
|
||||
secondOrderLinks = [...linkset].join(" ");
|
||||
}
|
||||
return secondOrderLinks;
|
||||
}
|
||||
|
||||
export const getFrameBasedOnFrameNameOrId = (frameName: string, elements: ExcalidrawElement[]): ExcalidrawFrameElement | null => {
|
||||
const frames = elements
|
||||
.filter((el: ExcalidrawElement)=>el.type==="frame")
|
||||
.map((el: ExcalidrawFrameElement, idx: number)=>{
|
||||
return {el: el, id: el.id, name: el.name ?? `Frame ${String(idx+1).padStart(2,"0")}`};
|
||||
})
|
||||
.filter((item:any) => item.id === frameName || item.name === frameName)
|
||||
.map((item:any)=>item.el as ExcalidrawFrameElement);
|
||||
return frames.length === 1 ? frames[0] : null;
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { loadPdfJs, normalizePath, Notice, requestUrl, RequestUrlResponse, TAbstractFile, TFile, TFolder, Vault } from "obsidian";
|
||||
import { DEVICE, URLFETCHTIMEOUT } from "src/constants/constants";
|
||||
import { App, loadPdfJs, normalizePath, Notice, requestUrl, RequestUrlResponse, TAbstractFile, TFile, TFolder, Vault } from "obsidian";
|
||||
import { DEVICE, FRONTMATTER_KEYS, URLFETCHTIMEOUT } from "src/constants/constants";
|
||||
import { IMAGE_MIME_TYPES, MimeType } from "src/EmbeddedFileLoader";
|
||||
import { ExcalidrawSettings } from "src/settings";
|
||||
import { errorlog, getDataURL } from "./Utils";
|
||||
import ExcalidrawPlugin from "src/main";
|
||||
import { CROPPED_PREFIX } from "./CarveOut";
|
||||
import { ANNOTATED_PREFIX, CROPPED_PREFIX } from "./CarveOut";
|
||||
import { getAttachmentsFolderAndFilePath } from "./ObsidianUtils";
|
||||
|
||||
/**
|
||||
@@ -387,6 +387,19 @@ export const getCropFileNameAndFolder = async (plugin: ExcalidrawPlugin, hostPat
|
||||
return {folderpath, filename};
|
||||
}
|
||||
|
||||
export const getAnnotationFileNameAndFolder = async (plugin: ExcalidrawPlugin, hostPath: string, baseNewFileName: string):Promise<{folderpath: string, filename: string}> => {
|
||||
let prefix = plugin.settings.annotatePrefix;
|
||||
if(!prefix || prefix.trim() === "") prefix = ANNOTATED_PREFIX;
|
||||
const filename = prefix + baseNewFileName + ".md";
|
||||
if(!plugin.settings.annotateFolder || plugin.settings.annotateFolder.trim() === "") {
|
||||
const folderpath = (await getAttachmentsFolderAndFilePath(plugin.app, hostPath, filename)).folder;
|
||||
return {folderpath, filename};
|
||||
}
|
||||
const folderpath = normalizePath(plugin.settings.annotateFolder);
|
||||
await checkAndCreateFolder(folderpath);
|
||||
return {folderpath, filename};
|
||||
}
|
||||
|
||||
export const getListOfTemplateFiles = (plugin: ExcalidrawPlugin):TFile[] | null => {
|
||||
const normalizedTemplatePath = normalizePath(plugin.settings.templateFilePath);
|
||||
const template = plugin.app.vault.getAbstractFileByPath(normalizedTemplatePath);
|
||||
@@ -407,4 +420,12 @@ export const getListOfTemplateFiles = (plugin: ExcalidrawPlugin):TFile[] | null
|
||||
return [templateFile];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const fileShouldDefaultAsExcalidraw = (path:string, app:App):boolean => {
|
||||
if(!path) return false;
|
||||
const cache = app.metadataCache.getCache(path);
|
||||
return cache?.frontmatter &&
|
||||
cache.frontmatter[FRONTMATTER_KEYS["plugin"].name] &&
|
||||
!Boolean(cache.frontmatter[FRONTMATTER_KEYS["open-as-markdown"].name]);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import {
|
||||
App,
|
||||
FrontMatterCache,
|
||||
normalizePath, OpenViewState, parseFrontMatterEntry, TFile, View, Workspace, WorkspaceLeaf, WorkspaceSplit
|
||||
} from "obsidian";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import { checkAndCreateFolder, splitFolderAndFilename } from "./FileUtils";
|
||||
import { linkClickModifierType, ModifierKeys } from "./ModifierkeyHelper";
|
||||
import { REG_BLOCK_REF_CLEAN, REG_SECTION_REF_CLEAN } from "src/constants/constants";
|
||||
import yaml from "js-yaml";
|
||||
|
||||
export const getParentOfClass = (element: Element, cssClass: string):HTMLElement | null => {
|
||||
let parent = element.parentElement;
|
||||
@@ -288,4 +290,50 @@ export const openLeaf = ({
|
||||
leaf = fnGetLeaf();
|
||||
const promise = leaf.openFile(file, openState);
|
||||
return {leaf, promise};
|
||||
}
|
||||
}
|
||||
|
||||
export const mergeMarkdownFiles = (template: string, target: string): string => {
|
||||
// Extract frontmatter from the template
|
||||
const templateFrontmatterEnd = template.indexOf('---', 4); // Find end of frontmatter
|
||||
const templateFrontmatter = template.substring(4, templateFrontmatterEnd).trim();
|
||||
const templateContent = template.substring(templateFrontmatterEnd + 3); // Skip frontmatter and ---
|
||||
|
||||
// Parse template frontmatter
|
||||
const templateFrontmatterObj: FrontMatterCache = yaml.load(templateFrontmatter) || {};
|
||||
|
||||
// Extract frontmatter from the target if it exists
|
||||
let targetFrontmatterObj: FrontMatterCache = {};
|
||||
let targetContent = '';
|
||||
if (target.includes('---')) {
|
||||
const targetFrontmatterEnd = target.indexOf('---', 4); // Find end of frontmatter
|
||||
const targetFrontmatter = target.substring(4, targetFrontmatterEnd).trim();
|
||||
targetContent = target.substring(targetFrontmatterEnd + 3); // Skip frontmatter and ---
|
||||
|
||||
// Parse target frontmatter
|
||||
targetFrontmatterObj = yaml.load(targetFrontmatter) || {};
|
||||
} else {
|
||||
// If target doesn't have frontmatter, consider the entire content as target content
|
||||
targetContent = target.trim();
|
||||
}
|
||||
|
||||
// Merge frontmatter with target values taking precedence
|
||||
const mergedFrontmatter: FrontMatterCache = { ...templateFrontmatterObj };
|
||||
|
||||
// Merge arrays by combining and removing duplicates
|
||||
for (const key in targetFrontmatterObj) {
|
||||
if (Array.isArray(targetFrontmatterObj[key]) && Array.isArray(mergedFrontmatter[key])) {
|
||||
const combinedArray = [...new Set([...mergedFrontmatter[key], ...targetFrontmatterObj[key]])];
|
||||
mergedFrontmatter[key] = combinedArray;
|
||||
} else {
|
||||
mergedFrontmatter[key] = targetFrontmatterObj[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Convert merged frontmatter back to YAML
|
||||
const mergedFrontmatterYaml = yaml.dump(mergedFrontmatter);
|
||||
|
||||
// Concatenate frontmatter and content
|
||||
const mergedMarkdown = `---\n${mergedFrontmatterYaml}---\n${targetContent}\n\n${templateContent.trim()}\n`;
|
||||
|
||||
return mergedMarkdown;
|
||||
};
|
||||
@@ -750,8 +750,8 @@ export const getContainerElement = (
|
||||
return null;
|
||||
};
|
||||
|
||||
export const updateFrontmatterInString = (data:string, keyValuePairs: [string,string][]):string => {
|
||||
if(!data) return data;
|
||||
export const updateFrontmatterInString = (data:string, keyValuePairs?: [string,string][]):string => {
|
||||
if(!data || !keyValuePairs) return data;
|
||||
for(const kvp of keyValuePairs) {
|
||||
const r = new RegExp(`${kvp[0]}:\\s.*\\n`,"g");
|
||||
data = data.match(r)
|
||||
|
||||
Reference in New Issue
Block a user