Compare commits

...

14 Commits

Author SHA1 Message Date
zsviczian
b8655cff5e 2.7.0-beta-3 embeddable debugging 2024-12-13 23:07:29 +01:00
zsviczian
be452fee6d moved mathjax to a separate module and zip it in main.js 2024-12-13 20:02:28 +01:00
zsviczian
90589dd075 2.7.0-beta-2, 0.17.6-20 fixed mermaid race condition and settings save on startup 2024-12-12 22:46:43 +01:00
zsviczian
9c5b48c037 restructured onload 2024-12-12 11:47:03 +01:00
zsviczian
4406709920 fixed onceOffGPTVersionReset 2024-12-12 11:38:17 +01:00
zsviczian
b7ba0f8909 2.7.0-beta-1 2024-12-10 22:22:59 +01:00
zsviczian
c28911c739 Merge pull request #2144 from dmscode/master
Update zh-cn.ts to 9e1d491
2024-12-10 20:15:37 +01:00
dmscode
28088754ad Update zh-cn.ts to 9e1d491 2024-12-09 08:06:00 +08:00
zsviczian
9e1d491981 2.6.8 2024-12-08 16:09:00 +01:00
zsviczian
ab5caa4877 Merge pull request #2142 from TrillStones/master
[Script Contribution] [ Update] Image Occlusion
2024-12-08 15:56:47 +01:00
trillstones
44b580ae78 add image for image-occlusion script 2024-12-08 19:26:50 +08:00
trillstones
3859eddc80 add new image for image-occlusion script 2024-12-08 19:01:13 +08:00
trillstones
6098e1b42e add setting - Generate Images No Matter What
change card's and folder's naming logic
2024-12-08 17:29:00 +08:00
zsviczian
6ad8d2f620 2.6.8 - before field suggester implementation 2024-12-08 07:00:11 +01:00
35 changed files with 1517 additions and 2840 deletions

96
MathjaxToSVG/index.ts Normal file
View File

@@ -0,0 +1,96 @@
import {mathjax} from "mathjax-full/js/mathjax";
import {TeX} from 'mathjax-full/js/input/tex.js';
import {SVG} from 'mathjax-full/js/output/svg.js';
import {LiteAdaptor, liteAdaptor} from 'mathjax-full/js/adaptors/liteAdaptor.js';
import {RegisterHTMLHandler} from 'mathjax-full/js/handlers/html.js';
import {AllPackages} from 'mathjax-full/js/input/tex/AllPackages.js';
import { customAlphabet } from "nanoid";
type DataURL = string & { _brand: "DataURL" };
type FileId = string & { _brand: "FileId" };
const fileid = customAlphabet("1234567890abcdef", 40);
let adaptor: LiteAdaptor;
let html: any;
let preamble: string;
function svgToBase64(svg: string): string {
return `data:image/svg+xml;base64,${btoa(
decodeURIComponent(encodeURIComponent(svg.replaceAll(" ", " "))),
)}`;
}
async function getImageSize(src: string): Promise<{ height: number; width: number }> {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve({ height: img.naturalHeight, width: img.naturalWidth });
img.onerror = reject;
img.src = src;
});
}
export async function tex2dataURL(
tex: string,
scale: number = 4,
app?: any
): Promise<{
mimeType: string;
fileId: FileId;
dataURL: DataURL;
created: number;
size: { height: number; width: number };
}> {
let input: TeX<unknown, unknown, unknown>;
let output: SVG<unknown, unknown, unknown>;
if(!adaptor) {
if (app) {
const file = app.vault.getAbstractFileByPath("preamble.sty");
preamble = file ? await app.vault.read(file) : null;
}
adaptor = liteAdaptor();
RegisterHTMLHandler(adaptor);
input = new TeX({
packages: AllPackages,
...(preamble ? {
inlineMath: [['$', '$']],
displayMath: [['$$', '$$']]
} : {}),
});
output = new SVG({ fontCache: "local" });
html = mathjax.document("", { InputJax: input, OutputJax: output });
}
try {
const node = html.convert(
preamble ? `${preamble}${tex}` : tex,
{ display: true, scale }
);
const svg = new DOMParser().parseFromString(adaptor.innerHTML(node), "image/svg+xml").firstChild as SVGSVGElement;
if (svg) {
if(svg.width.baseVal.valueInSpecifiedUnits < 2) {
svg.width.baseVal.valueAsString = `${(svg.width.baseVal.valueInSpecifiedUnits+1).toFixed(3)}ex`;
}
const img = svgToBase64(svg.outerHTML);
svg.width.baseVal.valueAsString = (svg.width.baseVal.valueInSpecifiedUnits * 10).toFixed(3);
svg.height.baseVal.valueAsString = (svg.height.baseVal.valueInSpecifiedUnits * 10).toFixed(3);
const dataURL = svgToBase64(svg.outerHTML);
return {
mimeType: "image/svg+xml",
fileId: fileid() as FileId,
dataURL: dataURL as DataURL,
created: Date.now(),
size: await getImageSize(img),
};
}
} catch (e) {
console.error(e);
}
return null;
}
export function clearMathJaxVariables(): void {
adaptor = null;
html = null;
preamble = null;
}

23
MathjaxToSVG/package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "@zsviczian/mathjax-to-svg",
"version": "1.0.0",
"main": "dist/index.js",
"scripts": {
"build": "cross-env NODE_ENV=production rollup --config rollup.config.js",
"dev": "cross-env NODE_ENV=development rollup --config rollup.config.js"
},
"dependencies": {
"mathjax-full": "^3.2.2",
"nanoid": "^4.0.2"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-typescript": "^11.1.6",
"cross-env": "^7.0.3",
"obsidian": "1.5.7-1",
"rollup": "^2.70.1",
"typescript": "^5.2.2",
"rollup-plugin-terser": "^7.0.2"
}
}

View File

@@ -0,0 +1,35 @@
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import { terser } from 'rollup-plugin-terser';
const isProd = (process.env.NODE_ENV === 'production');
export default {
input: './index.ts',
output: {
dir: 'dist',
format: 'iife',
name: 'MathjaxToSVG', // Global variable name
exports: 'named',
sourcemap: !isProd,
},
plugins: [
typescript({
tsconfig: '../tsconfig.json',
}),
commonjs(),
nodeResolve({
browser: true,
preferBuiltins: false
}),
isProd && terser({
format: {
comments: false,
},
compress: {
passes: 2,
}
})
].filter(Boolean)
};

View File

@@ -518,29 +518,28 @@ const getImageName = (fileId) => {
return 'image';
};
// Create timestamp in format: YYMMDDHHmmssSSS
const now = new Date();
const timestamp = now.getFullYear().toString().slice(-2) +
(now.getMonth() + 1).toString().padStart(2, '0') +
now.getDate().toString().padStart(2, '0') +
now.getHours().toString().padStart(2, '0') +
now.getMinutes().toString().padStart(2, '0') +
now.getSeconds().toString().padStart(2, '0') +
now.getMilliseconds().toString().padStart(3, '0');
// Function to generate current timestamp for file names
// Function to generate current timestamp for file names (For card file names)
const getCurrentTimestamp = () => {
const now = new Date();
const baseTimestamp = now.getFullYear().toString().slice(-2) +
(now.getMonth() + 1).toString().padStart(2, '0') +
now.getDate().toString().padStart(2, '0') +
now.getHours().toString().padStart(2, '0') +
now.getMinutes().toString().padStart(2, '0') +
now.getSeconds().toString().padStart(2, '0') +
now.getMilliseconds().toString().padStart(3, '0');
const baseTimestamp = now.getFullYear() +
(now.getMonth() + 1).toString().padStart(2, '0') +
now.getDate().toString().padStart(2, '0') +
now.getHours().toString().padStart(2, '0') +
now.getMinutes().toString().padStart(2, '0') +
now.getSeconds().toString().padStart(2, '0') +
now.getMilliseconds().toString().padStart(3, '0');
return baseTimestamp;
};
// Create timestamp for folder name (For folder naming)
const now = new Date();
const timestamp = now.getFullYear() + '-' + // 使用完整年份
(now.getMonth() + 1).toString().padStart(2, '0') + '-' +
now.getDate().toString().padStart(2, '0') + ' ' +
now.getHours().toString().padStart(2, '0') + '.' +
now.getMinutes().toString().padStart(2, '0') + '.' +
now.getSeconds().toString().padStart(2, '0');
// Initialize or get script settings for card location
let settings = ea.getScriptSettings();
@@ -585,6 +584,11 @@ const defaultSettings = {
value: "#ffd700",
description: "Color used to highlight the target mask in 'Hide All, Guess One' mode (e.g., #ffd700 for gold, #ff0000 for red)",
valueset: [] // Empty array allows free text input
},
"Generate Images No Matter What": {
value: "no",
description: "Always generate images even when template selection is cancelled (yes/no)",
valueset: ["yes", "no"]
}
};
@@ -732,7 +736,7 @@ const getImageFolder = (imageName, timestamp) => {
const normalizedBase = baseFolder
.replace(/\\/g, '/')
.replace(/\/+$/, '');
return `${normalizedBase}/${imageName}-${timestamp}`;
return `${normalizedBase}/${imageName}__${timestamp}`;
};
// Function to determine final output folder path based on settings or user choice
@@ -1013,14 +1017,24 @@ const getTemplateFile = async (templates) => {
// Begin card generation process based on selected mode
let counter = 1;
let templateFile = null; // Move templateFile declaration to outer scope
if(mode === "hideAll") {
// Get template selection from user for Hide All mode
const templates = getTemplates();
if (!templates) return;
// Get template file based on settings or user selection
const templateFile = await getTemplateFile(templates);
if (!templateFile) return;
// Only try to get template if templates exist
if (templates) {
// Get template file based on settings or user selection
templateFile = await getTemplateFile(templates);
}
// Check if we should proceed without template
const generateImagesNoMatterWhat = settings["Generate Images No Matter What"]?.value === "yes";
if (!templateFile && !generateImagesNoMatterWhat) {
new Notice("Operation cancelled - no template selected");
return;
}
// Generate cards for each mask in Hide All mode
for(let i = 0; i < masks.length; i++) {
@@ -1040,108 +1054,147 @@ if(mode === "hideAll") {
...mask,
elements: mask.elements.map(el => ({
...el,
strokeWidth: 4, // Thicker border
strokeColor: highlightColor, // Use configured highlight color
strokeStyle: "solid", // Solid line
roughness: 0 // Smooth border
strokeWidth: 4,
strokeColor: highlightColor,
strokeStyle: "solid",
roughness: 0
}))
};
}
// Handle single element masks
return {
...mask,
strokeWidth: 4, // Thicker border
strokeColor: highlightColor, // Use configured highlight color
strokeStyle: "solid", // Solid line
roughness: 0 // Smooth border
strokeWidth: 4,
strokeColor: highlightColor,
strokeStyle: "solid",
roughness: 0
};
}
return mask;
});
// Generate question image with all masks visible
const questionDataURL = await generateMaskedImage(questionMasks, []);
const questionPath = `${imageFolder}/q-${fileTimestamp}.png`;
await app.vault.adapter.writeBinary(
questionPath,
base64ToBinary(questionDataURL)
);
// Generate answer image with one mask hidden and others visible
const dataURL = await generateMaskedImage(visibleMasks, hiddenMasks);
const imagePath = `${imageFolder}/a-${fileTimestamp}.png`;
// Save answer image to disk
await app.vault.adapter.writeBinary(
imagePath,
base64ToBinary(dataURL)
);
if (templateFile || generateImagesNoMatterWhat) {
// Generate question image with all masks visible
const questionDataURL = await generateMaskedImage(questionMasks, []);
const questionPath = `${imageFolder}/q-${fileTimestamp}.png`;
await app.vault.adapter.writeBinary(
questionPath,
base64ToBinary(questionDataURL)
);
// Generate answer image with one mask hidden and others visible
const dataURL = await generateMaskedImage(visibleMasks, hiddenMasks);
const imagePath = `${imageFolder}/a-${fileTimestamp}.png`;
// Save answer image to disk
await app.vault.adapter.writeBinary(
imagePath,
base64ToBinary(dataURL)
);
// Create markdown file with full paths to images
const fullPaths = {
question: app.vault.adapter.getFullPath(questionPath),
answer: app.vault.adapter.getFullPath(imagePath)
};
// Generate card file from template with all necessary information
await createMarkdownFromTemplate(
templateFile,
fileTimestamp,
fullPaths,
sourceFile
);
// Only create markdown file if template was selected
if (templateFile) {
const fullPaths = {
question: app.vault.adapter.getFullPath(questionPath),
answer: app.vault.adapter.getFullPath(imagePath)
};
await createMarkdownFromTemplate(
templateFile,
fileTimestamp,
fullPaths,
sourceFile
);
}
}
}
} else {
} else if(mode === "hideOne") {
// Process Hide One, Guess One mode
const templates = getTemplates();
if (!templates) return;
// Get template file based on settings or user selection
const templateFile = await getTemplateFile(templates);
if (!templateFile) return;
// Only try to get template if templates exist
if (templates) {
templateFile = await getTemplateFile(templates);
}
// Generate common answer image first (all masks hidden)
const commonAnswerTimestamp = getCurrentTimestamp();
const commonAnswerDataURL = await generateMaskedImage([], masks);
const commonAnswerPath = `${imageFolder}/a-${commonAnswerTimestamp}.png`;
await app.vault.adapter.writeBinary(
commonAnswerPath,
base64ToBinary(commonAnswerDataURL)
);
// Get full path for common answer image
const commonAnswerFullPath = app.vault.adapter.getFullPath(commonAnswerPath);
// Check if we should proceed without template
const generateImagesNoMatterWhat = settings["Generate Images No Matter What"]?.value === "yes";
if (!templateFile && !generateImagesNoMatterWhat) {
new Notice("Operation cancelled - no template selected");
return;
}
// Process each mask individually
for(const mask of masks) {
// Set current mask as visible, others as hidden for question
const visibleMasks = masks.filter(m => m !== mask);
const hiddenMasks = [mask];
// Generate unique timestamp for this card
const fileTimestamp = getCurrentTimestamp();
// Generate question image showing only the current mask
const questionDataURL = await generateMaskedImage([mask], visibleMasks);
const questionPath = `${imageFolder}/q-${fileTimestamp}.png`;
if (templateFile || generateImagesNoMatterWhat) {
// Generate common answer image first (all masks hidden)
const commonAnswerTimestamp = getCurrentTimestamp();
const commonAnswerDataURL = await generateMaskedImage([], masks);
const commonAnswerPath = `${imageFolder}/a-${commonAnswerTimestamp}.png`;
await app.vault.adapter.writeBinary(
questionPath,
base64ToBinary(questionDataURL)
commonAnswerPath,
base64ToBinary(commonAnswerDataURL)
);
// Create markdown file with paths to question and answer images
const fullPaths = {
question: app.vault.adapter.getFullPath(questionPath),
answer: commonAnswerFullPath
};
// Generate card file using template and image paths
await createMarkdownFromTemplate(
templateFile,
fileTimestamp,
fullPaths,
sourceFile
);
// Get full path for common answer image
const commonAnswerFullPath = app.vault.adapter.getFullPath(commonAnswerPath);
// Process each mask individually
for(const mask of masks) {
// Set current mask as visible, others as hidden for question
const visibleMasks = masks.filter(m => m !== mask);
const hiddenMasks = [mask];
// Generate unique timestamp for this card
const fileTimestamp = getCurrentTimestamp();
// Generate question image showing only the current mask
const questionDataURL = await generateMaskedImage([mask], visibleMasks);
const questionPath = `${imageFolder}/q-${fileTimestamp}.png`;
await app.vault.adapter.writeBinary(
questionPath,
base64ToBinary(questionDataURL)
);
// Only create markdown file if template was selected
if (templateFile) {
const fullPaths = {
question: app.vault.adapter.getFullPath(questionPath),
answer: commonAnswerFullPath
};
await createMarkdownFromTemplate(
templateFile,
fileTimestamp,
fullPaths,
sourceFile
);
}
}
}
} else if(mode === "deleteFiles") {
try {
const currentFile = app.workspace.getActiveFile();
if (currentFile) {
// Get all batch markers and their folders
const batchMarkersMap = await getBatchMarkersInfo(currentFile);
if (batchMarkersMap.size === 0) {
new Notice("No files found to delete");
return;
}
// ... rest of deleteFiles mode code remains the same ...
}
} catch (error) {
console.error("Error during file deletion:", error);
new Notice("Error occurred during file deletion");
}
}
// Display completion message with number of cards created
new Notice(`Generated ${masks.length} sets of files in ${imageFolder}/`);
// Move completion message inside a try-catch block
try {
if (templateFile || settings["Generate Images No Matter What"]?.value === "yes") {
const messagePrefix = templateFile ? "Generated" : "Generated images only with";
new Notice(`${messagePrefix} ${masks.length} sets of files in ${imageFolder}/`);
}
} catch (error) {
console.error("Error showing completion message:", error);
new Notice("Operation completed with some errors");
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

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

View File

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

View File

@@ -8,18 +8,22 @@
"lib/**/*"
],
"scripts": {
"dev": "cross-env NODE_ENV=development rollup --config rollup.config.js -w",
"dev": "cross-env NODE_ENV=development rollup --config rollup.config.js",
"build": "cross-env NODE_ENV=production rollup --config rollup.config.js",
"lib": "cross-env NODE_ENV=lib rollup --config rollup.config.js",
"code:fix": "eslint --max-warnings=0 --ext .ts,.tsx ./src --fix",
"madge": "madge --circular ."
"madge": "madge --circular .",
"build:mathjax": "cd MathjaxToSVG && npm run build",
"build:all": "npm run build:mathjax && npm run build",
"dev:mathjax": "cd MathjaxToSVG && npm run dev",
"dev:all": "npm run dev:mathjax && npm run dev"
},
"keywords": [],
"author": "",
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@zsviczian/excalidraw": "0.17.6-17",
"@zsviczian/excalidraw": "0.17.6-20",
"chroma-js": "^2.4.2",
"clsx": "^2.0.0",
"@zsviczian/colormaster": "^1.2.2",

View File

@@ -19,6 +19,8 @@ const isProd = (process.env.NODE_ENV === "production");
const isLib = (process.env.NODE_ENV === "lib");
console.log(`Running: ${process.env.NODE_ENV}; isProd: ${isProd}; isLib: ${isLib}`);
const mathjaxtosvg_pkg = isLib ? "" : fs.readFileSync("./MathjaxToSVG/dist/index.js", "utf8");
const excalidraw_pkg = isLib ? "" : isProd
? fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/excalidraw.production.min.js", "utf8")
: fs.readFileSync("./node_modules/@zsviczian/excalidraw/dist/excalidraw.development.js", "utf8");
@@ -48,7 +50,9 @@ if (!isLib) {
const manifestStr = isLib ? "" : fs.readFileSync("manifest.json", "utf-8");
const manifest = isLib ? {} : JSON.parse(manifestStr);
if (!isLib) console.log(manifest.version);
if (!isLib) {
console.log(manifest.version);
}
const packageString = isLib
? ""
@@ -56,11 +60,16 @@ const packageString = isLib
'\nlet REACT_PACKAGES = `' +
jsesc(react_pkg + reactdom_pkg, { quotes: 'backtick' }) +
'`;\n' +
'let EXCALIDRAW_PACKAGE = ""; const unpackExcalidraw = () => {EXCALIDRAW_PACKAGE = LZString.decompressFromBase64("' + LZString.compressToBase64(excalidraw_pkg) + '");};\n' +
/* 'let EXCALIDRAW_PACKAGE = `' +
jsesc(excalidraw_pkg, { quotes: 'backtick' }) +
'`;\n' +*/
'const unpackExcalidraw = () => LZString.decompressFromBase64("' + LZString.compressToBase64(excalidraw_pkg) + '");\n' +
'let {react, reactDOM } = window.eval.call(window, `(function() {' + '${REACT_PACKAGES};' + 'return {react: React, reactDOM: ReactDOM};})();`);\n' +
`let excalidrawLib = {};\n` +
'let excalidrawLib = {};\n' +
'const loadMathjaxToSVG = () => window.eval.call(window, `(function() {' +
'${LZString.decompressFromBase64("' + LZString.compressToBase64(mathjaxtosvg_pkg) + '")}' +
'return MathjaxToSVG;})();`);\n' +
'const PLUGIN_VERSION="' + manifest.version + '";';
const BASE_CONFIG = {
input: 'src/main.ts',

View File

@@ -6,13 +6,16 @@ import {
EditorSuggestTriggerInfo,
TFile,
} from "obsidian";
import { FRONTMATTER_KEYS_INFO } from "./SuggesterInfo";
import { FRONTMATTER_KEYS_INFO } from "../../dialogs/SuggesterInfo";
import {
EXCALIDRAW_AUTOMATE_INFO,
EXCALIDRAW_SCRIPTENGINE_INFO,
} from "./SuggesterInfo";
import type ExcalidrawPlugin from "../main";
} from "../../dialogs/SuggesterInfo";
import type ExcalidrawPlugin from "../../main";
/**
* The field suggester recommends document properties in source mode, ea and utils function and attribute names.
*/
export class FieldSuggester extends EditorSuggest<string> {
plugin: ExcalidrawPlugin;
suggestType: "ea" | "excalidraw" | "utils";

View File

@@ -0,0 +1,142 @@
import {
FuzzyMatch,
TFile,
CachedMetadata,
TextComponent,
App,
setIcon,
} from "obsidian";
import { SuggestionModal } from "./SuggestionModal";
import { t } from "src/lang/helpers";
import { LinkSuggestion } from "src/types/types";
import ExcalidrawPlugin from "src/main";
import { AUDIO_TYPES, CODE_TYPES, ICON_NAME, IMAGE_TYPES, VIDEO_TYPES } from "src/constants/constants";
export class FileSuggestionModal extends SuggestionModal<LinkSuggestion> {
text: TextComponent;
cache: CachedMetadata;
filesAndAliases: LinkSuggestion[];
file: TFile;
constructor(app: App, input: TextComponent, items: TFile[], private plugin: ExcalidrawPlugin) {
const filesAndAliases = [];
for (const file of items) {
const path = file.path;
filesAndAliases.push({ file, path, alias: "" });
const metadata = app.metadataCache.getFileCache(file); // Get metadata for the file
const aliases = metadata?.frontmatter?.aliases || []; // Check for frontmatter aliases
for (const alias of aliases) {
if(!alias) continue; // Skip empty aliases
filesAndAliases.push({ file, path, alias });
}
}
super(app, input.inputEl, filesAndAliases);
this.limit = 20;
this.filesAndAliases = filesAndAliases;
this.text = input;
this.suggestEl.style.maxWidth = "100%";
this.suggestEl.style.width = `${input.inputEl.clientWidth}px`;
this.inputEl.addEventListener("input", () => this.getFile());
this.setPlaceholder(t("SELECT_FILE_TO_INSERT"));
this.emptyStateText = t("NO_MATCH");
}
getFile() {
const v = this.inputEl.value;
const file = this.app.vault.getAbstractFileByPath(v);
if (file === this.file) {
return;
}
if (!(file instanceof TFile)) {
return;
}
this.file = file;
this.onInputChanged();
}
getSelectedItem() {
return this.file;
}
getItemText(item: LinkSuggestion) {
return `${item.file.path}${item.alias ? `|${item.alias}` : ""}`;
}
onChooseItem(item: LinkSuggestion) {
this.file = item.file;
this.text.setValue(this.getItemText(item));
this.text.onChanged();
}
selectSuggestion({ item }: FuzzyMatch<LinkSuggestion>) {
this.file = item.file;
this.text.setValue(this.getItemText(item));
this.onClose();
this.text.onChanged();
this.close();
}
renderSuggestion(result: FuzzyMatch<LinkSuggestion>, itemEl: HTMLElement) {
const { item, match: matches } = result || {};
itemEl.addClass("mod-complex");
const contentEl = itemEl.createDiv("suggestion-content");
const auxEl = itemEl.createDiv("suggestion-aux");
const titleEl = contentEl.createDiv("suggestion-title");
const noteEl = contentEl.createDiv("suggestion-note");
//el.style.flexDirection = "column";
//content.style.flexDirection = "initial";
if (!item) {
titleEl.setText(this.emptyStateText);
itemEl.addClass("is-selected");
return;
}
const path = item.file?.path ?? item.path;
const pathLength = path.length - item.file.name.length;
const matchElements = matches.matches.map((m) => {
return createSpan("suggestion-highlight");
});
const itemText = this.getItemText(item);
for (let i = pathLength; i < itemText.length; i++) {
const match = matches.matches.find((m) => m[0] === i);
if (match) {
const element = matchElements[matches.matches.indexOf(match)];
titleEl.appendChild(element);
element.appendText(itemText.substring(match[0], match[1]));
i += match[1] - match[0] - 1;
continue;
}
titleEl.appendText(itemText[i]);
}
noteEl.setText(path);
if(this.plugin.isExcalidrawFile(item.file)) {
setIcon(auxEl, ICON_NAME);
} else if (item.file.extension === "md") {
setIcon(auxEl, "square-pen");
} else if (IMAGE_TYPES.includes(item.file.extension)) {
setIcon(auxEl, "image");
} else if (VIDEO_TYPES.includes(item.file.extension)) {
setIcon(auxEl, "monitor-play");
} else if (AUDIO_TYPES.includes(item.file.extension)) {
setIcon(auxEl, "file-audio");
} else if (CODE_TYPES.includes(item.file.extension)) {
setIcon(auxEl, "file-code");
} else if (item.file.extension === "canvas") {
setIcon(auxEl, "layout-dashboard");
} else if (item.file.extension === "pdf") {
setIcon(auxEl, "book-open-text");
} else {
auxEl.setText(item.file.extension);
}
}
getItems() {
return this.filesAndAliases;
}
}

View File

@@ -0,0 +1,87 @@
import {
FuzzyMatch,
CachedMetadata,
TextComponent,
App,
TFolder,
} from "obsidian";
import { SuggestionModal } from "./SuggestionModal";
export class FolderSuggestionModal extends SuggestionModal<TFolder> {
text: TextComponent;
cache: CachedMetadata;
folders: TFolder[];
folder: TFolder;
constructor(app: App, input: TextComponent, items: TFolder[]) {
super(app, input.inputEl, items);
this.folders = [...items];
this.text = input;
this.inputEl.addEventListener("input", () => this.getFolder());
}
getFolder() {
const v = this.inputEl.value;
const folder = this.app.vault.getAbstractFileByPath(v);
if (folder == this.folder) {
return;
}
if (!(folder instanceof TFolder)) {
return;
}
this.folder = folder;
this.onInputChanged();
}
getItemText(item: TFolder) {
return item.path;
}
onChooseItem(item: TFolder) {
this.text.setValue(item.path);
this.folder = item;
}
selectSuggestion({ item }: FuzzyMatch<TFolder>) {
const link = item.path;
this.text.setValue(link);
this.onClose();
this.close();
}
renderSuggestion(result: FuzzyMatch<TFolder>, el: HTMLElement) {
const { item, match: matches } = result || {};
const content = el.createDiv({
cls: "suggestion-content",
});
if (!item) {
content.setText(this.emptyStateText);
content.parentElement.addClass("is-selected");
return;
}
const pathLength = item.path.length - item.name.length;
const matchElements = matches.matches.map((m) => {
return createSpan("suggestion-highlight");
});
for (let i = pathLength; i < item.path.length; i++) {
const match = matches.matches.find((m) => m[0] === i);
if (match) {
const element = matchElements[matches.matches.indexOf(match)];
content.appendChild(element);
element.appendText(item.path.substring(match[0], match[1]));
i += match[1] - match[0] - 1;
continue;
}
content.appendText(item.path[i]);
}
el.createDiv({
cls: "suggestion-note",
text: item.path,
});
}
getItems() {
return this.folders;
}
}

View File

@@ -0,0 +1,163 @@
import {
FuzzyMatch,
TFile,
BlockCache,
HeadingCache,
CachedMetadata,
TextComponent,
App,
} from "obsidian";
import { SuggestionModal } from "./SuggestionModal";
export class PathSuggestionModal extends SuggestionModal<
TFile | BlockCache | HeadingCache
> {
file: TFile;
files: TFile[];
text: TextComponent;
cache: CachedMetadata;
constructor(app: App, input: TextComponent, items: TFile[]) {
super(app, input.inputEl, items);
this.files = [...items];
this.text = input;
//this.getFile();
this.inputEl.addEventListener("input", this.getFile.bind(this));
}
getFile() {
const v = this.inputEl.value;
const file = this.app.metadataCache.getFirstLinkpathDest(
v.split(/[\^#]/).shift() || "",
"",
);
if (file == this.file) {
return;
}
this.file = file;
if (this.file) {
this.cache = this.app.metadataCache.getFileCache(this.file);
}
this.onInputChanged();
}
getItemText(item: TFile | HeadingCache | BlockCache) {
if (item instanceof TFile) {
return item.path;
}
if (Object.prototype.hasOwnProperty.call(item, "heading")) {
return (<HeadingCache>item).heading;
}
if (Object.prototype.hasOwnProperty.call(item, "id")) {
return (<BlockCache>item).id;
}
}
onChooseItem(item: TFile | HeadingCache | BlockCache) {
if (item instanceof TFile) {
this.text.setValue(item.basename);
this.file = item;
this.cache = this.app.metadataCache.getFileCache(this.file);
} else if (Object.prototype.hasOwnProperty.call(item, "heading")) {
this.text.setValue(
`${this.file.basename}#${(<HeadingCache>item).heading}`,
);
} else if (Object.prototype.hasOwnProperty.call(item, "id")) {
this.text.setValue(`${this.file.basename}^${(<BlockCache>item).id}`);
}
}
selectSuggestion({ item }: FuzzyMatch<TFile | BlockCache | HeadingCache>) {
let link: string;
if (item instanceof TFile) {
link = item.basename;
} else if (Object.prototype.hasOwnProperty.call(item, "heading")) {
link = `${this.file.basename}#${(<HeadingCache>item).heading}`;
} else if (Object.prototype.hasOwnProperty.call(item, "id")) {
link = `${this.file.basename}^${(<BlockCache>item).id}`;
}
this.text.setValue(link);
this.onClose();
this.close();
}
renderSuggestion(
result: FuzzyMatch<TFile | BlockCache | HeadingCache>,
el: HTMLElement,
) {
const { item, match: matches } = result || {};
const content = el.createDiv({
cls: "suggestion-content",
});
if (!item) {
content.setText(this.emptyStateText);
content.parentElement.addClass("is-selected");
return;
}
if (item instanceof TFile) {
const pathLength = item.path.length - item.name.length;
const matchElements = matches.matches.map((m) => {
return createSpan("suggestion-highlight");
});
for (
let i = pathLength;
i < item.path.length - item.extension.length - 1;
i++
) {
const match = matches.matches.find((m) => m[0] === i);
if (match) {
const element = matchElements[matches.matches.indexOf(match)];
content.appendChild(element);
element.appendText(item.path.substring(match[0], match[1]));
i += match[1] - match[0] - 1;
continue;
}
content.appendText(item.path[i]);
}
el.createDiv({
cls: "suggestion-note",
text: item.path,
});
} else if (Object.prototype.hasOwnProperty.call(item, "heading")) {
content.setText((<HeadingCache>item).heading);
content.prepend(
createSpan({
cls: "suggestion-flair",
text: `H${(<HeadingCache>item).level}`,
}),
);
} else if (Object.prototype.hasOwnProperty.call(item, "id")) {
content.setText((<BlockCache>item).id);
}
}
get headings() {
if (!this.file) {
return [];
}
if (!this.cache) {
this.cache = this.app.metadataCache.getFileCache(this.file);
}
return this.cache.headings || [];
}
get blocks() {
if (!this.file) {
return [];
}
if (!this.cache) {
this.cache = this.app.metadataCache.getFileCache(this.file);
}
return Object.values(this.cache.blocks || {}) || [];
}
getItems() {
const v = this.inputEl.value;
if (/#/.test(v)) {
this.modifyInput = (i) => i.split(/#/).pop();
return this.headings;
} else if (/\^/.test(v)) {
this.modifyInput = (i) => i.split(/\^/).pop();
return this.blocks;
}
return this.files;
}
}

View File

@@ -0,0 +1,119 @@
import {
SuggestModal,
Scope,
} from "obsidian";
export class Suggester<T> {
owner: SuggestModal<T>;
items: T[];
suggestions: HTMLDivElement[];
selectedItem: number;
containerEl: HTMLElement;
constructor(owner: SuggestModal<T>, containerEl: HTMLElement, scope: Scope) {
this.containerEl = containerEl;
this.owner = owner;
containerEl.on(
"click",
".suggestion-item",
this.onSuggestionClick.bind(this),
);
containerEl.on(
"mousemove",
".suggestion-item",
this.onSuggestionMouseover.bind(this),
);
scope.register([], "ArrowUp", () => {
this.setSelectedItem(this.selectedItem - 1, true);
return false;
});
scope.register([], "ArrowDown", () => {
this.setSelectedItem(this.selectedItem + 1, true);
return false;
});
scope.register([], "Enter", (evt) => {
this.useSelectedItem(evt);
return false;
});
scope.register([], "Tab", (evt) => {
this.chooseSuggestion(evt);
return false;
});
}
chooseSuggestion(evt: KeyboardEvent) {
if (!this.items || !this.items.length) {
return;
}
const currentValue = this.items[this.selectedItem];
if (currentValue) {
this.owner.onChooseSuggestion(currentValue, evt);
}
}
onSuggestionClick(event: MouseEvent, el: HTMLDivElement): void {
event.preventDefault();
if (!this.suggestions || !this.suggestions.length) {
return;
}
const item = this.suggestions.indexOf(el);
this.setSelectedItem(item, false);
this.useSelectedItem(event);
}
onSuggestionMouseover(event: MouseEvent, el: HTMLDivElement): void {
if (!this.suggestions || !this.suggestions.length) {
return;
}
const item = this.suggestions.indexOf(el);
this.setSelectedItem(item, false);
}
empty() {
this.containerEl.empty();
}
setSuggestions(items: T[]) {
this.containerEl.empty();
const els: HTMLDivElement[] = [];
items.forEach((item) => {
const suggestionEl = this.containerEl.createDiv("suggestion-item");
this.owner.renderSuggestion(item, suggestionEl);
els.push(suggestionEl);
});
this.items = items;
this.suggestions = els;
this.setSelectedItem(0, false);
}
useSelectedItem(event: MouseEvent | KeyboardEvent) {
if (!this.items || !this.items.length) {
return;
}
const currentValue = this.items[this.selectedItem];
if (currentValue) {
this.owner.selectSuggestion(currentValue, event);
}
}
wrap(value: number, size: number): number {
return ((value % size) + size) % size;
}
setSelectedItem(index: number, scroll: boolean) {
const nIndex = this.wrap(index, this.suggestions.length);
const prev = this.suggestions[this.selectedItem];
const next = this.suggestions[nIndex];
if (prev) {
prev.removeClass("is-selected");
}
if (next) {
next.addClass("is-selected");
}
this.selectedItem = nIndex;
if (scroll) {
next.scrollIntoView(false);
}
}
}

View File

@@ -0,0 +1,128 @@
import {
FuzzyMatch,
App,
FuzzySuggestModal,
Scope,
} from "obsidian";
import { createPopper, Instance as PopperInstance } from "@popperjs/core";
import { Suggester } from "./Suggester";
export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
items: T[] = [];
suggestions: HTMLDivElement[];
popper: WeakRef<PopperInstance>;
//@ts-ignore
scope: Scope = new Scope(this.app.scope);
suggester: Suggester<FuzzyMatch<T>>;
suggestEl: HTMLDivElement;
promptEl: HTMLDivElement;
emptyStateText: string = "No match found";
limit: number = 100;
shouldNotOpen: boolean;
constructor(app: App, inputEl: HTMLInputElement, items: T[]) {
super(app);
this.inputEl = inputEl;
this.items = items;
this.suggestEl = createDiv("suggestion-container");
this.contentEl = this.suggestEl.createDiv("suggestion");
this.suggester = new Suggester(this, this.contentEl, this.scope);
this.scope.register([], "Escape", this.onEscape.bind(this));
this.inputEl.addEventListener("input", this.onInputChanged.bind(this));
this.inputEl.addEventListener("focus", this.onFocus.bind(this));
this.inputEl.addEventListener("blur", this.close.bind(this));
this.suggestEl.on(
"mousedown",
".suggestion-container",
(event: MouseEvent) => {
event.preventDefault();
},
);
}
empty() {
this.suggester.empty();
}
onInputChanged(): void {
if (this.shouldNotOpen) {
return;
}
const inputStr = this.modifyInput(this.inputEl.value);
const suggestions = this.getSuggestions(inputStr);
if (suggestions.length > 0) {
this.suggester.setSuggestions(suggestions.slice(0, this.limit));
} else {
this.onNoSuggestion();
}
this.open();
}
onFocus(): void {
this.shouldNotOpen = false;
this.onInputChanged();
}
modifyInput(input: string): string {
return input;
}
onNoSuggestion() {
this.empty();
this.renderSuggestion(null, this.contentEl.createDiv("suggestion-item"));
}
open(): void {
// TODO: Figure out a better way to do this. Idea from Periodic Notes plugin
this.app.keymap.pushScope(this.scope);
this.inputEl.ownerDocument.body.appendChild(this.suggestEl);
this.popper = new WeakRef(createPopper(this.inputEl, this.suggestEl, {
placement: "bottom-start",
modifiers: [
{
name: "offset",
options: {
offset: [0, 10],
},
},
{
name: "flip",
options: {
fallbackPlacements: ["top"],
},
},
],
}));
}
onEscape(): void {
this.close();
this.shouldNotOpen = true;
}
close(): void {
// TODO: Figure out a better way to do this. Idea from Periodic Notes plugin
this.app.keymap.popScope(this.scope);
this.suggester.setSuggestions([]);
if (this.popper?.deref()) {
this.popper.deref().destroy();
}
this.inputEl.removeEventListener("input", this.onInputChanged.bind(this));
this.inputEl.removeEventListener("focus", this.onFocus.bind(this));
this.inputEl.removeEventListener("blur", this.close.bind(this));
this.suggestEl.detach();
}
createPrompt(prompts: HTMLSpanElement[]) {
if (!this.promptEl) {
this.promptEl = this.suggestEl.createDiv("prompt-instructions");
}
const prompt = this.promptEl.createDiv("prompt-instruction");
for (const p of prompts) {
prompt.appendChild(p);
}
}
abstract onChooseItem(item: T, evt: MouseEvent | KeyboardEvent): void;
abstract getItemText(arg: T): string;
abstract getItems(): T[];
}

View File

@@ -701,7 +701,11 @@ export class EmbeddedFilesLoader {
return;
}
const data = getMermaidText(element);
const result = await mermaidToExcalidraw(data, {fontSize: 20}, true);
const result = await mermaidToExcalidraw(
data,
{ themeVariables: { fontSize: "20" } },
true
);
if(!result) {
return;
}
@@ -767,7 +771,7 @@ export class EmbeddedFilesLoader {
}, 1200);
const iterator = loadIterator.bind(this)();
const concurency = 5;
const concurency = 3;
await new PromisePool(iterator, concurency).all();
clearInterval(addFilesTimer);

View File

@@ -1489,7 +1489,12 @@ export class ExcalidrawAutomate {
diagram: string,
groupElements: boolean = true,
): Promise<string[]|string> {
const result = await mermaidToExcalidraw(diagram, {fontSize: this.style.fontSize});
const result = await mermaidToExcalidraw(
diagram, {
themeVariables: {fontSize: `${this.style.fontSize}`},
flowchart: {curve: this.style.roundness===null ? "linear" : "basis"},
}
);
const ids:string[] = [];
if(!result) return null;
if(result?.error) return result.error;

View File

@@ -6,7 +6,38 @@ import { FontMetadata } from "@zsviczian/excalidraw/types/excalidraw/fonts/metad
import { AppState, BinaryFiles, DataURL, GenerateDiagramToCode, Zoom } from "@zsviczian/excalidraw/types/excalidraw/types";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { GlobalPoint } from "@zsviczian/excalidraw/types/math/types";
import ExcalidrawPlugin from "./main";
interface MermaidConfig {
/**
* Whether to start the diagram automatically when the page loads.
* @default false
*/
startOnLoad?: boolean;
/**
* The flowchart curve style.
* @default "linear"
*/
flowchart?: {
curve?: "linear" | "basis";
};
/**
* Theme variables
* @default { fontSize: "25px" }
*/
themeVariables?: {
fontSize?: string;
};
/**
* Maximum number of edges to be rendered.
* @default 1000
*/
maxEdges?: number;
/**
* Maximum number of characters to be rendered.
* @default 1000
*/
maxTextSize?: number;
}
type EmbeddedLink =
| ({
@@ -159,7 +190,7 @@ declare namespace ExcalidrawLib {
function mermaidToExcalidraw(
mermaidDefinition: string,
opts: {fontSize: number},
opts: MermaidConfig,
forceSVG?: boolean,
): Promise<{
elements?: ExcalidrawElement[];

View File

@@ -3267,6 +3267,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
markdownlink: string,
path: string,
alias: string,
originalLink?: string,
) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addLink, "ExcalidrawView.addLink", markdownlink, path, alias);
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
@@ -3280,16 +3281,28 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
}
const selectedElementId = Object.keys(api.getAppState().selectedElementIds)[0];
const selectedElement = api.getSceneElements().find(el=>el.id === selectedElementId);
if(!selectedElement || (selectedElement && selectedElement.link !== null)) {
if(!selectedElement || (!Boolean(originalLink) && (selectedElement && selectedElement.link !== null) )) {
if(selectedElement) new Notice("Selected element already has a link. Inserting link as text.");
this.addText(markdownlink);
return;
}
const ea = getEA(this) as ExcalidrawAutomate;
ea.copyViewElementsToEAforEditing([selectedElement]);
if(originalLink?.match(/\[\[(.*?)\]\]/)?.[1]) {
markdownlink = originalLink.replace(/(\[\[.*?\]\])/,markdownlink);
}
ea.getElement(selectedElementId).link = markdownlink;
await ea.addElementsToView(false, true);
ea.destroy();
if(Boolean(originalLink)) {
this.updateScene({
appState: {
showHyperlinkPopup: {
newValue : "info", oldValue : "editor"
}
}
});
}
}
public async addText (
@@ -5094,6 +5107,15 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
new Notice("Image successfully converted to local file");
}
private insertLinkAction(linkVal: string) {
let link = linkVal.match(/\[\[(.*?)\]\]/)?.[1];
if(!link) {
link = linkVal.replaceAll("[","").replaceAll("]","");
link = link.split("|")[0].trim();
}
this.plugin.insertLinkDialog.start(this.file.path, (markdownlink: string, path:string, alias:string) => this.addLink(markdownlink, path, alias, linkVal), link);
}
private onContextMenu(elements: readonly ExcalidrawElement[], appState: AppState, onClose: (callback?: () => void) => void) {
const React = this.packages.react;
const contextMenuActions = [];
@@ -5916,6 +5938,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
renderEmbeddable: this.renderEmbeddable.bind(this),
renderMermaid: shouldRenderMermaid,
showDeprecatedFonts: true,
insertLinkAction: this.insertLinkAction.bind(this),
},
this.renderCustomActionsMenu(),
this.renderWelcomeScreen(),

View File

@@ -1,18 +1,22 @@
// LaTeX.ts
import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
import {mathjax} from "mathjax-full/js/mathjax";
import {TeX} from 'mathjax-full/js/input/tex.js';
import {SVG} from 'mathjax-full/js/output/svg.js';
import {LiteAdaptor, liteAdaptor} from 'mathjax-full/js/adaptors/liteAdaptor.js';
import {RegisterHTMLHandler} from 'mathjax-full/js/handlers/html.js';
import {AllPackages} from 'mathjax-full/js/input/tex/AllPackages.js';
import ExcalidrawView from "./ExcalidrawView";
import { FileData, MimeType } from "./EmbeddedFileLoader";
import { FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { getImageSize, svgToBase64 } from "./utils/Utils";
import { fileid } from "./constants/constants";
import { TFile } from "obsidian";
import { MathDocument } from "mathjax-full/js/core/MathDocument";
declare const loadMathjaxToSVG: Function;
let mathjaxLoaded = false;
let tex2dataURLExternal: Function;
let clearVariables: Function;
const loadMathJax = async () => {
if (!mathjaxLoaded) {
const module = await loadMathjaxToSVG();
tex2dataURLExternal = module.tex2dataURL;
clearVariables = module.clearMathJaxVariables;
mathjaxLoaded = true;
}
};
export const updateEquation = async (
equation: string,
@@ -20,13 +24,14 @@ export const updateEquation = async (
view: ExcalidrawView,
addFiles: Function,
) => {
const data = await tex2dataURL(equation);
await loadMathJax();
const data = await tex2dataURLExternal(equation, 4, app);
if (data) {
const files: FileData[] = [];
files.push({
mimeType: data.mimeType,
mimeType: data.mimeType as MimeType,
id: fileId as FileId,
dataURL: data.dataURL,
dataURL: data.dataURL as DataURL,
created: data.created,
size: data.size,
hasSVGwithBitmap: false,
@@ -36,27 +41,9 @@ export const updateEquation = async (
}
};
let adaptor: LiteAdaptor;
let html: MathDocument<any, any, any>;
let preamble: string;
export const clearMathJaxVariables = () => {
adaptor = null;
html = null;
preamble = null;
};
//https://github.com/xldenis/obsidian-latex/blob/master/main.ts
const loadPreamble = async () => {
const file = app.vault.getAbstractFileByPath("preamble.sty");
preamble = file && file instanceof TFile
? await app.vault.read(file)
: null;
};
export async function tex2dataURL(
tex: string,
scale: number = 4 // Default scale value, adjust as needed
scale: number = 4
): Promise<{
mimeType: MimeType;
fileId: FileId;
@@ -64,47 +51,12 @@ export async function tex2dataURL(
created: number;
size: { height: number; width: number };
}> {
let input: TeX<unknown, unknown, unknown>;
let output: SVG<unknown, unknown, unknown>;
await loadMathJax();
return tex2dataURLExternal(tex, scale, app);
}
if(!adaptor) {
await loadPreamble();
adaptor = liteAdaptor();
RegisterHTMLHandler(adaptor);
input = new TeX({
packages: AllPackages,
...Boolean(preamble) ? {
inlineMath: [['$', '$']],
displayMath: [['$$', '$$']]
} : {},
});
output = new SVG({ fontCache: "local" });
html = mathjax.document("", { InputJax: input, OutputJax: output });
export const clearMathJaxVariables = () => {
if (clearVariables) {
clearVariables();
}
try {
const node = html.convert(
Boolean(preamble) ? `${preamble}${tex}` : tex,
{ display: true, scale }
);
const svg = new DOMParser().parseFromString(adaptor.innerHTML(node), "image/svg+xml").firstChild as SVGSVGElement;
if (svg) {
if(svg.width.baseVal.valueInSpecifiedUnits < 2) {
svg.width.baseVal.valueAsString = `${(svg.width.baseVal.valueInSpecifiedUnits+1).toFixed(3)}ex`;
}
const img = svgToBase64(svg.outerHTML);
svg.width.baseVal.valueAsString = (svg.width.baseVal.valueInSpecifiedUnits * 10).toFixed(3);
svg.height.baseVal.valueAsString = (svg.height.baseVal.valueInSpecifiedUnits * 10).toFixed(3);
const dataURL = svgToBase64(svg.outerHTML);
return {
mimeType: "image/svg+xml",
fileId: fileid() as FileId,
dataURL: dataURL as DataURL,
created: Date.now(),
size: await getImageSize(img),
};
}
} catch (e) {
console.error(e);
}
return null;
}
};

View File

@@ -195,6 +195,10 @@ export const ANIMATED_IMAGE_TYPES = ["gif", "webp", "apng", "svg"];
export const EXPORT_TYPES = ["svg", "dark.svg", "light.svg", "png", "dark.png", "light.png"];
export const MAX_IMAGE_SIZE = 500;
export const VIDEO_TYPES = ["mp4", "webm", "ogv", "mov", "mkv"];
export const AUDIO_TYPES = ["mp3", "wav", "m4a", "3gp", "flac", "ogg", "oga", "opus"];
export const CODE_TYPES = ["json", "css", "js"];
export const FRONTMATTER_KEYS:{[key:string]: {name: string, type: string, depricated?:boolean}} = {
"plugin": {name: "excalidraw-plugin", type: "text"},
"export-transparent": {name: "excalidraw-export-transparent", type: "checkbox"},

View File

@@ -265,20 +265,20 @@ function RenderObsidianView(
const color = element?.backgroundColor
? (element.backgroundColor.toLowerCase() === "transparent"
? "transparent"
: ea.getCM(element.backgroundColor).alphaTo(opacity).stringHEX())
: ea.getCM(element.backgroundColor).alphaTo(opacity).stringHEX({alpha: true}))
: "transparent";
color === "transparent" ? canvasNode?.addClass("transparent") : canvasNode?.removeClass("transparent");
canvasNode?.style.setProperty("--canvas-background", color);
canvasNode?.style.setProperty("--background-primary", color);
canvasNodeContainer?.style.setProperty("background-color", color);
} else if (!(mdProps?.backgroundMatchElement ?? true )) {
} else if (!(mdProps.backgroundMatchElement ?? true )) {
const opacity = (mdProps.backgroundOpacity??100)/100;
const color = mdProps.backgroundMatchCanvas
? (canvasColor.toLowerCase() === "transparent"
? "transparent"
: ea.getCM(canvasColor).alphaTo(opacity).stringHEX())
: ea.getCM(mdProps.backgroundColor).alphaTo((mdProps.backgroundOpacity??100)/100).stringHEX();
: ea.getCM(canvasColor).alphaTo(opacity).stringHEX({alpha: true}))
: ea.getCM(mdProps.backgroundColor).alphaTo((mdProps.backgroundOpacity??100)/100).stringHEX({alpha: true});
color === "transparent" ? canvasNode?.addClass("transparent") : canvasNode?.removeClass("transparent");
canvasNode?.style.setProperty("--canvas-background", color);
@@ -291,13 +291,13 @@ function RenderObsidianView(
const color = element?.strokeColor
? (element.strokeColor.toLowerCase() === "transparent"
? "transparent"
: ea.getCM(element.strokeColor).alphaTo(opacity).stringHEX())
: ea.getCM(element.strokeColor).alphaTo(opacity).stringHEX({alpha: true}))
: "transparent";
canvasNode?.style.setProperty("--canvas-border", color);
canvasNode?.style.setProperty("--canvas-color", color);
//canvasNodeContainer?.style.setProperty("border-color", color);
} else if(!(mdProps?.borderMatchElement ?? true)) {
const color = ea.getCM(mdProps.borderColor).alphaTo((mdProps.borderOpacity??100)/100).stringHEX();
const color = ea.getCM(mdProps.borderColor).alphaTo((mdProps.borderOpacity??100)/100).stringHEX({alpha: true});
canvasNode?.style.setProperty("--canvas-border", color);
canvasNode?.style.setProperty("--canvas-color", color);
//canvasNodeContainer?.style.setProperty("border-color", color);
@@ -315,8 +315,16 @@ function RenderObsidianView(
const canvasNode = containerRef.current;
if(!canvasNode.hasClass("canvas-node")) return;
setColors(canvasNode, element, mdProps, canvasColor);
console.log("Setting colors");
}, [
mdProps,
mdProps?.useObsidianDefaults,
mdProps?.backgroundMatchCanvas,
mdProps?.backgroundMatchElement,
mdProps?.backgroundColor,
mdProps?.backgroundOpacity,
mdProps?.borderMatchElement,
mdProps?.borderColor,
mdProps?.borderOpacity,
elementRef.current,
containerRef.current,
canvasColor,
@@ -395,7 +403,8 @@ function RenderObsidianView(
const previousIsActive = isActiveRef.current;
isActiveRef.current = (activeEmbeddable?.element.id === element.id) && (activeEmbeddable?.state === "active");
const node = leafRef.current?.node as ObsidianCanvasNode;
if (previousIsActive === isActiveRef.current) {
return;
}
@@ -414,15 +423,15 @@ function RenderObsidianView(
isEditingRef.current = false;
return;
}
} else if (leafRef.current?.node) {
} else if (node) {
//Handle canvas node
if(view.plugin.settings.markdownNodeOneClickEditing && !containerRef.current?.hasClass("is-editing")) {
if(isActiveRef.current && view.plugin.settings.markdownNodeOneClickEditing && !containerRef.current?.hasClass("is-editing")) { //!node.isEditing
const newTheme = getTheme(view, themeRef.current);
containerRef.current?.addClasses(["is-editing", "is-focused"]);
view.canvasNodeFactory.startEditing(leafRef.current.node, newTheme);
view.canvasNodeFactory.startEditing(node, newTheme);
} else {
containerRef.current?.removeClasses(["is-editing", "is-focused"]);
view.canvasNodeFactory.stopEditing(leafRef.current.node);
view.canvasNodeFactory.stopEditing(node);
}
}
}, [

View File

@@ -46,6 +46,16 @@ export class EmbeddalbeMDFileCustomDataSettingsComponent {
);
}
contentEl.createEl("h4",{text: t("ES_BACKGROUND_HEAD")});
const descDiv = contentEl.createDiv({ cls: "excalidraw-setting-desc" });
descDiv.textContent = t("ES_BACKGROUND_DESC_INFO");
descDiv.addEventListener("click", () => {
if (descDiv.textContent === t("ES_BACKGROUND_DESC_INFO")) {
descDiv.textContent = t("ES_BACKGROUND_DESC_DETAIL");
} else {
descDiv.textContent = t("ES_BACKGROUND_DESC_INFO");
}
});
let bgSetting: Setting;
let bgMatchElementToggle: ToggleComponent;

View File

@@ -1,569 +0,0 @@
import {
FuzzyMatch,
TFile,
BlockCache,
HeadingCache,
CachedMetadata,
TextComponent,
App,
TFolder,
FuzzySuggestModal,
SuggestModal,
Scope,
} from "obsidian";
import { createPopper, Instance as PopperInstance } from "@popperjs/core";
class Suggester<T> {
owner: SuggestModal<T>;
items: T[];
suggestions: HTMLDivElement[];
selectedItem: number;
containerEl: HTMLElement;
constructor(owner: SuggestModal<T>, containerEl: HTMLElement, scope: Scope) {
this.containerEl = containerEl;
this.owner = owner;
containerEl.on(
"click",
".suggestion-item",
this.onSuggestionClick.bind(this),
);
containerEl.on(
"mousemove",
".suggestion-item",
this.onSuggestionMouseover.bind(this),
);
scope.register([], "ArrowUp", () => {
this.setSelectedItem(this.selectedItem - 1, true);
return false;
});
scope.register([], "ArrowDown", () => {
this.setSelectedItem(this.selectedItem + 1, true);
return false;
});
scope.register([], "Enter", (evt) => {
this.useSelectedItem(evt);
return false;
});
scope.register([], "Tab", (evt) => {
this.chooseSuggestion(evt);
return false;
});
}
chooseSuggestion(evt: KeyboardEvent) {
if (!this.items || !this.items.length) {
return;
}
const currentValue = this.items[this.selectedItem];
if (currentValue) {
this.owner.onChooseSuggestion(currentValue, evt);
}
}
onSuggestionClick(event: MouseEvent, el: HTMLDivElement): void {
event.preventDefault();
if (!this.suggestions || !this.suggestions.length) {
return;
}
const item = this.suggestions.indexOf(el);
this.setSelectedItem(item, false);
this.useSelectedItem(event);
}
onSuggestionMouseover(event: MouseEvent, el: HTMLDivElement): void {
if (!this.suggestions || !this.suggestions.length) {
return;
}
const item = this.suggestions.indexOf(el);
this.setSelectedItem(item, false);
}
empty() {
this.containerEl.empty();
}
setSuggestions(items: T[]) {
this.containerEl.empty();
const els: HTMLDivElement[] = [];
items.forEach((item) => {
const suggestionEl = this.containerEl.createDiv("suggestion-item");
this.owner.renderSuggestion(item, suggestionEl);
els.push(suggestionEl);
});
this.items = items;
this.suggestions = els;
this.setSelectedItem(0, false);
}
useSelectedItem(event: MouseEvent | KeyboardEvent) {
if (!this.items || !this.items.length) {
return;
}
const currentValue = this.items[this.selectedItem];
if (currentValue) {
this.owner.selectSuggestion(currentValue, event);
}
}
wrap(value: number, size: number): number {
return ((value % size) + size) % size;
}
setSelectedItem(index: number, scroll: boolean) {
const nIndex = this.wrap(index, this.suggestions.length);
const prev = this.suggestions[this.selectedItem];
const next = this.suggestions[nIndex];
if (prev) {
prev.removeClass("is-selected");
}
if (next) {
next.addClass("is-selected");
}
this.selectedItem = nIndex;
if (scroll) {
next.scrollIntoView(false);
}
}
}
export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
items: T[] = [];
suggestions: HTMLDivElement[];
popper: WeakRef<PopperInstance>;
//@ts-ignore
scope: Scope = new Scope(this.app.scope);
suggester: Suggester<FuzzyMatch<T>>;
suggestEl: HTMLDivElement;
promptEl: HTMLDivElement;
emptyStateText: string = "No match found";
limit: number = 100;
shouldNotOpen: boolean;
constructor(app: App, inputEl: HTMLInputElement, items: T[]) {
super(app);
this.inputEl = inputEl;
this.items = items;
this.suggestEl = createDiv("suggestion-container");
this.contentEl = this.suggestEl.createDiv("suggestion");
this.suggester = new Suggester(this, this.contentEl, this.scope);
this.scope.register([], "Escape", this.onEscape.bind(this));
this.inputEl.addEventListener("input", this.onInputChanged.bind(this));
this.inputEl.addEventListener("focus", this.onFocus.bind(this));
this.inputEl.addEventListener("blur", this.close.bind(this));
this.suggestEl.on(
"mousedown",
".suggestion-container",
(event: MouseEvent) => {
event.preventDefault();
},
);
}
empty() {
this.suggester.empty();
}
onInputChanged(): void {
if (this.shouldNotOpen) {
return;
}
const inputStr = this.modifyInput(this.inputEl.value);
const suggestions = this.getSuggestions(inputStr);
if (suggestions.length > 0) {
this.suggester.setSuggestions(suggestions.slice(0, this.limit));
} else {
this.onNoSuggestion();
}
this.open();
}
onFocus(): void {
this.shouldNotOpen = false;
this.onInputChanged();
}
modifyInput(input: string): string {
return input;
}
onNoSuggestion() {
this.empty();
this.renderSuggestion(null, this.contentEl.createDiv("suggestion-item"));
}
open(): void {
// TODO: Figure out a better way to do this. Idea from Periodic Notes plugin
this.app.keymap.pushScope(this.scope);
this.inputEl.ownerDocument.body.appendChild(this.suggestEl);
this.popper = new WeakRef(createPopper(this.inputEl, this.suggestEl, {
placement: "bottom-start",
modifiers: [
{
name: "offset",
options: {
offset: [0, 10],
},
},
{
name: "flip",
options: {
fallbackPlacements: ["top"],
},
},
],
}));
}
onEscape(): void {
this.close();
this.shouldNotOpen = true;
}
close(): void {
// TODO: Figure out a better way to do this. Idea from Periodic Notes plugin
this.app.keymap.popScope(this.scope);
this.suggester.setSuggestions([]);
if (this.popper?.deref()) {
this.popper.deref().destroy();
}
this.inputEl.removeEventListener("input", this.onInputChanged.bind(this));
this.inputEl.removeEventListener("focus", this.onFocus.bind(this));
this.inputEl.removeEventListener("blur", this.close.bind(this));
this.suggestEl.detach();
}
createPrompt(prompts: HTMLSpanElement[]) {
if (!this.promptEl) {
this.promptEl = this.suggestEl.createDiv("prompt-instructions");
}
const prompt = this.promptEl.createDiv("prompt-instruction");
for (const p of prompts) {
prompt.appendChild(p);
}
}
abstract onChooseItem(item: T, evt: MouseEvent | KeyboardEvent): void;
abstract getItemText(arg: T): string;
abstract getItems(): T[];
}
export class PathSuggestionModal extends SuggestionModal<
TFile | BlockCache | HeadingCache
> {
file: TFile;
files: TFile[];
text: TextComponent;
cache: CachedMetadata;
constructor(app: App, input: TextComponent, items: TFile[]) {
super(app, input.inputEl, items);
this.files = [...items];
this.text = input;
//this.getFile();
this.inputEl.addEventListener("input", this.getFile.bind(this));
}
getFile() {
const v = this.inputEl.value;
const file = this.app.metadataCache.getFirstLinkpathDest(
v.split(/[\^#]/).shift() || "",
"",
);
if (file == this.file) {
return;
}
this.file = file;
if (this.file) {
this.cache = this.app.metadataCache.getFileCache(this.file);
}
this.onInputChanged();
}
getItemText(item: TFile | HeadingCache | BlockCache) {
if (item instanceof TFile) {
return item.path;
}
if (Object.prototype.hasOwnProperty.call(item, "heading")) {
return (<HeadingCache>item).heading;
}
if (Object.prototype.hasOwnProperty.call(item, "id")) {
return (<BlockCache>item).id;
}
}
onChooseItem(item: TFile | HeadingCache | BlockCache) {
if (item instanceof TFile) {
this.text.setValue(item.basename);
this.file = item;
this.cache = this.app.metadataCache.getFileCache(this.file);
} else if (Object.prototype.hasOwnProperty.call(item, "heading")) {
this.text.setValue(
`${this.file.basename}#${(<HeadingCache>item).heading}`,
);
} else if (Object.prototype.hasOwnProperty.call(item, "id")) {
this.text.setValue(`${this.file.basename}^${(<BlockCache>item).id}`);
}
}
selectSuggestion({ item }: FuzzyMatch<TFile | BlockCache | HeadingCache>) {
let link: string;
if (item instanceof TFile) {
link = item.basename;
} else if (Object.prototype.hasOwnProperty.call(item, "heading")) {
link = `${this.file.basename}#${(<HeadingCache>item).heading}`;
} else if (Object.prototype.hasOwnProperty.call(item, "id")) {
link = `${this.file.basename}^${(<BlockCache>item).id}`;
}
this.text.setValue(link);
this.onClose();
this.close();
}
renderSuggestion(
result: FuzzyMatch<TFile | BlockCache | HeadingCache>,
el: HTMLElement,
) {
const { item, match: matches } = result || {};
const content = el.createDiv({
cls: "suggestion-content",
});
if (!item) {
content.setText(this.emptyStateText);
content.parentElement.addClass("is-selected");
return;
}
if (item instanceof TFile) {
const pathLength = item.path.length - item.name.length;
const matchElements = matches.matches.map((m) => {
return createSpan("suggestion-highlight");
});
for (
let i = pathLength;
i < item.path.length - item.extension.length - 1;
i++
) {
const match = matches.matches.find((m) => m[0] === i);
if (match) {
const element = matchElements[matches.matches.indexOf(match)];
content.appendChild(element);
element.appendText(item.path.substring(match[0], match[1]));
i += match[1] - match[0] - 1;
continue;
}
content.appendText(item.path[i]);
}
el.createDiv({
cls: "suggestion-note",
text: item.path,
});
} else if (Object.prototype.hasOwnProperty.call(item, "heading")) {
content.setText((<HeadingCache>item).heading);
content.prepend(
createSpan({
cls: "suggestion-flair",
text: `H${(<HeadingCache>item).level}`,
}),
);
} else if (Object.prototype.hasOwnProperty.call(item, "id")) {
content.setText((<BlockCache>item).id);
}
}
get headings() {
if (!this.file) {
return [];
}
if (!this.cache) {
this.cache = this.app.metadataCache.getFileCache(this.file);
}
return this.cache.headings || [];
}
get blocks() {
if (!this.file) {
return [];
}
if (!this.cache) {
this.cache = this.app.metadataCache.getFileCache(this.file);
}
return Object.values(this.cache.blocks || {}) || [];
}
getItems() {
const v = this.inputEl.value;
if (/#/.test(v)) {
this.modifyInput = (i) => i.split(/#/).pop();
return this.headings;
} else if (/\^/.test(v)) {
this.modifyInput = (i) => i.split(/\^/).pop();
return this.blocks;
}
return this.files;
}
}
export class FolderSuggestionModal extends SuggestionModal<TFolder> {
text: TextComponent;
cache: CachedMetadata;
folders: TFolder[];
folder: TFolder;
constructor(app: App, input: TextComponent, items: TFolder[]) {
super(app, input.inputEl, items);
this.folders = [...items];
this.text = input;
this.inputEl.addEventListener("input", () => this.getFolder());
}
getFolder() {
const v = this.inputEl.value;
const folder = this.app.vault.getAbstractFileByPath(v);
if (folder == this.folder) {
return;
}
if (!(folder instanceof TFolder)) {
return;
}
this.folder = folder;
this.onInputChanged();
}
getItemText(item: TFolder) {
return item.path;
}
onChooseItem(item: TFolder) {
this.text.setValue(item.path);
this.folder = item;
}
selectSuggestion({ item }: FuzzyMatch<TFolder>) {
const link = item.path;
this.text.setValue(link);
this.onClose();
this.close();
}
renderSuggestion(result: FuzzyMatch<TFolder>, el: HTMLElement) {
const { item, match: matches } = result || {};
const content = el.createDiv({
cls: "suggestion-content",
});
if (!item) {
content.setText(this.emptyStateText);
content.parentElement.addClass("is-selected");
return;
}
const pathLength = item.path.length - item.name.length;
const matchElements = matches.matches.map((m) => {
return createSpan("suggestion-highlight");
});
for (let i = pathLength; i < item.path.length; i++) {
const match = matches.matches.find((m) => m[0] === i);
if (match) {
const element = matchElements[matches.matches.indexOf(match)];
content.appendChild(element);
element.appendText(item.path.substring(match[0], match[1]));
i += match[1] - match[0] - 1;
continue;
}
content.appendText(item.path[i]);
}
el.createDiv({
cls: "suggestion-note",
text: item.path,
});
}
getItems() {
return this.folders;
}
}
export class FileSuggestionModal extends SuggestionModal<TFile> {
text: TextComponent;
cache: CachedMetadata;
files: TFile[];
file: TFile;
constructor(app: App, input: TextComponent, items: TFile[]) {
super(app, input.inputEl, items);
this.limit = 20;
this.files = [...items];
this.text = input;
this.inputEl.addEventListener("input", () => this.getFile());
}
getFile() {
const v = this.inputEl.value;
const file = this.app.vault.getAbstractFileByPath(v);
if (file === this.file) {
return;
}
if (!(file instanceof TFile)) {
return;
}
this.file = file;
this.onInputChanged();
}
getSelectedItem() {
return this.file;
}
getItemText(item: TFile) {
return item.path;
}
onChooseItem(item: TFile) {
this.file = item;
this.text.setValue(item.path);
this.text.onChanged();
}
selectSuggestion({ item }: FuzzyMatch<TFile>) {
this.file = item;
this.text.setValue(item.path);
this.onClose();
this.text.onChanged();
this.close();
}
renderSuggestion(result: FuzzyMatch<TFile>, el: HTMLElement) {
const { item, match: matches } = result || {};
const content = el.createDiv({
cls: "suggestion-content",
});
if (!item) {
content.setText(this.emptyStateText);
content.parentElement.addClass("is-selected");
return;
}
const pathLength = item.path.length - item.name.length;
const matchElements = matches.matches.map((m) => {
return createSpan("suggestion-highlight");
});
for (let i = pathLength; i < item.path.length; i++) {
const match = matches.matches.find((m) => m[0] === i);
if (match) {
const element = matchElements[matches.matches.indexOf(match)];
content.appendChild(element);
element.appendText(item.path.substring(match[0], match[1]));
i += match[1] - match[0] - 1;
continue;
}
content.appendText(item.path[i]);
}
el.createDiv({
cls: "suggestion-note",
text: item.path,
});
}
getItems() {
return this.files;
}
}

View File

@@ -1,10 +1,12 @@
import { App, FuzzySuggestModal, TFile } from "obsidian";
import { REG_LINKINDEX_INVALIDCHARS } from "../constants/constants";
import { FuzzyMatch, FuzzySuggestModal, setIcon } from "obsidian";
import { AUDIO_TYPES, CODE_TYPES, ICON_NAME, IMAGE_TYPES, REG_LINKINDEX_INVALIDCHARS, VIDEO_TYPES } from "../constants/constants";
import { t } from "../lang/helpers";
import ExcalidrawPlugin from "src/main";
import { getLink } from "src/utils/FileUtils";
import { LinkSuggestion } from "src/types/types";
export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
export class InsertLinkDialog extends FuzzySuggestModal<LinkSuggestion> {
private addText: Function;
private drawingPath: string;
@@ -28,7 +30,7 @@ export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
this.emptyStateText = t("NO_MATCH");
}
getItems(): any[] {
getItems(): LinkSuggestion[] {
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/422
return (
this.app.metadataCache
@@ -39,11 +41,11 @@ export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
);
}
getItemText(item: any): string {
getItemText(item: LinkSuggestion): string {
return item.path + (item.alias ? `|${item.alias}` : "");
}
onChooseItem(item: any): void {
onChooseItem(item: LinkSuggestion): void {
let filepath = item.path;
if (item.file) {
filepath = this.app.metadataCache.fileToLinktext(
@@ -56,6 +58,65 @@ export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
this.addText(getLink(this.plugin,{embed: false, path: filepath, alias: item.alias}), filepath, item.alias);
}
renderSuggestion(result: FuzzyMatch<LinkSuggestion>, itemEl: HTMLElement) {
const { item, match: matches } = result || {};
itemEl.addClass("mod-complex");
const contentEl = itemEl.createDiv("suggestion-content");
const auxEl = itemEl.createDiv("suggestion-aux");
const titleEl = contentEl.createDiv("suggestion-title");
const noteEl = contentEl.createDiv("suggestion-note");
if (!item) {
titleEl.setText(this.emptyStateText);
itemEl.addClass("is-selected");
return;
}
const path = item.file?.path ?? item.path;
const pathLength = path.length - (item.file?.name.length ?? 0);
const matchElements = matches.matches.map((m) => {
return createSpan("suggestion-highlight");
});
const itemText = this.getItemText(item);
for (let i = pathLength; i < itemText.length; i++) {
const match = matches.matches.find((m) => m[0] === i);
if (match) {
const element = matchElements[matches.matches.indexOf(match)];
titleEl.appendChild(element);
element.appendText(itemText.substring(match[0], match[1]));
i += match[1] - match[0] - 1;
continue;
}
titleEl.appendText(itemText[i]);
}
noteEl.setText(path);
if(!item.file) {
setIcon(auxEl, "ghost");
} else if(this.plugin.isExcalidrawFile(item.file)) {
setIcon(auxEl, ICON_NAME);
} else if (item.file.extension === "md") {
setIcon(auxEl, "square-pen");
} else if (IMAGE_TYPES.includes(item.file.extension)) {
setIcon(auxEl, "image");
} else if (VIDEO_TYPES.includes(item.file.extension)) {
setIcon(auxEl, "monitor-play");
} else if (AUDIO_TYPES.includes(item.file.extension)) {
setIcon(auxEl, "file-audio");
} else if (CODE_TYPES.includes(item.file.extension)) {
setIcon(auxEl, "file-code");
} else if (item.file.extension === "canvas") {
setIcon(auxEl, "layout-dashboard");
} else if (item.file.extension === "pdf") {
setIcon(auxEl, "book-open-text");
} else {
auxEl.setText(item.file.extension);
}
}
onClose(): void {
window.setTimeout(()=>{
this.addText = null
@@ -63,9 +124,19 @@ export class InsertLinkDialog extends FuzzySuggestModal<TFile> {
super.onClose();
}
public start(drawingPath: string, addText: Function) {
private inLink: string;
onOpen(): void {
super.onOpen();
if(this.inLink) {
this.inputEl.value = this.inLink;
this.inputEl.dispatchEvent(new Event('input'));
}
}
public start(drawingPath: string, addText: Function, link?: string) {
this.addText = addText;
this.drawingPath = drawingPath;
this.inLink = link;
this.open();
}
}

View File

@@ -3,7 +3,7 @@ import ExcalidrawView from "../ExcalidrawView";
import ExcalidrawPlugin from "../main";
import { getPDFDoc } from "src/utils/FileUtils";
import { Modal, Setting, TextComponent } from "obsidian";
import { FileSuggestionModal } from "./FolderSuggester";
import { FileSuggestionModal } from "../Components/Suggesters/FileSuggestionModal";
import { getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
@@ -206,7 +206,11 @@ export class InsertPDFModal extends Modal {
const search = new TextComponent(ce);
search.inputEl.style.width = "100%";
const suggester = new FileSuggestionModal(this.app, search,app.vault.getFiles().filter((f: TFile) => f.extension.toLowerCase() === "pdf"));
const suggester = new FileSuggestionModal(
this.app,
search,this.app.vault.getFiles().filter((f: TFile) => f.extension.toLowerCase() === "pdf"),
this.plugin
);
search.onChange(async () => {
const file = suggester.getSelectedItem();
await setFile(file);

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ import { ButtonComponent, DropdownComponent, TFile, ToggleComponent } from "obsi
import ExcalidrawView from "../ExcalidrawView";
import ExcalidrawPlugin from "../main";
import { Modal, Setting, TextComponent } from "obsidian";
import { FileSuggestionModal } from "./FolderSuggester";
import { FileSuggestionModal } from "../Components/Suggesters/FileSuggestionModal";
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";
@@ -146,7 +146,9 @@ export class UniversalInsertFileModal extends Modal {
const suggester = new FileSuggestionModal(
this.app,
search,
this.app.vault.getFiles().filter((f: TFile) => sections?.length > 0 || f!==this.view.file));
this.app.vault.getFiles().filter((f: TFile) => sections?.length > 0 || f!==this.view.file),
this.plugin
);
search.onChange(() => {
file = suggester.getSelectedItem();
updateForm();

View File

@@ -1,3 +1,4 @@
import { FILE } from "dns";
import {
DEVICE,
FRONTMATTER_KEYS,
@@ -10,6 +11,8 @@ declare const PLUGIN_VERSION:string;
// English
export default {
// Sugester
SELECT_FILE_TO_INSERT: "Select a file to insert",
// main.ts
CONVERT_URL_TO_FILE: "Save image from URL to local file",
UNZIP_CURRENT_FILE: "Decompress current Excalidraw file",
@@ -907,6 +910,8 @@ FILENAME_HEAD: "Filename",
ES_YOUTUBE_START_INVALID: "The YouTube Start Time is invalid. Please check the format and try again",
ES_FILENAME_VISIBLE: "Filename Visible",
ES_BACKGROUND_HEAD: "Embedded note background color",
ES_BACKGROUND_DESC_INFO: "Click here for more info on colors",
ES_BACKGROUND_DESC_DETAIL: "Background color affects only the preview mode of the markdown embeddable. When editing, it follows the Obsidian light/dark theme as set for the scene (via document property) or in plugin settings. The background color has two layers: the element background color (lower layer) and a color on top (upper layer). Selecting 'Match Element Background' means both layers follow the element color. Selecting 'Match Canvas' or a specific background color keeps the element background layer. Setting opacity (e.g., 50%) mixes the canvas or selected color with the element background color. To remove the element background layer, set the element color to transparent in Excalidraw's element properties editor. This makes only the upper layer effective.",
ES_BACKGROUND_MATCH_ELEMENT: "Match Element Background Color",
ES_BACKGROUND_MATCH_CANVAS: "Match Canvas Background Color",
ES_BACKGROUND_COLOR: "Background Color",

View File

@@ -1,3 +1,4 @@
import { FILE } from "dns";
import {
DEVICE,
FRONTMATTER_KEYS,
@@ -10,6 +11,8 @@ declare const PLUGIN_VERSION:string;
// 简体中文
export default {
// Sugester
SELECT_FILE_TO_INSERT: "选择一个要插入的文件",
// main.ts
CONVERT_URL_TO_FILE: "从 URL 下载图像到本地",
UNZIP_CURRENT_FILE: "解压当前 Excalidraw 文件",

View File

@@ -19,7 +19,6 @@ import {
Workspace,
Editor,
MarkdownFileInfo,
loadMermaid,
} from "obsidian";
import {
BLANK_DRAWING,
@@ -113,7 +112,7 @@ import {
legacyExcalidrawPopoverObserver,
} from "./MarkdownPostProcessor";
import { FieldSuggester } from "./dialogs/FieldSuggester";
import { FieldSuggester } from "./Components/Suggesters/FieldSuggester";
import { ReleaseNotes } from "./dialogs/ReleaseNotes";
import { Packages } from "./types/types";
import { PreviewImageType } from "./utils/UtilTypes";
@@ -135,7 +134,6 @@ import { CustomMutationObserver, debug, log, DEBUGGING, setDebugging } from "./u
import { carveOutImage, carveOutPDF, createImageCropperFile } from "./utils/CarveOut";
import { ExcalidrawConfig } from "./utils/ExcalidrawConfig";
import { EditorHandler } from "./CodeMirrorExtension/EditorHandler";
import { clearMathJaxVariables } from "./LaTeX";
import { showFrameSettings } from "./dialogs/FrameSettings";
import { ExcalidrawLib } from "./ExcalidrawLib";
import { Rank, SwordColors } from "./menu/ActionIcons";
@@ -145,9 +143,10 @@ import { WeakArray } from "./utils/WeakArray";
import { getCJKDataURLs } from "./utils/CJKLoader";
import { ExcalidrawLoading, switchToExcalidraw } from "./dialogs/ExcalidrawLoading";
import { insertImageToView } from "./utils/ExcalidrawViewUtils";
import { clearMathJaxVariables } from "./LaTeX";
declare let EXCALIDRAW_PACKAGE:string;
declare let REACT_PACKAGES:string;
//declare let EXCALIDRAW_PACKAGE:string;
declare const unpackExcalidraw: Function;
declare let react:any;
declare let reactDOM:any;
@@ -156,6 +155,7 @@ declare const PLUGIN_VERSION:string;
declare const INITIAL_TIMESTAMP: number;
export default class ExcalidrawPlugin extends Plugin {
private EXCALIDRAW_PACKAGE: string;
public eaInstances = new WeakArray<ExcalidrawAutomate>();
public fourthFontLoaded: boolean = false;
public excalidrawConfig: ExcalidrawConfig;
@@ -291,7 +291,7 @@ export default class ExcalidrawPlugin extends Plugin {
//@ts-ignore
const {react:r, reactDOM:rd, excalidrawLib:e} = win.eval.call(win,
`(function() {
${REACT_PACKAGES + EXCALIDRAW_PACKAGE};
${REACT_PACKAGES + this.EXCALIDRAW_PACKAGE};
return {react:React,reactDOM:ReactDOM,excalidrawLib:ExcalidrawLib};
})()`);
this.packageMap.set(win,{react:r, reactDOM:rd, excalidrawLib:e});
@@ -386,23 +386,8 @@ export default class ExcalidrawPlugin extends Plugin {
this.addRibbonIcon(ICON_NAME, t("CREATE_NEW"), this.actionRibbonClick.bind(this));
try {
this.loadSettings({reEnableAutosave:true}).then(async () => {
const updateSettings = !this.settings.onceOffCompressFlagReset || !this.settings.onceOffGPTVersionReset;
if(!this.settings.onceOffCompressFlagReset) {
this.settings.compress = true;
this.settings.onceOffCompressFlagReset = true;
}
if(!this.settings.onceOffGPTVersionReset) {
if(this.settings.openAIDefaultVisionModel === "gpt-4-vision-preview") {
this.settings.openAIDefaultVisionModel = "gpt-4o";
}
}
if(updateSettings) {
await this.saveSettings();
}
this.addSettingTab(new ExcalidrawSettingTab(this.app, this));
this.settingsReady = true;
});
this.loadSettings({reEnableAutosave:true})
.then(this.onloadCheckForOnceOffSettingsUpdates.bind(this));
} catch (e) {
new Notice("Error loading plugin settings", 6000);
console.error("Error loading plugin settings", e);
@@ -427,205 +412,218 @@ export default class ExcalidrawPlugin extends Plugin {
}
this.logStartupEvent("Markdown post processor added");
this.app.workspace.onLayoutReady(async () => {
this.loadTimestamp = Date.now();
this.lastLogTimestamp = this.loadTimestamp;
this.logStartupEvent("\n----------------------------------\nWorkspace onLayoutReady event fired (these actions are outside the plugin initialization)");
await this.awaitSettings();
this.logStartupEvent("Settings awaited");
try {
unpackExcalidraw();
excalidrawLib = window.eval.call(window,`(function() {${EXCALIDRAW_PACKAGE};return ExcalidrawLib;})()`);
this.packageMap.set(window,{react, reactDOM, excalidrawLib});
updateExcalidrawLib();
} catch (e) {
new Notice("Error loading the Excalidraw package", 6000);
console.error("Error loading the Excalidraw package", e);
}
this.logStartupEvent("Excalidraw package unpacked");
try {
initCompressionWorker();
} catch (e) {
new Notice("Error initializing compression worker", 6000);
console.error("Error initializing compression worker", e);
}
this.logStartupEvent("Compression worker initialized");
try {
this.excalidrawConfig = new ExcalidrawConfig(this);
} catch (e) {
new Notice("Error initializing Excalidraw config", 6000);
console.error("Error initializing Excalidraw config", e);
}
this.logStartupEvent("Excalidraw config initialized");
try {
await loadMermaid();
} catch (e) {
new Notice("Error loading Mermaid", 6000);
console.error("Error loading Mermaid", e);
}
this.logStartupEvent("Mermaid loaded");
try {
this.addThemeObserver();
} catch (e) {
new Notice("Error adding theme observer", 6000);
console.error("Error adding theme observer", e);
}
this.logStartupEvent("Theme observer added");
try {
//inspiration taken from kanban:
//https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/main.ts#L267
this.registerMonkeyPatches();
} catch (e) {
new Notice("Error registering monkey patches", 6000);
console.error("Error registering monkey patches", e);
}
this.logStartupEvent("Monkey patches registered");
try {
this.stylesManager = new StylesManager(this);
} catch (e) {
new Notice("Error initializing styles manager", 6000);
console.error("Error initializing styles manager", e);
}
this.logStartupEvent("Styles manager initialized");
try {
this.scriptEngine = new ScriptEngine(this);
} catch (e) {
new Notice("Error initializing script engine", 6000);
console.error("Error initializing script engine", e);
}
this.logStartupEvent("Script engine initialized");
try {
await this.initializeFonts();
} catch (e) {
new Notice("Error initializing fonts", 6000);
console.error("Error initializing fonts", e);
}
this.logStartupEvent("Fonts initialized");
try {
imageCache.initializeDB(this);
} catch (e) {
new Notice("Error initializing image cache", 6000);
console.error("Error initializing image cache", e);
}
this.logStartupEvent("Image cache initialized");
try {
this.isReady = true;
switchToExcalidraw(this.app);
this.switchToExcalidarwAfterLoad();
} catch (e) {
new Notice("Error switching views to Excalidraw", 6000);
console.error("Error switching views to Excalidraw", e);
}
this.logStartupEvent("Switched to Excalidraw views");
try {
if (this.settings.showReleaseNotes) {
//I am repurposing imageElementNotice, if the value is true, this means the plugin was just newly installed to Obsidian.
const obsidianJustInstalled = (this.settings.previousRelease === "0.0.0") || !this.settings.previousRelease;
if (isVersionNewerThanOther(PLUGIN_VERSION, this.settings.previousRelease ?? "0.0.0")) {
new ReleaseNotes(
this.app,
this,
obsidianJustInstalled ? null : PLUGIN_VERSION,
).open();
}
}
} catch (e) {
new Notice("Error opening release notes", 6000);
console.error("Error opening release notes", e);
}
this.logStartupEvent("Release notes opened");
//---------------------------------------------------------------------
//initialization that can happen after Excalidraw views are initialized
//---------------------------------------------------------------------
try {
this.registerEventListeners();
} catch (e) {
new Notice("Error registering event listeners", 6000);
console.error("Error registering event listeners", e);
}
this.logStartupEvent("Event listeners registered");
try {
this.runStartupScript();
} catch (e) {
new Notice("Error running startup script", 6000);
console.error("Error running startup script", e);
}
this.logStartupEvent("Startup script run");
try {
this.editorHandler = new EditorHandler(this);
this.editorHandler.setup();
} catch (e) {
new Notice("Error setting up editor handler", 6000);
console.error("Error setting up editor handler", e);
}
this.logStartupEvent("Editor handler initialized");
try {
this.registerInstallCodeblockProcessor();
} catch (e) {
new Notice("Error registering script install-codeblock processor", 6000);
console.error("Error registering script install-codeblock processor", e);
}
this.logStartupEvent("Script install-codeblock processor registered");
try {
this.experimentalFileTypeDisplayToggle(this.settings.experimentalFileType);
} catch (e) {
new Notice("Error setting up experimental file type display", 6000);
console.error("Error setting up experimental file type display", e);
}
this.logStartupEvent("Experimental file type display set");
try {
this.registerCommands();
} catch (e) {
new Notice("Error registering commands", 6000);
console.error("Error registering commands", e);
}
this.logStartupEvent("Commands registered");
try {
this.registerEditorSuggest(new FieldSuggester(this));
} catch (e) {
new Notice("Error registering editor suggester", 6000);
console.error("Error registering editor suggester", e);
}
this.logStartupEvent("Editor suggester registered");
try {
this.setPropertyTypes();
} catch (e) {
new Notice("Error setting up property types", 6000);
console.error("Error setting up property types", e);
}
this.logStartupEvent("Property types set");
try {
this.taskbone = new Taskbone(this);
} catch (e) {
new Notice("Error setting up taskbone", 6000);
console.error("Error setting up taskbone", e);
}
this.logStartupEvent("Taskbone set up");
});
this.app.workspace.onLayoutReady(this.onloadOnLayoutReady.bind(this));
this.logStartupEvent("Workspace ready event handler added");
}
private async onloadCheckForOnceOffSettingsUpdates() {
const updateSettings = !this.settings.onceOffCompressFlagReset || !this.settings.onceOffGPTVersionReset;
if(!this.settings.onceOffCompressFlagReset) {
this.settings.compress = true;
this.settings.onceOffCompressFlagReset = true;
}
if(!this.settings.onceOffGPTVersionReset) {
this.settings.onceOffGPTVersionReset = true;
if(this.settings.openAIDefaultVisionModel === "gpt-4-vision-preview") {
this.settings.openAIDefaultVisionModel = "gpt-4o";
}
}
if(updateSettings) {
await this.saveSettings();
}
this.addSettingTab(new ExcalidrawSettingTab(this.app, this));
this.settingsReady = true;
}
private async onloadOnLayoutReady() {
this.loadTimestamp = Date.now();
this.lastLogTimestamp = this.loadTimestamp;
this.logStartupEvent("\n----------------------------------\nWorkspace onLayoutReady event fired (these actions are outside the plugin initialization)");
await this.awaitSettings();
this.logStartupEvent("Settings awaited");
try {
this.EXCALIDRAW_PACKAGE = unpackExcalidraw();
excalidrawLib = window.eval.call(window,`(function() {${this.EXCALIDRAW_PACKAGE};return ExcalidrawLib;})()`);
this.packageMap.set(window,{react, reactDOM, excalidrawLib});
updateExcalidrawLib();
} catch (e) {
new Notice("Error loading the Excalidraw package", 6000);
console.error("Error loading the Excalidraw package", e);
}
this.logStartupEvent("Excalidraw package unpacked");
try {
initCompressionWorker();
} catch (e) {
new Notice("Error initializing compression worker", 6000);
console.error("Error initializing compression worker", e);
}
this.logStartupEvent("Compression worker initialized");
try {
this.excalidrawConfig = new ExcalidrawConfig(this);
} catch (e) {
new Notice("Error initializing Excalidraw config", 6000);
console.error("Error initializing Excalidraw config", e);
}
this.logStartupEvent("Excalidraw config initialized");
try {
this.addThemeObserver();
} catch (e) {
new Notice("Error adding theme observer", 6000);
console.error("Error adding theme observer", e);
}
this.logStartupEvent("Theme observer added");
try {
//inspiration taken from kanban:
//https://github.com/mgmeyers/obsidian-kanban/blob/44118e25661bff9ebfe54f71ae33805dc88ffa53/src/main.ts#L267
this.registerMonkeyPatches();
} catch (e) {
new Notice("Error registering monkey patches", 6000);
console.error("Error registering monkey patches", e);
}
this.logStartupEvent("Monkey patches registered");
try {
this.stylesManager = new StylesManager(this);
} catch (e) {
new Notice("Error initializing styles manager", 6000);
console.error("Error initializing styles manager", e);
}
this.logStartupEvent("Styles manager initialized");
try {
this.scriptEngine = new ScriptEngine(this);
} catch (e) {
new Notice("Error initializing script engine", 6000);
console.error("Error initializing script engine", e);
}
this.logStartupEvent("Script engine initialized");
try {
await this.initializeFonts();
} catch (e) {
new Notice("Error initializing fonts", 6000);
console.error("Error initializing fonts", e);
}
this.logStartupEvent("Fonts initialized");
try {
imageCache.initializeDB(this);
} catch (e) {
new Notice("Error initializing image cache", 6000);
console.error("Error initializing image cache", e);
}
this.logStartupEvent("Image cache initialized");
try {
this.isReady = true;
switchToExcalidraw(this.app);
this.switchToExcalidarwAfterLoad();
} catch (e) {
new Notice("Error switching views to Excalidraw", 6000);
console.error("Error switching views to Excalidraw", e);
}
this.logStartupEvent("Switched to Excalidraw views");
try {
if (this.settings.showReleaseNotes) {
//I am repurposing imageElementNotice, if the value is true, this means the plugin was just newly installed to Obsidian.
const obsidianJustInstalled = (this.settings.previousRelease === "0.0.0") || !this.settings.previousRelease;
if (isVersionNewerThanOther(PLUGIN_VERSION, this.settings.previousRelease ?? "0.0.0")) {
new ReleaseNotes(
this.app,
this,
obsidianJustInstalled ? null : PLUGIN_VERSION,
).open();
}
}
} catch (e) {
new Notice("Error opening release notes", 6000);
console.error("Error opening release notes", e);
}
this.logStartupEvent("Release notes opened");
//---------------------------------------------------------------------
//initialization that can happen after Excalidraw views are initialized
//---------------------------------------------------------------------
try {
this.registerEventListeners();
} catch (e) {
new Notice("Error registering event listeners", 6000);
console.error("Error registering event listeners", e);
}
this.logStartupEvent("Event listeners registered");
try {
this.runStartupScript();
} catch (e) {
new Notice("Error running startup script", 6000);
console.error("Error running startup script", e);
}
this.logStartupEvent("Startup script run");
try {
this.editorHandler = new EditorHandler(this);
this.editorHandler.setup();
} catch (e) {
new Notice("Error setting up editor handler", 6000);
console.error("Error setting up editor handler", e);
}
this.logStartupEvent("Editor handler initialized");
try {
this.registerInstallCodeblockProcessor();
} catch (e) {
new Notice("Error registering script install-codeblock processor", 6000);
console.error("Error registering script install-codeblock processor", e);
}
this.logStartupEvent("Script install-codeblock processor registered");
try {
this.experimentalFileTypeDisplayToggle(this.settings.experimentalFileType);
} catch (e) {
new Notice("Error setting up experimental file type display", 6000);
console.error("Error setting up experimental file type display", e);
}
this.logStartupEvent("Experimental file type display set");
try {
this.registerCommands();
} catch (e) {
new Notice("Error registering commands", 6000);
console.error("Error registering commands", e);
}
this.logStartupEvent("Commands registered");
try {
this.registerEditorSuggest(new FieldSuggester(this));
} catch (e) {
new Notice("Error registering editor suggester", 6000);
console.error("Error registering editor suggester", e);
}
this.logStartupEvent("Editor suggester registered");
try {
this.setPropertyTypes();
} catch (e) {
new Notice("Error setting up property types", 6000);
console.error("Error setting up property types", e);
}
this.logStartupEvent("Property types set");
try {
this.taskbone = new Taskbone(this);
} catch (e) {
new Notice("Error setting up taskbone", 6000);
console.error("Error setting up taskbone", e);
}
this.logStartupEvent("Taskbone set up");
}
public async awaitSettings() {
let counter = 0;
while(!this.settingsReady && counter < 150) {
@@ -3649,7 +3647,7 @@ export default class ExcalidrawPlugin extends Plugin {
this.settings = null;
clearMathJaxVariables();
EXCALIDRAW_PACKAGE = "";
this.EXCALIDRAW_PACKAGE = "";
REACT_PACKAGES = "";
//pluginPackages = null;
//PLUGIN_VERSION = null;

View File

@@ -127,7 +127,7 @@ export class EmbeddableMenu {
blockID = nanoid();
const fileContents = await app.vault.cachedRead(file);
if(!fileContents) return;
await app.vault.modify(file, fileContents.slice(0, offset) + ` ^${blockID}` + fileContents.slice(offset));
await this.view.app.vault.modify(file, fileContents.slice(0, offset) + ` ^${blockID}` + fileContents.slice(offset));
await sleep(200); //wait for cache to update
}
this.updateElement(`#^${blockID}`, element, file);
@@ -170,7 +170,6 @@ export class EmbeddableMenu {
renderButtons(appState: AppState) {
const view = this.view;
const app = view.app;
const api = view?.excalidrawAPI as ExcalidrawImperativeAPI;
if(!api) return null;
if(!view.file) return null;

View File

@@ -1,3 +1,4 @@
import { TFile } from "obsidian";
import { ExcalidrawAutomate } from "../ExcalidrawAutomate";
import { ExcalidrawLib } from "../ExcalidrawLib";
@@ -33,6 +34,12 @@ export type DeviceType = {
export type Point = [number, number];
export type LinkSuggestion = {
file: TFile;
path: string;
alias?: string;
}
declare global {
interface Window {
ExcalidrawAutomate: ExcalidrawAutomate;

View File

@@ -82,51 +82,74 @@ export class CanvasNodeFactory {
return node;
}
public async startEditing(node: ObsidianCanvasNode, theme: string) {
if (!this.initialized || !node) return;
if (node.file === this.view.file) {
await this.view.setEmbeddableIsEditingSelf();
private async waitForEditor(node: ObsidianCanvasNode): Promise<HTMLElement | null> {
let counter = 0;
while (!node.child.editor?.containerEl?.parentElement?.parentElement && counter++ < 100) {
await new Promise(resolve => setTimeout(resolve, 25));
}
node.startEditing();
const obsidianTheme = isObsidianThemeDark() ? "theme-dark" : "theme-light";
if (obsidianTheme === theme) return;
(async () => {
let counter = 0;
while (!node.child.editor?.containerEl?.parentElement?.parentElement && counter++ < 100) {
await sleep(25);
}
if (!node.child.editor?.containerEl?.parentElement?.parentElement) return;
node.child.editor.containerEl.parentElement.parentElement.classList.remove(obsidianTheme);
node.child.editor.containerEl.parentElement.parentElement.classList.add(theme);
const nodeObserverFn: MutationCallback = (mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const targetElement = mutation.target as HTMLElement;
if (targetElement.classList.contains(obsidianTheme)) {
targetElement.classList.remove(obsidianTheme);
targetElement.classList.add(theme);
}
return node.child.editor?.containerEl?.parentElement?.parentElement;
}
private setupThemeObserver(editorEl: HTMLElement, obsidianTheme: string, theme: string) {
const nodeObserverFn: MutationCallback = (mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const targetElement = mutation.target as HTMLElement;
if (targetElement.classList.contains(obsidianTheme)) {
targetElement.classList.remove(obsidianTheme);
targetElement.classList.add(theme);
}
}
};
this.observer = DEBUGGING
? new CustomMutationObserver(nodeObserverFn, "CanvasNodeFactory")
: new MutationObserver(nodeObserverFn);
this.observer.observe(node.child.editor.containerEl.parentElement.parentElement, { attributes: true });
})();
}
}
};
this.observer?.disconnect();
this.observer = DEBUGGING
? new CustomMutationObserver(nodeObserverFn, "CanvasNodeFactory")
: new MutationObserver(nodeObserverFn);
this.observer.observe(editorEl, { attributes: true });
}
public async startEditing(node: ObsidianCanvasNode, theme: string) {
if (!this.initialized || !node) return;
try {
if (node.file === this.view.file) {
await this.view.setEmbeddableIsEditingSelf();
}
node.startEditing();
node.isEditing = true;
const obsidianTheme = isObsidianThemeDark() ? "theme-dark" : "theme-light";
if (obsidianTheme === theme) return;
const editorEl = await this.waitForEditor(node);
if (!editorEl) return;
editorEl.classList.remove(obsidianTheme);
editorEl.classList.add(theme);
this.setupThemeObserver(editorEl, obsidianTheme, theme);
} catch (error) {
console.error('Error starting edit:', error);
node.isEditing = false;
}
}
public stopEditing(node: ObsidianCanvasNode) {
if(!this.initialized || !node) return;
if(!node.child.editMode) return;
if(node.file === this.view.file) {
this.view.clearEmbeddableIsEditingSelf();
if (!this.initialized || !node || !node.isEditing) return;
try {
if (node.file === this.view.file) {
this.view.clearEmbeddableIsEditingSelf();
}
node.child.showPreview();
node.isEditing = false;
this.observer?.disconnect();
} catch (error) {
console.error('Error stopping edit:', error);
}
node.child.showPreview();
}
removeNode(node: ObsidianCanvasNode) {

View File

@@ -651,3 +651,17 @@ textarea.excalidraw-wysiwyg, .excalidraw input {
.excalidraw .ToolIcon_type_button {
color: var(--text-primary-color);
}
.excalidraw-setting-desc {
padding: 10px;
cursor: pointer;
background-color: var(--background-secondary);
border: 1px solid var(--background-modifier-border);
border-radius: 5px;
transition: background-color 0.3s ease, color 0.3s ease;
}
.excalidraw-setting-desc:hover {
background-color: var(--background-modifier-hover);
color: var(--text-accent);
}